diff --git a/Core.py b/Core.py index db0e25d..0e6ee65 100644 --- a/Core.py +++ b/Core.py @@ -6,6 +6,7 @@ one accent colour (Purple/Mauve) used everywhere, not a mauve+lavender mix. """ import shutil +import subprocess from dataclasses import asdict, dataclass, field from pathlib import Path @@ -37,7 +38,11 @@ class Palette: AppName = "SaraFem" -FontFamily = "Fixedsys Excelsior" +# Bundled in assets/fonts and always registered, so it works as FontFamily +# without being installed. Any other FontFamily is looked up among the +# system's installed fonts. +BundledFontFamily = "Fixedsys Excelsior" +ActiveFontFamily = BundledFontFamily # set by ApplyAppearance, read by BuildTheme CacheDir = Path.home() / ".cache" / "SaraFem" ThumbnailCacheDir = CacheDir / "Thumbnails" @@ -48,6 +53,9 @@ SharedConfigDir = Path.home() / ".config" / "SaraSuite" ThemesDir = SharedConfigDir / "Themes" ConfigDir = SharedConfigDir / "SaraFem" ConfigPath = ConfigDir / "Config.toml" +# Settings shared by every SaraSuite program. Same key names as the matching +# fields in each program's own Config.toml. +SharedConfigPath = SharedConfigDir / "Config.toml" BookmarksPath = ConfigDir / ".sfem_bookmarks.toml" _OldConfigDir = Path.home() / ".config" / "SaraFem" @@ -152,10 +160,16 @@ class AppConfig: (nothing changes visually until the user opts in via Ctrl+B/P/H/T or edits Config.toml directly).""" + # True: Theme, FontFamily and HideWindowTitleBar come from the shared + # ~/.config/SaraSuite/Config.toml, like every other SaraSuite program. + # False: SaraFem uses its own values below instead. + UseSharedSettings: bool = True Theme: str = "CatppuccinMocha" # matches a file in ~/.config/SaraSuite/Themes/ + FontFamily: str = BundledFontFamily + HideWindowTitleBar: bool = False # the actual OS window title bar, not the toolbar below + ShowDotfiles: bool = True ViewMode: str = "list" # "list" or "grid" - HideWindowTitleBar: bool = False # the actual OS window title bar, not the toolbar below # Batch-rename pattern for multi-select rename, syntax modeled on # SaraSortd's own placeholder characters (see ~/Applications/SaraSortd's @@ -252,6 +266,68 @@ def LoadConfig() -> AppConfig: return AppConfig(**Filtered) +@dataclass +class SharedSettings: + """~/.config/SaraSuite/Config.toml: the settings every SaraSuite program + shares. A program whose own UseSharedSettings is off reads these same + keys from its own Config.toml instead.""" + + Theme: str = "CatppuccinMocha" + FontFamily: str = BundledFontFamily + HideWindowTitleBar: bool = False + + def Save(self) -> None: + SharedConfigDir.mkdir(parents=True, exist_ok=True) + SharedConfigPath.write_text(toml.dumps(asdict(self))) + + +def _OwnSettings(Config: AppConfig) -> SharedSettings: + return SharedSettings(Config.Theme, Config.FontFamily, Config.HideWindowTitleBar) + + +def LoadSharedSettings(Config: AppConfig) -> SharedSettings: + """The shared file, created from SaraFem's own values the first time so + switching to it doesn't change anything visible.""" + if not SharedConfigPath.exists(): + Shared = _OwnSettings(Config) + Shared.Save() + return Shared + Result = _OwnSettings(Config) # fallback for keys the file doesn't have + try: + Data = toml.loads(SharedConfigPath.read_text()) + except toml.TomlDecodeError: + return Result + for Key in SharedSettings.__dataclass_fields__: + if Key in Data: + setattr(Result, Key, Data[Key]) + return Result + + +def EffectiveSettings(Config: AppConfig) -> SharedSettings: + """What SaraFem actually uses: the shared settings, or its own.""" + return LoadSharedSettings(Config) if Config.UseSharedSettings else _OwnSettings(Config) + + +def ApplyAppearance(Settings: SharedSettings) -> None: + """Theme colours and font. Must run before anything reads Palette.X or + calls BuildTheme(). The window title bar is applied by Main.""" + global ActiveFontFamily + ApplyTheme(Settings.Theme) + ActiveFontFamily = Settings.FontFamily or BundledFontFamily + + +def InstalledFontFamilies() -> list[str]: + """Font families fontconfig knows about, for the font picker. The + bundled font is always first, since it isn't installed system-wide.""" + try: + Result = subprocess.run(["fc-list", ":", "family"], capture_output=True, text=True, timeout=5) + Families = {Line.split(",")[0].strip() for Line in Result.stdout.splitlines() if Line.strip()} + except (OSError, subprocess.SubprocessError): + Families = set() + Families.discard(BundledFontFamily) + return [BundledFontFamily] + sorted(Families, key=str.lower) + + def BuildTheme() -> ft.Theme: ColorScheme = ft.ColorScheme( primary=Palette.Mauve, @@ -275,7 +351,7 @@ def BuildTheme() -> ft.Theme: on_error=Palette.Crust, ) return ft.Theme( - font_family=FontFamily, + font_family=ActiveFontFamily, color_scheme=ColorScheme, use_material3=True, scaffold_bgcolor=Palette.Base, @@ -283,7 +359,7 @@ def BuildTheme() -> ft.Theme: 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), + title_text_style=ft.TextStyle(color=Palette.Mauve, size=18, font_family=ActiveFontFamily), ), ) diff --git a/Main.py b/Main.py index a35b0e3..87fce0d 100644 --- a/Main.py +++ b/Main.py @@ -11,7 +11,7 @@ from typing import Optional import flet as ft from Browser import Browser -from Core import AppName, ApplyTheme, BuildTheme, FontFamily, LoadConfig +from Core import AppName, ApplyAppearance, BuildTheme, BundledFontFamily, EffectiveSettings, LoadConfig from Sidebar import Sidebar @@ -189,16 +189,17 @@ def _RunPickerMode(Page: ft.Page, Config, Args: argparse.Namespace) -> None: def Main(Page: ft.Page) -> None: Config = LoadConfig() - ApplyTheme(Config.Theme) # must run before anything reads Palette.X + Appearance = EffectiveSettings(Config) # shared SaraSuite settings, or SaraFem's own + ApplyAppearance(Appearance) # must run before anything reads Palette.X Page.theme_mode = ft.ThemeMode.DARK Page.theme = BuildTheme() Page.dark_theme = BuildTheme() - Page.fonts = {FontFamily: "fonts/Fixedsys_Excelsior_Regular.ttf"} + Page.fonts = {BundledFontFamily: "fonts/Fixedsys_Excelsior_Regular.ttf"} Page.padding = 0 Page.window.width = 1100 Page.window.height = 700 - Page.window.title_bar_hidden = Config.HideWindowTitleBar + Page.window.title_bar_hidden = Appearance.HideWindowTitleBar PickerArgs = _ParsePickerArgs() if PickerArgs.pick_open or PickerArgs.pick_save or PickerArgs.pick_folder: diff --git a/Settings.py b/Settings.py index 0a416ff..ffa6a07 100644 --- a/Settings.py +++ b/Settings.py @@ -9,12 +9,15 @@ from typing import Callable import flet as ft -from Core import AppConfig, AvailableThemes, ChipButtonStyle, Palette +from Core import ( + AppConfig, AvailableThemes, ChipButtonStyle, InstalledFontFamilies, LoadSharedSettings, Palette, + SharedSettings, +) # Settings Browser can already hot-apply live (dotfiles, breadcrumbs, preview, tags). # The window title bar, sidebar (bookmarks/filetree) layout, theme, and SaraTag's # dynamically-loaded module need a restart to take effect. -RestartRequiredNote = "Window title bar, Theme, Sidebar and SaraTag settings need a restart to take effect." +RestartRequiredNote = "Window title bar, Theme, Font, Sidebar and SaraTag settings need a restart to take effect." DialogWidth = 700 DialogHeight = 520 @@ -64,13 +67,53 @@ def BuildSettingsDialog( OnSave: Callable[[], None], OnDismiss: Callable[[], None], ) -> ft.AlertDialog: - ThemeDropdown = ft.Dropdown( - value=Config.Theme, - label="Theme (shared across SaraSuite)", - options=[ft.DropdownOption(key=Name, text=Name) for Name in AvailableThemes()], - ) - DotfilesSwitch = ft.Switch(label="Show dotfiles", value=Config.ShowDotfiles) + Themes = AvailableThemes() + Fonts = InstalledFontFamilies() + + def ThemePicker(Value: str) -> ft.Dropdown: + return ft.Dropdown( + value=Value, label="Theme", options=[ft.DropdownOption(key=Name, text=Name) for Name in Themes], + ) + + def FontPicker(Value: str) -> ft.Dropdown: + # Installed fonts plus the bundled one; type to filter the long list. + Names = Fonts if Value in Fonts else [Value] + Fonts + return ft.Dropdown( + value=Value, label="Font", editable=True, enable_filter=True, menu_height=320, + options=[ft.DropdownOption(key=Name, text=Name) for Name in Names], + ) + + # "SaraSuite" tab: the shared settings every SaraSuite program reads from + # ~/.config/SaraSuite/Config.toml, and whether SaraFem follows them. + Shared = LoadSharedSettings(Config) + UseSharedSwitch = ft.Switch(label="SaraFem uses these shared settings", value=Config.UseSharedSettings) + SharedThemeDropdown = ThemePicker(Shared.Theme) + SharedFontDropdown = FontPicker(Shared.FontFamily) + SharedTitleBarSwitch = ft.Switch(label="Hide window title bar", value=Shared.HideWindowTitleBar) + + # SaraFem's own values ("General" tab), only used with shared settings off. + ThemeDropdown = ThemePicker(Config.Theme) + FontDropdown = FontPicker(Config.FontFamily) TitleBarSwitch = ft.Switch(label="Hide window title bar", value=Config.HideWindowTitleBar) + OwnAppearance = ft.Column( + [ + _SectionLabel("SaraFem's own appearance, used when shared settings are off (SaraSuite tab)"), + ThemeDropdown, + FontDropdown, + TitleBarSwitch, + ], + spacing=12, + disabled=Config.UseSharedSettings, + opacity=0.5 if Config.UseSharedSettings else 1, + ) + + def OnUseSharedChanged(e) -> None: + OwnAppearance.disabled = UseSharedSwitch.value + OwnAppearance.opacity = 0.5 if UseSharedSwitch.value else 1 + OwnAppearance.update() + + UseSharedSwitch.on_change = OnUseSharedChanged + DotfilesSwitch = ft.Switch(label="Show dotfiles", value=Config.ShowDotfiles) BatchRenamePatternField = ft.TextField( value=Config.BatchRenamePattern, label="Batch rename pattern (~ = name, ! = counter, ^ = extension)", @@ -166,17 +209,30 @@ def BuildSettingsDialog( ) Categories: dict[str, ft.Control] = { + "SaraSuite": ft.Column( + [ + _SectionLabel("Shared by every SaraSuite program (~/.config/SaraSuite/Config.toml). " + "Other programs pick changes up the next time they start."), + UseSharedSwitch, + ft.Divider(height=1, color=Palette.Surface1), + SharedThemeDropdown, + SharedFontDropdown, + SharedTitleBarSwitch, + ], + spacing=12, + ), "General": ft.Column( [ - ThemeDropdown, DotfilesSwitch, - TitleBarSwitch, BatchRenamePatternField, DeleteZipSwitch, _SectionLabel("Default compress format"), ArchiveFormatPicker, + ft.Divider(height=1, color=Palette.Surface1), + OwnAppearance, ], spacing=12, + scroll=ft.ScrollMode.AUTO, ), "Toolbar": ft.Column( [ @@ -270,9 +326,16 @@ def BuildSettingsDialog( ) def DoSave(e=None) -> None: + Config.UseSharedSettings = UseSharedSwitch.value Config.Theme = ThemeDropdown.value - Config.ShowDotfiles = DotfilesSwitch.value + Config.FontFamily = FontDropdown.value or Config.FontFamily Config.HideWindowTitleBar = TitleBarSwitch.value + SharedSettings( + Theme=SharedThemeDropdown.value, + FontFamily=SharedFontDropdown.value or Shared.FontFamily, + HideWindowTitleBar=SharedTitleBarSwitch.value, + ).Save() + Config.ShowDotfiles = DotfilesSwitch.value Config.BatchRenamePattern = BatchRenamePatternField.value.strip() or "~!^" Config.DeleteZipAfterExtract = DeleteZipSwitch.value Config.DefaultArchiveFormat = GetArchiveFormat()