pragma Singleton import QtQuick import Quickshell import Quickshell.Io import Quickshell.Hyprland import qs.Commons import qs.Services Singleton { id: root property string currentLayout: "Unknown" property int updateInterval: 1000 // Update every second // Timer to periodically update the layout Timer { id: updateTimer interval: updateInterval running: true repeat: true onTriggered: { updateLayout() } } // Process to get current keyboard layout using niri msg (Wayland native) Process { id: niriLayoutProcess running: false command: ["niri", "msg", "-j", "keyboard-layouts"] stdout: StdioCollector { onStreamFinished: { try { const data = JSON.parse(text) const layoutName = data.names[data.current_idx] root.currentLayout = extractLayoutCode(layoutName) } catch (e) { root.currentLayout = "Unknown" } } } } // Process to get current keyboard layout using hyprctl (Hyprland) Process { id: hyprlandLayoutProcess running: false command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { onStreamFinished: { try { const data = JSON.parse(text) // Find the main keyboard and get its active keymap const mainKeyboard = data.keyboards.find(kb => kb.main === true) if (mainKeyboard && mainKeyboard.active_keymap) { root.currentLayout = extractLayoutCode(mainKeyboard.active_keymap) } else { root.currentLayout = "Unknown" } } catch (e) { root.currentLayout = "Unknown" } } } } // Process for X11 systems using setxkbmap Process { id: x11LayoutProcess running: false command: ["setxkbmap", "-query"] stdout: StdioCollector { onStreamFinished: { try { const lines = text.split('\n') for (const line of lines) { if (line.startsWith('layout:')) { const layout = line.split(':')[1].trim() root.currentLayout = layout return } } root.currentLayout = "Unknown" } catch (e) { root.currentLayout = "Unknown" } } } } // Process for general Wayland using localectl (systemd) Process { id: localectlProcess running: false command: ["localectl", "status"] stdout: StdioCollector { onStreamFinished: { try { const lines = text.split('\n') for (const line of lines) { if (line.includes('X11 Layout:')) { const layout = line.split(':')[1].trim() if (layout && layout !== "n/a") { root.currentLayout = layout return } } if (line.includes('VC Keymap:')) { const keymap = line.split(':')[1].trim() if (keymap && keymap !== "n/a") { root.currentLayout = extractLayoutCode(keymap) return } } } root.currentLayout = "Unknown" } catch (e) { root.currentLayout = "Unknown" } } } } // Process for generic keyboard layout detection using gsettings (GNOME-based) Process { id: gsettingsProcess running: false command: ["gsettings", "get", "org.gnome.desktop.input-sources", "current"] stdout: StdioCollector { onStreamFinished: { try { const currentIndex = parseInt(text.trim()) gsettingsSourcesProcess.running = true } catch (e) { fallbackToLocalectl() } } } } Process { id: gsettingsSourcesProcess running: false command: ["gsettings", "get", "org.gnome.desktop.input-sources", "sources"] stdout: StdioCollector { onStreamFinished: { try { // Parse the sources array and extract layout codes const sourcesText = text.trim() const matches = sourcesText.match(/\('xkb', '([^']+)'\)/g) if (matches && matches.length > 0) { // Get the first layout as default const layoutMatch = matches[0].match(/\('xkb', '([^']+)'\)/) if (layoutMatch) { root.currentLayout = layoutMatch[1].split('+')[0] // Take first part before any variants } } else { fallbackToLocalectl() } } catch (e) { fallbackToLocalectl() } } } } function fallbackToLocalectl() { localectlProcess.running = true } // Extract layout code from various format strings using Commons data function extractLayoutCode(layoutString) { if (!layoutString) return "Unknown" const str = layoutString.toLowerCase() // If it's already a short code (2-3 chars), return as-is if (/^[a-z]{2,3}(\+.*)?$/.test(str)) { return str.split('+')[0] } // Extract from parentheses like "English (US)" const parenMatch = str.match(/\(([a-z]{2,3})\)/) if (parenMatch) { return parenMatch[1] } // Check for exact matches or partial matches in language map from Commons const entries = Object.entries(KeyboardLayout.languageMap) for (var i = 0; i < entries.length; i++) { const lang = entries[i][0] const code = entries[i][1] if (str.includes(lang)) { return code } } // If nothing matches, try first 2-3 characters if they look like a code const codeMatch = str.match(/^([a-z]{2,3})/) return codeMatch ? codeMatch[1] : "unknown" } Component.onCompleted: { Logger.log("KeyboardLayout", "Service started") updateLayout() } function updateLayout() { // Try compositor-specific methods first if (CompositorService.isHyprland) { hyprlandLayoutProcess.running = true } else if (CompositorService.isNiri) { niriLayoutProcess.running = true } else { // Try detection methods in order of preference if (Qt.platform.os === "linux") { // Check if we're in X11 or Wayland const sessionType = Qt.application.arguments.find(arg => arg.includes("QT_QPA_PLATFORM")) || process.env.XDG_SESSION_TYPE if (sessionType && sessionType.includes("xcb") || process.env.DISPLAY) { // X11 system x11LayoutProcess.running = true } else { // Wayland or unknown - try gsettings first, then localectl gsettingsProcess.running = true } } else { currentLayout = "Unknown" } } } }