Skip to content

Wayfinder: running-windows switcher design (seeded by #20) #116

Description

@jonocodes

Destination

Design spec for a running-windows switcher, built as three stages, each independently shippable:

  1. Fallback header label — when the focused app has no layout match, the client header shows Home (xterm) instead of bare Home. No enumeration, no list. (Stage 1: fallback header label — Home (xterm) #123)
  2. Running-programs list — tapping that header text opens a list of currently-running programs (only running ones — xterm appears even with no layout, disappears when closed), entries labeled by the layout they'd match with the raw program in parens on default fallback. Display only. (Stage 2: running-programs list UI & enumeration #120)
  3. Switch on tap — tapping a list entry raises that window (GNOME-only write side, raise: primitive underneath). (Stage 3: switch on tap — raise: primitive spec #122)

The map ends when the design decisions are resolved into build tickets ready to implement. Seeded by #20, which stays open as the raise-capability reference.

Notes

  • Domain: platform coupling (GNOME Shell extension in packaging/gnome-shell, daemon backends in daemon/deckd/platform.py), protocol (daemon/deckd/protocol.py, client/src/protocol.ts), layout model (daemon/deckd/layouts.py).
  • Read side today is focus-only (watch_active_app); enumeration arrives with stage 2 (Stage 2: running-programs list UI & enumeration #120 closed).
  • ADR-0005 (dynamic widget state) stays deferredStage 2: running-programs list UI & enumeration #120 confirmed the list is a chrome view, not a widget whose state mutates under a static layout.
  • Stages 1-3 are all spec'd and closed. Only the stage-3 build (Build stage 3: switch on tap — raise-window dispatch #127) remains open.
  • Committed early decisions: GNOME-only write side; chrome-internal raise (no raise: layout action primitive in v1); raise-only (no launch fallback).
  • Use /grilling + /domain-modeling for decision tickets; /prototype for the viability spike; /research for API fact-finding.
  • GNOME 50 drift: deckd-focus@local (stage-0 focus publishing) is currently dead on GNOME 50 — fix tracked in #125, independent of the switcher stages. Wayland dev loop needs a full logout/login per extension code change.

Decisions so far

  • GNOME Shell enumeration & activation API research — Meta/Shell APIs (get_window_actors, Meta.Window get_id/wm_class/title/workspace, window-created/unmanaging/notify::title, Main.activateWindow) are stable across GNOME 40-48 and in-process D-Bus export is the only Wayland path; low API risk, annual shell-version bump chore.
  • Stage 1: fallback header label — Home (xterm) — default-fallback layout shows Name (wm_class||app_id), raw; gated by an additive is_default: bool on the layout push (focused_app already present); suffix suppressed on identity/title matches, pinned views, and the auto-ignore hold. Build: #124.
  • Build stage 1: fallback header suffix Home (xterm) — landed is_default wire flag + client suffix (jonocodes/deckd@08dbb11). Server tracks _current_is_default against the focus-driven resolution (re-derived on reload; cleared on override); Session.push_current forces False for demo ?layout= pin and chrome select_view pin so neither leaks the underlying program. Client renders (wm_class||app_id) in heading, badge, and aria-live announcement. Auto-ignore hold verified — no deckd-identity push leaks through (no Home (deckd)). Stage 2 (Stage 2: running-programs list UI & enumeration #120) is the frontier.
  • Wayland raise viability spikeGO: Main.activateWindow reliably raises on GNOME 50 Wayland — cross-workspace (switches active workspace), multi-window (key by window id + MRU tie-break), minimized (un-minimizes), flatpak, and clean not-running no-op; all human-observed. Identity is three keys: wm_class (always present, primary), gtk_application_id (GTK apps only), sandboxed_app_id (flatpaks) — GNOME Shell enumeration & activation API research #118's gtk-app-id assumption corrected. Spike branch spike/117-wayland-raise. Surfaced #125 (focus ext dead on GNOME 50).
  • Window identity & matching semanticsper-window list (one row per window; per-app grouping deferred). window_id is the single opaque string handle across extension→daemon→client→tap→raise (client echoes, never parses; platform-neutral). MRU demotes to list sort order; multi-match moot. Daemon owns labels via the layout matcher (match→layout name, else raw wm_class, title last resort). Enumeration adds sandboxed_app_id to AppInfo and carries workspace/minimized — both unrendered in v1. Unblocks Stage 2: running-programs list UI & enumeration #120, Stage 3: switch on tap — raise: primitive spec #122.
  • Backend interface & failure behaviorGNOME backend interface for Stage 2: running-programs list UI & enumeration #120/Stage 3: switch on tap — raise: primitive spec #122. Enumerate: watch_windows() -> AsyncIterator[Sequence[WindowInfo]] (push, coalesced ~100ms; mirror of watch_active_app). WindowInfo is two-layer: identity + state (window_id, wm_class, gtk_application_id, sandboxed_app_id, title, workspace, minimized); the matched label is a separate wire-payload field the daemon produces — backend interface is free of layout-pipeline concerns. Decline contract: capabilities() -> frozenset[str] flag (default {"watch_active_app"}); base class implements watch_windows/raise_window as raise UnimplementedCapability(...) (new exception, distinct from FocusBackendUnavailable — different UX: install-hint vs. silent absence). Raise failure: re-validate the list (dead row drops as feedback) + emit raise_failed event on the diagnostic stream — no toast primitive. Capability-unsupported surfaces as the list being absent from the chrome.
  • Stage 2: running-programs list UI & enumerationchrome view (select_view: "windows", ADR-0008 carve-out, ADR-0005 deferred). Extension adds ListWindows() -> s only (polled at ~100ms, no WindowsChanged signal). New RunningWindowsMessage daemon→client ({type: "running_windows", windows: [{window_id, label, icon | null}, ...]}), full snapshot per push, broadcast to every session (chrome_media precedent). Daemon owns labels (per-push, no cache) — matched layout's display_name, default fallback to raw wm_class||app_id||title. icon copied from the matched layout when present, null on default fallback (honest absence, not a generic glyph). Chrome-btn always rendered; view shows "unsupported on this platform" when capabilities() lacks watch_windows (mirrors media browser's "no players" empty state). Build: #126.
  • Stage 3: switch on tap — raise: primitive specchrome-internal only: RaiseWindowMessage { type: "raise_window", window_id: "..." } joins ClientMessage union as fire-and-forget; no raise: layout action primitive in v1. Extension owns id↔Meta.Window map, exposes RaiseWindow(s) → b on org.deckd.Focus via Main.activateWindow. Daemon dispatches to backend.raise_window(); raise_failed on EventMessage (Live diagnostic event stream and correlation IDs #73) on failure. View auto-closes on raise (client sends RaiseWindowMessage then ClearViewMessage). Static raise: layout buttons deferred to future fog. Build: #127.

Not yet specified

Out of scope

  • KWin, macOS, and X11 raise/enumeration backends — deliberate follow-up effort once the GNOME-shaped v1 proves the interface; the backend interface is designed platform-neutral so these slot in later
  • Launch-or-raise fallback (spawning apps that aren't running)
  • Filtering/pinning/ordering preferences for the switcher list beyond the basics the spec settles

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions