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}