uncurses/terminal/handle.rs
1//! [`Terminal`] — a typed input/output handle with raw-mode state.
2//!
3//! A `Terminal<I, O>` bundles a readable input half, a writable output half,
4//! an [`Env`], and one optional saved raw-mode [`State`]. It implements
5//! [`Read`] and [`Write`] by delegating to those halves, so it can be used
6//! directly for byte-level terminal control or split into halves for a
7//! renderer and an event source.
8//!
9//! ## Raw-mode ownership
10//!
11//! `Terminal` is not `Copy` because it owns the saved state used by
12//! [`restore`](Terminal::restore). [`make_raw`](Terminal::make_raw) stores the
13//! pre-raw state inside the handle and returns a clone to the caller.
14//! [`restore`](Terminal::restore) applies and clears that cached state. There
15//! is no `Drop` restoration; callers must restore explicitly.
16//!
17//! ## Choosing handles
18//!
19//! [`Terminal::stdio`] uses inherited stdin/stdout. [`Terminal::open`] opens
20//! the controlling terminal directly, which keeps terminal I/O available when
21//! stdio is redirected. For custom handles, [`Terminal::new`] pairs any
22//! platform terminal input/output types with an explicit environment.
23//!
24//! ```rust,ignore
25//! use std::io::Write;
26//! use uncurses::buffer::TextBuffer;
27//! use uncurses::event::EventSource;
28//! use uncurses::terminal::Terminal;
29//! use uncurses::text::Encode;
30//!
31//! let mut term = Terminal::open()?;
32//! let _saved = term.make_raw()?;
33//! let size = term.get_window_size()?;
34//! let mut frame = TextBuffer::new(size.col, size.row);
35//! let mut source = EventSource::new(term.input())?;
36//!
37//! // Paint `frame`, read events from `source`.
38//! frame.encode(&mut term.output())?;
39//! term.restore()?;
40//! # Ok::<(), std::io::Error>(())
41//! ```
42
43use std::io::{self, Read, Write};
44
45#[cfg(unix)]
46use std::os::fd::{AsFd, BorrowedFd};
47#[cfg(windows)]
48use std::os::windows::io::{AsHandle, BorrowedHandle};
49
50use super::env::{Env, ProcessEnv};
51use super::raw::{self, State};
52use super::size::{Winsize, get_window_size};
53use super::stdio::{Stdin, Stdout, stdin, stdout};
54use super::tty::{TtyInput, TtyOutput, open_tty};
55
56/// Owned terminal handle pairing input, output, environment, and raw-mode
57/// state.
58///
59/// `Terminal` implements [`Read`] from `I` and [`Write`] to `O`. The handle is
60/// generic so callers can use inherited stdio, the controlling terminal, or
61/// test doubles, while sharing one raw-mode and window-size API on supported
62/// platforms.
63///
64/// [`EventSource`]: crate::event::EventSource
65pub struct Terminal<I, O> {
66 input: I,
67 output: O,
68 /// State captured by the most recent [`make_raw`](Self::make_raw),
69 /// applied (and cleared) by [`restore`](Self::restore).
70 saved: Option<State>,
71 env: Box<dyn Env>,
72}
73
74impl Terminal<Stdin, Stdout> {
75 /// Create a terminal over inherited standard input and output.
76 ///
77 /// The returned handle uses [`stdin`] for input, [`stdout`] for output, and
78 /// [`ProcessEnv`] for its environment. Use this when the
79 /// process is expected to be connected directly to the terminal.
80 ///
81 /// # Returns
82 ///
83 /// A `Terminal<Stdin, Stdout>` with no saved raw-mode state.
84 ///
85 /// # Errors and panics
86 ///
87 /// This constructor does not fail or intentionally panic.
88 ///
89 /// # Usage note
90 ///
91 /// If stdin or stdout may be redirected, prefer [`Terminal::open`] to open
92 /// the controlling terminal directly.
93 pub fn stdio() -> Self {
94 Self::new(stdin(), stdout(), ProcessEnv)
95 }
96}
97
98impl Terminal<TtyInput, TtyOutput> {
99 /// Open the controlling terminal directly.
100 ///
101 /// On Unix this opens `/dev/tty` for both input and output. On Windows it
102 /// opens `CONIN$` for input and `CONOUT$` for output. The returned
103 /// `Terminal` reads the live process environment through [`ProcessEnv`].
104 ///
105 /// # Returns
106 ///
107 /// A `Terminal<TtyInput, TtyOutput>` backed by the controlling terminal.
108 ///
109 /// # Errors
110 ///
111 /// Returns the error from `open_tty` if the process has no controlling
112 /// terminal or if the platform device cannot be opened.
113 ///
114 /// # Panics
115 ///
116 /// This function does not intentionally panic.
117 pub fn open() -> io::Result<Self> {
118 let (input, output) = open_tty()?;
119 Ok(Self::new(input, output, ProcessEnv))
120 }
121}
122
123impl<I, O> Terminal<I, O> {
124 /// Build a terminal directly from its parts, without touching any fd.
125 ///
126 /// Test-only constructor used to assemble a [`Terminal`] over in-memory
127 /// or non-tty handles (for example a `Vec<u8>` output) where the
128 /// fd-bound [`new`](Self::new) cannot apply. No raw-mode state is
129 /// captured.
130 #[cfg(test)]
131 pub(crate) fn from_parts(input: I, output: O, env: impl Env + 'static) -> Self {
132 Self {
133 input,
134 output,
135 saved: None,
136 env: Box::new(env),
137 }
138 }
139
140 /// Return the terminal's environment.
141 ///
142 /// Whether lookups see later changes to the process environment depends on
143 /// the [`Env`] the terminal was built with.
144 ///
145 /// # Returns
146 ///
147 /// A shared reference to the terminal's [`Env`].
148 ///
149 /// # Errors and panics
150 ///
151 /// This method does not fail or intentionally panic.
152 pub fn env(&self) -> &dyn Env {
153 self.env.as_ref()
154 }
155
156 /// Look up an environment variable.
157 ///
158 /// # Parameters
159 ///
160 /// * `key` — environment variable name.
161 ///
162 /// # Returns
163 ///
164 /// The value for `key`, or `None` if it is absent.
165 ///
166 /// # Errors and panics
167 ///
168 /// This method does not fail or intentionally panic.
169 pub fn get_env(&self, key: &str) -> Option<String> {
170 self.env.get(key)
171 }
172
173 /// Return whether an environment variable is present and non-empty.
174 ///
175 /// # Parameters
176 ///
177 /// * `key` — environment variable name.
178 ///
179 /// # Returns
180 ///
181 /// `true` when `key` is present with a non-empty value.
182 ///
183 /// # Errors and panics
184 ///
185 /// This method does not fail or intentionally panic.
186 pub fn has_env(&self, key: &str) -> bool {
187 self.env.has(key)
188 }
189
190 /// Return a copy of the input half.
191 ///
192 /// This is available only when `I: Copy`, which is true for the standard
193 /// and controlling-terminal handle types provided by this module. Use it to
194 /// pass input to [`EventSource::new`](crate::event::EventSource::new) while
195 /// retaining the `Terminal` for raw-mode restoration.
196 ///
197 /// # Returns
198 ///
199 /// A copy of the input handle.
200 ///
201 /// # Errors and panics
202 ///
203 /// This method does not fail or intentionally panic.
204 pub fn input(&self) -> I
205 where
206 I: Copy,
207 {
208 self.input
209 }
210
211 /// Return a copy of the output half.
212 ///
213 /// This is available only when `O: Copy`, which is true for the standard
214 /// and controlling-terminal output types provided by this module. Use it to
215 /// pass output to a renderer such as [`TextBuffer`](crate::buffer::TextBuffer) while
216 /// retaining the `Terminal` for raw-mode restoration.
217 ///
218 /// # Returns
219 ///
220 /// A copy of the output handle.
221 ///
222 /// # Errors and panics
223 ///
224 /// This method does not fail or intentionally panic.
225 pub fn output(&self) -> O
226 where
227 O: Copy,
228 {
229 self.output
230 }
231
232 /// Consume the terminal and return its input and output halves.
233 ///
234 /// Any cached raw-mode state is dropped without being applied. Restore
235 /// before calling this method if the terminal is in raw mode.
236 ///
237 /// # Returns
238 ///
239 /// `(input, output)`.
240 ///
241 /// # Errors and panics
242 ///
243 /// This method does not fail or intentionally panic.
244 pub fn into_halves(self) -> (I, O) {
245 (self.input, self.output)
246 }
247}
248
249impl<I: Read, O> Read for Terminal<I, O> {
250 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
251 self.input.read(buf)
252 }
253}
254
255impl<I, O: Write> Write for Terminal<I, O> {
256 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
257 self.output.write(buf)
258 }
259
260 fn flush(&mut self) -> io::Result<()> {
261 self.output.flush()
262 }
263}
264
265#[cfg(unix)]
266impl<I: AsFd, O> AsFd for Terminal<I, O> {
267 /// Borrow the input descriptor.
268 ///
269 /// This exposes the input half, not the output half. Use
270 /// [`output`](Self::output) and borrow that handle when an output
271 /// descriptor is required.
272 fn as_fd(&self) -> BorrowedFd<'_> {
273 self.input.as_fd()
274 }
275}
276
277#[cfg(windows)]
278impl<I: AsHandle, O> AsHandle for Terminal<I, O> {
279 /// Borrow the input handle.
280 ///
281 /// This exposes the input half, not the output half. Use
282 /// [`output`](Self::output) and borrow that handle when an output handle is
283 /// required.
284 fn as_handle(&self) -> BorrowedHandle<'_> {
285 self.input.as_handle()
286 }
287}
288
289#[cfg(unix)]
290impl<I: AsFd, O: AsFd> Terminal<I, O> {
291 /// Build a terminal from input and output descriptors plus an [`Env`].
292 ///
293 /// The environment is stored exactly as provided and is not required to
294 /// describe the given descriptors. Use [`Terminal::stdio`] or
295 /// [`Terminal::open`] to use the process environment automatically.
296 ///
297 /// The two descriptors need not refer to the same device. Raw mode is
298 /// applied to and restored from each one independently, so pairing two
299 /// different terminals configures both, and pairing a terminal with a pipe
300 /// configures only the terminal half.
301 ///
302 /// # Parameters
303 ///
304 /// * `input` — readable terminal descriptor.
305 /// * `output` — writable terminal descriptor.
306 /// * `env` — environment this terminal reads variables from.
307 ///
308 /// # Returns
309 ///
310 /// A `Terminal` with no saved raw-mode state.
311 ///
312 /// # Errors and panics
313 ///
314 /// This constructor does not fail or intentionally panic.
315 pub fn new(input: I, output: O, env: impl Env + 'static) -> Self {
316 Self {
317 input,
318 output,
319 saved: None,
320 env: Box::new(env),
321 }
322 }
323
324 /// Put the terminal into raw mode and save the previous state.
325 ///
326 /// This calls `make_raw_mode` with the terminal's input and output
327 /// descriptors. The returned pre-raw [`State`] is cloned into the terminal
328 /// so [`restore`](Self::restore) can later apply it without an argument.
329 ///
330 /// # Returns
331 ///
332 /// The state that was active before raw mode was applied.
333 ///
334 /// # Errors
335 ///
336 /// Returns any error from reading the current state or applying the raw
337 /// state.
338 ///
339 /// # Panics
340 ///
341 /// This method does not intentionally panic.
342 pub fn make_raw(&mut self) -> io::Result<State> {
343 let prev = raw::make_raw_mode(&self.input, &self.output)?;
344 self.saved = Some(prev.clone());
345 Ok(prev)
346 }
347
348 /// Restore the state cached by the most recent [`make_raw`](Self::make_raw).
349 ///
350 /// If a state is cached, it is applied with `set_state` and then
351 /// cleared. If no state is cached, this is a no-op. The cache is kept when
352 /// applying fails, so a failed restore can be retried rather than losing
353 /// the only copy of the pre-raw state.
354 ///
355 /// # Returns
356 ///
357 /// `Ok(())` when no state was cached or restoration succeeded.
358 ///
359 /// # Errors
360 ///
361 /// Returns any error from applying the cached state.
362 ///
363 /// # Panics
364 ///
365 /// This method does not intentionally panic.
366 pub fn restore(&mut self) -> io::Result<()> {
367 if let Some(state) = self.saved.as_ref() {
368 raw::set_state(&self.input, &self.output, state)?;
369 self.saved = None;
370 }
371 Ok(())
372 }
373
374 /// Snapshot the current terminal mode.
375 ///
376 /// This reads the terminal state without modifying the cached state used by
377 /// [`restore`](Self::restore).
378 ///
379 /// # Returns
380 ///
381 /// The current [`State`].
382 ///
383 /// # Errors
384 ///
385 /// Returns any error from `get_state`.
386 ///
387 /// # Panics
388 ///
389 /// This method does not intentionally panic.
390 pub fn get_state(&self) -> io::Result<State> {
391 raw::get_state(&self.input, &self.output)
392 }
393
394 /// Apply a previously snapshotted terminal mode.
395 ///
396 /// This does not update or clear the state cached by
397 /// [`make_raw`](Self::make_raw). Use it for manual state management; use
398 /// [`restore`](Self::restore) for the terminal-owned raw-mode lifecycle.
399 /// `state` should have been read from this terminal's own descriptors, since
400 /// a [`State`] records each half separately.
401 ///
402 /// # Parameters
403 ///
404 /// * `state` — state to apply to the terminal.
405 ///
406 /// # Returns
407 ///
408 /// `Ok(())` when the state was applied.
409 ///
410 /// # Errors
411 ///
412 /// Returns any error from `set_state`.
413 ///
414 /// # Panics
415 ///
416 /// This method does not intentionally panic.
417 pub fn set_state(&self, state: &State) -> io::Result<()> {
418 raw::set_state(&self.input, &self.output, state)
419 }
420
421 /// Report whether the input and output halves are terminals.
422 ///
423 /// # Returns
424 ///
425 /// `(input_is_terminal, output_is_terminal)`.
426 ///
427 /// # Errors and panics
428 ///
429 /// This method does not fail or intentionally panic.
430 pub fn is_terminal(&self) -> (bool, bool) {
431 (
432 raw::is_terminal(&self.input),
433 raw::is_terminal(&self.output),
434 )
435 }
436
437 /// Query the current terminal window size.
438 ///
439 /// On Unix this tries the output descriptor first and falls back to the
440 /// input descriptor if the output query fails. If both fail, the output
441 /// descriptor's error is returned.
442 ///
443 /// # Returns
444 ///
445 /// The current window size in cells and, when reported by the platform,
446 /// pixels.
447 ///
448 /// # Errors
449 ///
450 /// Returns an OS error if the size cannot be queried from either half.
451 ///
452 /// # Panics
453 ///
454 /// This method does not intentionally panic.
455 pub fn get_window_size(&self) -> io::Result<Winsize> {
456 get_window_size(&self.output).or_else(|e| get_window_size(&self.input).map_err(|_| e))
457 }
458}
459
460#[cfg(windows)]
461impl<I: AsHandle, O: AsHandle> Terminal<I, O> {
462 /// Build a terminal from input and output handles plus an [`Env`].
463 ///
464 /// The environment is stored exactly as provided and is not required to
465 /// describe the given handles. Use [`Terminal::stdio`] or
466 /// [`Terminal::open`] to use the process environment automatically.
467 ///
468 /// # Parameters
469 ///
470 /// * `input` — readable console handle.
471 /// * `output` — writable console handle.
472 /// * `env` — environment this terminal reads variables from.
473 ///
474 /// # Returns
475 ///
476 /// A `Terminal` with no saved raw-mode state.
477 ///
478 /// # Errors and panics
479 ///
480 /// This constructor does not fail or intentionally panic.
481 pub fn new(input: I, output: O, env: impl Env + 'static) -> Self {
482 Self {
483 input,
484 output,
485 saved: None,
486 env: Box::new(env),
487 }
488 }
489
490 /// Put the terminal into raw mode and save the previous state.
491 ///
492 /// This calls `make_raw_mode` with the terminal's input and output
493 /// handles. The returned pre-raw [`State`] is cloned into the terminal so
494 /// [`restore`](Self::restore) can later apply it without an argument.
495 ///
496 /// # Returns
497 ///
498 /// The state that was active before raw mode was applied.
499 ///
500 /// # Errors
501 ///
502 /// Returns any error from reading the current state or applying the raw
503 /// state.
504 ///
505 /// # Panics
506 ///
507 /// This method does not intentionally panic.
508 pub fn make_raw(&mut self) -> io::Result<State> {
509 let prev = raw::make_raw_mode(&self.input, &self.output)?;
510 self.saved = Some(prev.clone());
511 Ok(prev)
512 }
513
514 /// Restore the state cached by the most recent [`make_raw`](Self::make_raw).
515 ///
516 /// If a state is cached, it is applied with `set_state` and then
517 /// cleared. If no state is cached, this is a no-op. The cache is kept when
518 /// applying fails, so a failed restore can be retried rather than losing
519 /// the only copy of the pre-raw state.
520 ///
521 /// # Returns
522 ///
523 /// `Ok(())` when no state was cached or restoration succeeded.
524 ///
525 /// # Errors
526 ///
527 /// Returns any error from applying the cached state.
528 ///
529 /// # Panics
530 ///
531 /// This method does not intentionally panic.
532 pub fn restore(&mut self) -> io::Result<()> {
533 if let Some(state) = self.saved.as_ref() {
534 raw::set_state(&self.input, &self.output, state)?;
535 self.saved = None;
536 }
537 Ok(())
538 }
539
540 /// Snapshot the current terminal mode.
541 ///
542 /// This reads the console modes without modifying the cached state used by
543 /// [`restore`](Self::restore).
544 ///
545 /// # Returns
546 ///
547 /// The current [`State`].
548 ///
549 /// # Errors
550 ///
551 /// Returns any error from `get_state`.
552 ///
553 /// # Panics
554 ///
555 /// This method does not intentionally panic.
556 pub fn get_state(&self) -> io::Result<State> {
557 raw::get_state(&self.input, &self.output)
558 }
559
560 /// Apply a previously snapshotted terminal mode.
561 ///
562 /// This does not update or clear the state cached by
563 /// [`make_raw`](Self::make_raw). Use it for manual state management; use
564 /// [`restore`](Self::restore) for the terminal-owned raw-mode lifecycle.
565 /// `state` should have been read from this terminal's own descriptors, since
566 /// a [`State`] records each half separately.
567 ///
568 /// # Parameters
569 ///
570 /// * `state` — state to apply to the terminal.
571 ///
572 /// # Returns
573 ///
574 /// `Ok(())` when the state was applied.
575 ///
576 /// # Errors
577 ///
578 /// Returns any error from `set_state`.
579 ///
580 /// # Panics
581 ///
582 /// This method does not intentionally panic.
583 pub fn set_state(&self, state: &State) -> io::Result<()> {
584 raw::set_state(&self.input, &self.output, state)
585 }
586
587 /// Report whether the input and output halves are terminals.
588 ///
589 /// # Returns
590 ///
591 /// `(input_is_terminal, output_is_terminal)`.
592 ///
593 /// # Errors and panics
594 ///
595 /// This method does not fail or intentionally panic.
596 pub fn is_terminal(&self) -> (bool, bool) {
597 (
598 raw::is_terminal(&self.input),
599 raw::is_terminal(&self.output),
600 )
601 }
602
603 /// Query the current terminal window size.
604 ///
605 /// On Windows this queries the output console screen buffer. Pixel
606 /// dimensions are unavailable and are reported as `0`.
607 ///
608 /// # Returns
609 ///
610 /// The current visible console window size in cells.
611 ///
612 /// # Errors
613 ///
614 /// Returns an OS error if the output handle is not a console screen buffer
615 /// or the size query fails.
616 ///
617 /// # Panics
618 ///
619 /// This method does not intentionally panic.
620 pub fn get_window_size(&self) -> io::Result<Winsize> {
621 get_window_size(&self.output)
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628 use crate::terminal::EnvList;
629
630 #[test]
631 fn env_helpers_delegate_to_env() {
632 let env =
633 EnvList::from_pairs([("TERM", "xterm-256color"), ("NO_COLOR", "1"), ("EMPTY", "")]);
634 let term = Terminal::new(stdin(), stdout(), env);
635
636 assert_eq!(term.get_env("TERM").as_deref(), Some("xterm-256color"));
637 assert!(term.has_env("TERM"));
638 assert!(!term.has_env("EMPTY")); // present but empty
639 assert!(!term.has_env("MISSING"));
640 // The full environment is reachable for richer queries.
641 assert_eq!(term.env().get("NO_COLOR").as_deref(), Some("1"));
642 }
643
644 /// A restore that fails must not consume the cached state: it is the only
645 /// copy of the pre-raw attributes, and dropping it turns a transient
646 /// failure into a terminal the caller can no longer put back.
647 #[cfg(all(unix, not(target_os = "l4re")))]
648 #[test]
649 fn a_failed_restore_keeps_the_saved_state_for_a_retry() {
650 use crate::testutil::{ScriptedFd, open_pty_pair, opost, prime};
651 use std::os::fd::AsFd;
652
653 let (Some((_ma, a)), Some((_mb, b))) = (open_pty_pair(), open_pty_pair()) else {
654 return;
655 };
656 // A fresh pty's attributes are implementation-defined, so put `OPOST`
657 // where the assertions below need it rather than assuming it.
658 prime(&b, true);
659
660 // The two `make_raw` borrows land on the pty, the first restore lands on
661 // a pipe and fails, and the retry lands on the pty again.
662 let pipe = std::io::pipe().expect("pipe").0;
663 let output = ScriptedFd::new(&[&b as &dyn AsFd, &b, &pipe, &b]);
664 let mut term = Terminal::new(&a, output, EnvList::from_pairs([("TERM", "dumb")]));
665
666 term.make_raw().expect("raw mode");
667 assert!(!opost(&b), "the output half must be raw");
668
669 term.restore()
670 .expect_err("a restore through a pipe cannot succeed");
671 // Without the cached state this is a silent no-op that reports success.
672 term.restore().expect("the retry restores");
673 assert!(opost(&b), "the retry must put the output half back");
674 }
675}