"""Settings dialog: the shared SaraSuite settings (with export/import), SaraSort's own, and where SaraSortd lives. Laid out like SaraFem's and SaraSuite's Settings (a category list on the left, the chosen category on the right).""" import subprocess from pathlib import Path from typing import Callable import flet as ft import tomlkit from Core import ( AppConfig, AvailableThemes, BundledFontFamily, ChildEnvironment, ChipButtonStyle, ExportSettings, Home, ImportSettings, LoadSharedSettings, Palette, SharedSettings, ) from Widgets import App DialogWidth = 700 DialogHeight = 520 CategoryListWidth = 150 RestartNote = "Theme, font and title bar changes apply the next time SaraSort starts." StartPages = ["Flow", "Service", "Configs", "Templates", "Tools"] def _Note(Text: str) -> ft.Text: return ft.Text(Text, color=Palette.Overlay0, size=12) def _Fonts() -> list[str]: """Installed font families (fontconfig), with the bundled font first.""" 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 _NumberField(Label: str, Value: int) -> ft.TextField: return ft.TextField(label=Label, value=str(Value), keyboard_type=ft.KeyboardType.NUMBER) def _ReadInt(Field: ft.TextField, Fallback: int, Minimum: int) -> int: try: return max(Minimum, int(Field.value)) except (TypeError, ValueError): return Fallback # keep the previous value rather than crash on bad input def ShowSettingsDialog(TheApp: App, OnSaved: Callable[[], None]) -> None: Page = TheApp.Page Config: AppConfig = TheApp.Config Themes = AvailableThemes() 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], ) # SaraSuite: ~/.config/SaraSuite/Config.toml, read by every SaraSuite program. Shared = LoadSharedSettings(Config) UseSharedSwitch = ft.Switch(label="SaraSort 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) SettingsFileField = ft.TextField(value=str(Home / "SaraSort-Settings.toml"), label="Settings file path") # SaraSort'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("SaraSort'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 StartPage = ft.Dropdown( label="Start page", value=Config.StartPage if Config.StartPage in StartPages else "Flow", options=[ft.DropdownOption(key=Name, text=Name) for Name in StartPages], ) LabelLength = _NumberField("Arrow label length before it's shortened", Config.LabelLength) ConfigDirField = ft.TextField(label="SaraSortd config folder", value=Config.SortdConfigDir) AppDirField = ft.TextField(label="SaraSortd program folder", value=Config.SortdAppDir) UnitField = ft.TextField(label="systemd user unit", value=Config.ServiceName) AutoRestartSwitch = ft.Switch(label="Restart SaraSortd after changes to GlobalConf.toml", value=Config.AutoRestart) RefreshField = _NumberField("Service page refresh (seconds)", Config.RefreshSeconds) LogLinesField = _NumberField("Log lines to show", Config.LogLines) def OnExport(e) -> None: PathText = (SettingsFileField.value or "").strip() if not PathText: return try: ExportSettings(Config, Path(PathText).expanduser()) except OSError as Error: TheApp.Notify(f"Couldn't export settings: {Error}", "error") return TheApp.Notify("Settings exported.", "success") def OnImport(e) -> None: PathText = (SettingsFileField.value or "").strip() if not PathText: return try: ImportSettings(Config, Path(PathText).expanduser()) except (OSError, tomlkit.exceptions.TOMLKitError) as Error: TheApp.Notify(f"Couldn't import settings: {Error}", "error") return # The open dialog still shows the pre-import values; closing it # rather than leaving a stale Save that would overwrite the import. Page.pop_dialog() OnSaved() TheApp.Notify("Settings imported. Reopen Settings to see them; restart for Theme/Font/Title bar.", "success") Categories: dict[str, ft.Control] = { "SaraSuite": ft.Column( [ _Note("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), SharedTheme, SharedFont, SharedTitleBar, ft.Divider(height=1, color=Palette.Surface1), _Note("Export/import settings as one file: this tab's shared settings plus everything else " "in Settings. Same format as SaraFem's."), SettingsFileField, ft.Row([ ft.OutlinedButton(content="Export", style=ChipButtonStyle(), on_click=OnExport), ft.OutlinedButton(content="Import", style=ChipButtonStyle(), on_click=OnImport), ]), ], spacing=12, scroll=ft.ScrollMode.AUTO, ), "General": ft.Column( [ StartPage, LabelLength, ft.Divider(height=1, color=Palette.Surface1), OwnAppearance, ], spacing=12, scroll=ft.ScrollMode.AUTO, ), "SaraSortd": ft.Column( [ _Note("Where SaraSortd lives. The defaults are where SaraSuite installs it."), ConfigDirField, AppDirField, UnitField, AutoRestartSwitch, _Note("SaraSortd only reads GlobalConf.toml when it starts."), ft.Divider(height=1, color=Palette.Surface1), RefreshField, LogLinesField, ], spacing=12, scroll=ft.ScrollMode.AUTO, ), } DetailArea = ft.Container(Categories["General"], 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 == "General", 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, scroll=ft.ScrollMode.AUTO) def Save(e) -> None: Config.UseSharedSettings = UseSharedSwitch.value Config.Theme = OwnTheme.value or Config.Theme Config.FontFamily = OwnFont.value or Config.FontFamily Config.HideWindowTitleBar = OwnTitleBar.value Config.StartPage = StartPage.value or Config.StartPage Config.LabelLength = _ReadInt(LabelLength, Config.LabelLength, 6) Config.SortdConfigDir = (ConfigDirField.value or "").strip() or Config.SortdConfigDir Config.SortdAppDir = (AppDirField.value or "").strip() or Config.SortdAppDir Config.ServiceName = (UnitField.value or "").strip() or Config.ServiceName Config.AutoRestart = AutoRestartSwitch.value Config.RefreshSeconds = _ReadInt(RefreshField, Config.RefreshSeconds, 1) Config.LogLines = _ReadInt(LogLinesField, Config.LogLines, 10) Config.Save() SharedSettings( Theme=SharedTheme.value, FontFamily=SharedFont.value or Shared.FontFamily, HideWindowTitleBar=SharedTitleBar.value, ).Save() Page.pop_dialog() OnSaved() TheApp.Notify("Settings saved.", "success") 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, vertical_alignment=ft.CrossAxisAlignment.STRETCH, ), 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), ], ))