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, a
4//! captured [`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 snapshot.
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;
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: 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    /// [`Env::from_process`] for its environment snapshot. 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(), Env::from_process())
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` snapshots the process environment with [`Env::from_process`].
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, Env::from_process()))
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: Env) -> Self {
132        Self {
133            input,
134            output,
135            saved: None,
136            env,
137        }
138    }
139
140    /// Borrow the output half. Test-only accessor for inspecting bytes
141    /// written to an in-memory sink.
142    #[cfg(test)]
143    pub(crate) fn output_ref(&self) -> &O {
144        &self.output
145    }
146
147    /// Mutably borrow the output half. Test-only accessor used to swap the
148    /// in-memory sink between captured frames.
149    #[cfg(test)]
150    pub(crate) fn output_mut(&mut self) -> &mut O {
151        &mut self.output
152    }
153
154    /// Return the captured environment snapshot.
155    ///
156    /// The snapshot is taken by the constructor and is not updated if the
157    /// process environment later changes.
158    ///
159    /// # Returns
160    ///
161    /// A shared reference to the terminal's [`Env`].
162    ///
163    /// # Errors and panics
164    ///
165    /// This method does not fail or intentionally panic.
166    pub fn env(&self) -> &Env {
167        &self.env
168    }
169
170    /// Look up an environment variable in the captured snapshot.
171    ///
172    /// # Parameters
173    ///
174    /// * `key` — environment variable name.
175    ///
176    /// # Returns
177    ///
178    /// The last captured value for `key`, or `None` if it is absent.
179    ///
180    /// # Errors and panics
181    ///
182    /// This method does not fail or intentionally panic.
183    pub fn get_env(&self, key: &str) -> Option<String> {
184        self.env.get(key)
185    }
186
187    /// Return whether an environment variable is present and non-empty.
188    ///
189    /// # Parameters
190    ///
191    /// * `key` — environment variable name.
192    ///
193    /// # Returns
194    ///
195    /// `true` when the captured snapshot contains `key` with a non-empty
196    /// value.
197    ///
198    /// # Errors and panics
199    ///
200    /// This method does not fail or intentionally panic.
201    pub fn has_env(&self, key: &str) -> bool {
202        self.env.has(key)
203    }
204
205    /// Return a copy of the input half.
206    ///
207    /// This is available only when `I: Copy`, which is true for the standard
208    /// and controlling-terminal handle types provided by this module. Use it to
209    /// pass input to [`EventSource::new`](crate::event::EventSource::new) while
210    /// retaining the `Terminal` for raw-mode restoration.
211    ///
212    /// # Returns
213    ///
214    /// A copy of the input handle.
215    ///
216    /// # Errors and panics
217    ///
218    /// This method does not fail or intentionally panic.
219    pub fn input(&self) -> I
220    where
221        I: Copy,
222    {
223        self.input
224    }
225
226    /// Return a copy of the output half.
227    ///
228    /// This is available only when `O: Copy`, which is true for the standard
229    /// and controlling-terminal output types provided by this module. Use it to
230    /// pass output to a renderer such as [`TextBuffer`](crate::buffer::TextBuffer) while
231    /// retaining the `Terminal` for raw-mode restoration.
232    ///
233    /// # Returns
234    ///
235    /// A copy of the output handle.
236    ///
237    /// # Errors and panics
238    ///
239    /// This method does not fail or intentionally panic.
240    pub fn output(&self) -> O
241    where
242        O: Copy,
243    {
244        self.output
245    }
246
247    /// Consume the terminal and return its input and output halves.
248    ///
249    /// Any cached raw-mode state is dropped without being applied. Restore
250    /// before calling this method if the terminal is in raw mode.
251    ///
252    /// # Returns
253    ///
254    /// `(input, output)`.
255    ///
256    /// # Errors and panics
257    ///
258    /// This method does not fail or intentionally panic.
259    pub fn into_halves(self) -> (I, O) {
260        (self.input, self.output)
261    }
262}
263
264impl<I: Read, O> Read for Terminal<I, O> {
265    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
266        self.input.read(buf)
267    }
268}
269
270impl<I, O: Write> Write for Terminal<I, O> {
271    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
272        self.output.write(buf)
273    }
274
275    fn flush(&mut self) -> io::Result<()> {
276        self.output.flush()
277    }
278}
279
280#[cfg(unix)]
281impl<I: AsFd, O> AsFd for Terminal<I, O> {
282    /// Borrow the input descriptor.
283    ///
284    /// This exposes the input half, not the output half. Use
285    /// [`output`](Self::output) and borrow that handle when an output
286    /// descriptor is required.
287    fn as_fd(&self) -> BorrowedFd<'_> {
288        self.input.as_fd()
289    }
290}
291
292#[cfg(windows)]
293impl<I: AsHandle, O> AsHandle for Terminal<I, O> {
294    /// Borrow the input handle.
295    ///
296    /// This exposes the input half, not the output half. Use
297    /// [`output`](Self::output) and borrow that handle when an output handle is
298    /// required.
299    fn as_handle(&self) -> BorrowedHandle<'_> {
300        self.input.as_handle()
301    }
302}
303
304#[cfg(unix)]
305impl<I: AsFd, O: AsFd> Terminal<I, O> {
306    /// Build a terminal from input and output descriptors plus an [`Env`].
307    ///
308    /// The environment is stored exactly as provided and is not required to
309    /// describe the given descriptors. Use [`Terminal::stdio`] or
310    /// [`Terminal::open`] to snapshot the process environment automatically.
311    ///
312    /// # Parameters
313    ///
314    /// * `input` — readable terminal descriptor.
315    /// * `output` — writable terminal descriptor.
316    /// * `env` — environment snapshot associated with this terminal.
317    ///
318    /// # Returns
319    ///
320    /// A `Terminal` with no saved raw-mode state.
321    ///
322    /// # Errors and panics
323    ///
324    /// This constructor does not fail or intentionally panic.
325    pub fn new(input: I, output: O, env: Env) -> Self {
326        Self {
327            input,
328            output,
329            saved: None,
330            env,
331        }
332    }
333
334    /// Put the terminal into raw mode and save the previous state.
335    ///
336    /// This calls `make_raw_mode` with the terminal's input and output
337    /// descriptors. The returned pre-raw [`State`] is cloned into the terminal
338    /// so [`restore`](Self::restore) can later apply it without an argument.
339    ///
340    /// # Returns
341    ///
342    /// The state that was active before raw mode was applied.
343    ///
344    /// # Errors
345    ///
346    /// Returns any error from reading the current state or applying the raw
347    /// state.
348    ///
349    /// # Panics
350    ///
351    /// This method does not intentionally panic.
352    pub fn make_raw(&mut self) -> io::Result<State> {
353        let prev = raw::make_raw_mode(&self.input, &self.output)?;
354        self.saved = Some(prev.clone());
355        Ok(prev)
356    }
357
358    /// Restore the state cached by the most recent [`make_raw`](Self::make_raw).
359    ///
360    /// If a state is cached, it is applied with `set_state` and then
361    /// cleared. If no state is cached, this is a no-op.
362    ///
363    /// # Returns
364    ///
365    /// `Ok(())` when no state was cached or restoration succeeded.
366    ///
367    /// # Errors
368    ///
369    /// Returns any error from applying the cached state.
370    ///
371    /// # Panics
372    ///
373    /// This method does not intentionally panic.
374    pub fn restore(&mut self) -> io::Result<()> {
375        match self.saved.take() {
376            Some(state) => raw::set_state(&self.input, &self.output, &state),
377            None => Ok(()),
378        }
379    }
380
381    /// Snapshot the current terminal mode.
382    ///
383    /// This reads the terminal state without modifying the cached state used by
384    /// [`restore`](Self::restore).
385    ///
386    /// # Returns
387    ///
388    /// The current [`State`].
389    ///
390    /// # Errors
391    ///
392    /// Returns any error from `get_state`.
393    ///
394    /// # Panics
395    ///
396    /// This method does not intentionally panic.
397    pub fn get_state(&self) -> io::Result<State> {
398        raw::get_state(&self.input, &self.output)
399    }
400
401    /// Apply a previously snapshotted terminal mode.
402    ///
403    /// This does not update or clear the state cached by
404    /// [`make_raw`](Self::make_raw). Use it for manual state management; use
405    /// [`restore`](Self::restore) for the terminal-owned raw-mode lifecycle.
406    ///
407    /// # Parameters
408    ///
409    /// * `state` — state to apply to the terminal.
410    ///
411    /// # Returns
412    ///
413    /// `Ok(())` when the state was applied.
414    ///
415    /// # Errors
416    ///
417    /// Returns any error from `set_state`.
418    ///
419    /// # Panics
420    ///
421    /// This method does not intentionally panic.
422    pub fn set_state(&self, state: &State) -> io::Result<()> {
423        raw::set_state(&self.input, &self.output, state)
424    }
425
426    /// Report whether the input and output halves are terminals.
427    ///
428    /// # Returns
429    ///
430    /// `(input_is_terminal, output_is_terminal)`.
431    ///
432    /// # Errors and panics
433    ///
434    /// This method does not fail or intentionally panic.
435    pub fn is_terminal(&self) -> (bool, bool) {
436        (
437            raw::is_terminal(&self.input),
438            raw::is_terminal(&self.output),
439        )
440    }
441
442    /// Query the current terminal window size.
443    ///
444    /// On Unix this tries the output descriptor first and falls back to the
445    /// input descriptor if the output query fails. If both fail, the output
446    /// descriptor's error is returned.
447    ///
448    /// # Returns
449    ///
450    /// The current window size in cells and, when reported by the platform,
451    /// pixels.
452    ///
453    /// # Errors
454    ///
455    /// Returns an OS error if the size cannot be queried from either half.
456    ///
457    /// # Panics
458    ///
459    /// This method does not intentionally panic.
460    pub fn get_window_size(&self) -> io::Result<Winsize> {
461        get_window_size(&self.output).or_else(|e| get_window_size(&self.input).map_err(|_| e))
462    }
463}
464
465#[cfg(windows)]
466impl<I: AsHandle, O: AsHandle> Terminal<I, O> {
467    /// Build a terminal from input and output handles plus an [`Env`].
468    ///
469    /// The environment is stored exactly as provided and is not required to
470    /// describe the given handles. Use [`Terminal::stdio`] or
471    /// [`Terminal::open`] to snapshot the process environment automatically.
472    ///
473    /// # Parameters
474    ///
475    /// * `input` — readable console handle.
476    /// * `output` — writable console handle.
477    /// * `env` — environment snapshot associated with this terminal.
478    ///
479    /// # Returns
480    ///
481    /// A `Terminal` with no saved raw-mode state.
482    ///
483    /// # Errors and panics
484    ///
485    /// This constructor does not fail or intentionally panic.
486    pub fn new(input: I, output: O, env: Env) -> Self {
487        Self {
488            input,
489            output,
490            saved: None,
491            env,
492        }
493    }
494
495    /// Put the terminal into raw mode and save the previous state.
496    ///
497    /// This calls `make_raw_mode` with the terminal's input and output
498    /// handles. The returned pre-raw [`State`] is cloned into the terminal so
499    /// [`restore`](Self::restore) can later apply it without an argument.
500    ///
501    /// # Returns
502    ///
503    /// The state that was active before raw mode was applied.
504    ///
505    /// # Errors
506    ///
507    /// Returns any error from reading the current state or applying the raw
508    /// state.
509    ///
510    /// # Panics
511    ///
512    /// This method does not intentionally panic.
513    pub fn make_raw(&mut self) -> io::Result<State> {
514        let prev = raw::make_raw_mode(&self.input, &self.output)?;
515        self.saved = Some(prev.clone());
516        Ok(prev)
517    }
518
519    /// Restore the state cached by the most recent [`make_raw`](Self::make_raw).
520    ///
521    /// If a state is cached, it is applied with `set_state` and then
522    /// cleared. If no state is cached, this is a no-op.
523    ///
524    /// # Returns
525    ///
526    /// `Ok(())` when no state was cached or restoration succeeded.
527    ///
528    /// # Errors
529    ///
530    /// Returns any error from applying the cached state.
531    ///
532    /// # Panics
533    ///
534    /// This method does not intentionally panic.
535    pub fn restore(&mut self) -> io::Result<()> {
536        match self.saved.take() {
537            Some(state) => raw::set_state(&self.input, &self.output, &state),
538            None => Ok(()),
539        }
540    }
541
542    /// Snapshot the current terminal mode.
543    ///
544    /// This reads the console modes without modifying the cached state used by
545    /// [`restore`](Self::restore).
546    ///
547    /// # Returns
548    ///
549    /// The current [`State`].
550    ///
551    /// # Errors
552    ///
553    /// Returns any error from `get_state`.
554    ///
555    /// # Panics
556    ///
557    /// This method does not intentionally panic.
558    pub fn get_state(&self) -> io::Result<State> {
559        raw::get_state(&self.input, &self.output)
560    }
561
562    /// Apply a previously snapshotted terminal mode.
563    ///
564    /// This does not update or clear the state cached by
565    /// [`make_raw`](Self::make_raw). Use it for manual state management; use
566    /// [`restore`](Self::restore) for the terminal-owned raw-mode lifecycle.
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
629    #[test]
630    fn env_helpers_delegate_to_snapshot() {
631        let env = Env::from_pairs([("TERM", "xterm-256color"), ("NO_COLOR", "1"), ("EMPTY", "")]);
632        let term = Terminal::new(stdin(), stdout(), env);
633
634        assert_eq!(term.get_env("TERM").as_deref(), Some("xterm-256color"));
635        assert!(term.has_env("TERM"));
636        assert!(!term.has_env("EMPTY")); // present but empty
637        assert!(!term.has_env("MISSING"));
638        // The full snapshot is reachable for richer queries (e.g. bool).
639        assert_eq!(term.env().get("NO_COLOR").as_deref(), Some("1"));
640        assert!(term.env().is_truthy("NO_COLOR"));
641    }
642}