macos: fold relaunch prompt into the warning row instead of a toast

The cut-off toast UX ("Accessibility granted. Relaunch Lan Mouse so
capture and emulat…") was unreadable in a compact window and split
the "grant" and "relaunch" flows into two disconnected surfaces. Fold
everything into the existing warning row with state-dependent content:

- AX missing:
    title    = "input capture is disabled"
    subtitle = "grant Accessibility permission to enable"
    button   = "Grant"   → opens System Settings → Accessibility

- AX granted, daemon still bailed:
    title    = "relaunch required"
    subtitle = "Accessibility granted — restart to activate capture
                and emulation"
    button   = "Relaunch" → spawns a fresh bundle via `open` after
                            a 1s delay, then quits.

- Both active: row hidden.

The emulation_status_row is kept hidden on macOS because capture and
emulation share the same TCC gate — a single row is sufficient and
two identical-looking warnings were noisy. `handle_emulation` still
exists for the non-macOS platforms where the rows are distinct.

Side effects:
- `relaunch_bundle` moved from lib.rs to macos_privacy so imp.rs can
  call it from the row button handler.
- AX watcher callback shrinks to `window.present()` +
  `refresh_capture_emulation_status()`; the toast-based dialog is
  gone along with its helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jon Kinney
2026-04-24 09:15:40 -05:00
committed by Ferdinand Schober
parent 2dc9ebb6cd
commit 5d7d14fbf7
5 changed files with 112 additions and 102 deletions

View File

@@ -22,6 +22,17 @@ use crate::{
use super::{client_object::ClientObject, client_row::ClientRow};
#[cfg(target_os = "macos")]
fn set_button_content_label(button: &gtk::Button, label: &str) {
// The Reenable/Grant/Relaunch button wraps its icon+label in an
// AdwButtonContent (see window.ui). Walk into it and swap the label
// rather than GtkButton::set_label, which would replace the content
// widget and drop the icon.
if let Some(content) = button.child().and_downcast::<adw::ButtonContent>() {
content.set_label(label);
}
}
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends adw::ApplicationWindow, gtk::Window, gtk::Widget,
@@ -459,24 +470,52 @@ impl Window {
let capture = self.imp().capture_active.get();
let emulation = self.imp().emulation_active.get();
// On macOS the yellow "Reenable" row only makes sense when
// Accessibility is actually missing. When AX is granted but
// capture/emulation are still off, the daemon simply hasn't been
// restarted yet — the Relaunch toast covers that state, and a
// yellow "grant permission" warning on top of it would be
// redundant and confusing.
#[cfg(target_os = "macos")]
let show_warning = !crate::macos_privacy::accessibility_granted();
#[cfg(not(target_os = "macos"))]
let show_warning = true;
{
// On macOS, capture and emulation share the same TCC gate
// (Accessibility). Collapse to a single warning row —
// emulation_status_row stays hidden and capture_status_row
// doubles as the shared status indicator. Its text and
// button mutate based on whether we're waiting for AX or
// waiting for the user to relaunch the app.
let anything_off = !capture || !emulation;
self.imp().emulation_status_row.set_visible(false);
self.imp().capture_status_row.set_visible(anything_off);
self.imp().capture_emulation_group.set_visible(anything_off);
let show_capture_row = !capture && show_warning;
let show_emulation_row = !emulation && show_warning;
self.imp().capture_status_row.set_visible(show_capture_row);
self.imp().emulation_status_row.set_visible(show_emulation_row);
self.imp()
.capture_emulation_group
.set_visible(show_capture_row || show_emulation_row);
if anything_off {
self.update_macos_warning_row_text();
}
}
#[cfg(not(target_os = "macos"))]
{
self.imp().capture_status_row.set_visible(!capture);
self.imp().emulation_status_row.set_visible(!emulation);
self.imp()
.capture_emulation_group
.set_visible(!capture || !emulation);
}
}
#[cfg(target_os = "macos")]
fn update_macos_warning_row_text(&self) {
let row = &self.imp().capture_status_row;
let button = &self.imp().input_capture_button;
if crate::macos_privacy::accessibility_granted() {
// AX granted but capture/emulation still off → the daemon
// subprocess bailed at startup and needs a fresh process to
// re-initialize with the new grant in place.
row.set_title("relaunch required");
row.set_subtitle("Accessibility granted — restart to activate capture and emulation");
set_button_content_label(button, "Relaunch");
} else {
// AX missing → send the user to System Settings.
row.set_title("input capture is disabled");
row.set_subtitle("grant Accessibility permission to enable");
set_button_content_label(button, "Grant");
}
}
pub(super) fn set_authorized_keys(&self, fingerprints: HashMap<String, String>) {