pragma Singleton import QtQuick import Quickshell import Quickshell.Io import Quickshell.Hyprland /** * Exposes the active Hyprland Xkb keyboard layout name and code for indicators. */ Singleton { id: root // You can read these property list layoutCodes: [] property var cachedLayoutCodes: ({}) property string currentLayoutName: "" property string currentLayoutCode: "" // For the service property string targetDeviceName: "hl-virtual-keyboard" property var baseLayoutFilePath: "/usr/share/X11/xkb/rules/base.lst" property bool needsLayoutRefresh: false // Update the layout code according to the layout name (Hyprland gives the name not the code) onCurrentLayoutNameChanged: root.updateLayoutCode() function updateLayoutCode() { if (cachedLayoutCodes.hasOwnProperty(currentLayoutName)) { root.currentLayoutCode = cachedLayoutCodes[currentLayoutName]; } else { getLayoutProc.running = true; } } // Get the layout code from the base.lst file by grabbing the line with the current layout name Process { id: getLayoutProc command: ["cat", root.baseLayoutFilePath] stdout: StdioCollector { id: layoutCollector onStreamFinished: { const lines = layoutCollector.text.split("\n"); const targetDescription = root.currentLayoutName; const foundLine = lines.find(line => { // Skip comment lines and empty lines if (!line.trim() || line.trim().startsWith('!')) return false; // Match: key + whitespace + description const match = line.match(/^\s*(\S+)\s+(.+)$/); if (match && match[2] === targetDescription) { root.cachedLayoutCodes[match[2]] = match[1]; root.currentLayoutCode = match[1]; return true; } }); // console.log("[HyprlandXkb] Found line:", foundLine); // console.log("[HyprlandXkb] Layout:", root.currentLayoutName, "| Code:", root.currentLayoutCode); // console.log("[HyprlandXkb] Cached layout codes:", JSON.stringify(root.cachedLayoutCodes, null, 2)); } } } // Find out available layouts and current active layout. Should only be necessary on init Process { id: fetchLayoutsProc running: true command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { id: devicesCollector onStreamFinished: { const parsedOutput = JSON.parse(devicesCollector.text); const hyprlandKeyboard = parsedOutput["keyboards"].find(kb => kb.name === root.targetDeviceName); root.layoutCodes = hyprlandKeyboard["layout"].split(","); root.currentLayoutName = hyprlandKeyboard["active_keymap"]; // console.log("[HyprlandXkb] Fetched | Layouts (multiple: " + (root.layouts.length > 1) + "): " // + root.layouts.join(", ") + " | Active: " + root.currentLayoutName); } } } // Update the layout name when it changes Connections { target: Hyprland function onRawEvent(event) { if (event.name === "activelayout") { // We're triggering refresh here because Hyprland virtual kb after a config reload disappears // from `hyprctl devices` and it only comes back at the next activelayout event. if (root.needsLayoutRefresh) { root.needsLayoutRefresh = false; fetchLayoutsProc.running = true; } // If there's only one layout, the updated layout is always the same if (root.layoutCodes.length <= 1) return; // Update when layout might have changed const dataString = event.data; if (!dataString.startsWith(root.targetDeviceName)) return; root.currentLayoutName = dataString.split(",")[1]; } else if (event.name == "configreloaded") { // Mark layout code list to be updated when config is reloaded root.needsLayoutRefresh = true; } } } }