aboutsummaryrefslogtreecommitdiffstats
path: root/window-switcher
diff options
context:
space:
mode:
Diffstat (limited to 'window-switcher')
-rw-r--r--window-switcher/README.md151
1 files changed, 151 insertions, 0 deletions
diff --git a/window-switcher/README.md b/window-switcher/README.md
new file mode 100644
index 0000000..3cfb28b
--- /dev/null
+++ b/window-switcher/README.md
@@ -0,0 +1,151 @@
+# window-switcher
+
+Every open window as a live preview, in one centred grid over a dimmed screen.
+ALT+TAB opens it, a click or Enter picks a window, Escape drops it. It replaces
+a rofi list that showed the same windows as text.
+
+ ┌──────────────────────────────────────────────────────────────┐
+ │ │
+ │ ┌────────────────────┐ ┌────────────────────┐ │
+ │ │ [icon] [x] │ │ [icon] [x] │ │
+ │ │ │ │ │ │
+ │ │ live preview │ │ live preview │ │
+ │ │ fitted in a │ │ │ │
+ │ │ 16:10 box │ │ │ │
+ │ └────────────────────┘ └────────────────────┘ │
+ │ firefox kitty │
+ │ a page title, elided... ~/Programming/GIT/... │
+ │ [1] [4] │
+ │ │
+ └──────────────────────────────────────────────────────────────┘
+
+The cards are ordered most recently used first, so the window you just left is
+the first one, the same order ALT+TAB implies. Windows on a `special:`
+workspace are the scratchpad, which has its own bind, and are left out.
+
+With nothing to switch to it says so, because a full-screen dim with nothing in
+it reads as a hang.
+
+## Running it
+
+ qs -p .
+
+It is started from `autostart.lua` and reached over IPC, so the shell has to be
+running for the keybind to do anything:
+
+ qs -p ~/Programming/GIT/quickshell/window-switcher ipc call switcher toggle
+
+Write that path out in full in the real config: Hyprland's Lua strings have no
+shell to expand `~`. The bind lives in `keybindings.lua` on ALT+TAB. `show` and
+`close` exist on the same IPC target for anything that wants one direction only.
+
+## Interaction
+
+| | |
+| --- | --- |
+| ALT+TAB | open, or close if already open |
+| click a card, or Enter | focus that window and dismiss |
+| arrows | move the selection, up and down by a row |
+| Tab / Shift+Tab | move the selection by one |
+| the `x` on a card | close that window, overlay stays open |
+| Escape, or a click on the backdrop | dismiss, focus unchanged |
+
+Closing leaves the overlay up on purpose. Tidying several windows would
+otherwise mean reopening between each one, and the card goes when Hyprland says
+the window went, not when the button is pressed.
+
+## Hyprland dispatches from QML
+
+**Dispatch arguments are evaluated as Lua on Hyprland 0.56.2.** The form that
+reads like the documented one, `dispatch focuswindow address:0x...`, is a
+syntax error rather than a command, and it fails silently unless stderr is
+read. What works:
+
+ hl.dsp.focus({ window = "address:0x..." })
+ hl.dsp.window.close({ window = "address:0x..." })
+
+`~/bin/hypr-windows.sh`, the script behind the rofi list this replaces, is
+written that way for this reason. While planning, its syntax was read as
+legacy oddity worth modernising, which would have broken it.
+
+**A focus dispatched while the overlay is up is accepted and then ignored.**
+The layer surface holds keyboard focus exclusively and the compositor will not
+move window focus out from under that grab. The dispatch reports `ok`, nothing
+happens, and no log line says otherwise. Closing in the same turn does not help
+either: `close()` only clears a property, and the surface survives until the
+frame after. So the target is remembered, the overlay closes, and a 60ms timer
+does the focus once the grab is really gone. The symptom was picking a window
+on the other monitor and seeing nothing happen.
+
+**Focus alone raises a stacked window.** Workspaces 1 and 8 here are `monocle`
+and 4 and 5 are scrolling, so stacking is normal rather than an edge case, and
+it was tested with two stacked windows on workspace 1 in both directions. No
+`alterzorder` and no batch sequence are needed, which is why the fullscreen
+batch in `hypr-windows.sh` was not carried over.
+
+## Reading the toplevel list
+
+**`Hyprland.toplevels` reads 0 until `refreshToplevels()` is called.** The
+model is refreshed on every opening, which also picks up windows opened since
+the last one.
+
+**`HyprlandToplevel` has no `focusHistoryID` property.** It reads `undefined`,
+so a sort on it compares `NaN` and does nothing at all, leaving a plausible
+looking list in arbitrary order. The value is on `lastIpcObject` instead, which
+is what `focusOrder()` reads, and a missing one sorts last rather than as 0.
+
+**`HyprlandToplevel.address` omits the `0x`** that `hyprctl clients` prints and
+that a dispatch requires, so `addressOf()` puts it back.
+
+**`toplevel.wayland` can be null.** The Hyprland toplevel and its wlr handle
+are separate objects created at different moments, so a window that is
+appearing or being destroyed has one and not the other. The card guards every
+use of it.
+
+## Card geometry
+
+**A card is a fixed 16:10 box with the preview fitted inside**, not a box
+shaped to its preview. The previews are not one shape: DP-1 windows are near
+21:9 (2556x1034) and DP-3 reports `transform=1`, so its windows are portrait
+9:16 (1076x1916). Sizing each card to its own window gives ragged rows and
+breaks the alignment of the three text lines under them.
+
+**Card width is clamped by the height as well as the width.** A centred `Grid`
+has no way to scroll, so without the height term a busy desktop pushes rows off
+the top and bottom where they cannot be reached. The cards shrink instead.
+There is a ceiling on the width too, or a single window becomes a full-screen
+mirror of itself.
+
+**An uncaptured `ScreencopyView` reports `sourceSize` of `QSize(-1, -1)`**, not
+`(0, 0)`. The aspect ratio guard therefore tests for a positive height. The
+obvious defensive rewrites, `!== 0` or a plain truthiness check, all pass on
+`-1` and produce a negative ratio.
+
+**A `Repeater` delegate with required properties needs both `required property
+int index` and `required property var modelData`.** Declaring `modelData`
+required makes the implicit `index` unavailable.
+
+**An app with no themed icon renders a blank scrim square.**
+`Quickshell.iconPath("", true)` returns `""`, and an `IconImage` with an empty
+source draws nothing. There is no fallback glyph. Both corner overlays carry
+their own scrim regardless, because a themed icon on an arbitrary window
+preview can otherwise land on a same-coloured region and vanish.
+
+## Theme and blur
+
+`Theme.qml` is a symlink to `shared/Theme.qml`: the palette comes from
+`~/.cache/wal/udt-palette.qml` and is watched.
+
+Frosting is Hyprland's, matched on this window's namespace:
+
+ hl.layer_rule({
+ name = "blur-window-switcher",
+ match = { namespace = "^(quickshell-window-switcher)$" },
+ blur = true,
+ xray = false,
+ ignore_alpha = 0.1,
+ })
+
+Without the rule it still works, rendering flat translucent. The overlay covers
+the whole screen, waybar included, so unlike `mail-overview` it uses
+`ExclusionMode.Ignore`.