use std::io::{self, Read, Write};
#[cfg(unix)]
struct RawModeGuard {
original: libc::termios,
}
#[cfg(unix)]
impl RawModeGuard {
fn enter() -> io::Result<Self> {
let mut original: libc::termios = unsafe { std::mem::zeroed() };
if unsafe { libc::tcgetattr(libc::STDIN_FILENO, &mut original) } != 0 {
return Err(io::Error::last_os_error());
}
let mut raw = original;
raw.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG);
raw.c_cc[libc::VMIN] = 1;
raw.c_cc[libc::VTIME] = 0;
if unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &raw) } != 0 {
return Err(io::Error::last_os_error());
}
Ok(Self { original })
}
}
#[cfg(unix)]
impl Drop for RawModeGuard {
fn drop(&mut self) {
unsafe {
libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &self.original);
}
}
}
#[cfg(windows)]
mod win32 {
#[link(name = "kernel32")]
unsafe extern "system" {
pub fn GetStdHandle(nStdHandle: u32) -> isize;
pub fn GetConsoleMode(hConsoleHandle: isize, lpMode: *mut u32) -> i32;
pub fn SetConsoleMode(hConsoleHandle: isize, dwMode: u32) -> i32;
}
pub const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6; pub const ENABLE_PROCESSED_INPUT: u32 = 0x0001;
pub const ENABLE_LINE_INPUT: u32 = 0x0002;
pub const ENABLE_ECHO_INPUT: u32 = 0x0004;
pub const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;
}
#[cfg(windows)]
struct RawModeGuard {
handle: isize,
original_mode: u32,
}
#[cfg(windows)]
impl RawModeGuard {
fn enter() -> io::Result<Self> {
let handle = unsafe { win32::GetStdHandle(win32::STD_INPUT_HANDLE) };
if handle == -1 {
return Err(io::Error::last_os_error());
}
let mut original_mode: u32 = 0;
if unsafe { win32::GetConsoleMode(handle, &mut original_mode) } == 0 {
return Err(io::Error::last_os_error());
}
let new_mode = (original_mode
& !(win32::ENABLE_LINE_INPUT | win32::ENABLE_ECHO_INPUT | win32::ENABLE_PROCESSED_INPUT))
| win32::ENABLE_VIRTUAL_TERMINAL_INPUT;
if unsafe { win32::SetConsoleMode(handle, new_mode) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(Self {
handle,
original_mode,
})
}
}
#[cfg(windows)]
impl Drop for RawModeGuard {
fn drop(&mut self) {
unsafe {
win32::SetConsoleMode(self.handle, self.original_mode);
}
}
}
#[cfg(target_os = "wasi")]
struct RawModeGuard;
#[cfg(target_os = "wasi")]
impl RawModeGuard {
fn enter() -> io::Result<Self> {
Ok(Self)
}
}
pub trait LineHandler {
fn handle_line(&self, line: &str) -> Result<LoopAction, String>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopAction {
Continue,
Break,
}
impl<F> LineHandler for F
where
F: Fn(&str) -> Result<LoopAction, String>,
{
fn handle_line(&self, line: &str) -> Result<LoopAction, String> {
self(line)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyEvent {
Char(char),
Enter,
Backspace,
Delete,
Up,
Down,
Left,
Right,
Home,
End,
CtrlA,
CtrlE,
CtrlU,
CtrlK,
CtrlW,
CtrlD,
CtrlC,
}
pub struct LineEditor {
pub buffer: String,
pub cursor_pos: usize,
history_idx: usize,
saved_input: String,
}
impl LineEditor {
pub const fn new(history_len: usize) -> Self {
Self {
buffer: String::new(),
cursor_pos: 0,
history_idx: history_len,
saved_input: String::new(),
}
}
pub fn input_char(&mut self, code: u32, history: &[String]) -> Option<String> {
let key = match code {
1 => KeyEvent::CtrlA,
3 => KeyEvent::CtrlC,
4 => KeyEvent::CtrlD,
5 => KeyEvent::CtrlE,
8 | 127 => KeyEvent::Backspace,
11 => KeyEvent::CtrlK,
13 | 10 => KeyEvent::Enter,
21 => KeyEvent::CtrlU,
23 => KeyEvent::CtrlW,
27 => return None,
1001 => KeyEvent::Up,
1002 => KeyEvent::Down,
1003 => KeyEvent::Right,
1004 => KeyEvent::Left,
1005 => KeyEvent::Home,
1006 => KeyEvent::End,
1007 => KeyEvent::Delete,
c if c >= 0x20 => KeyEvent::Char(char::from_u32(c).unwrap_or(' ')),
_ => return None,
};
if key == KeyEvent::Enter {
Some(self.buffer.clone())
} else {
self.apply_key(key, history);
None
}
}
fn apply_key(&mut self, key: KeyEvent, history: &[String]) {
match key {
KeyEvent::Char(ch) => {
self.buffer.insert(self.cursor_pos, ch);
self.cursor_pos += 1;
}
KeyEvent::Backspace => {
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
self.buffer.remove(self.cursor_pos);
}
}
KeyEvent::Delete => {
if self.cursor_pos < self.buffer.len() {
self.buffer.remove(self.cursor_pos);
}
}
KeyEvent::Left => {
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
}
}
KeyEvent::Right => {
if self.cursor_pos < self.buffer.len() {
self.cursor_pos += 1;
}
}
KeyEvent::Home | KeyEvent::CtrlA => {
self.cursor_pos = 0;
}
KeyEvent::End | KeyEvent::CtrlE => {
self.cursor_pos = self.buffer.len();
}
KeyEvent::Up => {
if !history.is_empty() && self.history_idx > 0 {
if self.history_idx == history.len() {
self.saved_input = self.buffer.clone();
}
self.history_idx -= 1;
self.buffer = history[self.history_idx].clone();
self.cursor_pos = self.buffer.len();
}
}
KeyEvent::Down => {
if self.history_idx < history.len() {
self.history_idx += 1;
if self.history_idx == history.len() {
self.buffer = self.saved_input.clone();
} else {
self.buffer = history[self.history_idx].clone();
}
self.cursor_pos = self.buffer.len();
}
}
KeyEvent::CtrlU => {
self.buffer.clear();
self.cursor_pos = 0;
}
KeyEvent::CtrlK => {
self.buffer.truncate(self.cursor_pos);
}
KeyEvent::CtrlW => {
if self.cursor_pos > 0 {
let mut new_pos = self.cursor_pos;
while new_pos > 0 && self.buffer.as_bytes().get(new_pos - 1) == Some(&b' ') {
new_pos -= 1;
}
while new_pos > 0 && self.buffer.as_bytes().get(new_pos - 1) != Some(&b' ') {
new_pos -= 1;
}
self.buffer.drain(new_pos..self.cursor_pos);
self.cursor_pos = new_pos;
}
}
_ => {}
}
}
}
pub struct LineReader {
history: Vec<String>,
max_history: usize,
}
impl LineReader {
pub fn new(max_history: usize) -> Self {
Self {
history: Vec::new(),
max_history,
}
}
fn push_history(&mut self, line: &str) {
let trimmed = line.trim();
if trimmed.is_empty() {
return;
}
if self.history.last().map(|s| s.as_str()) == Some(trimmed) {
return;
}
self.history.push(trimmed.to_string());
if self.history.len() > self.max_history {
self.history.remove(0);
}
}
pub fn read_line(&mut self, prompt: &str, cancel_token: Option<wasibox_core::CancellationToken>) -> io::Result<Option<String>> {
let mut stdout = io::stdout();
write!(stdout, "{}", prompt)?;
stdout.flush()?;
let _guard = RawModeGuard::enter()?;
let mut reader = io::stdin();
self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
}
pub fn read_line_with_stdin(&mut self, prompt: &str, cancel_token: Option<wasibox_core::CancellationToken>, mut reader: Box<dyn Read>) -> io::Result<Option<String>> {
let mut stdout = io::stdout();
write!(stdout, "{}", prompt)?;
stdout.flush()?;
let _guard = RawModeGuard::enter()?;
self.read_line_from(&mut reader, &mut stdout, prompt, cancel_token)
}
pub fn run_loop<P, H>(&mut self, prompt_fn: P, handler: &H, cancel_token: wasibox_core::CancellationToken) -> io::Result<()>
where
P: Fn() -> String,
H: LineHandler,
{
loop {
let prompt = prompt_fn();
match self.read_line(&prompt, Some(cancel_token.clone()))? {
None => break,
Some(line) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
match handler.handle_line(trimmed) {
Ok(LoopAction::Continue) => {}
Ok(LoopAction::Break) => break,
Err(e) => {
eprintln!("{}", e);
}
}
}
}
}
Ok(())
}
pub fn run_loop_with_stdin<P, H>(&mut self, prompt_fn: P, handler: &H, cancel_token: wasibox_core::CancellationToken, mut reader: Box<dyn Read>) -> io::Result<()>
where
P: Fn() -> String,
H: LineHandler,
{
loop {
let prompt = prompt_fn();
let _guard = RawModeGuard::enter()?;
match self.read_line_from(&mut reader, &mut io::stdout(), &prompt, Some(cancel_token.clone()))? {
None => break,
Some(line) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
match handler.handle_line(trimmed) {
Ok(LoopAction::Continue) => {}
Ok(LoopAction::Break) => break,
Err(e) => {
eprintln!("{}", e);
}
}
}
}
}
Ok(())
}
#[cfg(test)]
fn run_loop_from<R: Read, W: Write, H: LineHandler>(
&mut self,
reader: &mut R,
writer: &mut W,
prompt: &str,
handler: &H,
cancel_token: Option<wasibox_core::CancellationToken>,
) -> io::Result<()> {
loop {
write!(writer, "{}", prompt)?;
writer.flush()?;
match self.read_line_from(reader, writer, prompt, cancel_token.clone())? {
None => break,
Some(line) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
match handler.handle_line(trimmed) {
Ok(LoopAction::Continue) => {}
Ok(LoopAction::Break) => break,
Err(e) => {
writeln!(writer, "Error: {}", e)?;
}
}
}
}
}
Ok(())
}
fn read_line_from<R: Read, W: Write>(
&mut self,
reader: &mut R,
writer: &mut W,
prompt: &str,
cancel_token: Option<wasibox_core::CancellationToken>,
) -> io::Result<Option<String>> {
let mut editor = LineEditor::new(self.history.len());
loop {
let b = {
let mut buf = [0u8; 1];
reader.read_exact(&mut buf)?;
buf[0]
};
let code = match b {
4 => {
if editor.buffer.is_empty() {
write!(writer, "\r\n")?;
writer.flush()?;
return Ok(None);
}
4
}
3 => {
if let Some(token) = &cancel_token {
token.cancel();
}
write!(writer, "^C\r\n")?;
writer.flush()?;
return Ok(Some(String::new()));
}
27 => {
let seq1 = {
let mut buf = [0u8; 1];
reader.read_exact(&mut buf)?;
buf[0]
};
if seq1 == b'[' {
let seq2 = {
let mut buf = [0u8; 1];
reader.read_exact(&mut buf)?;
buf[0]
};
match seq2 {
b'A' => 1001, b'B' => 1002, b'C' => 1003, b'D' => 1004, b'H' => 1005, b'F' => 1006, b'3' => {
let seq3 = {
let mut buf = [0u8; 1];
reader.read_exact(&mut buf)?;
buf[0]
};
if seq3 == b'~' {
1007 } else {
continue;
}
}
_ => continue,
}
} else {
continue;
}
}
other => other as u32,
};
let old_pos = editor.cursor_pos;
let old_len = editor.buffer.len();
if let Some(completed_line) = editor.input_char(code, &self.history) {
write!(writer, "\r\n")?;
writer.flush()?;
self.push_history(&completed_line);
return Ok(Some(completed_line));
}
if code >= 0x20 && code < 1000 && old_pos == old_len && editor.cursor_pos == editor.buffer.len() {
write!(writer, "{}", char::from_u32(code).unwrap())?;
writer.flush()?;
} else if code == 1004 && old_pos > editor.cursor_pos && old_pos > 0 { write!(writer, "\x1b[D")?;
writer.flush()?;
} else if code == 1003 && old_pos < editor.cursor_pos && old_pos < old_len { write!(writer, "\x1b[C")?;
writer.flush()?;
} else {
Self::redraw_line(writer, prompt, &editor.buffer, editor.cursor_pos)?;
}
}
}
fn redraw_line<W: Write>(
writer: &mut W,
prompt: &str,
line: &str,
cursor_pos: usize,
) -> io::Result<()> {
write!(writer, "\r\x1b[K{}{}", prompt, line)?;
let total_len = prompt.len() + line.len();
let target = prompt.len() + cursor_pos;
if target < total_len {
write!(writer, "\x1b[{}D", total_len - target)?;
}
writer.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn keys(parts: &[&[u8]]) -> Cursor<Vec<u8>> {
let mut buf = Vec::new();
for part in parts {
buf.extend_from_slice(part);
}
Cursor::new(buf)
}
const UP: &[u8] = b"\x1b[A";
const DOWN: &[u8] = b"\x1b[B";
const ENTER: &[u8] = b"\r";
#[test]
fn test_line_editor_basic() {
let mut editor = LineEditor::new(0);
let history = vec![];
assert!(editor.input_char('a' as u32, &history).is_none());
assert!(editor.input_char('b' as u32, &history).is_none());
assert_eq!(editor.buffer, "ab");
assert_eq!(editor.cursor_pos, 2);
assert!(editor.input_char(1004, &history).is_none()); assert_eq!(editor.cursor_pos, 1);
assert!(editor.input_char('c' as u32, &history).is_none());
assert_eq!(editor.buffer, "acb");
assert_eq!(editor.cursor_pos, 2);
assert!(editor.input_char(127, &history).is_none()); assert_eq!(editor.buffer, "ab");
assert_eq!(editor.cursor_pos, 1);
let result = editor.input_char(13, &history); assert_eq!(result, Some("ab".to_string()));
}
#[test]
fn test_line_editor_history() {
let history = vec!["first".to_string(), "second".to_string()];
let mut editor = LineEditor::new(history.len());
editor.input_char(1001, &history); assert_eq!(editor.buffer, "second");
editor.input_char(1001, &history); assert_eq!(editor.buffer, "first");
editor.input_char(1002, &history); assert_eq!(editor.buffer, "second");
editor.input_char(1002, &history); assert_eq!(editor.buffer, ""); }
#[test]
fn test_simple_input() {
let mut reader = LineReader::new(100);
let mut input = keys(&[b"hello", ENTER]);
let mut out = Vec::new();
let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
assert_eq!(result, Some("hello".to_string()));
}
#[test]
fn test_eof_on_empty() {
let mut reader = LineReader::new(100);
let mut input = Cursor::new(vec![4u8]); let mut out = Vec::new();
let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_history_up_arrow() {
let mut reader = LineReader::new(100);
let mut out = Vec::new();
let mut input = keys(&[b"echo hello", ENTER]);
reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
let mut input = keys(&[UP, ENTER]);
out.clear();
let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
assert_eq!(result, Some("echo hello".to_string()));
}
#[test]
fn test_history_up_down_arrow() {
let mut reader = LineReader::new(100);
let mut out = Vec::new();
let mut input = keys(&[b"first", ENTER]);
reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
let mut input = keys(&[b"second", ENTER]);
reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
let mut input = keys(&[UP, UP, DOWN, ENTER]);
out.clear();
let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
assert_eq!(result, Some("second".to_string()));
}
#[test]
fn test_history_down_restores_current_input() {
let mut reader = LineReader::new(100);
let mut out = Vec::new();
let mut input = keys(&[b"old", ENTER]);
reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
let mut input = keys(&[b"new", UP, DOWN, ENTER]);
out.clear();
let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
assert_eq!(result, Some("new".to_string()));
}
#[test]
fn test_history_dedup() {
let mut reader = LineReader::new(100);
let mut out = Vec::new();
let mut input = keys(&[b"dup", ENTER]);
reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
let mut input = keys(&[b"dup", ENTER]);
reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
let mut input = keys(&[UP, UP, ENTER]);
out.clear();
let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
assert_eq!(result, Some("dup".to_string()));
}
#[test]
fn test_history_max_size() {
let mut reader = LineReader::new(3);
let mut out = Vec::new();
for cmd in &["aaa", "bbb", "ccc", "ddd"] {
let mut input = keys(&[cmd.as_bytes(), ENTER]);
reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
}
let mut input = keys(&[UP, UP, UP, ENTER]);
out.clear();
let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
assert_eq!(result, Some("bbb".to_string()));
}
#[test]
fn test_backspace() {
let mut reader = LineReader::new(100);
let mut input = keys(&[b"helloo", &[127], ENTER]);
let mut out = Vec::new();
let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
assert_eq!(result, Some("hello".to_string()));
}
#[test]
fn test_ctrl_u_clears_line() {
let mut reader = LineReader::new(100);
let mut input = keys(&[b"garbage", &[21], b"clean", ENTER]); let mut out = Vec::new();
let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
assert_eq!(result, Some("clean".to_string()));
}
#[test]
fn test_empty_line_not_in_history() {
let mut reader = LineReader::new(100);
let mut out = Vec::new();
let mut input = keys(&[b"real", ENTER]);
reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
let mut input = keys(&[ENTER]);
reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
let mut input = keys(&[UP, ENTER]);
out.clear();
let result = reader.read_line_from(&mut input, &mut out, "$ ", None).unwrap();
assert_eq!(result, Some("real".to_string()));
}
#[test]
fn test_run_loop_with_handler() {
use std::sync::{Arc, Mutex};
let executed = Arc::new(Mutex::new(Vec::new()));
let exec_clone = Arc::clone(&executed);
let handler = move |line: &str| -> Result<LoopAction, String> {
exec_clone.lock().unwrap().push(line.to_string());
Ok(LoopAction::Continue)
};
let mut reader = LineReader::new(100);
let mut input = keys(&[b"echo hello", ENTER, b"ls", ENTER, &[4]]);
let mut out = Vec::new();
reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
let cmds = executed.lock().unwrap();
assert_eq!(cmds.len(), 2);
assert_eq!(cmds[0], "echo hello");
assert_eq!(cmds[1], "ls");
}
#[test]
fn test_run_loop_break_on_exit() {
let handler = |line: &str| -> Result<LoopAction, String> {
if line == "exit" {
Ok(LoopAction::Break)
} else {
Ok(LoopAction::Continue)
}
};
let mut reader = LineReader::new(100);
let mut input = keys(&[b"cmd1", ENTER, b"exit", ENTER, b"cmd2", ENTER]);
let mut out = Vec::new();
reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
}
#[test]
fn test_run_loop_error_continues() {
use std::sync::{Arc, Mutex};
let count = Arc::new(Mutex::new(0u32));
let count_clone = Arc::clone(&count);
let handler = move |line: &str| -> Result<LoopAction, String> {
*count_clone.lock().unwrap() += 1;
if line == "fail" {
Err("simulated error".to_string())
} else {
Ok(LoopAction::Continue)
}
};
let mut reader = LineReader::new(100);
let mut input = keys(&[b"ok", ENTER, b"fail", ENTER, b"ok2", ENTER, &[4]]);
let mut out = Vec::new();
reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
assert_eq!(*count.lock().unwrap(), 3);
}
#[test]
fn test_run_loop_with_history_navigation() {
use std::sync::{Arc, Mutex};
let executed = Arc::new(Mutex::new(Vec::new()));
let exec_clone = Arc::clone(&executed);
let handler = move |line: &str| -> Result<LoopAction, String> {
exec_clone.lock().unwrap().push(line.to_string());
Ok(LoopAction::Continue)
};
let mut reader = LineReader::new(100);
let mut input = keys(&[
b"echo hello", ENTER,
UP, ENTER, &[4], ]);
let mut out = Vec::new();
reader.run_loop_from(&mut input, &mut out, "$ ", &handler, None).unwrap();
let cmds = executed.lock().unwrap();
assert_eq!(cmds.len(), 2);
assert_eq!(cmds[0], "echo hello");
assert_eq!(cmds[1], "echo hello"); }
#[test]
fn test_run_loop_with_handle_parallel() {
use std::sync::{Arc, Mutex};
use crate::{CommandRegistry, handle_parallel, ArcVecWriter};
let registry = Arc::new(CommandRegistry::with_builtins());
let output = Arc::new(Mutex::new(Vec::<u8>::new()));
let reg = Arc::clone(®istry);
let out_ref = Arc::clone(&output);
let handler = move |line: &str| -> Result<LoopAction, String> {
if line == "exit" {
return Ok(LoopAction::Break);
}
let results = handle_parallel(
vec![line.to_string()],
Box::new(std::io::empty()),
Box::new(ArcVecWriter { inner: Arc::clone(&out_ref) }),
Arc::clone(®),
wasibox_core::CancellationToken::new(),
);
for res in results {
res?;
}
Ok(LoopAction::Continue)
};
let mut reader = LineReader::new(100);
let mut input = keys(&[
b"echo hello", ENTER,
UP, ENTER, b"exit", ENTER,
]);
let mut term_out = Vec::new();
reader.run_loop_from(&mut input, &mut term_out, "$ ", &handler, None).unwrap();
let buf = output.lock().unwrap();
let result = String::from_utf8_lossy(&buf);
let lines: Vec<&str> = result.trim().lines().collect();
assert_eq!(lines.len(), 2);
assert_eq!(lines[0], "hello");
assert_eq!(lines[1], "hello"); }
}