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