mirror of
https://git.srhtnin.garden/sarah/SaraSuite.git
synced 2026-10-14 02:28:11 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70c9730017 | ||
|
|
2061babb43 | ||
|
|
21ae6379ac | ||
|
|
63e06eea40 | ||
|
|
5890b52da6 |
No files matched your search
@@ -24,7 +24,7 @@ import tomlkit
|
||||
AppName = "SaraSuite"
|
||||
# Compared with the latest Gitea release to offer self-updates (SelfUpdate.py).
|
||||
# Bump it, then run Packaging/Release.py, to publish a new SaraSuite.
|
||||
AppVersion = "1.2.2"
|
||||
AppVersion = "1.2.4"
|
||||
IsFrozen = bool(getattr(sys, "frozen", False)) # running from the AppImage / .exe
|
||||
IsWindows = sys.platform == "win32"
|
||||
Platform = "Windows" if IsWindows else "Linux" # matches manifests' Platforms / Platform
|
||||
@@ -109,11 +109,50 @@ _ThemeRoleMap = {
|
||||
}
|
||||
|
||||
|
||||
# The themes every SaraSuite program seeds into ThemesDir if they're missing
|
||||
# (same values as SaraFem's and SaraView's), so whichever program is installed
|
||||
# first, the folder and these three are there to choose from.
|
||||
BuiltinThemes: dict[str, dict[str, str]] = {
|
||||
"CatppuccinMocha": {
|
||||
"Background": "#1e1e2e", "Surface": "#181825", "SurfaceAlt": "#313244", "Border": "#45475a",
|
||||
"Text": "#cdd6f4", "TextMuted": "#6c7086", "Black": "#11111b", "White": "#cdd6f4",
|
||||
"Red": "#f38ba8", "Orange": "#fab387", "Yellow": "#f9e2af", "Green": "#a6e3a1",
|
||||
"Blue": "#89b4fa", "Purple": "#cba6f7",
|
||||
},
|
||||
"Forest": {
|
||||
"Background": "#1b2420", "Surface": "#141b17", "SurfaceAlt": "#243029", "Border": "#334539",
|
||||
"Text": "#d6e5d0", "TextMuted": "#7c9184", "Black": "#0d120f", "White": "#eef5ea",
|
||||
"Red": "#e0685a", "Orange": "#e0a458", "Yellow": "#e0c458", "Green": "#7fb069",
|
||||
"Blue": "#5c9e8f", "Purple": "#9b7fb0",
|
||||
},
|
||||
"Gruvbox": {
|
||||
"Background": "#282828", "Surface": "#1d2021", "SurfaceAlt": "#3c3836", "Border": "#504945",
|
||||
"Text": "#ebdbb2", "TextMuted": "#a89984", "Black": "#1d2021", "White": "#fbf1c7",
|
||||
"Red": "#fb4934", "Orange": "#fe8019", "Yellow": "#fabd2f", "Green": "#b8bb26",
|
||||
"Blue": "#83a598", "Purple": "#d3869b",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def EnsureSharedThemes() -> None:
|
||||
"""Create ThemesDir with the built-in themes that aren't there yet
|
||||
(never overwriting one: it may have been edited)."""
|
||||
try:
|
||||
ThemesDir.mkdir(parents=True, exist_ok=True)
|
||||
for Name, RoleValues in BuiltinThemes.items():
|
||||
ThemeFile = ThemesDir / f"{Name}.toml"
|
||||
if not ThemeFile.exists():
|
||||
ThemeFile.write_text(tomlkit.dumps({"Palette": RoleValues}))
|
||||
except OSError:
|
||||
pass # read-only or full: the built-in Catppuccin Mocha values still apply
|
||||
|
||||
|
||||
def ApplyTheme(ThemeName: str) -> None:
|
||||
"""Mutate Palette in place from ~/.config/SaraSuite/Themes/<ThemeName>.toml.
|
||||
Falls back to the built-in Catppuccin Mocha values if the file is missing
|
||||
(SaraFem seeds that directory; SaraSuite only reads it). Must run before
|
||||
any control reads Palette.X."""
|
||||
Seeds the built-in themes first (EnsureSharedThemes); a missing file keeps
|
||||
the built-in Catppuccin Mocha values. Must run before any control reads
|
||||
Palette.X."""
|
||||
EnsureSharedThemes()
|
||||
ThemeFile = ThemesDir / f"{ThemeName}.toml"
|
||||
if not ThemeFile.exists():
|
||||
return
|
||||
|
||||
@@ -38,6 +38,23 @@ else:
|
||||
_MenuLabel = "Start menu" if IsWindows else "app menu"
|
||||
|
||||
|
||||
def _KeepFallbackLibraries() -> None:
|
||||
"""Linux AppImage: copy the bundled libsecret (and what it needs) to
|
||||
~/.local/share/SaraSuite/Libraries, for FletIdentity.py to give the Flet
|
||||
client of every installed program on systems without it."""
|
||||
Bundled = Path(getattr(sys, "_MEIPASS", "")) / "FallbackLibs"
|
||||
if IsWindows or not Bundled.is_dir():
|
||||
return
|
||||
Kept = DataDir / "Libraries"
|
||||
try:
|
||||
Kept.mkdir(parents=True, exist_ok=True)
|
||||
for Library in Bundled.iterdir():
|
||||
if not (Kept / Library.name).exists() or (Kept / Library.name).stat().st_size != Library.stat().st_size:
|
||||
shutil.copy2(Library, Kept / Library.name)
|
||||
except OSError:
|
||||
pass # only a fallback
|
||||
|
||||
|
||||
def _LoadFletIdentity():
|
||||
# Lives in Programs/ (bundled as data) because installed programs get a
|
||||
# copy of the same file next to them.
|
||||
@@ -324,6 +341,7 @@ def Main(Page: ft.Page) -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_KeepFallbackLibraries() # also before a command (an install from the command line)
|
||||
if len(sys.argv) > 1:
|
||||
if IsWindows and getattr(sys, "frozen", False):
|
||||
# The .exe is built windowed (no console of its own); write the
|
||||
|
||||
@@ -18,8 +18,17 @@ podman run --rm --security-opt label=disable -v "$PWD:/src" -w /src \
|
||||
quay.io/pypa/manylinux_2_28_x86_64 bash -euo pipefail -c '
|
||||
uv venv --quiet --allow-existing --managed-python --python 3.14 build/Container/venv
|
||||
uv pip install --quiet --python build/Container/venv -r requirements.txt flet-desktop pyinstaller
|
||||
# The Flet window needs libsecret, which some systems lack: bundled as a
|
||||
# fallback (RestoreLibraryPath.py uses only what the system lacks).
|
||||
dnf -y -q install libsecret >/dev/null
|
||||
mkdir -p build/FallbackLibs
|
||||
for Library in libsecret-1.so.0 libgcrypt.so.20 libgpg-error.so.0; do cp -L /usr/lib64/$Library build/FallbackLibs/; done
|
||||
# Each finds the others beside it, wherever they are put (a client lib folder).
|
||||
patchelf --set-rpath "\$ORIGIN" build/FallbackLibs/libsecret-1.so.0 build/FallbackLibs/libgcrypt.so.20
|
||||
build/Container/venv/bin/pyinstaller --noconfirm --clean --log-level WARN \
|
||||
--onefile --windowed --name SaraSuite --collect-data flet \
|
||||
--runtime-hook "$PWD/Packaging/RestoreLibraryPath.py" \
|
||||
--add-data "$PWD/build/FallbackLibs:FallbackLibs" \
|
||||
--add-data "$PWD/assets:assets" --add-data "$PWD/Programs:Programs" \
|
||||
--distpath build/PyInstaller/dist --workpath build/PyInstaller/work --specpath build/PyInstaller \
|
||||
"$PWD/Main.py"'
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# PyInstaller runtime hook. The frozen app runs with LD_LIBRARY_PATH pointing
|
||||
# at its bundled libraries, and programs it starts inherit that: Flet's
|
||||
# desktop client then loads the bundled (older) libstdc++ instead of the
|
||||
# system's and won't start ("GLIBCXX_3.4.29 not found"). Give them the
|
||||
# original value back; the app itself has already loaded what it needs.
|
||||
#
|
||||
# Flet's window also needs libsecret, which isn't part of GTK and some
|
||||
# systems don't install ("libsecret-1.so.0: cannot open shared object file").
|
||||
# The build bundles it in FallbackLibs, with libgcrypt and libgpg-error, which
|
||||
# it needs; the ones this system lacks (only those) are offered to what the
|
||||
# app starts, after the original path.
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def SystemHas(Name):
|
||||
"""Whether the system itself has a library: asked of its linker cache
|
||||
and standard folders, not dlopen, which would also find this bundle's
|
||||
own copies (PyInstaller adds what bundled libraries need)."""
|
||||
for Folder in ("/usr/lib64", "/usr/lib", "/lib64", "/lib", "/usr/lib/x86_64-linux-gnu", "/lib/x86_64-linux-gnu"):
|
||||
if os.path.exists(os.path.join(Folder, Name)):
|
||||
return True
|
||||
try:
|
||||
Cache = subprocess.run(["ldconfig", "-p"], capture_output=True, text=True, timeout=10,
|
||||
env={"PATH": "/usr/sbin:/usr/bin:/sbin:/bin"}).stdout
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
return any(Line.strip().startswith(Name + " ") for Line in Cache.splitlines())
|
||||
|
||||
Paths = [os.environ["LD_LIBRARY_PATH_ORIG"]] if os.environ.get("LD_LIBRARY_PATH_ORIG") else []
|
||||
|
||||
Bundle = getattr(sys, "_MEIPASS", "")
|
||||
Fallback = os.path.join(Bundle, "FallbackLibs")
|
||||
if sys.platform.startswith("linux") and os.path.isdir(Fallback):
|
||||
InUse = os.path.join(Bundle, "FallbackLibsInUse")
|
||||
os.makedirs(InUse, exist_ok=True)
|
||||
for Name in ("libgpg-error.so.0", "libgcrypt.so.20", "libsecret-1.so.0"):
|
||||
if not SystemHas(Name) and not os.path.lexists(os.path.join(InUse, Name)):
|
||||
os.symlink(os.path.join(Fallback, Name), os.path.join(InUse, Name))
|
||||
if os.listdir(InUse):
|
||||
Paths.append(InUse)
|
||||
|
||||
if Paths:
|
||||
os.environ["LD_LIBRARY_PATH"] = os.pathsep.join(Paths)
|
||||
else:
|
||||
os.environ.pop("LD_LIBRARY_PATH", None)
|
||||
@@ -18,13 +18,51 @@ Used two ways:
|
||||
case Flet just uses its normal client.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
OriginalId = b"com.appveyor.flet"
|
||||
# Flet's window needs libsecret, which isn't part of GTK and some systems don't
|
||||
# install. SaraSuite keeps copies of it and what it needs (from its AppImage)
|
||||
# here; AddMissingLibraries puts the ones this system lacks in a fallback-libs
|
||||
# folder beside the client copy, which the program's Launch.sh adds to
|
||||
# LD_LIBRARY_PATH (a plugin loads libsecret, so the client's own lib folder,
|
||||
# its RUNPATH, wouldn't be searched for it).
|
||||
LibrariesDir = Path(os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share") / "SaraSuite" / "Libraries"
|
||||
FallbackLibraries = ("libgpg-error.so.0", "libgcrypt.so.20", "libsecret-1.so.0")
|
||||
CopiesDir = Path.home() / ".cache" / "SaraSuite" / "FletClient"
|
||||
|
||||
|
||||
def _SystemHas(Name: str) -> bool:
|
||||
"""Whether the system has a library (its standard folders and linker
|
||||
cache; the same check as the AppImages' RestoreLibraryPath.py)."""
|
||||
import subprocess
|
||||
|
||||
for Folder in ("/usr/lib64", "/usr/lib", "/lib64", "/lib", "/usr/lib/x86_64-linux-gnu", "/lib/x86_64-linux-gnu"):
|
||||
if (Path(Folder) / Name).exists():
|
||||
return True
|
||||
try:
|
||||
Cache = subprocess.run(["ldconfig", "-p"], capture_output=True, text=True, timeout=10,
|
||||
env={"PATH": "/usr/sbin:/usr/bin:/sbin:/bin"}).stdout
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
return any(Line.strip().startswith(Name + " ") for Line in Cache.splitlines())
|
||||
|
||||
|
||||
def AddMissingLibraries(Folder: Path) -> None:
|
||||
"""Copies SaraSuite's fallback libraries the system doesn't have into
|
||||
Folder (created only if one is needed)."""
|
||||
if not sys.platform.startswith("linux") or not LibrariesDir.is_dir():
|
||||
return
|
||||
for Name in FallbackLibraries:
|
||||
Source = LibrariesDir / Name
|
||||
if Source.is_file() and not (Folder / Name).exists() and not _SystemHas(Name):
|
||||
Folder.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(Source, Folder / Name)
|
||||
|
||||
|
||||
def ClientFor(AppId: str) -> str:
|
||||
"""Folder of a client copy whose app ID is AppId ("" if not possible).
|
||||
AppId needs a dot (a GTK rule) and at most 17 characters, the length of
|
||||
@@ -45,6 +83,7 @@ def ClientFor(AppId: str) -> str:
|
||||
if Binary.count(b"\x00" + OriginalId + b"\x00") != 1:
|
||||
return "" # not the client layout this was written for
|
||||
|
||||
AddMissingLibraries(Copy.parent / "fallback-libs")
|
||||
Target = Copy / "flet"
|
||||
if not (Target.exists() and Target.read_bytes() == Patched):
|
||||
Copy.mkdir(parents=True, exist_ok=True)
|
||||
@@ -64,4 +103,16 @@ def ClientFor(AppId: str) -> str:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(ClientFor(sys.argv[1]) if len(sys.argv) > 1 else "")
|
||||
# Only the folder goes to stdout, which the launcher reads: Flet's own
|
||||
# messages ("Preparing Flet ... for the first use", written straight to
|
||||
# the file descriptor) go to stderr.
|
||||
sys.stdout.flush()
|
||||
Stdout = os.dup(1)
|
||||
os.dup2(2, 1)
|
||||
try:
|
||||
Folder = ClientFor(sys.argv[1]) if len(sys.argv) > 1 else ""
|
||||
finally:
|
||||
sys.stdout.flush()
|
||||
os.dup2(Stdout, 1)
|
||||
os.close(Stdout)
|
||||
print(Folder)
|
||||
@@ -4,5 +4,9 @@
|
||||
# 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}")"
|
||||
# Libraries Flet's window needs that this system lacks (libsecret), from
|
||||
# SaraSuite: FletIdentity.py puts them beside the client copy.
|
||||
Fallback="$FLET_VIEW_PATH/../fallback-libs"
|
||||
[ -n "$FLET_VIEW_PATH" ] && [ -d "$Fallback" ] && export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$Fallback"
|
||||
cd "{AppDir}" || exit 1
|
||||
exec "{Python}" Main.py "$@"
|
||||
@@ -4,5 +4,9 @@
|
||||
# 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}")"
|
||||
# Libraries Flet's window needs that this system lacks (libsecret), from
|
||||
# SaraSuite: FletIdentity.py puts them beside the client copy.
|
||||
Fallback="$FLET_VIEW_PATH/../fallback-libs"
|
||||
[ -n "$FLET_VIEW_PATH" ] && [ -d "$Fallback" ] && export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$Fallback"
|
||||
cd "{AppDir}" || exit 1
|
||||
exec "{Python}" Main.py "$@"
|
||||
@@ -4,5 +4,9 @@
|
||||
# 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}")"
|
||||
# Libraries Flet's window needs that this system lacks (libsecret), from
|
||||
# SaraSuite: FletIdentity.py puts them beside the client copy.
|
||||
Fallback="$FLET_VIEW_PATH/../fallback-libs"
|
||||
[ -n "$FLET_VIEW_PATH" ] && [ -d "$Fallback" ] && export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$Fallback"
|
||||
cd "{AppDir}" || exit 1
|
||||
exec "{Python}" Main.py "$@"
|
||||
@@ -6,5 +6,9 @@ export FLET_DESKTOP_FLAVOR=full
|
||||
# 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}")"
|
||||
# Libraries Flet's window needs that this system lacks (libsecret), from
|
||||
# SaraSuite: FletIdentity.py puts them beside the client copy.
|
||||
Fallback="$FLET_VIEW_PATH/../fallback-libs"
|
||||
[ -n "$FLET_VIEW_PATH" ] && [ -d "$Fallback" ] && export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$Fallback"
|
||||
cd "{AppDir}" || exit 1
|
||||
exec "{Python}" Main.py "$@"
|
||||
@@ -4,5 +4,9 @@
|
||||
# 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}")"
|
||||
# Libraries Flet's window needs that this system lacks (libsecret), from
|
||||
# SaraSuite: FletIdentity.py puts them beside the client copy.
|
||||
Fallback="$FLET_VIEW_PATH/../fallback-libs"
|
||||
[ -n "$FLET_VIEW_PATH" ] && [ -d "$Fallback" ] && export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}$Fallback"
|
||||
cd "{AppDir}/Client" || exit 1
|
||||
exec "{Python}" Main.py "$@"
|
||||
+4
-3
@@ -8,8 +8,8 @@ from typing import Callable
|
||||
import flet as ft
|
||||
|
||||
from Core import (
|
||||
AppConfig, BundledFontFamily, ChildEnvironment, ChipButtonStyle, IsWindows, LoadSharedSettings, Palette,
|
||||
SharedSettings, ThemesDir,
|
||||
AppConfig, BundledFontFamily, ChildEnvironment, ChipButtonStyle, EnsureSharedThemes, IsWindows,
|
||||
LoadSharedSettings, Palette, SharedSettings, ThemesDir,
|
||||
)
|
||||
from Installer import DefaultAnswers, DefaultOptions
|
||||
from Manifests import Program
|
||||
@@ -25,8 +25,9 @@ def _Note(Text: str) -> ft.Text:
|
||||
|
||||
|
||||
def _Themes() -> list[str]:
|
||||
EnsureSharedThemes()
|
||||
Names = {Path.stem for Path in ThemesDir.glob("*.toml")} if ThemesDir.is_dir() else set()
|
||||
Names.add("CatppuccinMocha") # built in, even before SaraFem seeds the folder
|
||||
Names.add("CatppuccinMocha") # built in, even where the folder can't be written
|
||||
return sorted(Names)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user