Windows support

This commit is contained in:
sarah committed 2026-10-09 14:21:42 +02:00
1 parent 9757518d8e
commit 8eb9943a30
14 files changed
+92 -17

No files matched your search

+4 -1
View File
@@ -59,6 +59,9 @@ SaraTagRecordsPath = ApplicationDirectory / ".SaraTagRecords.toml"
SaraTagHistoryPath = ApplicationDirectory / ".SaraTagHistory.toml"
# Empty means the default app; Windows rarely has mpv.
DefaultVideoPlayer = "" if sys.platform == "win32" else "mpv"
DefaultSaraTagConf = f'''# SaraTag's main portable configuration file.
# Paths in this file are interpreted relative to the SaraTag directory
# unless they are absolute.
@@ -83,7 +86,7 @@ ShowInvalidEntries = true
ShowImagePreviews = true
[Media]
VideoPlayer = "mpv"
VideoPlayer = "{DefaultVideoPlayer}"
[History]
Enabled = true
+1 -1
View File
@@ -207,7 +207,7 @@ def RenameFile(SourcePath: Path, NewName: str) -> Path:
"""Renames a file and its Book together; undoes the file rename if the
Book can't follow. Returns the new path."""
NewName = NewName.strip()
if not NewName or "/" in NewName or NewName in (".", ".."):
if not NewName or "/" in NewName or "\\" in NewName or NewName in (".", ".."):
raise DataError("That isn't a valid file name.")
if NewName.startswith("."):
raise DataError("Names starting with a dot are hidden, and SaraTag skips hidden files.")
+14 -4
View File
@@ -23,9 +23,17 @@ AppVersion = "0.2.0-beta"
BundledFontFamily = "Fixedsys Excelsior"
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()
ConfigHome = Home / ".config"
DataHome = Home / ".local" / "share"
if IsWindows:
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"
ThemesDir = SharedConfigDir / "Themes"
@@ -222,6 +230,8 @@ def ChildEnvironment() -> dict[str, str]:
Environment.pop("LD_LIBRARY_PATH", None)
for Key in ("PYTHONHOME", "PYTHONPATH"):
Environment.pop(Key, None)
if IsWindows:
Environment["PYTHONUTF8"] = "1" # every Sara program reads and writes UTF-8
return Environment
@@ -301,8 +311,8 @@ def Heading(Text: str) -> ft.Text:
def DisplayPath(PathText: str) -> str:
"""/home/you/Pictures -> ~/Pictures, for labels."""
"""/home/you/Pictures -> ~/Pictures, for labels (C:\\Users\\you\\Pictures -> ~\\Pictures)."""
HomeText = str(Home)
if PathText == HomeText or PathText.startswith(HomeText + "/"):
if PathText == HomeText or PathText.startswith(HomeText + os.sep):
return "~" + PathText[len(HomeText):]
return PathText
+1 -1
View File
@@ -363,7 +363,7 @@ def KnownLexicons(Directories: list[Path]) -> list[Path]:
def NewSharedLexicon(Name: str) -> Path:
"""An empty Lexicon in Lexicons/ for other Lexicons to inherit from."""
Name = Name.strip().removesuffix(".toml")
if not Name or "/" in Name or Name.startswith("."):
if not Name or "/" in Name or "\\" in Name or Name.startswith("."):
raise DataError("That isn't a valid file name.")
LexiconsDir.mkdir(parents=True, exist_ok=True)
FilePath = LexiconsDir / f"{Name}.toml"
+5 -1
View File
@@ -4,7 +4,9 @@ The window and navigation; each page lives in its own *Page.py."""
import flet as ft
from BrowsePage import BrowsePage
from Core import AppName, ApplyAppearance, AssetsDir, BuildTheme, BundledFontFamily, EffectiveSettings, LoadConfig, Palette
from Core import (
AppName, ApplyAppearance, AssetsDir, BuildTheme, BundledFontFamily, EffectiveSettings, IsWindows, LoadConfig, Palette,
)
from LexiconsPage import LexiconsPage
from LibrariesPage import LibrariesPage
from Library import TagCore
@@ -40,6 +42,8 @@ def Main(Page: ft.Page) -> None:
Page.bgcolor = Palette.Base
Page.window.width = 1280
Page.window.height = 820
if IsWindows:
Page.window.icon = str(AssetsDir / "Icon.ico") # taskbar/title bar (Linux: the desktop entry)
Page.window.min_width = 900
Page.window.min_height = 560
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

+17
View File
@@ -1,5 +1,6 @@
# Kept in sync between SaraSuite/Programs/SaraTag/ and SaraTag/Packaging/SaraSuite/.
Name = "SaraTag"
Platforms = ["Linux", "Windows"]
Description = "Tag files with Books (sidecars) defined by Lexicons: browse, filter and sort by tag, edit tags on many files at once."
[Version]
@@ -20,18 +21,34 @@ RequirementsFile = "requirements.txt"
Source = "Location.toml"
Target = "{AppDir}/.saratag/Location.toml" # tells .saratag/Core.py where the data lives
# Linux: a launcher and a desktop entry.
[[Templates]]
Platform = "Linux"
Source = "Launch.sh"
Target = "{AppDir}/Packaging/Launch.sh"
Executable = true
[[Templates]]
Platform = "Linux"
Source = "SaraTag.desktop"
Target = "{ApplicationsDir}/SaraTag.desktop"
# Windows: a Start Menu shortcut (pythonw.exe Main.py, from the app folder).
[[Shortcuts]]
Platform = "Windows"
Name = "SaraTag"
Script = "Main.py"
Icon = "assets/Icon.png"
# Made by SaraTag itself. The Lexicons are your tag definitions: only tick
# "remove configs" if you don't need them any more.
[[Leftovers]]
Path = "{ConfigDir}"
Kind = "Config"
Note = "SaraTag's settings, Records, Libraries, history and Lexicons (Books next to your files are never touched)"
# 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
# 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
exec "{Python}" Main.py "$@"
+2 -1
View File
@@ -1,3 +1,4 @@
# Written by SaraSuite. Where SaraTag keeps SaraTagConf.toml, its Records,
# history and Lexicons (see .saratag/Core.py).
DataDir = "{ConfigDir}"
# A literal ('...') string: Windows paths are full of backslashes.
DataDir = '{ConfigDir}'
@@ -5,5 +5,6 @@ Comment=Tag files with Books defined by Lexicons
Exec={AppDir}/Packaging/Launch.sh
Icon={AppDir}/assets/Icon.png
Terminal=false
Categories=Utility;Graphics;
Categories=Utility;
StartupNotify=true
StartupWMClass=srhtnin.{Name}
+1 -1
View File
@@ -253,7 +253,7 @@ def RenameRecordDirectory(Index: int, NewDirectoryName: str) -> Path:
if TheRecord is None:
raise DataError("That Record isn't in .SaraTagRecords.toml any more; reload.")
NewDirectoryName = NewDirectoryName.strip()
if not NewDirectoryName or "/" in NewDirectoryName or NewDirectoryName.startswith("."):
if not NewDirectoryName or "/" in NewDirectoryName or "\\" in NewDirectoryName or NewDirectoryName.startswith("."):
raise DataError("That isn't a valid directory name.")
OldPath = TheRecord.Directory
NewPath = OldPath.with_name(NewDirectoryName)
+34 -2
View File
@@ -12,7 +12,7 @@ import tomlkit
from Core import (
AppConfig, AvailableThemes, BundledFontFamily, ChildEnvironment, ChipButtonStyle, ExportSettings, Home,
ImportSettings, LoadSharedSettings, Palette, SharedSettings,
ImportSettings, IsWindows, LoadSharedSettings, Palette, SharedSettings,
)
from Library import ConfPath, ConfValue, DataDir, DataError, SaveConfValues
from Widgets import App
@@ -28,8 +28,40 @@ def _Note(Text: str) -> ft.Text:
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]:
"""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:
Result = subprocess.run(
["fc-list", ":", "family"], capture_output=True, text=True, timeout=5, env=ChildEnvironment(),
+7 -4
View File
@@ -12,7 +12,7 @@ from typing import Callable, Optional
import flet as ft
from Core import AppConfig, ChildEnvironment, ChipButton, Note, NotificationStyle, Palette
from Core import AppConfig, ChildEnvironment, ChipButton, IsWindows, Note, NotificationStyle, Palette
from Library import ConfValue
@@ -72,7 +72,7 @@ class App:
else to the default app."""
from Query import VideoExtensions
Command = ["xdg-open", str(TargetPath)]
Command = None if IsWindows else ["xdg-open", str(TargetPath)] # Windows: os.startfile below
if TargetPath.suffix.lower() in VideoExtensions:
Player = str(ConfValue("Media", "VideoPlayer", "") or "").strip()
if Player and shutil.which(Player.split()[0]):
@@ -80,8 +80,11 @@ class App:
elif Player:
self.Notify(f"Video player '{Player}' isn't installed; using the default app.", "warning")
try:
subprocess.Popen(Command, env=ChildEnvironment(), start_new_session=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if Command is None:
os.startfile(str(TargetPath)) # the default app
else:
subprocess.Popen(Command, env=ChildEnvironment(), start_new_session=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except OSError as Error:
self.Notify(f"Couldn't open {TargetPath.name}: {Error}", "error")
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB