dotfiles
Desktop

The Hyprland keybinds

Edit on GitHub

modules/home/desktop/hypr/lua/keybinds.lua. At parity with the Arch/Kingston setup, with the tools adapted to the NixOS stack.

The comments in that file are not only comments: the cheatsheet (SUPER+H) is GENERATED from them at runtime. The first line of a comment block becomes the GROUP, and the trailing comment on a bind's line becomes its DESCRIPTION. See desktop-plumbing.md for the parser's rules. So keep the first line of every block a good label, and keep the trailing comments.

The monitor data has a self-contained fallback

The dofile of ~/.config/theme/monitors.lua is wrapped in a pcall with a literal fallback, because if the file is missing (the first boot before a rebuild, or new data not generated yet) the dofile BLOWS UP and aborts the config. Since "autostart" comes later in the load order, the session would come up with no services at all.

Do NOT factor this into a global helper: Hyprland does not share globals between dofile calls.

The "/" key over remote access

Moonlight does NOT send the ABNT2 "/ ?" key (bug #1789, an international key). SCROLL LOCK is remapped to "/" and Shift+ScrollLock to "?", since it is an idle key and this TKL has no Menu.

It uses send_shortcut, a NATIVE Hyprland dispatcher that sends the keysym straight to the active window, independent of the layout and with no external exec, unlike wtype, which did not inject through the bind. The ABNT2 layout is untouched and it works locally too.

key = "code:97" and NOT "slash", and this one took real digging: send_shortcut's resolveKeycode sweeps the keymap with xkb_state_key_get_one_sym, which respects the modifiers HELD AT THAT MOMENT, so it only finds a keysym at the ACTIVE level. With Shift held (the "?" bind) no keycode produces slash, they produce question, giving "send_shortcut: key not found", and the "?" never came out. The code: prefix short-circuits BEFORE touching the xkb state, so it is immune to modifiers.

97 = <AB11>, the ABNT2 "/ ?" key (evdev KEY_RO 89 plus 8), checked against xkbcli compile-keymap --layout br --variant abnt2.

The same reasoning is why the cheatsheet is on SUPER+H and not SUPER+/.

Moonlight 6.2.0 sends the key, as the WRONG one

Retested on 08/10/2026 with a 6.2.0 client, and the bind stays. The key no longer vanishes: it now types \. The cause has two parts, one on each side of the wire:

  • Client. 6.1.0 did not handle SDL_SCANCODE_INTERNATIONAL1 at all (Unhandled button event: 135). 6.2.0 (commit fabb4fda) falls it through to the NONUSBACKSLASH case, so it goes out as VK 0xE2 (VK_OEM_102), the SAME code as the ISO extra key, with only the SS_KBE_FLAG_NON_NORMALIZED flag to tell them apart.
  • Host. Sunshine on Linux injects through inputtino, which maps 0xE2 to KEY_102ND, which is <LSGT> (keycode 94) on ABNT2: backslash, bar. inputtino has no entry for 0xC1 or KEY_RO.

It cannot be fixed from the host. The client's real \ | key sends that same 0xE2, so remapping <LSGT> on Sunshine's virtual keyboard would trade "/" for "" instead of recovering it. The fix proposed on #1789 (send VK_ABNT_C1, 0xC1) is also only half of one for a Linux host: inputtino would need a 0xC1 to KEY_RO entry as well.

AltGr+Q and AltGr+W need no bind at all

ScrollLock is not the only way in, and on a LAPTOP it is the wrong one, because a laptop usually has no ScrollLock key (or hides it behind Fn), so the bind above is present and unreachable. The ABNT2 layout ALREADY carries both characters on level 3, on two ordinary letter keys:

key <AD01> { [ q, Q, slash,    slash        ] };   Q, AltGr = "/"
key <AD02> { [ w, W, question, question     ] };   W, AltGr = "?"
key <AB11> { [ slash, question, degree, questiondown ] };   the key Moonlight drops
key <RALT> { type= "ONE_LEVEL", symbols[1]= [ ISO_Level3_Shift ] };   AltGr IS right Alt

Measured the same way as the 97 above, xkbcli compile-keymap --layout br --variant abnt2: <AD01> is keycode 24, <AD02> is 25 and <RALT> is 108. So AltGr+Q types "/" and AltGr+W types "?", over the stream and locally, with NOTHING declared: they ride on keys every keyboard has and that Moonlight does transmit, which is exactly what <AB11> is not.

This is worth writing down rather than rediscovering, because the ScrollLock bind reads as if it were the only path and it sent me looking for a second bind when the layout already answered. The bind still earns its place on the desktop: a dedicated key beats a three-finger combo when the key is sitting there idle. What it does NOT do is cover the laptop.

SSH is not one of these surfaces

An SSH session cannot be fixed from this repo, and expecting it to is a category error worth naming once: SSH carries BYTES, not key events, so the client resolves the physical key into a character with ITS OWN layout before anything reaches the wire. There is no server-side layout in the path, which is why console.keyMap, services.xserver.xkb and Hyprland's kb_variant are all irrelevant to it. If a character does not come out over SSH, the machine to fix is the one being typed on. The remote-access surfaces this section covers are Moonlight and Sunshine, where a virtual keyboard (keyboard-passthrough in hyprctl devices) does go through the host's xkb.

The scrolling layout's tape

move scrolls the VIEW without touching the focus; with follow_mouse=1 passing the mouse over the column that came in is enough for it to receive the keyboard. The arrows still hold, because follow_focus (the default) already brings the focused column into view.

There is NO guard, because ALL the workspaces are scrolling. If any goes back to dwindle, reintroducing the guard is MANDATORY: a bind is global, a layout message is not, and on dwindle Hyprland answers "Unknown dwindle layoutmsg" emitting ONE NOTIFICATION PER EVENT, so with the thumbwheel in a burst the screen becomes a wall of toasts.

And pcall does not help: checkResult emits the notification and returns {ok=false} without raising a Lua error. The guard (the version in git, commit 7f74ae8) filtered by hl.get_active_workspace().id, because the workspace object does NOT expose layout. Inside a lambda the dispatch is hl.dispatch(d), never d() ("dispatcher objects cannot be called directly").

The mouse on the tape

The MX Master's thumbwheel is SUPER plus the horizontal wheel. logiops does NOT divert the wheel anymore (see mouse.md): it goes back to emitting native REL_HWHEEL, so horizontal scrolling inside the apps works normally, and the tape only moves with SUPER held.

The binds:scroll_event_delay ceiling (300 ms, ~3 firings/s) stopped being a problem: it was fatal for smooth PIXEL scrolling, but for a COLUMN jump 3/s is plenty, all the more with column_width=1.0, where one column is already the whole screen.

The VERTICAL wheel moves along the tape too. It used to switch workspaces, which was never used, since a workspace is always keyboard (SUPER+1-8 or TAB). With no SUPER the wheel behaves normally in the apps. Scrolling DOWN goes right and UP goes left, like reading a page. The first mapping was the opposite and was flipped by hand on 17/10/2026.

Reordering, resizing and the view modes

swapcol moves the WHOLE COLUMN, the stack along with it, and wraps at the ends. To move a single window out of a stack, expel (SUPER+O) first.

SUPER+ALT+,/. cycles the width of ONLY the active column. The CTRL trio changes the WHOLE tape:

BindMessageWhat it does
SUPER+CTRL+Gfit allSEE EVERYTHING: resizes and repositions every column so they all fit
SUPER+CTRL+.colresize all 1.0FOCUS: everything at 100%, one window per screen
SUPER+CTRL+,colresize all 0.5everything at 50%, two side by side, fixed

fit all differs from colresize all 0.5, which forces 2-per-screen even with 5 windows open. And careful: colresize all N on its own does NOT bring the view along, since shrinking 2 columns to 0.5 left both off screen, to the left. fit all does both things.

fit active and NOT fit_into_view: the wiki documents the second, but 0.55.4 answers "no such layoutmsg for scrolling". Every layout message requires a FOCUSED window; with no focus they return "no focused window" and do nothing.

"Arrange side by side", in one click

SUPER+middle click. It exists because on scrolling EVERY way of moving a window sideways (dragging, window.swap, window.move) STACKS it into the destination column. Checked on all three, and it is hardcoded in CScrollingAlgorithm. There is no horizontal drop.

So instead of fighting the drag, the bind UNDOES and normalizes: expel takes the window out of any stack, into a column of its own, and fit all shows the whole tape on the screen.

It needs a lambda plus hl.dispatch because a bind accepts only ONE dispatcher, and here there are two in order.

The mouse RESIZE (SUPER+right drag) is what puts one window beside another with the mouse: scrolling implements a real resize-drag (the left border keeps the right one still by adjusting the camera; the right border keeps the left one fixed), and shrinking the column reveals its neighbor. The DRAG does not do that.

Brightness, and the parenthesis that stopped working

There are no brightness keys on this keyboard, and no backlight on this desktop, so "brightness" is hyprsunset's gamma with Quickshell's own OSD. SUPER+VolUp is brighter, SUPER+VolDown is darker, and SUPER+SHIFT+B resets to 100%.

The reset used to be SHIFT+code:19, the physical 0 key. Binding that CONSUMED the keystroke, and on ABNT2 ) is Shift+0, so there was no closing a parenthesis. SUPER+SHIFT+B steals no typing key at all.

SUPER alone opens the NixOS Center

SUPER + SUPER_L with { release = true, click = true }, the shape the 0.56 wiki gives for a modifier on its own. release waits for the key to come up, and click drops it when the mouse moved past binds:drag_threshold, so SUPER plus a window drag does not open the Center. What the wiki does NOT say is whether a release after a combination (SUPER+Q) or after the MX Master's thumbwheel (SUPER plus the horizontal wheel) still fires it. Checked on the desk on 22/10/2026: releasing SUPER does NOT open the Center after SUPER+Q, after the thumbwheel, or after a SUPER window drag. If that ever changes, the fallback is a plain combination on the same command.

Small things worth not rediscovering

  • locked = true means the bind works with the screen locked; repeating = true means it repeats while held. Media play/pause/next/prev get locked only, since repeating makes no sense there.
  • SUPER+P (pseudo) is a dwindle thing and a no-op on scrolling, but it answers ok and raises no toast, so it stays harmless.
  • SUPER+L goes loginctl to logind to hypridle's lock_cmd, and never duplicates the lock. See lockscreen.md.
  • The screenshot submap's 1/2 go by ROLE: 1 is the MAIN panel and 2 the secondary, the same numbering sc1/sc2 and wp1/wp2 use. See flameshot.md.
  • SUPER+F9 toggles the hyprsunset SERVICE; the other three F9 binds are one-off IPC overrides that hold until the schedule's next profile takes over. See hyprsunset.md.
  • Relative workspace navigation (SUPER+TAB) is KEYBOARD ONLY: the mouse wheel left here and went to the tape, since switching workspaces on scroll was never used and the tape is what makes you want to scroll with the mouse.