1use crate::stack::{Stack, pop, push};
30use crate::value::Value;
31use std::sync::atomic::{AtomicBool, Ordering};
32
33static RAW_MODE_ENABLED: AtomicBool = AtomicBool::new(false);
35
36static mut SAVED_TERMIOS: Option<libc::termios> = None;
38
39static mut SAVED_SIGINT_ACTION: Option<libc::sigaction> = None;
41static mut SAVED_SIGTERM_ACTION: Option<libc::sigaction> = None;
42
43#[unsafe(no_mangle)]
55pub unsafe extern "C" fn patch_seq_terminal_raw_mode(stack: Stack) -> Stack {
56 assert!(!stack.is_null(), "terminal_raw_mode: stack is empty");
57
58 let (rest, value) = unsafe { pop(stack) };
59
60 match value {
61 Value::Bool(enable) => {
62 if enable {
63 enable_raw_mode();
64 } else {
65 disable_raw_mode();
66 }
67 rest
68 }
69 _ => panic!("terminal_raw_mode: expected Bool on stack, got {:?}", value),
70 }
71}
72
73#[unsafe(no_mangle)]
87pub unsafe extern "C" fn patch_seq_terminal_read_char(stack: Stack) -> Stack {
88 let mut buf = [0u8; 1];
89 let result =
90 unsafe { libc::read(libc::STDIN_FILENO, buf.as_mut_ptr() as *mut libc::c_void, 1) };
91
92 let char_value = if result == 1 {
93 buf[0] as i64
94 } else {
95 -1 };
97
98 unsafe { push(stack, Value::Int(char_value)) }
99}
100
101#[unsafe(no_mangle)]
114pub unsafe extern "C" fn patch_seq_terminal_read_char_nonblock(stack: Stack) -> Stack {
115 let flags = unsafe { libc::fcntl(libc::STDIN_FILENO, libc::F_GETFL) };
117 if flags < 0 {
118 return unsafe { push(stack, Value::Int(-1)) };
119 }
120
121 unsafe { libc::fcntl(libc::STDIN_FILENO, libc::F_SETFL, flags | libc::O_NONBLOCK) };
123
124 let mut buf = [0u8; 1];
125 let result =
126 unsafe { libc::read(libc::STDIN_FILENO, buf.as_mut_ptr() as *mut libc::c_void, 1) };
127
128 unsafe { libc::fcntl(libc::STDIN_FILENO, libc::F_SETFL, flags) };
130
131 let char_value = if result == 1 {
132 buf[0] as i64
133 } else {
134 -1 };
136
137 unsafe { push(stack, Value::Int(char_value)) }
138}
139
140#[unsafe(no_mangle)]
149pub unsafe extern "C" fn patch_seq_terminal_width(stack: Stack) -> Stack {
150 let width = get_terminal_size().0;
151 unsafe { push(stack, Value::Int(width)) }
152}
153
154#[unsafe(no_mangle)]
163pub unsafe extern "C" fn patch_seq_terminal_height(stack: Stack) -> Stack {
164 let height = get_terminal_size().1;
165 unsafe { push(stack, Value::Int(height)) }
166}
167
168#[unsafe(no_mangle)]
178pub unsafe extern "C" fn patch_seq_terminal_flush(stack: Stack) -> Stack {
179 use std::io::Write;
180 let _ = std::io::stdout().flush();
181 stack
182}
183
184extern "C" fn signal_handler(sig: libc::c_int) {
197 unsafe {
200 if let Some(ref saved) = SAVED_TERMIOS {
201 libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, saved);
202 }
203 }
204
205 unsafe {
207 libc::signal(sig, libc::SIG_DFL);
208 libc::raise(sig);
209 }
210}
211
212fn install_signal_handlers() {
214 unsafe {
215 let mut new_action: libc::sigaction = std::mem::zeroed();
216 new_action.sa_sigaction = signal_handler as usize;
217 libc::sigemptyset(&mut new_action.sa_mask);
218 new_action.sa_flags = 0;
219
220 let mut old_sigint: libc::sigaction = std::mem::zeroed();
222 if libc::sigaction(libc::SIGINT, &new_action, &mut old_sigint) == 0 {
223 SAVED_SIGINT_ACTION = Some(old_sigint);
224 }
225
226 let mut old_sigterm: libc::sigaction = std::mem::zeroed();
228 if libc::sigaction(libc::SIGTERM, &new_action, &mut old_sigterm) == 0 {
229 SAVED_SIGTERM_ACTION = Some(old_sigterm);
230 }
231 }
232}
233
234fn restore_signal_handlers() {
236 unsafe {
237 if let Some(ref action) = SAVED_SIGINT_ACTION {
238 libc::sigaction(libc::SIGINT, action, std::ptr::null_mut());
239 }
240 SAVED_SIGINT_ACTION = None;
241
242 if let Some(ref action) = SAVED_SIGTERM_ACTION {
243 libc::sigaction(libc::SIGTERM, action, std::ptr::null_mut());
244 }
245 SAVED_SIGTERM_ACTION = None;
246 }
247}
248
249fn enable_raw_mode() {
250 if RAW_MODE_ENABLED.load(Ordering::SeqCst) {
251 return; }
253
254 unsafe {
255 if libc::isatty(libc::STDIN_FILENO) != 1 {
257 return; }
259
260 let mut termios: libc::termios = std::mem::zeroed();
261
262 if libc::tcgetattr(libc::STDIN_FILENO, &mut termios) != 0 {
264 return; }
266
267 SAVED_TERMIOS = Some(termios);
269
270 termios.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG | libc::IEXTEN);
276
277 termios.c_iflag &= !(libc::IXON | libc::ICRNL);
281
282 termios.c_oflag &= !libc::OPOST;
285
286 termios.c_cc[libc::VMIN] = 1;
288 termios.c_cc[libc::VTIME] = 0;
289
290 if libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &termios) == 0 {
292 RAW_MODE_ENABLED.store(true, Ordering::SeqCst);
293 install_signal_handlers();
295 }
296 }
297}
298
299fn disable_raw_mode() {
300 if !RAW_MODE_ENABLED.load(Ordering::SeqCst) {
301 return; }
303
304 restore_signal_handlers();
306
307 unsafe {
308 if let Some(ref saved) = SAVED_TERMIOS {
309 libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, saved);
310 }
311 SAVED_TERMIOS = None;
312 RAW_MODE_ENABLED.store(false, Ordering::SeqCst);
313 }
314}
315
316fn get_terminal_size() -> (i64, i64) {
317 unsafe {
318 if libc::isatty(libc::STDOUT_FILENO) != 1 {
320 return (80, 24); }
322
323 let mut winsize: libc::winsize = std::mem::zeroed();
324 if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut winsize) == 0 {
325 let cols = if winsize.ws_col > 0 {
326 winsize.ws_col as i64
327 } else {
328 80
329 };
330 let rows = if winsize.ws_row > 0 {
331 winsize.ws_row as i64
332 } else {
333 24
334 };
335 (cols, rows)
336 } else {
337 (80, 24) }
339 }
340}
341
342pub use patch_seq_terminal_flush as terminal_flush;
344pub use patch_seq_terminal_height as terminal_height;
345pub use patch_seq_terminal_raw_mode as terminal_raw_mode;
346pub use patch_seq_terminal_read_char as terminal_read_char;
347pub use patch_seq_terminal_read_char_nonblock as terminal_read_char_nonblock;
348pub use patch_seq_terminal_width as terminal_width;
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn test_terminal_size() {
356 let (width, height) = get_terminal_size();
358 assert!(width > 0);
359 assert!(height > 0);
360 }
361
362 #[test]
363 fn test_terminal_width_stack() {
364 unsafe {
365 let stack = crate::stack::alloc_test_stack();
366 let stack = terminal_width(stack);
367 let (_, value) = pop(stack);
368 match value {
369 Value::Int(w) => assert!(w > 0),
370 _ => panic!("expected Int"),
371 }
372 }
373 }
374
375 #[test]
376 fn test_terminal_height_stack() {
377 unsafe {
378 let stack = crate::stack::alloc_test_stack();
379 let stack = terminal_height(stack);
380 let (_, value) = pop(stack);
381 match value {
382 Value::Int(h) => assert!(h > 0),
383 _ => panic!("expected Int"),
384 }
385 }
386 }
387
388 #[test]
389 fn test_raw_mode_toggle() {
390 enable_raw_mode();
393 disable_raw_mode();
394 assert!(!RAW_MODE_ENABLED.load(Ordering::SeqCst));
396 }
397}