Client: macOS support

This commit is contained in:
sarah committed 2026-10-14 13:18:19 +02:00
1 parent 5c04a4a77d
commit d86e33fd4a
4 files changed
+80 -9

No files matched your search

+58 -6
View File
@@ -45,14 +45,19 @@ BundledFontFamily = "Fixedsys Excelsior"
ActiveFontFamily = BundledFontFamily # set by ApplyAppearance, read by BuildTheme ActiveFontFamily = BundledFontFamily # set by ApplyAppearance, read by BuildTheme
IsWindows = sys.platform == "win32" IsWindows = sys.platform == "win32"
IsMac = sys.platform == "darwin"
# Each system's own per-user folders, like SaraSuite's: ~/.config, ~/.cache # Each system's own per-user folders, like SaraSuite's: ~/.config, ~/.cache
# and ~/.local/share on Linux; %APPDATA% and %LOCALAPPDATA% on Windows. # and ~/.local/share on Linux; %APPDATA% and %LOCALAPPDATA% on Windows;
# ~/Library/Application Support and ~/Library/Caches on macOS.
Home = Path.home() Home = Path.home()
if IsWindows: if IsWindows:
ConfigHome = Path(os.environ.get("APPDATA") or Home / "AppData" / "Roaming") ConfigHome = Path(os.environ.get("APPDATA") or Home / "AppData" / "Roaming")
DataHome = Path(os.environ.get("LOCALAPPDATA") or Home / "AppData" / "Local") DataHome = Path(os.environ.get("LOCALAPPDATA") or Home / "AppData" / "Local")
CacheHome = DataHome CacheHome = DataHome
elif IsMac:
ConfigHome = DataHome = Home / "Library" / "Application Support"
CacheHome = Home / "Library" / "Caches"
else: else:
ConfigHome = Path(os.environ.get("XDG_CONFIG_HOME") or Home / ".config") ConfigHome = Path(os.environ.get("XDG_CONFIG_HOME") or Home / ".config")
DataHome = Path(os.environ.get("XDG_DATA_HOME") or Home / ".local" / "share") DataHome = Path(os.environ.get("XDG_DATA_HOME") or Home / ".local" / "share")
@@ -337,7 +342,7 @@ def OpenExternally(FilePath: Path) -> None:
if IsWindows: if IsWindows:
os.startfile(str(FilePath)) os.startfile(str(FilePath))
return return
subprocess.Popen(["xdg-open", str(FilePath)], env=ChildEnvironment(), start_new_session=True) subprocess.Popen(["open" if IsMac else "xdg-open", str(FilePath)], env=ChildEnvironment(), start_new_session=True)
def _WindowsFonts() -> set[str]: def _WindowsFonts() -> set[str]:
@@ -367,11 +372,58 @@ def _WindowsFonts() -> set[str]:
return Families return Families
def _MacFonts() -> set[str]:
"""Font families on macOS (no fontconfig there), read from the font files'
name tables in the system, shared and personal Fonts folders. The
typographic family (name ID 16) where there is one, else ID 1."""
import struct
Families: set[str] = set()
for Folder in (Path("/System/Library/Fonts"), Path("/Library/Fonts"), Path.home() / "Library" / "Fonts"):
if not Folder.is_dir():
continue
for FilePath in Folder.rglob("*"):
if FilePath.suffix.lower() not in (".ttf", ".otf", ".ttc"):
continue
try:
Data = FilePath.read_bytes()
except OSError:
continue
Offsets = [0]
if Data[:4] == b"ttcf": # a collection: every font in it
Count = struct.unpack_from(">I", Data, 8)[0]
Offsets = list(struct.unpack_from(f">{Count}I", Data, 12))
for Offset in Offsets:
try:
for Index in range(struct.unpack_from(">H", Data, Offset + 4)[0]):
Tag, _Sum, Start, _Length = struct.unpack_from(">4sIII", Data, Offset + 12 + Index * 16)
if Tag != b"name":
continue
_Format, Count, Strings = struct.unpack_from(">HHH", Data, Start)
Found: dict[int, str] = {}
for Record in range(Count):
PlatformId, _Encoding, Language, NameId, Length, At = struct.unpack_from(
">6H", Data, Start + 6 + Record * 12)
if NameId not in (1, 16) or NameId in Found:
continue
Raw = Data[Start + Strings + At:Start + Strings + At + Length]
if PlatformId in (0, 3):
Found[NameId] = Raw.decode("utf-16-be", "replace")
elif PlatformId == 1 and Language == 0:
Found[NameId] = Raw.decode("mac-roman", "replace")
Name = (Found.get(16) or Found.get(1) or "").strip()
if Name and not Name.startswith("."): # ".SF NS" and co. are system-internal
Families.add(Name)
except struct.error:
continue
return Families
def InstalledFontFamilies() -> list[str]: def InstalledFontFamilies() -> list[str]:
"""Font families fontconfig (or, on Windows, the registry) knows about, """Font families fontconfig (or, on Windows, the registry; on macOS, the
with the bundled font first.""" font files themselves) knows about, with the bundled font first."""
if IsWindows: if IsWindows or IsMac:
Families = _WindowsFonts() Families = _WindowsFonts() if IsWindows else _MacFonts()
Families.discard(BundledFontFamily) Families.discard(BundledFontFamily)
return [BundledFontFamily] + sorted(Families, key=str.lower) return [BundledFontFamily] + sorted(Families, key=str.lower)
try: try:
+13 -1
View File
@@ -31,7 +31,7 @@ from typing import Optional
import httpx import httpx
from Core import AssetsDir, ChildEnvironment, DataHome, AppName, IsWindows from Core import AssetsDir, ChildEnvironment, DataHome, AppName, Home, IsMac, IsWindows
ToolsDir = DataHome / AppName / "DiagramTools" ToolsDir = DataHome / AppName / "DiagramTools"
PythonDir = ToolsDir / "Python" PythonDir = ToolsDir / "Python"
@@ -53,6 +53,12 @@ WindowsBrowsers = [
r"Google\Chrome\Application\chrome.exe", r"Google\Chrome\Application\chrome.exe",
r"BraveSoftware\Brave-Browser\Application\brave.exe", r"BraveSoftware\Brave-Browser\Application\brave.exe",
] ]
MacBrowsers = [ # in /Applications or ~/Applications
"Google Chrome.app/Contents/MacOS/Google Chrome",
"Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
"Brave Browser.app/Contents/MacOS/Brave Browser",
"Chromium.app/Contents/MacOS/Chromium",
]
class DiagramError(Exception): class DiagramError(Exception):
@@ -169,6 +175,12 @@ def _InstalledBrowser() -> Optional[str]:
if Base and (Path(Base) / Relative).is_file(): if Base and (Path(Base) / Relative).is_file():
return str(Path(Base) / Relative) return str(Path(Base) / Relative)
return None return None
if IsMac:
for Base in (Path("/Applications"), Home / "Applications"):
for Relative in MacBrowsers:
if (Base / Relative).is_file():
return str(Base / Relative)
return None
return next((Found for Name in BrowserNames if (Found := shutil.which(Name))), None) return next((Found for Name in BrowserNames if (Found := shutil.which(Name))), None)
+1 -1
View File
@@ -23,7 +23,7 @@ from Settings import ShowSettingsDialog
def _SetCursorThemeFallback() -> None: def _SetCursorThemeFallback() -> None:
"""GTK reads XCURSOR_THEME directly and doesn't always inherit it (shows as """GTK reads XCURSOR_THEME directly and doesn't always inherit it (shows as
'Unable to load ... from the cursor theme'). Same fix as SaraFem's.""" 'Unable to load ... from the cursor theme'). Same fix as SaraFem's."""
if os.environ.get("XCURSOR_THEME") or sys.platform == "win32": # GTK/GNOME only if os.environ.get("XCURSOR_THEME") or not sys.platform.startswith("linux"): # GTK/GNOME only
return return
try: try:
Result = subprocess.run(["gsettings", "get", "org.gnome.desktop.interface", "cursor-theme"], Result = subprocess.run(["gsettings", "get", "org.gnome.desktop.interface", "cursor-theme"],
+8 -1
View File
@@ -1,6 +1,6 @@
# Kept in sync between SaraSuite/Programs/SaraView/ and SaraView/Client/Packaging/SaraSuite/. # Kept in sync between SaraSuite/Programs/SaraView/ and SaraView/Client/Packaging/SaraSuite/.
Name = "SaraView" Name = "SaraView"
Platforms = ["Linux", "Windows"] Platforms = ["Linux", "Windows", "macOS"]
Description = "SaraView's desktop client: view and edit files in a local folder, or on a SaraView server." Description = "SaraView's desktop client: view and edit files in a local folder, or on a SaraView server."
[Source] [Source]
@@ -32,6 +32,13 @@ Name = "SaraView"
Script = "Client/Main.py" Script = "Client/Main.py"
Icon = "Client/assets/Icon.png" Icon = "Client/assets/Icon.png"
# macOS: an app bundle, ~/Applications/SaraView.app (python Client/Main.py, from the app folder).
[[Shortcuts]]
Platform = "macOS"
Name = "SaraView"
Script = "Client/Main.py"
Icon = "Client/assets/Icon.png"
# Made by SaraView itself: Config.toml and Session.toml (your saved login). # Made by SaraView itself: Config.toml and Session.toml (your saved login).
[[Leftovers]] [[Leftovers]]
Path = "{ConfigDir}" Path = "{ConfigDir}"