Skip to main content

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}