diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/ansi/mod.rs | 63 | ||||
| -rw-r--r-- | src/main.rs | 258 | ||||
| -rw-r--r-- | src/run/builtin.rs | 125 | ||||
| -rw-r--r-- | src/run/mod.rs | 18 | ||||
| -rw-r--r-- | src/rw.rs | 34 |
5 files changed, 386 insertions, 112 deletions
diff --git a/src/ansi/mod.rs b/src/ansi/mod.rs index 4fc550b..0b43ede 100644 --- a/src/ansi/mod.rs +++ b/src/ansi/mod.rs @@ -114,26 +114,33 @@ fn read_escape(debug: bool) -> KeyboardInput { } } -pub fn read(debug: bool) -> KeyboardInput { - use KeyboardInput::*; - - let Some(x) = read1() else { - return KeyboardInput::Eof; - }; - - match x { - 1 => CtrlA, - 2 => CtrlB, - 3 => CtrlC, - 4 => CtrlD, - 5 => CtrlE, - 8 | 127 => DeleteLeft, - 12 => CtrlL, - 18 => CtrlR, - 27 => read_escape(debug), - b'\t' | b'\r' => Key(x), - x if !x.is_ascii_control() => Key(x), - x => todo!("unimplemented control code: {x}"), +pub fn read(debug: bool) -> Option<KbInput<'static>> { + let trie = EscapeTrie::from(ti()); + let trie = Box::leak(Box::new(trie)); // TODO don't leak memory, this is only temporary + let mut reader = EscapingStdinReader::new(trie); + loop { + let Some(b) = read1() else { + break None; + }; + match reader.process_byte(b) { + ByteProcessingResult::Done(kb) => { + if debug { + if let KbInput::Escape(e) = &kb { + for k in e.keys.iter() { + print!("{k} ") + } + } + println!("{}\r", kb.as_bytes().escape_ascii()); + } + if let KbInput::InvalidEscape(x) = &kb { + if x.len() == 1 { + break Some(KbInput::Key([x[0]])); + } + } + break Some(kb); + } + ByteProcessingResult::Continue(r) => reader = r, + } } } @@ -183,7 +190,8 @@ enum EscapeTrie { More(BTreeMap<u8, EscapeTrie>), } -enum KbInput<'a> { +#[derive(Debug)] +pub enum KbInput<'a> { Key([u8; 1]), Escape(Escape<'a>), InvalidEscape(Vec<u8>), @@ -199,9 +207,10 @@ impl<'a> KbInput<'a> { } } -struct Escape<'a> { - keys: &'a [&'a str], - value: Vec<u8>, +#[derive(Debug)] +pub struct Escape<'a> { + pub keys: &'a [&'a str], + pub value: Vec<u8>, } use terminfo_lean::parse::Terminfo; @@ -280,12 +289,12 @@ fn trie_from_words(words: Vec<(&'static str, &[u8])>) -> EscapeTrie { } } -impl From<&'static Terminfo<'static>> for EscapeTrie { - fn from(ti: &'static Terminfo<'static>) -> Self { +impl From<&Terminfo<'static>> for EscapeTrie { + fn from(ti: &Terminfo<'static>) -> Self { let w: Vec<(&'static str, &'static [u8])> = ti .strings .iter() - .filter(|(_, v)| !is_parametrized(v)) + .filter(|(k, v)| k.starts_with("k") && !is_parametrized(v)) .map(|(k, v)| (*k, *v)) .collect(); trie_from_words(w) diff --git a/src/main.rs b/src/main.rs index d388ce1..f2b7544 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,7 +39,7 @@ use crate::completion::{PathCache, completion}; use crate::ctrlc::CtrlC; use crate::cursor::{Direction, move_cursor}; use crate::history::HistoryEntry; -use crate::parse::{Block, ExpString, Parse}; +use crate::parse::{Block, ExpString, Parse, PostExpansion}; macro_rules! print { ($($x:tt)*) => {{ @@ -89,6 +89,11 @@ pub struct Session { path_cache: PathCache, ctrlc: CtrlC, + /// terminfo identifier to command invocation + ti_keybinds: HashMap<BString, parse::Command<PostExpansion>>, + /// byte literals to command invocation + ascii_keybinds: HashMap<BString, parse::Command<PostExpansion>>, + debug_keystrokes: bool, loud: bool, @@ -146,6 +151,11 @@ impl Session { self.line.clear(); } + fn prompt_clear(&mut self) { + self.clear_prompt(); + self.history_visit = 0; + } + fn reprint_prompt(&self) { print!("{}", self.prompt()); self.line.display_pre(); @@ -180,49 +190,6 @@ impl Session { } } - // move to next - fn move_left_word(&mut self) { - let mut i = 0; - - // find word - while let Some(b' ') = self.line.get_left() { - self.line.left(); - i += 1; - } - - // skip it - while let Some(x) = self.line.get_left() - && !x.is_ascii_whitespace() - { - self.line.left(); - i += 1; - } - - cursor::move_cursor(Direction::Left, i); - io::stdout().flush().unwrap(); - } - - fn move_right_word(&mut self) { - let mut i = 0; - - // find word - while let Some(b' ') = self.line.get_right() { - self.line.right(); - i += 1; - } - - // skip it - while let Some(x) = self.line.get_right() - && !x.is_ascii_whitespace() - { - self.line.right(); - i += 1; - } - - cursor::move_cursor(Direction::Right, i); - io::stdout().flush().unwrap(); - } - fn type_byte(&mut self, b: u8) { self.line.add(b); io::stdout().lock().write_all(&[b]).unwrap(); @@ -273,6 +240,82 @@ impl Session { } self.line.display_post(&vec![b' '; del]); } + + fn cursor_left(&mut self) { + if self.line.left() { + move_cursor(Direction::Left, 1); + io::stdout().lock().flush().unwrap(); + } + } + + fn cursor_right(&mut self) { + if self.line.right() { + move_cursor(Direction::Right, 1); + io::stdout().lock().flush().unwrap(); + } + } + + // move to next + fn cursor_left_word(&mut self) { + let mut i = 0; + + // find word + while let Some(b' ') = self.line.get_left() { + self.line.left(); + i += 1; + } + + // skip it + while let Some(x) = self.line.get_left() + && !x.is_ascii_whitespace() + { + self.line.left(); + i += 1; + } + + cursor::move_cursor(Direction::Left, i); + io::stdout().flush().unwrap(); + } + + fn cursor_right_word(&mut self) { + let mut i = 0; + + // find word + while let Some(b' ') = self.line.get_right() { + self.line.right(); + i += 1; + } + + // skip it + while let Some(x) = self.line.get_right() + && !x.is_ascii_whitespace() + { + self.line.right(); + i += 1; + } + + cursor::move_cursor(Direction::Right, i); + io::stdout().flush().unwrap(); + } + + fn complete(session: Arc<Mutex<Session>>) { + let cmd = session.lock().unwrap().line.pre().to_vec(); + + let comp = completion(session.clone(), &cmd); + + let mut se = session.lock().unwrap(); + + se.type_bytes(&comp.shared_prefix); + + if comp.suggestions.len() > 1 { + print!("\r\n"); + for s in comp.suggestions { + io::stdout().lock().write_all(&s.display).unwrap(); + println!(); + } + se.reprint_prompt(); + } + } } fn exec_rc_file(se: Arc<Mutex<Session>>) { @@ -332,8 +375,10 @@ fn event_loop() { aliases: run::Aliases::new(), path_cache: Default::default(), ctrlc: Default::default(), - debug_keystrokes: false, + debug_keystrokes: true, loud: false, + ti_keybinds: HashMap::new(), + ascii_keybinds: HashMap::new(), }; let session = Arc::new(Mutex::new(se)); @@ -348,20 +393,102 @@ fn event_loop() { let _sock_dropper = export_fun::listen(session.clone()); let _ctrlc = ctrlc::setup(session.clone()); - loop { + 'repl: loop { let mut se = session.lock().unwrap(); - use ansi::KeyboardInput as Kb; - match ansi::read(se.debug_keystrokes) { - Kb::CtrlA => se.move_to_begin(), - Kb::CtrlB => { - println!(" Ctrl+B is not yet implemented"); - se.reprint_prompt(); + let Some(key) = ansi::read(se.debug_keystrokes) else { + break; + }; + + if se.debug_keystrokes { + println!("{key:?}"); + } + + if let Some(cmd) = se.ascii_keybinds.get(key.as_bytes()) { + let cmd = cmd.clone(); + drop(se); + // not sure if/how to report this error - would be strange to print something to console every time a keybind command returns nonzero exit code. + let _ = run::run_quiet(session.clone(), cmd); + continue 'repl; + } + + match key { + // TODO: make simple characters also keybinds and do not specially handle tab or newline here. + ansi::KbInput::Key([b'\t']) => { + drop(se); + Session::complete(session.clone()); } - Kb::CtrlC => { - se.clear_prompt(); - se.history_visit = 0; + ansi::KbInput::Key([b'\r' | b'\n']) => { + let line = se.line.into_bytes(); + + if !line.is_empty() { + let parsed = match parse::do_parse(&line) { + Ok(p) => p, + Err((crate::parse::ParseError::Eof, _)) => { + se.line.add(b'\n'); + print!("\r\n> "); + continue; + } + Err(e) => { + println!("{e:?}\n{}", se.prompt()); + continue; + } + }; + print!("\r\n"); + let entry = HistoryEntry::new(line.clone()); + history::persist(&entry); + se.history.push(entry); + se.history_visit = 0; + se.line.dump(); + drop(se); + run::run(session.clone(), parsed); + } } + ansi::KbInput::Key([x]) => se.type_byte(x), + ansi::KbInput::Escape(escape) => { + for terminfo_key in escape.keys.iter() { + if let Some(cmd) = se.ti_keybinds.get(terminfo_key.as_bytes()) { + let cmd = cmd.clone(); + drop(se); + // not sure if/how to report this error - would be strange to print something to console every time a keybind command returns nonzero exit code. + let _ = run::run_quiet(session.clone(), cmd); + continue 'repl; + } + } + + if matches!(&escape.value[..], b"\r" | b"\n") { + let line = se.line.into_bytes(); + + if !line.is_empty() { + let parsed = match parse::do_parse(&line) { + Ok(p) => p, + Err((crate::parse::ParseError::Eof, _)) => { + se.line.add(b'\n'); + print!("\r\n> "); + continue; + } + Err(e) => { + println!("{e:?}\n{}", se.prompt()); + continue; + } + }; + print!("\r\n"); + let entry = HistoryEntry::new(line.clone()); + history::persist(&entry); + se.history.push(entry); + se.history_visit = 0; + se.line.dump(); + drop(se); + run::run(session.clone(), parsed); + } + } + } + ansi::KbInput::InvalidEscape(_) => continue, + } + + /* + match key { + Kb::CtrlA => se.move_to_begin(), Kb::CtrlE => se.move_to_end(), Kb::Eof | Kb::CtrlD => break, Kb::CtrlL => { @@ -401,25 +528,7 @@ fn event_loop() { run::run(session.clone(), parsed); } } - Kb::Key(b'\t') => { - let cmd = se.line.pre().to_vec(); - drop(se); - - let comp = completion(session.clone(), &cmd); - - let mut se = session.lock().unwrap(); - - se.type_bytes(&comp.shared_prefix); - - if comp.suggestions.len() > 1 { - print!("\r\n"); - for s in comp.suggestions { - io::stdout().lock().write_all(&s.display).unwrap(); - println!(); - } - se.reprint_prompt(); - } - } + Kb::Key(b'\t') => {} Kb::Arrow(dir) => match dir { Direction::Up => se.history_up(), Direction::Down => se.history_down(), @@ -459,6 +568,7 @@ fn event_loop() { Kb::End => se.move_to_end(), Kb::Key(x) => se.type_byte(x), } + */ } session.lock().unwrap().raw.disable(); diff --git a/src/run/builtin.rs b/src/run/builtin.rs index f5dff81..507759f 100644 --- a/src/run/builtin.rs +++ b/src/run/builtin.rs @@ -601,3 +601,128 @@ impl Builtin for terminfo { Ok(()) } } + +pub struct bind; +impl Builtin for bind { + fn name(&self) -> &str { + "bind" + } + + fn io( + &self, + session: Arc<Mutex<Session>>, + args: &[BString], + _stdin: &mut dyn Read, + stdout: &mut dyn Write, + ) -> Result { + let mut usage = || { + writeln!( + stdout, + "usage: bind ti NAMED_KEYBIND COMMAND | bind key KEY COMMAND" + )?; + Err(Error::Exit(1)) + }; + + if args.len() < 3 { + return usage(); + } + + let kind = &args[0]; + let key = &args[1]; + let cmd = &args[2]; + let args = &args[3..]; + + let mut se = session.lock().unwrap(); + + let map = match &kind[..] { + b"ti" => &mut se.ti_keybinds, + b"key" => &mut se.ascii_keybinds, + _ => return usage(), + }; + + map.insert( + key.clone(), + crate::parse::Command { + cmd: cmd.clone(), + args: args.to_vec(), + }, + ); + + Ok(()) + } +} + +pub struct exit; +impl Builtin for exit { + fn name(&self) -> &str { + "exit" + } + + fn special(&self, _session: Arc<Mutex<Session>>, args: &[BString]) { + let exit_code: i32 = loop { + let Some(arg) = args.get(0) else { + break 0; + }; + let Ok(arg) = String::from_utf8(arg.clone()) else { + break 1; + }; + let Ok(num) = arg.parse() else { + break 1; + }; + break num; + }; + + std::process::exit(exit_code); + } + + fn io( + &self, + _session: Arc<Mutex<Session>>, + _args: &[BString], + _stdin: &mut dyn Read, + _stdout: &mut dyn Write, + ) -> Result { + Ok(()) + } +} + +/// control terminal +pub struct ct; +impl Builtin for ct { + fn name(&self) -> &str { + "ct" + } + + fn io( + &self, + session: Arc<Mutex<Session>>, + args: &[BString], + _stdin: &mut dyn Read, + _stdout: &mut dyn Write, + ) -> Result { + let Some(arg) = args.get(0) else { + return Err(Error::Exit(-1)); + }; + + let mut se = session.lock().unwrap(); + + match &arg[..] { + b"cursor_begin" => se.move_to_begin(), + b"cursor_end" => se.move_to_end(), + b"cursor_right" => se.cursor_right(), + b"cursor_left" => se.cursor_left(), + b"cursor_right_word" => se.cursor_right_word(), + b"cursor_left_word" => se.cursor_left_word(), + b"prompt_clear" => se.prompt_clear(), + b"complete" => { + drop(se); + Session::complete(session) + } + b"history_previous" => se.history_up(), + b"history_next" => se.history_down(), + _ => return Err(Error::Exit(-2)), + } + + Ok(()) + } +} diff --git a/src/run/mod.rs b/src/run/mod.rs index c866c6e..a31ff1a 100644 --- a/src/run/mod.rs +++ b/src/run/mod.rs @@ -477,6 +477,21 @@ fn exec(se: Arc<Mutex<Session>>, ast: Ast<PreExpansion>) -> Result<(), ExecError exec.exec_loop(cmd, &mut [c1, c2]) } +pub fn run_quiet( + se: Arc<Mutex<Session>>, + cmd: parse::Command<PostExpansion>, +) -> Result<(), ExecError> { + let mut exec = Executor { + se: se.clone(), + args: None, + expand_commands: true, + }; + let (i, c1) = InputReader::new(Input::Null); + let (o, c2) = OutputWriter::new(Output::Null); + let cmd = exec.execute_pipeline(parse::Pipes { cmds: vec![cmd] }, i, o); + exec.exec_loop(cmd, &mut [c1, c2]) +} + pub fn run(se: Arc<Mutex<Session>>, parsed: Ast<PreExpansion>) { se.lock().unwrap().raw.disable(); let result = exec(se.clone(), parsed); @@ -550,6 +565,9 @@ const BUILTINS: &[&'static dyn Builtin] = &[ #[cfg(debug_assertions)] &builtin::debug, &builtin::terminfo, + &builtin::bind, + &builtin::exit, + &builtin::ct, ]; pub fn builtin_map() -> HashMap<BString, &'static dyn Builtin> { @@ -13,12 +13,14 @@ use std::{ use nix::poll::{PollFd, PollFlags}; pub enum Input { + Null, Stdin, Pipe(PipeReader), File(File), } pub enum Output { + Null, Stdout, Pipe(PipeWriter), File(File), @@ -27,6 +29,7 @@ pub enum Output { impl From<Input> for Stdio { fn from(value: Input) -> Self { match value { + Input::Null => Stdio::null(), Input::Stdin => Stdio::inherit(), Input::Pipe(reader) => reader.into(), Input::File(file) => file.into(), @@ -37,6 +40,7 @@ impl From<Input> for Stdio { impl From<Output> for Stdio { fn from(value: Output) -> Stdio { match value { + Output::Null => Stdio::null(), Output::Stdout => Stdio::inherit(), Output::Pipe(writer) => writer.into(), Output::File(file) => file.into(), @@ -47,6 +51,7 @@ impl From<Output> for Stdio { impl Input { pub fn try_clone(&self) -> io::Result<Self> { Ok(match self { + Input::Null => Input::Null, Input::Stdin => Input::Stdin, Input::Pipe(pr) => Input::Pipe(pr.try_clone()?), Input::File(f) => Input::File(f.try_clone()?), @@ -57,6 +62,7 @@ impl Input { impl Output { pub fn try_clone(&self) -> io::Result<Self> { Ok(match self { + Output::Null => Output::Null, Output::Stdout => Output::Stdout, Output::Pipe(pw) => Output::Pipe(pw.try_clone()?), Output::File(f) => Output::File(f.try_clone()?), @@ -116,17 +122,18 @@ enum PollStatus { fn check<'a>( canceled: &AtomicBool, cancel: &PipeReader, - fd: BorrowedFd<'a>, + fd: Option<BorrowedFd<'a>>, flags: PollFlags, ) -> PollStatus { if canceled.load(SeqCst) { return PollStatus::Cancel; } - let mut poll_fds = [ - PollFd::new(cancel.as_fd(), PollFlags::POLLIN), - PollFd::new(fd, flags), - ]; + let mut poll_fds = Vec::with_capacity(2); + poll_fds.push(PollFd::new(cancel.as_fd(), PollFlags::POLLIN)); + if let Some(fd) = fd { + poll_fds.push(PollFd::new(fd, flags)); + } if nix::poll::poll(&mut poll_fds, TIMEOUT_MS).is_err() { canceled.store(true, SeqCst); @@ -153,9 +160,10 @@ impl InputReader { fn poll(&mut self) -> PollStatus { let stdin = io::stdin(); let read_fd = match &self.input { - Input::Stdin => stdin.as_fd(), - Input::Pipe(pipe) => pipe.as_fd(), - Input::File(file) => file.as_fd(), + Input::Null => None, + Input::Stdin => Some(stdin.as_fd()), + Input::Pipe(pipe) => Some(pipe.as_fd()), + Input::File(file) => Some(file.as_fd()), }; check(&self.canceled, &self.cancel, read_fd, PollFlags::POLLIN) } @@ -181,6 +189,7 @@ impl Read for InputReader { PollStatus::Wait => continue, } return match &mut self.input { + Input::Null => Ok(0), Input::Stdin => io::stdin().read(buf), Input::Pipe(reader) => reader.read(buf), Input::File(file) => file.read(buf), @@ -210,9 +219,10 @@ impl OutputWriter { fn poll(&mut self) -> PollStatus { let stdout = io::stdout(); let write_fd = match &self.output { - Output::Stdout => stdout.as_fd(), - Output::Pipe(pipe) => pipe.as_fd(), - Output::File(file) => file.as_fd(), + Output::Null => None, + Output::Stdout => Some(stdout.as_fd()), + Output::Pipe(pipe) => Some(pipe.as_fd()), + Output::File(file) => Some(file.as_fd()), }; check(&self.canceled, &self.cancel, write_fd, PollFlags::POLLOUT) } @@ -237,6 +247,7 @@ impl Write for OutputWriter { PollStatus::Wait => continue, } return match &mut self.output { + Output::Null => Ok(buf.len()), Output::Stdout => io::stdout().write(buf), Output::Pipe(writer) => writer.write(buf), Output::File(file) => file.write(buf), @@ -246,6 +257,7 @@ impl Write for OutputWriter { fn flush(&mut self) -> io::Result<()> { match &mut self.output { + Output::Null => Ok(()), Output::Stdout => io::stdout().flush(), Output::Pipe(writer) => writer.flush(), Output::File(file) => file.flush(), |
