Launcher pass - #514
Conversation
First step of the QML -> Luau rewrite. Adds the v5 plugin skeleton and the read-only half of the feature; no `pass`/`wtype`/clipboard integration yet. New files: - plugin.toml: manifest with the [[launcher_provider]] entry (prefix "pass", debounce_ms 200, include_in_global_search false) and the full settings schema (storePath, clipTimeout, typeDelay, wtypeDelay). plugin_api = 24, the level at which argv-form runAsync lands (highest feature used so far). - launcher.luau: the provider entry script. onQuery lists the store via `find` (maxdepth-1 children for an empty query, recursive *.gpg otherwise), parses names the same way the QML did, and filters/scores them with an exact port of fuzzyMatch (whitespace-split query, every fragment a contiguous substring, score favouring earlier path segments and alphabetically-earlier names). Folder drill-in uses a module-local nav stack plus launcher.setQuery to re-fire onQuery; a "Go back" row pops it. A 5 s per-key cache keeps debounced keystrokes from re-spawning `find`. Activating a password entry is a stub notification for now. - translations/en.json: keys used so far. Deviations from CLAUDE.md, chosen against the working sibling plugins: - settings declared as top-level [[setting]] (docs only cover nested settings for widget/panel; every sibling launcher plugin uses top-level); - translations kept as nested JSON (what noctalia.tr + label_key resolve against in pass/k8s-status/file-search); - plugin_api pinned to 24 rather than the 28 ceiling. Also fixes a latent QML bug: synthesized parent-folder rows in recursive search now carry a currentPath-prefixed fullPath, so drilling into them resolves to the right directory. The v4 .qml files are kept in place for the remaining port steps. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The ported QML segment score made leaf length dominate: it built a
base-27 value per path segment, so a long leaf like "root_new_1"
outscored a shallower ".../root" no matter the per-depth weighting, and
the "_new" variants sorted above the plain entry.
Replace it with rankEntry(), sorting by:
1. exact segment hits — query fragments that equal a whole path
segment (fragment "root" == leaf "root" beats "root" as a substring
of "root_new_1"), so plain entries group above suffixed variants;
2. depth — fewer path segments first;
3. full path, alphabetically.
Unlike the QML formula this takes the query into account. It also drops
the per-character arithmetic: a small fragment x segment loop over the
capped grep output. SEG_W / pathScore removed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Folder navigation put find spawn + parse on the critical path between selecting a row and the list updating (~1s with the debounce on top). Navigation always drills into a folder visible in the current list, so: - at module load, warm the store-root listing; - whenever a browse view is published, fetch its child folders' listings in the background (capped at 10, deduped against an in-flight set and the browse cache TTL). Activating a folder then hits a warm cache and renders synchronously, with no subprocess and no loading-row flash; rendering that view warms its children in turn, so the effect propagates down the tree. Recursive search still goes through grep and does not prefetch, so typing a query does not spawn a burst of find processes. Removed the debounce timer since cache is improving navigation enough by itself Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…stubbed) Implements CLAUDE.md port step "1. Detail mode": - onActivate on a password-entry row now rewrites the query to a leading ":" + entry path instead of stub-notifying immediately. This mirrors the trailing-"/" folder marker already used for browse: a sigil never produced by normal browsing, so a fresh ">pass " always falls through to browse and resets detail state for free (no onOpened()-style hook needed). - onQuery routes that leading-":" text to a new fetchDetail(), which shows a "Decrypting..." row, runs `env PASSWORD_STORE_DIR=<dir> pass show <entry>` via runAsync, and renders the parsed result. Decrypted entries are cached 60s per path so a debounce re-fire doesn't trigger a second pass show / pinentry prompt. A failed/timed-out pass show renders a "Go back" + error row. - parsePassEntry: first non-empty line is the password, later lines split on the first ": " into key/value fields (file order), any line containing "otpauth://" flags OTP support — per "Behaviour to preserve noctalia-dev#5". - extractUsername implements "Behaviour to implement noctalia-dev#1": pulls the value of the first login/user/username field (case-insensitive) out of the field list, falling back to the entry's basename; the field is removed from the remaining list so it isn't shown twice. - renderDetailRows orders rows per "Behaviour to implement noctalia-dev#2": Password, OTP (only if present), Username, then remaining fields in file order — each as a Copy + Type pair, plus "Go back" to the entry's parent folder. - Row actions carry no field data in their id: each Copy/Type row gets an opaque "act:<n>" id (n derived from the row count, so no separate counter), and a module-local `detailActions` table maps id -> {verb, kind, key, value, label} for onActivate to read. Avoids encoding arbitrary field keys/values (which can contain almost anything) into the id string; the table is simply rebuilt each render since only one detail view is ever live. - onActivate on an "act:" id is currently a stub noctalia.notify(label, "<label> is not implemented yet") — real copy/type/OTP/clipboard-timeout /wtype wiring is the next port step. - Refactored the "Go back" row (shared by browse and detail) into goBackRow(parent, subtitle), and factored out parentOf(); renderRows updated to use both, no behaviour change there. - translations/en.json: added action.copy*/type* (password, OTP, username, field) and detail.decrypting/error/actionPending keys, all used by the new code; removed the now-unused detail.pending stub key. - CLAUDE.md: updated Port status to reflect detail-mode listing being done, row actions being the new stub point, and resolved the detail-mode query encoding as an "Open questions" / "Decisions" entry. Verified by loading launcher.luau in an isolated Luau environment (via loadstring + setfenv) with mocked noctalia/launcher globals and a canned `pass show` output, exercising: entry activation -> query rewrite -> onQuery(":path") -> pass show -> parsed row order/content (password, OTP, username, url field) -> stub notify on both a fixed and a dynamic-key action -> "Go back" returning to the correct folder query; plus a no-OTP/no-login-field entry (username falls back to basename) and a failed pass show (renders the error row) and a sanity check that plain browse queries still render after the goBackRow/parentOf refactor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VggPYgn8B9vM1MtsT6VSZN
…el IPC
The launcher panel is a layer-shell surface with an exclusive keyboard
grab, so the GPG pinentry dialog `pass show` pops on a locked store never
receives focus while the panel is open — same problem the old QML version
fixed by closing/reopening the launcher. There is no noctalia.* plugin API
to hand focus to an arbitrary window, but the installed noctalia binary's
own `noctalia msg --help` documents a panel IPC that does the same job:
panel-close [id] "Close the active panel, or close the named
panel if it is active" (safe to call even if
already closed)
panel-open <id> [ctx] "Open a panel by id, optionally with context
(e.g. launcher /emo, control-center audio)"
panel-toggle <id> [ctx] same, toggled
fetchDetail now:
1. closeLauncherPanel() -> `noctalia msg panel-close launcher`
2. runs `pass show` with the panel down, with an explicit 300s timeout
(DECRYPT_TIMEOUT_MS) instead of none — pinentry can legitimately
block a long time on a slow passphrase or a hardware-key touch, and
this bound exists only as a safety net so a truly wedged gpg-agent
eventually reopens the panel rather than leaving it closed forever
3. populates detailCache / calls launcher.setResults from the result
4. reopenLauncherPanel(text) -> `noctalia msg panel-open launcher
">pass " .. text`, so the host re-delivers the exact prior input as a
fresh onQuery and re-renders from the now-warm cache
Step 3 has to happen before step 4: the reopened panel's re-delivered
onQuery is a second, re-entrant call into fetchDetail, and it must see a
cache hit or it would kick off a second `pass show` (and a second
pinentry prompt) right on top of the first.
closeLauncherPanel/reopenLauncherPanel are factored out for reuse: the
next port step's `pass -c` / `pass otp -c` calls (copy/type actions) can
also hit pinentry if the gpg-agent cache has expired, and should wrap
themselves the same way.
Verified in the same isolated-Luau mock harness as the previous commit,
extended to track panel-close/panel-open calls: confirms close-then-open
ordering, the reopen context is exactly ">pass :<entryPath>", the dance
is skipped entirely on a detailCache hit, and the panel is still reopened
when `pass show` fails. Not yet exercised against a real pinentry prompt
on the live desktop — flagged in CLAUDE.md as worth a manual check.
CLAUDE.md: resolved the "pinentry dance" and "IPC toggle" open questions
(the latter answered by the same panel-* IPC, generalized to any panel
id — this is the v5 analogue of v4's pluginApi.toggleLauncher()).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VggPYgn8B9vM1MtsT6VSZN
The pinentry-dance panel reopen hardcoded ">pass " .. text as the
panel-open "context" argument, but the leading ">" is itself a user
setting (shell.launcher.provider_prefix), not a fixed character — this
dev machine has it configured to "/", confirmed via
`noctalia config export full` (`[shell.launcher] provider_prefix = "/"`).
Reopening with the wrong leader produces a string the host no longer
recognises as a launcher command at all, so the detail view never came
back after pinentry.
launcherLeader() now reads noctalia.getSetting("shell.launcher.provider_prefix")
and falls back to ">" only when that returns nil (an older host without
plugin_api 26 support, per the runtime-api docs: "Requires plugin_api =
26"). reopenLauncherPanel builds the context as
launcherLeader() .. "pass " .. text instead of a hardcoded ">pass ".
plugin_api bumped 24 -> 26 accordingly (getSetting is the new
highest-level feature in use).
Verified in the same isolated-Luau mock harness as the previous two
commits: getSetting mocked to return "/" (matching this machine's real
config) confirms the reopen context is "/pass :<entryPath>", not
">pass :<entryPath>"; a second case with getSetting returning nil
confirms the ">" fallback still works for hosts below plugin_api 26.
CLAUDE.md: documented the shell.launcher.provider_prefix setting and the
plugin_api bump; noted this is the one value in the pinentry dance that
must never be hardcoded.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VggPYgn8B9vM1MtsT6VSZN
…nlocked
Previously fetchDetail always closed the launcher panel before running
`pass show`, even when gpg-agent already had the passphrase cached and no
pinentry dialog was going to appear at all — a pointless close/reopen
flicker on every decrypt.
fetchDetail now races `pass show` against a short, configurable grace
period (new `pinentryGraceMs` setting, default 200ms) instead of closing
the panel unconditionally up front:
- starts `pass show` (unchanged: DECRYPT_TIMEOUT_MS, no panel touched)
- simultaneously starts `{"sleep", "<pinentryGraceMs/1000>"}`
- whichever finishes first decides:
* `pass show` first -> `resolved = true`; the sleep callback later
sees that and does nothing. No pinentry was needed; the panel is
never closed or reopened.
* sleep first -> `panelClosed = true`; closeLauncherPanel()
runs belatedly. `pass show`'s callback later sees `panelClosed`
and calls reopenLauncherPanel() once it finally resolves.
Both flags are plain upvalues shared by the two callbacks — safe without
locking since Luau callbacks run to completion before the next one
starts; there's no true concurrency, just two independent subprocesses
racing to finish first.
plugin.toml: new `pinentryGraceMs` int setting (default 200, advanced),
translations/en.json: matching label/desc keys.
Verified in the same isolated-Luau mock harness as the previous commits,
extended with a deferred-callback technique to simulate the slow
(pinentry-needed) path: the mock's `pass show` handler can now stash its
callback instead of firing it immediately, letting a test assert the
panel closes once the grace-period sleep fires while `pass show` is still
pending, then reopens correctly (right context, correct rendered rows)
once the stashed callback is finally invoked. Confirms: a fast decrypt
never touches the panel (3 separate scenarios: normal entry, a second
entry, and a failing-but-fast one); a slow decrypt closes-then-reopens
with the right context; the ">" leader fallback still exercises the dance
correctly; and a configured pinentryGraceMs=500 becomes a 0.5s sleep.
CLAUDE.md: documented the race and updated the "How launcher.luau works
now" / Decisions sections; noted the fifth settings entry.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VggPYgn8B9vM1MtsT6VSZN
The previous commit's plugin.toml landed with default = 500 for the new
pinentryGraceMs setting, inconsistent with the 200ms the user actually
asked for, with launcher.luau's own PINENTRY_GRACE_DEFAULT_MS = 200
fallback constant, and with the translation text ("default: 200"). Fixed
to 200 so the manifest default matches the code and docs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VggPYgn8B9vM1MtsT6VSZN
The act:<n> rows were all stub notifications. Copy Password now runs for
real; the other Copy/Type actions stay stubs for the next steps.
- onActivate's "act" branch calls runAction(ctx), which dispatches
verb=copy/kind=password to copyViaPass({"pass","-c",entryPath}, <basename>).
- copyViaPass closes the launcher panel first (same idea as the QML
version's launcher.close() before `pass -c`: with the panel gone a
pinentry prompt on a stale gpg-agent cache takes focus on its own, so
no close/reopen dance is needed), then runs `pass -c` with the
DECRYPT_TIMEOUT_MS safety net and posts notification.copied /
notification.copyFailed. The launcher stays closed afterwards, the
terminal state QML left it in.
- New helpers: positiveInt + clipTimeout resolve the clip-clear timeout
(clipTimeout setting -> PASSWORD_STORE_CLIP_TIME env -> nil, let pass
default), per "Behaviour to preserve noctalia-dev#6". passArgv(cmd, withClip)
builds the `env PASSWORD_STORE_DIR=... [PASSWORD_STORE_CLIP_TIME=...]
<cmd>` argv; fetchDetail's `pass show` call was refactored onto it.
- en.json: add notification.copied / notification.copyFailed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
runAction now dispatches verb=copy/kind=otp to
copyViaPass({"pass","otp","-c",entryPath}, <basename>), reusing the same
clip-timeout resolution and close-launcher-first pinentry handling as Copy
Password. The OTP rows still only render when the decrypted body contains
otpauth:// (data.hasOtp). A missing pass-otp extension currently surfaces
as notification.copyFailed; the commandExists probe to hide the rows
instead is still pending.
Copy field / Copy username / all Type actions remain stubs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
runAction now dispatches verb=copy/kind=username to copyPlain(ctx.value, <basename>). ctx.value is data.username, already resolved by extractUsername (first login/user/username field, case-insensitive; entry basename as fallback) — the copy path just consumes that single source of truth. New helper copyPlain: for values already decrypted in memory there's no subprocess and no pinentry, so it copies straight through noctalia.copyToClipboard, posts notification.copied, and closes the launcher (same terminal state as the pass -c path; QML also closed the launcher before piping to wl-copy). No clipboard auto-clear here — pass isn't in the loop — matching QML, which only ran the clearing timer for pass -c / pass otp -c. Copy field and every Type action remain stubs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
noctalia.copyToClipboard requires a second argument — without it the host raises "missing argument noctalia-dev#2 to 'copyToClipboard' (string expected)" and onActivate aborts, so Copy Username copied nothing and showed no notice. Pass "text/plain" (CLIP_MIME). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
runAction now dispatches verb=copy/kind=field to copyPlain(ctx.value, ctx.key): a parsed key: value line is copied verbatim through noctalia.copyToClipboard, with the field key as the notification body. Reuses the same plain-copy path as Copy Username (no subprocess, no pinentry, no clipboard auto-clear), so this completes every Copy action. Type actions remain stubs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
Native auto-paste is out: LauncherPanel::finishActivation() fires it only
when provider.supportsAutoPaste(), which PluginLauncherProvider never
overrides — no manifest key, noctalia.* call, or `noctalia msg` command
exposes it or the virtual-keyboard paste. So typing is done with wtype.
- typeValue(value): runAsync({"sleep", typeDelay/1000}) then
runAsync({"wtype","-d",wtypeDelay,"--",value}). Argv only — the value
sits after "--" so a leading "-" is still text; no `printf | wtype -`
pipe, no shellEscape. Resolves the "wtype stdin" open question.
- typeViaWtype(value): close the launcher (wtype types into the focused
window) then typeValue.
- typeOtp(entryPath): close launcher -> `pass otp` (no -c) -> typeValue的
the printed code. `pass otp` can hit pinentry, which has focus since the
panel is already closed.
- runAction branches on verb (copy/type) then kind.
- wtypeAvailable(): memoised commandExists("wtype"); addAction omits the
Type row when false, so a host without wtype shows Copy-only rows.
- intSetting(key, default): non-negative int setting (0 valid), for
typeDelay / wtypeDelay.
- en.json: + notification.typeFailed; - detail.actionPending (the last
stub-notice user, now gone).
- plugin.toml: + "wtype" to dependencies.
Copy actions are unchanged from the previous pass (restored after the
clipboard-only experiment).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
Two behaviour changes for detail mode: 1. onActivate on an entry row now sets the query to ":<path> " (trailing space) instead of ":<path>", so the just-opened detail view carries an empty row filter. 2. onQuery parses the post-":" text as "<entryPath> <rowFilter>" and fuzzy-filters the rendered Copy/Type rows by title with the browse match rule (whitespace-split, each fragment a case-insensitive contiguous substring, order-independent). ":<path> otp" -> only the Copy OTP / Type OTP rows. Details: - splitDetailQuery(rest): an entry path can contain spaces, so the path/filter boundary is resolved against lastDetailPath (new module local, set on every fetchDetail) first, then any warm detailCache key (longest match wins), else the whole trimmed string is the path. If the host ever strips the trailing space, this degrades to "no filter" rather than breaking. - matchesFragments(): factored out of renderRows and reused for the detail filter. - renderDetailRows(entryPath, data, filter): builds all rows (still recording detailActions for every one), then keeps only the rows whose title matches; empty filter -> all rows. The "Go back" row is filtered too (a non-matching filter hides it). - onQuery now parses the ":" branch off `text` directly, not trim(text), so the filter's spacing survives. Fixes the pre-existing bug where typing anything while a detail view was open turned the whole query into a bogus entry path and fired a `pass show` per keystroke. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
translations/{de,fr,it,es,ja,nl,pt,ru,tr,zh-CN}.json, each with the exact
31-key set of the canonical en.json (nested JSON; noctalia.tr resolves the
dotted keys against the nesting).
- Legacy strings reused from i18n/<locale>.json.
- Translated fresh: detail.decrypting, detail.error,
notification.copyFailed, notification.typeFailed, action.copyUsername,
action.typeUsername, settings.pinentryGraceMs.{label,desc}, plus the
reworded settings.typeDelay.label ("Type delay", not the old misleading
"Launcher Close Delay") and settings.clipTimeout.desc (no more {value}
placeholder).
- Dropped legacy-only keys absent from the v5 code: result.passwordEntry,
settings.tab.{general,advanced}.
- Fixed legacy typos while porting: nl "Gekopiëerd" -> "Gekopieerd",
pt "predefenido" -> "predefinido".
Key parity across all 11 locale files verified by script. i18n/ stays on
disk until the Cleanup step.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
Replaces the v4 README (which still referenced Settings.qml labels and the dead `qs -c noctalia-shell ipc call plugin:launcher-pass toggle`). Follows the community-plugins README_TEMPLATE.md layout (Plugin table / Requirements / Usage / Settings / IPC / Notes), matching sibling pass/README.md: - `/pass` prefix, with the shell.launcher.provider_prefix caveat. - Required deps (pass, find, grep; + gpg, wl-clipboard for `pass -c`) vs optional (pass-otp for the OTP rows, wtype for the Type rows). - Detail-view row list + the ":<path> <filter>" row filter, described in user terms. - Copy vs Type behaviour and which copies auto-clear the clipboard. - pinentry hide/reopen. - IPC: no custom plugin IPC; `noctalia msg panel-open|toggle|close launcher "/pass "` to open into the provider. - Notes: the native auto-paste limitation, filesystem reads, spawned processes, secret handling, no network, no writes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
Removes the superseded Noctalia v4 files now that plugin.toml +
launcher.luau + translations/ cover the whole feature set:
- Main.qml, LauncherProvider.qml, Settings.qml (QML entry points)
- manifest.json (-> plugin.toml)
- settings.json (stale local snapshot)
- i18n/ (-> translations/, ported
in the previous commit)
preview.png is kept for now — it's the source for thumbnail.webp (Next
steps noctalia-dev#4) and gets deleted in that commit.
launcher.luau's header comment refreshed: dropped the "Port step 1/2"
framing and the reference to the now-deleted LauncherProvider.qml.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
Two new advanced select settings shape the Copy/Type row layout in an
entry's detail view:
- detailActionOrder: "copy" (default) or "type" — which verb's row comes
first for each value.
- detailActionGrouping: "interleaved" (default) pairs each value's Copy
and Type rows; "grouped" emits every Copy row then every Type row, each
block in the detailActionOrder direction.
renderDetailRows now assembles a `specs` list in the fixed value order
(password, OTP, username, fields), resolves a `verbs` list from
detailActionOrder ("type" dropped when wtype is absent), then nests
spec x verb either way. addAction became actionRow(entryPath, spec, verb,
n), which returns one row and records its detailActions[act:<n>] context;
the id counter is now an explicit `n` instead of `#rows`.
Default (copy / interleaved) reproduces the previous layout exactly.
plugin.toml: two [[setting]] blocks with inline-table `options`.
Translations: 8 new keys (settings.detailAction{Order,Grouping}.* incl.
.options.*) across all 11 locales; the generator was reworked to also
emit en.json (byte-identical to the hand-maintained file) so all 11 stay
in lockstep. README settings table updated.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
rankEntry previously scored each matched entry with a Luau double loop (count fragments equal to a whole path segment). It now sums noctalia.fuzzyScore(fragment, nameLower) over the query fragments — the host's native fzy scorer (case-insensitive; exact match = 1024; prefix / word-boundary / consecutive-run bonuses; longer text penalised) — and returns that plus path depth for the tiebreak. renderRows' sort key follows (score desc, depth asc, name asc). - Matching is unchanged: the `grep -iF` pre-filter and matchesFragments still decide which rows appear. fuzzyScore is only the sort key, so "Behaviour to preserve noctalia-dev#4" (whitespace-split, each fragment a contiguous substring, order-independent, space = wildcard) holds. - Summed per fragment rather than passed as one fzy pattern, so the match stays order-independent and the inter-fragment space keeps acting as a wildcard; a fragment fzy rejects contributes 0. - Moves the per-entry scoring from the Luau interpreter into C, over the capped grep hit set — the point of this optimization step for large stores. - No plugin_api bump; fuzzyScore is in the noctalia base lib. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
Fixes `script callback 'async command callback' exceeded its CPU budget`
(25 ms) on a large store — first for a broad search ("w"), then, after an
interim all-in-Luau index, at load in parseIndex (:227).
Architecture:
- buildIndex writes <pluginDataDir>/store.index once via
`find … -name '.*' -prune -o -type f -name '*.gpg' -printf '%P\n'
-o -type d -printf '%P/\n' > file` (shell string, redirected). The
callback only sets indexReady/indexAt — negligible Luau, safe at load.
Rebuilt in the background past INDEX_TTL_MS, grepping the current file
meanwhile.
- onQuery greps that file in a subprocess (off the Luau budget):
searchGrepCmd = `grep -iF` AND-chain + `head -n 500`; browseGrepCmd =
`grep -E '^<reEsc folder>/[^/]+/?$'` + `head -n 2000` (root pattern
`^[^/]+/?$`). Luau parseHits only the capped output.
- renderSearchRows: prerank (cheap pure-Luau, segHits+depth) over every
hit, then fuzzySum (Σ noctalia.fuzzyScore) over only the top
SCORE_POOL=64. fuzzyScore ≤ 64×/keystroke, not ~900×.
- renderBrowseRows: sort the already-direct-children hits by basename.
Removed: listCache, parseEntries, searchCmd/browseArgv/browseCacheKey,
folderDir, prefetchBrowse/prefetchChildren/pendingFetch, GREP_CAP/
BROWSE_TTL_MS/SEARCH_TTL_MS, renderRows. Added: shq (back), reEsc,
indexPath, parseHits, browseGrepCmd/searchGrepCmd, renderBrowseRows/
renderSearchRows.
Match semantics unchanged. Measured with real luau against
~/.password-store (843 entries): ~1.5 ms Luau/keystroke, flat over 1000
repeats.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
The port is complete, so the doc no longer tracks it. Removed: "what this needs to become", Port status, Next steps, Open questions, the v5 target file structure sketch, the plugin.toml/translations sketches, the QML→Luau runtime mapping, and the v4/QML legacy reference. New structure, ordered for further development: - Files + plugin_api level - How launcher.luau works — navigation encoding, index file + grep, ranking, detail mode, copy/type actions, the pinentry dance - Behaviour contract — the invariants not to regress - Design decisions — dedicated chapter summarising the choices made during development, each with its rationale - Noctalia plugin runtime reference — provider contract, the noctalia.* calls actually used, the 25 ms CPU budget, subprocess rules - Luau conventions & gotchas (kept) - Working on this plugin — the luau-harness test approach, the translations regen rule, the commit workflow Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ABXQ7ZihMJoovXkfYCxZYt
b0d1ebb to
b34aaea
Compare
The plugin-store validator rejects `translations/*.json` keys that aren't a single lowercase segment (no uppercase, no dots) and `label_key` / `description_key` values in plugin.toml that contain uppercase. Every camelCase translation key tripped it: `result.goBack`, `notification.copyFailed`, `action.copyPassword…`, `settings.storePath…`, etc. Renamed the offending key segments to kebab-case across en.json, it.json, plugin.toml (label_key / description_key / option label_key), and the `noctalia.tr(...)` call sites in launcher.luau. The setting `key` fields (`storePath`, `detailActionOrder`, …) are unchanged — they're config keys read by `noctalia.getConfig`, not translation keys, and the validator doesn't touch them. Nesting and English/Italian text are untouched. Updated the two now-stale key examples in CLAUDE.md and noted the kebab-case rule. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6Xb6bXQJSm8PamMPYa1hA
299c2d0 to
c2761d2
Compare
fetchDetail closes the launcher panel when the pinentry grace period elapses, then reopened it unconditionally once `pass show` resolved. If the user dismissed the pinentry dialog (or gpg errored / timed out), `pass show` exits non-zero, the error row is rendered, but the panel was still reopened with the detail context — the host re-delivers that as a fresh onQuery, fetchDetail runs again (the failure was never cached), starts another `pass show`, and pinentry pops again. Nothing between the reopen and the next prompt is user-driven, so it never stops. Reopen the panel only when the decrypt succeeds. A non-zero exit or timeout now leaves the launcher closed — the graceful exit the user asked for; retrying is an explicit reopen. The success path (reopen -> warm detailCache -> render) is unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wJaioGzCA4WwL1s4Dygj1
|
Looks good to me |
|
Seems like a superset of the existing "Pass" plugin.
|
Plugin
launcher-pass
Id:
mellotanica/launcher-passNew plugin
Update to an existing plugin (version bumped in
plugin.toml)What it does
Browse a GNU Pass password store from the
Noctalia launcher and copy or auto-type passwords, OTP codes, usernames, and any
other field without opening a terminal with Noctalia fuzzy find always on.
External dependencies
passandpass-otpto retrieve password contentswtypeto perform password content autotypingfindandgrepto build and prefilter the a password store index cache used to provide the launcher content listsleepto implement timeouts on external actions (give time to the pinentry program to show up and unlock password store, or wait for the focus to return to the previous spot before autotyping)Testing
I tested both explicit prefix input in launcher and documented IPC activation via hyprland keybinding, each setting field was tested after its implementation, using a separate password store to check back and forth that store selection worked as well as the other parameters.
Also I am using the plugin constantly since I relied on previous version on Noctalia v4 up until I updated to v5 beta.
Screenshots / Videos
None
Checklist
Ready-for-review requirement: Every box in this section must be checked. If any statement is not true, keep the
pull request as Draft. An explanation does not replace a required check.
idafter the/inplugin.tomlexactly.plugin.toml,README.md,thumbnail.webp, andtranslations/en.json.README.mdfollows theREADME template, documents
every entry id and dependency, and includes exact panel IPC commands and launcher prefixes where applicable.
thumbnail.webpwith the thumbnail generator.versionfollows semver and is bumped in this PR;plugin_apiis the oldest API level this plugin requires.understand that language well enough to review and maintain it (no unreviewed machine/LLM translations).
catalog.toml; CI generates it.Code review attestation
Plugins run as trusted, unsandboxed Luau in the user's session. Confirm:
Ready-for-review requirement: Every attestation below must be checked.
licensedeclared inplugin.toml.