From 954bf39c8f0d535ce888d9ad90535f5bdc12555a Mon Sep 17 00:00:00 2001 From: SRHTNIN Date: Fri, 9 Oct 2026 14:25:03 +0200 Subject: [PATCH] Client: Windows support --- Client/Core.py | 60 ++++++++++++++++++++++++++++++++++++----- Client/Main.py | 5 +++- Client/assets/Icon.ico | Bin 0 -> 8936 bytes 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 Client/assets/Icon.ico 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 0000000000000000000000000000000000000000..073e24b60443503190d50ad0fa63a5a292380372 GIT binary patch literal 8936 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 F{{a3#3m*Ug literal 0 HcmV?d00001