diff options
| -rw-r--r-- | AGENTS.md | 41 | ||||
| -rw-r--r-- | README.md | 9 | ||||
| -rw-r--r-- | window-switcher/README.md | 151 |
3 files changed, 191 insertions, 10 deletions
@@ -8,10 +8,11 @@ Quickshell components for a Hyprland desktop, one per directory, each a complete shell in its own right. They are not modules of a single bar: any of them runs alone, and running one does not require the others. - volume-osd/ volume for output and input, plus what is playing - vm-manager/ libvirt drawer: state, live stats, snapshots - appearance/ wallpaper picker and colour scheme switcher - mail-overview/ notmuch unread counts per account, waybar icon and drawer + volume-osd/ volume for output and input, plus what is playing + vm-manager/ libvirt drawer: state, live stats, snapshots + appearance/ wallpaper picker and colour scheme switcher + mail-overview/ notmuch unread counts per account, waybar icon and drawer + window-switcher/ open windows as live previews in a grid, on ALT+TAB They are started from `~/.config/hypr/sections/autostart.lua` and keep running for the whole session. @@ -102,6 +103,31 @@ changing that component. The ones that generalise: `block.allocation` is qcow2 growth on the host, not usage inside the guest. The real numbers come from qemu-guest-agent, and the panel shows a dash rather than substituting the host-side ones. +- **Hyprland 0.56.2 evaluates dispatch arguments as Lua.** `dispatch + focuswindow address:0x...` is a syntax error rather than a command, and it + fails silently unless stderr is read. The working form is `dispatch + hl.dsp.focus({ window = "address:0x..." })`. This is why + `~/bin/hypr-windows.sh` is written the way it is, and its syntax was misread + as legacy oddity worth modernising. +- **A focus dispatched while an overlay holds the keyboard is accepted and then + ignored.** A layer surface with `keyboardFocus: Exclusive` grabs the + keyboard, and the compositor will not move window focus out from under that + grab. The dispatch reports `ok`, nothing moves, and no log line says + otherwise. Closing in the same turn does not help either, because a `close()` + that clears a property leaves the surface alive until the frame after. Focus + after the overlay is really gone, which `window-switcher` does with a 60ms + timer. +- **`HyprlandToplevel` has no `focusHistoryID` property.** It reads + `undefined`, so a sort on it compares `NaN` and silently does nothing, + leaving a plausible looking list in arbitrary order. The value is on + `lastIpcObject`. +- **`HyprlandToplevel.address` omits the `0x`** that `hyprctl clients` prints + and that every dispatch requires. +- **`Hyprland.toplevels` reads 0 until `refreshToplevels()` is called.** +- **An uncaptured `ScreencopyView` reports `sourceSize` of `QSize(-1, -1)`**, + not `(0, 0)`. An aspect ratio guard has to test for a positive height: the + obvious rewrites, `!== 0` or a truthiness check, all pass on `-1` and produce + a negative ratio. ## Theme @@ -128,7 +154,7 @@ they never had. A symlink rather than a shared import path because a singleton outside the config directory needs a `qmldir`, which is the same friction that keeps the palette parsed rather than imported; quickshell follows the link and resolves the singleton with no qmldir and no consumer change. Editing any -component's `Theme.qml` edits all four. Do not replace a link with a copy. +component's `Theme.qml` edits all five. Do not replace a link with a copy. ## Blur @@ -144,9 +170,12 @@ A panel that should sit below waybar rather than over it wants exclusive zone without the component knowing the bar's height. Measured with `hyprctl layers`: waybar at `y=-540 h=42`, a `Normal` overlay on the same screen at `y=-498 h=1038`, starting exactly where the bar ends, so the -backdrop never dims it. `mail-overview` does this; the other three use +backdrop never dims it. `mail-overview` does this; the other four use `ExclusionMode.Ignore` and cover the whole screen. +`window-switcher` is the fifth, with namespace `quickshell-window-switcher` and +a `blur-window-switcher` rule of its own. + ## Reloading | | How | @@ -13,10 +13,11 @@ repos stay independent, this one has no build-time dependency on that one. ## Implementations - volume-osd/ on-screen display for output and input volume - vm-manager/ libvirt VM drawer: state, live stats, snapshots - appearance/ wallpaper picker and colour scheme switcher - mail-overview/ unread mail per account, as a waybar count and a drawer + volume-osd/ on-screen display for output and input volume + vm-manager/ libvirt VM drawer: state, live stats, snapshots + appearance/ wallpaper picker and colour scheme switcher + mail-overview/ unread mail per account, as a waybar count and a drawer + window-switcher/ open windows as live previews in a grid, on ALT+TAB Each directory has its own README covering what it does and how to run it. 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`. |
