I use the DVORAK keyboard layout for all work and typing in general, but while gaming on linux that means that I need to manually rebind the game controls in strange ways that don’t make a lot of sense when looking at the settings menu, only via muscle memory. So I made a script that automatically switches to QWERTY when Steam games are launched

Prerequisites
  • python3, setxkbmap
1. Niri config → register keyboard layouts

In ~/.config/niri/cfg/input.kdl, change the xkb layout to expose both Dvorak and QWERTY.

xkb {
	layout "us, us"    // Index 0 = Dvorak, Index 1 = QWERTY
	variant "dvorak,"
}
2. Script → auto-switch daemon for Steam games

~/.local/bin/niri-game-layout (chmod +x)

#!/usr/bin/env python3
import json
import subprocess
 
QWERTY = "1"
DVORAK = "0"
 
def switch_layout(index):
	subprocess.run(["niri", "msg", "action", "switch-layout", index], capture_output=True)
	if index == QWERTY:
		subprocess.run(["setxkbmap", "us"], capture_output=True)
	else:
		subprocess.run(["setxkbmap", "us", "dvorak"], capture_output=True)
 
def is_steam_game(app_id):
	return bool(app_id and app_id.startswith("steam_app_"))
	
def main():
	steam_windows = set()
	proc = subprocess.Popen(
		["niri", "msg", "-j", "event-stream"],
		stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True,
	)
	try:
		for line in proc.stdout:
			line = line.strip()
			if not line:
				continue
			try:
				event = json.loads(line)
			except json.JSONDecodeError:
				continue
			
			was_gaming = bool(steam_windows)
			
			if "WindowsChanged" in event:
				steam_windows = {
					w["id"] for w in event["WindowsChanged"]["windows"]
					if is_steam_game(w.get("app_id"))
				}
			elif "WindowOpenedOrChanged" in event:
				w = event["WindowOpenedOrChanged"]["window"]
				if is_steam_game(w.get("app_id")):
					steam_windows.add(w["id"])
				elif "WindowClosed" in event:
					steam_windows.discard(event["WindowClosed"]["id"])
					
				is_gaming = bool(steam_windows)
				if is_gaming and not was_gaming:
					switch_layout(QWERTY)
				elif not is_gaming and was_gaming:
					switch_layout(DVORAK)
		except KeyboardInterrupt:
			pass
		finally:
			proc.terminate()
			if steam_windows:
				switch_layout(DVORAK)
	
if __name__ == "__main__":
	main()
3. Script → manual toggle keybind

In case the auto-switch doesn’t work: ~/.local/bin/niri-toggle-layout (chmod +x)

#!/bin/sh
current=$(niri.msg -j keyboard-layouts | python3 -c "import json,sys; print(json.load(sys.stdin)['current_idk'])")
if [ "$current" = "0" ]; then
	niri msg action switch-layout 1
	setxkbmap us
else
	niri msg action switch-layout 0
	setxkbmap us dvorak
fi
4. Niri config → autostart the daemon

In ~/.config/niri/cfg/autostart.kdl (or equivalent):

spawn-sh-at-startup "/home/USERNAME/.local/bin/niri-game-layout &"
5. Niri config → keybind for manual toggle

In ~/.config/niri/cfg/keybinds.kdl:

Mod+F10 { spawn-sh "/home/USERNAME/.local/bin/niri-toggle-layout"; }

Must use a function key → letter-key binds are keysym-based, so the physical key changes between layouts and the bind stops working in QWERTY mode. F-keys are layout-independent.

6. Fish function → for non-Steam games (optional)

~/.config/fish/functions/game.fish

function game --description "Launch a program with US QWERTY layout, restoring Dvorak on exit"
	niri msg action switch-layout 1
	$argv
	niri msg action switch-layout 0
end

Usage: game ./some-game or game lutris