Ask for the keyboard layout only when the answer can change (#6727)

The widget polled hyprctl every 10 seconds per monitor, including on the single-layout install where it never shows. Keep the poll only where its answer can change - a seat with more than one keyboard, where Hyprland moves the main flag with no event to announce it - and stop it entirely once a one-keyboard seat has been read.

Also coalesce a refresh that arrives mid-query instead of dropping it, time out a query that never returns rather than letting it hold the guard shut for good, and re-read the layout on configreloaded.

Co-Authored-By: markbus-ai <markbus-ai@users.noreply.github.com>
This commit is contained in:
markbus-ai
2026-08-12 13:28:44 +02:00
committed by GitHub
co-authored by markbus-ai
parent 0fcdd716a1
commit 5c74f82300
+69 -10
View File
@@ -13,6 +13,10 @@ BarWidget {
property string layoutFull: ""
property string keyboardName: ""
// Real keyboards on the seat, virtual ones excluded, and whether the last
// reading left that shape in doubt.
property int keyboardCount: 0
property bool keyboardUnresolved: false
// Nothing to read or switch on the single-layout install most people run, so
// the widget ships on the bar and stays out of the way until there are two.
// An older Hyprland that doesn't report the list keeps showing the label.
@@ -22,16 +26,31 @@ BarWidget {
property var layoutBriefs: ({})
readonly property string layoutLabel: KeyboardLayoutModel.shortLabel(layoutFull, layoutBriefs)
// A query already in flight was started before this event, so it may read the
// layout the switch replaced. Remember the request and re-run once it lands
// rather than dropping it; nothing else would correct the label afterwards.
property bool refreshPending: false
function refresh() {
if (!queryProc.running) queryProc.running = true
if (queryProc.running) {
refreshPending = true
return
}
refreshPending = false
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"))
// Leave those out, so the label keeps tracking a keyboard someone types on.
function typedKeyboards(keyboards) {
return keyboards.filter(k => !String(k.name).startsWith("hl-virtual-keyboard"))
}
// Stay on the keyboard we last read until a real one is active again, so an
// input method holding the main flag freezes nothing.
function selectKeyboard(typed) {
return typed.find(k => k.main) ?? typed.find(k => k.name === root.keyboardName)
}
@@ -52,25 +71,49 @@ BarWidget {
target: Hyprland
function onRawEvent(event) {
if (!event || !event.name) return
if (String(event.name).indexOf("activelayout") !== -1) root.refresh()
var name = String(event.name)
// A reload that edits kb_layout changes both the label and whether the
// widget shows at all, and switches no layout, so it raises no
// activelayout of its own.
if (name.indexOf("activelayout") !== -1 || name === "configreloaded") root.refresh()
}
}
Process {
id: queryProc
command: ["hyprctl", "-j", "devices"]
onRunningChanged: {
if (running) {
stallTimer.restart()
return
}
stallTimer.stop()
if (root.refreshPending) root.refresh()
}
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
let kb
let typed
try {
kb = root.selectKeyboard(JSON.parse(text || "{}").keyboards ?? [])
typed = root.typedKeyboards(JSON.parse(text || "{}").keyboards ?? [])
} catch (e) {
return
}
if (!kb || !kb.active_keymap) return
const kb = root.selectKeyboard(typed)
if (!kb || !kb.active_keymap) {
// Keyboards are there but none of them answers to main, so the seat
// just changed shape under an input method holding the flag. Say so,
// rather than letting a count from before it changed settle the poll.
if (typed.length > 0) root.keyboardUnresolved = true
return
}
// A query the watchdog killed reports nothing at all, so only a reading
// that named a keyboard gets to speak for the seat.
root.keyboardUnresolved = false
root.keyboardCount = typed.length
root.keyboardName = String(kb.name || "")
root.multipleLayouts = kb.layout === undefined || String(kb.layout).indexOf(",") !== -1
root.layoutFull = kb.active_keymap
@@ -97,9 +140,25 @@ BarWidget {
onTriggered: root.refresh()
}
// A query that never returns would freeze the label until the shell restarts,
// since a Process that is already running can't be re-run. Give up on one that
// overstays so the next refresh gets through.
Timer {
id: stallTimer
interval: 5000
onTriggered: queryProc.running = false
}
// Hyprland hands the seat's main flag to whichever keyboard was typed on last
// and announces nothing when it moves, so a seat holding more than one keyboard
// can only learn which one the label is describing by asking. Poll while there
// is that ambiguity, until a first reading lands so a query that failed at login
// still recovers, and while a reading has left the seat's shape in doubt. The
// one-keyboard install has none of those, and is left alone rather than spawning
// hyprctl forever for an answer that cannot change.
Timer {
interval: 10000
running: true
running: !root.keyboardName || root.keyboardUnresolved || root.keyboardCount > 1
repeat: true
onTriggered: root.refresh()
}