My Keyboard Knob Switches Workspaces Now
Sep 6, 2026
I bought an RK M75 back in 2024 - 75%, gasket mount, a metal knob on the top right that ships bound to volume - and mostly just typed on it for two years. Lately I've been going through my setup looking for ergonomics wins, and the knob was the obvious one: a physical control that only turns volume up and down is a waste of a knob. I wanted it switching xmonad workspaces.
First instinct: the vendor software
RK ships a web-based configurator ("RK Launcher") that talks to the keyboard over WebHID - point Chrome at it, no driver install, works on Linux in theory. It shows a picture of your keyboard; click any key to remap it.
Click the knob, though, and its icon just sits there greyed out. Tried wired instead of wireless, checked for a firmware update - still locked. Whatever RK exposes for remapping normal keys, the knob isn't part of it on this model.
The actual realization
Before going further down that hole I looked at my own xmonad.hs and found this already sitting there:
} `additionalKeysP` [ ( "<XF86AudioRaiseVolume>" , spawn "pactl set-sink-volume @DEFAULT_SINK@ +5%" )
, ( "<XF86AudioLowerVolume>" , spawn "pactl set-sink-volume @DEFAULT_SINK@ -5%" )
...
The knob was never going through RK's software at all. It's a plain HID media key - turning it just emits XF86AudioRaiseVolume/XF86AudioLowerVolume, the same keysyms any keyboard's volume keys send - and xmonad's own key grab was already intercepting those, system-wide, no daemon involved. The greyed-out launcher UI was a dead end that didn't need solving.
The fix
Repoint those two bindings from pactl to XMonad.Actions.CycleWS:
} `additionalKeysP` [ ( "<XF86AudioRaiseVolume>" , moveTo Next (XMonad.Actions.CycleWS.Not emptyWS) )
, ( "<XF86AudioLowerVolume>" , moveTo Prev (XMonad.Actions.CycleWS.Not emptyWS) )
, ( "<XF86AudioMute>" , spawn "pactl set-sink-mute @DEFAULT_SINK@ toggle" )
, ( "<XF86AudioMicMute>" , spawn "pactl set-source-mute @DEFAULT_SOURCE@ toggle" )
, ( "<Print>" , spawn "flameshot gui" )
]
Turn the knob, cycle non-empty workspaces. mod-q to recompile and restart, done.
Getting volume back, and then some
X11 hands over modifier state along with the keysym, so I'm not limited to one action per direction - holding a modifier while turning the knob is a free extra layer:
} `additionalKeysP` [ ( "<XF86AudioRaiseVolume>" , moveTo Next (XMonad.Actions.CycleWS.Not emptyWS) )
, ( "<XF86AudioLowerVolume>" , moveTo Prev (XMonad.Actions.CycleWS.Not emptyWS) )
, ( "M-<XF86AudioRaiseVolume>" , spawn "pactl set-sink-volume @DEFAULT_SINK@ +5%" )
, ( "M-<XF86AudioLowerVolume>" , spawn "pactl set-sink-volume @DEFAULT_SINK@ -5%" )
, ( "M-C-<XF86AudioRaiseVolume>" , windows W.focusDown )
, ( "M-C-<XF86AudioLowerVolume>" , windows W.focusUp )
, ( "S-<XF86AudioRaiseVolume>" , spawn "xdotool key --clearmodifiers ctrl+Next" )
, ( "S-<XF86AudioLowerVolume>" , spawn "xdotool key --clearmodifiers ctrl+Prior" )
, ( "C-<XF86AudioRaiseVolume>" , sendMessage Expand )
, ( "C-<XF86AudioLowerVolume>" , sendMessage Shrink )
, ( "<XF86AudioMute>" , spawn "pactl set-sink-mute @DEFAULT_SINK@ toggle" )
, ( "<XF86AudioMicMute>" , spawn "pactl set-source-mute @DEFAULT_SOURCE@ toggle" )
, ( "<Print>" , spawn "flameshot gui" )
]
One physical control, five bound actions, zero new dependencies - xdotool for the tab-switch layer was already in my config driving clickable workspace labels in xmobar.
| Modifier held | Action |
|---|---|
| none | workspace next/prev |
| Alt | volume up/down |
| Alt+Ctrl | focus next/prev window |
| Shift | browser tab next/prev |
| Ctrl | expand/shrink master pane |
Keeping the docs honest
This config has always had a help.py that formats every keybinding into a table for a mod-Shift-/ rofi popup. It needed the knob layers added or it'd lie to future-me:
("-- knob (rotate CW = RaiseVolume event, CCW = LowerVolume event)", None),
("Knob", "Cycle to next/prev non-empty workspace"),
("mod-Knob", "Volume up/down 5%"),
("mod-ctrl-Knob", "Move focus to next/prev window"),
("shift-Knob", "Next/prev browser tab (ctrl+PageDown/Up via xdotool)"),
("ctrl-Knob", "Expand/shrink the master area"),
One dead end, for the record
I also looked at driving keyboard RGB from xmonad state - flash a color on an urgent window, tint by workspace. OpenRGB doesn't list the M75 as a supported device; the only path in is unofficial custom QMK/SignalRGB firmware built for the R75, not confirmed to even work on the M75. Not worth the brick risk for a cosmetic feature, so that one stays an idea.
Turns out the keyboard's own software was never the bottleneck - the WM already had its hands on the hardware the whole time.