"""Settings dialog: the shared SaraSuite settings, SaraSuite's own, and the install defaults for each program. Laid out like SaraFem's Settings (a category list on the left, the chosen category on the right).""" import subprocess from typing import Callable import flet as ft from Core import ( AppConfig, BundledFontFamily, ChildEnvironment, ChipButtonStyle, IsWindows, LoadSharedSettings, Palette, SharedSettings, ThemesDir, ) from Installer import DefaultAnswers, DefaultOptions from Manifests import Program DialogWidth = 700 DialogHeight = 480 CategoryListWidth = 150 RestartNote = "Theme, font and title bar changes apply the next time a program starts." def _Note(Text: str) -> ft.Text: return ft.Text(Text, color=Palette.Overlay0, size=12) def _Themes() -> list[str]: Names = {Path.stem for Path in ThemesDir.glob("*.toml")} if ThemesDir.is_dir() else set() Names.add("CatppuccinMocha") # built in, even before SaraFem seeds the folder return sorted(Names) def _Fonts() -> list[str]: """Installed font families (fontconfig, or the registry on Windows), with the bundled font first.""" if IsWindows: import WindowsSupport Families = WindowsSupport.FontFamilies() 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(), ) 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 ShowSettingsDialog(Page: ft.Page, Config: AppConfig, Programs: list[Program], OnSaved: Callable[[], None]) -> None: Themes = _Themes() Fonts = _Fonts() def ThemePicker(Value: str) -> ft.Dropdown: Names = Themes if Value in Themes else [Value] + Themes return ft.Dropdown(value=Value, label="Theme", options=[ft.DropdownOption(key=Name, text=Name) for Name in Names]) def FontPicker(Value: str) -> ft.Dropdown: # 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], ) # Shared: ~/.config/SaraSuite/Config.toml, read by every SaraSuite program. Shared = LoadSharedSettings(Config) UseSharedSwitch = ft.Switch(label="SaraSuite uses these shared settings", value=Config.UseSharedSettings) SharedTheme = ThemePicker(Shared.Theme) SharedFont = FontPicker(Shared.FontFamily) SharedTitleBar = ft.Switch(label="Hide window title bar", value=Shared.HideWindowTitleBar) # SaraSuite's own appearance, only used with shared settings off. OwnTheme = ThemePicker(Config.Theme) OwnFont = FontPicker(Config.FontFamily) OwnTitleBar = ft.Switch(label="Hide window title bar", value=Config.HideWindowTitleBar) OwnAppearance = ft.Column( [_Note("SaraSuite's own appearance, used when shared settings are off"), OwnTheme, OwnFont, OwnTitleBar], 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 GitHostField = ft.TextField( value=Config.GitHost, label="Git host", hint_text="https://git.srhtnin.garden/sarah", ) Categories: dict[str, ft.Control] = { "Shared": ft.Column( [ _Note("Shared by every SaraSuite program (~/.config/SaraSuite/Config.toml)."), UseSharedSwitch, ft.Divider(height=1, color=Palette.Surface1), SharedTheme, SharedFont, SharedTitleBar, ], spacing=12, ), "SaraSuite": ft.Column( [ GitHostField, _Note("Where programs are installed and updated from; each program's repo is /."), ft.Divider(height=1, color=Palette.Surface1), OwnAppearance, ], spacing=12, scroll=ft.ScrollMode.AUTO, ), } # One tab per program: what its install dialog starts with. DefaultBoxes: dict[str, dict[str, ft.Checkbox]] = {} DefaultFields: dict[str, dict[str, ft.TextField]] = {} for Current in Programs: if not Current.Options and not Current.Questions: continue Answers = DefaultAnswers(Current) Ticked = DefaultOptions(Current) DefaultFields[Current.Name] = { Question.Id: ft.TextField(label=Question.Prompt, value=Answers[Question.Id]) for Question in Current.Questions } DefaultBoxes[Current.Name] = { Option.Id: ft.Checkbox(label=Option.Label, value=Ticked[Option.Id]) for Option in Current.Options } Categories[Current.Name] = ft.Column( [_Note(f"What installing {Current.Name} starts with.")] + list(DefaultFields[Current.Name].values()) + list(DefaultBoxes[Current.Name].values()), spacing=12, scroll=ft.ScrollMode.AUTO, ) DetailArea = ft.Container(Categories["Shared"], expand=True, padding=ft.Padding.only(left=12)) CategoryTiles: dict[str, ft.ListTile] = {} def SelectCategory(Name: str) -> None: for CategoryName, Tile in CategoryTiles.items(): Tile.selected = CategoryName == Name DetailArea.content = Categories[Name] DetailArea.update() CategoryList.update() for Name in Categories: CategoryTiles[Name] = ft.ListTile( title=Name, selected=Name == "Shared", selected_tile_color=Palette.Surface1, selected_color=Palette.Mauve, on_click=lambda e, n=Name: SelectCategory(n), ) CategoryList = ft.Column(list(CategoryTiles.values()), spacing=0) def Save(e) -> None: Config.UseSharedSettings = UseSharedSwitch.value Config.Theme = OwnTheme.value Config.FontFamily = OwnFont.value or Config.FontFamily Config.HideWindowTitleBar = OwnTitleBar.value Config.GitHost = (GitHostField.value or "").strip().rstrip("/") or Config.GitHost for Name, Boxes in DefaultBoxes.items(): Config.OptionDefaults[Name] = {Id: bool(Box.value) for Id, Box in Boxes.items()} for Name, Fields in DefaultFields.items(): if Fields: Config.AnswerDefaults[Name] = {Id: (Field.value or "").strip() for Id, Field in Fields.items()} Config.Save() SharedSettings( Theme=SharedTheme.value, FontFamily=SharedFont.value or Shared.FontFamily, HideWindowTitleBar=SharedTitleBar.value, ).Save() Page.pop_dialog() OnSaved() Page.show_dialog(ft.AlertDialog( title="Settings", content=ft.Column( [ ft.Row( [ ft.Container( CategoryList, width=CategoryListWidth, bgcolor=Palette.Surface0, border_radius=4, padding=4, ), DetailArea, ], expand=True, ), ft.Divider(height=1, color=Palette.Surface1), _Note(RestartNote), ], width=DialogWidth, height=DialogHeight, ), actions=[ ft.OutlinedButton(content="Cancel", style=ChipButtonStyle(), on_click=lambda e: Page.pop_dialog()), ft.OutlinedButton(content="Save", style=ChipButtonStyle(), on_click=Save), ], ))