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',