Skip to main content

uncurses/program/
mod.rs

1//! [`Program`] — the interactive terminal session.
2//!
3//! `Program<I, O>` is what you build a terminal application on. It owns the
4//! things a session needs and a [`Screen`] to render with:
5//!
6//! - a [`Terminal`] for the raw-mode lifecycle,
7//! - an [`EventSource`] for decoded input,
8//! - the terminal and input modes (mouse, bracketed paste, focus reporting,
9//!   in-band resize, titles, colors, cursor style, keyboard enhancements),
10//!   tracked so they can be torn down on a shell handoff and re-applied after,
11//! - the [`Capabilities`] the terminal has reported, recorded from replies as
12//!   they pass through the read path.
13//!
14//! Drawing is not on `Program`. Reach the renderer with
15//! [`screen_mut`](Program::screen_mut) and call
16//! [`render`](Screen::render) on it — that is the only `render` in the crate,
17//! and the only `flush`.
18//!
19//! Construction is inert: [`Program::new`] (and the [`stdio`](Program::stdio)
20//! / [`open`](Program::open) shortcuts) only build the program. Begin a
21//! session with [`Program::init`], which enters raw mode. Nothing is probed
22//! unless you ask: call [`Program::query_capabilities`] for that. Teardown is
23//! explicit: there is **no** `Drop`.
24//! Hand the terminal back to the shell with [`Program::finish`] (consume),
25//! [`Program::pause`] (keep, e.g. to shell out), or [`Program::suspend`]
26//! (pause, then stop the process with `SIGTSTP`); resume a
27//! paused/suspended program with [`Program::resume`].
28//!
29//! ```no_run
30//! use uncurses::program::Program;
31//! use uncurses::style::Style;
32//! use uncurses::text::TextSurface;
33//!
34//! # fn main() -> std::io::Result<()> {
35//! let mut program = Program::open()?; // build over /dev/tty
36//! program.init()?; // raw mode; probes nothing on its own
37//! program.enter_alt_screen()?;
38//!
39//! let screen = program.screen_mut();
40//! screen.set_str((0, 0), "hello", Style::default());
41//! screen.render()?;
42//!
43//! let event = program.read_event()?; // reply tracking is automatic
44//! program.finish()?; // restore the terminal
45//! # Ok(())
46//! # }
47//! ```
48//!
49//! # Options and defaults
50//!
51//! [`init`](Program::init) uses [`ProgramOptions::default`];
52//! [`init_with`](Program::init_with) takes an explicit [`ProgramOptions`] to
53//! choose whether to enable bracketed paste and mouse tracking at startup.
54//! Those take effect immediately at init.
55//!
56//! The three `prefer_*` fields are discovery-driven instead: they enable
57//! grapheme-cluster mode, in-band resize, and synchronized output only once
58//! the terminal reports the mode as available. Since a program never probes
59//! on its own, that means calling
60//! [`query_capabilities`](Program::query_capabilities) and
61//! reading the replies (see [`capabilities`](Program::capabilities)).
62//!
63//! [`Terminal`]: crate::terminal::Terminal
64//! [`EventSource`]: crate::event::EventSource
65
66mod cursor;
67mod modes;
68mod state;
69#[cfg(test)]
70mod tests;
71
72pub use cursor::CursorShape;
73pub use state::Capabilities;
74
75use std::collections::VecDeque;
76use std::io::{self, Write};
77use std::sync::{Arc, Mutex};
78use std::time::Duration;
79
80use bitflags::bitflags;
81
82use crate::ansi::{mode, progress};
83use crate::color::Profile;
84use crate::event::Input;
85use crate::event::{Event, EventSource};
86use crate::layout::{Position, Size};
87use crate::renderer::Optimizations;
88use crate::screen::Screen;
89use crate::terminal::Terminal;
90
91/// An interactive terminal session composing a [`Terminal`], an
92/// [`EventSource`], and a [`Screen`] to render with. See
93/// the [module documentation](self) for the lifecycle.
94///
95/// `Program` is [`Send`] and [`Sync`] whenever its input and output handles
96/// are, so it can be moved onto another thread or held across an `.await`
97/// point in a multi-threaded async runtime.
98///
99/// [`Terminal`]: crate::terminal::Terminal
100/// [`EventSource`]: crate::event::EventSource
101pub struct Program<I, O>
102where
103    I: Input,
104    O: Write,
105{
106    /// Owns the raw-mode state and answers the fd-bound queries (window size,
107    /// is-a-tty). Never written through: output goes through `screen`, which
108    /// holds a copy of the same handle, so one staging buffer keeps
109    /// everything in order.
110    terminal: Terminal<I, O>,
111    /// The renderer. Reach it with [`screen`](Self::screen) /
112    /// [`screen_mut`](Self::screen_mut).
113    screen: Screen<O>,
114    /// Input source behind the read path ([`Self::read_event`] and friends).
115    /// Held in an `Arc<Mutex<_>>`; the lock is uncontended in the common
116    /// single-reader case.
117    source: Arc<Mutex<EventSource<I>>>,
118    /// Events handed back by [`Self::unread_event`], which the read path
119    /// drains before the source. Kept here rather than in `source` because
120    /// these were already observed: routing them back through the source
121    /// would observe them a second time, and a reply counts once.
122    unread: VecDeque<Event>,
123    state: state::State,
124    /// Terminal capabilities, recorded by intercepting replies as they pass
125    /// through the read path. Empty until [`Self::query_capabilities`] is
126    /// called and the replies are read.
127    caps: Capabilities,
128    /// Desired default behaviors, set by [`Self::init_with`].
129    options: ProgramOptions,
130    /// Last observed full terminal size in cells, from resize and
131    /// `WindowCellSize` reports. `None` until first observed.
132    window_cells: Option<Size>,
133    /// Last observed full terminal size in pixels, from resize (when it
134    /// carries pixel dimensions) and `WindowPixelSize` reports. `None`
135    /// until first observed.
136    window_pixels: Option<Size>,
137    /// Cell size in pixels as reported by the last `CSI 16 t` reply, kept
138    /// until another reply replaces it. `None` until the terminal answers
139    /// one; [`cell_pixels`](Self::cell_pixels) falls back to dividing the
140    /// window sizes.
141    cell_pixels: Option<Size>,
142    /// Physical screen coordinate (0-based, from the terminal's top-left) of
143    /// the managed area's top-left cell, tracked for inline sessions. Only
144    /// meaningful inline; fullscreen [`origin`](Self::origin) is always
145    /// `(0, 0)`. Refreshed by [`request_origin`](Self::request_origin), whose
146    /// reply is captured in [`observe_event`](Self::observe_event).
147    origin: Position,
148    /// How many origin `CSI 6n` requests are outstanding, so
149    /// [`observe_event`](Self::observe_event) knows which
150    /// [`CursorPosition`](Event::CursorPosition) replies are ours to capture.
151    /// A count rather than a flag so a burst of requests keeps the last
152    /// reply instead of the first.
153    origin_queries_pending: u16,
154}
155
156/// Defaults applied by [`Program::init_with`].
157///
158/// Most fields take effect at init unconditionally. The three `prefer_*`
159/// fields are the exception: they depend on capability detection, so they do
160/// nothing until the terminal reports the matching mode as available. A
161/// [`Program`] never probes on its own, so that report only arrives if you
162/// call
163/// [`query_capabilities`](Program::query_capabilities) and read the replies.
164/// Without it these two fields stay dormant and the modes are never enabled.
165#[derive(Debug, Clone)]
166pub struct ProgramOptions {
167    /// Enable bracketed paste at init. Defaults to `true`.
168    pub bracketed_paste: bool,
169    /// Enable mouse tracking at init with the given [`MouseTracking`] extras
170    /// (see [`Program::enable_mouse`]). The request is emitted unconditionally;
171    /// terminals ignore modes they do not support and degrade gracefully.
172    /// Defaults to `None` (mouse tracking off).
173    pub mouse: Option<MouseTracking>,
174    /// Enable grapheme-cluster mode (DEC mode 2027) once the terminal reports
175    /// it as available, so the terminal and the [`Screen`] measure text the
176    /// same way. Defaults to `true`.
177    ///
178    /// Nothing is emitted until that report arrives, which requires
179    /// [`query_capabilities`](Program::query_capabilities) and a read loop.
180    /// Set to `false` to keep per-code-point measurement, or call
181    /// [`enable_grapheme_clusters`](Program::enable_grapheme_clusters)
182    /// yourself to opt in without waiting for a report.
183    pub prefer_grapheme_clusters: bool,
184    /// Enable in-band resize notifications (DEC mode 2048) once the terminal
185    /// reports them as available, so resizes arrive on the event stream with
186    /// pixel dimensions instead of through `SIGWINCH`. Defaults to `true`.
187    ///
188    /// Nothing is emitted until that report arrives, which requires
189    /// [`query_capabilities`](Program::query_capabilities) and a read loop.
190    /// Set to `false` to stay on the signal path, or call
191    /// [`enable_in_band_resize`](Program::enable_in_band_resize) yourself to
192    /// opt in without waiting for a report.
193    pub prefer_in_band_resize: bool,
194    /// Wrap each frame in synchronized-output markers (DEC mode 2026) once the
195    /// terminal reports them as available, so a frame is presented in one
196    /// piece instead of tearing. Defaults to `true`.
197    ///
198    /// Unlike the two above this emits nothing of its own: synchronized output
199    /// is a render property, so adopting it only tells the [`Screen`] to start
200    /// bracketing frames. Set to `false` to keep frames unwrapped for the whole
201    /// session. [`Screen::set_synchronized_output`] drives the same property
202    /// directly, but the program cannot see a choice made there, so a call made
203    /// before the terminal's first report is still overridden by adoption; use
204    /// this field when the decision has to hold from the start.
205    ///
206    /// [`Screen::set_synchronized_output`]: crate::screen::Screen::set_synchronized_output
207    pub prefer_synchronized_output: bool,
208}
209
210bitflags! {
211    /// Optional mouse tracking features layered on top of basic button
212    /// tracking.
213    ///
214    /// When mouse tracking is enabled, button-event tracking (presses,
215    /// releases, and drags) and SGR encoding are always requested; these flags
216    /// add optional extras on top. An empty set ([`MouseTracking::empty()`])
217    /// means basic tracking with no extras.
218    ///
219    /// Mouse tracking is turned *off* through [`Program::disable_mouse`] or by
220    /// leaving [`ProgramOptions::mouse`] as `None`, not by an empty flag set.
221    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
222    pub struct MouseTracking: u8 {
223        /// Report pointer motion with no button held (any-event tracking).
224        /// Adds hover motion on terminals that support it.
225        const MOTION = 1 << 0;
226        /// Request pixel coordinates (SGR-pixel). Terminals that support it
227        /// report pixels; the rest fall back to SGR cell coordinates.
228        const PIXELS = 1 << 1;
229    }
230}
231
232/// A progress indication reported to the terminal with `OSC 9;4`, shown in
233/// the taskbar, tab, or window chrome by terminals that support it.
234///
235/// Set it with [`Program::set_progress_state`] and take it down with
236/// [`Program::reset_progress_state`]. Percentages are clamped to `0..=100`
237/// when emitted.
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
239pub enum ProgressState {
240    /// Determinate progress at the given percentage.
241    Normal(u8),
242    /// A failed operation, at the given percentage. Usually red.
243    Error(u8),
244    /// An operation needing attention, at the given percentage. Usually
245    /// yellow. Named "paused" in ConEmu, which originated the sequence.
246    Warning(u8),
247    /// Work in progress of unknown duration. Usually a pulsing bar.
248    Indeterminate,
249}
250
251impl ProgressState {
252    /// Emit the `OSC 9;4` sequence for this state.
253    fn write<W: Write>(self, w: &mut W) -> io::Result<()> {
254        match self {
255            ProgressState::Normal(p) => progress::write_set_progress_bar(w, p.into()),
256            ProgressState::Error(p) => progress::write_set_error_progress_bar(w, p.into()),
257            ProgressState::Warning(p) => progress::write_set_warning_progress_bar(w, p.into()),
258            ProgressState::Indeterminate => w.write_all(progress::SET_INDETERMINATE_PROGRESS_BAR),
259        }
260    }
261}
262
263impl Default for ProgramOptions {
264    fn default() -> Self {
265        Self {
266            bracketed_paste: true,
267            mouse: None,
268            prefer_grapheme_clusters: true,
269            prefer_in_band_resize: true,
270            prefer_synchronized_output: true,
271        }
272    }
273}
274
275impl<I, O> Program<I, O>
276where
277    I: Input,
278    O: Write,
279{
280    // --- The terminal ----------------------------------------------------
281
282    /// Borrow the [`Terminal`] this program drives.
283    ///
284    /// Shared on purpose: the program keeps ownership, so its record of the
285    /// modes and raw-mode state it emitted stays the authority for
286    /// [`finish`](Self::finish). Everything the program itself changes has a
287    /// method here. The borrow is not a seal, though. `Terminal::set_state`
288    /// takes `&self`, so a caller holding this can still change raw mode
289    /// behind the program's back, and the restore record will not know.
290    pub fn terminal(&self) -> &Terminal<I, O> {
291        &self.terminal
292    }
293
294    /// The environment the [`Terminal`] reads variables from.
295    ///
296    /// This is where the answers the terminal never sends live: `TERM`,
297    /// `COLORTERM`, `TERM_PROGRAM`, and the rest. Shorthand for
298    /// [`terminal().env()`](crate::terminal::Terminal::env), and the
299    /// counterpart to [`capabilities`](Self::capabilities), which holds only
300    /// what the terminal answered.
301    pub fn env(&self) -> &dyn crate::terminal::Env {
302        self.terminal.env()
303    }
304
305    // --- The screen ------------------------------------------------------
306
307    /// Borrow the [`Screen`] this program renders with.
308    pub fn screen(&self) -> &Screen<O> {
309        &self.screen
310    }
311
312    /// Borrow the [`Screen`] mutably — this is how you draw.
313    ///
314    /// ```no_run
315    /// # use uncurses::program::Program;
316    /// # use uncurses::style::Style;
317    /// # use uncurses::text::TextSurface;
318    /// # fn main() -> std::io::Result<()> {
319    /// # let mut program = Program::open()?;
320    /// let screen = program.screen_mut();
321    /// screen.set_str((0, 0), "hello", Style::default());
322    /// screen.render()?;
323    /// # Ok(())
324    /// # }
325    /// ```
326    ///
327    /// Drawing is the expected use. The screen's *render properties* are also
328    /// reachable here, and setting one directly moves only how frames are
329    /// drawn — it emits no mode, so the terminal never hears about it. Prefer
330    /// the program's own [`enter_alt_screen`](Self::enter_alt_screen),
331    /// [`hide_cursor`](Self::hide_cursor), and
332    /// [`enable_grapheme_clusters`](Self::enable_grapheme_clusters), which
333    /// emit the mode *and* move the property together. Teardown follows what
334    /// this program emitted, so a property changed behind its back is not
335    /// undone by [`finish`](Self::finish) and does not survive a
336    /// [`pause`](Self::pause) / [`resume`](Self::resume) round trip.
337    pub fn screen_mut(&mut self) -> &mut Screen<O> {
338        &mut self.screen
339    }
340
341    // --- Event delegates -------------------------------------------------
342
343    /// Drive the input source for up to `timeout`, returning whether any
344    /// event became available. See [`EventSource::poll`].
345    pub fn poll_event(&self, timeout: Option<Duration>) -> io::Result<bool> {
346        if !self.unread.is_empty() {
347            return Ok(true);
348        }
349        let ready = self.source.lock().unwrap().poll(timeout)?;
350        Ok(ready)
351    }
352
353    /// Take the next queued event without doing I/O, tracking capabilities as
354    /// it passes through. See [`EventSource::try_read`].
355    pub fn try_read_event(&mut self) -> io::Result<Option<Event>> {
356        if let Some(event) = self.unread.pop_front() {
357            return Ok(Some(event));
358        }
359        let Some(event) = self.source.lock().unwrap().try_read() else {
360            return Ok(None);
361        };
362        self.observe_event(&event)?;
363        Ok(Some(event))
364    }
365
366    /// Block until the next event, tracking capabilities as it passes
367    /// through. See [`EventSource::read`].
368    pub fn read_event(&mut self) -> io::Result<Event> {
369        if let Some(event) = self.unread.pop_front() {
370            return Ok(event);
371        }
372        let event = self.source.lock().unwrap().read()?;
373        self.observe_event(&event)?;
374        Ok(event)
375    }
376
377    /// Return an event to the front of the input queue, so the next
378    /// [`read_event`](Self::read_event) / [`try_read_event`](Self::try_read_event)
379    /// yields it before anything already queued. Restore a batch in original
380    /// order by unreading in reverse.
381    ///
382    /// The event was observed on its way out and is deliberately not observed
383    /// again on the way back in: a reply counts once, and observing it twice
384    /// would match it against two of the requests still in flight. These
385    /// events are therefore held by the program, not returned to the shared
386    /// [`EventSource`] — use [`EventSource::unread`] through
387    /// [`event_source`](Self::event_source) for events the program never saw.
388    pub fn unread_event(&mut self, event: Event) {
389        self.unread.push_front(event);
390    }
391
392    /// A shared handle to the input source behind
393    /// [`read_event`](Self::read_event) and friends, for driving input from a
394    /// separate reader over the same decoder rather than a second one racing
395    /// the same file descriptor.
396    ///
397    /// The main use is async input: build an
398    /// [`EventStream`](crate::event::EventStream) with
399    /// [`EventStream::from_shared`](crate::event::EventStream::from_shared) from
400    /// this handle and poll it on your executor.
401    ///
402    /// Events taken this way bypass the program, so capability tracking does
403    /// not run on them — feed each one to
404    /// [`observe_event`](Self::observe_event) yourself.
405    ///
406    /// Sharing one source between a live reader and the program's own
407    /// [`read_event`](Self::read_event) is best-effort: an event goes to
408    /// whichever consumer drains it first, so pick one reader in steady state.
409    pub fn event_source(&self) -> Arc<Mutex<EventSource<I>>> {
410        Arc::clone(&self.source)
411    }
412
413    /// Build an async [`EventStream`](crate::event::EventStream) over this
414    /// program's input, for reading events with `events.next().await` inside a
415    /// `select!` on any executor. The stream shares the program's decoder, so
416    /// it does not race a second reader on the same file descriptor.
417    ///
418    /// The stream hands back events directly, so — unlike
419    /// [`read_event`](Self::read_event) — capability tracking does not run.
420    /// Pass each event to [`observe_event`](Self::observe_event) to keep it
421    /// alive. Read through the stream *or* through `read_event` in steady
422    /// state, not both at once: a shared source hands each event to whichever
423    /// consumer drains it first.
424    #[cfg(feature = "async")]
425    pub fn event_stream(&self) -> crate::event::EventStream<I>
426    where
427        I: 'static,
428    {
429        crate::event::EventStream::from_shared(Arc::clone(&self.source))
430    }
431
432    // --- Capabilities and geometry ---------------------------------------
433
434    /// What the terminal has told us about itself so far, as the replies
435    /// themselves rather than a summary. Empty until the terminal has answered
436    /// something, whichever way the question was put:
437    /// [`query_capabilities`](Self::query_capabilities), an individual
438    /// `request_*` method, or a report the terminal sends unprompted, such as a
439    /// color-scheme change under DEC mode 2031.
440    pub fn capabilities(&self) -> &Capabilities {
441        &self.caps
442    }
443
444    /// Last observed full terminal size in cells, or `None` before the first
445    /// observation. This is the whole terminal, which inline differs from the
446    /// managed area returned by [`Screen::size`].
447    pub fn window_cells(&self) -> Option<Size> {
448        self.window_cells
449    }
450
451    /// Last observed full terminal size in pixels, or `None` when the
452    /// terminal has not reported one.
453    pub fn window_pixels(&self) -> Option<Size> {
454        self.window_pixels
455    }
456
457    /// Size of one character cell in pixels, or `None` when the terminal has
458    /// reported nothing to derive it from.
459    ///
460    /// Prefers the terminal's own `CSI 16 t` reply (see
461    /// [`request_cell_pixel_size`](Self::request_cell_pixel_size)) and
462    /// otherwise divides [`window_pixels`](Self::window_pixels) by
463    /// [`window_cells`](Self::window_cells), which only approximates it: the
464    /// window pixel size includes any padding the terminal draws around the
465    /// grid, so the quotient can be a pixel or two short.
466    ///
467    /// The `CSI 16 t` value is the last one the terminal reported, and it is
468    /// kept until another reply replaces it. A font-size change resizes the
469    /// cell without any reply, so call
470    /// [`request_cell_pixel_size`](Self::request_cell_pixel_size) again after
471    /// a resize to refresh it.
472    pub fn cell_pixels(&self) -> Option<Size> {
473        if let Some(cell) = self.cell_pixels.filter(|c| c.width > 0 && c.height > 0) {
474            return Some(cell);
475        }
476        let pixels = self.window_pixels?;
477        let cells = self.window_cells?;
478        if cells.width == 0 || cells.height == 0 {
479            return None;
480        }
481        let cell = Size::new(pixels.width / cells.width, pixels.height / cells.height);
482        (cell.width > 0 && cell.height > 0).then_some(cell)
483    }
484
485    /// The terminal's self-reported name from its XTVERSION reply (e.g.
486    /// `"XTerm(380)"`), or `None` when it has not answered. Shorthand for
487    /// [`Capabilities::terminal_name`].
488    pub fn terminal_name(&self) -> Option<&str> {
489        self.caps.terminal_name()
490    }
491
492    /// Convert a mouse event carrying pixel coordinates into cell
493    /// coordinates, using [`cell_pixels`](Self::cell_pixels). Returns `None`
494    /// when the cell size is unknown. It is not refreshed on its own: call
495    /// [`request_cell_pixel_size`](Self::request_cell_pixel_size) at startup,
496    /// and again after a resize or a font-size change.
497    pub fn mouse_pixels_to_cells(&self, mouse: crate::event::Mouse) -> Option<crate::event::Mouse> {
498        // A cell size the terminal reported is exact, so dividing by it lands
499        // in the right cell. The derived one is a truncated quotient and is
500        // narrower than the real cell whenever the pixel size is not an exact
501        // multiple of the grid, so dividing by that drifts right across the
502        // row and runs off the end. Scale across the grid in that case.
503        if let Some(cell) = self.cell_pixels.filter(|c| c.width > 0 && c.height > 0) {
504            return Some(crate::event::Mouse::new(
505                mouse.x / cell.width,
506                mouse.y / cell.height,
507                mouse.button,
508                mouse.modifiers,
509            ));
510        }
511        let pixels = self.window_pixels?;
512        let cells = self.window_cells?;
513        if pixels.width == 0 || pixels.height == 0 || cells.width == 0 || cells.height == 0 {
514            return None;
515        }
516        Some(crate::event::mouse_pixel_to_cell(
517            mouse,
518            pixels.width,
519            pixels.height,
520            cells.width,
521            cells.height,
522        ))
523    }
524
525    /// The tracked physical screen coordinate of the managed area's top-left
526    /// cell. Always `(0, 0)` in fullscreen. Inline it holds whatever the last
527    /// [`request_origin`](Self::request_origin) reply reported, and stays at
528    /// `(0, 0)` until you make that call.
529    pub fn origin(&self) -> Position {
530        if self.screen.fullscreen() {
531            Position::ORIGIN
532        } else {
533            self.origin
534        }
535    }
536
537    /// Translate a mouse event's screen coordinates into coordinates relative
538    /// to the managed area, by subtracting the tracked [`origin`](Self::origin).
539    /// A no-op in fullscreen, where the origin is `(0, 0)`, and inline until
540    /// [`request_origin`](Self::request_origin) has answered.
541    pub fn mouse_to_origin(&self, mouse: crate::event::Mouse) -> crate::event::Mouse {
542        let origin = self.origin();
543        crate::event::Mouse::new(
544            mouse.x.saturating_sub(origin.x),
545            mouse.y.saturating_sub(origin.y),
546            mouse.button,
547            mouse.modifiers,
548        )
549    }
550
551    /// Cache a fresh terminal size. Pure bookkeeping: nothing is written, and
552    /// the pixel dimensions are only updated when the size carried them. Some
553    /// platforms (the Windows console) report cell sizes only, leaving
554    /// [`window_pixels`](Self::window_pixels) at its last known value; call
555    /// [`request_window_pixel_size`](Self::request_window_pixel_size) to
556    /// refresh it.
557    fn cache_window_size(&mut self, ws: crate::terminal::Winsize) {
558        self.window_cells = Some(Size::new(ws.col, ws.row));
559        if ws.xpixel > 0 && ws.ypixel > 0 {
560            self.window_pixels = Some(Size::new(ws.xpixel, ws.ypixel));
561        }
562    }
563
564    /// Clip a queried origin so the whole managed area stays on screen: when
565    /// the area is shorter than the terminal, its top row sits no lower than
566    /// `terminal_height - area_height`.
567    fn clip_origin(&self, pos: Position) -> Position {
568        let height = self.screen.size().height;
569        let terminal_height = self.window_cells.map_or(height, |s| s.height);
570        let max_y = terminal_height.saturating_sub(height);
571        Position::new(pos.x, pos.y.min(max_y))
572    }
573
574    /// Apply an event to the program's capability tracking. The event is
575    /// inspected, never consumed.
576    ///
577    /// [`read_event`](Self::read_event) and
578    /// [`try_read_event`](Self::try_read_event) call this for you — you only
579    /// need it when you take events from somewhere else, namely the async
580    /// [`event_stream`](Self::event_stream) or a shared
581    /// [`event_source`](Self::event_source).
582    ///
583    /// Observe each event exactly once. A second call on an event a read
584    /// already observed is not harmless: replies are matched against the
585    /// requests still in flight, so observing one reply twice consumes two
586    /// requests and the answer to the second goes unrecorded.
587    ///
588    /// Capability-report replies to the queries you fire with
589    /// [`query_capabilities`](Self::query_capabilities) and the individual
590    /// `request_*` methods are recorded into
591    /// [`capabilities`](Self::capabilities), window-size reports update
592    /// [`window_cells`](Self::window_cells) /
593    /// [`window_pixels`](Self::window_pixels), and the render-affecting
594    /// reports are applied to the [`Screen`].
595    ///
596    /// A reply is recorded only when it says what it is about: a mode report
597    /// carries its mode, an XTGETTCAP reply its capability names, a palette
598    /// reply its index. The DECRPSS setting report
599    /// ([`Event::SettingReport`]) is the one reply that does not. An
600    /// unrecognized reply names nothing at all, and a success spells the
601    /// setting out as one CSI string, with the control function and its
602    /// parameters run together.
603    /// Which setting was asked about is knowable only from the DECRQSS
604    /// request, and that request is yours, so the report reaches you
605    /// unchanged and nothing is stored.
606    ///
607    /// Observing is otherwise passive, with one class of exception: a mode
608    /// report proving support for grapheme clusters or in-band resize enables
609    /// that mode when the matching
610    /// [`ProgramOptions`] `prefer_*` field is set, which writes to the
611    /// terminal. That adoption happens only while the application has taken no
612    /// position of its own: calling the mode's `enable_*` or `disable_*`
613    /// method, in either direction and at any point, settles it for good, and
614    /// adoption itself counts as settling it. So each mode is adopted at most
615    /// once, and never against an explicit choice.
616    ///
617    /// Observing never queries. Nothing here asks the terminal a question, so
618    /// no reply appears on the event stream that the application did not ask
619    /// for. Values the terminal only reports on request, such as the pixel
620    /// sizes and the inline [`origin`](Self::origin), go stale until you call
621    /// the matching `request_*` method.
622    ///
623    /// ```ignore
624    /// // Async loop: the stream bypasses the program, so observe explicitly.
625    /// use tokio_stream::StreamExt;
626    ///
627    /// let mut events = program.event_stream();
628    /// while let Some(ev) = events.next().await {
629    ///     let ev = ev?;
630    ///     program.observe_event(&ev)?;
631    ///     // ... handle ev ...
632    ///     program.screen_mut().render()?;
633    /// }
634    /// ```
635    pub fn observe_event(&mut self, event: &Event) -> io::Result<()> {
636        use crate::ansi::mode::Mode;
637        match *event {
638            Event::ModeReport { mode, setting } => {
639                // Record every report, including "not recognized": a definite
640                // no is information an app may want, and is not the same as
641                // the terminal staying silent.
642                self.caps.modes.insert(mode, setting);
643                // Adopt a preferred mode only while the application has taken
644                // no position on it. Calling enable_* or disable_* records the
645                // position, and adopting records it too, so a mode is adopted
646                // at most once and an explicit choice is never overridden --
647                // including one made before the terminal ever reported, when
648                // the mode field alone still reads as its default. The
649                // caller's options are only read, never rewritten.
650                if setting.is_available() && !self.state.chosen.contains(&mode) {
651                    match mode {
652                        // Render-affecting and free to adopt: the screen emits
653                        // the 2026 markers per frame, so knowing the terminal
654                        // understands them is all it takes. Turn it off for the
655                        // session with `prefer_synchronized_output`, or per
656                        // frame with [`Screen::set_synchronized_output`].
657                        Mode::SYNCHRONIZED_OUTPUT => {
658                            if self.options.prefer_synchronized_output {
659                                self.screen.set_synchronized_output(true);
660                            }
661                            self.state.chosen.insert(mode);
662                        }
663                        Mode::UNICODE_CORE if self.options.prefer_grapheme_clusters => {
664                            self.enable_grapheme_clusters()?;
665                        }
666                        Mode::IN_BAND_RESIZE if self.options.prefer_in_band_resize => {
667                            self.enable_in_band_resize()?;
668                        }
669                        _ => {}
670                    }
671                }
672            }
673            Event::KittyKeyboardEnhancements(flags) => self.caps.kitty_keyboard = Some(flags),
674            // Any modifyOtherKeys report (`CSI > 4 ; n m`) answers our
675            // query, so a reply means the terminal recognizes the feature.
676            Event::ModifyOtherKeys(mode) => self.caps.modify_other_keys = Some(mode),
677            Event::PrimaryDeviceAttributes(ref attrs) => {
678                // Stored unparsed: the attribute numbers are a terminal-author
679                // extension point, so callers test the ones they care about.
680                self.caps.primary_device_attributes = Some(attrs.clone());
681            }
682            Event::SecondaryDeviceAttributes(ref attrs) => {
683                self.caps.secondary_device_attributes = Some(attrs.clone());
684            }
685            Event::TertiaryDeviceAttributes(ref id) => {
686                self.caps.tertiary_device_attributes = Some(id.clone());
687            }
688            Event::TerminalName(ref report) => {
689                self.caps.terminal_name = Some(report.clone());
690            }
691            // A graphics response is the Kitty protocol's own support test:
692            // terminals that do not implement it stay silent.
693            Event::KittyGraphics { .. } => self.caps.kitty_graphics = true,
694            // The terminal's own colors, as distinct from the overrides the
695            // facade installs (tracked in `state`).
696            Event::ForegroundColor(color) => self.caps.foreground_color = Some(color),
697            Event::BackgroundColor(color) => self.caps.background_color = Some(color),
698            Event::CursorColor(color) => self.caps.cursor_color = Some(color),
699            Event::PaletteColor { index, color } => {
700                self.caps.palette.insert(index, color);
701            }
702            // Mode 2031 keeps sending these as the scheme changes, so the
703            // record is the current scheme rather than a one-time answer.
704            Event::ColorScheme(scheme) => self.caps.color_scheme = Some(scheme),
705            // Cache the full terminal size as it changes. Refitting the
706            // managed area is left to the app (call autoresize() as desired).
707            Event::Resize(ws) => {
708                self.cache_window_size(ws);
709            }
710            Event::WindowCellSize { width, height } => {
711                self.window_cells = Some(Size::new(width, height));
712            }
713            Event::WindowPixelSize { width, height } => {
714                self.window_pixels = Some(Size::new(width, height));
715            }
716            Event::CellPixelSize { width, height } => {
717                self.cell_pixels = Some(Size::new(width, height));
718            }
719            // Capture the reply to our own `request_origin`. Observing never
720            // consumes, so an application that also queries the cursor still
721            // sees this event.
722            Event::CursorPosition(pos) if self.origin_queries_pending > 0 => {
723                self.origin_queries_pending -= 1;
724                self.origin = self.clip_origin(pos);
725            }
726            Event::Termcap {
727                recognized,
728                ref entries,
729            } => {
730                // A failure reply echoes the requested names, so it is
731                // recorded as an explicit "not supported" rather than
732                // dropped.
733                for (name, value) in entries {
734                    self.caps.termcap.insert(
735                        name.clone(),
736                        recognized.then(|| value.clone().unwrap_or_default()),
737                    );
738                }
739                // A truecolor capability upgrades the renderer's profile.
740                // The profile, not this record, is the answer to "can I send
741                // 24-bit color": the environment can establish it just as
742                // well, without any terminal reply to record here.
743                if self.caps.supports_termcap("RGB") || self.caps.supports_termcap("Tc") {
744                    self.screen
745                        .set_color_profile(crate::color::Profile::TrueColor);
746                }
747            }
748            _ => {}
749        }
750        Ok(())
751    }
752
753    /// Enable [`TABS`](Optimizations::TABS) and [`BS`](Optimizations::BS),
754    /// the two optimizations raw mode makes safe: `\t` and `\x08` now
755    /// reach the terminal intact instead of being rewritten on the way.
756    ///
757    /// On Unix raw mode clears `OPOST`, disabling output processing
758    /// wholesale, so the kernel can no longer expand `\t` into spaces. On
759    /// Windows it enables virtual-terminal processing, which is the same
760    /// bargain. So this reads no terminal state: `make_raw` returning
761    /// `Ok` *is* the answer, and no `$TERM` baseline carries either flag,
762    /// so a program that never enters raw mode emits escape sequences
763    /// instead.
764    ///
765    /// [`ONLCR`](Optimizations::ONLCR) is deliberately untouched. Raw
766    /// mode makes it false, but it is opt-in: a caller who set it knows
767    /// something about their output path that we do not, and clobbering
768    /// that is worse than the bytes it would save.
769    ///
770    /// Runs after every successful `make_raw`, including
771    /// [`resume`](Self::resume), since whatever ran while paused may have
772    /// put the terminal back into cooked mode.
773    #[cfg(any(unix, windows))]
774    fn enable_tabs_and_bs(&mut self) {
775        let opts = self.screen.optimizations().with_tabs(true).with_bs(true);
776        self.screen.set_optimizations(opts);
777    }
778
779    /// Whether the host is Apple's `Terminal.app`, which does not support
780    /// most of the queried features and mishandles the queries themselves.
781    fn is_apple_terminal(&self) -> bool {
782        self.terminal.get_env("TERM_PROGRAM").as_deref() == Some("Apple_Terminal")
783    }
784
785    /// The major version of Apple's `Terminal.app`, parsed from
786    /// `TERM_PROGRAM_VERSION` (e.g. `"470"` or `"470.1"` yield `470`).
787    /// `None` when the variable is absent or not numeric.
788    fn apple_terminal_version(&self) -> Option<u32> {
789        let raw = self.terminal.get_env("TERM_PROGRAM_VERSION")?;
790        raw.split('.').next()?.trim().parse().ok()
791    }
792
793    /// Detect the environment-derived color profile and apply it to the
794    /// screen, clamping to no color when the output half is not a terminal
795    /// (e.g. redirected to a file or pipe). `is_tty` is the output's
796    /// terminal status; the caller supplies it since the platform handle
797    /// bounds live on `init_with`.
798    fn apply_env_color_profile(&mut self, is_tty: bool) {
799        let profile = Profile::detect_from(self.terminal.env(), is_tty);
800        self.screen.set_color_profile(profile);
801    }
802
803    /// Reconcile the terminal's hardware tab stops with the every-eight
804    /// columns layout the renderer assumes. A prior program may have left
805    /// arbitrary stops behind, which would make the `HT` (`\t`) moves the
806    /// cursor planner emits land on the wrong columns. Modern terminals
807    /// reset in one cursor-safe write via DECST8C; the rest get the
808    /// portable TBC-then-HTS fallback. Staged and flushed so it reaches
809    /// the terminal even when capability queries are disabled.
810    ///
811    /// Runs whether or not `TABS` is set: the stops belong to the
812    /// terminal, not to our willingness to use them, so turning `TABS` on
813    /// later must not find them unknown.
814    fn reset_tab_stops(&mut self) -> io::Result<()> {
815        if Optimizations::supports_decst8c(self.terminal.env()) {
816            self.screen
817                .write_all(crate::ansi::screen::SET_TAB_EVERY_8_COLUMNS)?;
818        } else {
819            let width = self.screen.size().width;
820            crate::ansi::screen::write_reset_tab_stops_every_8(&mut self.screen, width)?;
821        }
822        self.screen.flush()
823    }
824
825    /// Reset LNM (ANSI mode 20) so a `\n` moves the cursor down without
826    /// touching the column.
827    ///
828    /// The cursor planner emits a bare `\n` for downward moves and, unless
829    /// [`Optimizations::ONLCR`] says the host's line discipline expands it,
830    /// carries the column across unchanged. A terminal left in LNM by a prior
831    /// program breaks that assumption on the terminal's side of the wire:
832    /// LNM makes a *received* LF return to column 1, so every horizontal leg
833    /// planned after a `\n` starts from a column the cursor is not in.
834    ///
835    /// No query first. LNM reset is the documented default — the VT510
836    /// manual asks that it be kept reset — so there is nothing to learn from
837    /// asking, and this follows [`reset_tab_stops`](Self::reset_tab_stops) in
838    /// imposing the state the planner assumes rather than trusting what it
839    /// inherited. Reset on every raw-mode entry, since a program run during a
840    /// [`pause`](Self::pause) can set it while we are not looking.
841    ///
842    /// Staged and flushed so it reaches the terminal even when capability
843    /// queries are disabled.
844    fn reset_lnm(&mut self) -> io::Result<()> {
845        mode::Mode::LINE_FEED_NEW_LINE.reset(&mut self.screen)?;
846        self.screen.flush()
847    }
848
849    /// Hand the terminal back: reset every staged mode and the managed area to defaults, and flush. The
850    /// caller restores the saved raw-mode state afterward.
851    fn teardown(&mut self) -> io::Result<()> {
852        self.reset()?;
853        self.screen.flush()
854    }
855}
856
857impl<I, O> Program<I, O>
858where
859    I: Input + Copy,
860    O: Write + Copy,
861{
862    /// Build the screen and event source over `terminal`, sizing the managed
863    /// area to `size`. The color profile and renderer optimizations are
864    /// detected from the terminal's environment. The terminal is
865    /// left as-is.
866    fn with_render(terminal: Terminal<I, O>, size: (u16, u16)) -> io::Result<Self> {
867        let env = terminal.env();
868        // Provisional profile; init_with reapplies it with the real
869        // output-is-tty signal via apply_env_color_profile.
870        let color_profile = Profile::detect_from(env, true);
871        let optimizations = Optimizations::from_env(env);
872        let mut screen = Screen::new(terminal.output(), size);
873        screen.set_color_profile(color_profile);
874        screen.set_optimizations(optimizations);
875
876        let source = Arc::new(Mutex::new(EventSource::new(terminal.input())?));
877        Ok(Self {
878            terminal,
879            screen,
880            source,
881            unread: VecDeque::new(),
882            state: state::State::default(),
883            caps: Capabilities::default(),
884            options: ProgramOptions::default(),
885            window_cells: None,
886            window_pixels: None,
887            cell_pixels: None,
888            origin: Position::ORIGIN,
889            origin_queries_pending: 0,
890        })
891    }
892
893    /// Probe the terminal for its capabilities, then flush.
894    ///
895    /// A [`Program`] never queries the terminal on its own — call this when
896    /// you want [`capabilities`](Self::capabilities) populated. It writes the
897    /// default query set (Kitty keyboard, the DECRQM modes behind
898    /// [`Capabilities`], XTVERSION, xterm modifyOtherKeys, and — when the
899    /// environment did not already imply true color — XTGETTCAP `RGB`/`Tc`),
900    /// then `extra`, then a Primary DA request.
901    ///
902    /// `extra` is written verbatim, so it can carry any additional query
903    /// escapes you want answered under the same Primary DA terminator. Pass
904    /// `&[]` for none.
905    ///
906    /// The DECRQM, XTVERSION, and XTGETTCAP queries are skipped on Apple's
907    /// `Terminal.app`, which mishandles them. Nothing is recorded in their
908    /// place: [`capabilities`](Self::capabilities) keeps reporting only what
909    /// the terminal actually said. Its known direct-color support is applied
910    /// to the renderer's color profile alone.
911    ///
912    /// # Draining the replies is yours
913    ///
914    /// This method only *writes*. The replies arrive asynchronously as
915    /// ordinary events, and reading them is the caller's job — nothing here
916    /// waits. Primary DA is sent last precisely so its reply terminates the
917    /// stream: read events until [`Event::PrimaryDeviceAttributes`] lands and
918    /// every earlier reply has necessarily arrived, at which point
919    /// [`capabilities`](Self::capabilities) is complete.
920    ///
921    /// If you never read that far, the unread replies are still sitting in the
922    /// input buffer when you restore the terminal, and the shell will see them
923    /// as typed input. A terminal that answers nothing never sends the Primary
924    /// DA reply either, so bound the wait yourself with
925    /// [`poll_event`](Self::poll_event).
926    ///
927    /// ```no_run
928    /// # use uncurses::{program::Program, event::Event};
929    /// # use std::time::{Duration, Instant};
930    /// # fn main() -> std::io::Result<()> {
931    /// let mut program = Program::stdio()?;
932    /// program.init()?;
933    /// program.query_capabilities(&[])?;
934    ///
935    /// let deadline = Instant::now() + Duration::from_millis(300);
936    /// while let Some(timeout) = deadline.checked_duration_since(Instant::now()) {
937    ///     if !program.poll_event(Some(timeout))? {
938    ///         break;
939    ///     }
940    ///     if matches!(program.try_read_event()?, Some(Event::PrimaryDeviceAttributes(_))) {
941    ///         break;
942    ///     }
943    /// }
944    /// let caps = program.capabilities();
945    /// # program.finish()
946    /// # }
947    /// ```
948    ///
949    /// [`Event::PrimaryDeviceAttributes`]: crate::event::Event::PrimaryDeviceAttributes
950    pub fn query_capabilities(&mut self, extra: &[u8]) -> io::Result<()> {
951        use crate::ansi::ctrl::{REQUEST_PRIMARY_DA, REQUEST_XTVERSION};
952        use crate::ansi::kitty::REQUEST_KITTY_KEYBOARD;
953        use crate::ansi::mode::Mode;
954        use crate::ansi::termcap::write_xtgettcap;
955
956        // The env-derived profile is already applied by init_with via
957        // apply_env_color_profile; read it back to decide whether there is
958        // headroom to upgrade via XTGETTCAP.
959        let profile = self.screen.color_profile();
960
961        // Always-safe queries.
962        self.screen.write_all(REQUEST_KITTY_KEYBOARD)?;
963
964        if !self.is_apple_terminal() {
965            for mode in [
966                Mode::SYNCHRONIZED_OUTPUT,
967                Mode::UNICODE_CORE,
968                Mode::IN_BAND_RESIZE,
969                Mode::VISIBILITY_REPORTS,
970                Mode::MOUSE_NORMAL,
971                Mode::MOUSE_BUTTON,
972                Mode::MOUSE_ANY,
973                Mode::MOUSE_SGR,
974                Mode::MOUSE_SGR_PIXEL,
975            ] {
976                mode.request(&mut self.screen)?;
977            }
978            self.screen.write_all(REQUEST_XTVERSION)?;
979            self.screen
980                .write_all(crate::ansi::xterm::QUERY_MODIFY_OTHER_KEYS)?;
981            if profile < Profile::TrueColor {
982                // One key per query: some terminals only answer the first
983                // capability when several are batched in a single request.
984                write_xtgettcap(&mut self.screen, &["RGB"])?;
985                write_xtgettcap(&mut self.screen, &["Tc"])?;
986            }
987        } else {
988            // Terminal.app gained direct-color support in the build shipped
989            // with macOS Tahoe. It does not answer capability queries, so the
990            // renderer is upgraded from the version alone; `capabilities()`
991            // keeps reporting only what the terminal actually said.
992            if profile < Profile::TrueColor
993                && self.apple_terminal_version().is_some_and(|v| v >= 470)
994            {
995                self.screen.set_color_profile(Profile::TrueColor);
996            }
997        }
998
999        self.screen.write_all(extra)?;
1000        self.screen.write_all(REQUEST_PRIMARY_DA)?;
1001        self.screen.flush()
1002    }
1003}
1004
1005#[cfg(unix)]
1006impl<I, O> Program<I, O>
1007where
1008    I: Input + Copy + std::os::fd::AsFd,
1009    O: Write + Copy + std::os::fd::AsFd,
1010{
1011    /// Construct a program over `terminal` without touching the terminal:
1012    /// size the screen to it and create an [`EventSource`] on its input
1013    /// half. The terminal is left as-is; call [`Self::init`] to enter raw
1014    /// mode and begin a session.
1015    pub fn new(terminal: Terminal<I, O>) -> io::Result<Self> {
1016        let ws = terminal.get_window_size()?;
1017        Self::with_render(terminal, (ws.col, ws.row))
1018    }
1019
1020    /// Begin a session with the default [`ProgramOptions`]. See
1021    /// [`Self::init_with`].
1022    pub fn init(&mut self) -> io::Result<()> {
1023        self.init_with(ProgramOptions::default())
1024    }
1025
1026    /// Begin a session: enter raw mode and apply the always-on defaults from
1027    /// `options`. This never probes the terminal; the `prefer_*` defaults
1028    /// stay dormant until you call
1029    /// [`query_capabilities`](Self::query_capabilities) and read the replies.
1030    /// Call once after [`Self::new`], before rendering.
1031    pub fn init_with(&mut self, options: ProgramOptions) -> io::Result<()> {
1032        self.options = options;
1033        self.terminal.make_raw()?;
1034        self.enable_tabs_and_bs();
1035        self.reset_lnm()?;
1036        self.autoresize()?;
1037        // Apply the env color profile on every path so output downsamples
1038        // correctly even when capability queries are skipped. Disable color
1039        // when the output is not a terminal (redirected to a file or pipe).
1040        let is_tty = self.terminal.is_terminal().1;
1041        self.apply_env_color_profile(is_tty);
1042        self.reset_tab_stops()?;
1043        if self.options.bracketed_paste {
1044            self.enable_bracketed_paste()?;
1045        }
1046        if let Some(tracking) = self.options.mouse {
1047            self.enable_mouse(tracking)?;
1048        }
1049        Ok(())
1050    }
1051
1052    /// Query the current terminal window size (output half first, input as
1053    /// fallback). This is a live query; the cached
1054    /// [`window_cells`](Self::window_cells) /
1055    /// [`window_pixels`](Self::window_pixels) accessors return the
1056    /// last-observed values without I/O.
1057    pub fn get_window_size(&self) -> io::Result<crate::terminal::Winsize> {
1058        self.terminal.get_window_size()
1059    }
1060
1061    /// Re-query the terminal size and resize the managed area to fit: the full
1062    /// terminal size when fullscreen, or the terminal width with the current
1063    /// managed height preserved when inline. Refreshes the cached
1064    /// [`window_cells`](Self::window_cells), and
1065    /// [`window_pixels`](Self::window_pixels) when the platform reports pixel
1066    /// dimensions. Nothing is asked of the terminal: this reads the size the
1067    /// operating system already knows. On platforms whose size query carries
1068    /// no pixel dimensions (the Windows console), refresh those with
1069    /// [`request_window_pixel_size`](Self::request_window_pixel_size).
1070    ///
1071    /// When the managed area already fits the queried size, this returns
1072    /// without resizing and without repainting, so it is safe to call on every
1073    /// resize report. Terminals send a report per pixel of a window drag while
1074    /// the cell grid only changes at cell boundaries, so most reports ask for a
1075    /// size the area already has. To re-establish the area whatever the size,
1076    /// call [`Screen::resize`](crate::screen::Screen::resize) instead.
1077    pub fn autoresize(&mut self) -> io::Result<()> {
1078        let Ok(ws) = self.terminal.get_window_size() else {
1079            // Keep the current size when the query fails rather than
1080            // collapsing the managed area to zero.
1081            return Ok(());
1082        };
1083        self.cache_window_size(ws);
1084        let height = match self.screen.fullscreen() {
1085            true => ws.row,
1086            false => self.screen.size().height,
1087        };
1088        // Terminals report resizes for changes that leave the cell grid alone
1089        // (a font change that keeps rows and columns, a window move on some
1090        // terminals). This runs on every one of them, and `Screen::resize`
1091        // repaints unconditionally, so skip the ones that change nothing
1092        // rather than flickering on each. A caller that wants the repaint
1093        // anyway calls `Screen::resize` directly.
1094        if self.screen.size() == crate::layout::Size::new(ws.col, height) {
1095            return Ok(());
1096        }
1097        self.screen.resize((ws.col, height));
1098        Ok(())
1099    }
1100
1101    /// Consume the program and hand the terminal back to the shell: tear down
1102    /// every staged mode, reset the managed area, flush, and restore the
1103    /// terminal's prior state.
1104    ///
1105    /// The terminal state is restored even when the teardown writes fail, so a
1106    /// broken pipe cannot leave the terminal in raw mode. The teardown error is
1107    /// still returned.
1108    pub fn finish(mut self) -> io::Result<()> {
1109        // Restore even when teardown fails. Teardown writes to the output
1110        // half, so a broken pipe or a closed terminal fails it routinely, and
1111        // returning early there would leave the terminal raw. `finish` consumes
1112        // the program, so there would be nothing left to retry with.
1113        let teardown = self.teardown();
1114        let restore = self.terminal.restore();
1115        teardown.and(restore)
1116    }
1117
1118    /// Hand the terminal back to the shell without consuming the program,
1119    /// e.g. to run a child process. Re-enter with [`Self::resume`]. Like
1120    /// [`Self::finish`] but keeps the program so the session can continue, and
1121    /// likewise restores the terminal even when the teardown writes fail.
1122    pub fn pause(&mut self) -> io::Result<()> {
1123        // Restore even when teardown fails, for the same reason as `finish`:
1124        // the caller asked for the terminal back, and a failed write to it is
1125        // not a reason to keep it raw.
1126        let teardown = self.teardown();
1127        let restore = self.terminal.restore();
1128        teardown.and(restore)
1129    }
1130
1131    /// Re-acquire the terminal after a [`Self::pause`] or [`Self::suspend`]:
1132    /// re-enter raw mode, refit the managed area to the current viewport, re-apply
1133    /// the saved render state and modes, and force a full repaint.
1134    ///
1135    /// Re-enables [`TABS`](Optimizations::TABS) and
1136    /// [`BS`](Optimizations::BS) and resets the hardware tab stops, since
1137    /// whatever ran while paused may have disturbed both.
1138    pub fn resume(&mut self) -> io::Result<()> {
1139        self.terminal.make_raw()?;
1140        self.enable_tabs_and_bs();
1141        self.reset_lnm()?;
1142        self.autoresize()?;
1143        self.reset_tab_stops()?;
1144        self.restore()?;
1145        self.screen.invalidate();
1146        self.screen.flush()
1147    }
1148
1149    /// Suspend the process: [`pause`](Self::pause) the program, then stop
1150    /// the process with `SIGTSTP`. Returns once the process is
1151    /// foregrounded again; the caller should then call [`Self::resume`].
1152    pub fn suspend(&mut self) -> io::Result<()> {
1153        self.pause()?;
1154        // SAFETY: raise is async-signal-safe.
1155        unsafe { libc::raise(libc::SIGTSTP) };
1156        Ok(())
1157    }
1158}
1159
1160#[cfg(windows)]
1161impl<I, O> Program<I, O>
1162where
1163    I: Input + Copy + std::os::windows::io::AsHandle,
1164    O: Write + Copy + std::os::windows::io::AsHandle,
1165{
1166    /// Construct a program over `terminal` without touching the terminal:
1167    /// size the screen to it and create an [`EventSource`] on its input
1168    /// half. The terminal is left as-is; call [`Self::init`] to enter raw
1169    /// mode and begin a session.
1170    pub fn new(terminal: Terminal<I, O>) -> io::Result<Self> {
1171        let ws = terminal.get_window_size()?;
1172        Self::with_render(terminal, (ws.col, ws.row))
1173    }
1174
1175    /// Begin a session with the default [`ProgramOptions`]. See
1176    /// [`Self::init_with`].
1177    pub fn init(&mut self) -> io::Result<()> {
1178        self.init_with(ProgramOptions::default())
1179    }
1180
1181    /// Begin a session: enter raw mode and apply the always-on defaults from
1182    /// `options`. This never probes the terminal; the `prefer_*` defaults
1183    /// stay dormant until you call
1184    /// [`query_capabilities`](Self::query_capabilities) and read the replies.
1185    /// Call once after [`Self::new`], before rendering.
1186    pub fn init_with(&mut self, options: ProgramOptions) -> io::Result<()> {
1187        self.options = options;
1188        self.terminal.make_raw()?;
1189        self.enable_tabs_and_bs();
1190        self.reset_lnm()?;
1191        self.autoresize()?;
1192        // Apply the env color profile on every path so output downsamples
1193        // correctly even when capability queries are skipped. Disable color
1194        // when the output is not a terminal (redirected to a file or pipe).
1195        let is_tty = self.terminal.is_terminal().1;
1196        self.apply_env_color_profile(is_tty);
1197        self.reset_tab_stops()?;
1198        if self.options.bracketed_paste {
1199            self.enable_bracketed_paste()?;
1200        }
1201        if let Some(tracking) = self.options.mouse {
1202            self.enable_mouse(tracking)?;
1203        }
1204        Ok(())
1205    }
1206
1207    /// Query the current terminal window size (output half first, input as
1208    /// fallback). This is a live query; the cached
1209    /// [`window_cells`](Self::window_cells) /
1210    /// [`window_pixels`](Self::window_pixels) accessors return the
1211    /// last-observed values without I/O.
1212    pub fn get_window_size(&self) -> io::Result<crate::terminal::Winsize> {
1213        self.terminal.get_window_size()
1214    }
1215
1216    /// Re-query the terminal size and resize the managed area to fit: the full
1217    /// terminal size when fullscreen, or the terminal width with the current
1218    /// managed height preserved when inline. Refreshes the cached
1219    /// [`window_cells`](Self::window_cells), and
1220    /// [`window_pixels`](Self::window_pixels) when the platform reports pixel
1221    /// dimensions. Nothing is asked of the terminal: this reads the size the
1222    /// operating system already knows. On platforms whose size query carries
1223    /// no pixel dimensions (the Windows console), refresh those with
1224    /// [`request_window_pixel_size`](Self::request_window_pixel_size).
1225    ///
1226    /// When the managed area already fits the queried size, this returns
1227    /// without resizing and without repainting, so it is safe to call on every
1228    /// resize report. Terminals send a report per pixel of a window drag while
1229    /// the cell grid only changes at cell boundaries, so most reports ask for a
1230    /// size the area already has. To re-establish the area whatever the size,
1231    /// call [`Screen::resize`](crate::screen::Screen::resize) instead.
1232    pub fn autoresize(&mut self) -> io::Result<()> {
1233        let Ok(ws) = self.terminal.get_window_size() else {
1234            // Keep the current size when the query fails rather than
1235            // collapsing the managed area to zero.
1236            return Ok(());
1237        };
1238        self.cache_window_size(ws);
1239        let height = match self.screen.fullscreen() {
1240            true => ws.row,
1241            false => self.screen.size().height,
1242        };
1243        // Terminals report resizes for changes that leave the cell grid alone
1244        // (a font change that keeps rows and columns, a window move on some
1245        // terminals). This runs on every one of them, and `Screen::resize`
1246        // repaints unconditionally, so skip the ones that change nothing
1247        // rather than flickering on each. A caller that wants the repaint
1248        // anyway calls `Screen::resize` directly.
1249        if self.screen.size() == crate::layout::Size::new(ws.col, height) {
1250            return Ok(());
1251        }
1252        self.screen.resize((ws.col, height));
1253        Ok(())
1254    }
1255
1256    /// Consume the program and hand the terminal back to the shell: tear down
1257    /// every staged mode, reset the managed area, flush, and restore the
1258    /// terminal's prior state.
1259    ///
1260    /// The terminal state is restored even when the teardown writes fail, so a
1261    /// broken pipe cannot leave the terminal in raw mode. The teardown error is
1262    /// still returned.
1263    pub fn finish(mut self) -> io::Result<()> {
1264        let teardown = self.teardown();
1265        let restore = self.terminal.restore();
1266        teardown.and(restore)
1267    }
1268
1269    /// Hand the terminal back to the shell without consuming the program,
1270    /// e.g. to run a child process. Re-enter with [`Self::resume`].
1271    pub fn pause(&mut self) -> io::Result<()> {
1272        let teardown = self.teardown();
1273        let restore = self.terminal.restore();
1274        teardown.and(restore)
1275    }
1276
1277    /// Re-acquire the terminal after a [`Self::pause`]: re-enter raw mode,
1278    /// refit the managed area to the current viewport, re-apply the saved
1279    /// render state and modes, and force a full repaint.
1280    ///
1281    /// Re-enables [`TABS`](Optimizations::TABS) and
1282    /// [`BS`](Optimizations::BS) and resets the hardware tab stops, since
1283    /// whatever ran while paused may have disturbed both.
1284    pub fn resume(&mut self) -> io::Result<()> {
1285        self.terminal.make_raw()?;
1286        self.enable_tabs_and_bs();
1287        self.reset_lnm()?;
1288        self.autoresize()?;
1289        self.reset_tab_stops()?;
1290        self.restore()?;
1291        self.screen.invalidate();
1292        self.screen.flush()
1293    }
1294}
1295
1296impl Program<crate::terminal::Stdin, crate::terminal::Stdout> {
1297    /// Build a program over the process stdio (`stdin` + `stdout`).
1298    pub fn stdio() -> io::Result<Self> {
1299        Self::new(Terminal::stdio())
1300    }
1301}
1302
1303impl Program<crate::terminal::TtyInput, crate::terminal::TtyOutput> {
1304    /// Build a program over the controlling terminal (`/dev/tty`, or
1305    /// `CONIN$`/`CONOUT$` on Windows), useful when stdio is redirected.
1306    pub fn open() -> io::Result<Self> {
1307        Self::new(Terminal::open()?)
1308    }
1309}