uncurses/screen/mod.rs
1//! [`Screen`] — a self-managing terminal application facade.
2//!
3//! `Screen<I, O>` bundles the three primitives a full-screen terminal
4//! program needs into one owned handle:
5//!
6//! - a [`Terminal`] for the raw-mode lifecycle,
7//! - a cell-diff renderer, and
8//! - an [`EventSource`] for decoded input (read synchronously).
9//!
10//! It additionally owns the non-render terminal/input modes (mouse,
11//! bracketed paste, focus reporting, in-band resize, the default
12//! foreground/background/cursor colors, the window title, the cursor
13//! style, and color-scheme update reports) and tracks them so they can be
14//! torn down on a shell handoff and re-applied afterwards.
15//!
16//! Construction is inert: [`Screen::new`] (and the [`stdio`](Screen::stdio)
17//! / [`open`](Screen::open) shortcuts) only build the screen. Begin a
18//! session with [`Screen::init`], which enters raw mode and sends the
19//! capability queries. Teardown is explicit: there is **no** `Drop`.
20//! Hand the terminal back to the shell with [`Screen::finish`] (consume),
21//! [`Screen::pause`] (keep, e.g. to shell out), or [`Screen::suspend`]
22//! (pause, then stop the process with `SIGTSTP`); resume a
23//! paused/suspended screen with [`Screen::resume`].
24//!
25//! ```no_run
26//! use uncurses::screen::Screen;
27//! use uncurses::style::Style;
28//! use uncurses::text::TextSurface;
29//!
30//! # fn main() -> std::io::Result<()> {
31//! let mut screen = Screen::open()?; // build over /dev/tty
32//! screen.init()?; // raw mode + capability queries
33//! screen.enter_alt_screen()?;
34//! screen.set_str((0, 0), "hello", Style::default());
35//! screen.render()?;
36//! let event = screen.read_event()?;
37//! screen.observe_event(&event)?; // keep capability tracking alive
38//! screen.finish()?; // restore the terminal
39//! # Ok(())
40//! # }
41//! ```
42//!
43//! # Inline and fullscreen
44//!
45//! With the alternate screen on (after [`enter_alt_screen`](Screen::enter_alt_screen))
46//! the managed area is the whole terminal viewport, addressed with absolute
47//! moves. Without it (the default) the screen is *inline*: it occupies the
48//! full terminal width but only as many rows as you draw, anchored in the
49//! normal buffer so scrollback above and the returning shell prompt below
50//! stay intact. Set the inline height with [`resize`](Screen::resize), and
51//! push lines into the scrollback above the surface with
52//! [`insert_above`](Screen::insert_above). Call
53//! [`autoresize`](Screen::autoresize) to refit to the current window.
54//!
55//! ```text
56//! Inline (default): the surface lives in the normal buffer, only as
57//! many rows as you draw; scrollback and the shell prompt stay intact.
58//!
59//! $ earlier shell output
60//! $ ... scrollback ...
61//! ┌─────────────────────────┐
62//! │ managed surface │ <- only the rows you draw, full width
63//! └─────────────────────────┘
64//! $ shell prompt resumes
65//!
66//! Fullscreen (after enter_alt_screen): the whole viewport is the
67//! surface, addressed with absolute moves, and restored on exit.
68//!
69//! ┌─────────────────────────────┐
70//! │ │
71//! │ the whole terminal │
72//! │ viewport is the surface │
73//! │ │
74//! └─────────────────────────────┘
75//! ```
76//!
77//! # Options and defaults
78//!
79//! [`init`](Screen::init) uses [`ScreenOptions::default`];
80//! [`init_with`](Screen::init_with) takes an explicit [`ScreenOptions`] to
81//! choose the desired keyboard enhancements, whether to enable mouse
82//! tracking at startup, and the in-band-resize and pixel-size behaviors.
83//! Always-on defaults (such as bracketed paste) take effect immediately;
84//! discovery-driven defaults are applied once the terminal answers the
85//! capability queries (see [`capabilities`](Screen::capabilities)).
86//!
87//! [`Terminal`]: crate::terminal::Terminal
88//! [`EventSource`]: crate::event::EventSource
89
90mod cursor;
91mod modes;
92mod state;
93#[cfg(test)]
94mod tests;
95
96pub use cursor::CursorShape;
97pub use state::Capabilities;
98
99/// Cell-diff capability flags controlling which optimized escape
100/// sequences the screen's renderer may emit. Re-exported from the
101/// renderer so applications can configure rendering with
102/// [`Screen::set_optimizations`] without depending on renderer internals.
103pub use crate::renderer::Optimizations;
104
105use std::io::{self, Write};
106use std::sync::{Arc, Mutex};
107use std::time::{Duration, Instant};
108
109use bitflags::bitflags;
110
111use crate::ansi::{kitty, mode};
112use crate::buffer::{Bounded, Surface, SurfaceMut};
113use crate::cell::Cell;
114use crate::color::Profile;
115use crate::event::Input;
116use crate::event::{Event, EventSource};
117use crate::layout::{Position, Rect, Size};
118use crate::renderer::{RenderBuffer, Renderer};
119use crate::terminal::Terminal;
120use crate::text::{TextSurface, WidthMode};
121
122/// A self-managing terminal application facade composing a [`Terminal`],
123/// a cell-diff renderer, and an [`EventSource`] with the non-render terminal and
124/// input modes. See the [module documentation](self) for the lifecycle.
125///
126/// `Screen` is [`Send`] and [`Sync`] whenever its input and output handles are,
127/// so it can be moved onto another thread or held across an `.await` point in a
128/// multi-threaded async runtime.
129///
130/// [`Terminal`]: crate::terminal::Terminal
131/// [`EventSource`]: crate::event::EventSource
132pub struct Screen<I, O>
133where
134 I: Input,
135 O: Write,
136{
137 terminal: Terminal<I, O>,
138 /// Caller-facing desired cell grid. Touched spans record where the
139 /// application wrote since the last sync; the renderer filters them
140 /// again against its staging buffer before diffing the terminal.
141 front_buf: RenderBuffer,
142 /// The diff renderer holding the tracked on-screen buffer, cursor model,
143 /// fullscreen/relative-cursor layout, color profile, and optimizations.
144 renderer: Renderer,
145 /// Scratch byte buffer that drawing and mode methods stage escape bytes
146 /// into before [`io::Write::flush`] drains them through the terminal.
147 out_buf: Vec<u8>,
148 /// Managed area width in cells.
149 width: u16,
150 /// Managed area height in cells.
151 height: u16,
152 /// East-Asian Ambiguous width policy used when measuring strings: when
153 /// `true`, code points whose East-Asian-Width property is `Ambiguous`
154 /// are measured as 2 cells instead of 1. See [`crate::text::char_width`].
155 eaw_wide: bool,
156 /// Input source behind the synchronous read path ([`Self::read_event`]
157 /// and friends). Held in an `Arc<Mutex<_>>`; the lock is uncontended in
158 /// the common single-reader case.
159 source: Arc<Mutex<EventSource<I>>>,
160 state: state::State,
161 /// Terminal capabilities detected by intercepting the replies to the
162 /// queries [`Self::init`] fires. Reads are pure; capability-report events
163 /// are recorded here only when the caller feeds them back through
164 /// [`Self::observe_event`].
165 caps: Capabilities,
166 /// Desired default behaviors, set by [`Self::init_with`].
167 options: ScreenOptions,
168 /// Set once the discovery-dependent defaults have been applied (on the
169 /// terminating Primary DA reply), so they are applied at most once.
170 defaults_applied: bool,
171 /// Last observed full terminal size in cells, from resize and
172 /// `WindowCellSize` reports. `None` until first observed.
173 window_cells: Option<Size>,
174 /// Last observed full terminal size in pixels, from resize (when it
175 /// carries pixel dimensions) and `WindowPixelSize` reports. `None`
176 /// until first observed.
177 window_pixels: Option<Size>,
178 /// The raw XTVERSION reply identifying the terminal (e.g.
179 /// `"XTerm(380)"`). `None` until the reply is observed.
180 terminal_name: Option<String>,
181 /// When the [`init`](Self::init) capability queries were written, used
182 /// to bound the teardown drain that consumes their replies. `None`
183 /// when no queries were sent (or once the drain has run).
184 queries_sent_at: Option<Instant>,
185}
186
187/// Desired default behaviors applied by [`Screen::init_with`].
188///
189/// Always-on defaults (e.g. [`bracketed_paste`](Self::bracketed_paste))
190/// take effect at init regardless of capability detection. Discovery-driven
191/// defaults are applied once the terminating Primary DA reply confirms the
192/// detected [`Capabilities`]; if the terminal never answers, only the
193/// always-on defaults are in effect.
194///
195/// Most fields are app-level toggles you set to taste. Two are low-level
196/// transport knobs that most applications should leave at their defaults:
197/// [`request_pixel_size_on_resize`](Self::request_pixel_size_on_resize)
198/// (when to re-query the window pixel size) and
199/// [`query_drain_timeout`](Self::query_drain_timeout) (how long teardown
200/// waits for capability replies). They exist for unusual terminals and
201/// latency-sensitive teardown, not everyday configuration.
202#[derive(Debug, Clone)]
203pub struct ScreenOptions {
204 /// Enable bracketed paste at init. Defaults to `true`.
205 pub bracketed_paste: bool,
206 /// Desired Kitty keyboard enhancements. When non-empty, the screen
207 /// enables as many as the terminal supports, preferring the Kitty
208 /// protocol and falling back to xterm modifyOtherKeys when Kitty is
209 /// unavailable. Defaults to
210 /// [`KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES`].
211 ///
212 /// [`KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES`]: crate::ansi::kitty::KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES
213 pub keyboard_enhancements: crate::ansi::kitty::KittyKeyboardFlags,
214 /// Prefer in-band resize reports over the `SIGWINCH` path when the
215 /// terminal supports them. Defaults to `true`.
216 pub prefer_in_band_resize: bool,
217 /// Request the window pixel size (XTWINOPS `CSI 14 t`) whenever a resize
218 /// is observed that does not itself carry pixel dimensions, keeping
219 /// [`window_pixels`](Screen::window_pixels) current on platforms that
220 /// report cell sizes only. Skipped while in-band resize is active, since
221 /// those reports already carry pixel dimensions. Defaults to `true` on
222 /// Windows (whose console resize events carry no pixel size) and `false`
223 /// elsewhere, where resize reports already include pixel dimensions.
224 pub request_pixel_size_on_resize: bool,
225 /// Enable mouse tracking at init with the given [`MouseTracking`] extras
226 /// (see [`Screen::enable_mouse`]). The request is emitted unconditionally;
227 /// terminals ignore modes they do not support and degrade gracefully.
228 /// Defaults to `None` (mouse tracking off).
229 pub mouse: Option<MouseTracking>,
230 /// Send terminal capability queries during
231 /// [`init`](Screen::init_with).
232 ///
233 /// When `true` (the default), [`init_with`](Screen::init_with) probes the
234 /// terminal for its keyboard, color, and feature support and waits for the
235 /// replies, populating [`capabilities`](Screen::capabilities). Set it to
236 /// `false` for output-only programs that draw frames and never read input:
237 /// `init_with` still enters raw mode, sizes the managed area, and applies the
238 /// environment-detected color profile, but emits no query escapes and
239 /// waits for no replies, so the terminal is never probed and
240 /// [`capabilities`](Screen::capabilities) stays at its env-derived
241 /// defaults.
242 pub query_capabilities: bool,
243 /// How long teardown ([`finish`](Screen::finish) /
244 /// [`pause`](Screen::pause)) will wait for the capability-query replies
245 /// to arrive before restoring the terminal, so they cannot leak to the
246 /// shell (or a child after `pause`) as stray input.
247 ///
248 /// The wait is measured from when [`init`](Screen::init) sent the
249 /// queries and ends early as soon as the terminating Primary DA reply
250 /// lands, so a responsive terminal costs only its round-trip and a
251 /// long-running app pays nothing (the replies are consumed by the event
252 /// loop, and the budget has long since elapsed). Only the rare path that
253 /// tears down before the replies were consumed waits here. Defaults to
254 /// 300ms.
255 pub query_drain_timeout: Duration,
256}
257
258bitflags! {
259 /// Optional mouse tracking features layered on top of basic button
260 /// tracking.
261 ///
262 /// When mouse tracking is enabled, button-event tracking (presses,
263 /// releases, and drags) and SGR encoding are always requested; these flags
264 /// add optional extras on top. An empty set ([`MouseTracking::empty()`])
265 /// means basic tracking with no extras.
266 ///
267 /// Mouse tracking is turned *off* through [`Screen::disable_mouse`] or by
268 /// leaving [`ScreenOptions::mouse`] as `None`, not by an empty flag set.
269 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
270 pub struct MouseTracking: u8 {
271 /// Report pointer motion with no button held (any-event tracking).
272 /// Adds hover motion on terminals that support it.
273 const MOTION = 1 << 0;
274 /// Request pixel coordinates (SGR-pixel). Terminals that support it
275 /// report pixels; the rest fall back to SGR cell coordinates.
276 const PIXELS = 1 << 1;
277 }
278}
279
280impl Default for ScreenOptions {
281 fn default() -> Self {
282 Self {
283 bracketed_paste: true,
284 keyboard_enhancements:
285 crate::ansi::kitty::KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES,
286 prefer_in_band_resize: true,
287 request_pixel_size_on_resize: cfg!(windows),
288 mouse: None,
289 query_capabilities: true,
290 query_drain_timeout: Duration::from_millis(300),
291 }
292 }
293}
294
295impl<I, O> Screen<I, O>
296where
297 I: Input,
298 O: Write,
299{
300 // --- Drawing -------------------------------------------------------
301
302 /// Write `cell` at `pos` in the desired frame.
303 pub fn set_cell(&mut self, pos: impl Into<Position>, cell: &Cell) {
304 self.front_buf.set_cell(pos.into(), cell);
305 }
306
307 /// Borrow the cell at `pos` mutably, marking its columns touched.
308 pub fn cell_mut(&mut self, pos: impl Into<Position>) -> Option<&mut Cell> {
309 self.front_buf.cell_mut(pos.into())
310 }
311
312 /// Diff the staged frame against the tracked terminal, stage the
313 /// minimal escape bytes, and flush them through the terminal.
314 ///
315 /// When a declarative cursor rest position has been staged with
316 /// [`set_cursor_position`](Self::set_cursor_position), the cursor is moved
317 /// there at the end of the frame, inside the same hide/synchronized-output
318 /// bracket as the cell diff, so it lands atomically and without flicker.
319 pub fn render(&mut self) -> io::Result<()> {
320 let changed = self.renderer.sync_front(&mut self.front_buf);
321 if changed || self.cursor_move_pending() {
322 self.write_frame();
323 }
324 self.flush()
325 }
326
327 /// Force a full redraw on the next [`render`](Self::render).
328 pub fn invalidate(&mut self) {
329 self.renderer.request_clear();
330 }
331
332 /// Resize the managed area. In fullscreen pass the terminal viewport
333 /// size; inline, the terminal width and the application surface height.
334 pub fn resize(&mut self, size: impl Into<Size>) {
335 let size = size.into();
336 self.width = size.width;
337 self.height = size.height;
338 self.front_buf.resize(size.width, size.height);
339 self.renderer.request_clear();
340 }
341
342 /// Cache a fresh terminal size and request the pixel size over the wire
343 /// when the size carried none (e.g. the Windows console, whose
344 /// `get_window_size` reports no pixel dimensions), gated by
345 /// [`request_pixel_size_on_resize`](ScreenOptions::request_pixel_size_on_resize)
346 /// and the absence of in-band resize. The pixel reply arrives later as a
347 /// [`WindowPixelSize`](crate::event::Event::WindowPixelSize) event.
348 fn cache_window_size(&mut self, ws: crate::terminal::Winsize) -> io::Result<()> {
349 self.window_cells = Some(Size::new(ws.col, ws.row));
350 if ws.xpixel > 0 && ws.ypixel > 0 {
351 self.window_pixels = Some(Size::new(ws.xpixel, ws.ypixel));
352 } else if self.options.request_pixel_size_on_resize && !self.state.in_band_resize {
353 self.request_window_pixel_size()?;
354 }
355 Ok(())
356 }
357
358 /// Insert `content` into the scrollback above the managed area and flush
359 /// it to the terminal. In inline mode this pushes the lines into the
360 /// terminal's scrollback; in alternate-screen mode they go into the alt
361 /// screen's hidden scrollback. The managed area is preserved in place, so
362 /// no redraw is needed and a following [`render`](Self::render) sees no
363 /// change. An empty string is a no-op.
364 ///
365 /// # Errors
366 ///
367 /// Returns any error from flushing the inserted lines to the terminal.
368 pub fn insert_above(&mut self, content: &str) -> io::Result<()> {
369 if content.is_empty() {
370 return Ok(());
371 }
372
373 let width = self.width;
374 let height = self.height;
375 let y = self.renderer.cursor_position().y;
376
377 self.out_buf.write_all(b"\r").unwrap();
378 let down = height.saturating_sub(y).saturating_sub(1);
379 if down > 0 {
380 crate::ansi::cursor::write_cud(&mut self.out_buf, down).unwrap();
381 }
382
383 let lines: Vec<&str> = content.split('\n').collect();
384 let mut offset: u16 = lines.len() as u16;
385 let width_mode = self.width_mode();
386 for line in &lines {
387 let lw =
388 crate::ansi::text::string_width(line.as_bytes(), width_mode, self.eaw_wide) as u16;
389 if let Some(n) = lw.checked_div(width) {
390 offset = offset.saturating_add(n);
391 }
392 }
393
394 for _ in 0..offset {
395 self.out_buf.write_all(b"\n").unwrap();
396 }
397
398 let up = offset.saturating_add(height).saturating_sub(1);
399 if up > 0 {
400 crate::ansi::cursor::write_cuu(&mut self.out_buf, up).unwrap();
401 }
402 crate::ansi::screen::write_insert_lines(&mut self.out_buf, offset).unwrap();
403 for line in &lines {
404 self.out_buf.write_all(line.as_bytes()).unwrap();
405 self.out_buf
406 .write_all(crate::ansi::screen::ERASE_LINE_RIGHT)
407 .unwrap();
408 self.out_buf.write_all(b"\r\n").unwrap();
409 }
410
411 self.renderer.set_cursor_position(Position { y: 0, x: 0 });
412 self.flush()
413 }
414
415 /// The managed area size in cells.
416 pub fn size(&self) -> Size {
417 Size::new(self.width, self.height)
418 }
419
420 /// Immediately move the terminal cursor to a buffer-relative position and
421 /// flush. No-op when the renderer already reports the cursor there with
422 /// both axes known.
423 ///
424 /// This is imperative: the move is emitted and flushed now, independent of
425 /// [`render`](Self::render). It does **not** affect the declarative resting
426 /// position staged with [`set_cursor_position`](Self::set_cursor_position);
427 /// a subsequent `render` will snap the cursor back to that sticky position
428 /// if one is set. To change where frames leave the cursor, use
429 /// `set_cursor_position` instead.
430 pub fn move_cursor_to(&mut self, pos: impl Into<Position>) -> io::Result<()> {
431 let target = pos.into();
432 if self.renderer.cursor_known() && self.renderer.cursor_position() == target {
433 return Ok(());
434 }
435 self.renderer
436 .move_to(&mut self.out_buf, &self.front_buf, target.y, target.x)
437 .unwrap();
438 self.flush()
439 }
440
441 /// Immediately move the terminal cursor relative to the
442 /// [tracked cursor](Self::tracked_cursor) and flush.
443 ///
444 /// Convenience over [`move_cursor_to`](Self::move_cursor_to): the target
445 /// is the tracked cursor offset by `(dx, dy)`, saturating at the buffer
446 /// origin. Like `move_cursor_to`, it does not clamp to the right or
447 /// bottom edge. An unknown tracked cursor is treated as the origin.
448 pub fn move_cursor_by(&mut self, dx: i16, dy: i16) -> io::Result<()> {
449 let cur = self.tracked_cursor().unwrap_or(Position::ORIGIN);
450 let x = cur.x.saturating_add_signed(dx);
451 let y = cur.y.saturating_add_signed(dy);
452 self.move_cursor_to((x, y))
453 }
454
455 /// Stage a declarative resting position for the cursor, applied at the end
456 /// of every [`render`](Self::render).
457 ///
458 /// This is the cursor analogue of [`set_cell`](Self::set_cell): it stages
459 /// intent rather than emitting now. `render` leaves the terminal cursor at
460 /// the buffer-relative `pos` after each frame's cell diff. Call
461 /// [`clear_cursor_position`](Self::clear_cursor_position) to stop steering
462 /// it and leave the cursor wherever the diff ended.
463 ///
464 /// The position is **sticky** — it persists across frames and is re-applied
465 /// on every `render` (cheaply, as a no-op when the cursor is already there)
466 /// until you change or clear it. An app whose cursor follows content
467 /// (e.g. a text field) should call this each time that content moves.
468 ///
469 /// Cursor visibility is orthogonal: this never shows or hides the cursor.
470 /// Use [`show_cursor`](Self::show_cursor) / [`hide_cursor`](Self::hide_cursor)
471 /// for that. A position outside the managed area is clamped to its edges.
472 ///
473 /// The argument is anything that converts into a [`Position`], so a bare
474 /// `(x, y)` works:
475 ///
476 /// ```no_run
477 /// # fn main() -> std::io::Result<()> {
478 /// let mut screen = uncurses::screen::Screen::open()?;
479 /// screen.set_cursor_position((4, 0)); // stage
480 /// screen.clear_cursor_position(); // stop steering it
481 /// # Ok(())
482 /// # }
483 /// ```
484 pub fn set_cursor_position(&mut self, pos: impl Into<Position>) {
485 self.state.desired_cursor = Some(pos.into());
486 }
487
488 /// Clear the staged cursor [resting position](Self::set_cursor_position),
489 /// leaving the cursor wherever each frame's cell diff ends.
490 pub fn clear_cursor_position(&mut self) {
491 self.state.desired_cursor = None;
492 }
493
494 /// Clamp a buffer-relative position to the managed area's edges.
495 fn clamp_to_surface(&self, pos: Position) -> Position {
496 Position {
497 x: pos.x.min(self.width.saturating_sub(1)),
498 y: pos.y.min(self.height.saturating_sub(1)),
499 }
500 }
501
502 /// Whether a declarative cursor rest position is staged and the renderer's
503 /// tracked cursor isn't already there, so [`render`](Self::render) must
504 /// emit a move even when no cells changed.
505 fn cursor_move_pending(&self) -> bool {
506 match self.state.desired_cursor {
507 Some(pos) => {
508 let pos = self.clamp_to_surface(pos);
509 !self.renderer.cursor_known() || self.renderer.cursor_position() != pos
510 }
511 None => false,
512 }
513 }
514
515 /// The renderer's tracked cursor: the buffer-relative cell where the
516 /// renderer believes the terminal cursor currently sits, or `None` when
517 /// that position is unknown (initially, after a screen reset, or after
518 /// [`invalidate_tracked_cursor`](Self::invalidate_tracked_cursor)). This
519 /// is bookkeeping, not a live cursor-position query.
520 pub fn tracked_cursor(&self) -> Option<Position> {
521 self.renderer
522 .cursor_known()
523 .then(|| self.renderer.cursor_position())
524 }
525
526 /// Mark the tracked cursor position unknown, so the next staged move
527 /// always emits rather than short-circuiting on a matching tracked
528 /// position. Use after moving the terminal cursor by a means the
529 /// renderer cannot see (e.g. a raw escape written directly).
530 pub fn invalidate_tracked_cursor(&mut self) {
531 self.renderer.invalidate_cursor();
532 }
533
534 /// Set the tracked cursor to buffer-relative `pos`, with both axes
535 /// known, *without* emitting any move. This only updates the renderer's
536 /// belief; the caller must have already placed the terminal cursor there
537 /// (e.g. with a raw escape the renderer cannot see). For an actual cursor
538 /// move use [`move_cursor_to`](Self::move_cursor_to).
539 pub fn set_tracked_cursor(&mut self, pos: impl Into<Position>) {
540 self.renderer.set_cursor_position(pos.into());
541 }
542
543 // --- Render-coupled mode toggles ------------------------------------
544
545 /// Enter the alternate screen and flush.
546 pub fn enter_alt_screen(&mut self) -> io::Result<()> {
547 self.stage_set_alt_screen(true);
548 self.flush()
549 }
550
551 /// Leave the alternate screen and flush.
552 pub fn exit_alt_screen(&mut self) -> io::Result<()> {
553 self.stage_set_alt_screen(false);
554 self.flush()
555 }
556
557 /// Show the cursor and flush.
558 pub fn show_cursor(&mut self) -> io::Result<()> {
559 self.stage_set_cursor_visible(true);
560 self.flush()
561 }
562
563 /// Hide the cursor and flush.
564 pub fn hide_cursor(&mut self) -> io::Result<()> {
565 self.stage_set_cursor_visible(false);
566 self.flush()
567 }
568
569 /// Enable or disable synchronized-output frame wrapping.
570 ///
571 /// When enabled, each non-empty [`render`](Self::render) is wrapped in
572 /// begin/end synchronized-output sequences (DEC mode 2026) so terminals
573 /// that support it present the frame atomically, with no mid-frame
574 /// repaint. Terminals that don't support 2026 ignore the markers.
575 ///
576 /// This is your switch to flip: uncurses does not second-guess it against
577 /// detected capabilities. It is enabled automatically when the terminal
578 /// reports 2026 support during [`init`](Self::init), and you can override
579 /// that here at any time.
580 ///
581 /// Enabling it also changes how the cursor is handled per frame. With sync
582 /// off, a visible cursor is hidden around the cell diff so it doesn't dance
583 /// across cells as the renderer repositions it. With sync on, the frame is
584 /// presented in one step, so that hide/show pair is dropped: it is
585 /// redundant, and toggling the cursor every frame resets its blink phase,
586 /// which reads as flicker.
587 ///
588 /// This only sets state; the markers are emitted on the next `render`.
589 pub fn set_synchronized_output(&mut self, enabled: bool) {
590 self.state.sync_updates = enabled;
591 }
592
593 /// Enable Unicode core / grapheme-cluster mode (DECSET 2027) and flush:
594 /// [`set_str`](crate::text::TextSurface::set_str) and
595 /// [`insert_above`](Self::insert_above) measure cell widths per extended
596 /// grapheme cluster (UTS-29 plus emoji presentation rules).
597 pub fn enable_grapheme_clusters(&mut self) -> io::Result<()> {
598 self.stage_set_grapheme_clusters(true);
599 self.flush()
600 }
601
602 /// Disable grapheme-cluster mode (DECRST 2027) and flush, falling back
603 /// to per-code-point wcwidth-style measurement.
604 pub fn disable_grapheme_clusters(&mut self) -> io::Result<()> {
605 self.stage_set_grapheme_clusters(false);
606 self.flush()
607 }
608
609 /// Set the per-screen kitty keyboard enhancements and flush.
610 /// `Some(flags)` enables the selected progressive-enhancement bits;
611 /// `None` disables every enhancement.
612 pub fn set_kitty_keyboard(
613 &mut self,
614 flags: Option<crate::ansi::kitty::KittyKeyboardFlags>,
615 ) -> io::Result<()> {
616 let flags = flags.unwrap_or(crate::ansi::kitty::KittyKeyboardFlags::empty());
617 self.stage_set_kitty_keyboard_flags(flags);
618 self.flush()
619 }
620
621 /// Set the color profile used when emitting styled cells.
622 pub fn set_color_profile(&mut self, profile: crate::color::Profile) {
623 self.renderer.set_color_profile(profile);
624 }
625
626 /// Return the color profile used when emitting styled cells.
627 ///
628 /// This is the profile the renderer downsamples colors to, set by
629 /// [`set_color_profile`](Self::set_color_profile) or detected from the
630 /// environment when the screen was constructed. Pass it to
631 /// [`Encode::encode_with`](crate::text::Encode::encode_with) to serialize
632 /// a surface the same way this screen renders it.
633 pub fn color_profile(&self) -> crate::color::Profile {
634 self.renderer.color_profile()
635 }
636
637 /// Set the renderer optimization flags.
638 pub fn set_optimizations(&mut self, optimizations: Optimizations) {
639 self.renderer.set_optimizations(optimizations);
640 }
641
642 /// Return the renderer optimization flags currently in effect.
643 pub fn optimizations(&self) -> Optimizations {
644 self.renderer.optimizations()
645 }
646
647 /// Return whether Unicode core / grapheme-cluster mode (DEC 2027) is
648 /// active. When `true`, text is measured per extended grapheme cluster;
649 /// when `false`, per code point (wcwidth-style).
650 pub fn grapheme_clusters(&self) -> bool {
651 self.state.grapheme_clusters
652 }
653
654 // --- Render staging internals ---------------------------------------
655
656 /// Stage a single rendered frame into [`out_buf`](Self::out_buf):
657 /// synchronized-output begin, the renderer's cell diff, the optional
658 /// declarative cursor move, synchronized-output end. Assumes the front
659 /// buffer was synced.
660 ///
661 /// A visible cursor is hidden around the diff so it doesn't dance across
662 /// cells as the renderer repositions it, *unless* synchronized output is
663 /// enabled. A synchronized frame is presented in one step, so the cursor
664 /// never visibly moves mid-frame; the hide/show pair is then skipped, both
665 /// because it is redundant and because toggling DECTCEM every frame resets
666 /// the cursor's blink phase, which reads as flicker. Whether to trust
667 /// synchronized output is the caller's choice via
668 /// [`set_synchronized_output`](Self::set_synchronized_output), not gated on
669 /// detected capabilities.
670 fn write_frame(&mut self) {
671 let bracket_cursor = self.state.cursor_visible && !self.state.sync_updates;
672
673 if self.state.sync_updates {
674 mode::Mode::SYNCHRONIZED_OUTPUT
675 .set(&mut self.out_buf)
676 .unwrap();
677 }
678 if bracket_cursor {
679 mode::Mode::CURSOR_VISIBLE.reset(&mut self.out_buf).unwrap();
680 }
681
682 self.renderer.render_back(&mut self.out_buf).unwrap();
683
684 // Apply the declarative resting position (if any) inside the same
685 // bracket as the cell diff, so the cursor lands atomically.
686 // Sticky: re-applied every frame; move_to no-ops when already there.
687 if let Some(pos) = self.state.desired_cursor {
688 let pos = self.clamp_to_surface(pos);
689 self.renderer
690 .move_to(&mut self.out_buf, &self.front_buf, pos.y, pos.x)
691 .unwrap();
692 }
693
694 if bracket_cursor {
695 mode::Mode::CURSOR_VISIBLE.set(&mut self.out_buf).unwrap();
696 }
697 if self.state.sync_updates {
698 mode::Mode::SYNCHRONIZED_OUTPUT
699 .reset(&mut self.out_buf)
700 .unwrap();
701 }
702 }
703
704 /// Stage the alternate-screen toggle. Always emits DECSET/DECRST 1049;
705 /// the bookkeeping side effects (save/restore the renderer cursor model,
706 /// flip fullscreen/relative-cursor, request a clear, and re-apply the
707 /// tracked Kitty keyboard flags onto the newly active buffer) run only on
708 /// an actual transition.
709 fn stage_set_alt_screen(&mut self, alt_screen: bool) {
710 let changed = self.state.alt_screen != alt_screen;
711 if alt_screen {
712 if changed {
713 self.renderer.save_cursor();
714 }
715 mode::Mode::ALT_SCREEN_SAVE_CURSOR
716 .set(&mut self.out_buf)
717 .unwrap();
718 if changed {
719 self.state.alt_screen = true;
720 self.renderer.set_fullscreen(true);
721 self.renderer.set_relative_cursor(false);
722 self.renderer.request_clear();
723 }
724 } else {
725 mode::Mode::ALT_SCREEN_SAVE_CURSOR
726 .reset(&mut self.out_buf)
727 .unwrap();
728 if changed {
729 self.state.alt_screen = false;
730 self.renderer.set_fullscreen(false);
731 self.renderer.set_relative_cursor(true);
732 self.renderer.restore_cursor();
733 }
734 }
735 // The kitty keyboard stack is per-screen-buffer; on an actual buffer
736 // switch, re-apply the tracked flags onto the buffer we entered.
737 if changed && !self.state.kitty_keyboard.is_empty() {
738 kitty::write_set_kitty_keyboard(
739 &mut self.out_buf,
740 self.state.kitty_keyboard,
741 kitty::KittyKeyboardMode::Set,
742 )
743 .unwrap();
744 }
745 }
746
747 /// Stage a cursor-visibility change. Always emits the DECTCEM set/reset;
748 /// the tracked state is updated to match.
749 fn stage_set_cursor_visible(&mut self, visible: bool) {
750 if visible {
751 mode::Mode::CURSOR_VISIBLE.set(&mut self.out_buf).unwrap();
752 } else {
753 mode::Mode::CURSOR_VISIBLE.reset(&mut self.out_buf).unwrap();
754 }
755 self.state.cursor_visible = visible;
756 }
757
758 /// Stage a grapheme-cluster (DEC 2027) toggle. Always emits the
759 /// DECSET/DECRST; the tracked state is updated to match.
760 fn stage_set_grapheme_clusters(&mut self, enable: bool) {
761 if enable {
762 mode::Mode::UNICODE_CORE.set(&mut self.out_buf).unwrap();
763 } else {
764 mode::Mode::UNICODE_CORE.reset(&mut self.out_buf).unwrap();
765 }
766 self.state.grapheme_clusters = enable;
767 }
768
769 /// Stage a replacement Kitty keyboard enhancement flag set. Always emits
770 /// the `CSI = flags ; 1 u` set; the tracked set is updated to match.
771 fn stage_set_kitty_keyboard_flags(&mut self, flags: crate::ansi::kitty::KittyKeyboardFlags) {
772 kitty::write_set_kitty_keyboard(
773 &mut self.out_buf,
774 flags,
775 crate::ansi::kitty::KittyKeyboardMode::Set,
776 )
777 .unwrap();
778 self.state.kitty_keyboard = flags;
779 }
780
781 // --- Event delegates -------------------------------------------------
782 //
783 // These are pure input reads: they lock the shared source, move bytes,
784 // and hand back events. They do NOT track capabilities. Feed every event
785 // you take back through [`observe_event`](Self::observe_event) so resize
786 // handling and the discovery-driven defaults still apply — the sync and
787 // async ([`event_stream`](Self::event_stream)) paths follow the same rule.
788
789 /// Drive the input source for up to `timeout`, returning whether any
790 /// event became available. A pure readiness wait: no capability tracking.
791 /// See [`EventSource::poll`].
792 pub fn poll_event(&self, timeout: Option<Duration>) -> io::Result<bool> {
793 self.source.lock().unwrap().poll(timeout)
794 }
795
796 /// Take the next queued event without doing I/O. A pure read: pass the
797 /// event to [`observe_event`](Self::observe_event) to keep capability
798 /// tracking alive. See [`EventSource::try_read`].
799 pub fn try_read_event(&self) -> Option<Event> {
800 self.source.lock().unwrap().try_read()
801 }
802
803 /// Block until the next event. A pure read: pass the event to
804 /// [`observe_event`](Self::observe_event) to keep capability tracking
805 /// alive. See [`EventSource::read`].
806 pub fn read_event(&self) -> io::Result<Event> {
807 self.source.lock().unwrap().read()
808 }
809
810 /// Return an event to the front of the input queue, so the next
811 /// [`read_event`](Self::read_event) / [`try_read_event`](Self::try_read_event)
812 /// yields it before anything already queued. See [`EventSource::unread`].
813 pub fn unread_event(&self, event: Event) {
814 self.source.lock().unwrap().unread(event);
815 }
816
817 /// A shared handle to the input source behind
818 /// [`read_event`](Self::read_event) and friends, for driving input from a
819 /// separate reader over the same decoder rather than a second one racing
820 /// the same file descriptor.
821 ///
822 /// The main use is async input: build an
823 /// [`EventStream`](crate::event::EventStream) with
824 /// [`EventStream::from_shared`](crate::event::EventStream::from_shared) from
825 /// this handle and poll it on your executor. Like every read path, it is
826 /// pure — feed each event back through [`observe_event`](Self::observe_event)
827 /// to keep capability tracking alive.
828 ///
829 /// Sharing one source between a live reader and the screen's own
830 /// [`read_event`](Self::read_event) is best-effort: an event goes to
831 /// whichever consumer drains it first, so pick one reader in steady state.
832 pub fn event_source(&self) -> Arc<Mutex<EventSource<I>>> {
833 Arc::clone(&self.source)
834 }
835
836 /// Build an async [`EventStream`](crate::event::EventStream) over this
837 /// screen's input, for reading events with `events.next().await` inside a
838 /// `select!` on any executor. The stream shares the screen's decoder, so it
839 /// does not race a second reader on the same file descriptor.
840 ///
841 /// Reads are pure: feed each event to [`observe_event`](Self::observe_event)
842 /// to keep capability tracking alive, exactly as the sync path does. Read
843 /// through the stream *or* through [`read_event`](Self::read_event) in
844 /// steady state, not both at once: a shared source hands each event to
845 /// whichever consumer drains it first.
846 #[cfg(feature = "async")]
847 pub fn event_stream(&self) -> crate::event::EventStream<I>
848 where
849 I: 'static,
850 {
851 crate::event::EventStream::from_shared(Arc::clone(&self.source))
852 }
853
854 /// Consume any still-pending replies to the capability queries
855 /// [`init`](Self::init) fired, so they cannot leak to the shell (or a
856 /// child after [`pause`](Self::pause)) as stray input once the terminal
857 /// is restored to cooked mode.
858 ///
859 /// No-op unless queries were sent and their terminating Primary DA reply
860 /// has not yet been observed. Otherwise it waits at most the time left in
861 /// [`ScreenOptions::query_drain_timeout`], measured from when the queries
862 /// were sent, and returns as soon as that Primary DA reply lands. Reusing
863 /// the normal decode path means replies are consumed (not flushed), which
864 /// is race-free and identical on every platform.
865 fn drain_pending_queries(&mut self) -> io::Result<()> {
866 if self.defaults_applied {
867 return Ok(());
868 }
869 let Some(sent_at) = self.queries_sent_at.take() else {
870 return Ok(());
871 };
872 let budget = self.options.query_drain_timeout;
873 while !self.defaults_applied {
874 let Some(remaining) = budget.checked_sub(sent_at.elapsed()) else {
875 break;
876 };
877 if remaining.is_zero() {
878 break;
879 }
880 // Wait up to the remaining budget for input, then decode whatever
881 // arrived. Reads are pure now, so observe each event explicitly;
882 // `observe_event` flips `defaults_applied` on the Primary DA reply
883 // that terminates the capability-reply stream.
884 if !self.poll_event(Some(remaining))? {
885 break;
886 }
887 while let Some(ev) = self.try_read_event() {
888 let _ = self.observe_event(&ev);
889 if self.defaults_applied {
890 break;
891 }
892 }
893 }
894 Ok(())
895 }
896
897 /// Terminal capabilities detected so far from intercepted query
898 /// replies. Populated as the relevant reports arrive through the event
899 /// delegates after [`Self::init`].
900 pub fn capabilities(&self) -> Capabilities {
901 self.caps
902 }
903
904 /// Last observed full terminal size in cells, cached from resize and
905 /// `WindowCellSize` reports as they flow through the event delegates.
906 /// `None` until one has been observed.
907 pub fn window_cells(&self) -> Option<Size> {
908 self.window_cells
909 }
910
911 /// Last observed full terminal size in pixels, cached from resize
912 /// (when it carries pixel dimensions) and from
913 /// [`request_window_pixel_size`](Self::request_window_pixel_size)
914 /// replies. `None` until one has been observed.
915 pub fn window_pixels(&self) -> Option<Size> {
916 self.window_pixels
917 }
918
919 /// The raw XTVERSION reply identifying the terminal (e.g.
920 /// `"XTerm(380)"`). `None` until the reply has been observed.
921 pub fn terminal_name(&self) -> Option<&str> {
922 self.terminal_name.as_deref()
923 }
924
925 /// Convert a [`Mouse`](crate::event::Mouse) event reported in pixel
926 /// coordinates (SGR-pixel encoding) to cell coordinates using the
927 /// cached terminal size. Returns `None` when the window pixel size has
928 /// not been observed yet, so no conversion is possible — request it
929 /// with [`request_window_pixel_size`](Self::request_window_pixel_size),
930 /// or rely on an in-band resize report to populate it.
931 pub fn mouse_pixels_to_cells(&self, mouse: crate::event::Mouse) -> Option<crate::event::Mouse> {
932 let pixels = self.window_pixels?;
933 let cells = self.window_cells.unwrap_or_else(|| self.size());
934 Some(crate::event::mouse_pixel_to_cell(
935 mouse,
936 pixels.width,
937 pixels.height,
938 cells.width,
939 cells.height,
940 ))
941 }
942
943 /// Apply an event to the screen's capability tracking. The event is
944 /// inspected, never consumed.
945 ///
946 /// Reads are pure: [`read_event`](Self::read_event),
947 /// [`try_read_event`](Self::try_read_event), and the async
948 /// [`event_stream`](Self::event_stream) all hand back events *without*
949 /// tracking capabilities. Pass every event you receive here — on both the
950 /// sync and async paths — so capability detection stays alive.
951 ///
952 /// Capability-report replies to the queries [`init`](Self::init) fires are
953 /// recorded into [`capabilities`](Self::capabilities), window-size reports
954 /// update [`window_cells`](Self::window_cells) /
955 /// [`window_pixels`](Self::window_pixels), and the render-affecting reports
956 /// are applied. On the terminating Primary DA reply, the discovery-driven
957 /// defaults from the active [`ScreenOptions`] are applied once (enabling
958 /// mouse, keyboard enhancements, and in-band resize as configured), which
959 /// may emit escapes to the terminal.
960 ///
961 /// ```ignore
962 /// // Sync loop: read, observe, handle, render.
963 /// loop {
964 /// let ev = screen.read_event()?;
965 /// screen.observe_event(&ev)?; // keep capability tracking alive
966 /// // ... handle ev ...
967 /// screen.render()?;
968 /// }
969 /// ```
970 ///
971 /// ```ignore
972 /// // Async loop: same contract over an EventStream.
973 /// use tokio_stream::StreamExt;
974 ///
975 /// let mut events = screen.event_stream();
976 /// while let Some(ev) = events.next().await {
977 /// let ev = ev?;
978 /// screen.observe_event(&ev)?; // keep capability tracking alive
979 /// // ... handle ev ...
980 /// screen.render()?;
981 /// }
982 /// ```
983 pub fn observe_event(&mut self, event: &Event) -> io::Result<()> {
984 use crate::ansi::mode::Mode;
985 match *event {
986 Event::ModeReport { mode, setting } if setting.is_available() => match mode {
987 // Render-affecting: record and apply.
988 Mode::SYNCHRONIZED_OUTPUT => {
989 self.caps.synchronized_output = true;
990 self.state.sync_updates = true;
991 }
992 Mode::UNICODE_CORE => {
993 self.caps.grapheme_clusters = true;
994 self.stage_set_grapheme_clusters(true);
995 }
996 // Recorded only; enabling is the app's choice.
997 Mode::IN_BAND_RESIZE => self.caps.in_band_resize = true,
998 Mode::MOUSE_NORMAL => self.caps.mouse_normal = true,
999 Mode::MOUSE_BUTTON => self.caps.mouse_button = true,
1000 Mode::MOUSE_ANY => self.caps.mouse_any = true,
1001 Mode::MOUSE_SGR => self.caps.mouse_sgr = true,
1002 Mode::MOUSE_SGR_PIXEL => self.caps.mouse_sgr_pixel = true,
1003 _ => {}
1004 },
1005 Event::KittyKeyboardEnhancements(_) => self.caps.kitty_keyboard = true,
1006 // Any modifyOtherKeys report (`CSI > 4 ; n m`) answers our
1007 // query, so a reply means the terminal recognizes the feature.
1008 Event::ModifyOtherKeys(_) => self.caps.modify_other_keys = true,
1009 Event::PrimaryDeviceAttributes(ref attrs) => {
1010 // These come for free in the DA1 reply, which is sent as the
1011 // capability-query terminator regardless.
1012 if attrs.contains(&Some(4)) {
1013 self.caps.sixel = true;
1014 }
1015 if attrs.contains(&Some(52)) {
1016 self.caps.clipboard = true;
1017 }
1018 // Primary DA is the terminating reply: every capability is
1019 // now known, so apply the discovery-driven defaults once.
1020 if !self.defaults_applied {
1021 self.defaults_applied = true;
1022 self.apply_defaults()?;
1023 }
1024 }
1025 Event::TerminalName(ref report) => {
1026 self.terminal_name = Some(report.clone());
1027 }
1028 // Cache the full terminal size as it changes. Refitting the
1029 // managed area is left to the app (call autoresize() as desired).
1030 Event::Resize(ws) => {
1031 self.cache_window_size(ws)?;
1032 }
1033 Event::WindowCellSize { width, height } => {
1034 self.window_cells = Some(Size::new(width, height));
1035 }
1036 Event::WindowPixelSize { width, height } => {
1037 self.window_pixels = Some(Size::new(width, height));
1038 }
1039 // A successful XTGETTCAP reply for a truecolor capability
1040 // confirms direct-color support: record and upgrade the
1041 // renderer's color profile.
1042 Event::Termcap {
1043 recognized: true,
1044 ref payload,
1045 } if payload.contains("RGB") || payload.contains("Tc") => {
1046 self.caps.true_color = true;
1047 self.renderer
1048 .set_color_profile(crate::color::Profile::TrueColor);
1049 }
1050 _ => {}
1051 }
1052 Ok(())
1053 }
1054
1055 /// Apply the discovery-driven defaults from the active [`ScreenOptions`]
1056 /// once every capability is known (called on the Primary DA reply).
1057 fn apply_defaults(&mut self) -> io::Result<()> {
1058 use crate::event::ModifyOtherKeysMode;
1059
1060 // Prefer in-band resize over the SIGWINCH path when supported.
1061 if self.options.prefer_in_band_resize && self.caps.in_band_resize {
1062 self.enable_in_band_resize()?;
1063 self.source.lock().unwrap().set_handle_resize(false);
1064 }
1065
1066 // Keyboard enhancements: prefer the Kitty protocol, falling back to
1067 // xterm modifyOtherKeys, enabling only what the terminal supports.
1068 if !self.options.keyboard_enhancements.is_empty() {
1069 if self.caps.kitty_keyboard {
1070 self.set_kitty_keyboard(Some(self.options.keyboard_enhancements))?;
1071 } else if self.caps.modify_other_keys {
1072 self.set_modify_other_keys(ModifyOtherKeysMode::Mode2)?;
1073 }
1074 }
1075
1076 // Mouse tracking: enable exactly what the options request. enable_mouse
1077 // emits the mode set unconditionally; terminals that lack a requested
1078 // mode (e.g. SGR-pixel) ignore it and degrade to cell coordinates.
1079 if let Some(tracking) = self.options.mouse {
1080 self.enable_mouse(tracking)?;
1081 }
1082 Ok(())
1083 }
1084
1085 /// Whether the host is Apple's `Terminal.app`, which does not support
1086 /// most of the queried features and mishandles the queries themselves.
1087 fn is_apple_terminal(&self) -> bool {
1088 self.terminal.get_env("TERM_PROGRAM").as_deref() == Some("Apple_Terminal")
1089 }
1090
1091 /// The major version of Apple's `Terminal.app`, parsed from
1092 /// `TERM_PROGRAM_VERSION` (e.g. `"470"` or `"470.1"` yield `470`).
1093 /// `None` when the variable is absent or not numeric.
1094 fn apple_terminal_version(&self) -> Option<u32> {
1095 let raw = self.terminal.get_env("TERM_PROGRAM_VERSION")?;
1096 raw.split('.').next()?.trim().parse().ok()
1097 }
1098
1099 /// Stage the initial capability queries into the output stream. Their
1100 /// replies arrive asynchronously through the normal event flow and are
1101 /// recorded as a side effect by [`observe`](Self::observe).
1102 ///
1103 /// The mode (DECRQM), XTVERSION, and XTGETTCAP queries are skipped on
1104 /// Apple's `Terminal.app`, which mishandles them. A Primary DA request
1105 /// is sent last so its reply marks the end of the capability replies.
1106 /// Detect the environment-derived color profile and apply it to the
1107 /// renderer. Called by `init_with` on every path so output downsamples
1108 /// correctly even when capability queries are skipped.
1109 /// Detect the environment-derived color profile and apply it to the
1110 /// renderer, clamping to no color when the output half is not a terminal
1111 /// (e.g. redirected to a file or pipe). `is_tty` is the output's
1112 /// terminal status; the caller supplies it since the platform handle
1113 /// bounds live on `init_with`.
1114 fn apply_env_color_profile(&mut self, is_tty: bool) {
1115 let profile = crate::color::Profile::detect_from(self.terminal.env(), is_tty);
1116 self.renderer.set_color_profile(profile);
1117 }
1118
1119 /// Reconcile the terminal's hardware tab stops with the every-eight
1120 /// columns layout the renderer assumes whenever the `TABS`
1121 /// optimization is on. A prior program may have left arbitrary stops
1122 /// behind, which would make the `HT` (`\t`) moves the cursor planner
1123 /// emits land on the wrong columns. Modern terminals reset in one
1124 /// cursor-safe write via DECST8C; the rest get the portable
1125 /// TBC-then-HTS fallback. Skipped entirely when `TABS` is off, since
1126 /// the planner then never relies on tab stops. Staged and flushed so
1127 /// it reaches the terminal even when capability queries are disabled.
1128 fn reset_tab_stops(&mut self) -> io::Result<()> {
1129 if !self.optimizations().contains(Optimizations::TABS) {
1130 return Ok(());
1131 }
1132 if Optimizations::supports_decst8c(self.terminal.env()) {
1133 self.out_buf
1134 .write_all(crate::ansi::screen::SET_TAB_EVERY_8_COLUMNS)?;
1135 } else {
1136 crate::ansi::screen::write_reset_tab_stops_every_8(&mut self.out_buf, self.width)?;
1137 }
1138 self.flush()
1139 }
1140
1141 fn send_init_queries(&mut self) -> io::Result<()> {
1142 use crate::ansi::ctrl::{REQUEST_PRIMARY_DA, REQUEST_XTVERSION};
1143 use crate::ansi::kitty::REQUEST_KITTY_KEYBOARD;
1144 use crate::ansi::mode::Mode;
1145 use crate::ansi::termcap::write_xtgettcap;
1146 use crate::color::Profile;
1147
1148 // The env-derived profile is already applied by init_with via
1149 // apply_env_color_profile; read it back to decide whether there is
1150 // headroom to upgrade via XTGETTCAP.
1151 let profile = self.renderer.color_profile();
1152
1153 // Always-safe queries.
1154 self.out_buf.write_all(REQUEST_KITTY_KEYBOARD)?;
1155
1156 if !self.is_apple_terminal() {
1157 for mode in [
1158 Mode::SYNCHRONIZED_OUTPUT,
1159 Mode::UNICODE_CORE,
1160 Mode::IN_BAND_RESIZE,
1161 Mode::MOUSE_NORMAL,
1162 Mode::MOUSE_BUTTON,
1163 Mode::MOUSE_ANY,
1164 Mode::MOUSE_SGR,
1165 Mode::MOUSE_SGR_PIXEL,
1166 ] {
1167 mode.request(&mut self.out_buf)?;
1168 }
1169 self.out_buf.write_all(REQUEST_XTVERSION)?;
1170 self.out_buf
1171 .write_all(crate::ansi::xterm::QUERY_MODIFY_OTHER_KEYS)?;
1172 if profile < Profile::TrueColor {
1173 // One key per query: some terminals only answer the first
1174 // capability when several are batched in a single request.
1175 write_xtgettcap(&mut self.out_buf, &["RGB"])?;
1176 write_xtgettcap(&mut self.out_buf, &["Tc"])?;
1177 }
1178 } else {
1179 // Terminal.app mishandles the capability queries, but its
1180 // support for these features is known, so record them directly:
1181 // mouse tracking (normal/button/any) and the SGR encoding (no
1182 // pixel reporting). Bracketed paste is enabled unconditionally,
1183 // so it needs no capability flag.
1184 self.caps.mouse_normal = true;
1185 self.caps.mouse_button = true;
1186 self.caps.mouse_any = true;
1187 self.caps.mouse_sgr = true;
1188 // Terminal.app gained direct-color support in the build shipped
1189 // with macOS Tahoe; record it and upgrade the renderer when the
1190 // env-derived profile hasn't already.
1191 if profile < Profile::TrueColor
1192 && self.apple_terminal_version().is_some_and(|v| v >= 470)
1193 {
1194 self.caps.true_color = true;
1195 self.renderer.set_color_profile(Profile::TrueColor);
1196 }
1197 }
1198
1199 self.out_buf.write_all(REQUEST_PRIMARY_DA)?;
1200 self.flush()
1201 }
1202}
1203
1204impl<I, O> Write for Screen<I, O>
1205where
1206 I: Input,
1207 O: Write,
1208{
1209 /// Append raw bytes to the staging buffer, ordered with any staged mode
1210 /// or frame bytes. They reach the terminal on the next [`flush`](Self::flush).
1211 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
1212 self.out_buf.extend_from_slice(buf);
1213 Ok(buf.len())
1214 }
1215
1216 /// Drain the staging buffer through the terminal and flush it.
1217 fn flush(&mut self) -> io::Result<()> {
1218 if !self.out_buf.is_empty() {
1219 #[cfg(debug_assertions)]
1220 crate::trace::tee_output(&self.out_buf);
1221 self.terminal.write_all(&self.out_buf)?;
1222 self.out_buf.clear();
1223 }
1224 self.terminal.flush()
1225 }
1226}
1227
1228impl<I, O> Bounded for Screen<I, O>
1229where
1230 I: Input,
1231 O: Write,
1232{
1233 fn bounds(&self) -> Rect {
1234 self.front_buf.bounds()
1235 }
1236}
1237
1238impl<I, O> Surface for Screen<I, O>
1239where
1240 I: Input,
1241 O: Write,
1242{
1243 fn cell(&self, pos: Position) -> Option<&Cell> {
1244 self.front_buf.cell(pos)
1245 }
1246}
1247
1248impl<I, O> SurfaceMut for Screen<I, O>
1249where
1250 I: Input,
1251 O: Write,
1252{
1253 fn set_cell(&mut self, pos: Position, cell: &Cell) {
1254 self.front_buf.set_cell(pos, cell);
1255 }
1256
1257 fn cell_mut(&mut self, pos: Position) -> Option<&mut Cell> {
1258 self.front_buf.cell_mut(pos)
1259 }
1260
1261 fn insert_lines(&mut self, y: u16, n: u16, bounds_bottom: u16, fill: &Cell) {
1262 self.front_buf.insert_lines(y, n, bounds_bottom, fill);
1263 }
1264
1265 fn delete_lines(&mut self, y: u16, n: u16, bounds_bottom: u16, fill: &Cell) {
1266 self.front_buf.delete_lines(y, n, bounds_bottom, fill);
1267 }
1268
1269 fn insert_cells(&mut self, pos: Position, n: u16, bounds_right: u16, fill: &Cell) {
1270 self.front_buf.insert_cells(pos, n, bounds_right, fill);
1271 }
1272
1273 fn delete_cells(&mut self, pos: Position, n: u16, bounds_right: u16, fill: &Cell) {
1274 self.front_buf.delete_cells(pos, n, bounds_right, fill);
1275 }
1276}
1277
1278impl<I, O> TextSurface for Screen<I, O>
1279where
1280 I: Input,
1281 O: Write,
1282{
1283 fn width_mode(&self) -> WidthMode {
1284 if self.state.grapheme_clusters {
1285 WidthMode::Grapheme
1286 } else {
1287 WidthMode::Wc
1288 }
1289 }
1290
1291 fn eaw_wide(&self) -> bool {
1292 self.eaw_wide
1293 }
1294}
1295
1296impl<I, O> Screen<I, O>
1297where
1298 I: Input + Copy,
1299 O: Write,
1300{
1301 /// Build the render fields and event source over `terminal`, sizing the
1302 /// managed area to `size`. The color profile and renderer optimizations
1303 /// are detected from the terminal's captured environment. The terminal is
1304 /// left as-is.
1305 fn with_render(terminal: Terminal<I, O>, size: (u16, u16)) -> io::Result<Self> {
1306 let env = terminal.env();
1307 // Provisional profile; init_with reapplies it with the real
1308 // output-is-tty signal via apply_env_color_profile.
1309 let color_profile = Profile::detect_from(env, true);
1310 let optimizations = Optimizations::from_env(env);
1311 let mut renderer = Renderer::new();
1312 renderer.set_color_profile(color_profile);
1313 renderer.set_optimizations(optimizations);
1314 // Defaults match inline (no alt screen): the surface is anchored
1315 // wherever the cursor sits, so moves stay relative.
1316 renderer.set_fullscreen(false);
1317 renderer.set_relative_cursor(true);
1318
1319 let source = Arc::new(Mutex::new(EventSource::new(terminal.input())?));
1320 let mut screen = Self {
1321 terminal,
1322 front_buf: RenderBuffer::new(0, 0),
1323 renderer,
1324 out_buf: Vec::with_capacity(4096),
1325 width: 0,
1326 height: 0,
1327 eaw_wide: false,
1328 source,
1329 state: state::State::default(),
1330 caps: Capabilities::default(),
1331 options: ScreenOptions::default(),
1332 defaults_applied: false,
1333 window_cells: None,
1334 window_pixels: None,
1335 terminal_name: None,
1336 queries_sent_at: None,
1337 };
1338 let (w, h) = size;
1339 if w != 0 || h != 0 {
1340 screen.resize((w, h));
1341 }
1342 Ok(screen)
1343 }
1344}
1345
1346#[cfg(unix)]
1347impl<I, O> Screen<I, O>
1348where
1349 I: Input + Copy + std::os::fd::AsFd,
1350 O: Write + Copy + std::os::fd::AsFd,
1351{
1352 /// Construct a screen over `terminal` without touching the terminal:
1353 /// size the renderer to it and create an [`EventSource`] on its input
1354 /// half. The terminal is left as-is; call [`Self::init`] to enter raw
1355 /// mode and begin a session.
1356 pub fn new(terminal: Terminal<I, O>) -> io::Result<Self> {
1357 let ws = terminal.get_window_size()?;
1358 Self::with_render(terminal, (ws.col, ws.row))
1359 }
1360
1361 /// Begin a session with the default [`ScreenOptions`]. See
1362 /// [`Self::init_with`].
1363 pub fn init(&mut self) -> io::Result<()> {
1364 self.init_with(ScreenOptions::default())
1365 }
1366
1367 /// Begin a session: enter raw mode, apply the always-on defaults from
1368 /// `options`, and send the capability queries whose replies the event
1369 /// loop consumes. Discovery-driven defaults are applied later, once the
1370 /// terminating Primary DA reply confirms the detected capabilities (see
1371 /// [`Self::capabilities`]). Call once after [`Self::new`], before
1372 /// rendering.
1373 pub fn init_with(&mut self, options: ScreenOptions) -> io::Result<()> {
1374 self.options = options;
1375 self.terminal.make_raw()?;
1376 self.autoresize()?;
1377 // Apply the env color profile on every path so output downsamples
1378 // correctly even when capability queries are skipped. Disable color
1379 // when the output is not a terminal (redirected to a file or pipe).
1380 let is_tty = self.terminal.is_terminal().1;
1381 self.apply_env_color_profile(is_tty);
1382 self.reset_tab_stops()?;
1383 if self.options.bracketed_paste {
1384 self.enable_bracketed_paste()?;
1385 }
1386 if self.options.query_capabilities {
1387 self.send_init_queries()?;
1388 self.queries_sent_at = Some(Instant::now());
1389 }
1390 Ok(())
1391 }
1392
1393 /// Query the current terminal window size (output half first, input as
1394 /// fallback). This is a live query; the cached
1395 /// [`window_cells`](Self::window_cells) /
1396 /// [`window_pixels`](Self::window_pixels) accessors return the
1397 /// last-observed values without I/O.
1398 pub fn get_window_size(&self) -> io::Result<crate::terminal::Winsize> {
1399 self.terminal.get_window_size()
1400 }
1401
1402 /// Re-query the terminal size and resize the managed area to fit: the full
1403 /// terminal size in fullscreen (alternate screen on), or the terminal
1404 /// width with the current managed height preserved inline (alternate
1405 /// screen off). Refreshes the cached [`window_cells`](Self::window_cells)
1406 /// / [`window_pixels`](Self::window_pixels); on platforms whose size
1407 /// query reports no pixel size (e.g. the Windows console) the pixel size
1408 /// is requested over the wire.
1409 pub fn autoresize(&mut self) -> io::Result<()> {
1410 let Ok(ws) = self.terminal.get_window_size() else {
1411 // Keep the current size when the query fails rather than
1412 // collapsing the managed area to zero.
1413 return Ok(());
1414 };
1415 self.cache_window_size(ws)?;
1416 let height = if self.state.alt_screen {
1417 ws.row
1418 } else {
1419 self.height
1420 };
1421 self.resize((ws.col, height));
1422 Ok(())
1423 }
1424
1425 /// Consume the screen and hand the terminal back to the shell: tear down
1426 /// every staged mode, reset the managed area, flush, and restore the
1427 /// terminal's prior state.
1428 pub fn finish(mut self) -> io::Result<()> {
1429 self.teardown()?;
1430 self.terminal.restore()
1431 }
1432
1433 /// Hand the terminal back to the shell without consuming the screen,
1434 /// e.g. to run a child process. Re-enter with [`Self::resume`]. Like
1435 /// [`Self::finish`] but keeps the screen so the session can continue.
1436 pub fn pause(&mut self) -> io::Result<()> {
1437 self.teardown()?;
1438 self.terminal.restore()
1439 }
1440
1441 /// Re-acquire the terminal after a [`Self::pause`] or [`Self::suspend`]:
1442 /// re-enter raw mode, refit the managed area to the current viewport, re-apply
1443 /// the saved render state and modes, and force a full repaint.
1444 pub fn resume(&mut self) -> io::Result<()> {
1445 self.terminal.make_raw()?;
1446 self.autoresize()?;
1447 self.restore()?;
1448 self.invalidate();
1449 self.flush()
1450 }
1451
1452 /// Suspend the process: [`pause`](Self::pause) the screen, then stop
1453 /// the process with `SIGTSTP`. Returns once the process is
1454 /// foregrounded again; the caller should then call [`Self::resume`].
1455 pub fn suspend(&mut self) -> io::Result<()> {
1456 self.pause()?;
1457 // SAFETY: raise is async-signal-safe.
1458 unsafe { libc::raise(libc::SIGTSTP) };
1459 Ok(())
1460 }
1461
1462 /// Hand the terminal back: consume any pending capability-query replies,
1463 /// reset every staged mode and the managed area to defaults, and flush. The
1464 /// caller restores the saved raw-mode state afterward.
1465 fn teardown(&mut self) -> io::Result<()> {
1466 self.drain_pending_queries()?;
1467 self.reset()?;
1468 self.flush()
1469 }
1470}
1471
1472#[cfg(windows)]
1473impl<I, O> Screen<I, O>
1474where
1475 I: Input + Copy + std::os::windows::io::AsHandle,
1476 O: Write + Copy + std::os::windows::io::AsHandle,
1477{
1478 /// Construct a screen over `terminal` without touching the terminal:
1479 /// size the renderer to it and create an [`EventSource`] on its input
1480 /// half. The terminal is left as-is; call [`Self::init`] to enter raw
1481 /// mode and begin a session.
1482 pub fn new(terminal: Terminal<I, O>) -> io::Result<Self> {
1483 let ws = terminal.get_window_size()?;
1484 Self::with_render(terminal, (ws.col, ws.row))
1485 }
1486
1487 /// Begin a session with the default [`ScreenOptions`]. See
1488 /// [`Self::init_with`].
1489 pub fn init(&mut self) -> io::Result<()> {
1490 self.init_with(ScreenOptions::default())
1491 }
1492
1493 /// Begin a session: enter raw mode, apply the always-on defaults from
1494 /// `options`, and send the capability queries whose replies the event
1495 /// loop consumes. Discovery-driven defaults are applied later, once the
1496 /// terminating Primary DA reply confirms the detected capabilities (see
1497 /// [`Self::capabilities`]). Call once after [`Self::new`], before
1498 /// rendering.
1499 pub fn init_with(&mut self, options: ScreenOptions) -> io::Result<()> {
1500 self.options = options;
1501 self.terminal.make_raw()?;
1502 self.autoresize()?;
1503 // Apply the env color profile on every path so output downsamples
1504 // correctly even when capability queries are skipped. Disable color
1505 // when the output is not a terminal (redirected to a file or pipe).
1506 let is_tty = self.terminal.is_terminal().1;
1507 self.apply_env_color_profile(is_tty);
1508 self.reset_tab_stops()?;
1509 if self.options.bracketed_paste {
1510 self.enable_bracketed_paste()?;
1511 }
1512 if self.options.query_capabilities {
1513 self.send_init_queries()?;
1514 self.queries_sent_at = Some(Instant::now());
1515 }
1516 Ok(())
1517 }
1518
1519 /// Query the current terminal window size (output half first, input as
1520 /// fallback). This is a live query; the cached
1521 /// [`window_cells`](Self::window_cells) /
1522 /// [`window_pixels`](Self::window_pixels) accessors return the
1523 /// last-observed values without I/O.
1524 pub fn get_window_size(&self) -> io::Result<crate::terminal::Winsize> {
1525 self.terminal.get_window_size()
1526 }
1527
1528 /// Re-query the terminal size and resize the managed area to fit: the full
1529 /// terminal size in fullscreen (alternate screen on), or the terminal
1530 /// width with the current managed height preserved inline (alternate
1531 /// screen off). Refreshes the cached [`window_cells`](Self::window_cells)
1532 /// / [`window_pixels`](Self::window_pixels); on platforms whose size
1533 /// query reports no pixel size (e.g. the Windows console) the pixel size
1534 /// is requested over the wire.
1535 pub fn autoresize(&mut self) -> io::Result<()> {
1536 let Ok(ws) = self.terminal.get_window_size() else {
1537 // Keep the current size when the query fails rather than
1538 // collapsing the managed area to zero.
1539 return Ok(());
1540 };
1541 self.cache_window_size(ws)?;
1542 let height = if self.state.alt_screen {
1543 ws.row
1544 } else {
1545 self.height
1546 };
1547 self.resize((ws.col, height));
1548 Ok(())
1549 }
1550
1551 /// Consume the screen and hand the terminal back to the shell: tear down
1552 /// every staged mode, reset the managed area, flush, and restore the
1553 /// terminal's prior state.
1554 pub fn finish(mut self) -> io::Result<()> {
1555 self.teardown()?;
1556 self.terminal.restore()
1557 }
1558
1559 /// Hand the terminal back to the shell without consuming the screen,
1560 /// e.g. to run a child process. Re-enter with [`Self::resume`]. Like
1561 /// [`Self::finish`] but keeps the screen so the session can continue.
1562 pub fn pause(&mut self) -> io::Result<()> {
1563 self.teardown()?;
1564 self.terminal.restore()
1565 }
1566
1567 /// Re-acquire the terminal after a [`Self::pause`]: re-enter raw mode,
1568 /// refit the managed area to the current viewport, re-apply the saved
1569 /// render state and modes, and force a full repaint.
1570 pub fn resume(&mut self) -> io::Result<()> {
1571 self.terminal.make_raw()?;
1572 self.autoresize()?;
1573 self.restore()?;
1574 self.invalidate();
1575 self.flush()
1576 }
1577
1578 /// Hand the terminal back: consume any pending capability-query replies,
1579 /// reset every staged mode and the managed area to defaults, and flush. The
1580 /// caller restores the saved raw-mode state afterward.
1581 fn teardown(&mut self) -> io::Result<()> {
1582 self.drain_pending_queries()?;
1583 self.reset()?;
1584 self.flush()
1585 }
1586}
1587
1588impl Screen<crate::terminal::Stdin, crate::terminal::Stdout> {
1589 /// Build a screen over the process stdio (`stdin` + `stdout`).
1590 pub fn stdio() -> io::Result<Self> {
1591 Self::new(Terminal::stdio())
1592 }
1593}
1594
1595impl Screen<crate::terminal::TtyInput, crate::terminal::TtyOutput> {
1596 /// Build a screen over the controlling terminal (`/dev/tty`, or
1597 /// `CONIN$`/`CONOUT$` on Windows), useful when stdio is redirected.
1598 pub fn open() -> io::Result<Self> {
1599 Self::new(Terminal::open()?)
1600 }
1601}