* 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>
255 lines
10 KiB
QML
255 lines
10 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Hyprland
|
|
import Quickshell.Io
|
|
import qs.Ui
|
|
import qs.Commons
|
|
import "KeyboardLayoutModel.js" as KeyboardLayoutModel
|
|
|
|
BarWidget {
|
|
id: root
|
|
moduleName: "omarchy.keyboard-layout"
|
|
|
|
|
|
property string layoutFull: ""
|
|
// The keyboard the last reading spoke for, which is the one a click switches,
|
|
// and separately the one activelayout named as being typed on. A reading
|
|
// confirms the first is really there, so the click has a keyboard to reach
|
|
// from the first reading onwards rather than only after a switch, and stops
|
|
// naming one that has been unplugged.
|
|
property string keyboardName: ""
|
|
property string typedKeyboardName: ""
|
|
// Keyboards on the seat, buttons and 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.
|
|
property bool multipleLayouts: true
|
|
// Where the reading sits in the layout list, how long that list is, and every
|
|
// keyboard sharing it. A switch moves that set together, so it needs all three.
|
|
property int layoutIndex: 0
|
|
property int layoutCount: 0
|
|
property var syncNames: []
|
|
// Short language code per layout description ("English (US)": "en"), read from
|
|
// xkb's own table rather than maintained by hand.
|
|
property var layoutBriefs: ({})
|
|
readonly property string layoutLabel: KeyboardLayoutModel.shortLabel(layoutFull, layoutBriefs)
|
|
readonly property var inputState: InputMethodState.state
|
|
readonly property bool multipleInputs: (inputState.methods || []).length > 1
|
|
readonly property string inputLabel: KeyboardLayoutModel.inputLabel(inputState, layoutLabel)
|
|
|
|
// 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) {
|
|
refreshPending = true
|
|
return
|
|
}
|
|
|
|
refreshPending = false
|
|
queryProc.running = true
|
|
}
|
|
|
|
// Keyboards someone can actually type on, which is not everything Hyprland
|
|
// calls a keyboard.
|
|
function typedKeyboards(keyboards) {
|
|
return keyboards.filter(k => KeyboardLayoutModel.isTypedKeyboard(k.name))
|
|
}
|
|
|
|
// The main flag names no keyboard for long: fcitx5 takes it with the virtual
|
|
// keyboard it binds to inject, which leaves no typed keyboard holding it and
|
|
// nothing to read at all, and once that unbinds it lands on whichever device
|
|
// Hyprland saw last, a power button included. Go by layout progress instead,
|
|
// and by the keyboard activelayout named.
|
|
function selectKeyboard(typed) {
|
|
return KeyboardLayoutModel.selectKeyboard(typed, root.typedKeyboardName)
|
|
}
|
|
|
|
// switchxkblayout is a hyprctl command rather than a dispatcher, so it has to
|
|
// be run rather than sent over the dispatch socket.
|
|
//
|
|
// Move every keyboard holding the same layout list, rather than the single one
|
|
// the last reading spoke for. Naming one device puts the whole switch behind
|
|
// UNTYPED_KEYBOARDS recognising every non-keyboard by name, and that list
|
|
// cannot keep up with what a seat carries: vendor hotkey blocks
|
|
// (intel-hid-events, dell-wmi-hotkeys), HID consumer controls, and Bluetooth
|
|
// AVRCP endpoints from a pair of headphones all arrive holding the seat's
|
|
// layout list, and they sort ahead of the keyboard being typed on. The click
|
|
// then advances a device nobody types on; that device is now the furthest
|
|
// along, so it wins the next reading too, and the label describes it while the
|
|
// real keyboard never moved.
|
|
//
|
|
// An absolute index rather than "next", because "next" advances each device
|
|
// from wherever it already sits: a seat that has drifted apart stays drifted
|
|
// and merely inverts. One index converges them in a single click, and a seat
|
|
// in lockstep is what leaves the reading nothing to disagree about afterwards.
|
|
//
|
|
// Keyboards given their own kb_layout hold a different list and are left out:
|
|
// an index into this list would not mean the same layout to them.
|
|
function cycleLayout() {
|
|
if (!root.bar || root.layoutCount < 2 || root.syncNames.length === 0) return
|
|
const next = (root.layoutIndex + 1) % root.layoutCount
|
|
root.bar.run(root.syncNames.map(name =>
|
|
"hyprctl switchxkblayout " + Util.shellQuote(name) + " " + next).join("; "))
|
|
refreshTimer.restart()
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
briefsProc.running = true
|
|
refresh()
|
|
}
|
|
|
|
Connections {
|
|
target: Hyprland
|
|
function onRawEvent(event) {
|
|
if (!event || !event.name) return
|
|
var name = String(event.name)
|
|
// The event names the keyboard that switched ahead of the layout it moved
|
|
// to, and that is the keyboard being typed on whatever holds the main flag.
|
|
if (name === "activelayout") {
|
|
const named = KeyboardLayoutModel.eventKeyboardName(event)
|
|
if (named) root.typedKeyboardName = named
|
|
}
|
|
|
|
// A reload that adds a layout to kb_layout decides whether the widget
|
|
// shows at all, and leaves every keyboard on the layout it was already
|
|
// reading, so it raises no activelayout to notice it by.
|
|
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 listed
|
|
try {
|
|
listed = JSON.parse(text || "{}").keyboards
|
|
} catch (e) {
|
|
return
|
|
}
|
|
|
|
// A query the watchdog killed reports nothing at all, and an empty
|
|
// string parses into the same shape a seat with no keyboards would.
|
|
// Tell them apart by the list itself, so only a reading that reached
|
|
// hyprctl gets to speak for the seat.
|
|
if (!Array.isArray(listed)) return
|
|
|
|
const typed = root.typedKeyboards(listed)
|
|
const kb = root.selectKeyboard(typed)
|
|
if (!kb || !kb.active_keymap) {
|
|
// Either the last keyboard has been unplugged, which the label has to
|
|
// stop describing and the click has to stop naming, or keyboards are
|
|
// there and none of them reports a keymap. Both leave the shape in
|
|
// doubt, so keep asking rather than letting a count from before it
|
|
// changed settle the poll.
|
|
root.keyboardUnresolved = true
|
|
if (typed.length === 0) {
|
|
root.layoutFull = ""
|
|
root.keyboardName = ""
|
|
}
|
|
return
|
|
}
|
|
|
|
root.keyboardUnresolved = false
|
|
root.keyboardCount = typed.length
|
|
root.keyboardName = String(kb.name || "")
|
|
root.multipleLayouts = kb.layout === undefined || String(kb.layout).indexOf(",") !== -1
|
|
root.layoutIndex = kb.active_layout_index || 0
|
|
root.layoutCount = kb.layout === undefined ? 0 : String(kb.layout).split(",").length
|
|
// Buttons and virtual keyboards are included on purpose: they hold the
|
|
// same list, and leaving them behind is what lets a reading drift onto
|
|
// one of them later.
|
|
root.syncNames = listed.filter(k => String(k.layout) === String(kb.layout))
|
|
.map(k => String(k.name || ""))
|
|
.filter(name => name !== "")
|
|
root.layoutFull = kb.active_keymap
|
|
}
|
|
}
|
|
}
|
|
|
|
// The table only changes when xkb data is upgraded, so read it at startup and
|
|
// leave it alone. The bar is built per monitor, so this runs once per widget.
|
|
// The exotic rulesets cover layouts like trans (IPA) that ship in the same xkb
|
|
// package and set just as well, so load them or those labels lose their code.
|
|
Process {
|
|
id: briefsProc
|
|
command: ["xkbcli", "list", "--load-exotic"]
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: root.layoutBriefs = KeyboardLayoutModel.layoutBriefs(text)
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: refreshTimer
|
|
interval: 600
|
|
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, and ask again: the reading it
|
|
// never delivered may have been the only one due on a settled seat, and
|
|
// nothing else would come back for it.
|
|
Timer {
|
|
id: stallTimer
|
|
interval: 5000
|
|
onTriggered: {
|
|
queryProc.running = false
|
|
refreshTimer.restart()
|
|
}
|
|
}
|
|
|
|
// Which keyboard on a crowded seat the label is describing can change without
|
|
// Hyprland announcing it, since a device arriving or leaving raises no event
|
|
// of its own, and that can only be learned 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: !root.keyboardName || root.keyboardUnresolved || root.keyboardCount > 1
|
|
repeat: true
|
|
onTriggered: root.refresh()
|
|
}
|
|
|
|
visible: inputLabel !== "" && KeyboardLayoutModel.showIndicator(layoutLabel, multipleLayouts, multipleInputs)
|
|
implicitWidth: button.implicitWidth
|
|
implicitHeight: button.implicitHeight
|
|
|
|
WidgetButton {
|
|
id: button
|
|
anchors.fill: parent
|
|
bar: root.bar
|
|
text: root.inputLabel
|
|
fontSize: Style.font.caption
|
|
horizontalMargin: 6
|
|
tooltipText: KeyboardLayoutModel.inputTooltip(root.inputState, root.layoutFull, root.multipleLayouts)
|
|
onPressed: function(button) {
|
|
if (root.multipleInputs && button === Qt.LeftButton) {
|
|
InputMethodState.cycle()
|
|
} else if (root.multipleLayouts) {
|
|
root.cycleLayout()
|
|
}
|
|
}
|
|
}
|
|
}
|