A previous install can leave ~/.local/share/applications/vocalinux.desktop (under $XDG_DATA_HOME), ~/.local/bin/vocalinux and ~/.local/bin/vocalinux-gui behind. When their target is gone they start nothing, yet they sit beside Voculinux in launchers, and picking the stale one fails silently.
This adds a check to the one-time legacy import (src/vocalinux/legacy_import.py), which runs after the tray is up.
Detection. A launcher is stale only on positive evidence:
Desktop entry: the Exec= in the [Desktop Entry] group names a target that does not exist. The target is the first word after any env and VAR=... prefix. A bare name is resolved on PATH. An empty or unparseable Exec= is not flagged.
Wrapper files: a dangling symlink, or a #! script whose shebang interpreter or an absolute .../bin/python* or .../bin/vocalinux(-gui) path does not exist. A compiled binary is never read as a wrapper.
A working install is never flagged.
Behaviour:
Runs as a GLib idle callback after the tray is built, next to the model load. It does stat calls and reads small files only: no subprocess, no network.
Logs a warning per stale file and shows one notification, "Old Vocalinux launcher found", naming the files to remove. The notification goes through the existing ui/notifications.py helper.
Records $XDG_DATA_HOME/voculinux/stale-upstream-launcher-notified, so the notice shows once. A clean run writes nothing.
Never deletes or modifies a file.
README gets a "Migrating from a previous Vocalinux install" section with the removal commands. docs/INSTALL.md has no migration section, so it is unchanged.
Fixes#52. Spec context: #16 (legacy import from #18).
Type of change
Bug fix
New feature
Breaking change
Documentation
Refactor
Tests
Packaging / CI
Checklist
Code follows project style (Black, isort, flake8). The critical flake8 gate, black --check, isort --check and mypy src/ are clean.
Documentation updated when user-facing behavior or install steps change
Tests added or updated for the change
Local tests pass (pytest). Not ticked: I ran the related files one at a time (legacy import, rename sweep, app identity, custom dictionary, dbus activation, main, notifications, host process, and the packaging tests that read README). The full suite was not run because of the pre-existing order-dependent gi-mock failures on main.
Conventional commit message style
Screenshots (if applicable)
N/A. The notice is a desktop popup and was not captured.
Notes for reviewers
Seams under test, all offline with a fake HOME, XDG roots and PATH under tmp_path: missing target; env-prefixed Exec into a wrapper whose venv is gone (the reported case); working install left alone; both wrappers named; dangling symlink; compiled binary not read as a script; bare name resolved on PATH; empty, env-only and unparseable Exec; Desktop Action Exec ignored; marker suppresses a repeat; empty XDG_DATA_HOME falls back to ~/.local/share; the idle callback returns False so GLib runs it once; a failing check is swallowed.
The marker is new. The legacy import writes no marker. It is exactly-once only through existence checks. A stale launcher can exist without any legacy config, so this check cannot reuse that existence test. It writes its own marker.
The rename sweep allowlist is untouched. Legacy names in src/ come from LEGACY_APP_DIR_NAME, and the notice text says "previous Vocalinux", which the sweep accepts.
The notification body is not markup-escaped. A home path containing & or < may render oddly on a server that parses markup. Known, not handled here.
## Description
A previous install can leave `~/.local/share/applications/vocalinux.desktop` (under `$XDG_DATA_HOME`), `~/.local/bin/vocalinux` and `~/.local/bin/vocalinux-gui` behind. When their target is gone they start nothing, yet they sit beside Voculinux in launchers, and picking the stale one fails silently.
This adds a check to the one-time legacy import (`src/vocalinux/legacy_import.py`), which runs after the tray is up.
Detection. A launcher is stale only on positive evidence:
- Desktop entry: the `Exec=` in the `[Desktop Entry]` group names a target that does not exist. The target is the first word after any `env` and `VAR=...` prefix. A bare name is resolved on `PATH`. An empty or unparseable `Exec=` is not flagged.
- Wrapper files: a dangling symlink, or a `#!` script whose shebang interpreter or an absolute `.../bin/python*` or `.../bin/vocalinux(-gui)` path does not exist. A compiled binary is never read as a wrapper.
- A working install is never flagged.
Behaviour:
- Runs as a GLib idle callback after the tray is built, next to the model load. It does stat calls and reads small files only: no subprocess, no network.
- Logs a warning per stale file and shows one notification, "Old Vocalinux launcher found", naming the files to remove. The notification goes through the existing `ui/notifications.py` helper.
- Records `$XDG_DATA_HOME/voculinux/stale-upstream-launcher-notified`, so the notice shows once. A clean run writes nothing.
- Never deletes or modifies a file.
README gets a "Migrating from a previous Vocalinux install" section with the removal commands. `docs/INSTALL.md` has no migration section, so it is unchanged.
Fixes #52
## Related issue
Fixes #52. Spec context: #16 (legacy import from #18).
## Type of change
- [x] Bug fix
- [ ] New feature
- [ ] Breaking change
- [x] Documentation
- [ ] Refactor
- [x] Tests
- [ ] Packaging / CI
## Checklist
- [x] Code follows project style (Black, isort, flake8). The critical flake8 gate, `black --check`, `isort --check` and `mypy src/` are clean.
- [x] Documentation updated when user-facing behavior or install steps change
- [x] Tests added or updated for the change
- [ ] Local tests pass (`pytest`). Not ticked: I ran the related files one at a time (legacy import, rename sweep, app identity, custom dictionary, dbus activation, main, notifications, host process, and the packaging tests that read README). The full suite was not run because of the pre-existing order-dependent gi-mock failures on main.
- [x] Conventional commit message style
## Screenshots (if applicable)
N/A. The notice is a desktop popup and was not captured.
## Notes for reviewers
- Seams under test, all offline with a fake HOME, XDG roots and PATH under `tmp_path`: missing target; env-prefixed Exec into a wrapper whose venv is gone (the reported case); working install left alone; both wrappers named; dangling symlink; compiled binary not read as a script; bare name resolved on PATH; empty, env-only and unparseable Exec; Desktop Action Exec ignored; marker suppresses a repeat; empty `XDG_DATA_HOME` falls back to `~/.local/share`; the idle callback returns False so GLib runs it once; a failing check is swallowed.
- The marker is new. The legacy import writes no marker. It is exactly-once only through existence checks. A stale launcher can exist without any legacy config, so this check cannot reuse that existence test. It writes its own marker.
- The rename sweep allowlist is untouched. Legacy names in `src/` come from `LEGACY_APP_DIR_NAME`, and the notice text says "previous Vocalinux", which the sweep accepts.
- The notification body is not markup-escaped. A home path containing `&` or `<` may render oddly on a server that parses markup. Known, not handled here.
A previous Vocalinux install can leave ~/.local/share/applications/vocalinux.desktop
and ~/.local/bin/vocalinux(-gui) behind. When their target or venv is gone they
start nothing, yet they sit next to Voculinux in launchers.
The one-time legacy import now also checks those three paths, after the tray is
up, with stat calls and small reads only. A launcher is stale when its target is
missing, a link is dangling, or a script names a missing interpreter or venv
entry. A working install is never reported. One notice names the stale files and
a marker in the new data directory keeps it to once. No file is removed or
modified.
Fixes#52
xavierk
merged commit 8b842e5712 into main2026-11-06 21:03:29 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Description
A previous install can leave
~/.local/share/applications/vocalinux.desktop(under$XDG_DATA_HOME),~/.local/bin/vocalinuxand~/.local/bin/vocalinux-guibehind. When their target is gone they start nothing, yet they sit beside Voculinux in launchers, and picking the stale one fails silently.This adds a check to the one-time legacy import (
src/vocalinux/legacy_import.py), which runs after the tray is up.Detection. A launcher is stale only on positive evidence:
Exec=in the[Desktop Entry]group names a target that does not exist. The target is the first word after anyenvandVAR=...prefix. A bare name is resolved onPATH. An empty or unparseableExec=is not flagged.#!script whose shebang interpreter or an absolute.../bin/python*or.../bin/vocalinux(-gui)path does not exist. A compiled binary is never read as a wrapper.Behaviour:
ui/notifications.pyhelper.$XDG_DATA_HOME/voculinux/stale-upstream-launcher-notified, so the notice shows once. A clean run writes nothing.README gets a "Migrating from a previous Vocalinux install" section with the removal commands.
docs/INSTALL.mdhas no migration section, so it is unchanged.Fixes #52
Related issue
Fixes #52. Spec context: #16 (legacy import from #18).
Type of change
Checklist
black --check,isort --checkandmypy src/are clean.pytest). Not ticked: I ran the related files one at a time (legacy import, rename sweep, app identity, custom dictionary, dbus activation, main, notifications, host process, and the packaging tests that read README). The full suite was not run because of the pre-existing order-dependent gi-mock failures on main.Screenshots (if applicable)
N/A. The notice is a desktop popup and was not captured.
Notes for reviewers
tmp_path: missing target; env-prefixed Exec into a wrapper whose venv is gone (the reported case); working install left alone; both wrappers named; dangling symlink; compiled binary not read as a script; bare name resolved on PATH; empty, env-only and unparseable Exec; Desktop Action Exec ignored; marker suppresses a repeat; emptyXDG_DATA_HOMEfalls back to~/.local/share; the idle callback returns False so GLib runs it once; a failing check is swallowed.src/come fromLEGACY_APP_DIR_NAME, and the notice text says "previous Vocalinux", which the sweep accepts.&or<may render oddly on a server that parses markup. Known, not handled here.