feat(paste): auto-insert transcript at cursor via wtype/xdotool/ydotool/osascript/SendKeys

Adds an opt-in "auto-paste into focused window" toggle. When enabled, the
dictation pipeline sets the clipboard and then sends a Ctrl+V / Cmd+V
keystroke to whatever window currently has focus — the common case after a
global-hotkey dictation, since Kon's own window never stole focus.

Backend (src-tauri/src/commands/paste.rs) probes for a platform paste tool
and falls back cleanly:
- Linux Wayland: wtype > ydotool > xdotool
- Linux X11: xdotool > ydotool > wtype
- macOS: osascript System Events keystroke
- Windows: PowerShell WScript.Shell SendKeys

detect_paste_backends is a pure probe used by Settings to describe the
available backend next to the toggle (or nudge the user to install one).
paste_text always copies first, so auto-paste failure degrades to the
existing clipboard-only behaviour and surfaces a warn toast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 07:45:16 +01:00
parent 36efcf2320
commit 4c0c876ade
7 changed files with 304 additions and 1 deletions

View File

@@ -479,7 +479,22 @@
replaceSegmentsWithCleanedText(cleanedTranscript);
}
if (settings.autoCopy) {
if (settings.autoPaste) {
try {
const outcome = await invoke("paste_text", { text: transcript });
if (!outcome?.pasted && outcome?.message) {
toasts.warn(
"Auto-paste unavailable",
`${outcome.message}${outcome.copied ? ". Transcript is on your clipboard." : ""}`,
);
}
} catch (err) {
await navigator.clipboard
.writeText(transcript)
.catch(() => invoke("copy_to_clipboard", { text: transcript }).catch(() => {}));
toasts.warn("Auto-paste failed", String(err));
}
} else if (settings.autoCopy) {
navigator.clipboard.writeText(transcript).catch(() => {
invoke("copy_to_clipboard", { text: transcript }).catch(() => {});
});

View File

@@ -22,6 +22,13 @@
let engineOk = $state(false);
let downloadedModels = $state([]);
let runtimeCapabilities = $state(null);
let pasteBackends = $state([]);
let pasteBackendsDescription = $derived.by(() => {
if (pasteBackends.length === 0) {
return "Install wtype (Wayland) or xdotool (X11) to enable auto-paste. Focus must already be on the target window.";
}
return `Uses ${pasteBackends.join(", ")}. Focus must already be on the target window.`;
});
let downloadingModel = $state("");
let downloadProgress = $state(0);
let unlisten = null;
@@ -541,6 +548,12 @@
downloadedModels = await invoke("list_models");
} catch {}
try {
pasteBackends = (await invoke("detect_paste_backends")) || [];
} catch {
pasteBackends = [];
}
// Parakeet status
try {
parakeetOk = await invoke("check_parakeet_engine");
@@ -1498,6 +1511,11 @@
<div class="px-5 pb-5 animate-fade-in">
<div class="space-y-0.5">
<Toggle bind:checked={settings.autoCopy} label="Auto-copy to clipboard" />
<Toggle
bind:checked={settings.autoPaste}
label="Auto-paste into focused window"
description={pasteBackendsDescription}
/>
<Toggle bind:checked={settings.includeTimestamps} label="Include timestamps in exports" />
<Toggle
bind:checked={settings.saveAudio}

View File

@@ -46,6 +46,7 @@ const defaults: SettingsState = {
antiHallucination: true,
britishEnglish: true,
autoCopy: true,
autoPaste: false,
includeTimestamps: true,
theme: "Dark",
fontSize: 14,

View File

@@ -36,6 +36,7 @@ export interface SettingsState {
antiHallucination: boolean;
britishEnglish: boolean;
autoCopy: boolean;
autoPaste: boolean;
includeTimestamps: boolean;
theme: "Dark" | "Light" | "System";
fontSize: number;