pub enum ClipboardRead {
Smart,
Text,
}Expand description
Scope of one background clipboard read.
Variants§
Smart
Smart content: image, then copied file paths, then text
(read_clipboard).
Text
Plain text only (read_clipboard_text) — the Ctrl+Shift+V
verbatim-insert contract, no image or file-URL interpretation.
Implementations§
Source§impl ClipboardRead
impl ClipboardRead
Sourcepub const fn for_key(key: Key) -> Option<Self>
pub const fn for_key(key: Key) -> Option<Self>
The read a paste chord requests: Key::Paste smart,
Key::PasteRaw text-only, anything else none.
Examples found in repository?
examples/chat/main.rs (line 209)
90async fn chat<'a>(
91 terminal: &'a mut Terminal,
92 renderer: &'a mut Renderer<TtyOut>,
93 ctx: &'a UiContext,
94) -> io::Result<()> {
95 let mut viewport = terminal.size()?;
96 if !run_welcome(terminal, renderer, ctx.charset, &mut viewport).await? {
97 return Ok(());
98 }
99 // The welcome scene held the alternate screen; releasing it restores the
100 // untouched shell and the chat pushes inline from a clean slate.
101 terminal.leave_alt()?;
102
103 let mut demo = Demo::new(ctx);
104 let mut overlay: Option<Overlay> = None;
105 let mut current_model = 0_usize;
106 let started = Instant::now();
107 let mut sidebar = Sidebar::new(MODELS[current_model].name, ctx);
108 demo.set_right_inset(sidebar.reserved(viewport));
109 {
110 let rendered = demo.render(viewport);
111 let layers = rail_layers(&mut sidebar, viewport, started.elapsed());
112 present(renderer, rendered, viewport, &layers)?;
113 }
114
115 // Alternate-screen ownership for the chat scene: a resize gesture borrows
116 // it for throwaway drag frames, an open overlay holds it for its lifetime.
117 let mut drag_alt = false;
118 let mut overlay_stale = false;
119 let mut resize = None;
120 // At most one in-flight background clipboard read (Ctrl+V/Ctrl+Shift+V).
121 let mut paste_read: Option<PasteRead> = None;
122 let mut next_frame = Instant::now() + FRAME_INTERVAL;
123 loop {
124 let paste_deadline = paste_read.as_ref().map(|read| read.abandon_at);
125 tokio::select! {
126 // The terminal branch pauses while a clipboard read is in flight:
127 // the event mailbox buffers input in order, so an Enter typed
128 // right after Ctrl+V lands *after* the paste instead of
129 // submitting an empty prompt. The read below is bounded, so the
130 // pause is too; retained App hosts get the finer-grained
131 // per-event queue instead.
132 event = terminal.next(), if paste_read.is_none() => match event? {
133 TerminalEvent::Resize => {
134 let now = Instant::now();
135 let resized = observe_resize(terminal, &mut viewport, &mut resize, now)?;
136 demo.set_right_inset(sidebar.reserved(viewport));
137 if overlay.is_some() && resized {
138 overlay_stale = true;
139 }
140 },
141 TerminalEvent::Debug(_) => {},
142 TerminalEvent::Closed => return Ok(()),
143 TerminalEvent::Input(event) => {
144 let Some(event) = user_event(terminal, renderer, event)? else {
145 continue;
146 };
147 match event {
148 InputEvent::Key(key) => {
149 if overlay.is_some() {
150 if key == Key::Ctrl('c') {
151 break;
152 }
153 let event = overlay
154 .as_mut()
155 .expect("overlay checked above")
156 .handle_key(key);
157 if apply_overlay_event(
158 event,
159 &mut overlay,
160 &mut current_model,
161 terminal,
162 renderer,
163 &mut demo,
164 &mut sidebar,
165 viewport,
166 started.elapsed(),
167 &mut overlay_stale,
168 &mut resize,
169 ctx,
170 )? {
171 break;
172 }
173 } else if key == Key::Ctrl('b') {
174 sidebar.toggle();
175 demo.set_right_inset(sidebar.reserved(viewport));
176 } else if key == Key::Ctrl('k') {
177 overlay = Some(Overlay::Palette(CommandPalette::open(ctx)));
178 open_overlay(
179 terminal,
180 renderer,
181 &mut demo,
182 overlay.as_mut().expect("palette just opened"),
183 &mut sidebar,
184 viewport,
185 started.elapsed(),
186 &mut drag_alt,
187 &mut overlay_stale,
188 &mut resize,
189 )?;
190 } else if sidebar.focused() {
191 if key == Key::Ctrl('c') {
192 break;
193 }
194 sidebar.handle_key(key);
195 } else if key == Key::Ctrl('p') || key == Key::Alt('p') {
196 overlay = Some(Overlay::Picker(ModelPicker::open(current_model, ctx)));
197 open_overlay(
198 terminal,
199 renderer,
200 &mut demo,
201 overlay.as_mut().expect("picker just opened"),
202 &mut sidebar,
203 viewport,
204 started.elapsed(),
205 &mut drag_alt,
206 &mut overlay_stale,
207 &mut resize,
208 )?;
209 } else if let Some(scope) = ClipboardRead::for_key(key) {
210 // The terminal did not claim the chord; read the
211 // system clipboard off-thread, preferring images
212 // unless the raw spelling asked for text only. A
213 // failed spawn closes the channel, so the receive
214 // branch below recovers input immediately.
215 paste_read = Some(PasteRead::start(scope));
216 } else {
217 let quit = demo.handle_key(key);
218 if demo.take_switch_request() {
219 overlay = Some(Overlay::Picker(ModelPicker::open(current_model, ctx)));
220 open_overlay(
221 terminal,
222 renderer,
223 &mut demo,
224 overlay.as_mut().expect("picker just opened"),
225 &mut sidebar,
226 viewport,
227 started.elapsed(),
228 &mut drag_alt,
229 &mut overlay_stale,
230 &mut resize,
231 )?;
232 }
233 if quit {
234 break;
235 }
236 }
237 next_frame = Instant::now();
238 },
239 InputEvent::Paste(text) => {
240 let event = overlay.as_mut().map(|active| active.handle_paste(&text));
241 match event {
242 Some(event) => {
243 if apply_overlay_event(
244 event,
245 &mut overlay,
246 &mut current_model,
247 terminal,
248 renderer,
249 &mut demo,
250 &mut sidebar,
251 viewport,
252 started.elapsed(),
253 &mut overlay_stale,
254 &mut resize,
255 ctx,
256 )? {
257 break;
258 }
259 },
260 None if sidebar.focused() => {},
261 None => demo.handle_paste(&text),
262 }
263 next_frame = Instant::now();
264 },
265 InputEvent::Mouse(report) => {
266 // An open overlay owns pointer input — hover, wheel,
267 // and clicks route through the compositor's band,
268 // never the occluded editor beneath.
269 let event = overlay
270 .as_mut()
271 .map(|active| active.handle_mouse(report.col, report.row, report.kind, viewport));
272 match event {
273 Some(event) => {
274 if apply_overlay_event(
275 event,
276 &mut overlay,
277 &mut current_model,
278 terminal,
279 renderer,
280 &mut demo,
281 &mut sidebar,
282 viewport,
283 started.elapsed(),
284 &mut overlay_stale,
285 &mut resize,
286 ctx,
287 )? {
288 break;
289 }
290 },
291 None => {
292 if !sidebar.handle_mouse(report.col, report.row, report.kind, viewport)
293 {
294 demo.handle_mouse(&report);
295 }
296 },
297 }
298 next_frame = Instant::now();
299 },
300 InputEvent::Focus(_) | InputEvent::Response(_) => {},
301 }
302 },
303 },
304 clipboard = async { (&mut paste_read.as_mut().expect("branch gated on Some").clipboard).await },
305 if paste_read.is_some() =>
306 {
307 let read = paste_read.take().expect("branch gated on Some");
308 // A closed channel (the reader thread never spawned) reads
309 // as an empty clipboard.
310 if let Ok(Some(clipboard)) = clipboard
311 && let Some(text) = clipboard_paste_text(clipboard)
312 && overlay.is_none()
313 && !sidebar.focused()
314 {
315 // Ctrl+Shift+V inserts verbatim: no attachment staging,
316 // no large-paste collapse.
317 match read.scope {
318 ClipboardRead::Text => demo.handle_paste_raw(&text),
319 ClipboardRead::Smart => demo.handle_paste(&text),
320 }
321 next_frame = Instant::now();
322 }
323 },
324 // The deadline is absolute, so the frame tick recreating this
325 // branch's future cannot reset it: a hung reader is abandoned and
326 // terminal input re-enables. Dropping the receiver makes the
327 // reader's eventual send fail; the detached thread dies with the
328 // process instead of stalling shutdown.
329 () = deadline(paste_deadline) => {
330 paste_read = None;
331 },
332 () = deadline(Some(next_frame)) => {
333 let now = Instant::now();
334 let resized = observe_resize(terminal, &mut viewport, &mut resize, now)?;
335 demo.set_right_inset(sidebar.reserved(viewport));
336 if overlay.is_some() && resized {
337 overlay_stale = true;
338 }
339 if resize.is_some() {
340 // Drag frames compose exactly one viewport tail at the
341 // new geometry — O(viewport) per frame; the O(history)
342 // transcript reflow waits for the settle rebuild (or the
343 // overlay close). Without an overlay, a width change
344 // borrows the alternate screen (the inline transcript
345 // rewraps underneath) while height-only churn repaints
346 // in place — alt toggling on a height echo can
347 // self-sustain.
348 let preview = demo.render_resize_preview(viewport);
349 if let Some(active) = overlay.as_mut() {
350 let mut layers = rail_layers(&mut sidebar, viewport, started.elapsed());
351 layers.push(active.layer(viewport));
352 renderer.preview_overlaid(&preview, &layers, viewport.height, "")?;
353 } else {
354 let width_changed =
355 resize.is_some_and(|state| state.width_changed);
356 let alt_enter = if drag_alt || !width_changed {
357 None
358 } else {
359 let staged = terminal.stage_alt_enter(AltScreenUse::Resize);
360 drag_alt = staged.is_some();
361 staged
362 };
363 let layers = rail_layers(&mut sidebar, viewport, started.elapsed());
364 renderer.preview_overlaid(
365 &preview,
366 &layers,
367 viewport.height,
368 alt_enter.as_deref().unwrap_or(""),
369 )?;
370 }
371 } else if let Some(active) = overlay.as_mut() {
372 let rendered = demo.render(viewport);
373 let mut layers = rail_layers(&mut sidebar, viewport, started.elapsed());
374 layers.push(active.layer(viewport));
375 renderer.preview_overlaid(rendered.frame, &layers, viewport.height, "")?;
376 } else {
377 let rendered = demo.render(viewport);
378 let layers = rail_layers(&mut sidebar, viewport, started.elapsed());
379 present(renderer, rendered, viewport, &layers)?;
380 }
381 next_frame = now + FRAME_INTERVAL;
382 },
383 () = deadline(resize.map(ResizeState::deadline)) => {
384 let now = Instant::now();
385 if !resize.is_some_and(|state| state.settled(now)) {
386 continue;
387 }
388 if overlay.is_some() {
389 // The overlay keeps holding the alternate screen; the
390 // transcript reflows once at close.
391 overlay_stale = true;
392 resize = None;
393 continue;
394 }
395 demo.set_right_inset(sidebar.reserved(viewport));
396 let rendered = demo.render(viewport);
397 let alt_exit = if drag_alt {
398 drag_alt = false;
399 terminal.stage_alt_leave().unwrap_or("")
400 } else {
401 ""
402 };
403 renderer.rebuild(
404 rendered.frame.clone(),
405 viewport.height,
406 rendered.stable_rows,
407 alt_exit,
408 )?;
409 // The rebuild repainted the raw document; recomposite the
410 // rail on top without touching the fresh history.
411 let layers = rail_layers(&mut sidebar, viewport, started.elapsed());
412 if !layers.is_empty() {
413 renderer.present_overlaid(
414 rendered.frame,
415 &[],
416 viewport.height,
417 rendered.stable_rows,
418 &layers,
419 )?;
420 }
421 resize = None;
422 next_frame = now + FRAME_INTERVAL;
423 },
424 }
425 }
426 Ok(())
427}Trait Implementations§
Source§impl Clone for ClipboardRead
impl Clone for ClipboardRead
Source§fn clone(&self) -> ClipboardRead
fn clone(&self) -> ClipboardRead
Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
Performs copy-assignment from
source. Read moreimpl Copy for ClipboardRead
Source§impl Debug for ClipboardRead
impl Debug for ClipboardRead
impl Eq for ClipboardRead
Source§impl PartialEq for ClipboardRead
impl PartialEq for ClipboardRead
impl StructuralPartialEq for ClipboardRead
Auto Trait Implementations§
impl Freeze for ClipboardRead
impl RefUnwindSafe for ClipboardRead
impl Send for ClipboardRead
impl Sync for ClipboardRead
impl Unpin for ClipboardRead
impl UnsafeUnpin for ClipboardRead
impl UnwindSafe for ClipboardRead
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Convert
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Convert
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
Convert
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
Convert
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
Compare self to
key and return true if they are equal.Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
Converts
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
Converts
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more