aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/ansi/mod.rs63
-rw-r--r--src/main.rs258
-rw-r--r--src/run/builtin.rs125
-rw-r--r--src/run/mod.rs18
-rw-r--r--src/rw.rs34
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> {
diff --git a/src/rw.rs b/src/rw.rs
index 601a2ec..89f6d76 100644
--- a/src/rw.rs
+++ b/src/rw.rs
@@ -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(),