diff options
Diffstat (limited to 'docs/superpowers/specs/2026-09-12-window-switcher-design.md')
| -rw-r--r-- | docs/superpowers/specs/2026-09-12-window-switcher-design.md | 159 |
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. |
