From a6f487eac06732b299b32d7784d37903231e4242 Mon Sep 17 00:00:00 2001 From: Ala Alkhafaji <3akevdev@gmail.com> Date: Tue, 11 Nov 2025 01:53:49 +0100 Subject: [PATCH] KeyboardLayout: stopgap solution for sway --- Services/Compositor/SwayService.qml | 82 +++++++++++ Services/Keyboard/KeyboardLayoutService.qml | 142 -------------------- 2 files changed, 82 insertions(+), 142 deletions(-) diff --git a/Services/Compositor/SwayService.qml b/Services/Compositor/SwayService.qml index e5ac8f6e..582c331f 100644 --- a/Services/Compositor/SwayService.qml +++ b/Services/Compositor/SwayService.qml @@ -4,6 +4,7 @@ import Quickshell.I3 import Quickshell.Wayland import Quickshell.Io import qs.Commons +import qs.Services.Keyboard Item { id: root @@ -37,10 +38,12 @@ Item { try { I3.refreshWorkspaces() + I3.dispatch('(["input"])') Qt.callLater(() => { safeUpdateWorkspaces() safeUpdateWindows() queryDisplayScales() + queryKeyboardLayout() }) initialized = true Logger.i("SwayService", "Service started") @@ -109,6 +112,60 @@ Item { } } + Timer { + id: keyboardLayoutUpdateTimer + interval: 1000 + running: true + repeat: true + onTriggered: { + queryKeyboardLayout() + } + } + + function queryKeyboardLayout() { + swayInputsProcess.running = true + } + // Sway inputs process for keyboard layout detection + Process { + id: swayInputsProcess + running: false + command: ["swaymsg", "-t", "get_inputs", "-r"] + + property string accumulatedOutput: "" + + stdout: SplitParser { + onRead: function (line) { + // Accumulate lines instead of parsing each one + swayInputsProcess.accumulatedOutput += line + } + } + + onExited: function (exitCode) { + if (exitCode !== 0 || !accumulatedOutput) { + Logger.e("SwayService", "Failed to query inputs, exit code:", exitCode) + accumulatedOutput = "" + return + } + + try { + const inputsData = JSON.parse(accumulatedOutput) + for (const input of inputsData) { + if (input.type == "keyboard") { + const layoutName = input.xkb_active_layout_name + KeyboardLayoutService.setCurrentLayout(layoutName) + Logger.d("SwayService", "Keyboard layout switched:", layoutName) + break + } + } + } catch (e) { + Logger.e("SwayService", "Failed to parse inputs:", e) + } finally { + // Clear accumulated output for next query + accumulatedOutput = "" + } + } + } + // Safe update wrapper function safeUpdate() { safeUpdateWindows() @@ -234,6 +291,27 @@ Item { return defaultValue } + function handleInputEvent(ev) { + try { + let beforeParenthesis + const parenthesisPos = ev.lastIndexOf('(') + + if (parenthesisPos === -1) { + beforeParenthesis = ev + } else { + beforeParenthesis = ev.substring(0, parenthesisPos) + } + + const layoutNameStart = beforeParenthesis.lastIndexOf(',') + 1 + const layoutName = ev.substring(layoutNameStart) + + KeyboardLayoutService.setCurrentLayout(layoutName) + Logger.d("HyprlandService", "Keyboard layout switched:", layoutName) + } catch (e) { + Logger.e("HyprlandService", "Error handling activelayout:", e) + } + } + // Connections to I3 Connections { target: I3.workspaces @@ -263,6 +341,10 @@ Item { if (event.type === "output") { Qt.callLater(queryDisplayScales) } + + if (event.type == "get_inputs") { + handleInputEvent(event.data) + } } } diff --git a/Services/Keyboard/KeyboardLayoutService.qml b/Services/Keyboard/KeyboardLayoutService.qml index 65423956..c449d300 100644 --- a/Services/Keyboard/KeyboardLayoutService.qml +++ b/Services/Keyboard/KeyboardLayoutService.qml @@ -12,122 +12,6 @@ Singleton { property string currentLayout: I18n.tr("system.unknown-layout") property string previousLayout: "" property bool isInitialized: false - 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 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 = I18n.tr("system.unknown-layout") - } catch (e) { - root.currentLayout = I18n.tr("system.unknown-layout") - } - } - } - } - - // 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 = I18n.tr("system.unknown-layout") - } catch (e) { - root.currentLayout = I18n.tr("system.unknown-layout") - } - } - } - } - - // 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 - } // Updates current layout from various format strings. Called by compositors function setCurrentLayout(layoutString) { @@ -186,7 +70,6 @@ Singleton { Component.onCompleted: { Logger.i("KeyboardLayout", "Service started") - updateLayout() // Mark as initialized after a delay to allow first layout update to complete // This prevents showing a toast on the initial load initializationTimer.start() @@ -203,31 +86,6 @@ Singleton { } } - function updateLayout() { - // Try compositor-specific methods first - if (CompositorService.isHyprland) { - - } else if (CompositorService.isNiri) { - - } 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 = I18n.tr("system.unknown-layout") - } - } - } - // Comprehensive language name to ISO code mapping property var languageMap: { "english"// English variants