Skip to main content

hyprcorrect_platform/linux/
clipboard.rs

1//! Clipboard / selection fallback (M5).
2//!
3//! When the focused window's keystroke buffer is empty — typically
4//! because focus moved and the user wants to fix something they
5//! *didn't just type* — we simulate Ctrl+Shift+Left to select the
6//! previous word, Ctrl+C to copy it, read the clipboard via
7//! `wl-paste`, run the offline corrector over it, and then type
8//! the correction. The selection is still active when we type, so
9//! the replacement overwrites it in place.
10//!
11//! Best-effort: doesn't work in terminals (no select-previous-word
12//! shortcut) or in apps that interpret Ctrl+Shift+Left differently.
13//! Per `DESIGN.md`'s secondary-mode notes.
14
15use std::process::Command;
16use std::thread::sleep;
17use std::time::Duration;
18
19/// Errors from a clipboard-fallback round trip.
20#[derive(Debug, thiserror::Error)]
21pub enum ClipboardError {
22    /// `wtype` or `wl-paste` could not be spawned (typically not
23    /// installed). The daemon falls back silently — no fix happens,
24    /// but the user can install `wl-clipboard` to enable it.
25    #[error("could not run {0}: {1}")]
26    Spawn(String, String),
27    /// The helper exited non-zero — usually means the protocol or
28    /// the compositor refused the request.
29    #[error("{0} exited non-zero: {1}")]
30    Exit(String, String),
31    /// `wl-paste` returned empty — the select-previous-word
32    /// keystroke didn't land (terminal, no-text-field, …).
33    #[error("clipboard was empty after the copy step — selection likely failed")]
34    Empty,
35    /// The clipboard contained non-UTF-8 bytes — we don't try to
36    /// correct image / PDF / etc. payloads.
37    #[error("clipboard contents were not valid UTF-8")]
38    NotUtf8,
39}
40
41/// Select the previous word, copy it, and return the contents.
42/// Leaves the selection active so a subsequent
43/// [`type_replacement`] call overwrites it.
44pub fn copy_previous_word() -> Result<String, ClipboardError> {
45    // Select previous word: Ctrl+Shift+Left (press, then release in
46    // the reverse order). wtype's `-M` is modifier-press, `-k` is
47    // keysym press+release, `-m` is modifier-release.
48    wtype(&[
49        "-M", "ctrl", "-M", "shift", "-k", "Left", "-m", "shift", "-m", "ctrl",
50    ])?;
51    sleep(Duration::from_millis(30));
52
53    // Copy: Ctrl+C.
54    wtype(&["-M", "ctrl", "-k", "c", "-m", "ctrl"])?;
55    sleep(Duration::from_millis(80)); // compositor + clipboard manager
56
57    let output = Command::new("wl-paste")
58        .arg("-n") // strip trailing newline
59        .output()
60        .map_err(|e| ClipboardError::Spawn("wl-paste".into(), e.to_string()))?;
61    if !output.status.success() {
62        return Err(ClipboardError::Exit(
63            "wl-paste".into(),
64            String::from_utf8_lossy(&output.stderr).into_owned(),
65        ));
66    }
67    let text = std::str::from_utf8(&output.stdout)
68        .map_err(|_| ClipboardError::NotUtf8)?
69        .to_string();
70    if text.is_empty() {
71        return Err(ClipboardError::Empty);
72    }
73    Ok(text)
74}
75
76/// Type the replacement text. With a selection still active (from
77/// [`copy_previous_word`]), this overwrites the selection in
78/// place. Safe to call standalone too.
79pub fn type_replacement(text: &str) -> Result<(), ClipboardError> {
80    // `--` ends wtype's option parsing so a replacement that starts
81    // with `-` won't be parsed as a flag.
82    let output = Command::new("wtype")
83        .arg("--")
84        .arg(text)
85        .output()
86        .map_err(|e| ClipboardError::Spawn("wtype".into(), e.to_string()))?;
87    if !output.status.success() {
88        return Err(ClipboardError::Exit(
89            "wtype".into(),
90            String::from_utf8_lossy(&output.stderr).into_owned(),
91        ));
92    }
93    Ok(())
94}
95
96fn wtype(args: &[&str]) -> Result<(), ClipboardError> {
97    let output = Command::new("wtype")
98        .args(args)
99        .output()
100        .map_err(|e| ClipboardError::Spawn("wtype".into(), e.to_string()))?;
101    if !output.status.success() {
102        return Err(ClipboardError::Exit(
103            "wtype".into(),
104            String::from_utf8_lossy(&output.stderr).into_owned(),
105        ));
106    }
107    Ok(())
108}