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}