1use std::io::{self, Read, Write};
2
3#[cfg(unix)]
8struct RawModeGuard {
9 original: libc::termios,
10}
11
12#[cfg(unix)]
13impl RawModeGuard {
14 fn enter() -> io::Result<Self> {
15 let mut original: libc::termios = unsafe { std::mem::zeroed() };
16 if unsafe { libc::tcgetattr(libc::STDIN_FILENO, &mut original) } != 0 {
17 return Err(io::Error::last_os_error());
18 }
19 let mut raw = original;
20 raw.c_lflag &= !(libc::ICANON | libc::ECHO);
21 raw.c_cc[libc::VMIN] = 1;
22 raw.c_cc[libc::VTIME] = 0;
23 if unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &raw) } != 0 {
24 return Err(io::Error::last_os_error());
25 }
26 Ok(Self { original })
27 }
28}
29
30#[cfg(unix)]
31impl Drop for RawModeGuard {
32 fn drop(&mut self) {
33 unsafe {
34 libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &self.original);
35 }
36 }
37}
38
39#[cfg(windows)]
42mod win32 {
43 #[link(name = "kernel32")]
44 unsafe extern "system" {
45 pub fn GetStdHandle(nStdHandle: u32) -> isize;
46 pub fn GetConsoleMode(hConsoleHandle: isize, lpMode: *mut u32) -> i32;
47 pub fn SetConsoleMode(hConsoleHandle: isize, dwMode: u32) -> i32;
48 }
49
50 pub const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6; pub const ENABLE_LINE_INPUT: u32 = 0x0002;
52 pub const ENABLE_ECHO_INPUT: u32 = 0x0004;
53 pub const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;
54}
55
56#[cfg(windows)]
57struct RawModeGuard {
58 handle: isize,
59 original_mode: u32,
60}
61
62#[cfg(windows)]
63impl RawModeGuard {
64 fn enter() -> io::Result<Self> {
65 let handle = unsafe { win32::GetStdHandle(win32::STD_INPUT_HANDLE) };
66 if handle == -1 {
67 return Err(io::Error::last_os_error());
68 }
69 let mut original_mode: u32 = 0;
70 if unsafe { win32::GetConsoleMode(handle, &mut original_mode) } == 0 {
71 return Err(io::Error::last_os_error());
72 }
73 let new_mode = (original_mode
74 & !(win32::ENABLE_LINE_INPUT | win32::ENABLE_ECHO_INPUT))
75 | win32::ENABLE_VIRTUAL_TERMINAL_INPUT;
76 if unsafe { win32::SetConsoleMode(handle, new_mode) } == 0 {
77 return Err(io::Error::last_os_error());
78 }
79 Ok(Self {
80 handle,
81 original_mode,
82 })
83 }
84}
85
86#[cfg(windows)]
87impl Drop for RawModeGuard {
88 fn drop(&mut self) {
89 unsafe {
90 win32::SetConsoleMode(self.handle, self.original_mode);
91 }
92 }
93}
94
95#[cfg(target_os = "wasi")]
98struct RawModeGuard;
99
100#[cfg(target_os = "wasi")]
101impl RawModeGuard {
102 fn enter() -> io::Result<Self> {
103 Ok(Self)
104 }
105}
106
107pub trait LineHandler {
121 fn handle_line(&self, line: &str) -> Result<LoopAction, String>;
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum LoopAction {
127 Continue,
129 Break,
131}
132
133impl<F> LineHandler for F
135where
136 F: Fn(&str) -> Result<LoopAction, String>,
137{
138 fn handle_line(&self, line: &str) -> Result<LoopAction, String> {
139 self(line)
140 }
141}
142
143pub struct LineReader {
159 history: Vec<String>,
160 max_history: usize,
161}
162
163impl LineReader {
164 pub fn new(max_history: usize) -> Self {
166 Self {
167 history: Vec::new(),
168 max_history,
169 }
170 }
171
172 fn push_history(&mut self, line: &str) {
174 let trimmed = line.trim();
175 if trimmed.is_empty() {
176 return;
177 }
178 if self.history.last().map(|s| s.as_str()) == Some(trimmed) {
179 return;
180 }
181 self.history.push(trimmed.to_string());
182 if self.history.len() > self.max_history {
183 self.history.remove(0);
184 }
185 }
186
187 pub fn read_line(&mut self, prompt: &str, cancel_token: Option<wasibox_core::CancellationToken>) -> io::Result<Option<String>> {
191 let mut stdout = io::stdout();
192 write!(stdout, "{}", prompt)?;
193 stdout.flush()?;
194
195 let _guard = RawModeGuard::enter()?;
196
197 let mut reader = io::stdin();
198 self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
199 }
200
201 pub fn run_loop<P, H>(&mut self, prompt_fn: P, handler: &H, cancel_token: wasibox_core::CancellationToken) -> io::Result<()>
208 where
209 P: Fn() -> String,
210 H: LineHandler,
211 {
212 loop {
213 let prompt = prompt_fn();
214 match self.read_line(&prompt, Some(cancel_token.clone()))? {
215 None => break,
216 Some(line) => {
217 let trimmed = line.trim();
218 if trimmed.is_empty() {
219 continue;
220 }
221 match handler.handle_line(trimmed) {
222 Ok(LoopAction::Continue) => {}
223 Ok(LoopAction::Break) => break,
224 Err(e) => {
225 eprintln!("{}", e);
226 }
227 }
228 }
229 }
230 }
231 Ok(())
232 }
233
234 #[cfg(test)]
236 fn run_loop_from<R: Read, W: Write, H: LineHandler>(
237 &mut self,
238 reader: &mut R,
239 writer: &mut W,
240 prompt: &str,
241 handler: &H,
242 cancel_token: Option<wasibox_core::CancellationToken>,
243 ) -> io::Result<()> {
244 loop {
245 write!(writer, "{}", prompt)?;
246 writer.flush()?;
247 match self.read_line_from(reader, writer, prompt, cancel_token.clone())? {
248 None => break,
249 Some(line) => {
250 let trimmed = line.trim();
251 if trimmed.is_empty() {
252 continue;
253 }
254 match handler.handle_line(trimmed) {
255 Ok(LoopAction::Continue) => {}
256 Ok(LoopAction::Break) => break,
257 Err(e) => {
258 writeln!(writer, "Error: {}", e)?;
259 }
260 }
261 }
262 }
263 }
264 Ok(())
265 }
266
267 fn read_line_from<R: Read, W: Write>(
270 &mut self,
271 reader: &mut R,
272 writer: &mut W,
273 prompt: &str,
274 cancel_token: Option<wasibox_core::CancellationToken>,
275 ) -> io::Result<Option<String>> {
276 let mut line = String::new();
277 let mut cursor_pos: usize = 0;
278 let mut history_idx: usize = self.history.len();
279 let mut saved_input = String::new();
280
281 loop {
282 let b = {
283 let mut buf = [0u8; 1];
284 reader.read_exact(&mut buf)?;
285 buf[0]
286 };
287 match b {
288 4 => {
290 if line.is_empty() {
291 write!(writer, "\r\n")?;
292 writer.flush()?;
293 return Ok(None);
294 }
295 }
296 3 => {
298 if let Some(token) = &cancel_token {
299 token.cancel();
300 }
301 write!(writer, "^C\r\n")?;
302 writer.flush()?;
303 return Ok(Some(String::new()));
304 }
305 b'\r' | b'\n' => {
307 write!(writer, "\r\n")?;
308 writer.flush()?;
309 self.push_history(&line);
310 return Ok(Some(line));
311 }
312 127 | 8 => {
314 if cursor_pos > 0 {
315 cursor_pos -= 1;
316 line.remove(cursor_pos);
317 Self::redraw_line(writer, prompt, &line, cursor_pos)?;
318 }
319 }
320 1 => {
322 cursor_pos = 0;
323 Self::redraw_line(writer, prompt, &line, cursor_pos)?;
324 }
325 5 => {
327 cursor_pos = line.len();
328 Self::redraw_line(writer, prompt, &line, cursor_pos)?;
329 }
330 21 => {
332 line.clear();
333 cursor_pos = 0;
334 Self::redraw_line(writer, prompt, &line, cursor_pos)?;
335 }
336 11 => {
338 line.truncate(cursor_pos);
339 Self::redraw_line(writer, prompt, &line, cursor_pos)?;
340 }
341 23 => {
343 if cursor_pos > 0 {
344 let mut new_pos = cursor_pos;
345 while new_pos > 0 && line.as_bytes()[new_pos - 1] == b' ' {
346 new_pos -= 1;
347 }
348 while new_pos > 0 && line.as_bytes()[new_pos - 1] != b' ' {
349 new_pos -= 1;
350 }
351 line.drain(new_pos..cursor_pos);
352 cursor_pos = new_pos;
353 Self::redraw_line(writer, prompt, &line, cursor_pos)?;
354 }
355 }
356 27 => {
358 let seq1 = {
359 let mut buf = [0u8; 1];
360 reader.read_exact(&mut buf)?;
361 buf[0]
362 };
363 if seq1 == b'[' {
364 let seq2 = {
365 let mut buf = [0u8; 1];
366 reader.read_exact(&mut buf)?;
367 buf[0]
368 };
369 match seq2 {
370 b'A' => {
372 if !self.history.is_empty() && history_idx > 0 {
373 if history_idx == self.history.len() {
374 saved_input = line.clone();
375 }
376 history_idx -= 1;
377 line = self.history[history_idx].clone();
378 cursor_pos = line.len();
379 Self::redraw_line(writer, prompt, &line, cursor_pos)?;
380 }
381 }
382 b'B' => {
384 if history_idx < self.history.len() {
385 history_idx += 1;
386 if history_idx == self.history.len() {
387 line = saved_input.clone();
388 } else {
389 line = self.history[history_idx].clone();
390 }
391 cursor_pos = line.len();
392 Self::redraw_line(writer, prompt, &line, cursor_pos)?;
393 }
394 }
395 b'C' => {
397 if cursor_pos < line.len() {
398 cursor_pos += 1;
399 write!(writer, "\x1b[C")?;
400 writer.flush()?;
401 }
402 }
403 b'D' => {
405 if cursor_pos > 0 {
406 cursor_pos -= 1;
407 write!(writer, "\x1b[D")?;
408 writer.flush()?;
409 }
410 }
411 b'H' => {
413 cursor_pos = 0;
414 Self::redraw_line(writer, prompt, &line, cursor_pos)?;
415 }
416 b'F' => {
418 cursor_pos = line.len();
419 Self::redraw_line(writer, prompt, &line, cursor_pos)?;
420 }
421 b'3' => {
423 let seq3 = {
424 let mut buf = [0u8; 1];
425 reader.read_exact(&mut buf)?;
426 buf[0]
427 };
428 if seq3 == b'~' && cursor_pos < line.len() {
429 line.remove(cursor_pos);
430 Self::redraw_line(writer, prompt, &line, cursor_pos)?;
431 }
432 }
433 _ => {
434 }
436 }
437 }
438 }
440 b if b >= 0x20 => {
442 line.insert(cursor_pos, b as char);
443 cursor_pos += 1;
444 if cursor_pos == line.len() {
445 write!(writer, "{}", b as char)?;
446 writer.flush()?;
447 } else {
448 Self::redraw_line(writer, prompt, &line, cursor_pos)?;
449 }
450 }
451 _ => {
452 }
454 }
455 }
456 }
457
458 fn redraw_line<W: Write>(
460 writer: &mut W,
461 prompt: &str,
462 line: &str,
463 cursor_pos: usize,
464 ) -> io::Result<()> {
465 write!(writer, "\r\x1b[K{}{}", prompt, line)?;
466 let total_len = prompt.len() + line.len();
467 let target = prompt.len() + cursor_pos;
468 if target < total_len {
469 write!(writer, "\x1b[{}D", total_len - target)?;
470 }
471 writer.flush()
472 }
473}
474
475#[cfg(test)]
480mod tests {
481 use super::*;
482 use std::io::Cursor;
483
484 fn keys(parts: &[&[u8]]) -> Cursor<Vec<u8>> {
486 let mut buf = Vec::new();
487 for part in parts {
488 buf.extend_from_slice(part);
489 }
490 Cursor::new(buf)
491 }
492
493 const UP: &[u8] = b"\x1b[A";
494 const DOWN: &[u8] = b"\x1b[B";
495 const ENTER: &[u8] = b"\r";
496
497 #[test]
498 fn test_simple_input() {
499 let mut reader = LineReader::new(100);
500 let mut input = keys(&[b"hello", ENTER]);
501 let mut out = Vec::new();
502 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
503 assert_eq!(result, Some("hello".to_string()));
504 }
505
506 #[test]
507 fn test_eof_on_empty() {
508 let mut reader = LineReader::new(100);
509 let mut input = Cursor::new(vec![4u8]); let mut out = Vec::new();
511 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
512 assert_eq!(result, None);
513 }
514
515 #[test]
516 fn test_history_up_arrow() {
517 let mut reader = LineReader::new(100);
518 let mut out = Vec::new();
519
520 let mut input = keys(&[b"echo hello", ENTER]);
522 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
523
524 let mut input = keys(&[UP, ENTER]);
526 out.clear();
527 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
528 assert_eq!(result, Some("echo hello".to_string()));
529 }
530
531 #[test]
532 fn test_history_up_down_arrow() {
533 let mut reader = LineReader::new(100);
534 let mut out = Vec::new();
535
536 let mut input = keys(&[b"first", ENTER]);
538 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
539 let mut input = keys(&[b"second", ENTER]);
540 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
541
542 let mut input = keys(&[UP, UP, DOWN, ENTER]);
544 out.clear();
545 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
546 assert_eq!(result, Some("second".to_string()));
547 }
548
549 #[test]
550 fn test_history_down_restores_current_input() {
551 let mut reader = LineReader::new(100);
552 let mut out = Vec::new();
553
554 let mut input = keys(&[b"old", ENTER]);
556 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
557
558 let mut input = keys(&[b"new", UP, DOWN, ENTER]);
560 out.clear();
561 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
562 assert_eq!(result, Some("new".to_string()));
563 }
564
565 #[test]
566 fn test_history_dedup() {
567 let mut reader = LineReader::new(100);
568 let mut out = Vec::new();
569
570 let mut input = keys(&[b"dup", ENTER]);
572 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
573 let mut input = keys(&[b"dup", ENTER]);
574 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
575
576 let mut input = keys(&[UP, UP, ENTER]);
579 out.clear();
580 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
581 assert_eq!(result, Some("dup".to_string()));
582 }
583
584 #[test]
585 fn test_history_max_size() {
586 let mut reader = LineReader::new(3);
587 let mut out = Vec::new();
588
589 for cmd in &["aaa", "bbb", "ccc", "ddd"] {
590 let mut input = keys(&[cmd.as_bytes(), ENTER]);
591 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
592 }
593
594 let mut input = keys(&[UP, UP, UP, ENTER]);
596 out.clear();
597 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
598 assert_eq!(result, Some("bbb".to_string()));
599 }
600
601 #[test]
602 fn test_backspace() {
603 let mut reader = LineReader::new(100);
604 let mut input = keys(&[b"helloo", &[127], ENTER]);
605 let mut out = Vec::new();
606 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
607 assert_eq!(result, Some("hello".to_string()));
608 }
609
610 #[test]
611 fn test_ctrl_u_clears_line() {
612 let mut reader = LineReader::new(100);
613 let mut input = keys(&[b"garbage", &[21], b"clean", ENTER]); let mut out = Vec::new();
615 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
616 assert_eq!(result, Some("clean".to_string()));
617 }
618
619 #[test]
620 fn test_empty_line_not_in_history() {
621 let mut reader = LineReader::new(100);
622 let mut out = Vec::new();
623
624 let mut input = keys(&[b"real", ENTER]);
626 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
627
628 let mut input = keys(&[ENTER]);
630 reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
631
632 let mut input = keys(&[UP, ENTER]);
634 out.clear();
635 let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
636 assert_eq!(result, Some("real".to_string()));
637 }
638
639 #[test]
642 fn test_run_loop_with_handler() {
643 use std::sync::{Arc, Mutex};
644
645 let executed = Arc::new(Mutex::new(Vec::new()));
646 let exec_clone = Arc::clone(&executed);
647
648 let handler = move |line: &str| -> Result<LoopAction, String> {
649 exec_clone.lock().unwrap().push(line.to_string());
650 Ok(LoopAction::Continue)
651 };
652
653 let mut reader = LineReader::new(100);
654 let mut input = keys(&[b"echo hello", ENTER, b"ls", ENTER, &[4]]);
656 let mut out = Vec::new();
657 reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
658
659 let cmds = executed.lock().unwrap();
660 assert_eq!(cmds.len(), 2);
661 assert_eq!(cmds[0], "echo hello");
662 assert_eq!(cmds[1], "ls");
663 }
664
665 #[test]
666 fn test_run_loop_break_on_exit() {
667 let handler = |line: &str| -> Result<LoopAction, String> {
668 if line == "exit" {
669 Ok(LoopAction::Break)
670 } else {
671 Ok(LoopAction::Continue)
672 }
673 };
674
675 let mut reader = LineReader::new(100);
676 let mut input = keys(&[b"cmd1", ENTER, b"exit", ENTER, b"cmd2", ENTER]);
677 let mut out = Vec::new();
678 reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
679 }
681
682 #[test]
683 fn test_run_loop_error_continues() {
684 use std::sync::{Arc, Mutex};
685
686 let count = Arc::new(Mutex::new(0u32));
687 let count_clone = Arc::clone(&count);
688
689 let handler = move |line: &str| -> Result<LoopAction, String> {
690 *count_clone.lock().unwrap() += 1;
691 if line == "fail" {
692 Err("simulated error".to_string())
693 } else {
694 Ok(LoopAction::Continue)
695 }
696 };
697
698 let mut reader = LineReader::new(100);
699 let mut input = keys(&[b"ok", ENTER, b"fail", ENTER, b"ok2", ENTER, &[4]]);
700 let mut out = Vec::new();
701 reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
702
703 assert_eq!(*count.lock().unwrap(), 3);
705 }
706
707 #[test]
708 fn test_run_loop_with_history_navigation() {
709 use std::sync::{Arc, Mutex};
710
711 let executed = Arc::new(Mutex::new(Vec::new()));
712 let exec_clone = Arc::clone(&executed);
713
714 let handler = move |line: &str| -> Result<LoopAction, String> {
715 exec_clone.lock().unwrap().push(line.to_string());
716 Ok(LoopAction::Continue)
717 };
718
719 let mut reader = LineReader::new(100);
720 let mut input = keys(&[
722 b"echo hello", ENTER,
723 UP, ENTER, &[4], ]);
726 let mut out = Vec::new();
727 reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
728
729 let cmds = executed.lock().unwrap();
730 assert_eq!(cmds.len(), 2);
731 assert_eq!(cmds[0], "echo hello");
732 assert_eq!(cmds[1], "echo hello"); }
734
735 #[test]
736 fn test_run_loop_with_handle_parallel() {
737 use std::sync::{Arc, Mutex};
738 use crate::{CommandRegistry, handle_parallel, ArcVecWriter};
739
740 let registry = Arc::new(CommandRegistry::with_builtins());
741 let output = Arc::new(Mutex::new(Vec::<u8>::new()));
742
743 let reg = Arc::clone(®istry);
744 let out_ref = Arc::clone(&output);
745
746 let handler = move |line: &str| -> Result<LoopAction, String> {
747 if line == "exit" {
748 return Ok(LoopAction::Break);
749 }
750 let results = handle_parallel(
751 vec![line.to_string()],
752 Box::new(std::io::empty()),
753 Box::new(ArcVecWriter { inner: Arc::clone(&out_ref) }),
754 Arc::clone(®),
755 wasibox_core::CancellationToken::new(),
756 );
757 for res in results {
758 res?;
759 }
760 Ok(LoopAction::Continue)
761 };
762
763 let mut reader = LineReader::new(100);
764 let mut input = keys(&[
766 b"echo hello", ENTER,
767 UP, ENTER, b"exit", ENTER,
769 ]);
770 let mut term_out = Vec::new();
771 reader.run_loop_from(&mut input, &mut term_out, "$ ", &handler, None).unwrap();
772
773 let buf = output.lock().unwrap();
774 let result = String::from_utf8_lossy(&buf);
775 let lines: Vec<&str> = result.trim().lines().collect();
776 assert_eq!(lines.len(), 2);
777 assert_eq!(lines[0], "hello");
778 assert_eq!(lines[1], "hello"); }
780}