"""Shared app-wide constants: paths, palette, theme. The palette and theming mirror SaraFem's Core.py (which mirrors SaraView's), and read the same shared ~/.config/SaraSuite/Themes/*.toml files, so a theme picked for one Sara program looks the same in all of them. """ import os import sys from dataclasses import asdict, dataclass from pathlib import Path import flet as ft import tomlkit AppName = "SaraSuite" FontFamily = "Fixedsys Excelsior" Home = Path.home() ConfigHome = Home / ".config" DataHome = Home / ".local" / "share" CacheHome = Home / ".cache" SharedConfigDir = ConfigHome / "SaraSuite" ThemesDir = SharedConfigDir / "Themes" ConfigDir = SharedConfigDir / "SaraSuite" ConfigPath = ConfigDir / "Config.toml" # Everything SaraSuite itself keeps about installed programs. Nothing in here # is user-edited, so it's all fair game to delete when a program is removed. DataDir = DataHome / "SaraSuite" ProgramsDir = DataDir / "Programs" # installed program files + venvs ReceiptsDir = DataDir / "Receipts" # what each install placed, for exact removal PristineDir = DataDir / "Pristine" # the default configs as last installed (merge base) BackupsDir = DataDir / "Backups" # displaced files and pre-merge config copies DownloadsDir = CacheHome / "SaraSuite" / "Downloads" # extracted source archives # Bundled read-only files (program manifests, fonts). Inside a PyInstaller # build they're unpacked to sys._MEIPASS rather than next to the code. BundleDir = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent)) ManifestsDir = BundleDir / "Programs" AssetsDir = BundleDir / "assets" class Palette: """Live values for the active theme (see ApplyTheme). Same attribute names and SaraView role mapping as SaraFem's Palette.""" Mauve = "#cba6f7" Red = "#f38ba8" Peach = "#fab387" Yellow = "#f9e2af" Green = "#a6e3a1" Blue = "#89b4fa" Text = "#cdd6f4" White = "#cdd6f4" Overlay0 = "#6c7086" Surface1 = "#45475a" Surface0 = "#313244" Base = "#1e1e2e" Mantle = "#181825" Crust = "#11111b" _ThemeRoleMap = { "Background": "Base", "Surface": "Mantle", "SurfaceAlt": "Surface0", "Border": "Surface1", "Text": "Text", "TextMuted": "Overlay0", "Black": "Crust", "White": "White", "Red": "Red", "Orange": "Peach", "Yellow": "Yellow", "Green": "Green", "Blue": "Blue", "Purple": "Mauve", } def ApplyTheme(ThemeName: str) -> None: """Mutate Palette in place from ~/.config/SaraSuite/Themes/.toml. Falls back to the built-in Catppuccin Mocha values if the file is missing (SaraFem seeds that directory; SaraSuite only reads it). Must run before any control reads Palette.X.""" ThemeFile = ThemesDir / f"{ThemeName}.toml" if not ThemeFile.exists(): return try: RoleValues = tomlkit.parse(ThemeFile.read_text()).unwrap().get("Palette", {}) except tomlkit.exceptions.TOMLKitError: return for RoleName, AttrName in _ThemeRoleMap.items(): if RoleName in RoleValues: setattr(Palette, AttrName, RoleValues[RoleName]) @dataclass class AppConfig: Theme: str = "CatppuccinMocha" # matches a file in ~/.config/SaraSuite/Themes/ # Where the Sara repos live (Gitea on SpineS); manifests # use it as {GitHost}. GitHost: str = "https://git.srhtnin.garden/sarah" def Save(self) -> None: ConfigDir.mkdir(parents=True, exist_ok=True) ConfigPath.write_text(tomlkit.dumps(asdict(self))) def LoadConfig() -> AppConfig: if not ConfigPath.exists(): Config = AppConfig() Config.Save() return Config try: Data = tomlkit.parse(ConfigPath.read_text()).unwrap() except tomlkit.exceptions.TOMLKitError: return AppConfig() KnownFields = set(AppConfig.__dataclass_fields__) return AppConfig(**{Key: Value for Key, Value in Data.items() if Key in KnownFields}) def ChildEnvironment() -> dict[str, str]: """The environment for child processes (systemctl, pip, python3 -m venv). A PyInstaller build points LD_LIBRARY_PATH at its own bundled libraries and keeps the original in LD_LIBRARY_PATH_ORIG; system tools need the original back or they can load the wrong libraries.""" Environment = dict(os.environ) if getattr(sys, "frozen", False): Original = Environment.pop("LD_LIBRARY_PATH_ORIG", None) if Original is not None: Environment["LD_LIBRARY_PATH"] = Original else: Environment.pop("LD_LIBRARY_PATH", None) for Key in ("PYTHONHOME", "PYTHONPATH"): Environment.pop(Key, None) return Environment def CurrentDesktop() -> str: """First entry of $XDG_CURRENT_DESKTOP as-is (e.g. "COSMIC"), used for portal descriptors' UseIn= and the -portals.conf file name.""" return os.environ.get("XDG_CURRENT_DESKTOP", "").split(":")[0] def BuildTheme() -> ft.Theme: ColorScheme = ft.ColorScheme( primary=Palette.Mauve, on_primary=Palette.Crust, primary_container=Palette.Surface0, on_primary_container=Palette.Mauve, secondary=Palette.Mauve, on_secondary=Palette.Crust, secondary_container=Palette.Surface0, on_secondary_container=Palette.Mauve, surface=Palette.Base, on_surface=Palette.Text, on_surface_variant=Palette.Overlay0, surface_container=Palette.Mantle, surface_container_low=Palette.Mantle, surface_container_high=Palette.Surface0, surface_container_highest=Palette.Surface1, outline=Palette.Surface1, outline_variant=Palette.Surface1, error=Palette.Red, on_error=Palette.Crust, ) return ft.Theme( font_family=FontFamily, color_scheme=ColorScheme, use_material3=True, scaffold_bgcolor=Palette.Base, canvas_color=Palette.Base, dialog_theme=ft.DialogTheme( bgcolor=Palette.Mantle, shape=ft.RoundedRectangleBorder(radius=8, side=ft.BorderSide(1, Palette.Mauve)), title_text_style=ft.TextStyle(color=Palette.Mauve, size=18, font_family=FontFamily), ), ) def ChipButtonStyle(Accent: str = "") -> ft.ButtonStyle: """SaraView's button look: SurfaceAlt fill, 1px accent border, small radius. Accent defaults to Mauve; Remove buttons pass Palette.Red.""" return ft.ButtonStyle( color=Palette.Text, bgcolor=Palette.Surface0, side=ft.BorderSide(1, Accent or Palette.Mauve), shape=ft.RoundedRectangleBorder(radius=4), )