Per-program Flet client app IDs so the dock shows the right icons

This commit is contained in:
sarah committed 2026-10-08 22:43:34 +02:00
1 parent 19bbad33a4
commit 56f10885de
12 files changed
+114 -18

No files matched your search

+16 -5
View File
@@ -1,6 +1,7 @@
"""SaraSuite entry point: installs, updates and removes Sara programs. """SaraSuite entry point: installs, updates and removes Sara programs.
With arguments it runs the command-line mode instead (see Cli.py).""" With arguments it runs the command-line mode instead (see Cli.py)."""
import importlib.util
import os import os
import shutil import shutil
import sys import sys
@@ -11,7 +12,7 @@ import flet as ft
from Core import ( from Core import (
AppName, ApplyAppearance, AssetsDir, BuildTheme, BundledFontFamily, ChipButtonStyle, DataDir, AppName, ApplyAppearance, AssetsDir, BuildTheme, BundledFontFamily, ChipButtonStyle, DataDir,
DataHome, EffectiveSettings, LoadConfig, Palette, DataHome, EffectiveSettings, LoadConfig, ManifestsDir, Palette,
) )
from Dialogs import ShowInstallDialog, ShowOptionsDialog, ShowRemoveDialog, ShowUpdateDialog from Dialogs import ShowInstallDialog, ShowOptionsDialog, ShowRemoveDialog, ShowUpdateDialog
from Installer import Apply, Fetch, FetchCommit, GetStatus, PlanConfigs, PlanRemoval, Remove, SourceVersion from Installer import Apply, Fetch, FetchCommit, GetStatus, PlanConfigs, PlanRemoval, Remove, SourceVersion
@@ -27,6 +28,15 @@ _MenuEntry = DataHome / "applications" / "SaraSuite.desktop"
_MenuIcon = DataDir / "SaraSuite.png" _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: def _SetMenuEntry(Enabled: bool) -> None:
if not Enabled: if not Enabled:
_MenuEntry.unlink(missing_ok=True) _MenuEntry.unlink(missing_ok=True)
@@ -39,7 +49,7 @@ def _SetMenuEntry(Enabled: bool) -> None:
"[Desktop Entry]\nType=Application\nName=SaraSuite\n" "[Desktop Entry]\nType=Application\nName=SaraSuite\n"
"Comment=Install, update and remove Sara programs\n" "Comment=Install, update and remove Sara programs\n"
f"Exec={_AppImage}\nIcon={_MenuIcon}\nTerminal=false\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 from Cli import Run
sys.exit(Run(sys.argv[1:])) sys.exit(Run(sys.argv[1:]))
# The window's app ID, so the desktop matches it to SaraSuite.desktop # A Flet client copy with the window app ID srhtnin.SaraSuite, so the
# (icon, name) instead of showing it as Flet's generic "com.appveyor.flet". # desktop matches the window to SaraSuite.desktop (StartupWMClass) instead
os.environ.setdefault("FLET_APP_ID", AppName) # 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)) ft.run(Main, assets_dir=str(AssetsDir))
+1 -1
View File
@@ -30,7 +30,7 @@ Exec=SaraSuite
Icon=SaraSuite Icon=SaraSuite
Terminal=false Terminal=false
Categories=System; Categories=System;
StartupWMClass=SaraSuite StartupWMClass=srhtnin.SaraSuite
DESKTOP DESKTOP
mkdir -p dist mkdir -p dist
+67
View File
@@ -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 "")
+5
View File
@@ -65,3 +65,8 @@ Note = "thumbnail cache"
Path = "{ConfigHome}/SaraFem" Path = "{ConfigHome}/SaraFem"
Kind = "Config" Kind = "Config"
Note = "old pre-SaraSuite config location" Note = "old pre-SaraSuite config location"
# Shared with the other Flet programs; see Programs/FletIdentity.py.
[[Templates]]
Source = "../../FletIdentity.py"
Target = "{ProgramDir}/FletIdentity.py"
+4 -3
View File
@@ -1,7 +1,8 @@
#!/bin/sh #!/bin/sh
# Written by SaraSuite. Runs from the app folder so Flet finds assets/. # 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) # A Flet client copy with the window app ID srhtnin.{Name}, so the desktop
# instead of showing every Flet program as "com.appveyor.flet". # matches the window to {Name}.desktop (StartupWMClass) instead of showing it
export FLET_APP_ID="{Name}" # 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 cd "{AppDir}" || exit 1
exec "{Python}" Main.py "$@" exec "{Python}" Main.py "$@"
+1 -1
View File
@@ -8,4 +8,4 @@ Terminal=false
Categories=System;FileManager; Categories=System;FileManager;
MimeType=inode/directory; MimeType=inode/directory;
StartupNotify=true StartupNotify=true
StartupWMClass={Name} StartupWMClass=srhtnin.{Name}
+5
View File
@@ -25,3 +25,8 @@ Target = "{ApplicationsDir}/SaraSort.desktop"
Path = "{ConfigDir}" Path = "{ConfigDir}"
Kind = "Config" Kind = "Config"
Note = "SaraSort's settings, flowchart layout and your templates" 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"
+4 -3
View File
@@ -1,7 +1,8 @@
#!/bin/sh #!/bin/sh
# Written by SaraSuite. Runs from the app folder so Flet finds assets/. # 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) # A Flet client copy with the window app ID srhtnin.{Name}, so the desktop
# instead of showing every Flet program as "com.appveyor.flet". # matches the window to {Name}.desktop (StartupWMClass) instead of showing it
export FLET_APP_ID="{Name}" # 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 cd "{AppDir}" || exit 1
exec "{Python}" Main.py "$@" exec "{Python}" Main.py "$@"
+1 -1
View File
@@ -7,4 +7,4 @@ Icon={AppDir}/assets/Icon.png
Terminal=false Terminal=false
Categories=Utility; Categories=Utility;
StartupNotify=true StartupNotify=true
StartupWMClass={Name} StartupWMClass=srhtnin.{Name}
+5
View File
@@ -35,3 +35,8 @@ Target = "{ApplicationsDir}/SaraTag.desktop"
Path = "{ConfigDir}" Path = "{ConfigDir}"
Kind = "Config" Kind = "Config"
Note = "SaraTag's settings, Records, Libraries, history and Lexicons (Books next to your files are never touched)" 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"
+4 -3
View File
@@ -1,7 +1,8 @@
#!/bin/sh #!/bin/sh
# Written by SaraSuite. Runs from the app folder so Flet finds assets/. # 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) # A Flet client copy with the window app ID srhtnin.{Name}, so the desktop
# instead of showing every Flet program as "com.appveyor.flet". # matches the window to {Name}.desktop (StartupWMClass) instead of showing it
export FLET_APP_ID="{Name}" # 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 cd "{AppDir}" || exit 1
exec "{Python}" Main.py "$@" exec "{Python}" Main.py "$@"
+1 -1
View File
@@ -7,4 +7,4 @@ Icon={AppDir}/assets/Icon.png
Terminal=false Terminal=false
Categories=Utility;Graphics; Categories=Utility;Graphics;
StartupNotify=true StartupNotify=true
StartupWMClass={Name} StartupWMClass=srhtnin.{Name}