feat(linux): App-section UX — install-for-all-users, start-menu icon, complete uninstall (beta.49)#315
Merged
Merged
Conversation
…cleanup (beta.49) Route the data-root keep/wipe to the group that OWNS it, fixing a keep violation: for a system service the data root is /var/opt (config.json + logs live there), so the old unconditional `rm -rf /var/opt` in the privileged group wiped preserved data even on keep=true. Now: - Extract `data_root_commands(rm, data_root, keep)`: keep=false wipes the whole root; keep=true deletes only dependencies/bin/control, never config.json/logs. - user_owned emits it ONLY for local/user installs (data_root is ~/.local/...). - privileged emits it ONLY for a system service (data_root is /var/opt). - /opt staging is still ALWAYS fully removed (binary + bundled deps). - machine-wide-no-service => no /var/opt data-root removal line at all. Defensive cleanup also added to the plan: - adb reap (`pkill -KILL -x adb`) after the pattern pkill. - tray autostart removal (`rm -f <HOME>/.config/autostart/ws-scrcpy-web-tray.desktop`). - menu-cache refresh (`update-desktop-database /usr/share/applications`) right after the system .desktop removal. Tests: add system_keep_preserves_var_opt_config_logs and user_service_keep; rewrite local_wipe to exact ordered (incl. adb + autostart); system_install is now the system-WIPE case and asserts the menu-cache refresh. keep/wipe matrix (local/user/system) fully covered.
…eta.49) Wires the pure app_uninstall_commands builder (Task 1) into a runnable two-entry dispatch: - --linux-app-uninstall (unelevated; the entry Node spawns via `systemd-run --user --collect`): runs the privileged group FIRST under ONE pkexec re-invocation of the launcher itself (argv all the way, no `sh -c`), then the best-effort user_owned group. A pkexec decline (126/127), a privileged failure, or a spawn error aborts and relaunches the running AppImage locally so the user is never stranded. - --linux-app-uninstall-elevated (the pkexec re-invoke, as root): runs ONLY the privileged group. Same builder + same flags on both sides, so the privileged/user_owned split is identical and each instance runs exactly its own half. parse_args is a pure flag parser (--scope user|system|none, --machine-wide 0|1, --keep XOR --wipe, --data-root, optional --relaunch); the run fns only shell out. Every tool resolves under tool_dir/sbindir (absolute, Local-Deps, never PATH), reusing linux_service::tool_dir. Dispatch is wired into main.rs ahead of service-defer and the /opt bootstrapper so an uninstall invocation can't be diverted into a browser-open or an /opt re-exec that would drop its flags. Drops the module's blanket dead_code allow now that the builder is live. Tests cover parse_args round-trip, --scope none/user mapping, keep/wipe exclusivity, and invalid-scope rejection. The run fns shell out and aren't compiled on non-Linux, so they're not unit-testable here; CI (cargo test/clippy on Linux) is the gate.
…a.49) run_unelevated now supports BOTH elevation modes for the privileged teardown group, mirroring the service-update path's `getuid()==0 ? direct : pkexec` split: - already root (the launcher was started by the ROOT system-service): run the privileged group DIRECTLY, best-effort — no pkexec (which would prompt redundantly/wrongly). A complete uninstall, so it never relaunches. - non-root (local / user-scope server): unchanged — re-invoke self under ONE pkexec; a decline (126/127), failure, or spawn error still aborts + relaunches the local AppImage. - empty privileged group: unchanged — skip elevation entirely. The decision is a pure `privileged_mode(is_root, privileged_empty) -> Skip|Direct|Pkexec` helper, unit-tested for all three outcomes (the run fns shell out and aren't unit-testable); run_unelevated matches on it. Root detection uses rustix::process::getuid().is_root() — rustix is already a launcher dep (single_instance's flock). The `process` feature is added scoped to cfg(target_os = "linux") so the Windows build, which never compiles this module, doesn't enable it. handle_elevated and the user_owned group are unchanged. Linux-cfg'd and unverifiable on Windows; CI (cargo test/clippy on Linux) is the gate.
Linux-only endpoint that spawns the detached Rust app-uninstall helper
(--linux-app-uninstall) via systemd-run, then exits the local instance so
the out-of-cgroup helper can tear the app down from underneath it. Mirrors
the existing service-teardown handoff in handleUninstall.
- AppUninstallRequest { keep } type for the request body.
- buildUninstallHelperArgs(): pure, exported arg-vector builder (root ->
system transient unit --collect; non-root -> --user --collect), so the
spawn shape is unit-tested without mocking process/systemd.
- handleAppUninstall(): resolves installed scope (getInstalledScope),
machine-wide /opt presence, isRoot, and the home AppImage relaunch path,
then forwards them to the helper. keep=true resets installMode to null up
front so a preserved config.json boots in local mode, not a phantom
service mode. Missing staged helper -> 500; non-linux -> 200 unsupported.
Tests: 3 pure arg-builder cases + 4 handler cases (keep success+spawn args,
installMode reset, missing-helper 500/no-spawn, non-linux unsupported).
Full suite 899/899 green; tsc --noEmit exit 0.
… to App section
Adds two Linux-only rows to Settings -> App, driven by a pure, unit-tested
state helper (appSectionButtonsState) applied from renderServiceState once
/api/service/status resolves:
- "install ws-scrcpy-web for all users": POSTs /api/service/install-system-wide
(pkexec -> /opt -> re-exec; the OS pkexec prompt is the confirmation) and
reloads on success. Disabled with an "already installed for all users (/opt)"
note once machine-wide installed.
- "uninstall ws-scrcpy-web": always enabled on Linux (not gated on service mode).
Reuses the settings-confirm-panel pattern with a "keep my settings & logs"
checkbox; confirm POSTs /api/service/uninstall-app { keep } and shows a terminal
"uninstalled -- close this tab" message on success.
Both rows are hidden on non-Linux (inline display:none overrides the
.settings-row display:contents rule). Logic lives in exported helpers
(appSectionButtonsState, buildInstallAllUsersControl, buildUninstallControl)
with vitest coverage; full suite 908 passing, tsc --noEmit clean.
…nstall (beta.49) The machine-wide install writes a system .desktop with Icon=ws-scrcpy-web, but nothing installed an icon file under that name, so the menu entry showed a generic icon. The AppImage already embeds the icon (vpk --icon -> .DirIcon), so buildMachineWideInstallScript now takes an optional iconSource and, when present, copies it into /usr/share/icons/hicolor/256x256/apps/ws-scrcpy-web.png and refreshes the icon cache (best-effort). The whole icon block is one best-effort subshell group so a missing .DirIcon (cp fails) cannot break the && chain and skip the home-AppImage delete. handleInstallSystemWide resolves the icon from $APPDIR/.DirIcon and passes it through; absent $APPDIR skips the icon entirely. Tools resolve via systemTools (absolute paths, Local-Deps). Teardown already removes the icon (launcher/src/linux_app_uninstall.rs SYS_ICON).
Smoke checklist (v0.1.30-beta.44-full.md): new Module 14 with seven [Linux] rows covering the install-for-all-users button, the start-menu icon, and the four in-app uninstall paths (local, user-service cascade, system-service cascade, keep-settings-&-logs) plus the SELinux-clean check. CHANGELOG [Unreleased] gains an Added block describing the three user-facing features.
The beta.49 App-section rows belong in the execution-ordered run-sheet (v0.1.30-beta.48-checklist.md), the active tracker — not in beta.44-full.md, which is the module-organized reference under deferred refresh (item 46). Adds batch '## #15 - beta.49 App-section UX (Linux)' before Global pass criteria with the seven rows relabeled 14.x->15.x and tags converted [Linux]->[L] to match the run-sheet's format; reverts the Module 14 section from beta.44-full.md so it flows Module 13 -> --- -> Global pass criteria again.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Three Linux-only Settings → App features (spec
docs/specs/2026-06-08-linux-app-section-ux-design.md, plandocs/plans/2026-06-08-linux-app-section-ux.md):/optinstall (reusesPOST /api/service/install-system-wide); greyed with "already installed for all users (/opt)" once on/opt..desktopIcon=ws-scrcpy-webresolves (no more generic placeholder).How
app_uninstall_commandsbuilder (privileged / user-owned split) + a--linux-app-uninstalldispatch. Elevation mirrors the service-update path: already-root (system service) runs the privileged group directly; non-root (local / user-scope) re-invokes the launcher under one pkexec — a declined prompt aborts before anything is removed and relaunches the app. Local-Dependencies-Only: every tool resolved under the launcher bindir.POST /api/service/uninstall-app {keep}spawns the detached helper (systemd-run --collect,--userwhen the server is the user) and exits so the out-of-cgroup teardown runs from underneath; resetsinstallModein the kept config so a reinstall comes up clean.config.json+logs/only (at the data root's owner —~/.localor/var/opt); dependencies always re-download.Verification
tsc --noEmitexit 0.cargo test --workspace+clippygates it (the launcher Linux teardown code can't be compiled/tested from the Windows dev host). Builder/parser tests cover the local/user/system × keep/wipe matrix + theprivileged_modebranch.docs/smoke-tests/v0.1.30-beta.48-checklist.mdgains batch chore(ci): bump setup-dotnet v4 → v5.2.0 + dotnet-version 9.x → 10.x #15 — to run on the Fedora VM.Notes
$APPDIR/.DirIcon) is a runtime assumption verified by the smoke; the install icon step is best-effort and never aborts the install.