Skip to main content

ClipboardRead

Enum ClipboardRead 

Source
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

Source

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

Source§

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)

Performs copy-assignment from source. Read more
Source§

impl Copy for ClipboardRead

Source§

impl Debug for ClipboardRead

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Eq for ClipboardRead

Source§

impl PartialEq for ClipboardRead

Source§

fn eq(&self, other: &ClipboardRead) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for ClipboardRead

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

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>

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)

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)

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
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

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
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

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
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.