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.
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))
+1 -1
View File
@@ -30,7 +30,7 @@ Exec=SaraSuite
Icon=SaraSuite
Terminal=false
Categories=System;
StartupWMClass=SaraSuite
StartupWMClass=srhtnin.SaraSuite
DESKTOP
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"
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"
+4 -3
View File
@@ -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 "$@"
+1 -1
View File
@@ -8,4 +8,4 @@ Terminal=false
Categories=System;FileManager;
MimeType=inode/directory;
StartupNotify=true
StartupWMClass={Name}
StartupWMClass=srhtnin.{Name}
+5
View File
@@ -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"
+4 -3
View File
@@ -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 "$@"
+1 -1
View File
@@ -7,4 +7,4 @@ Icon={AppDir}/assets/Icon.png
Terminal=false
Categories=Utility;
StartupNotify=true
StartupWMClass={Name}
StartupWMClass=srhtnin.{Name}
+5
View File
@@ -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"
+4 -3
View File
@@ -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 "$@"
+1 -1
View File
@@ -7,4 +7,4 @@ Icon={AppDir}/assets/Icon.png
Terminal=false
Categories=Utility;Graphics;
StartupNotify=true
StartupWMClass={Name}
StartupWMClass=srhtnin.{Name}