← back

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.