From ba5497c5c60e6e94e282d072a14bc025800bb7d1 Mon Sep 17 00:00:00 2001 From: Aman Mehtar Date: Thu, 22 Oct 2026 02:12:57 +0530 Subject: [PATCH] fix numpad input in launcher Ignore Qt's keypad flag when checking printable text. Add CI regressions for keypad digits, operators, alias and action filtering, and shortcut dispatch without changing the calculator. Co-Authored-By: Claude Code --- CLAUDE.md | 4 +- plugin/ui/LauncherWindow.qml | 4 +- tests/launcher-input.test.js | 110 +++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 tests/launcher-input.test.js diff --git a/CLAUDE.md b/CLAUDE.md index c5b984b..c88ec60 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -114,7 +114,7 @@ Works: - **Providers:** apps from `shell.appLibrary`, the whole Omarchy menu (about 400 items, with breadcrumbs, guards, ✓ state and font/power-profile provider rows), and the built-in "Omarchy Menu". - **Ranking state:** frecency (+100 per launch, 10-day half-life) and learned search terms in `db/frecency.json`. Aliases (exact → top; duplicates rejected), favourites (Ctrl+Shift+F, reorder Ctrl+Alt+↑↓) and disabled commands live in `db/settings.json` `commands`. - **Navigation:** pushed Omarchy submenus (search within them), the alias editor, back with Esc / Ctrl+[ / Backspace on an empty query / the header arrow, and Shift+Esc to pop to root. -- **Keyboard:** Ctrl+K action panel (filterable, per-row shortcuts), ↵ / Ctrl+↵ / Ctrl+Shift+↵ for the first three actions, Ctrl+1–9, Ctrl+↑↓ to jump sections, PageUp/PageDown, Ctrl+N/P (emacs), ↑ on an empty root for search history, Ctrl+Shift+C copy, Alt+Shift+D disable, Ctrl + / − / 0 zoom. +- **Keyboard:** Ctrl+K action panel (filterable, per-row shortcuts), ↵ / Ctrl+↵ / Ctrl+Shift+↵ for the first three actions, Ctrl+1–9, Ctrl+↑↓ to jump sections, PageUp/PageDown, Ctrl+N/P (emacs), ↑ on an empty root for search history, Ctrl+Shift+C copy, Alt+Shift+D disable, Ctrl + / − / 0 zoom. Numpad digits, operators and decimal separators work in queries, aliases and action filters; keypad Enter and Ctrl shortcuts retain their actions. - **Feedback:** toasts in the footer, a HUD through Omarchy's OSD when the window closes, and a 2 px loading bar while guards and providers refresh (on every open, about 250 ms in the background). - **Window behaviour:** - The `escapeKeyBehavior` setting works both ways. @@ -133,7 +133,7 @@ Works: ↵ copies the answer (HUD), Ctrl+↵ pastes it through `omarchy-clipboard-paste-text`, Ctrl+Shift+C copies "question = answer". Each one saves to **Calculator History** (`db/calculator.json`, 4096 rows / 90 days): a built-in command and pushed view, searchable, Ctrl+D deletes an entry, Ctrl+Shift+D deletes all. RPC: `calculator.calculate|historyGetAll|historyDelete|historyDeleteAll|refreshCurrencies|getCurrencies`. A bare number, word or quantity (`42`, `firefox`, `5 kg`) never produces a row, because the section takes ↵. - **Latency:** show → mapped in 37–44 ms, hide in 9–25 ms (`tests/smoke.sh`). -- **Tests:** root search, calculator and default-agent hand-off coverage (`node --test tests/*.test.js`). +- **Tests:** root search, calculator, default-agent hand-off and launcher typing/keypad shortcut coverage (`node --test tests/*.test.js`). **Default-agent hand-off (fork, 2026-10-20):** user-authorized addition outside the original phase plan. The root fallback "Ask default agent" and Ctrl+Alt+Enter pass the query to `omarchy-agent --prompt` as a literal argv element. `AgentLauncher.qml` checks the configured agent/terminal with a bounded, question-free preflight, prevents overlapping submissions and cancels on hide/unload. `Launcher.qml` dismisses the overlay before launching through `Util.execArgv`. Errors leave the query visible. There are no new dependencies, model APIs, credentials or question-history writes. Existing CLI approval behaviour is inherited. Portable checks do not establish live terminal focus, authentication, or provider compatibility; verify those on Omarchy. diff --git a/plugin/ui/LauncherWindow.qml b/plugin/ui/LauncherWindow.qml index 8053637..a6a7cfb 100644 --- a/plugin/ui/LauncherWindow.qml +++ b/plugin/ui/LauncherWindow.qml @@ -393,8 +393,10 @@ PanelWindow { } function isTyping(event) { + // Qt marks numpad digits and operators with KeypadModifier, not a shortcut. + var modifiers = event.modifiers & ~Qt.KeypadModifier return event.text && event.text.length === 1 && event.text.charCodeAt(0) >= 32 && event.text.charCodeAt(0) !== 127 - && (event.modifiers === Qt.NoModifier || event.modifiers === Qt.ShiftModifier) + && (modifiers === Qt.NoModifier || modifiers === Qt.ShiftModifier) } Connections { diff --git a/tests/launcher-input.test.js b/tests/launcher-input.test.js new file mode 100644 index 0000000..23efead --- /dev/null +++ b/tests/launcher-input.test.js @@ -0,0 +1,110 @@ +const test = require("node:test") +const assert = require("node:assert/strict") +const fs = require("node:fs") +const vm = require("node:vm") + +// Run the actual QML handlers without a desktop or Qt dependency in CI. +const source = fs.readFileSync(require.resolve("../plugin/ui/LauncherWindow.qml"), "utf8") +const handlers = ["isTyping", "handleKey"].map((name) => { + const match = source.match(new RegExp(`^ function ${name}\\(event\\) \\{[\\s\\S]*?^ \\}`, "m")) + assert.ok(match, `missing QML handler ${name}`) + return match[0] +}).join("\n") + +// Qt::KeyboardModifier and Qt::Key values from qnamespace.h. +const Qt = { + NoModifier: 0, ShiftModifier: 0x02000000, ControlModifier: 0x04000000, + AltModifier: 0x08000000, MetaModifier: 0x10000000, + KeypadModifier: 0x20000000, GroupSwitchModifier: 0x40000000, + Key_Escape: 0x01000000, Key_Backspace: 0x01000003, + Key_Return: 0x01000004, Key_Enter: 0x01000005, + Key_Up: 0x01000013, Key_Down: 0x01000015, + Key_PageUp: 0x01000016, Key_PageDown: 0x01000017, + Key_Space: 32, Key_Plus: 43, Key_Minus: 45, Key_Equal: 61, + Key_0: 48, Key_1: 49, Key_9: 57, Key_BracketLeft: 91, + Key_C: 67, Key_D: 68, Key_F: 70, Key_K: 75, Key_N: 78, Key_P: 80 +} + +function input(kind = "root") { + const actions = [] + const panel = { filter: "", activateSelected() { actions.push("panel") } } + const window = { + view: { kind }, query: "", actionPanelOpen: false, + setQuery(text) { this.query = text }, + performNth(n) { actions.push(n) }, + nthItem(n) { return n - 1 }, + perform(action, index) { actions.push([action, index]) } + } + const api = vm.runInNewContext(`${handlers}\n({ isTyping, handleKey })`, { + Qt, window, actionPanel: panel, Util: { editsFilter() { return false } } + }) + window.isTyping = api.isTyping + return { ...api, window, panel, actions } +} + +function key(text, modifiers = Qt.KeypadModifier) { + return { text, key: text.toUpperCase().charCodeAt(0), modifiers } +} + +test("numpad digits, operators and decimal separators are printable", () => { + const { isTyping } = input() + for (const text of "0123456789+-/*.,") + for (const modifiers of [Qt.KeypadModifier, Qt.KeypadModifier | Qt.ShiftModifier]) + assert.equal(isTyping(key(text, modifiers)), true, text) +}) + +test("normal typing still accepts plain and Shift-modified text", () => { + const { isTyping } = input() + for (const text of "aA5+-/*., ") + for (const modifiers of [Qt.NoModifier, Qt.ShiftModifier]) + assert.equal(isTyping(key(text, modifiers)), true, text) +}) + +test("keypad flags do not turn shortcut modifiers into text", () => { + const { isTyping } = input() + for (const modifier of [Qt.ControlModifier, Qt.AltModifier, Qt.MetaModifier, Qt.GroupSwitchModifier]) + for (const extra of [0, Qt.KeypadModifier, Qt.KeypadModifier | Qt.ShiftModifier]) + assert.equal(isTyping(key("5", modifier | extra)), false) +}) + +test("empty text, control characters and multi-character text are not typing", () => { + const { isTyping } = input() + for (const text of ["", "\r", "\n", "\t", "\x1b", "\x7f", "12"]) + assert.equal(Boolean(isTyping(key(text))), false, JSON.stringify(text)) +}) + +test("numpad text reaches the root query and alias editor", () => { + for (const kind of ["root", "alias"]) { + const { handleKey, window, actions } = input(kind) + const text = kind === "root" ? "2+3*4-6/2+0.5" : "56" + for (const character of text) assert.equal(handleKey(key(character)), true) + assert.equal(window.query, text) + assert.deepEqual(actions, []) + } +}) + +test("numpad text filters actions without changing the root query", () => { + const { handleKey, window, panel, actions } = input() + window.query = "2+2" + window.actionPanelOpen = true + for (const text of "+5") assert.equal(handleKey(key(text)), true) + assert.equal(panel.filter, "+5") + assert.equal(window.query, "2+2") + assert.deepEqual(actions, []) +}) + +test("keypad Enter and Ctrl-number shortcuts keep their actions", () => { + for (const [modifiers, expected] of [ + [Qt.KeypadModifier, 0], + [Qt.KeypadModifier | Qt.ControlModifier, 1], + [Qt.KeypadModifier | Qt.ControlModifier | Qt.ShiftModifier, 2] + ]) { + const { handleKey, window, actions } = input() + assert.equal(handleKey({ key: Qt.Key_Enter, text: "\r", modifiers }), true) + assert.deepEqual(actions, [expected]) + assert.equal(window.query, "") + } + const { handleKey, actions } = input() + assert.equal(handleKey(key("1", Qt.ControlModifier | Qt.KeypadModifier)), true) + assert.deepEqual(actions, [["open", 0]]) +})