Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.

Expand Down
4 changes: 3 additions & 1 deletion plugin/ui/LauncherWindow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
110 changes: 110 additions & 0 deletions tests/launcher-input.test.js
Original file line number Diff line number Diff line change
@@ -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]])
})