From 56f10885de899fbff0aff006447f8a494312cea4 Mon Sep 17 00:00:00 2001 From: SRHTNIN Date: Thu, 8 Oct 2026 22:43:34 +0200 Subject: [PATCH] Per-program Flet client app IDs so the dock shows the right icons --- Main.py | 21 ++++-- Packaging/BuildAppImage.sh | 2 +- Programs/FletIdentity.py | 67 ++++++++++++++++++++ Programs/SaraFem/Manifest.toml | 5 ++ Programs/SaraFem/Templates/Launch.sh | 7 +- Programs/SaraFem/Templates/SaraFem.desktop | 2 +- Programs/SaraSort/Manifest.toml | 5 ++ Programs/SaraSort/Templates/Launch.sh | 7 +- Programs/SaraSort/Templates/SaraSort.desktop | 2 +- Programs/SaraTag/Manifest.toml | 5 ++ Programs/SaraTag/Templates/Launch.sh | 7 +- Programs/SaraTag/Templates/SaraTag.desktop | 2 +- 12 files changed, 114 insertions(+), 18 deletions(-) create mode 100644 Programs/FletIdentity.py diff --git a/Main.py b/Main.py index b3a121a..dbc1021 100644 --- a/Main.py +++ b/Main.py @@ -1,6 +1,7 @@ """SaraSuite entry point: installs, updates and removes Sara programs. With arguments it runs the command-line mode instead (see Cli.py).""" +import importlib.util import os import shutil import sys @@ -11,7 +12,7 @@ import flet as ft from Core import ( AppName, ApplyAppearance, AssetsDir, BuildTheme, BundledFontFamily, ChipButtonStyle, DataDir, - DataHome, EffectiveSettings, LoadConfig, Palette, + DataHome, EffectiveSettings, LoadConfig, ManifestsDir, Palette, ) from Dialogs import ShowInstallDialog, ShowOptionsDialog, ShowRemoveDialog, ShowUpdateDialog from Installer import Apply, Fetch, FetchCommit, GetStatus, PlanConfigs, PlanRemoval, Remove, SourceVersion @@ -27,6 +28,15 @@ _MenuEntry = DataHome / "applications" / "SaraSuite.desktop" _MenuIcon = DataDir / "SaraSuite.png" +def _LoadFletIdentity(): + # Lives in Programs/ (bundled as data) because installed programs get a + # copy of the same file next to them. + Spec = importlib.util.spec_from_file_location("FletIdentity", ManifestsDir / "FletIdentity.py") + Module = importlib.util.module_from_spec(Spec) + Spec.loader.exec_module(Module) + return Module + + def _SetMenuEntry(Enabled: bool) -> None: if not Enabled: _MenuEntry.unlink(missing_ok=True) @@ -39,7 +49,7 @@ def _SetMenuEntry(Enabled: bool) -> None: "[Desktop Entry]\nType=Application\nName=SaraSuite\n" "Comment=Install, update and remove Sara programs\n" f"Exec={_AppImage}\nIcon={_MenuIcon}\nTerminal=false\n" - f"Categories=System;\nStartupNotify=true\nStartupWMClass={AppName}\n" + f"Categories=System;\nStartupNotify=true\nStartupWMClass=srhtnin.{AppName}\n" ) @@ -234,7 +244,8 @@ if __name__ == "__main__": from Cli import Run sys.exit(Run(sys.argv[1:])) - # The window's app ID, so the desktop matches it to SaraSuite.desktop - # (icon, name) instead of showing it as Flet's generic "com.appveyor.flet". - os.environ.setdefault("FLET_APP_ID", AppName) + # A Flet client copy with the window app ID srhtnin.SaraSuite, so the + # desktop matches the window to SaraSuite.desktop (StartupWMClass) instead + # of showing it as "com.appveyor.flet". See Programs/FletIdentity.py. + os.environ.setdefault("FLET_VIEW_PATH", _LoadFletIdentity().ClientFor(f"srhtnin.{AppName}")) ft.run(Main, assets_dir=str(AssetsDir)) diff --git a/Packaging/BuildAppImage.sh b/Packaging/BuildAppImage.sh index ce8ff37..8876452 100755 --- a/Packaging/BuildAppImage.sh +++ b/Packaging/BuildAppImage.sh @@ -30,7 +30,7 @@ Exec=SaraSuite Icon=SaraSuite Terminal=false Categories=System; -StartupWMClass=SaraSuite +StartupWMClass=srhtnin.SaraSuite DESKTOP mkdir -p dist diff --git a/Programs/FletIdentity.py b/Programs/FletIdentity.py new file mode 100644 index 0000000..ac95711 --- /dev/null +++ b/Programs/FletIdentity.py @@ -0,0 +1,67 @@ +"""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 sys +from pathlib import Path + +OriginalId = b"com.appveyor.flet" +CopiesDir = Path.home() / ".cache" / "SaraSuite" / "FletClient" + + +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 + + 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__": + print(ClientFor(sys.argv[1]) if len(sys.argv) > 1 else "") diff --git a/Programs/SaraFem/Manifest.toml b/Programs/SaraFem/Manifest.toml index 79f5ae9..131d152 100644 --- a/Programs/SaraFem/Manifest.toml +++ b/Programs/SaraFem/Manifest.toml @@ -65,3 +65,8 @@ Note = "thumbnail cache" Path = "{ConfigHome}/SaraFem" Kind = "Config" Note = "old pre-SaraSuite config location" + +# Shared with the other Flet programs; see Programs/FletIdentity.py. +[[Templates]] +Source = "../../FletIdentity.py" +Target = "{ProgramDir}/FletIdentity.py" diff --git a/Programs/SaraFem/Templates/Launch.sh b/Programs/SaraFem/Templates/Launch.sh index ad4b090..70e1413 100644 --- a/Programs/SaraFem/Templates/Launch.sh +++ b/Programs/SaraFem/Templates/Launch.sh @@ -1,7 +1,8 @@ #!/bin/sh # Written by SaraSuite. Runs from the app folder so Flet finds assets/. -# The window's app ID, so the desktop matches it to {Name}.desktop (icon, name) -# instead of showing every Flet program as "com.appveyor.flet". -export FLET_APP_ID="{Name}" +# 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 "$@" diff --git a/Programs/SaraFem/Templates/SaraFem.desktop b/Programs/SaraFem/Templates/SaraFem.desktop index ef68d1d..133f8b5 100644 --- a/Programs/SaraFem/Templates/SaraFem.desktop +++ b/Programs/SaraFem/Templates/SaraFem.desktop @@ -8,4 +8,4 @@ Terminal=false Categories=System;FileManager; MimeType=inode/directory; StartupNotify=true -StartupWMClass={Name} +StartupWMClass=srhtnin.{Name} diff --git a/Programs/SaraSort/Manifest.toml b/Programs/SaraSort/Manifest.toml index 52aa6d1..69ae4cc 100644 --- a/Programs/SaraSort/Manifest.toml +++ b/Programs/SaraSort/Manifest.toml @@ -25,3 +25,8 @@ Target = "{ApplicationsDir}/SaraSort.desktop" Path = "{ConfigDir}" Kind = "Config" Note = "SaraSort's settings, flowchart layout and your templates" + +# Shared with the other Flet programs; see Programs/FletIdentity.py. +[[Templates]] +Source = "../../FletIdentity.py" +Target = "{ProgramDir}/FletIdentity.py" diff --git a/Programs/SaraSort/Templates/Launch.sh b/Programs/SaraSort/Templates/Launch.sh index ad4b090..70e1413 100755 --- a/Programs/SaraSort/Templates/Launch.sh +++ b/Programs/SaraSort/Templates/Launch.sh @@ -1,7 +1,8 @@ #!/bin/sh # Written by SaraSuite. Runs from the app folder so Flet finds assets/. -# The window's app ID, so the desktop matches it to {Name}.desktop (icon, name) -# instead of showing every Flet program as "com.appveyor.flet". -export FLET_APP_ID="{Name}" +# 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 "$@" diff --git a/Programs/SaraSort/Templates/SaraSort.desktop b/Programs/SaraSort/Templates/SaraSort.desktop index 876bce1..94d5e7c 100644 --- a/Programs/SaraSort/Templates/SaraSort.desktop +++ b/Programs/SaraSort/Templates/SaraSort.desktop @@ -7,4 +7,4 @@ Icon={AppDir}/assets/Icon.png Terminal=false Categories=Utility; StartupNotify=true -StartupWMClass={Name} +StartupWMClass=srhtnin.{Name} diff --git a/Programs/SaraTag/Manifest.toml b/Programs/SaraTag/Manifest.toml index 0b1e2dc..5acc438 100644 --- a/Programs/SaraTag/Manifest.toml +++ b/Programs/SaraTag/Manifest.toml @@ -35,3 +35,8 @@ Target = "{ApplicationsDir}/SaraTag.desktop" 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]] +Source = "../../FletIdentity.py" +Target = "{ProgramDir}/FletIdentity.py" diff --git a/Programs/SaraTag/Templates/Launch.sh b/Programs/SaraTag/Templates/Launch.sh index ad4b090..70e1413 100644 --- a/Programs/SaraTag/Templates/Launch.sh +++ b/Programs/SaraTag/Templates/Launch.sh @@ -1,7 +1,8 @@ #!/bin/sh # Written by SaraSuite. Runs from the app folder so Flet finds assets/. -# The window's app ID, so the desktop matches it to {Name}.desktop (icon, name) -# instead of showing every Flet program as "com.appveyor.flet". -export FLET_APP_ID="{Name}" +# 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 "$@" diff --git a/Programs/SaraTag/Templates/SaraTag.desktop b/Programs/SaraTag/Templates/SaraTag.desktop index 90399aa..0dd4ebf 100644 --- a/Programs/SaraTag/Templates/SaraTag.desktop +++ b/Programs/SaraTag/Templates/SaraTag.desktop @@ -7,4 +7,4 @@ Icon={AppDir}/assets/Icon.png Terminal=false Categories=Utility;Graphics; StartupNotify=true -StartupWMClass={Name} +StartupWMClass=srhtnin.{Name}