Files
omarchycn/shell/plugins/bar/widgets/KeyboardLayout.qml
T
9d61915b2e Fix KeyboardLayout plugin label permanently displaying ENG on some systems (#6646)
* Fix hyprctl output parsing in KeyboardLayout plugin

* Never fall back to a non-active keyboard for the layout label

find(k => k.main) returning nothing fell through to keyboards[0], which is
the case the fix is for: on hardware whose first device is a permanently
English (US) radio-control keyboard, the label was wrong and the 10s poll
kept it wrong. The seat can also hold no active keyboard while a device is
re-added, and older Hyprland has no main field at all. Keep the last known
value instead, and skip entries without an active_keymap, since assigning
undefined to the string property throws before the label is ever set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Run hyprctl directly from the keyboard layout widget

The shell wrapper only existed for a pipeline that is gone, so spawn the
command directly, as Style.qml already does for its own hyprctl query.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Read the layout from the keyboard the user types on

Every Omarchy install runs fcitx5 for ~/.XCompose, and it binds a virtual
keyboard that takes the seat's main flag whenever it injects. That keyboard
keeps the us layout the input method gave it, so on a machine configured for
another layout the widget flipped to ENG and the poll kept it there until the
next physical keypress. Skip virtual keyboards and hold the last known layout
instead, which the next poll corrects once a real keyboard is active again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep tracking the keyboard the layout was last read from

Holding a frozen label while fcitx5 owns the main flag went stale as soon as
the layout changed underneath it, and cycling still dispatched against
"current", which is that same virtual keyboard. Remember the keyboard the
label came from, re-read its layout on every poll, and cycle it by name so
the widget shows and switches one device.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 15:11:26 +02:00

96 lines
2.3 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import qs.Ui
import qs.Commons
BarWidget {
id: root
moduleName: "omarchy.keyboard-layout"
property string layoutLabel: ""
property string layoutFull: ""
property string keyboardName: ""
function refresh() {
if (!queryProc.running) queryProc.running = true
}
// fcitx5 binds a virtual keyboard and takes over the seat's main flag whenever
// it injects, but that keyboard keeps the us layout the input method gave it.
// Stay on the keyboard we last read until a real one is active again, so the
// label keeps tracking that keyboard's layout rather than freezing.
function selectKeyboard(keyboards) {
const typed = keyboards.filter(k => !String(k.name).startsWith("hl-virtual-keyboard"))
return typed.find(k => k.main) ?? typed.find(k => k.name === root.keyboardName)
}
function cycleLayout() {
if (!root.keyboardName) return
Hyprland.dispatch("switchxkblayout " + root.keyboardName + " next")
refreshTimer.restart()
}
Component.onCompleted: refresh()
Connections {
target: Hyprland
function onRawEvent(event) {
if (!event || !event.name) return
if (String(event.name).indexOf("activelayout") !== -1) root.refresh()
}
}
Process {
id: queryProc
command: ["hyprctl", "-j", "devices"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
let kb
try {
kb = root.selectKeyboard(JSON.parse(text || "{}").keyboards ?? [])
} catch (e) {
return
}
if (!kb || !kb.active_keymap) return
root.keyboardName = String(kb.name || "")
root.layoutFull = kb.active_keymap
root.layoutLabel = kb.active_keymap.split(/\s+/)[0].substring(0, 3).toUpperCase()
}
}
}
Timer {
id: refreshTimer
interval: 600
onTriggered: root.refresh()
}
Timer {
interval: 10000
running: true
repeat: true
onTriggered: root.refresh()
}
visible: layoutLabel !== ""
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.layoutLabel
fontSize: Style.font.caption
horizontalMargin: 6
tooltipText: root.layoutFull
onPressed: function() { root.cycleLayout() }
}
}