* Unify input methods with the installer keyboard selection * Preserve inherited input keys and later keyboard changes * Exercise input engines across desktop toolkits * Show input modes in the keyboard indicator * Keep Pinyin setup prompts visible * Remember input selections before an app gains focus * Remove the Japanese input welcome notification * Show explicit choices on notification cards * Suppress the Cloudpinyin first-use prompt * Show the input switching shortcut in keyboard hints * Verify input setup and composition stay quiet * Use Super+I for input switching independently of Fcitx keys * Restore linked input settings and attribute test notifications to Fcitx * Correct the input setup shortcut hint * Keep input tooltips to the name and switching shortcut * Place the input language selector with the right bar widgets * Show input switching only for multiple layouts or methods * Unify input method and keyboard layout switching * Select keyboard layouts and input methods in the Typing menu * Apply typing selections immediately from the menu * Use installer keyboard choices and keep typing selections accurate * Queue typing changes and wait for saved selections * Keep queued typing selections when reopening the menu * Cycle keyboard layouts alongside composition engines * Avoid shadowing Python typing and retain pending mixed input selections * Keep input clicks working through controller and compositor failures * Move the keyboard layout widget to the right side of existing bars Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Map keyboard choices to the layouts the installer sets Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the switching order when changing input methods Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Set input method fonts only after the selection saves Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restore confirmed picker checks when an immediate change fails Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Route Super+I through the shell's input reader so unfocused choices stick Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Restore the input group when the font preference cannot be written Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop the azerty conversion now that the installer no longer offers it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Apply the owner's keyboard layout once before persisting the input choice A quattro merge kept a second apply_keyboard after the retry loop, which re-ran loadkeys and firstboot, ignored their failure, and overwrote the XKB layout setup supplies for keymaps kbd-model-map lacks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Give the Lao keyboard choice a Lao desktop layout The row used la-latin1, kbd's Latin American Spanish keymap, so Lao installs typed Spanish. There is no Lao console keymap; like Korean, the console stays US and the desktop gets la, which Omarchy keeps behind US as a non-Latin layout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Load engines installed after Fcitx started when they are selected The Typing menu refused an engine whose package an update had installed, since Fcitx discovers engines only at startup. Restart input once and wait for the engine; name the package when it really is not installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cycle input methods back with Super+Shift+I Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop notification action buttons now that the Pinyin prompt is suppressed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cycle input only through the shell's reader Without the shell there is no bar or menu either, so the one-shot cycling path only duplicated the reader's logic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add engines from the command line through the Typing menu's path omarchy setup input <engine> had its own restart, append, select and live-rollback code in configure.py, parallel to the menu's. It now installs the packages and appends through typing_setup, leaving configure.py with install-time seeding and update defaults. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep Ctrl+Space out of Fcitx's toggles instead of probing for it The terminal prefix bind read the Fcitx config and profile on every press to avoid fighting a Ctrl+Space toggle. configure --defaults now removes that key from every trigger list, so the bind only checks for a terminal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Run the input-method scripts with the system Python A bare python resolves through PATH, so a mise, pyenv or venv interpreter could run them without the system's PyGObject. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
157 lines
9.9 KiB
Bash
Executable File
157 lines
9.9 KiB
Bash
Executable File
#!/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')
|
|
|
|
const japanese = { methods: ['keyboard-us', 'mozc'], current: 'mozc', name: 'Mozc', language: 'ja' }
|
|
assertEqual(model.inputLabel(japanese, 'EN'), 'あ', 'Japanese input has a distinct active label')
|
|
assertEqual(model.inputLabel({ ...japanese, label: 'ア' }, 'EN'), 'ア', 'the live engine mode takes precedence over its default glyph')
|
|
assertEqual(model.inputLabel({ ...japanese, current: 'keyboard-us' }, 'EN'), 'EN', 'Latin input retains the keyboard label')
|
|
assertEqual(model.inputLabel({}, 'EN'), 'EN', 'a stopped input service retains the keyboard label')
|
|
assertEqual(model.inputLabel({ current: 'hangul' }, 'EN'), '한', 'Korean input has a distinct label')
|
|
assertEqual(model.inputLabel({ current: 'custom', language: 'vi' }, 'EN'), 'VI', 'other engines use their reported language')
|
|
assertEqual(model.inputTooltip(japanese, 'English (US)', false), 'Mozc · Super + I', 'input tooltip shows only its name and shortcut')
|
|
assertEqual(model.inputTooltip(japanese, 'English (US)', true), 'Mozc · Super + I', 'multiple layouts keep the input tooltip compact')
|
|
assertEqual(model.inputTooltip({ ...japanese, current: 'keyboard-us', name: 'Keyboard - English (US)' }, 'English (US)', false), 'English (US) · Super + I', 'Latin input omits the redundant keyboard prefix')
|
|
assertEqual(model.inputTooltip({ ...japanese, current: 'keyboard-us', name: 'Keyboard - English (US)' }, 'Danish', true), 'Danish · Super + I', 'direct input tooltip follows the active compositor layout')
|
|
assertEqual(model.inputTooltip({ methods: ['keyboard-fr'] }, 'French', false), 'French', 'a single layout tooltip stays compact')
|
|
assertEqual(model.showIndicator('EN', false, false), false, 'English alone stays hidden')
|
|
assertEqual(model.showIndicator('ENG', false, false), false, 'English before the brief table loads stays hidden')
|
|
assertEqual(model.showIndicator('FR', false, false), false, 'French alone stays hidden')
|
|
assertEqual(model.showIndicator('EN', false, true), true, 'Latin with another input mode stays available')
|
|
assertEqual(model.showIndicator('EN', true, false), true, 'multiple keyboard layouts stay available')
|
|
assertEqual(model.showIndicator('FR', true, false), true, 'French and English layouts stay available')
|
|
assertEqual(model.inputTooltip({ methods: ['keyboard-fr'] }, 'French', true), 'French · Super + I', 'multiple layouts advertise their switching shortcut')
|
|
|
|
// 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')
|
|
|
|
// The seat as hyprctl reports it, virtual keyboards already filtered out: the
|
|
// buttons libinput calls keyboards sit beside the one being typed on, and the
|
|
// main flag lands on either, or on the virtual keyboard that isn't here.
|
|
const seat = (activeIndex, main) => [
|
|
{ name: 'power-button', active_layout_index: 0, active_keymap: 'English (US)', main: main === 'power-button' },
|
|
{ name: 'at-translated-set-2-keyboard', active_layout_index: activeIndex, active_keymap: activeIndex ? 'French' : 'English (US)', main: main === 'keyboard' },
|
|
]
|
|
|
|
assertEqual(model.selectKeyboard(seat(1)).active_keymap, 'French', 'the keyboard that switched is read when nothing holds main')
|
|
assertEqual(model.selectKeyboard(seat(1, 'power-button')).active_keymap, 'French', 'the keyboard that switched outranks a button holding main')
|
|
assertEqual(model.selectKeyboard(seat(0)).active_keymap, 'English (US)', 'before a switch every keyboard reads the same layout')
|
|
assertEqual(model.selectKeyboard(seat(0), 'at-translated-set-2-keyboard').name, 'at-translated-set-2-keyboard', 'the keyboard activelayout named is kept once it switches back')
|
|
assertEqual(model.selectKeyboard([{ name: 'a' }, { name: 'b' }]).name, 'a', 'a keyboard reporting no index still gets picked')
|
|
assertEqual(model.selectKeyboard([]), undefined, 'a seat with no typed keyboard picks none')
|
|
|
|
// Two real keyboards on the layout the seat started on, so nothing but the name
|
|
// says which one is being typed on. The one that wrapped from the last layout
|
|
// back to the first sits behind the other and is still the one that switched.
|
|
const pair = (activeIndex, otherIndex) => [
|
|
{ name: 'at-translated-set-2-keyboard', active_layout_index: otherIndex, active_keymap: otherIndex ? 'German' : 'English (US)' },
|
|
{ name: 'usb-keyboard', active_layout_index: activeIndex, active_keymap: activeIndex ? 'German' : 'English (US)' },
|
|
]
|
|
|
|
assertEqual(model.selectKeyboard(pair(0, 0), 'usb-keyboard').name, 'usb-keyboard', 'the keyboard that switched is read once it is named')
|
|
assertEqual(model.selectKeyboard(pair(0, 2), 'usb-keyboard').name, 'usb-keyboard', 'a keyboard that wrapped back to the first layout is still the one that switched')
|
|
assertEqual(model.selectKeyboard(pair(0, 2), 'gone-keyboard').name, 'at-translated-set-2-keyboard', 'a name no keyboard answers to falls back to layout progress')
|
|
|
|
// Hyprland calls the ACPI buttons keyboards too, and hands them the seat's
|
|
// layout list. Reading one would describe a layout nobody typed, and switching
|
|
// one would leave the keyboard where it was.
|
|
assertEqual(model.isTypedKeyboard('at-translated-set-2-keyboard'), true, 'a real keyboard is typed on')
|
|
assertEqual(model.isTypedKeyboard('power-button'), false, 'a power button is not')
|
|
assertEqual(model.isTypedKeyboard('lid-switch'), false, 'a lid switch is not')
|
|
assertEqual(model.isTypedKeyboard('sleep-button'), false, 'a sleep button is not')
|
|
assertEqual(model.isTypedKeyboard('hl-virtual-keyboard-1'), false, 'the keyboard an input method injects through is not')
|
|
assertEqual(model.isTypedKeyboard(''), true, 'a keyboard reporting no name is left where it was found')
|
|
|
|
// The activelayout event names the keyboard ahead of the layout it moved to,
|
|
// and a description with a comma in it has to survive the split.
|
|
const rawEvent = data => ({ data })
|
|
const parsedEvent = data => ({ data, parse: count => data.split(',', count - 1).concat(data.split(',').slice(count - 1).join(',')) })
|
|
|
|
assertEqual(model.eventKeyboardName(rawEvent('at-translated-set-2-keyboard,French')), 'at-translated-set-2-keyboard', 'the event names its keyboard')
|
|
assertEqual(model.eventKeyboardName(parsedEvent('at-translated-set-2-keyboard,English (US, intl.)')), 'at-translated-set-2-keyboard', 'a description carrying a comma leaves the name alone')
|
|
assertEqual(model.eventKeyboardName(rawEvent('hl-virtual-keyboard,English (US)')), '', 'the keyboard an input method injects through is not typed on')
|
|
assertEqual(model.eventKeyboardName({ parse: () => { throw new Error('unsupported') }, data: 'kb,French' }), 'kb', 'a binding without parse falls back to the raw data')
|
|
assertEqual(model.eventKeyboardName({}), '', 'an event with nothing in it names no keyboard')
|
|
JS
|