aboutsummaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs
diff options
context:
space:
mode:
Diffstat (limited to 'docs/superpowers/specs')
-rw-r--r--docs/superpowers/specs/2026-09-12-window-switcher-design.md159
1 files changed, 159 insertions, 0 deletions
diff --git a/docs/superpowers/specs/2026-09-12-window-switcher-design.md b/docs/superpowers/specs/2026-09-12-window-switcher-design.md
new file mode 100644
index 0000000..ce95f3e
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-12-window-switcher-design.md
@@ -0,0 +1,159 @@
+# window-switcher
+
+A full-screen grid of live window previews, replacing the rofi list that
+`ALT + TAB` runs today. The fifth quickshell component, and like the other four
+a complete shell in its own right.
+
+## Why
+
+`~/bin/hypr-windows.sh` works, but it is a textual rofi list: class, workspace,
+title and address in four columns. It says which windows exist, not what is in
+them. A preview grid answers "which window did I mean" by showing it, which is
+the whole reason a switcher exists.
+
+The replacement is therefore not a bug fix. The script's odd-looking
+`hl.dsp.focus({ window = "address:..." })` dispatch is confirmed working, so
+nothing is being repaired here, only improved.
+
+## What it looks like
+
+A full-screen translucent overlay on DP-1, windows as a wrapping grid of cards:
+
+ ┌─────────────────────────┐ ┌─────────────────────────┐
+ │ [icon] [x] │ │ [icon] [x] │
+ │ live preview │ │ live preview │
+ └─────────────────────────┘ └─────────────────────────┘
+ Firefox kitty
+ noland.dnx services — … ◐ HANDOFF.md
+ [workspace 1] [workspace 3]
+
+Per card, top to bottom:
+
+- The live preview, with two corner overlays: the app icon top-left and a
+ close button top-right.
+- The application name, centered.
+- The window title, left aligned, elided at the card width.
+- The workspace, centered, in brackets and in the accent colour.
+
+Cards are a fixed size and the grid wraps, centered. A preview is 21:9, so a
+card readable at arm's length is around 380px wide and DP-1 at 2560px fits six
+per row. Six or fewer windows therefore render as the single row this started
+as; more wrap rather than shrinking, so a large window count degrades by
+growing downwards instead of becoming unreadable.
+
+The name is redundant with the icon and usually with the title too. It is kept
+deliberately: it is short and stable, so the eye has something to land on when
+scanning a grid, and it still reads when a title is empty or cryptic.
+
+Both corner overlays need a scrim or shadow behind them. A themed icon drawn
+over an arbitrary window preview can land on a same-coloured region and
+disappear, a light icon on a white page being the obvious case.
+
+## Data
+
+One source: `Hyprland.toplevels`, refreshed with `Hyprland.refreshToplevels()`
+when the overlay opens. Each `HyprlandToplevel` carries everything the card
+needs:
+
+| Field | Use |
+| --- | --- |
+| `address` | focus and close dispatches |
+| `workspace.name` | the bracketed label, and the filter |
+| `title` | the title line |
+| `.wayland` | the `ScreencopyView` capture source, and `appId` |
+
+No `hyprctl` subprocess and no `jq`, unlike the script being replaced.
+
+Windows on a workspace whose name starts with `special:` are dropped, matching
+the script's `grep -v special`. Windows on other workspaces are kept whatever
+their visibility: jumping to a window on another desktop is the point, and the
+workspace label says where the jump goes.
+
+Order is by `focusHistoryID`, so the last-used window comes first.
+
+## Interaction
+
+Toggle-style, not hold-and-release. `ALT + TAB` opens the overlay and it stays
+open; there is no attempt to detect the ALT release.
+
+| Input | Result |
+| --- | --- |
+| click a card | focus that window, close the overlay |
+| close button | close that window, overlay stays open |
+| arrows / TAB | move the selection |
+| Enter | focus the selection, close the overlay |
+| Escape | close the overlay, focus nothing |
+
+The close button leaves the overlay open because closing is a side errand: a
+dismissal would mean reopening to close a second window. The card disappears on
+the Hyprland event, so nothing needs refreshing by hand.
+
+## The traps this has to clear
+
+Three are already recorded in AGENTS.md and all three apply here.
+
+**A config with no visible window exits.** The overlay is hidden most of the
+time, so the component needs the same 1x1 transparent `PanelWindow` with
+`mask: Region {}` that the other four hold themselves open with.
+
+**Key events reach a focused item, not a window.** `WlrLayershell.keyboardFocus:
+Exclusive` is necessary but not sufficient, and `Keys.onEscapePressed` on a
+`PanelWindow` never fires. Keys are handled on an `Item` inside the overlay with
+`focus: true`. This is the part to prove first, on a bare overlay, before any
+grid is built on top of it.
+
+**`qs` is the process name, and `pkill -f` matches the caller.** Verification
+uses `pkill -x qs` and `pgrep -cx qs`, and a detached `qs` does not survive a
+tool call, so it is started such that the harness owns the process and confirmed
+from the log rather than a later `pgrep`.
+
+A fourth is specific to this component: `HyprlandToplevel.address` reads
+`1140f190`, without the `0x` that `hyprctl clients` reports, while dispatches
+need the prefix.
+
+## What was verified before writing this
+
+Against Hyprland 0.56.2 and quickshell 0.3.1, in a scratch config:
+
+- `ScreencopyView` is compiled in, with both wlroots and hyprland toplevel
+ export. A capture rendered at 2556x1034.
+- All four open windows captured, the scratchpad on `special:special`
+ included. Hidden windows therefore do not need an icon-only fallback.
+- `Hyprland.toplevels` reads 0 until `refreshToplevels()` is called, then
+ reports all four with workspace and `.wayland` links intact.
+- `Quickshell.iconPath` resolves through `Material-Black-Plum-Suru`, for
+ `window-close` and for app icons.
+- `DesktopEntries.byId` returns null for bare app ids such as `firefox`. The
+ app id itself resolves as an icon name, so lookup falls back to it.
+
+## Theme
+
+No palette of its own. `Theme.qml` is a symlink to `shared/Theme.qml` like the
+other three, and the accent used for the workspace label comes from
+`udt-palette.qml`, so it follows the wallpaper.
+
+A distinct `WlrLayershell.namespace` of `window-switcher`, so a Hyprland
+`layer_rule` can blur it later. Without the rule it renders flat translucent,
+which is fine.
+
+## Deliberately left out
+
+- **The fullscreen dance.** The script has a `--batch` sequence for focusing a
+ window when another is fullscreen on the same workspace. Whether plain
+ `focuswindow` still needs it is unknown; it goes back in only if a fullscreen
+ window is actually observed staying on top.
+- **Multi-monitor overlays.** DP-1 only. DP-3's windows appear as cards, the
+ overlay just does not paint there.
+- **Hold-and-release ALT + TAB.** Considered and rejected: it depends on the
+ switcher seeing the ALT release, and a missed release leaves a stuck
+ full-screen overlay.
+- **Icon-only fallback for uncapturable windows.** Every window captured, so
+ the case has not been observed.
+
+## Done means
+
+The overlay opens on `ALT + TAB` over IPC, shows every non-special window as a
+live preview with icon, name, title and workspace, focuses on click and on
+Enter, closes a window from the corner button without dismissing, and leaves on
+Escape. The user judges the visual result; screenshots of a transient overlay
+are a race.