# 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.