diff --git a/.config/quickshell/ii/modules/bar/Bar.qml b/.config/quickshell/ii/modules/bar/Bar.qml index fbe9c184..f14a538d 100644 --- a/.config/quickshell/ii/modules/bar/Bar.qml +++ b/.config/quickshell/ii/modules/bar/Bar.qml @@ -459,6 +459,16 @@ Scope { color: rightSidebarButton.colText } } + Loader { + active: HyprlandXkb.layoutCodes.length > 1 + visible: active + Layout.rightMargin: indicatorsRowLayout.realSpacing + sourceComponent: StyledText { + text: HyprlandXkb.currentLayoutCode + font.pixelSize: Appearance.font.pixelSize.smaller + color: rightSidebarButton.colText + } + } MaterialSymbol { Layout.rightMargin: indicatorsRowLayout.realSpacing text: Network.materialSymbol diff --git a/.config/quickshell/ii/services/HyprlandXkb.qml b/.config/quickshell/ii/services/HyprlandXkb.qml new file mode 100644 index 00000000..76a7bd35 --- /dev/null +++ b/.config/quickshell/ii/services/HyprlandXkb.qml @@ -0,0 +1,109 @@ +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; + } + } + } +}