"""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)