Allow copying the highlighted item from the choice popup instead of inserting it (⌘C / Ctrl+C)
Description
In the popup that the choice extension shows, let the user copy the highlighted item to the clipboard instead of inserting it — a chord in that window (⌘C on macOS, Ctrl+C elsewhere), alongside the Enter that already inserts.
No schema change, no new config option: the choice is made by the user at the moment of use, in the window, rather than declared per match in YAML.
Where it would live (all in the window that choice already uses — SearchUI, not FormUI):
espanso-modulo/src/sys/search/search.cpp— aCopySelection()next to the existingSearchFrame::Submit().Submit()readswxIds[resultBox->GetSelection()], hands the id toresultCallbackand callsClose(true); the copy variant would look up the same id, put it on the clipboard, andClose(true)without invokingresultCallback. The chord slots intoSearchFrame::OnCharEvent, which already carries Escape / Tab / Alt+1‑9 / ↑↓ / Ctrl‑N / Ctrl‑P branches, so modifier chords in this window are established precedent.- Using wx's
event.ControlDown()(rather thanRawControlDown(), which the Ctrl‑N/Ctrl‑P branches use deliberately) gives ⌘C on macOS and Ctrl+C on Linux/Windows from one branch. - No new IPC result needed for the minimal version.
espanso/src/gui/modulo/search.rsreadsjson["selected"]and returnsOk(None)when it's absent;render/extension/choice.rsalready mapsOk(None) => ChoiceSelectorResult::Aborted. So "copied, inserted nothing" is an outcome the pipeline already handles.
Motivation
Motivation
The popup takes focus, and when it closes, the window that gets the injection is not always the window the user typed in. On macOS an NSPanel created with .nonactivatingPanel — floating notes and HUD-style panels, which several launcher-style apps use — holds keyboard focus without ever making its app the active application. Espanso's popup activates its own process (espanso-modulo/src/sys/common/mac.mm: activateWithOptions:(… | NSApplicationActivateIgnoringOtherApps)), and on dismissal macOS restores the previously active application. For those panels that's a different window, so the expansion and the backspaces that delete the trigger land in a document the user wasn't editing. Being able to copy instead makes the whole class of failure harmless: nothing is injected anywhere, and the user pastes where they actually are.
Alternatives
Alternatives
force_mode: clipboard doesn't do it — that's a transport to the same caret sink, and preserve_clipboard (default true, espanso-config/src/config/resolve.rs → restore_clipboard in worker/config.rs) puts the old clipboard back afterwards, so the expansion is deliberately not left behind. A paste is still injected into whatever window is active, so the wrong-window problem remains.
Shell workaround — works today, which is why I'd call this a papercut and not a blocker:
- trigger: ";pick"
replace: "{{out}}" # empty stdout, so nothing is injected
vars:
- name: pick
type: choice
params:
values: ["alpha", "beta"]
- name: out
type: shell
params:
cmd: 'printf "%s" "$ESPANSO_PICK" | pbcopy'Verified on macOS 26.6 / espanso 2.4.1: the chosen value lands on the clipboard and nothing is typed. ($ESPANSO_PICK rather than {{pick}} so a value containing a quote can't break out of the shell command.) But: a subprocess per expansion; an OS-specific clipboard command (pbcopy / xclip / wl-copy / clip.exe) so the match stops being portable; type: shell has to be acceptable in that config; it's all-or-nothing per match rather than a decision made at use time; and the intent is encoded as a side effect of an empty replacement, which is undiscoverable to whoever reads the file next.
Have you tried building it? Or would you like it?
- I'd be willing to contribute this feature to Espanso myself.
Source: espanso/espanso