Skip to main content

omp_tui/
tty.rs

1//! Terminal device resolution honoring the `OMP_TTY` override.
2//!
3//! The UI talks to the controlling terminal directly (`/dev/tty` on Unix), so
4//! a harness that only owns the process's pipes cannot observe or drive it.
5//! Setting `OMP_TTY` to an alternate terminal device — typically a pty slave
6//! whose master the harness holds — reroutes every terminal open (input,
7//! output via [`TtyOut`], capability probes) and the terminal identity to
8//! that device, delivering the complete byte stream a real terminal would
9//! see on the master side.
10//!
11//! Limitations under an override: `SIGWINCH` is only delivered for the
12//! controlling terminal, so live resizes will not propagate unless the
13//! terminal supports in-band resize; set the window size up front with
14//! `TIOCSWINSZ` on the master.
15
16use std::{
17	fs::{File, OpenOptions},
18	io::{self, IoSlice, Write},
19	path::PathBuf,
20	sync::LazyLock,
21};
22
23/// Environment variable naming an alternate terminal device.
24pub const TTY_OVERRIDE: &str = "OMP_TTY";
25
26/// The overriding device path, when [`TTY_OVERRIDE`] is set and non-empty.
27pub fn override_path() -> Option<PathBuf> {
28	std::env::var_os(TTY_OVERRIDE)
29		.filter(|value| !value.is_empty())
30		.map(PathBuf::from)
31}
32
33/// Whether an override device is configured. Cached: the environment is
34/// read once, matching the process-lifetime scope of the override.
35pub fn overridden() -> bool {
36	static OVERRIDDEN: LazyLock<bool> = LazyLock::new(|| override_path().is_some());
37	*OVERRIDDEN
38}
39
40/// Opens the terminal device with the given options, honoring [`TTY_OVERRIDE`].
41#[cfg(unix)]
42pub fn open(options: &OpenOptions) -> io::Result<File> {
43	match override_path() {
44		Some(path) => options.open(path),
45		None => options.open("/dev/tty"),
46	}
47}
48
49/// Terminal output sink: stdout normally, the `OMP_TTY` device when set.
50///
51/// [`crate::App`] and full-screen frontends render through this so that an
52/// `OMP_TTY` override captures rendered frames alongside the control
53/// sequences, not just the lifecycle bytes.
54pub struct TtyOut(Sink);
55
56enum Sink {
57	Stdout(io::Stdout),
58	Device(File),
59}
60
61impl TtyOut {
62	/// Opens the terminal output sink.
63	///
64	/// # Errors
65	/// Fails when `OMP_TTY` names a path that cannot be opened for writing;
66	/// a misconfigured override is reported rather than silently ignored.
67	pub fn new() -> io::Result<Self> {
68		match override_path() {
69			Some(path) => Ok(Self(Sink::Device(OpenOptions::new().write(true).open(path)?))),
70			None => Ok(Self(Sink::Stdout(io::stdout()))),
71		}
72	}
73}
74
75impl Write for TtyOut {
76	fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
77		match &mut self.0 {
78			Sink::Stdout(out) => out.write(buf),
79			Sink::Device(out) => out.write(buf),
80		}
81	}
82
83	fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
84		match &mut self.0 {
85			Sink::Stdout(out) => out.write_vectored(bufs),
86			Sink::Device(out) => out.write_vectored(bufs),
87		}
88	}
89
90	fn flush(&mut self) -> io::Result<()> {
91		match &mut self.0 {
92			Sink::Stdout(out) => out.flush(),
93			Sink::Device(out) => out.flush(),
94		}
95	}
96}