rust_expect/interact/
terminal.rs1use std::io::{self, Read, Write};
4use std::sync::Arc;
5use std::sync::atomic::{AtomicBool, Ordering};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum TerminalMode {
10 Raw,
12 Cooked,
14 Cbreak,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct TerminalSize {
21 pub cols: u16,
23 pub rows: u16,
25}
26
27impl Default for TerminalSize {
28 fn default() -> Self {
29 Self { cols: 80, rows: 24 }
30 }
31}
32
33impl TerminalSize {
34 #[must_use]
36 pub const fn new(cols: u16, rows: u16) -> Self {
37 Self { cols, rows }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct TerminalState {
44 pub mode: TerminalMode,
46 pub echo: bool,
48 pub canonical: bool,
50}
51
52impl Default for TerminalState {
53 fn default() -> Self {
54 Self {
55 mode: TerminalMode::Cooked,
56 echo: true,
57 canonical: true,
58 }
59 }
60}
61
62pub struct Terminal {
64 running: Arc<AtomicBool>,
66 mode: TerminalMode,
68 saved_state: Option<TerminalState>,
70}
71
72impl Terminal {
73 #[must_use]
75 pub fn new() -> Self {
76 Self {
77 running: Arc::new(AtomicBool::new(false)),
78 mode: TerminalMode::Cooked,
79 saved_state: None,
80 }
81 }
82
83 #[must_use]
85 pub fn is_running(&self) -> bool {
86 self.running.load(Ordering::SeqCst)
87 }
88
89 pub fn set_running(&self, running: bool) {
91 self.running.store(running, Ordering::SeqCst);
92 }
93
94 #[must_use]
96 pub fn running_flag(&self) -> Arc<AtomicBool> {
97 Arc::clone(&self.running)
98 }
99
100 #[must_use]
102 pub const fn mode(&self) -> TerminalMode {
103 self.mode
104 }
105
106 pub const fn set_mode(&mut self, mode: TerminalMode) {
108 self.mode = mode;
109 }
110
111 pub const fn save_state(&mut self) {
113 self.saved_state = Some(TerminalState {
114 mode: self.mode,
115 echo: true,
116 canonical: matches!(self.mode, TerminalMode::Cooked),
117 });
118 }
119
120 pub const fn restore_state(&mut self) {
122 if let Some(state) = self.saved_state.take() {
123 self.mode = state.mode;
124 }
125 }
126
127 pub fn size() -> io::Result<TerminalSize> {
129 let cols = std::env::var("COLUMNS")
131 .ok()
132 .and_then(|s| s.parse().ok())
133 .unwrap_or(80);
134 let rows = std::env::var("LINES")
135 .ok()
136 .and_then(|s| s.parse().ok())
137 .unwrap_or(24);
138 Ok(TerminalSize::new(cols, rows))
139 }
140
141 #[must_use]
143 #[allow(unsafe_code)]
144 pub fn is_tty() -> bool {
145 #[cfg(unix)]
146 {
147 use std::os::unix::io::AsRawFd;
148 unsafe { libc::isatty(std::io::stdin().as_raw_fd()) != 0 }
149 }
150 #[cfg(not(unix))]
151 {
152 false
153 }
154 }
155}
156
157impl Default for Terminal {
158 fn default() -> Self {
159 Self::new()
160 }
161}
162
163pub fn read_with_timeout(timeout_ms: u64) -> io::Result<Option<u8>> {
165 use std::time::{Duration, Instant};
166
167 let deadline = Instant::now() + Duration::from_millis(timeout_ms);
168
169 loop {
170 let mut buf = [0u8; 1];
172 match io::stdin().read(&mut buf) {
173 Ok(0) => return Ok(None),
174 Ok(_) => return Ok(Some(buf[0])),
175 Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
176 if Instant::now() >= deadline {
177 return Ok(None);
178 }
179 std::thread::sleep(Duration::from_millis(10));
180 }
181 Err(e) => return Err(e),
182 }
183 }
184}
185
186pub fn write_immediate(data: &[u8]) -> io::Result<()> {
188 let mut stdout = io::stdout();
189 stdout.write_all(data)?;
190 stdout.flush()
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn terminal_default() {
199 let term = Terminal::new();
200 assert!(!term.is_running());
201 assert_eq!(term.mode(), TerminalMode::Cooked);
202 }
203
204 #[test]
205 fn terminal_size_default() {
206 let size = TerminalSize::default();
207 assert_eq!(size.cols, 80);
208 assert_eq!(size.rows, 24);
209 }
210
211 #[test]
212 fn terminal_running_flag() {
213 let term = Terminal::new();
214 let flag = term.running_flag();
215
216 assert!(!term.is_running());
217 flag.store(true, Ordering::SeqCst);
218 assert!(term.is_running());
219 }
220}