Skip to content

Instantly share code, notes, and snippets.

@elranu
Last active September 2, 2026 13:27
Show Gist options
  • Select an option

  • Save elranu/ce49c551d9f4fbf0f58efedd2d1d3da3 to your computer and use it in GitHub Desktop.

Select an option

Save elranu/ce49c551d9f4fbf0f58efedd2d1d3da3 to your computer and use it in GitHub Desktop.
Omarchy/Hyprland: cycle keyboard layouts and announce the result on the native OSD (works with fcitx5 and docked external keyboards)

hypr-keyboard-layout-cycle

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.

The problem

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.

The approach

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.

The OSD card

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}'
  • icon is a logical name, not a glyph. The table lives in /usr/share/omarchy/shell/plugins/osd/OsdModel.js → iconFor(). Besides keyboard: 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.
  • message and value are mutually exclusive: either a progress bar, or text.
  • Text elides past ~190px, so keep the label short.
  • -q keeps a dead shell from breaking the script: the layout switch already happened, and failing because you couldn't announce it makes no sense.

Installation

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-cycle

The 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")

Usage

hypr-keyboard-layout-cycle          # switch to the next layout and show the card
hypr-keyboard-layout-cycle --show   # only show the card, change nothing

To give other layouts friendly names, edit the case in the script. Anything not listed there is announced by its xkb code in uppercase.

Bonus: the bar widget

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.

#!/usr/bin/env bash
#
# Pasa al siguiente layout de teclado en Hyprland y lo anuncia en el OSD de
# Omarchy. El por que de cada decision esta en el README del gist:
# https://gist.github.com/elranu/ce49c551d9f4fbf0f58efedd2d1d3da3
#
# hypr-keyboard-layout-cycle cambia al siguiente y muestra la card
# hypr-keyboard-layout-cycle --show solo muestra la card, no cambia nada
set -uo pipefail
# "all" y no un dispositivo: con un metodo de entrada (fcitx5, ibus) lo que
# realmente se tipea sale por su teclado virtual, asi que mover solo el teclado
# fisico cambia el indicador y no las teclas.
[[ "${1:-}" == "--show" ]] || hyprctl switchxkblayout all next >/dev/null
# Se lee de vuelta en vez de predecir: switchxkblayout no informa donde quedo, y
# la sesion se puede mover por afuera (omarchy-system-lock corre
# `switchxkblayout all 0` en cada bloqueo).
#
# Hyprland reporta como teclados a botones ACPI y teclados virtuales; y un
# dispositivo clavado a un solo layout no cambia de layout, asi que ninguno de
# los dos es el que hay que leer.
code=$(hyprctl devices -j | jq -r '
[ .keyboards[]
| select(.name | test("^(hl-virtual-keyboard|power-button|sleep-button|lid-switch|video-bus)") | not)
| select(.layout | contains(","))
]
| .[0] as $kb
| if $kb == null then "" else ($kb.layout | split(",")[$kb.active_layout_index]) end')
# Nombres lindos por codigo xkb. Agregar los que hagan falta segun el kb_layout
# de cada uno; lo que no este aca se anuncia con su codigo en mayusculas, que
# dice menos pero nunca queda vacio. Ojo que el OSD elide pasados ~190px.
case "$code" in
"") exit 0 ;;
us) label="Inglés US" ;;
latam) label="Español Latam" ;;
es) label="Español ES" ;;
*) label="${code^^}" ;;
esac
# -q para que un shell caido no haga fallar el cambio de layout, que ya ocurrio.
omarchy-shell -q osd show "$(jq -nc --arg m "$label" \
'{icon: "keyboard", message: $m, duration: 1200}')"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment