diff --git a/.saratag/Core.py b/.saratag/Core.py index 8d2229e..b0ebb7c 100755 --- a/.saratag/Core.py +++ b/.saratag/Core.py @@ -59,8 +59,8 @@ SaraTagRecordsPath = ApplicationDirectory / ".SaraTagRecords.toml" SaraTagHistoryPath = ApplicationDirectory / ".SaraTagHistory.toml" -# Empty means the default app; Windows rarely has mpv. -DefaultVideoPlayer = "" if sys.platform == "win32" else "mpv" +# Empty means the default app; Windows and macOS rarely have mpv. +DefaultVideoPlayer = "mpv" if sys.platform.startswith("linux") else "" DefaultSaraTagConf = f'''# SaraTag's main portable configuration file. # Paths in this file are interpreted relative to the SaraTag directory diff --git a/Core.py b/Core.py index 57551e0..5f0e621 100644 --- a/Core.py +++ b/Core.py @@ -24,13 +24,17 @@ BundledFontFamily = "Fixedsys Excelsior" ActiveFontFamily = BundledFontFamily # set by ApplyAppearance, read by BuildTheme IsWindows = sys.platform == "win32" +IsMac = sys.platform == "darwin" # Each system's own per-user folders, like SaraSuite's: ~/.config and -# ~/.local/share on Linux, %APPDATA% and %LOCALAPPDATA% on Windows. +# ~/.local/share on Linux, %APPDATA% and %LOCALAPPDATA% on Windows, +# ~/Library/Application Support (both) on macOS. Home = Path.home() if IsWindows: ConfigHome = Path(os.environ.get("APPDATA") or Home / "AppData" / "Roaming") DataHome = Path(os.environ.get("LOCALAPPDATA") or Home / "AppData" / "Local") +elif IsMac: + ConfigHome = DataHome = Home / "Library" / "Application Support" else: ConfigHome = Path(os.environ.get("XDG_CONFIG_HOME") or Home / ".config") DataHome = Path(os.environ.get("XDG_DATA_HOME") or Home / ".local" / "share") diff --git a/Packaging/SaraSuite/Manifest.toml b/Packaging/SaraSuite/Manifest.toml index 6b8fb54..77dc037 100644 --- a/Packaging/SaraSuite/Manifest.toml +++ b/Packaging/SaraSuite/Manifest.toml @@ -1,6 +1,6 @@ # Kept in sync between SaraSuite/Programs/SaraTag/ and SaraTag/Packaging/SaraSuite/. Name = "SaraTag" -Platforms = ["Linux", "Windows"] +Platforms = ["Linux", "Windows", "macOS"] Description = "Tag files with Books (sidecars) defined by Lexicons: browse, filter and sort by tag, edit tags on many files at once." [Version] @@ -40,6 +40,13 @@ Name = "SaraTag" Script = "Main.py" Icon = "assets/Icon.png" +# macOS: an app bundle, ~/Applications/SaraTag.app (python Main.py, from the app folder). +[[Shortcuts]] +Platform = "macOS" +Name = "SaraTag" +Script = "Main.py" +Icon = "assets/Icon.png" + # Made by SaraTag itself. The Lexicons are your tag definitions: only tick # "remove configs" if you don't need them any more. [[Leftovers]] diff --git a/Settings.py b/Settings.py index 597a55f..79ae056 100644 --- a/Settings.py +++ b/Settings.py @@ -11,8 +11,8 @@ import flet as ft import tomlkit from Core import ( - AppConfig, AvailableThemes, BundledFontFamily, ChildEnvironment, ChipButtonStyle, ExportSettings, Home, - ImportSettings, IsWindows, LoadSharedSettings, Palette, SharedSettings, + AppConfig, AvailableThemes, BundledFontFamily, ChildEnvironment, ChipButtonStyle, DisplayPath, ExportSettings, + Home, ImportSettings, IsMac, IsWindows, LoadSharedSettings, Palette, SharedConfigPath, SharedSettings, ) from Library import ConfPath, ConfValue, DataDir, DataError, SaveConfValues from Widgets import App @@ -55,11 +55,58 @@ def _WindowsFonts() -> set[str]: 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 _Fonts() -> list[str]: - """Installed font families (fontconfig, or the registry on Windows), with - the bundled font first.""" - if IsWindows: - Families = _WindowsFonts() + """Installed font families (fontconfig, the registry on Windows, the font + files themselves on macOS), with the bundled font first.""" + if IsWindows or IsMac: + Families = _WindowsFonts() if IsWindows else _MacFonts() Families.discard(BundledFontFamily) return [BundledFontFamily] + sorted(Families, key=str.lower) try: @@ -200,7 +247,7 @@ def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[], None]) -> None: Categories: dict[str, ft.Control] = { "SaraSuite": ft.Column( [ - _Note("Shared by every SaraSuite program (~/.config/SaraSuite/Config.toml). " + _Note(f"Shared by every SaraSuite program ({DisplayPath(str(SharedConfigPath))}). " "Other programs pick changes up the next time they start."), UseSharedSwitch, ft.Divider(height=1, color=Palette.Surface1), diff --git a/Widgets.py b/Widgets.py index 22c1b2b..4674683 100644 --- a/Widgets.py +++ b/Widgets.py @@ -12,7 +12,7 @@ from typing import Callable, Optional import flet as ft -from Core import AppConfig, ChildEnvironment, ChipButton, IsWindows, Note, NotificationStyle, Palette +from Core import AppConfig, ChildEnvironment, ChipButton, IsMac, IsWindows, Note, NotificationStyle, Palette from Library import ConfValue @@ -72,7 +72,8 @@ class App: else to the default app.""" from Query import VideoExtensions - Command = None if IsWindows else ["xdg-open", str(TargetPath)] # Windows: os.startfile below + # Windows: os.startfile below. + Command = None if IsWindows else ["open" if IsMac else "xdg-open", str(TargetPath)] if TargetPath.suffix.lower() in VideoExtensions: Player = str(ConfValue("Media", "VideoPlayer", "") or "").strip() if Player and shutil.which(Player.split()[0]):