Label the keyboard widget with the xkb language code (#6699)

* Label the keyboard widget with the xkb language code

The label was the first word of the layout description cut to three
characters, so a US layout read ENG and a Portuguese one read POR.

xkb already pairs every layout and variant with a short language code,
which is the code GNOME shows in its own indicator. Read that table once
at startup from xkbcli list and key it by description, which is what
hyprctl reports as the active keymap, so the same layouts read EN and PT.

The code is a language rather than a country, so it stays sensible for
the layouts named after neither: Esperanto is EO, Arabic is AR, and Latin
American Spanish is ES. Layouts missing from the table keep the old
truncated description.

* Read the exotic xkb rulesets for the keyboard label

xkbcli list leaves out the exotic rulesets, so layouts like trans were
missing from the table and fell back to the truncated description: the
IPA layout read INT rather than IPA. Those layouts ship in the same
xkeyboard-config package and set just as well, so read them too.

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

* Keep the keyboard label to three characters

The brief was used verbatim while the fallback was truncated, but not
every brief is two or three characters: Burmese (Zawgyi) is my-zwg and
Shan (Zawgyi) is shn-zwg. Selecting either widened the widget past its
neighbours on the bar. Drop the script suffix and cap the brief the same
way the fallback is capped, so those read MY and SHN.

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

* Stop an xkb brief carrying past its own block

The brief was only cleared once a description consumed it, so a block
printing a brief without one would hand its code to the next block's
description and label it wrongly rather than falling back. Nothing in
the current xkb data does that, and the option groups were skipped only
because the last layout happened to consume its brief first. Clear the
brief when a line starts a new block so the pairing is explicit, and
cover the option list the 2-space match is what keeps out.

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

* Fall back when a layout description names a built-in

A custom xkb group called constructor or toString reached an inherited
member of the lookup rather than a brief, and splitting it threw a
TypeError that took the whole label binding down instead of falling back
to the truncated description. Take the lookup only when it returns a
string.

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>
This commit is contained in:
Diogo Ferreira
2026-08-11 13:05:11 +02:00
committed by GitHub
co-authored by Claude Opus 5 David Heinemeier Hansson
parent 5edc3497fa
commit 66f3155f0c
4 changed files with 180 additions and 3 deletions
+88
View File
@@ -0,0 +1,88 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const model = requireFromRoot('shell/plugins/bar/widgets/KeyboardLayoutModel.js')
// Trimmed from xkbcli list, keeping the format of every section it prints,
// including the options nested under an option group: those quote their
// description and print an empty brief, one indent deeper than a layout's.
const listing = [
'models:',
'- name: pc105',
' vendor: Generic',
' description: Generic 105-key PC',
'layouts:',
"- layout: 'us'",
" variant: ''",
" brief: 'en'",
' description: English (US)',
" iso639: ['eng']",
" iso3166: ['USA']",
"- layout: 'us'",
" variant: 'intl'",
" brief: 'en'",
' description: English (US, intl., with dead keys)',
"- layout: 'br'",
" variant: ''",
" brief: 'pt'",
' description: Portuguese (Brazil)',
"- layout: 'epo'",
" variant: ''",
" brief: 'eo'",
' description: Esperanto',
"- layout: 'latam'",
" variant: ''",
" brief: 'es'",
' description: Spanish (Latin American)',
"- layout: 'mm'",
" variant: 'zawgyi'",
" brief: 'my-zwg'",
' description: Burmese (Zawgyi)',
'option_groups:',
"- name: 'grp'",
' description: Switching to another layout',
' allows_multiple: true',
' options:',
" - name: 'grp:switch'",
" brief: ''",
" description: 'Right Alt (while pressed)'",
' layout-specific: false',
].join('\n')
const briefs = model.layoutBriefs(listing)
assertEqual(briefs['English (US)'], 'en', 'the table reads a layout brief')
assertEqual(briefs['English (US, intl., with dead keys)'], 'en', 'the table reads a variant brief')
assertEqual(briefs['Generic 105-key PC'], undefined, 'the table skips keyboard models')
assertEqual(briefs['Switching to another layout'], undefined, 'the table skips option groups')
assertEqual(briefs["'Right Alt (while pressed)'"], undefined, 'the table skips the options under a group')
assertEqual(Object.keys(briefs).length, 6, 'the table holds nothing but the layouts')
assertEqual(model.shortLabel('English (US)', briefs), 'EN', 'the label is the language, not the country')
assertEqual(model.shortLabel('Portuguese (Brazil)', briefs), 'PT', 'a country variant keeps its language')
assertEqual(model.shortLabel('Esperanto', briefs), 'EO', 'a layout without a country still gets a code')
assertEqual(model.shortLabel('Spanish (Latin American)', briefs), 'ES', 'a layout spanning countries still gets a code')
assertEqual(model.shortLabel('Burmese (Zawgyi)', briefs), 'MY', 'a brief carrying a script drops it')
assertEqual(model.shortLabel('A user-defined custom Layout', { 'A user-defined custom Layout': 'custom' }), 'CUS', 'a brief that is a word is cut to size')
// A brief pairs with the description printed under it, so a block that prints
// one without the other must not hand its code to the block that follows.
const orphaned = model.layoutBriefs([
'layouts:',
"- layout: 'us'",
" brief: 'en'",
"- layout: 'gr'",
' description: Greek',
].join('\n'))
assertEqual(orphaned['Greek'], undefined, 'a brief stops at the end of its block')
assertEqual(model.shortLabel('Elvish (Tengwar)', briefs), 'ELV', 'an unlisted layout falls back to its description')
assertEqual(model.shortLabel('English (US)', {}), 'ENG', 'the label survives an empty table')
assertEqual(model.shortLabel('constructor', {}), 'CON', 'a description naming a built-in still falls back')
assertEqual(model.shortLabel('', briefs), '', 'no keymap means no label')
JS