Windows support

This commit is contained in:
sarah committed 2026-10-09 14:16:50 +02:00
1 parent f1935a119b
commit 6cb2bd74cc
13 files changed
+207 -25

No files matched your search

+19 -6
View File
@@ -19,9 +19,17 @@ AppName = "SaraSort"
BundledFontFamily = "Fixedsys Excelsior" BundledFontFamily = "Fixedsys Excelsior"
ActiveFontFamily = BundledFontFamily # set by ApplyAppearance, read by BuildTheme ActiveFontFamily = BundledFontFamily # set by ApplyAppearance, read by BuildTheme
IsWindows = sys.platform == "win32"
# Each system's own per-user folders, like SaraSuite's: ~/.config and
# ~/.local/share on Linux, %APPDATA% and %LOCALAPPDATA% on Windows.
Home = Path.home() Home = Path.home()
ConfigHome = Home / ".config" if IsWindows:
DataHome = Home / ".local" / "share" ConfigHome = Path(os.environ.get("APPDATA") or Home / "AppData" / "Roaming")
DataHome = Path(os.environ.get("LOCALAPPDATA") or Home / "AppData" / "Local")
else:
ConfigHome = Path(os.environ.get("XDG_CONFIG_HOME") or Home / ".config")
DataHome = Path(os.environ.get("XDG_DATA_HOME") or Home / ".local" / "share")
SharedConfigDir = ConfigHome / "SaraSuite" SharedConfigDir = ConfigHome / "SaraSuite"
ThemesDir = SharedConfigDir / "Themes" ThemesDir = SharedConfigDir / "Themes"
@@ -100,10 +108,13 @@ class AppConfig:
HideWindowTitleBar: bool = False HideWindowTitleBar: bool = False
# Where SaraSuite installs SaraSortd: GlobalConf.toml and the two # Where SaraSuite installs SaraSortd: GlobalConf.toml and the two
# directory config templates live in SortdConfigDir, SaraSortd.py in # directory config templates live in SortdConfigDir, SaraSortd.py in
# SortdAppDir, and it runs as a systemd user unit. # SortdAppDir, and it runs as a systemd user unit (Linux) or a background
SortdConfigDir: str = "~/.config/SaraSuite/SaraSortd" # service started from the Startup folder (Windows; see Service.py).
SortdAppDir: str = "~/.local/share/SaraSuite/Programs/SaraSortd/App" SortdConfigDir: str = (str(ConfigHome / "SaraSuite" / "SaraSortd") if IsWindows
ServiceName: str = "SaraSortd.service" else "~/.config/SaraSuite/SaraSortd")
SortdAppDir: str = (str(DataHome / "SaraSuite" / "Programs" / "SaraSortd" / "App") if IsWindows
else "~/.local/share/SaraSuite/Programs/SaraSortd/App")
ServiceName: str = "SaraSortd" if IsWindows else "SaraSortd.service"
# SaraSortd only reads GlobalConf.toml when it starts. # SaraSortd only reads GlobalConf.toml when it starts.
AutoRestart: bool = True AutoRestart: bool = True
RefreshSeconds: int = 5 # status and log refresh on the Service page RefreshSeconds: int = 5 # status and log refresh on the Service page
@@ -238,6 +249,8 @@ def ChildEnvironment() -> dict[str, str]:
Environment.pop("LD_LIBRARY_PATH", None) Environment.pop("LD_LIBRARY_PATH", None)
for Key in ("PYTHONHOME", "PYTHONPATH"): for Key in ("PYTHONHOME", "PYTHONPATH"):
Environment.pop(Key, None) Environment.pop(Key, None)
if IsWindows:
Environment["PYTHONUTF8"] = "1" # every Sara program reads and writes UTF-8
return Environment return Environment
+3 -1
View File
@@ -9,7 +9,7 @@ import flet as ft
from ConfigPage import ConfigPage from ConfigPage import ConfigPage
from Core import ( from Core import (
AppName, ApplyAppearance, AssetsDir, BuildTheme, BundledFontFamily, ChipButtonStyle, DataHome, EffectiveSettings, AppName, ApplyAppearance, AssetsDir, BuildTheme, BundledFontFamily, ChipButtonStyle, DataHome, EffectiveSettings,
LoadConfig, Palette, IsWindows, LoadConfig, Palette,
) )
from FlowPage import FlowPage from FlowPage import FlowPage
from ServicePage import ServicePage from ServicePage import ServicePage
@@ -65,6 +65,8 @@ def Main(Page: ft.Page) -> None:
Page.bgcolor = Palette.Base Page.bgcolor = Palette.Base
Page.window.width = 1180 Page.window.width = 1180
Page.window.height = 780 Page.window.height = 780
if IsWindows:
Page.window.icon = str(AssetsDir / "Icon.ico") # taskbar/title bar (Linux: the desktop entry)
Page.window.min_width = 760 Page.window.min_width = 760
Page.window.min_height = 520 Page.window.min_height = 520
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+17
View File
@@ -1,6 +1,7 @@
# Kept in sync between SaraSuite/Programs/SaraSort/ and SaraSort/Packaging/SaraSuite/. # Kept in sync between SaraSuite/Programs/SaraSort/ and SaraSort/Packaging/SaraSuite/.
Name = "SaraSort" Name = "SaraSort"
Parent = "SaraSortd" # a GUI for SaraSortd, listed under it Parent = "SaraSortd" # a GUI for SaraSortd, listed under it
Platforms = ["Linux", "Windows"]
Description = "A GUI for configuring SaraSortd: a flowchart of what goes where, its service and logs, configs and templates." Description = "A GUI for configuring SaraSortd: a flowchart of what goes where, its service and logs, configs and templates."
[Source] [Source]
@@ -11,17 +12,33 @@ Exclude = ["Packaging"]
[Python] [Python]
RequirementsFile = "requirements.txt" RequirementsFile = "requirements.txt"
# Linux: a launcher and a desktop entry.
[[Templates]] [[Templates]]
Platform = "Linux"
Source = "Launch.sh" Source = "Launch.sh"
Target = "{AppDir}/Packaging/Launch.sh" Target = "{AppDir}/Packaging/Launch.sh"
Executable = true Executable = true
[[Templates]] [[Templates]]
Platform = "Linux"
Source = "SaraSort.desktop" Source = "SaraSort.desktop"
Target = "{ApplicationsDir}/SaraSort.desktop" Target = "{ApplicationsDir}/SaraSort.desktop"
# Windows: a Start Menu shortcut (pythonw.exe Main.py, from the app folder).
[[Shortcuts]]
Platform = "Windows"
Name = "SaraSort"
Script = "Main.py"
Icon = "assets/Icon.png"
# Made by SaraSort itself: Config.toml, Layout.toml, Templates/. # Made by SaraSort itself: Config.toml, Layout.toml, Templates/.
[[Leftovers]] [[Leftovers]]
Path = "{ConfigDir}" Path = "{ConfigDir}"
Kind = "Config" Kind = "Config"
Note = "SaraSort's settings, flowchart layout and your templates" Note = "SaraSort's settings, flowchart layout and your templates"
# Shared with the other Flet programs; see Programs/FletIdentity.py.
[[Templates]]
Platform = "Linux"
Source = "../../FletIdentity.py"
Target = "{ProgramDir}/FletIdentity.py"
+4
View File
@@ -1,4 +1,8 @@
#!/bin/sh #!/bin/sh
# Written by SaraSuite. Runs from the app folder so Flet finds assets/. # Written by SaraSuite. Runs from the app folder so Flet finds assets/.
# A Flet client copy with the window app ID srhtnin.{Name}, so the desktop
# matches the window to {Name}.desktop (StartupWMClass) instead of showing it
# as "com.appveyor.flet". Empty if that fails: Flet then uses its own client.
export FLET_VIEW_PATH="$("{Python}" "{ProgramDir}/FletIdentity.py" "srhtnin.{Name}")"
cd "{AppDir}" || exit 1 cd "{AppDir}" || exit 1
exec "{Python}" Main.py "$@" exec "{Python}" Main.py "$@"
@@ -7,3 +7,4 @@ Icon={AppDir}/assets/Icon.png
Terminal=false Terminal=false
Categories=Utility; Categories=Utility;
StartupNotify=true StartupNotify=true
StartupWMClass=srhtnin.{Name}
+107 -4
View File
@@ -1,11 +1,22 @@
"""SaraSortd's systemd user unit: status, start/stop/restart, autostart, and """SaraSortd as a background service: status, start/stop/restart, autostart,
its journal. SaraSortd's own log files are read from its LogDir.""" and its journal. SaraSortd's own log files are read from its LogDir.
Linux: its systemd user unit (systemctl / journalctl).
Windows: SaraSuite's stand-in for a systemd service, used the same way here:
- it runs windowless as `pythonw.exe ServiceRun.py SaraSortd SaraSortd.py`
from SaraSortd's config folder, and ServiceRun.py records its PID in
%LOCALAPPDATA%\\SaraSuite\\Services\\SaraSortd.pid (that's how "running" is told);
- autostart is a shortcut to that command in the Startup folder.
Unit is the systemd unit on Linux and the service name ("SaraSortd") on Windows.
"""
import re
import subprocess import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from pathlib import Path from pathlib import Path
from Core import ChildEnvironment from Core import ChildEnvironment, ConfigHome, DataHome, IsWindows, LoadConfig
@dataclass @dataclass
@@ -22,11 +33,98 @@ class ServiceStatus:
return self.Active in ("active", "activating", "reloading") return self.Active in ("active", "activating", "reloading")
_NoWindow = getattr(subprocess, "CREATE_NO_WINDOW", 0)
def _Run(Arguments: list[str], Timeout: int = 15) -> subprocess.CompletedProcess: def _Run(Arguments: list[str], Timeout: int = 15) -> subprocess.CompletedProcess:
return subprocess.run(Arguments, capture_output=True, text=True, timeout=Timeout, env=ChildEnvironment()) return subprocess.run(Arguments, capture_output=True, text=True, timeout=Timeout, env=ChildEnvironment(),
creationflags=_NoWindow)
# --- Windows ------------------------------------------------------------------------------
def _PidFile(Name: str) -> Path:
return DataHome / "SaraSuite" / "Services" / f"{Name}.pid"
def _StartupLink(Name: str) -> Path:
return ConfigHome / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup" / f"{Name}.lnk"
def _WindowsCommand() -> tuple[Path, list[str], Path]:
"""(pythonw.exe, its arguments, working folder), from SaraSuite's install layout."""
Config = LoadConfig()
AppDir = Path(Config.SortdAppDir).expanduser()
ProgramDir = AppDir.parent
PythonW = ProgramDir / ".venv" / "Scripts" / "pythonw.exe"
Arguments = ["-X", "utf8", str(ProgramDir / "ServiceRun.py"), Config.ServiceName, str(Config.SortdScriptPath)]
return PythonW, Arguments, Path(Config.SortdConfigDir).expanduser()
def _WindowsPid(Name: str) -> str:
try:
Pid = int(_PidFile(Name).read_text().strip())
Output = _Run(["tasklist", "/FI", f"PID eq {Pid}", "/NH", "/FO", "CSV"]).stdout
except (OSError, ValueError, subprocess.SubprocessError):
return ""
return str(Pid) if re.search(rf'"{Pid}"', Output) else ""
def _WindowsStatus(Name: str) -> ServiceStatus:
PythonW, Arguments, _Folder = _WindowsCommand()
Pid = _WindowsPid(Name)
Since = ""
if Pid:
Since = datetime.fromtimestamp(_PidFile(Name).stat().st_mtime).strftime("%Y-%m-%d %H:%M:%S")
return ServiceStatus(
Active="active" if Pid else "inactive",
Sub="running" if Pid else "dead",
Enabled="enabled" if _StartupLink(Name).exists() else "disabled",
Pid=Pid,
Since=Since,
Found=PythonW.is_file() and Path(Arguments[2]).is_file(),
)
def _WindowsControl(Name: str, Action: str) -> str:
PythonW, Arguments, Folder = _WindowsCommand()
try:
if Action in ("stop", "restart"):
Pid = _WindowsPid(Name)
if Pid:
_Run(["taskkill", "/PID", Pid, "/T", "/F"])
_PidFile(Name).unlink(missing_ok=True)
if Action in ("start", "restart"):
if _WindowsPid(Name):
return ""
Folder.mkdir(parents=True, exist_ok=True)
subprocess.Popen(
[str(PythonW), *Arguments], cwd=str(Folder), env=ChildEnvironment(), close_fds=True,
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
creationflags=getattr(subprocess, "DETACHED_PROCESS", 0) | _NoWindow,
)
elif Action == "enable":
import pylnk3
Link = _StartupLink(Name)
Link.parent.mkdir(parents=True, exist_ok=True)
Quoted = " ".join(f'"{Item}"' if " " in Item or "\\" in Item else Item for Item in Arguments)
pylnk3.for_file(str(PythonW), str(Link), arguments=Quoted, work_dir=str(Folder),
description=f"{Name} (background service)")
elif Action == "disable":
_StartupLink(Name).unlink(missing_ok=True)
except (OSError, subprocess.SubprocessError) as Error:
return str(Error)
return ""
# --- Both -----------------------------------------------------------------------------------
def Status(Unit: str) -> ServiceStatus: def Status(Unit: str) -> ServiceStatus:
if IsWindows:
return _WindowsStatus(Unit)
try: try:
Result = _Run(["systemctl", "--user", "show", Unit, Result = _Run(["systemctl", "--user", "show", Unit,
"-p", "LoadState,ActiveState,SubState,UnitFileState,MainPID,ActiveEnterTimestamp"]) "-p", "LoadState,ActiveState,SubState,UnitFileState,MainPID,ActiveEnterTimestamp"])
@@ -45,6 +143,8 @@ def Status(Unit: str) -> ServiceStatus:
def Control(Unit: str, Action: str) -> str: def Control(Unit: str, Action: str) -> str:
"""Action: start, stop, restart, enable, disable. Returns "" or an error.""" """Action: start, stop, restart, enable, disable. Returns "" or an error."""
if IsWindows:
return _WindowsControl(Unit, Action)
try: try:
Result = _Run(["systemctl", "--user", Action, Unit]) Result = _Run(["systemctl", "--user", Action, Unit])
except (OSError, subprocess.SubprocessError) as Error: except (OSError, subprocess.SubprocessError) as Error:
@@ -60,6 +160,9 @@ def RestartIfRunning(Unit: str) -> str:
def Journal(Unit: str, Lines: int) -> list[str]: def Journal(Unit: str, Lines: int) -> list[str]:
if IsWindows:
return ["Windows has no systemd journal. SaraSortd's own log files (the other log source) "
"have everything it does."]
try: try:
Result = _Run(["journalctl", "--user", "-u", Unit, "-n", str(Lines), "--no-pager", "-o", "short-iso"]) Result = _Run(["journalctl", "--user", "-u", Unit, "-n", str(Lines), "--no-pager", "-o", "short-iso"])
except (OSError, subprocess.SubprocessError) as Error: except (OSError, subprocess.SubprocessError) as Error:
+3 -3
View File
@@ -7,7 +7,7 @@ from pathlib import Path
import flet as ft import flet as ft
import tomlkit import tomlkit
from Core import ChipButton, ChipButtonStyle, DataHome, Heading, Note, Palette from Core import ChipButton, ChipButtonStyle, DataHome, Heading, IsWindows, Note, Palette
from Service import Control, Journal, LogFiles, ReadTail, Status from Service import Control, Journal, LogFiles, ReadTail, Status
from Widgets import App from Widgets import App
@@ -105,10 +105,10 @@ class ServicePage(ft.Column):
self.StateText.value = Text self.StateText.value = Text
self.StateText.color = Color self.StateText.color = Color
self.Details.controls = [ self.Details.controls = [
Note(f"Unit: {self.App.Config.ServiceName}"), Note(f"{'Service' if IsWindows else 'Unit'}: {self.App.Config.ServiceName}"),
Note(f"Since: {Current.Since or '-'}"), Note(f"Since: {Current.Since or '-'}"),
Note(f"PID: {Current.Pid or '-'}"), Note(f"PID: {Current.Pid or '-'}"),
Note(f"Unit file: {Current.Enabled}"), Note(f"{'Autostart (Startup folder)' if IsWindows else 'Unit file'}: {Current.Enabled}"),
] ]
self.StartButton.disabled = Current.Running or not Current.Found self.StartButton.disabled = Current.Running or not Current.Found
self.StopButton.disabled = not Current.Running self.StopButton.disabled = not Current.Running
+34 -2
View File
@@ -12,7 +12,7 @@ import tomlkit
from Core import ( from Core import (
AppConfig, AvailableThemes, BundledFontFamily, ChildEnvironment, ChipButtonStyle, ExportSettings, Home, AppConfig, AvailableThemes, BundledFontFamily, ChildEnvironment, ChipButtonStyle, ExportSettings, Home,
ImportSettings, LoadSharedSettings, Palette, SharedSettings, ImportSettings, IsWindows, LoadSharedSettings, Palette, SharedSettings,
) )
from Sortd import MajorVersion, Setup, SortdError from Sortd import MajorVersion, Setup, SortdError
from SortdTransfer import ApplyBundle, Bundle, BundleDirectories, ExportSortd, MissingDirectories, ReadBundleSafely from SortdTransfer import ApplyBundle, Bundle, BundleDirectories, ExportSortd, MissingDirectories, ReadBundleSafely
@@ -29,8 +29,40 @@ def _Note(Text: str) -> ft.Text:
return ft.Text(Text, color=Palette.Overlay0, size=12) return ft.Text(Text, color=Palette.Overlay0, size=12)
def _WindowsFonts() -> set[str]:
"""Font families from the system and per-user Fonts registry keys
("Arial Bold (TrueType)" -> "Arial")."""
import re
import winreg
Families: set[str] = set()
Key = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
for Root in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER):
try:
with winreg.OpenKey(Root, Key) as Fonts:
Index = 0
while True:
try:
Name = winreg.EnumValue(Fonts, Index)[0]
except OSError:
break
Index += 1
Name = re.sub(r"\s*\(.*\)$", "", Name)
Name = re.sub(r"\s+(Bold|Italic|Light|Semibold|SemiBold|Black|Regular|Medium|Thin)\b.*$", "", Name)
if Name and "&" not in Name:
Families.add(Name.strip())
except OSError:
continue
return Families
def _Fonts() -> list[str]: def _Fonts() -> list[str]:
"""Installed font families (fontconfig), with the bundled font first.""" """Installed font families (fontconfig, or the registry on Windows), with
the bundled font first."""
if IsWindows:
Families = _WindowsFonts()
Families.discard(BundledFontFamily)
return [BundledFontFamily] + sorted(Families, key=str.lower)
try: try:
Result = subprocess.run( Result = subprocess.run(
["fc-list", ":", "family"], capture_output=True, text=True, timeout=5, env=ChildEnvironment(), ["fc-list", ":", "family"], capture_output=True, text=True, timeout=5, env=ChildEnvironment(),
+13 -7
View File
@@ -22,6 +22,11 @@ from typing import Any, Optional
import tomlkit import tomlkit
from tomlkit.items import AoT from tomlkit.items import AoT
def _Slashes(PathText: str) -> str:
"""Windows paths use "\\" (and configs may mix both); SaraSortd 7.5 names and compares with "/"."""
return PathText.replace("\\", "/")
from Core import AppConfig from Core import AppConfig
TemplateNames = {"In": "InputDirConf.toml", "Out": "OutputDirConf.toml"} TemplateNames = {"In": "InputDirConf.toml", "Out": "OutputDirConf.toml"}
@@ -276,20 +281,21 @@ class Setup:
return Path(os.path.normpath(self.GlobalPath.parent / Parsed)) return Path(os.path.normpath(self.GlobalPath.parent / Parsed))
def FlatName(self, DirPath: str) -> str: def FlatName(self, DirPath: str) -> str:
"""SaraSortd's FlatName: "%RootDir%/Pictures/Cats" -> "Pictures_Cats".""" """SaraSortd's FlatName: "%RootDir%/Pictures/Cats" -> "Pictures_Cats".
Root = self.ParsePath(str(self.Dirs.get("RootDir", ""))).rstrip("/") Like SaraSortd 7.5, "\\" counts as "/" and a drive's ":" is dropped (Windows)."""
DirPath = DirPath.rstrip("/") Root = _Slashes(self.ParsePath(str(self.Dirs.get("RootDir", "")))).rstrip("/")
DirPath = _Slashes(DirPath).rstrip("/")
if Root and DirPath.startswith(Root + "/"): if Root and DirPath.startswith(Root + "/"):
Relative = DirPath[len(Root) + 1:] Relative = DirPath[len(Root) + 1:]
else: else:
Relative = DirPath.strip("/") or os.path.basename(DirPath) Relative = DirPath.strip("/") or os.path.basename(DirPath)
return Relative.replace("/", "_") return Relative.replace(":", "").replace("/", "_")
def CentralConf(self, DirPath: str, Kind: str) -> Optional[Path]: def CentralConf(self, DirPath: str, Kind: str) -> Optional[Path]:
"""SaraSortd 7.2: a config named in [DirConfs], or one in DirConfDir. """SaraSortd 7.2: a config named in [DirConfs], or one in DirConfDir.
None when the config lives inside the directory.""" None when the config lives inside the directory."""
for Key, Value in (self.Data.get("DirConfs") or {}).items(): for Key, Value in (self.Data.get("DirConfs") or {}).items():
if self.ParsePath(str(Key)).rstrip("/") == DirPath.rstrip("/"): if _Slashes(self.ParsePath(str(Key))).rstrip("/") == _Slashes(DirPath).rstrip("/"):
if isinstance(Value, dict): if isinstance(Value, dict):
Value = Value.get(Kind) Value = Value.get(Kind)
if Value: if Value:
@@ -338,7 +344,7 @@ class Setup:
After = Setup(self.Config, NewDoc) After = Setup(self.Config, NewDoc)
Folder = After.CentralFolder() Folder = After.CentralFolder()
for InputPath in self.Inputs: for InputPath in self.Inputs:
if Folder and (Folder == Path(InputPath) or str(Folder).startswith(InputPath.rstrip("/") + "/")): if Folder and Folder.is_relative_to(Path(InputPath)):
raise SortdError(f"{Folder} is inside the input {InputPath}; SaraSortd would sort its own configs away.") raise SortdError(f"{Folder} is inside the input {InputPath}; SaraSortd would sort its own configs away.")
Plan: dict[Path, Path] = {} Plan: dict[Path, Path] = {}
for DirPath in dict.fromkeys(self.Inputs + self.Outputs): for DirPath in dict.fromkeys(self.Inputs + self.Outputs):
@@ -919,7 +925,7 @@ class Setup:
f"which SaraSortd {Daemon} doesn't know about. Update SaraSortd to 7.2.")) f"which SaraSortd {Daemon} doesn't know about. Update SaraSortd to 7.2."))
if Central: if Central:
for InputPath in Inputs: for InputPath in Inputs:
if Central == Path(InputPath) or str(Central).startswith(InputPath.rstrip("/") + "/"): if Central.is_relative_to(Path(InputPath)):
Found.append(("Error", f"DirConfDir ({Central}) is inside the input {InputPath}, so SaraSortd " Found.append(("Error", f"DirConfDir ({Central}) is inside the input {InputPath}, so SaraSortd "
"would sort its own configs away.")) "would sort its own configs away."))
if UsesOwnInputs and Daemon and not VersionAtLeast(Daemon, (7, 1)): if UsesOwnInputs and Daemon and not VersionAtLeast(Daemon, (7, 1)):
+5 -2
View File
@@ -7,7 +7,7 @@ from typing import Callable, Optional
import flet as ft import flet as ft
from Core import AppConfig, ChildEnvironment, ChipButton, Note, NotificationStyle, Palette from Core import AppConfig, ChildEnvironment, ChipButton, IsWindows, Note, NotificationStyle, Palette
from Service import RestartIfRunning from Service import RestartIfRunning
from Sortd import Setup, SortdError from Sortd import Setup, SortdError
@@ -69,7 +69,10 @@ class App:
def OpenFolder(self, PathText: str) -> None: def OpenFolder(self, PathText: str) -> None:
try: try:
subprocess.Popen(["xdg-open", PathText], env=ChildEnvironment(), start_new_session=True) if IsWindows:
os.startfile(PathText) # Explorer
else:
subprocess.Popen(["xdg-open", PathText], env=ChildEnvironment(), start_new_session=True)
except OSError as Error: except OSError as Error:
self.Notify(f"Couldn't open {PathText}: {Error}", "error") self.Notify(f"Couldn't open {PathText}: {Error}", "error")
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

+1
View File
@@ -1,3 +1,4 @@
flet>=1.0,<2.0 flet>=1.0,<2.0
flet-desktop>=1.0,<2.0 # the desktop client; otherwise Flet fetches it on first launch and quits once flet-desktop>=1.0,<2.0 # the desktop client; otherwise Flet fetches it on first launch and quits once
tomlkit tomlkit
pylnk3; sys_platform == "win32" # Windows: SaraSortd's autostart is a Startup-folder shortcut