Files
SaraSort/Settings.py
T
sarahandClaude Opus 5.5 e90f59bc16 Look like SaraFem and SaraSuite: no custom title bar
The window keeps its own title bar (or none, per HideWindowTitleBar), with
SaraSuite's header (name, Add to app menu for AppImages, gear). Settings
is now a dialog laid out like SaraFem's, with export/import on the
SaraSuite tab, and notifications use SaraFem's tinted toast style.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-07 22:36:04 +02:00

254 lines
10 KiB
Python

"""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),
],
))