Files
SaraSuite/Programs/FletIdentity.py
T
2026-10-13 18:45:33 +02:00

119 lines
5.1 KiB
Python

"""Give a Flet program's window its own app ID on Linux, so the desktop can
match it to the program's .desktop entry (icon, name) instead of showing
every Flet program as "com.appveyor.flet" with a generic icon.
The Flet client's ID is a string built into its small launcher binary
(`flet`, ~24 KB); the client sets it as both its GTK application ID and its
process name, so Flet's own FLET_APP_ID (which only changes argv[0]) can't
override it, on Wayland or X11. Instead this makes a per-program copy of the
client in ~/.cache/SaraSuite/FletClient/ with that string replaced; the big
parts (lib/, data/, share/) are symlinks to the original, so a copy costs
~24 KB. Pointing FLET_VIEW_PATH at it makes Flet use the copy.
Used two ways:
- SaraSuite imports it and calls ClientFor() before ft.run.
- Installed programs' launchers run it with their own venv's python:
export FLET_VIEW_PATH="$(python FletIdentity.py srhtnin.SaraFem)"
It prints the copy's folder, or nothing if anything goes wrong, in which
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
the original it replaces in place."""
try:
Encoded = AppId.encode("ascii")
if "." not in AppId or not 0 < len(Encoded) <= len(OriginalId):
return ""
from flet_desktop import ensure_client_cached # the program's own Flet version
Original = Path(ensure_client_cached()) / "flet"
Binary = (Original / "flet").read_bytes()
Copy = CopiesDir / f"{Original.parent.name}-{AppId}" / "flet"
Patched = Binary.replace(
b"\x00" + OriginalId + b"\x00",
b"\x00" + Encoded.ljust(len(OriginalId), b"\x00") + b"\x00",
)
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)
for Part in ("lib", "data", "share"):
Link = Copy / Part
if Link.is_symlink() or Link.exists():
Link.unlink()
if (Original / Part).exists():
Link.symlink_to(Original / Part)
Temporary = Target.with_suffix(".new")
Temporary.write_bytes(Patched)
Temporary.chmod(0o755)
Temporary.replace(Target)
return str(Copy)
except Exception:
return ""
if __name__ == "__main__":
# 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)