"""SaraSort's own settings (not SaraSortd's): the shared SaraSuite appearance, SaraSort's own, where SaraSortd lives, and exporting/importing settings in the same file format SaraFem uses ([SaraSuite] + one section per program).""" import subprocess from pathlib import Path from typing import Callable import flet as ft import tomlkit from Core import ( AppConfig, AvailableThemes, BundledFontFamily, ChildEnvironment, ChipButton, Heading, LoadSharedSettings, Note, Palette, SharedSettings, ExportSettings, ImportSettings, ) from Widgets import App CategoryListWidth = 170 RestartNote = "Theme, font and title bar changes apply the next time SaraSort starts." Pages = ["Flow", "Service", "Configs", "Templates", "Tools", "Settings"] def InstalledFonts() -> list[str]: 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) class SettingsPage(ft.Column): def __init__(self, TheApp: App, OnSaved: Callable[[], None]): super().__init__(expand=True, spacing=0) self.App = TheApp self.OnSaved = OnSaved self.Selected = "Shared" self.Detail = ft.Container(expand=True, padding=ft.Padding.only(left=16, right=16, top=8)) self.CategoryList = ft.Column(spacing=0) self.Fonts: list[str] = [] self.controls = [ ft.Container( ft.Row([ Heading("Settings"), ft.Container(expand=True), ChipButton("Import", self._Import, ft.Icons.FILE_DOWNLOAD_OUTLINED), ChipButton("Export", self._Export, ft.Icons.FILE_UPLOAD_OUTLINED), ChipButton("Revert", lambda e: self.Refresh(), ft.Icons.UNDO), ChipButton("Save", lambda e: self._Save(), ft.Icons.SAVE_OUTLINED), ]), padding=ft.Padding.only(left=16, right=16, top=12, bottom=8), ), ft.Divider(height=1, color=Palette.Surface1), ft.Row([ ft.Container(self.CategoryList, width=CategoryListWidth, bgcolor=Palette.Mantle, padding=4), self.Detail, ], expand=True, vertical_alignment=ft.CrossAxisAlignment.STRETCH), ft.Container(Note(RestartNote), padding=ft.Padding.only(left=16, bottom=10, top=6)), ] def Refresh(self) -> None: if not self.Fonts: self.Fonts = InstalledFonts() Config = self.App.Config Themes = AvailableThemes() 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: Names = self.Fonts if Value in self.Fonts else [Value] + self.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 = LoadSharedSettings(Config) self.UseShared = ft.Switch(label="SaraSort uses these shared settings", value=Config.UseSharedSettings, on_change=lambda e: self._DimOwn()) self.SharedTheme = ThemePicker(Shared.Theme) self.SharedFont = FontPicker(Shared.FontFamily) self.SharedTitleBar = ft.Switch(label="Hide window title bar", value=Shared.HideWindowTitleBar) self.OwnTheme = ThemePicker(Config.Theme) self.OwnFont = FontPicker(Config.FontFamily) self.OwnTitleBar = ft.Switch(label="Hide window title bar", value=Config.HideWindowTitleBar) self.OwnAppearance = ft.Column([ Note("SaraSort's own appearance, used when shared settings are off"), self.OwnTheme, self.OwnFont, self.OwnTitleBar, ], spacing=12) self.ConfigDir = ft.TextField(label="SaraSortd config folder (GlobalConf.toml and the templates)", value=Config.SortdConfigDir) self.AppDir = ft.TextField(label="SaraSortd program folder (SaraSortd.py)", value=Config.SortdAppDir) self.Unit = ft.TextField(label="systemd user unit", value=Config.ServiceName) self.AutoRestart = ft.Switch(label="Restart SaraSortd after changes to GlobalConf.toml", value=Config.AutoRestart) self.Refresh_ = ft.TextField(label="Service page refresh (seconds)", value=str(Config.RefreshSeconds), width=260, keyboard_type=ft.KeyboardType.NUMBER) self.LogLines = ft.TextField(label="Log lines to show", value=str(Config.LogLines), width=260, keyboard_type=ft.KeyboardType.NUMBER) self.LabelLength = ft.TextField(label="Arrow label length before shortening", value=str(Config.LabelLength), width=260, keyboard_type=ft.KeyboardType.NUMBER) self.StartPage = ft.Dropdown(label="Start page", value=Config.StartPage, width=260, options=[ft.DropdownOption(key=Name, text=Name) for Name in Pages[:-1]]) self.Categories = { "Shared": ft.Column([ Note("Shared by every SaraSuite program (~/.config/SaraSuite/Config.toml)."), self.UseShared, ft.Divider(height=1, color=Palette.Surface1), self.SharedTheme, self.SharedFont, self.SharedTitleBar, ], spacing=12, scroll=ft.ScrollMode.AUTO), "SaraSort": ft.Column([ self.StartPage, self.LabelLength, ft.Divider(height=1, color=Palette.Surface1), self.OwnAppearance, ], spacing=12, scroll=ft.ScrollMode.AUTO), "SaraSortd": ft.Column([ Note("Defaults are where SaraSuite installs SaraSortd."), self.ConfigDir, self.AppDir, self.Unit, self.AutoRestart, self.Refresh_, self.LogLines, ], spacing=12, scroll=ft.ScrollMode.AUTO), } self._DimOwn(Update=False) self._Select(self.Selected, Update=False) self.update() def _DimOwn(self, Update: bool = True) -> None: self.OwnAppearance.disabled = bool(self.UseShared.value) self.OwnAppearance.opacity = 0.5 if self.UseShared.value else 1 if Update: self.OwnAppearance.update() def _Select(self, Name: str, Update: bool = True) -> None: self.Selected = Name self.CategoryList.controls = [ ft.ListTile(title=Category, selected=Category == Name, selected_tile_color=Palette.Surface1, selected_color=Palette.Mauve, on_click=lambda e, Category=Category: self._Select(Category)) for Category in self.Categories ] self.Detail.content = self.Categories[Name] if Update: self.update() @staticmethod def _Int(Field: ft.TextField, Fallback: int, Minimum: int) -> int: try: return max(Minimum, int(Field.value)) except (TypeError, ValueError): return Fallback def _Save(self) -> None: Config: AppConfig = self.App.Config Config.UseSharedSettings = bool(self.UseShared.value) Config.Theme = self.OwnTheme.value or Config.Theme Config.FontFamily = self.OwnFont.value or Config.FontFamily Config.HideWindowTitleBar = bool(self.OwnTitleBar.value) Config.SortdConfigDir = (self.ConfigDir.value or "").strip() or Config.SortdConfigDir Config.SortdAppDir = (self.AppDir.value or "").strip() or Config.SortdAppDir Config.ServiceName = (self.Unit.value or "").strip() or Config.ServiceName Config.AutoRestart = bool(self.AutoRestart.value) Config.RefreshSeconds = self._Int(self.Refresh_, Config.RefreshSeconds, 1) Config.LogLines = self._Int(self.LogLines, Config.LogLines, 10) Config.LabelLength = self._Int(self.LabelLength, Config.LabelLength, 6) Config.StartPage = self.StartPage.value or Config.StartPage Config.Save() Shared = LoadSharedSettings(Config) SharedSettings( Theme=self.SharedTheme.value or Shared.Theme, FontFamily=self.SharedFont.value or Shared.FontFamily, HideWindowTitleBar=bool(self.SharedTitleBar.value), ).Save() self.App.Notify("Settings saved.", "success") self.OnSaved() async def _Export(self, e) -> None: Chosen = await self.App.Picker.save_file(dialog_title="Export settings", file_name="SaraSortSettings.toml", allowed_extensions=["toml"]) if not Chosen: return try: ExportSettings(self.App.Config, Path(Chosen)) except OSError as Error: self.App.Notify(f"Couldn't export settings: {Error}", "error") return self.App.Notify("Settings exported.", "success") async def _Import(self, e) -> None: Files = await self.App.Picker.pick_files(dialog_title="Import settings", allowed_extensions=["toml"]) if not Files: return try: ImportSettings(self.App.Config, Path(Files[0].path)) except (OSError, tomlkit.exceptions.TOMLKitError) as Error: self.App.Notify(f"Couldn't import settings: {Error}", "error") return self.Refresh() self.App.Notify("Settings imported. Theme, font and title bar apply on the next start.", "success") self.OnSaved()