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 }
152 }
153 #[cfg(not(unix))]
154 {
155 false
156 }
157 }
158}
159
160impl Default for Terminal {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166pub fn read_with_timeout(timeout_ms: u64) -> io::Result<Option<u8>> {
168 use std::time::{Duration, Instant};
169
170 let deadline = Instant::now() + Duration::from_millis(timeout_ms);
171
172 loop {
173 let mut buf = [0u8; 1];
175 match io::stdin().read(&mut buf) {
176 Ok(0) => return Ok(None),
177 Ok(_) => return Ok(Some(buf[0])),
178 Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
179 if Instant::now() >= deadline {
180 return Ok(None);
181 }
182 std::thread::sleep(Duration::from_millis(10));
183 }
184 Err(e) => return Err(e),
185 }
186 }
187}
188
189pub fn write_immediate(data: &[u8]) -> io::Result<()> {
191 let mut stdout = io::stdout();
192 stdout.write_all(data)?;
193 stdout.flush()
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn terminal_default() {
202 let term = Terminal::new();
203 assert!(!term.is_running());
204 assert_eq!(term.mode(), TerminalMode::Cooked);
205 }
206
207 #[test]
208 fn terminal_size_default() {
209 let size = TerminalSize::default();
210 assert_eq!(size.cols, 80);
211 assert_eq!(size.rows, 24);
212 }
213
214 #[test]
215 fn terminal_running_flag() {
216 let term = Terminal::new();
217 let flag = term.running_flag();
218
219 assert!(!term.is_running());
220 flag.store(true, Ordering::SeqCst);
221 assert!(term.is_running());
222 }
223}