mirror of
https://git.srhtnin.garden/sarah/SaraSort.git
synced 2026-10-14 10:48:11 +02:00
SaraSort: Flet GUI for configuring SaraSortd
Flowchart of inputs/outputs with editable arrows, service page (status, controls, autostart, version, logs), config editor (form + raw), templates, tools (warnings, filename tester, failed files) and settings with SaraFem-compatible export/import. AppImage build and a SaraSuite manifest in Packaging/. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
f7ed23edf9
22 files changed
+3399
No files matched your search
+222
@@ -0,0 +1,222 @@
|
||||
"""SaraSortd's service: status, start/stop/restart, start on login, its
|
||||
version, and its logs (its own log files and the systemd journal)."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import flet as ft
|
||||
import tomlkit
|
||||
|
||||
from Core import ChipButton, ChipButtonStyle, DataHome, Heading, Note, Palette
|
||||
from Service import Control, Journal, LogFiles, ReadTail, Status
|
||||
from Widgets import App
|
||||
|
||||
ReceiptPath = DataHome / "SaraSuite" / "Receipts" / "SaraSortd.toml"
|
||||
|
||||
|
||||
def LineColor(Line: str) -> str:
|
||||
Lower = Line.lower()
|
||||
if "error" in Lower or "traceback" in Lower or "failed" in Lower:
|
||||
return Palette.Red
|
||||
if "sorted" in Lower:
|
||||
return Palette.Green
|
||||
if "started" in Lower or "stopped" in Lower:
|
||||
return Palette.Mauve
|
||||
if "checking" in Lower:
|
||||
return Palette.Overlay0
|
||||
return Palette.Text
|
||||
|
||||
|
||||
class ServicePage(ft.Column):
|
||||
def __init__(self, TheApp: App):
|
||||
super().__init__(expand=True, spacing=0)
|
||||
self.App = TheApp
|
||||
self.Polling = False
|
||||
self.Source = "Files" # or "Journal"
|
||||
|
||||
self.Dot = ft.Container(width=14, height=14, border_radius=7, bgcolor=Palette.Overlay0)
|
||||
self.StateText = ft.Text("", size=16)
|
||||
self.Details = ft.Column(spacing=2)
|
||||
self.VersionInfo = ft.Column(spacing=2)
|
||||
self.Autostart = ft.Switch(label="Start SaraSortd on login", on_change=self._ToggleAutostart)
|
||||
self.StartButton = ChipButton("Start", lambda e: self._Do("start"), ft.Icons.PLAY_ARROW)
|
||||
self.StopButton = ChipButton("Stop", lambda e: self._Do("stop"), ft.Icons.STOP, Accent=Palette.Red)
|
||||
self.RestartButton = ChipButton("Restart", lambda e: self._Do("restart"), ft.Icons.RESTART_ALT)
|
||||
|
||||
self.FilesButton = ChipButton("Log files", lambda e: self._SetSource("Files"))
|
||||
self.JournalButton = ChipButton("Journal", lambda e: self._SetSource("Journal"))
|
||||
self.FilePicker = ft.Dropdown(label="Log file", dense=True, width=260, on_select=lambda e: self._LoadLog())
|
||||
self.HideChecking = ft.Checkbox(label="Hide \"Checking\" lines", value=True, on_change=lambda e: self._LoadLog())
|
||||
self.Follow = ft.Checkbox(label="Follow", value=True)
|
||||
self.LogView = ft.ListView(expand=True, spacing=0, padding=8, auto_scroll=True)
|
||||
|
||||
StatusCard = ft.Container(
|
||||
ft.Column([
|
||||
ft.Row([self.Dot, self.StateText], spacing=10),
|
||||
self.Details,
|
||||
ft.Row([self.StartButton, self.StopButton, self.RestartButton], wrap=True),
|
||||
self.Autostart,
|
||||
], spacing=10),
|
||||
padding=14, bgcolor=Palette.Mantle, border=ft.Border.all(1, Palette.Surface1), border_radius=6, expand=True,
|
||||
)
|
||||
VersionCard = ft.Container(
|
||||
ft.Column([ft.Text("Version", color=Palette.Mauve), self.VersionInfo], spacing=8),
|
||||
padding=14, bgcolor=Palette.Mantle, border=ft.Border.all(1, Palette.Surface1), border_radius=6, expand=True,
|
||||
)
|
||||
self.controls = [
|
||||
ft.Container(
|
||||
ft.Row([Heading("Service"), ft.Container(expand=True),
|
||||
ChipButton("Reload", lambda e: self.Refresh(), ft.Icons.REFRESH)]),
|
||||
padding=ft.Padding.only(left=16, right=16, top=12, bottom=8),
|
||||
),
|
||||
ft.Container(ft.Row([StatusCard, VersionCard], vertical_alignment=ft.CrossAxisAlignment.START),
|
||||
padding=ft.Padding.symmetric(horizontal=16)),
|
||||
ft.Container(
|
||||
ft.Row([self.FilesButton, self.JournalButton, self.FilePicker, ft.Container(width=8),
|
||||
self.HideChecking, self.Follow],
|
||||
vertical_alignment=ft.CrossAxisAlignment.CENTER, scroll=ft.ScrollMode.AUTO),
|
||||
padding=ft.Padding.only(left=16, right=16, top=14, bottom=6),
|
||||
),
|
||||
ft.Container(self.LogView, expand=True, bgcolor=Palette.Crust, margin=ft.Margin.only(left=16, right=16, bottom=16),
|
||||
border=ft.Border.all(1, Palette.Surface1), border_radius=6),
|
||||
]
|
||||
|
||||
# Status
|
||||
|
||||
def Refresh(self) -> None:
|
||||
self._RefreshStatus()
|
||||
self._RefreshVersion()
|
||||
self._RefreshLogFiles()
|
||||
self._LoadLog()
|
||||
self._StyleSourceButtons()
|
||||
self.update()
|
||||
|
||||
def _RefreshStatus(self) -> None:
|
||||
Current = Status(self.App.Config.ServiceName)
|
||||
if not Current.Found:
|
||||
Color, Text = Palette.Overlay0, f"{self.App.Config.ServiceName} isn't installed"
|
||||
elif Current.Running:
|
||||
Color, Text = Palette.Green, f"Running ({Current.Sub})"
|
||||
elif Current.Active == "failed":
|
||||
Color, Text = Palette.Red, "Failed"
|
||||
else:
|
||||
Color, Text = Palette.Yellow, f"Stopped ({Current.Active})"
|
||||
self.Dot.bgcolor = Color
|
||||
self.StateText.value = Text
|
||||
self.StateText.color = Color
|
||||
self.Details.controls = [
|
||||
Note(f"Unit: {self.App.Config.ServiceName}"),
|
||||
Note(f"Since: {Current.Since or '-'}"),
|
||||
Note(f"PID: {Current.Pid or '-'}"),
|
||||
Note(f"Unit file: {Current.Enabled}"),
|
||||
]
|
||||
self.StartButton.disabled = Current.Running or not Current.Found
|
||||
self.StopButton.disabled = not Current.Running
|
||||
self.RestartButton.disabled = not Current.Found
|
||||
self.Autostart.value = Current.Enabled == "enabled"
|
||||
self.Autostart.disabled = not Current.Found
|
||||
|
||||
def _RefreshVersion(self) -> None:
|
||||
TheSetup, Error = self.App.LoadSetup()
|
||||
Lines = []
|
||||
Daemon = TheSetup.DaemonVersion() if TheSetup else None
|
||||
Lines.append(ft.Text(f"SaraSortd {Daemon or '?'}", size=16))
|
||||
Lines.append(Note(str(self.App.Config.SortdScriptPath)))
|
||||
if TheSetup:
|
||||
ConfVersion = str(TheSetup.Data.get("Version", "?"))
|
||||
Mismatch = Daemon and ConfVersion.split(".")[0] != Daemon.split(".")[0]
|
||||
Lines.append(ft.Text(f"GlobalConf.toml {ConfVersion}" + (" (major version differs!)" if Mismatch else ""),
|
||||
color=Palette.Red if Mismatch else Palette.Text, size=13))
|
||||
else:
|
||||
Lines.append(ft.Text(Error, color=Palette.Red, size=12))
|
||||
try:
|
||||
Receipt = tomlkit.parse(ReceiptPath.read_text()).unwrap()
|
||||
Lines.append(Note(f"Installed by SaraSuite: commit {str(Receipt.get('Commit', '?'))[:10]}"))
|
||||
Lines.append(Note(f"Installed {Receipt.get('InstalledAt', '?')}, updated {Receipt.get('UpdatedAt', '?')}"))
|
||||
except (OSError, tomlkit.exceptions.TOMLKitError):
|
||||
Lines.append(Note("Not installed through SaraSuite (no receipt)."))
|
||||
self.VersionInfo.controls = Lines
|
||||
|
||||
def _Do(self, Action: str) -> None:
|
||||
Error = Control(self.App.Config.ServiceName, Action)
|
||||
if Error:
|
||||
self.App.Notify(Error, "error")
|
||||
else:
|
||||
self.App.Notify(f"SaraSortd: {Action} done.", "success")
|
||||
self.Refresh()
|
||||
|
||||
def _ToggleAutostart(self, e) -> None:
|
||||
Error = Control(self.App.Config.ServiceName, "enable" if self.Autostart.value else "disable")
|
||||
if Error:
|
||||
self.App.Notify(Error, "error")
|
||||
self._RefreshStatus()
|
||||
self.update()
|
||||
|
||||
# Logs
|
||||
|
||||
def _StyleSourceButtons(self) -> None:
|
||||
self.FilesButton.style = ChipButtonStyle(Palette.Mauve if self.Source == "Files" else Palette.Surface1)
|
||||
self.JournalButton.style = ChipButtonStyle(Palette.Mauve if self.Source == "Journal" else Palette.Surface1)
|
||||
self.FilePicker.visible = self.Source == "Files"
|
||||
|
||||
def _SetSource(self, Source: str) -> None:
|
||||
self.Source = Source
|
||||
self._StyleSourceButtons()
|
||||
self._LoadLog()
|
||||
self.update()
|
||||
|
||||
def _LogDir(self) -> Path | None:
|
||||
TheSetup, _ = self.App.LoadSetup()
|
||||
return TheSetup.SpecialDir("LogDir") if TheSetup else None
|
||||
|
||||
def _RefreshLogFiles(self) -> None:
|
||||
LogDir = self._LogDir()
|
||||
Files = LogFiles(LogDir) if LogDir else []
|
||||
Current = self.FilePicker.value
|
||||
self.FilePicker.options = [ft.DropdownOption(key=str(FilePath), text=FilePath.name) for FilePath in Files]
|
||||
Keys = [str(FilePath) for FilePath in Files]
|
||||
if Current not in Keys:
|
||||
self.FilePicker.value = Keys[0] if Keys else None
|
||||
|
||||
def _LoadLog(self) -> None:
|
||||
Limit = self.App.Config.LogLines
|
||||
if self.Source == "Journal":
|
||||
Lines = Journal(self.App.Config.ServiceName, Limit)
|
||||
elif self.FilePicker.value:
|
||||
Lines = ReadTail(Path(self.FilePicker.value), Limit * 4)
|
||||
else:
|
||||
Lines = ["No log files yet (is Logging = 1 in GlobalConf.toml?)."]
|
||||
if self.HideChecking.value:
|
||||
Lines = [Line for Line in Lines if "Checking" not in Line]
|
||||
Lines = Lines[-Limit:]
|
||||
self.LogView.auto_scroll = bool(self.Follow.value)
|
||||
self.LogView.controls = [
|
||||
ft.Text(Line, size=12, color=LineColor(Line), selectable=True) for Line in Lines
|
||||
]
|
||||
if self.page:
|
||||
self.LogView.update()
|
||||
|
||||
# Polling while the page is shown
|
||||
|
||||
def StartPolling(self) -> None:
|
||||
if self.Polling:
|
||||
return
|
||||
self.Polling = True
|
||||
self.App.Page.run_task(self._Poll)
|
||||
|
||||
def StopPolling(self) -> None:
|
||||
self.Polling = False
|
||||
|
||||
async def _Poll(self) -> None:
|
||||
while self.Polling:
|
||||
await asyncio.sleep(max(1, int(self.App.Config.RefreshSeconds)))
|
||||
if not self.Polling:
|
||||
break
|
||||
try:
|
||||
self._RefreshStatus()
|
||||
if self.Follow.value:
|
||||
self._RefreshLogFiles()
|
||||
self._LoadLog()
|
||||
self.update()
|
||||
except Exception: # the page may be gone mid-update
|
||||
pass
|
||||
Reference in new issue
Block a user