pragma Singleton // The active keyboard layout, for the bar. // // Only interesting on a machine with more than one layout configured -- a // single-layout machine gets no indicator at all, which is GNOME's behavior // too. "More than one" is read straight from the keyboardLayout preference, // the same value input.lua feeds the compositor, so no probe can disagree // with the config. // // Hyprland reports the active keymap by DESCRIPTION ("English (US)"), and // the bar wants the short code ("us"). Deriving one from the other by // truncation gives wrong answers immediately -- "German" is de, not "ge" -- // so the mapping comes from xkb's own registry (evdev.lst), the file every // layout chooser is built from. Parsed once; it changes when xkeyboard-config // updates, which is to say between logins. import Quickshell import Quickshell.Io import Quickshell.Hyprland import QtQuick import qs.config Singleton { id: root readonly property var configured: String(DesktopPreferences.get("keyboardLayout")) .split(",").map(code => code.trim()).filter(code => code !== "") readonly property bool multiple: root.configured.length > 1 // "English (US)", as Hyprland reports it. property string activeKeymap: "" // description -> code, from evdev.lst's layout and variant sections. property var codeByDescription: ({}) readonly property string shortLabel: { const mapped = root.codeByDescription[root.activeKeymap]; if (mapped) return mapped; // Registry miss (a custom keymap): fall back to the first configured // code, which at least names something real on this machine. return root.configured[0] ?? ""; } function refresh(): void { if (!query.running) query.running = true; } Process { id: query command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { onStreamFinished: { try { const keyboards = JSON.parse(this.text).keyboards ?? []; const main = keyboards.find(keyboard => keyboard.main) ?? keyboards[0]; root.activeKeymap = String(main?.active_keymap ?? ""); } catch (error) { console.warn("KeyboardLayout: could not parse hyprctl devices:", error); } } } } FileView { path: "/usr/share/X11/xkb/rules/evdev.lst" blockLoading: false printErrors: false onLoaded: { const map = {}; let section = ""; for (const line of this.text().split("\n")) { if (line.startsWith("!")) { section = line.slice(1).trim(); continue; } const trimmed = line.trim(); if (trimmed === "") continue; if (section === "layout") { // " us English (US)" const m = /^(\S+)\s+(.+)$/.exec(trimmed); if (m) map[m[2].trim()] = m[1]; } else if (section === "variant") { // " dvorak us: English (Dvorak)" const m = /^\S+\s+(\S+):\s+(.+)$/.exec(trimmed); if (m) map[m[2].trim()] = m[1]; } } root.codeByDescription = map; } } // activelayout fires on every switch; configreloaded catches a layout // list changed from Settings. Connections { target: Hyprland function onRawEvent(event: var): void { if (event.name === "activelayout" || event.name === "configreloaded") root.refresh(); } } Component.onCompleted: root.refresh() }