1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
//! Where a finished pipeline puts its output.
//!
//! [`StdoutEmitter`] has no dependencies and is always available.
//! [`ClipboardEmitter`] needs `arboard`, so it sits behind the
//! `clipboard` feature.
use async_trait::async_trait;
use crate::traits::OutputEmitter;
use crate::types::{EmitResult, RefinementOutput};
#[cfg(feature = "clipboard")]
mod clipboard {
use async_trait::async_trait;
use crate::traits::OutputEmitter;
use crate::types::{EmitResult, RefinementOutput};
// ---------------------------------------------------------------------------
// ClipboardEmitter — paste via system clipboard
// ---------------------------------------------------------------------------
/// Emits text by writing to the system clipboard.
///
/// On supported platforms, this performs:
/// 1. Save current clipboard contents
/// 2. Write refined text to clipboard
/// 3. (Caller is responsible for triggering Cmd+V / Ctrl+V if needed)
/// 4. Restore previous clipboard contents (via `undo`)
///
/// Uses the `arboard` crate for cross-platform clipboard access
/// (macOS, Windows, Linux/X11/Wayland).
pub struct ClipboardEmitter {
/// If true, save and restore the previous clipboard contents on emit/undo.
preserve_clipboard: bool,
/// Stashed clipboard content for restoration.
previous: std::sync::Arc<tokio::sync::Mutex<Option<String>>>,
}
impl ClipboardEmitter {
pub fn new() -> Self {
Self {
preserve_clipboard: true,
previous: std::sync::Arc::new(tokio::sync::Mutex::new(None)),
}
}
/// Create an emitter that does NOT preserve the previous clipboard contents.
pub fn without_preservation() -> Self {
Self {
preserve_clipboard: false,
previous: std::sync::Arc::new(tokio::sync::Mutex::new(None)),
}
}
}
impl Default for ClipboardEmitter {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl OutputEmitter for ClipboardEmitter {
async fn emit(&self, output: RefinementOutput) -> EmitResult {
let text = match &output {
RefinementOutput::TextInsertion { text, .. } => text.clone(),
RefinementOutput::Command { action, .. } => action.clone(),
RefinementOutput::StructuredInput { text, .. } => text.clone().unwrap_or_default(),
};
// Run clipboard operations on a blocking thread (arboard is sync)
let preserve = self.preserve_clipboard;
let previous = self.previous.clone();
let result = tokio::task::spawn_blocking(move || {
let mut clipboard = match arboard::Clipboard::new() {
Ok(c) => c,
Err(e) => return EmitResult::fail(format!("clipboard init: {e}")),
};
// Save previous contents
if preserve {
let prev = clipboard.get_text().ok();
// Store for later undo — we do this synchronously since we're
// already in a blocking context, but need to use the Arc<Mutex>
// We'll use try_lock since we're in a sync context
if let Ok(mut guard) = previous.try_lock() {
*guard = prev;
}
}
// Write new text
match clipboard.set_text(&text) {
Ok(()) => {
tracing::info!(len = text.len(), "text written to clipboard");
EmitResult::ok()
}
Err(e) => EmitResult::fail(format!("clipboard write: {e}")),
}
})
.await;
match result {
Ok(r) => r,
Err(e) => EmitResult::fail(format!("clipboard task: {e}")),
}
}
async fn undo(&self) -> EmitResult {
if !self.preserve_clipboard {
return EmitResult::fail("clipboard preservation disabled");
}
let previous = self.previous.clone();
let result = tokio::task::spawn_blocking(move || {
let prev = match previous.try_lock() {
Ok(guard) => guard.clone(),
Err(_) => return EmitResult::fail("could not access previous clipboard"),
};
match prev {
Some(text) => {
let mut clipboard = match arboard::Clipboard::new() {
Ok(c) => c,
Err(e) => return EmitResult::fail(format!("clipboard init: {e}")),
};
match clipboard.set_text(&text) {
Ok(()) => EmitResult::ok(),
Err(e) => EmitResult::fail(format!("clipboard restore: {e}")),
}
}
None => {
// Nothing to restore — previous clipboard was empty
EmitResult::ok()
}
}
})
.await;
match result {
Ok(r) => r,
Err(e) => EmitResult::fail(format!("clipboard undo task: {e}")),
}
}
}
}
#[cfg(feature = "clipboard")]
pub use clipboard::ClipboardEmitter;
// ---------------------------------------------------------------------------
// StdoutEmitter — print to stdout
// ---------------------------------------------------------------------------
/// Writes the refined text to stdout, one line per emission.
///
/// No dependencies, so this is available in every build. It used to live
/// in `mock`, which meant reaching for a test module to do something
/// entirely ordinary.
pub struct StdoutEmitter;
#[async_trait]
impl OutputEmitter for StdoutEmitter {
async fn emit(&self, output: RefinementOutput) -> EmitResult {
match &output {
RefinementOutput::TextInsertion { text, .. } => {
println!("{text}");
EmitResult::ok()
}
other => EmitResult::failed(crate::types::EmitError::Unsupported(format!(
"StdoutEmitter writes text; got {other:?}"
))),
}
}
async fn undo(&self) -> EmitResult {
EmitResult::failed(crate::types::EmitError::Unsupported(
"stdout cannot be un-printed".into(),
))
}
}