Skip to main content

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}