Skip to content

Commit 85c9c5a

Browse files
authored
Merge pull request #176 from grota/feature/restore_clipboard
feat: Clipboard Restoration Feature
2 parents ab88c93 + 284cc3a commit 85c9c5a

File tree

9 files changed

+519
-1
lines changed

9 files changed

+519
-1
lines changed

docs/CONFIGURATION.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,47 @@ paste_keys = "shift+insert" # For Hyprland/Omarchy
12001200
- Letters: `a-z`
12011201
- Special: `insert`, `enter`
12021202

1203+
### restore_clipboard
1204+
1205+
**Type:** Boolean
1206+
**Default:** `false`
1207+
**Required:** No
1208+
**Applies to:** Paste mode only
1209+
1210+
When `true`, voxtype saves your clipboard content before transcription and restores it after the paste operation completes. This prevents your original clipboard content from being overwritten by the transcription.
1211+
1212+
**How it works:**
1213+
1. Before transcription: Save current clipboard content (including MIME type for binary data)
1214+
2. Copy transcribed text to clipboard
1215+
3. Simulate paste keystroke
1216+
4. After brief delay: Restore original clipboard content
1217+
1218+
**Example:**
1219+
```toml
1220+
[output]
1221+
mode = "paste"
1222+
restore_clipboard = true # Preserve original clipboard content
1223+
```
1224+
1225+
**Note:** This only works in `mode = "paste"`. In `mode = "clipboard"`, the user manually pastes the content, so restoration would interfere with the intended workflow.
1226+
1227+
### restore_clipboard_delay_ms
1228+
1229+
**Type:** Integer
1230+
**Default:** `200`
1231+
**Required:** No
1232+
**Applies to:** Paste mode only (when `restore_clipboard = true`)
1233+
1234+
Delay in milliseconds after the paste keystroke before restoring the original clipboard content. Increase this if the restoration happens too quickly and interferes with the paste operation. The default of 200ms works well for most applications including Electron apps (Slack, Discord, VS Code).
1235+
1236+
**Example:**
1237+
```toml
1238+
[output]
1239+
mode = "paste"
1240+
restore_clipboard = true
1241+
restore_clipboard_delay_ms = 300 # Longer delay for slower systems
1242+
```
1243+
12031244
### fallback_to_clipboard
12041245

12051246
**Type:** Boolean

docs/TROUBLESHOOTING.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,27 @@ echo "test" | wl-copy
805805
wl-paste
806806
```
807807

808+
### Clipboard not restored after paste
809+
810+
**Cause:** The restore delay may be too short for your application, or `wl-paste` is not installed.
811+
812+
**Solution:**
813+
814+
1. Make sure `wl-clipboard` is installed (provides both `wl-copy` and `wl-paste`):
815+
```bash
816+
# Arch: sudo pacman -S wl-clipboard
817+
# Debian: sudo apt install wl-clipboard
818+
# Fedora: sudo dnf install wl-clipboard
819+
```
820+
821+
2. If the clipboard is restored before the application reads it, increase the delay:
822+
```toml
823+
[output]
824+
restore_clipboard_delay_ms = 500 # Try 300-500ms for slow applications
825+
```
826+
827+
3. On X11, make sure `xclip` is installed for clipboard restoration support.
828+
808829
### No desktop notification
809830

810831
**Cause:** notify-send not installed or notifications disabled.

docs/USER_MANUAL.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1350,9 +1350,19 @@ mode = "paste"
13501350
**Cons**:
13511351
- Requires both wl-copy and ydotool
13521352
- Won't work in applications where Ctrl+V has a different meaning (e.g., Vim command mode)
1353-
- Overwrites clipboard contents
1353+
- Overwrites clipboard contents (unless clipboard restoration is enabled)
13541354
- No fallback behavior
13551355

1356+
**Clipboard Restoration**: By default, paste mode overwrites your clipboard with the transcribed text. If you want to preserve your clipboard contents, enable clipboard restoration:
1357+
1358+
```toml
1359+
[output]
1360+
mode = "paste"
1361+
restore_clipboard = true
1362+
```
1363+
1364+
When enabled, voxtype saves your clipboard content before pasting, then restores it after a brief delay. This works with both text and binary clipboard content (images, files) on Wayland via `wl-paste`, and with text content on X11 via `xclip`. You can also enable it from the command line with `--restore-clipboard` or the `VOXTYPE_RESTORE_CLIPBOARD=true` environment variable.
1365+
13561366
### Fallback Behavior
13571367

13581368
Voxtype uses a fallback chain: wtype → eitype → dotool → ydotool → clipboard (wl-copy) → xclip

src/cli.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ pub struct Cli {
5353
#[arg(long)]
5454
pub paste: bool,
5555

56+
/// Restore clipboard content after paste mode completes
57+
/// Saves clipboard before transcription and restores it after paste
58+
#[arg(long)]
59+
pub restore_clipboard: bool,
60+
61+
/// Delay in milliseconds after paste before restoring clipboard (default: 200)
62+
#[arg(long, value_name = "MS")]
63+
pub restore_clipboard_delay_ms: Option<u32>,
64+
5665
/// Override model for transcription.
5766
/// Whisper: tiny, base, small, medium, large-v3, large-v3-turbo (and .en variants).
5867
/// Parakeet: parakeet-tdt-0.6b-v3, parakeet-tdt-0.6b-v3-int8

src/config.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ type_delay_ms = 0
181181
# Useful for applications where Enter submits (e.g., Cursor IDE, Slack, Discord)
182182
# shift_enter_newlines = false
183183
184+
# Restore clipboard content after paste mode (default: false)
185+
# Saves clipboard before transcription, restores it after paste keystroke
186+
# Only applies to mode = "paste". Useful when you want to preserve your
187+
# existing clipboard content across dictation operations.
188+
# restore_clipboard = false
189+
190+
# Delay after paste before restoring clipboard (milliseconds)
191+
# Allows time for the paste operation to complete (default: 200)
192+
# restore_clipboard_delay_ms = 200
193+
184194
# Pre/post output hooks (optional)
185195
# Commands to run before and after typing output. Useful for compositor integration.
186196
# Example: Block modifier keys during typing with Hyprland submap:
@@ -1487,6 +1497,10 @@ fn default_post_process_timeout() -> u64 {
14871497
30000 // 30 seconds - generous for LLM processing
14881498
}
14891499

1500+
fn default_restore_clipboard_delay() -> u32 {
1501+
200 // 200ms - delay for paste to complete before restoring clipboard
1502+
}
1503+
14901504
/// Text output configuration
14911505
#[derive(Debug, Clone, Deserialize, Serialize)]
14921506
pub struct OutputConfig {
@@ -1580,6 +1594,16 @@ pub struct OutputConfig {
15801594
/// Applies to both config-based file output and --output-file CLI flag
15811595
#[serde(default)]
15821596
pub file_mode: FileMode,
1597+
1598+
/// Restore original clipboard content after paste mode completes
1599+
/// Saves clipboard before transcription, restores it after paste keystroke
1600+
#[serde(default)]
1601+
pub restore_clipboard: bool,
1602+
1603+
/// Delay after paste before restoring clipboard content (milliseconds)
1604+
/// Allows time for the paste operation to complete
1605+
#[serde(default = "default_restore_clipboard_delay")]
1606+
pub restore_clipboard_delay_ms: u32,
15831607
}
15841608

15851609
impl OutputConfig {
@@ -1746,6 +1770,8 @@ impl Default for Config {
17461770
dotool_xkb_variant: None,
17471771
file_path: None,
17481772
file_mode: FileMode::default(),
1773+
restore_clipboard: false,
1774+
restore_clipboard_delay_ms: default_restore_clipboard_delay(),
17491775
},
17501776
engine: TranscriptionEngine::default(),
17511777
parakeet: None,
@@ -1953,6 +1979,14 @@ pub fn load_config(path: Option<&Path>) -> Result<Config, VoxtypeError> {
19531979
if let Ok(append_text) = std::env::var("VOXTYPE_APPEND_TEXT") {
19541980
config.output.append_text = Some(append_text);
19551981
}
1982+
if let Ok(val) = std::env::var("VOXTYPE_RESTORE_CLIPBOARD") {
1983+
config.output.restore_clipboard = val == "1" || val.eq_ignore_ascii_case("true");
1984+
}
1985+
if let Ok(val) = std::env::var("VOXTYPE_RESTORE_CLIPBOARD_DELAY_MS") {
1986+
if let Ok(ms) = val.parse::<u32>() {
1987+
config.output.restore_clipboard_delay_ms = ms;
1988+
}
1989+
}
19561990

19571991
Ok(config)
19581992
}
@@ -3399,4 +3433,63 @@ mod tests {
33993433
assert_eq!(config.meeting.diarization.backend, "simple");
34003434
assert_eq!(config.meeting.summary.backend, "disabled");
34013435
}
3436+
3437+
// =========================================================================
3438+
// Clipboard Restore Tests
3439+
// =========================================================================
3440+
3441+
#[test]
3442+
fn test_restore_clipboard_defaults() {
3443+
let config = Config::default();
3444+
assert!(!config.output.restore_clipboard);
3445+
assert_eq!(config.output.restore_clipboard_delay_ms, 200);
3446+
}
3447+
3448+
#[test]
3449+
fn test_restore_clipboard_deserialization() {
3450+
let toml_str = r#"
3451+
[hotkey]
3452+
key = "SCROLLLOCK"
3453+
3454+
[audio]
3455+
device = "default"
3456+
sample_rate = 16000
3457+
max_duration_secs = 30
3458+
3459+
[whisper]
3460+
model = "base.en"
3461+
3462+
[output]
3463+
mode = "paste"
3464+
restore_clipboard = true
3465+
restore_clipboard_delay_ms = 500
3466+
"#;
3467+
3468+
let config: Config = toml::from_str(toml_str).unwrap();
3469+
assert!(config.output.restore_clipboard);
3470+
assert_eq!(config.output.restore_clipboard_delay_ms, 500);
3471+
}
3472+
3473+
#[test]
3474+
fn test_restore_clipboard_missing_uses_defaults() {
3475+
let toml_str = r#"
3476+
[hotkey]
3477+
key = "SCROLLLOCK"
3478+
3479+
[audio]
3480+
device = "default"
3481+
sample_rate = 16000
3482+
max_duration_secs = 30
3483+
3484+
[whisper]
3485+
model = "base.en"
3486+
3487+
[output]
3488+
mode = "paste"
3489+
"#;
3490+
3491+
let config: Config = toml::from_str(toml_str).unwrap();
3492+
assert!(!config.output.restore_clipboard);
3493+
assert_eq!(config.output.restore_clipboard_delay_ms, 200);
3494+
}
34023495
}

src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ pub enum OutputError {
130130
#[error("wl-copy not found in PATH. Install wl-clipboard via your package manager.")]
131131
WlCopyNotFound,
132132

133+
#[error("wl-paste not found in PATH. Install wl-clipboard via your package manager.")]
134+
WlPasteNotFound,
135+
133136
#[error("xclip not found in PATH. Install xclip via your package manager.")]
134137
XclipNotFound,
135138

src/main.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ async fn main() -> anyhow::Result<()> {
9999
if cli.paste {
100100
config.output.mode = config::OutputMode::Paste;
101101
}
102+
if cli.restore_clipboard {
103+
config.output.restore_clipboard = true;
104+
}
105+
if let Some(delay) = cli.restore_clipboard_delay_ms {
106+
config.output.restore_clipboard_delay_ms = delay;
107+
}
102108
let top_level_model = cli.model.clone();
103109
if let Some(model) = cli.model {
104110
if setup::model::is_valid_model(&model) {
@@ -1085,6 +1091,11 @@ async fn show_config(config: &config::Config) -> anyhow::Result<()> {
10851091
}
10861092
println!(" type_delay_ms = {}", config.output.type_delay_ms);
10871093
println!(" pre_type_delay_ms = {}", config.output.pre_type_delay_ms);
1094+
println!(" restore_clipboard = {}", config.output.restore_clipboard);
1095+
println!(
1096+
" restore_clipboard_delay_ms = {}",
1097+
config.output.restore_clipboard_delay_ms
1098+
);
10881099

10891100
println!("\n[output.notification]");
10901101
println!(

src/output/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ pub fn create_output_chain_with_override(
287287
config.paste_keys.clone(),
288288
config.type_delay_ms,
289289
pre_type_delay_ms,
290+
config.restore_clipboard,
291+
config.restore_clipboard_delay_ms,
290292
)));
291293
}
292294
crate::config::OutputMode::File => {

0 commit comments

Comments
 (0)