mirror of
https://git.srhtnin.garden/sarah/SaraSort.git
synced 2026-10-13 18:28:18 +02:00
Windows support
This commit is contained in:
1 parent
f1935a119b
commit
6cb2bd74cc
13 files changed
+206
-24
No files matched your search
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
@@ -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"
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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(),
|
||||||
|
|||||||
@@ -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)):
|
||||||
|
|||||||
+4
-1
@@ -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,6 +69,9 @@ class App:
|
|||||||
|
|
||||||
def OpenFolder(self, PathText: str) -> None:
|
def OpenFolder(self, PathText: str) -> None:
|
||||||
try:
|
try:
|
||||||
|
if IsWindows:
|
||||||
|
os.startfile(PathText) # Explorer
|
||||||
|
else:
|
||||||
subprocess.Popen(["xdg-open", PathText], env=ChildEnvironment(), start_new_session=True)
|
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")
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@@ -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
|
||||||
Reference in new issue
Block a user