Switch keyboard layouts in Omarchy / Hyprland with a keybind, and get a native OSD card telling you which one you landed on.
It comes out of a concrete setup: a ThinkPad with kb_layout = "us,latam",
fcitx5 as the input method, and a dock with an external USB keyboard. In that
setup both of the obvious approaches fail, in ways you don't see.
grp:alt_shift_toggle isn't enough. The xkb option switches the group of
the physical keyboard the keys were pressed on. But with an input method
(fcitx5, ibus) keystrokes reach applications through a virtual keyboard —
hl-virtual-keyboard-fcitx5, which Hyprland marks main: true — and that one
never heard about it. The result: your bar indicator says ES and you keep
typing in US.
hyprctl switchxkblayout <device> next doesn't either, for the same
reason, plus a second problem: you have to pick which device, and Hyprland
reports plenty of things as keyboards that nobody types on. On this machine:
$ hyprctl devices -j | jq -r '.keyboards[] | "\(.active_layout_index) \(.name)"'
0 video-bus
0 intel-hid-events
0 power-button
0 sleep-button
0 at-translated-set-2-keyboard <- the only real one on the laptop
0 thinkpad-extra-buttons
0 dell-kb216-wired-keyboard <- the one on the dock
0 dell-kb216-wired-keyboard-consumer-control
0 dell-kb216-wired-keyboard-system-control
0 lenovo-thinkpad-usb-c-dock-gen2-usb-audio
1 hl-virtual-keyboard-fcitx5
ACPI buttons, an audio device from the dock, and virtual keyboards — all of
them answering to switchxkblayout, and all of them able to hold the main
flag.
Switch the whole seat with all. Every device advances in lockstep — the
fcitx5 virtual keyboard included, which is the one that decides what actually
gets typed — so none of them can drift from the rest.
Read back where it landed instead of predicting it. switchxkblayout
doesn't report the result, and the seat can move without you: omarchy-system-lock
runs hyprctl switchxkblayout all 0 on every lock, so after unlocking you're
on the first layout in kb_layout without having touched anything. A script
keeping its own count would start lying from there on.
Filter before reading. Devices nobody types on are dropped by name, and so are devices with a single layout: a one-layout device can't change layout, so it's never the one worth reading. That second filter is what makes this hold up without maintaining a list of device names.
Omarchy exposes its OSD — the volume/brightness card — over IPC, so no
notify-send or anything external is needed:
omarchy-shell -q osd show '{"icon":"keyboard","message":"Español Latam","duration":1200}'iconis a logical name, not a glyph. The table lives in/usr/share/omarchy/shell/plugins/osd/OsdModel.js→iconFor(). Besideskeyboard:volume,microphone,brightness,touchpad,reboot,shutdown,logout,media-play… A name that isn't in the table is passed through as text, so you can also send a Nerd Font glyph directly.messageandvalueare mutually exclusive: either a progress bar, or text.- Text elides past ~190px, so keep the label short.
-qkeeps a dead shell from breaking the script: the layout switch already happened, and failing because you couldn't announce it makes no sense.
Requires jq. The OSD requires Omarchy; without it the script still switches
the layout, just silently.
curl -o ~/.local/bin/hypr-keyboard-layout-cycle \
https://gist.githubusercontent.com/elranu/ce49c551d9f4fbf0f58efedd2d1d3da3/raw/hypr-keyboard-layout-cycle
chmod +x ~/.local/bin/hypr-keyboard-layout-cycleThe layouts themselves come from ~/.config/hypr/input.lua:
hl.config({
input = {
kb_layout = "us,latam",
},
})And the keybind from ~/.config/hypr/bindings.lua. Pick the key with some
care — a Hyprland bind is grabbed at the compositor, so it is taken away from
every application. Bare CTRL + SPACE is the tempting one and the wrong one:
that is fcitx5's own default trigger, and autocomplete in most editors.
SUPER + CTRL + SPACE stays clear of applications. Omarchy ships its
background switcher there, so move that out of the way first:
hl.unbind("SUPER + CTRL + SPACE")
o.bind("SUPER + CTRL + SPACE", "Cycle keyboard layout", "hypr-keyboard-layout-cycle")
o.bind("SUPER + B", "Background switcher", "omarchy-menu toggle background")hypr-keyboard-layout-cycle # switch to the next layout and show the card
hypr-keyboard-layout-cycle --show # only show the card, change nothingTo give other layouts friendly names, edit the case in the script. Anything
not listed there is announced by its xkb code in uppercase.
If you also run the omarchy.keyboard-layout bar widget, watch out for one
interaction: the widget picks which keyboard to read by taking the one
furthest along the layout list, and when they're all at index 0 that resolves
to whichever hyprctl returns first. If that happens to be a pseudo-keyboard
pinned to a single layout, the widget concludes there's only one layout and
hides itself from the bar entirely — no error, nothing in the logs.
The fix is the same filter this script uses: drop single-layout devices from
the selection. It means cloning the widget with
omarchy plugin clone omarchy.keyboard-layout and editing selectKeyboard()
in the copy's KeyboardLayoutModel.js.