diff --git a/PLAN.md b/PLAN.md
index 81fe071..149a8c1 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -668,9 +668,24 @@ the change rather than oppose it:
the deliberate act; the paste is its effect, not a hidden side effect of some other action.
What remains true is that a synthesized `Ctrl+V` does nothing in terminals that paste with
-`Ctrl+Shift+V`, so it is a setting rather than a law, and the addon refuses to paste when Spool's own
-window is in front — unspooling is meant to put a clip into the document you were already working in.
-**macOS, if it ever ships, serves without pasting**, and the reasoning above is why.
+`Ctrl+Shift+V`, and Windows refuses synthesized input to a window running as administrator, so it is
+a setting rather than a law. **macOS, if it ever ships, serves without pasting**, and the reasoning
+above is why.
+
+**Measured, and the first implementation did not work at all.** A hotkey fires on the key *down*, so
+at the instant the handler runs the user is still holding `Win+Alt`. Synthesizing `Ctrl+V` into that
+state delivers `Win+Alt+Ctrl+V`, which is a paste in no application on earth, and nothing happens.
+It presented as "unspooling advances the spool but never pastes", and it looked intermittent because
+a handler that happened to run after the keys came up worked perfectly — which is how it survived a
+round of testing. **So the addon lifts every modifier that is currently down before pressing Ctrl+V**,
+and does not restore them: the user's own keys are still physically held, their release is harmless,
+and re-pressing `Win` would open the Start menu.
+
+Two consequences follow from taking the user's account seriously rather than the code's. Pasting the
+whole spool pastes too — one key that pastes and one that silently changes the clipboard is an
+inconsistency, not a design. And **a paste that does not land says so**, naming `Ctrl+V` as the way
+out, because the alternative is what happened here: a key that appears to do nothing, and a user who
+reasonably concludes the app is broken.
### Hotkeys
diff --git a/native/clipboard/src/clipboard_unsupported.cc b/native/clipboard/src/clipboard_unsupported.cc
index c51b490..4d6a785 100644
--- a/native/clipboard/src/clipboard_unsupported.cc
+++ b/native/clipboard/src/clipboard_unsupported.cc
@@ -27,11 +27,16 @@ Napi::Value SendPaste(const Napi::CallbackInfo& info) {
return Napi::Boolean::New(info.Env(), false);
}
+Napi::Value ForegroundIsSelf(const Napi::CallbackInfo& info) {
+ return Napi::Boolean::New(info.Env(), false);
+}
+
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("start", Napi::Function::New(env, Start));
exports.Set("stop", Napi::Function::New(env, Stop));
exports.Set("isSupported", Napi::Function::New(env, IsSupported));
exports.Set("sendPaste", Napi::Function::New(env, SendPaste));
+ exports.Set("foregroundIsSelf", Napi::Function::New(env, ForegroundIsSelf));
return exports;
}
diff --git a/native/clipboard/src/clipboard_win.cc b/native/clipboard/src/clipboard_win.cc
index 5539d72..305ff8a 100644
--- a/native/clipboard/src/clipboard_win.cc
+++ b/native/clipboard/src/clipboard_win.cc
@@ -325,24 +325,66 @@ Napi::Value SendPaste(const Napi::CallbackInfo& info) {
GetWindowThreadProcessId(foreground, &foreground_pid);
if (foreground_pid == GetCurrentProcessId()) return Napi::Boolean::New(env, false);
- INPUT inputs[4] = {};
+ // **Release whatever the user is still holding first.**
+ //
+ // The hotkey that asked for this paste fires on the key *down*, so at this instant Win and Alt
+ // are almost certainly still held — the user has not let go of `Win+Alt+U` yet. Synthesizing
+ // Ctrl+V into that state delivers `Win+Alt+Ctrl+V`, which is not a paste in any application, and
+ // nothing happens. It cost a user their trust in the feature before it was understood, and it
+ // looked intermittent because a handler that happened to run after the keys came up worked fine.
+ //
+ // So: lift every modifier that is currently down, then press Ctrl+V cleanly. They are not
+ // restored afterwards. The user's own keys are still physically held and their next release is
+ // harmless, whereas re-pressing Win here would open the Start menu.
+ const WORD kModifiers[] = {VK_LWIN, VK_RWIN, VK_LMENU, VK_RMENU,
+ VK_LSHIFT, VK_RSHIFT, VK_LCONTROL, VK_RCONTROL};
+
+ std::vector inputs;
+ for (WORD vk : kModifiers) {
+ if ((GetAsyncKeyState(vk) & 0x8000) == 0) continue;
+ INPUT up = {};
+ up.type = INPUT_KEYBOARD;
+ up.ki.wVk = vk;
+ up.ki.dwFlags = KEYEVENTF_KEYUP;
+ inputs.push_back(up);
+ }
+
+ const size_t released = inputs.size();
- inputs[0].type = INPUT_KEYBOARD;
- inputs[0].ki.wVk = VK_CONTROL;
+ INPUT press = {};
+ press.type = INPUT_KEYBOARD;
+ press.ki.wVk = VK_CONTROL;
+ inputs.push_back(press);
- inputs[1].type = INPUT_KEYBOARD;
- inputs[1].ki.wVk = 'V';
+ press.ki.wVk = 'V';
+ inputs.push_back(press);
- inputs[2].type = INPUT_KEYBOARD;
- inputs[2].ki.wVk = 'V';
- inputs[2].ki.dwFlags = KEYEVENTF_KEYUP;
+ INPUT release = {};
+ release.type = INPUT_KEYBOARD;
+ release.ki.dwFlags = KEYEVENTF_KEYUP;
+ release.ki.wVk = 'V';
+ inputs.push_back(release);
- inputs[3].type = INPUT_KEYBOARD;
- inputs[3].ki.wVk = VK_CONTROL;
- inputs[3].ki.dwFlags = KEYEVENTF_KEYUP;
+ release.ki.wVk = VK_CONTROL;
+ inputs.push_back(release);
- const UINT sent = SendInput(4, inputs, sizeof(INPUT));
- return Napi::Boolean::New(env, sent == 4);
+ const UINT expected = static_cast(released + 4);
+ const UINT sent = SendInput(expected, inputs.data(), sizeof(INPUT));
+ return Napi::Boolean::New(env, sent == expected);
+}
+
+// Whether Spool's own window is the one in front.
+//
+// Asked before serving, because it decides where the clip is meant to go. If we are in front, the
+// window has to get out of the way first: the user is looking at Spool, but the clip is for
+// whatever they were working in before they opened it.
+Napi::Value ForegroundIsSelf(const Napi::CallbackInfo& info) {
+ HWND foreground = GetForegroundWindow();
+ if (foreground == nullptr) return Napi::Boolean::New(info.Env(), false);
+
+ DWORD foreground_pid = 0;
+ GetWindowThreadProcessId(foreground, &foreground_pid);
+ return Napi::Boolean::New(info.Env(), foreground_pid == GetCurrentProcessId());
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
@@ -350,6 +392,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("stop", Napi::Function::New(env, Stop));
exports.Set("isSupported", Napi::Function::New(env, IsSupported));
exports.Set("sendPaste", Napi::Function::New(env, SendPaste));
+ exports.Set("foregroundIsSelf", Napi::Function::New(env, ForegroundIsSelf));
return exports;
}
diff --git a/src/main/clipboard/writer.ts b/src/main/clipboard/writer.ts
index 693379a..2076e42 100644
--- a/src/main/clipboard/writer.ts
+++ b/src/main/clipboard/writer.ts
@@ -32,3 +32,14 @@ export function sendPaste(): boolean {
return false
}
}
+
+/** Whether Spool's own window is in front, which decides where a clip is meant to go. */
+export function foregroundIsSelf(): boolean {
+ try {
+ const require = createRequire(__filename)
+ const addon = require('spool-clipboard') as { foregroundIsSelf?: () => boolean }
+ return addon.foregroundIsSelf?.() ?? false
+ } catch {
+ return false
+ }
+}
diff --git a/src/main/index.ts b/src/main/index.ts
index 0f00311..730f9bd 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -15,11 +15,12 @@ import {
import { registerIpc } from './ipc'
import { Session } from './session'
import { explainStorageFailure, openStore, resetEverything, startFresh, storePaths } from './store'
-import { sendPaste, writeClipboardText } from './clipboard/writer'
+import { foregroundIsSelf, sendPaste, writeClipboardText } from './clipboard/writer'
import { createTray, reportCaptureState } from './tray'
import { loadSettings, saveSettings, settingsPath, type WindowState } from './settings'
import {
createCompactWindow,
+ dismissCompactWindow,
getCompactWindow,
restoreWindowState,
setWindowState,
@@ -28,6 +29,29 @@ import {
} from './window'
// One instance owns the tray icon and the hotkeys; a second launch summons the first.
+/**
+ * Paste into the window the user was actually working in (PLAN.md 8).
+ *
+ * A global hotkey does not steal focus, so normally the window they were typing in still has it and
+ * the paste simply lands. The exception is when Spool itself is in front — they summoned it and have
+ * not clicked away — and then the only sensible target is whatever they were in *before* they opened
+ * it. So the window gets out of the way first, and the paste follows once focus has moved back.
+ *
+ * The delay is the cost of that. Windows moves focus asynchronously after a window hides, and
+ * synthesizing a keystroke into the gap would deliver it nowhere.
+ */
+function pasteWhereTheUserWas(report: (pasted: boolean) => void): void {
+ if (foregroundIsSelf() && dismissCompactWindow()) {
+ setTimeout(() => report(sendPaste()), FOCUS_SETTLE_MS)
+ return
+ }
+
+ report(sendPaste())
+}
+
+/** Long enough for focus to land on the window behind ours, short enough not to be felt. */
+const FOCUS_SETTLE_MS = 120
+
if (!app.requestSingleInstanceLock()) {
app.quit()
} else {
@@ -43,7 +67,7 @@ if (!app.requestSingleInstanceLock()) {
app.isPackaged
)
- const spoolSession = new Session(writeClipboardText, sendPaste)
+ const spoolSession = new Session(writeClipboardText, pasteWhereTheUserWas)
/**
* Open the encrypted store and restore what it holds (PLAN.md 11, M6). A failure is reported
@@ -63,7 +87,7 @@ if (!app.requestSingleInstanceLock()) {
spoolSession.setSeparator(settings.separator)
spoolSession.setPrivacyAcknowledged(settings.privacyAcknowledged)
- spoolSession.setPasteOnServe(settings.pasteOnServe)
+ spoolSession.setAutoPaste(settings.autoPaste)
spoolSession.setConsentTimeout(settings.consentTimeoutSeconds)
registerIpc(spoolSession, getCompactWindow, {
@@ -82,7 +106,7 @@ if (!app.requestSingleInstanceLock()) {
consentTimeoutSeconds: spoolSession.getConsentTimeoutSeconds(),
privacyAcknowledged: true,
hotkeys: hotkeyOverrides(),
- pasteOnServe: spoolSession.getPasteOnServe()
+ autoPaste: spoolSession.getAutoPaste()
})
},
@@ -126,7 +150,7 @@ if (!app.requestSingleInstanceLock()) {
consentTimeoutSeconds: spoolSession.getConsentTimeoutSeconds(),
privacyAcknowledged: !spoolSession.isFirstRun(),
hotkeys: hotkeyOverrides(),
- pasteOnServe: spoolSession.getPasteOnServe()
+ autoPaste: spoolSession.getAutoPaste()
})
}
})
@@ -140,7 +164,7 @@ if (!app.requestSingleInstanceLock()) {
consentTimeoutSeconds: spoolSession.getConsentTimeoutSeconds(),
privacyAcknowledged: !spoolSession.isFirstRun(),
hotkeys: hotkeyOverrides(),
- pasteOnServe: spoolSession.getPasteOnServe()
+ autoPaste: spoolSession.getAutoPaste()
})
)
diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts
index ff61947..f6b66b9 100644
--- a/src/main/ipc/index.ts
+++ b/src/main/ipc/index.ts
@@ -84,9 +84,7 @@ export function registerIpc(
ipcMain.handle(CHANNELS.pauseCapture, () => session.pauseCapture())
ipcMain.handle(CHANNELS.acknowledgePrivacy, () => actions.acknowledgePrivacy())
ipcMain.handle(CHANNELS.toggleMode, () => session.toggleMode())
- ipcMain.handle(CHANNELS.setPasteOnServe, (_event, enabled: boolean) =>
- session.setPasteOnServe(enabled)
- )
+ ipcMain.handle(CHANNELS.setAutoPaste, (_event, enabled: boolean) => session.setAutoPaste(enabled))
ipcMain.handle(CHANNELS.setHotkey, (_event, action: HotkeyAction, accelerator: string) =>
actions.setHotkey(action, accelerator)
)
@@ -133,7 +131,7 @@ export function registerIpc(
ipcMain.removeHandler(CHANNELS.pauseCapture)
ipcMain.removeHandler(CHANNELS.acknowledgePrivacy)
ipcMain.removeHandler(CHANNELS.toggleMode)
- ipcMain.removeHandler(CHANNELS.setPasteOnServe)
+ ipcMain.removeHandler(CHANNELS.setAutoPaste)
ipcMain.removeHandler(CHANNELS.setHotkey)
ipcMain.removeHandler(CHANNELS.resetHotkey)
ipcMain.removeHandler(CHANNELS.resumeCapture)
diff --git a/src/main/session.test.ts b/src/main/session.test.ts
index 3e027cc..ef6a966 100644
--- a/src/main/session.test.ts
+++ b/src/main/session.test.ts
@@ -32,7 +32,7 @@ const text = (value: string, sourceApp: string | null = null): ClipboardSnapshot
sourceApp
})
-function started(paste: () => boolean = () => false): {
+function started(paste: (report: (pasted: boolean) => void) => void = (r) => r(false)): {
session: Session
watcher: ReturnType
written: string[]
@@ -278,9 +278,9 @@ describe('serving (PLAN.md 11, M4)', () => {
it('pastes the clip it serves into the window in front', () => {
let pastes = 0
- const { session, watcher, written } = started(() => {
+ const { session, watcher, written } = started((report) => {
pastes += 1
- return true
+ report(true)
})
watcher.change(text('into the form'))
@@ -292,11 +292,11 @@ describe('serving (PLAN.md 11, M4)', () => {
it('serves without pasting when the user has turned that off', () => {
let pastes = 0
- const { session, watcher, written } = started(() => {
+ const { session, watcher, written } = started((report) => {
pastes += 1
- return true
+ report(true)
})
- session.setPasteOnServe(false)
+ session.setAutoPaste(false)
watcher.change(text('placed, not typed'))
session.serveNext()
@@ -307,9 +307,9 @@ describe('serving (PLAN.md 11, M4)', () => {
it('does not paste when there was nothing to serve', () => {
let pastes = 0
- const { session } = started(() => {
+ const { session } = started((report) => {
pastes += 1
- return true
+ report(true)
})
session.serveNext()
@@ -318,6 +318,26 @@ describe('serving (PLAN.md 11, M4)', () => {
expect(session.getState().notice?.category).toBe('nothing_to_paste')
})
+ // A synthesized Ctrl+V can fail for reasons the app cannot control. Failing silently is what
+ // made the user conclude the app was broken rather than that the clip was on their clipboard.
+ it('says so when the paste did not land, and says what to do instead', () => {
+ const { session, watcher } = started((report) => report(false))
+ watcher.change(text('somewhere that refused it'))
+
+ session.serveNext()
+
+ expect(session.getState().notice?.message).toMatch(/Ctrl\+V/)
+ })
+
+ it('says nothing when the paste landed', () => {
+ const { session, watcher } = started((report) => report(true))
+ watcher.change(text('somewhere that took it'))
+
+ session.serveNext()
+
+ expect(session.getState().notice).toBeNull()
+ })
+
it('leaves the served clip on the clipboard to be pasted as often as the user likes', () => {
const { session, watcher, written } = started()
watcher.change(text('once served'))
diff --git a/src/main/session.ts b/src/main/session.ts
index 8ff413e..43cce6c 100644
--- a/src/main/session.ts
+++ b/src/main/session.ts
@@ -140,26 +140,56 @@ export class Session {
* which is also what lets every rule below be tested without launching the app.
*/
/**
- * Whether serving also pastes (PLAN.md 8). On by default: unspooling into the document you are
- * already typing in is the thing this app is for, and making it two keystrokes made the second
- * one feel like a tax. It stays a setting because a synthesized Ctrl+V does nothing in terminals
- * that paste with Ctrl+Shift+V, and because some people would rather place than place-and-type.
+ * Whether putting something on the clipboard also pastes it (PLAN.md 8). On by default:
+ * unspooling into the document you are already typing in is the thing this app is for, and making
+ * it two keystrokes made the second one feel like a tax.
+ *
+ * It governs **both** placements. Unspooling pasted and pasting the whole spool did not, which
+ * made `V` a key that silently changed the clipboard and showed nothing — the inconsistency was
+ * the bug, not either half of it.
+ *
+ * It stays a setting because a synthesized Ctrl+V does nothing in terminals that paste with
+ * Ctrl+Shift+V, and because Windows refuses synthesized input to an elevated window, which no
+ * amount of care here can change.
*/
- private pasteOnServe = true
+ private autoPaste = true
constructor(
private readonly writeText: (text: string) => void,
- /** Synthesize the paste. Returns false when it declined — our own window was in front. */
- private readonly paste: () => boolean = () => false
+ /**
+ * Put the clipboard where the user was working. It may have to dismiss our own window first and
+ * wait for focus to return, so the answer comes back through the callback rather than as a
+ * return value — and it always comes back, because a paste that did not land has to be said.
+ */
+ private readonly paste: (report: (pasted: boolean) => void) => void = (report) => report(false)
) {}
- setPasteOnServe(enabled: boolean): void {
- this.pasteOnServe = enabled
+ /**
+ * What happened to the last paste (PLAN.md 8).
+ *
+ * A synthesized Ctrl+V can fail for reasons this app cannot control — a terminal that pastes with
+ * Ctrl+Shift+V, a window running as administrator, which Windows refuses synthesized input to —
+ * and **failing silently is the worst of the options**. The user presses a key, nothing appears,
+ * and they conclude the app is broken rather than that the clip is sitting on their clipboard
+ * waiting for Ctrl+V. So it is reported, in the window, in the words of what to do next.
+ */
+ reportPasteResult(pasted: boolean): void {
+ if (pasted) return
+
+ this.notice = {
+ category: 'unsupported',
+ message: 'It is on your clipboard — press Ctrl+V. Spool could not paste into that window.'
+ }
+ this.publish()
+ }
+
+ setAutoPaste(enabled: boolean): void {
+ this.autoPaste = enabled
this.publish()
}
- getPasteOnServe(): boolean {
- return this.pasteOnServe
+ getAutoPaste(): boolean {
+ return this.autoPaste
}
/**
@@ -381,7 +411,7 @@ export class Session {
// Then put it where the user was typing. The clip stays on the clipboard afterwards, so the
// plan's reason for keeping these separate — serve once, paste into four places — still holds:
// this adds the first paste rather than taking the others away (PLAN.md 8).
- if (this.pasteOnServe) this.paste()
+ if (this.autoPaste) this.paste((pasted) => this.reportPasteResult(pasted))
this.publish()
}
@@ -418,8 +448,15 @@ export class Session {
this.state = { ...this.state, pendingSelfWrite: joined.text }
this.notice = {
category: 'pasted_spool',
- message: `${joined.clips} clips are on the clipboard, ready to paste`
+ message: this.autoPaste
+ ? `${joined.clips} clips pasted, and still on the clipboard`
+ : `${joined.clips} clips are on the clipboard, ready to paste`
}
+
+ // The whole spool lands the same way a single clip does. Pasting one but not the other made
+ // this key look broken: it changed the clipboard and showed nothing (PLAN.md 8).
+ if (this.autoPaste) this.paste((pasted) => this.reportPasteResult(pasted))
+
this.publish()
}
@@ -911,7 +948,7 @@ export class Session {
capacity: this.capacityView(),
firstRun: this.firstRun,
hotkeys: this.hotkeys,
- pasteOnServe: this.pasteOnServe,
+ autoPaste: this.autoPaste,
prompt: this.promptView(),
privacy: {
heuristics: HEURISTIC_RULES,
diff --git a/src/main/settings.test.ts b/src/main/settings.test.ts
index afb2c3b..84a80d9 100644
--- a/src/main/settings.test.ts
+++ b/src/main/settings.test.ts
@@ -28,7 +28,7 @@ describe('settings (PLAN.md 3, 8)', () => {
consentTimeoutSeconds: 45,
privacyAcknowledged: true,
hotkeys: {},
- pasteOnServe: true
+ autoPaste: true
})
expect(loadSettings(path())).toEqual({
@@ -38,7 +38,7 @@ describe('settings (PLAN.md 3, 8)', () => {
consentTimeoutSeconds: 45,
privacyAcknowledged: true,
hotkeys: {},
- pasteOnServe: true
+ autoPaste: true
})
})
@@ -71,7 +71,7 @@ describe('settings (PLAN.md 3, 8)', () => {
consentTimeoutSeconds: 30,
privacyAcknowledged: false,
hotkeys: {},
- pasteOnServe: true
+ autoPaste: true
})
// The cautious default: a settings file that says nothing about it has not agreed to anything.
diff --git a/src/main/settings.ts b/src/main/settings.ts
index e7295a1..65b1171 100644
--- a/src/main/settings.ts
+++ b/src/main/settings.ts
@@ -36,8 +36,8 @@ export interface Settings {
* as an empty string, and is why this is a sparse map rather than a full record (PLAN.md 8).
*/
readonly hotkeys: Partial>
- /** Whether serving a clip also pastes it into the foreground window (PLAN.md 8). */
- readonly pasteOnServe: boolean
+ /** Whether placing something on the clipboard also pastes it where you were (PLAN.md 8). */
+ readonly autoPaste: boolean
}
export const DEFAULT_SETTINGS: Settings = {
@@ -47,7 +47,7 @@ export const DEFAULT_SETTINGS: Settings = {
consentTimeoutSeconds: 30,
privacyAcknowledged: false,
hotkeys: {},
- pasteOnServe: true
+ autoPaste: true
}
export function settingsPath(userDataDirectory: string): string {
@@ -112,7 +112,9 @@ export function loadSettings(path: string): Settings {
privacyAcknowledged: raw.privacyAcknowledged === true,
hotkeys: readHotkeys(raw.hotkeys),
// Absent means on: the default is the behaviour, and only an explicit false turns it off.
- pasteOnServe: raw.pasteOnServe !== false
+ // `pasteOnServe` is the name this shipped under before it governed the whole spool as well;
+ // a settings file written by that build still says what its author meant.
+ autoPaste: raw.autoPaste !== false && (raw as { pasteOnServe?: unknown }).pasteOnServe !== false
}
}
diff --git a/src/main/window.ts b/src/main/window.ts
index aee1f5f..01b9ec1 100644
--- a/src/main/window.ts
+++ b/src/main/window.ts
@@ -107,6 +107,22 @@ export function showCompactWindow(): void {
window.focus()
}
+/**
+ * Get out of the way so a paste can land where the user was working (PLAN.md 8).
+ *
+ * Hiding returns focus to whatever had it before Spool was summoned, which is the window the clip
+ * is actually for. Returns whether anything was hidden, so the caller knows to wait for focus to
+ * settle before synthesizing anything.
+ */
+export function dismissCompactWindow(): boolean {
+ const window = compactWindow
+ if (window && window.isVisible()) {
+ window.hide()
+ return true
+ }
+ return false
+}
+
/** The summon hotkey is a toggle: visible and focused means dismiss (PLAN.md 8). */
export function toggleCompactWindow(): void {
const window = compactWindow
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 41c09bd..ba2f295 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -70,9 +70,9 @@ const api = {
/** The capacity advisor (PLAN.md 9): it recommends, the user decides. */
dismissCapacityAdvice: (): Promise => ipcRenderer.invoke(CHANNELS.dismissCapacityAdvice),
- /** Whether unspooling also pastes into the window in front (PLAN.md 8). */
- setPasteOnServe: (enabled: boolean): Promise =>
- ipcRenderer.invoke(CHANNELS.setPasteOnServe, enabled),
+ /** Whether placing something on the clipboard also pastes it where you were (PLAN.md 8). */
+ setAutoPaste: (enabled: boolean): Promise =>
+ ipcRenderer.invoke(CHANNELS.setAutoPaste, enabled),
/** Change direction. On the mode pill rather than a hotkey (PLAN.md 8). */
toggleMode: (): Promise => ipcRenderer.invoke(CHANNELS.toggleMode),
diff --git a/src/renderer/components/SettingsPanel.tsx b/src/renderer/components/SettingsPanel.tsx
index d6829a7..b5e9e74 100644
--- a/src/renderer/components/SettingsPanel.tsx
+++ b/src/renderer/components/SettingsPanel.tsx
@@ -117,22 +117,23 @@ export function SettingsPanel({ state, onBack }: { state: AppState; onBack: () =
-
+
- The clip stays on the clipboard either way, so you can paste it again elsewhere. Turn
- this off if you work in a terminal that pastes with Ctrl+Shift+V, where a synthesized
- Ctrl+V does nothing.
+ Applies to unspooling a clip and to pasting the whole spool. It stays on the clipboard
+ either way, so you can paste it again elsewhere. Two cases where it will not arrive:
+ terminals that paste with Ctrl+Shift+V, and windows running as administrator, which
+ Windows refuses synthesized input to.
diff --git a/src/renderer/env.d.ts b/src/renderer/env.d.ts
index 31ece53..4279c87 100644
--- a/src/renderer/env.d.ts
+++ b/src/renderer/env.d.ts
@@ -27,7 +27,7 @@ declare global {
pauseCapture(): Promise
acknowledgePrivacy(): Promise
toggleMode(): Promise
- setPasteOnServe(enabled: boolean): Promise
+ setAutoPaste(enabled: boolean): Promise
setHotkey(action: HotkeyAction, accelerator: string): Promise
resetHotkey(action: HotkeyAction): Promise
resumeCapture(): Promise
diff --git a/src/renderer/state/useAppState.ts b/src/renderer/state/useAppState.ts
index ff6208e..9e07e4e 100644
--- a/src/renderer/state/useAppState.ts
+++ b/src/renderer/state/useAppState.ts
@@ -31,7 +31,7 @@ const initialState: AppState = {
pendingJoin: null,
firstRun: false,
hotkeys: [],
- pasteOnServe: true,
+ autoPaste: true,
capacity: {
measure: 'bytes',
used: 0,
diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts
index 497c5a2..01ae58e 100644
--- a/src/shared/ipc.ts
+++ b/src/shared/ipc.ts
@@ -200,8 +200,8 @@ export interface AppState {
readonly firstRun: boolean
/** Every hotkey and whether it is live, for the panel that doubles as the reference (PLAN.md 8). */
readonly hotkeys: readonly HotkeyView[]
- /** Whether unspooling also pastes into the window in front (PLAN.md 8). */
- readonly pasteOnServe: boolean
+ /** Whether placing something on the clipboard also pastes it where you were (PLAN.md 8). */
+ readonly autoPaste: boolean
}
/** The channel names, in one place so the two sides cannot drift apart. */
@@ -229,7 +229,7 @@ export const CHANNELS = {
acknowledgePrivacy: 'spool:acknowledge-privacy',
toggleMode: 'spool:toggle-mode',
setHotkey: 'spool:set-hotkey',
- setPasteOnServe: 'spool:set-paste-on-serve',
+ setAutoPaste: 'spool:set-auto-paste',
resetHotkey: 'spool:reset-hotkey',
resumeCapture: 'spool:resume-capture',
deleteSpools: 'spool:delete-spools',