diff --git a/Client/Core.py b/Client/Core.py index 185ce8c..30cf714 100644 --- a/Client/Core.py +++ b/Client/Core.py @@ -44,8 +44,20 @@ AppName = "SaraView" BundledFontFamily = "Fixedsys Excelsior" ActiveFontFamily = BundledFontFamily # set by ApplyAppearance, read by BuildTheme +IsWindows = sys.platform == "win32" + +# Each system's own per-user folders, like SaraSuite's: ~/.config, ~/.cache +# and ~/.local/share on Linux; %APPDATA% and %LOCALAPPDATA% on Windows. Home = Path.home() -SharedConfigDir = Home / ".config" / "SaraSuite" +if IsWindows: + ConfigHome = Path(os.environ.get("APPDATA") or Home / "AppData" / "Roaming") + DataHome = Path(os.environ.get("LOCALAPPDATA") or Home / "AppData" / "Local") + CacheHome = DataHome +else: + ConfigHome = Path(os.environ.get("XDG_CONFIG_HOME") or Home / ".config") + DataHome = Path(os.environ.get("XDG_DATA_HOME") or Home / ".local" / "share") + CacheHome = Path(os.environ.get("XDG_CACHE_HOME") or Home / ".cache") +SharedConfigDir = ConfigHome / "SaraSuite" ThemesDir = SharedConfigDir / "Themes" SharedConfigPath = SharedConfigDir / "Config.toml" ConfigDir = SharedConfigDir / AppName @@ -54,8 +66,7 @@ ConfigPath = ConfigDir / "Config.toml" # never carries it. SessionPath = ConfigDir / "Session.toml" # Server files opened in another program are downloaded here first. -CacheDir = Home / ".cache" / AppName -DataHome = Home / ".local" / "share" +CacheDir = CacheHome / AppName / "Cache" if IsWindows else CacheHome / AppName # Inside a PyInstaller build, bundled files are unpacked to sys._MEIPASS. BundleDir = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent)) @@ -307,16 +318,53 @@ def ChildEnvironment() -> dict[str, str]: Environment.pop("LD_LIBRARY_PATH", None) for Key in ("PYTHONHOME", "PYTHONPATH"): Environment.pop(Key, None) + if IsWindows: + Environment["PYTHONUTF8"] = "1" # every Sara program reads and writes UTF-8 return Environment def OpenExternally(FilePath: Path) -> None: """Hands a file to the system's default program. Raises OSError.""" + if IsWindows: + os.startfile(str(FilePath)) + return subprocess.Popen(["xdg-open", str(FilePath)], env=ChildEnvironment(), start_new_session=True) +def _WindowsFonts() -> set[str]: + """Font families from the system and per-user Fonts registry keys + ("Arial Bold (TrueType)" -> "Arial").""" + import re + import winreg + + Families: set[str] = set() + Key = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" + for Root in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER): + try: + with winreg.OpenKey(Root, Key) as Fonts: + Index = 0 + while True: + try: + Name = winreg.EnumValue(Fonts, Index)[0] + except OSError: + break + Index += 1 + Name = re.sub(r"\s*\(.*\)$", "", Name) + Name = re.sub(r"\s+(Bold|Italic|Light|Semibold|SemiBold|Black|Regular|Medium|Thin)\b.*$", "", Name) + if Name and "&" not in Name: + Families.add(Name.strip()) + except OSError: + continue + return Families + + def InstalledFontFamilies() -> list[str]: - """Font families fontconfig knows about, with the bundled font first.""" + """Font families fontconfig (or, on Windows, the registry) knows about, + with the bundled font first.""" + if IsWindows: + Families = _WindowsFonts() + Families.discard(BundledFontFamily) + return [BundledFontFamily] + sorted(Families, key=str.lower) try: Result = subprocess.run( ["fc-list", ":", "family"], capture_output=True, text=True, timeout=5, env=ChildEnvironment(), @@ -489,9 +537,9 @@ def HumanSize(NumBytes: Optional[int]) -> str: def DisplayPath(PathText: str) -> str: - """/home/sarah/Notes -> ~/Notes, for labels.""" + """/home/sarah/Notes -> ~/Notes, for labels (C:\\Users\\x\\Notes -> ~\\Notes).""" HomeText = str(Home) - if PathText == HomeText or PathText.startswith(HomeText + "/"): + if PathText == HomeText or PathText.startswith(HomeText + os.sep): return "~" + PathText[len(HomeText):] return PathText diff --git a/Client/Main.py b/Client/Main.py index 8c2e895..733cd46 100644 --- a/Client/Main.py +++ b/Client/Main.py @@ -5,6 +5,7 @@ the right.""" import os import subprocess +import sys from typing import Optional import flet as ft @@ -22,7 +23,7 @@ from Settings import ShowSettingsDialog def _SetCursorThemeFallback() -> None: """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.""" - if os.environ.get("XCURSOR_THEME"): + if os.environ.get("XCURSOR_THEME") or sys.platform == "win32": # GTK/GNOME only return try: Result = subprocess.run(["gsettings", "get", "org.gnome.desktop.interface", "cursor-theme"], @@ -48,6 +49,8 @@ def Main(Page: ft.Page) -> None: Page.bgcolor = Palette.Base Page.window.width = 1180 Page.window.height = 760 + if sys.platform == "win32": + Page.window.icon = str(AssetsDir / "Icon.ico") # taskbar/title bar (Linux: the desktop entry) Page.window.min_width = 720 Page.window.min_height = 480 diff --git a/Client/assets/Icon.ico b/Client/assets/Icon.ico new file mode 100644 index 0000000..073e24b Binary files /dev/null and b/Client/assets/Icon.ico differ