From 399a9e1763aaac0f216dd24482149c577521a987 Mon Sep 17 00:00:00 2001 From: Krzysztof Juszczyk Date: Thu, 15 Feb 2024 14:11:36 +0100 Subject: [PATCH 1/8] [#55213] Remove TODO prints, improve error messages --- src/interpreter.rs | 10 +++++----- src/shell_base.rs | 3 --- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/interpreter.rs b/src/interpreter.rs index dfe7609..0083553 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -306,7 +306,7 @@ impl<'a> InputInterpreter<'a> { self.handle_compound_command(shell, cmd, background, redirects) } ast::PipeableCommand::FunctionDef(_name, _cmds) => { - eprintln!("FunctionDef not yet handled (but it would be cool)"); + eprintln!("FunctionDef not handled"); EXIT_FAILURE } } @@ -380,7 +380,7 @@ impl<'a> InputInterpreter<'a> { self.handle_compound_case(shell, word, arms, background) } any => { - eprintln!("CompoundCommandKind not yet handled: {any:#?}"); + eprintln!("CompoundCommandKind not handled: {any:#?}"); EXIT_FAILURE } }; @@ -823,7 +823,7 @@ impl<'a> InputInterpreter<'a> { } // TODO: Heredoc (multiline command parsing) implementation any => { - eprintln!("Redirect not yet handled: {any:?}"); + eprintln!("Redirect not handled: {any:?}"); None } } @@ -914,7 +914,7 @@ impl<'a> InputInterpreter<'a> { }, )), any => { - eprintln!("parameter not yet handled: {any:?}"); + eprintln!("parameter not handled: {any:?}"); None } }, @@ -923,7 +923,7 @@ impl<'a> InputInterpreter<'a> { ast::SimpleWord::SquareOpen => Some("[".to_string()), ast::SimpleWord::SquareClose => Some("]".to_string()), any => { - eprintln!("simple word not yet handled: {any:?}"); + eprintln!("simple word not handled: {any:?}"); None } } diff --git a/src/shell_base.rs b/src/shell_base.rs index 570767b..26b84d4 100644 --- a/src/shell_base.rs +++ b/src/shell_base.rs @@ -709,14 +709,12 @@ impl Shell { escaped = false; } [0x33, 0x3b] => { - println!("TODO: SHIFT + DELETE"); let mut c4 = [0; 2]; // TWO MORE! TODO: improve! io::stdin().read_exact(&mut c4).unwrap(); escaped = false; } _ => { - println!("TODO: [ + 0x{:02x} + 0x{:02x}", c2, c3); escaped = false; } } @@ -786,7 +784,6 @@ impl Shell { escaped = false; } _ => { - println!("WE HAVE UNKNOWN CONTROL CODE '[' + {}", c2); escaped = false; } } From 0900a27dfc6c7b04ef370cab2a5bd9f1892867e8 Mon Sep 17 00:00:00 2001 From: Krzysztof Juszczyk Date: Fri, 23 Feb 2024 16:20:16 +0100 Subject: [PATCH 2/8] [#55213] Include `vte` crate, implement `Perform` trait for `Shell` struct --- Cargo.lock | 28 +++++++++++ Cargo.toml | 1 + src/shell_base.rs | 126 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index cdc32fd..277965f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,6 +74,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + [[package]] name = "autocfg" version = "1.1.0" @@ -670,6 +676,27 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "vte" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "wash" version = "0.1.0" @@ -683,6 +710,7 @@ dependencies = [ "nix 0.26.4", "os_pipe", "regex", + "vte", "wasi", "wasi_ext_lib", ] diff --git a/Cargo.toml b/Cargo.toml index c4ff7a2..0a9de79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ color-eyre = "0.5" lazy_static = "1" regex = "1" glob = "0.3" +vte = "0.13.0" [target.'cfg(target_os = "wasi")'.dependencies] wasi_ext_lib = { git = "https://github.com/antmicro/wasi_ext_lib.git", branch = "main", features = ["hterm"] } diff --git a/src/shell_base.rs b/src/shell_base.rs index 26b84d4..ac84466 100644 --- a/src/shell_base.rs +++ b/src/shell_base.rs @@ -26,6 +26,8 @@ use std::path::{Path, PathBuf}; #[cfg(target_os = "wasi")] use wasi; +use vte::{Params, Parser, Perform}; + #[cfg(target_os = "wasi")] use wasi_ext_lib::termios; @@ -504,6 +506,7 @@ pub struct Shell { pub history: Vec, history_path: PathBuf, + input: String, should_echo: bool, cursor_position: usize, insert_mode: bool, @@ -512,6 +515,128 @@ pub struct Shell { reader: InternalReader, } +impl Perform for Shell { + fn print(&mut self, c: char) { + // regular characters + if self.cursor_position == self.input.len() { + self.input.push(c); + self.echo(&c.to_string()); + } else if self.insert_mode { + // in insert mode, when cursor is in the middle, new character expand CLI + // instead of replacing charcter under cursor + self.input.insert(self.cursor_position, c); + + // for wasi target, we assume that hterm has enabled insert mode + #[cfg(target_os = "wasi")] + self.echo(&c.to_string()); + + #[cfg(not(target_os = "wasi"))] + self.echo(&format!("\x1b[@{}", c)); + } else { + self.input.replace_range( + self.cursor_position..self.cursor_position + 1, + &c.to_string(), + ); + + self.echo(&c.to_string()); + } + + self.cursor_position += 1; + } + + fn execute(&mut self, byte: u8) { + // C0 and C1 control functions + match byte { + // enter + 0x10 | 0x13 => { + self.echo("\n"); + self.cursor_position = 0; + self.input = self.input.trim().to_string(); + } + // backspace + 127 => { + if !self.input.is_empty() && self.cursor_position > 0 { + self.echo("\x1b[D\x1b[P"); + self.input.remove(self.cursor_position - 1); + self.cursor_position -= 1; + } + } + _ => {/* ignore for now */} + } + } + + fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _c: char) { + /* ignore for now */ + } + + fn put(&mut self, _byte: u8) { + /* ignore for now */ + } + + fn unhook(&mut self) { + /* ignore for now */ + } + + fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) { + /* ignore for now */ + } + + fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, c: char) { + if params.len() == 1 { + let param = params.iter().next().unwrap(); + match (param[0], c) { + // UpArrow + (_, 'A') => { + + } + // DownArrow + (_, 'B') => { + + } + // RightArrow + (_, 'C') => { + + } + // LeftArrow + (_, 'D') => { + + } + // End + (_, 'F') => { + + } + // Home + (_, 'H') => { + + } + // PageUp + (5, '~') => { + + } + // PageDown + (6, '~') => { + + } + // Insert + (7, '~') => { + + } + // Del + (8, '~') => { + + } + (_, _) => { + + } + } + } + } + + fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) { + /* ignore for now */ + } +} + impl Shell { pub fn new(should_echo: bool, pwd: &str, args: VecDeque) -> Self { Shell { @@ -532,6 +657,7 @@ impl Shell { env!("CARGO_PKG_NAME") ) }), + input: String::new(), vars: HashMap::new(), last_exit_status: EXIT_SUCCESS, last_job_pid: None, From 0c507a2bc336d445d0efec76deb940e526a28f62 Mon Sep 17 00:00:00 2001 From: Krzysztof Juszczyk Date: Mon, 26 Feb 2024 16:20:04 +0100 Subject: [PATCH 3/8] [#55213] Create `cli` module, move CLI logic to `cli` module, fix handling `Del` and `Insert` --- src/cli.rs | 275 ++++++++++++++++++++++++++++++ src/internals.rs | 2 +- src/lib.rs | 1 + src/shell_base.rs | 425 +++------------------------------------------- 4 files changed, 301 insertions(+), 402 deletions(-) create mode 100644 src/cli.rs diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..819b38c --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2022-2024 Antmicro + * + * SPDX-License-Identifier: Apache-2.0 + */ + +use std::io; +use std::io::Write; + +use vte::{Params, Perform}; + +pub struct Cli { + pub history: Vec, + pub should_echo: bool, + pub cursor_position: usize, + pub input: String, + + history_entry_to_display: i32, + input_ready: bool, + input_stash: String, + insert_mode: bool, +} + +impl Cli { + pub fn new(should_echo: bool) -> Self { + Cli { + cursor_position: 0, + history: Vec::new(), + history_entry_to_display: -1, + input: String::new(), + input_ready: false, + input_stash: String::new(), + insert_mode: true, + should_echo, + } + } + + pub fn is_input_ready(&self) -> bool { + self.input_ready + } + + pub fn reset(&mut self) { + self.cursor_position = 0; + self.history_entry_to_display = -1; + self.input.clear(); + self.input_ready = false; + self.input_stash.clear(); + + if !self.insert_mode { + self.insert_mode = true; + } + + } + + fn echo(&self, output: &str) { + if self.should_echo { + // TODO: should this maybe use OutputDevice too? + print!("{output}"); + } else if output.contains('\n') { + println!(); + } + } + + fn get_cursor_to_beginning(&mut self) { + if self.cursor_position > 0 { + // bring cursor to the beggining with `ESC[nD` escape sequence + self.echo(&format!("\x1b[{}D", self.cursor_position)); + } + self.cursor_position = 0; + } + + fn get_cursor_to_end(&mut self) { + let to_end = self.input.len() - self.cursor_position; + if self.input.len() - self.cursor_position > 0 { + // bring cursor to the end with `ESC[nC` escape sequence + self.echo(&format!("\x1b[{}C", to_end)); + } + self.cursor_position = self.input.len(); + } + + fn erase_input(&mut self) { + // bring cursor to the beginning and clear line to the right with `ESC[0K` + self.get_cursor_to_beginning(); + self.echo("\x1b[0K"); + } +} + +impl Perform for Cli { + fn print(&mut self, c: char) { + let byte = c as u8; + match byte { + // backspace + 0x7f => { + if !self.input.is_empty() && self.cursor_position > 0 { + self.echo("\x1b[D\x1b[P"); + self.input.remove(self.cursor_position - 1); + self.cursor_position -= 1; + } + } + // regular characters + _ => { + if self.cursor_position == self.input.len() { + self.input.push(c); + self.echo(&c.to_string()); + } else if self.insert_mode { + // in insert mode, when cursor is in the middle, new character expand CLI + // instead of replacing charcter under cursor + self.input.insert(self.cursor_position, c); + + // for wasi target, we assume that hterm has enabled insert mode + #[cfg(target_os = "wasi")] + self.echo(&c.to_string()); + + #[cfg(not(target_os = "wasi"))] + self.echo(&format!("\x1b[@{}", c)); + } else { + self.input.replace_range( + self.cursor_position..self.cursor_position + 1, + &c.to_string(), + ); + + #[cfg(target_os = "wasi")] + self.echo(&format!("\x1b[P{}", c)); + + #[cfg(not(target_os = "wasi"))] + self.echo(&c.to_string()); + } + + self.cursor_position += 1; + } + } + io::stdout().flush().unwrap(); + } + + fn execute(&mut self, byte: u8) { + // C0 and C1 control functions + match byte { + // enter + 0xa | 0xd => { + self.echo("\n"); + self.cursor_position = 0; + self.input = self.input.trim().to_string(); + self.input_ready = true; + } + _ => {/* ignore for now */} + } + io::stdout().flush().unwrap(); + } + + fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _c: char) { + /* ignore for now */ + } + + fn put(&mut self, _byte: u8) { + /* ignore for now */ + } + + fn unhook(&mut self) { + /* ignore for now */ + } + + fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) { + /* ignore for now */ + } + + fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, c: char) { + if params.len() == 1 { + let param = params.iter().next().unwrap(); + match (param[0], c) { + // UpArrow + (_, 'A') => { + if !self.history.is_empty() && self.history_entry_to_display != 0 { + if self.history_entry_to_display == -1 { + self.history_entry_to_display = (self.history.len() - 1) as i32; + self.input_stash = self.input.clone(); + } else if self.history_entry_to_display > 0 { + self.history_entry_to_display -= 1; + } + + self.erase_input(); + self.input = + self.history[self.history_entry_to_display as usize].clone(); + self.cursor_position = self.input.len(); + self.echo(&self.input); + } + } + // DownArrow + (_, 'B') => { + if self.history_entry_to_display != -1 { + self.erase_input(); + if self.history.len() - 1 > (self.history_entry_to_display as usize) + { + self.history_entry_to_display += 1; + self.input = + self.history[self.history_entry_to_display as usize].clone(); + } else { + self.input = self.input_stash.clone(); + self.history_entry_to_display = -1; + } + self.cursor_position = self.input.len(); + self.echo(&self.input); + } + } + // RightArrow + (_, 'C') => { + if self.cursor_position < self.input.len() { + // move cursor right with `ESC[C` + self.echo("\x1b[C"); + self.cursor_position += 1; + } + } + // LeftArrow + (_, 'D') => { + if self.cursor_position > 0 { + // move cursor left with `ESC[D` + self.echo("\x1b[D"); + self.cursor_position -= 1; + } + } + // End + (_, 'F') => { + self.get_cursor_to_end(); + } + // Home + (_, 'H') => { + self.get_cursor_to_beginning(); + } + // Insert + (2, '~') => { + self.insert_mode = !self.insert_mode; + } + // Del + (3, '~') => { + if self.input.len() - self.cursor_position > 0 { + self.echo("\x1b[P"); + self.input.remove(self.cursor_position); + } + } + // PageUp + (5, '~') => { + if !self.history.is_empty() && self.history_entry_to_display != 0 { + if self.history_entry_to_display == -1 { + self.input_stash = self.input.clone(); + } + self.history_entry_to_display = 0; + self.erase_input(); + self.input = self.history[0].clone(); + self.cursor_position = self.input.len(); + self.echo(&self.input); + } + } + // PageDown + (6, '~') => { + if self.history_entry_to_display != -1 { + self.erase_input(); + self.input = self.input_stash.clone(); + self.history_entry_to_display = -1; + self.cursor_position = self.input.len(); + self.echo(&self.input); + } + } + (_, _) => { + /* ignore for now */ + } + } + } else { + /* ignore for now */ + } + io::stdout().flush().unwrap(); + } + + fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) { + /* ignore for now */ + } +} \ No newline at end of file diff --git a/src/internals.rs b/src/internals.rs index 3349c60..fa5a48e 100644 --- a/src/internals.rs +++ b/src/internals.rs @@ -109,7 +109,7 @@ fn history( _args: &mut [String], output_device: &mut OutputDevice, ) -> Result { - for (i, history_entry) in shell.history.iter().enumerate() { + for (i, history_entry) in shell.cli.history.iter().enumerate() { output_device.println(&format!("{}: {}", i + 1, history_entry)); } Ok(EXIT_SUCCESS) diff --git a/src/lib.rs b/src/lib.rs index 9ea03af..4b75cc4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +pub mod cli; pub mod internals; pub mod interpreter; pub mod output_device; diff --git a/src/shell_base.rs b/src/shell_base.rs index ac84466..21f783b 100644 --- a/src/shell_base.rs +++ b/src/shell_base.rs @@ -26,11 +26,12 @@ use std::path::{Path, PathBuf}; #[cfg(target_os = "wasi")] use wasi; -use vte::{Params, Parser, Perform}; - #[cfg(target_os = "wasi")] use wasi_ext_lib::termios; +use vte::Parser; + +use crate::cli::Cli; use crate::internals::INTERNALS_MAP; use crate::interpreter::InputInterpreter; use crate::output_device::OutputDevice; @@ -503,147 +504,18 @@ pub struct Shell { pub args: VecDeque, pub last_exit_status: i32, pub last_job_pid: Option, - pub history: Vec, + pub cli: Cli, history_path: PathBuf, - input: String, - should_echo: bool, - cursor_position: usize, - insert_mode: bool, termios_mode: Option, - reader: InternalReader, } -impl Perform for Shell { - fn print(&mut self, c: char) { - // regular characters - if self.cursor_position == self.input.len() { - self.input.push(c); - self.echo(&c.to_string()); - } else if self.insert_mode { - // in insert mode, when cursor is in the middle, new character expand CLI - // instead of replacing charcter under cursor - self.input.insert(self.cursor_position, c); - - // for wasi target, we assume that hterm has enabled insert mode - #[cfg(target_os = "wasi")] - self.echo(&c.to_string()); - - #[cfg(not(target_os = "wasi"))] - self.echo(&format!("\x1b[@{}", c)); - } else { - self.input.replace_range( - self.cursor_position..self.cursor_position + 1, - &c.to_string(), - ); - - self.echo(&c.to_string()); - } - - self.cursor_position += 1; - } - - fn execute(&mut self, byte: u8) { - // C0 and C1 control functions - match byte { - // enter - 0x10 | 0x13 => { - self.echo("\n"); - self.cursor_position = 0; - self.input = self.input.trim().to_string(); - } - // backspace - 127 => { - if !self.input.is_empty() && self.cursor_position > 0 { - self.echo("\x1b[D\x1b[P"); - self.input.remove(self.cursor_position - 1); - self.cursor_position -= 1; - } - } - _ => {/* ignore for now */} - } - } - - fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _c: char) { - /* ignore for now */ - } - - fn put(&mut self, _byte: u8) { - /* ignore for now */ - } - - fn unhook(&mut self) { - /* ignore for now */ - } - - fn osc_dispatch(&mut self, _params: &[&[u8]], _bell_terminated: bool) { - /* ignore for now */ - } - - fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, c: char) { - if params.len() == 1 { - let param = params.iter().next().unwrap(); - match (param[0], c) { - // UpArrow - (_, 'A') => { - - } - // DownArrow - (_, 'B') => { - - } - // RightArrow - (_, 'C') => { - - } - // LeftArrow - (_, 'D') => { - - } - // End - (_, 'F') => { - - } - // Home - (_, 'H') => { - - } - // PageUp - (5, '~') => { - - } - // PageDown - (6, '~') => { - - } - // Insert - (7, '~') => { - - } - // Del - (8, '~') => { - - } - (_, _) => { - - } - } - } - } - - fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) { - /* ignore for now */ - } -} - impl Shell { pub fn new(should_echo: bool, pwd: &str, args: VecDeque) -> Self { Shell { - should_echo, pwd: PathBuf::from(pwd), args, - history: Vec::new(), history_path: PathBuf::from(if PathBuf::from(env::var("HOME").unwrap()).exists() { format!( "{}/.{}_history", @@ -657,21 +529,19 @@ impl Shell { env!("CARGO_PKG_NAME") ) }), - input: String::new(), vars: HashMap::new(), last_exit_status: EXIT_SUCCESS, last_job_pid: None, - cursor_position: 0, - insert_mode: true, termios_mode: None, reader: InternalReader::OnlyStdin, + cli: Cli::new(should_echo), } } fn print_prompt(&mut self, input: &str) { print!("{}{}", self.parse_prompt_string(), input); io::stdout().flush().unwrap(); - self.cursor_position = input.len(); + self.cli.cursor_position = input.len(); } fn parse_prompt_string(&self) -> String { @@ -705,15 +575,6 @@ impl Shell { ) } - fn echo(&self, output: &str) { - if self.should_echo { - // TODO: should this maybe use OutputDevice too? - print!("{output}"); - } else if output.contains('\n') { - println!(); - } - } - pub fn run_command(&mut self, command: &str) -> Result { self.handle_input(command) } @@ -722,266 +583,27 @@ impl Shell { self.handle_input(&fs::read_to_string(script_name.into()).unwrap()) } - fn get_cursor_to_beginning(&mut self) { - if self.cursor_position > 0 { - // bring cursor to the beggining with `ESC[nD` escape sequence - self.echo(&format!("\x1b[{}D", self.cursor_position)); - } - self.cursor_position = 0; - } - - fn get_cursor_to_end(&mut self, input: &String) { - let to_end = input.len() - self.cursor_position; - if input.len() - self.cursor_position > 0 { - // bring cursor to the end with `ESC[nC` escape sequence - self.echo(&format!("\x1b[{}C", to_end)); - } - self.cursor_position = input.len(); - } - - fn erase_input(&mut self) { - // bring cursor to the beginning and clear line to the right with `ESC[0K` - self.get_cursor_to_beginning(); - self.echo("\x1b[0K"); - } - - /// Builds a line from standard input. - // TODO: maybe wrap in one more loop and only return when non-empty line is produced? - // returns Ok(false) when SigInt occurred fn get_line(&mut self, input: &mut String) -> Result { - let mut input_stash = String::new(); - - let mut c1; - let mut escaped = false; - let mut history_entry_to_display: i32 = -1; - if !self.insert_mode { - self.insert_mode = true; - } + let mut vt_parser = Parser::new(); + self.cli.reset(); - loop { - // this is to handle EOF when piping to shell + while !self.cli.is_input_ready() { match self.reader.read_byte()? { - Some(byte) => c1 = byte, + Some(byte) => { + vt_parser.advance(&mut self.cli, byte) + }, None => return Ok(false), } - if escaped { - match c1 { - 0x5b => { - let c2 = match self.reader.read_byte()? { - Some(byte) => byte, - None => return Ok(false), - }; - - match c2 { - 0x32 | 0x33 | 0x35 | 0x36 => { - let c3 = match self.reader.read_byte()? { - Some(byte) => byte, - None => return Ok(false), - }; - match [c2, c3] { - // PageUp - [0x35, 0x7e] => { - if !self.history.is_empty() && history_entry_to_display != 0 - { - if history_entry_to_display == -1 { - input_stash = input.clone(); - } - history_entry_to_display = 0; - self.erase_input(); - *input = self.history[0].clone(); - self.cursor_position = input.len(); - self.echo(input); - } - escaped = false; - } - // PageDown - [0x36, 0x7e] => { - if history_entry_to_display != -1 { - self.erase_input(); - *input = input_stash.clone(); - history_entry_to_display = -1; - self.cursor_position = input.len(); - self.echo(input); - } - escaped = false; - } - // Insert - [0x32, 0x7e] => { - self.insert_mode = !self.insert_mode; - escaped = false; - } - // delete key - [0x33, 0x7e] => { - if input.len() - self.cursor_position > 0 { - self.echo( - &" ".repeat(input.len() - self.cursor_position + 1), - ); - input.remove(self.cursor_position); - self.echo( - &format!("{}", 8 as char) - .repeat(input.len() - self.cursor_position + 2), - ); - self.echo( - &input - .chars() - .skip(self.cursor_position) - .collect::(), - ); - self.echo( - &format!("{}", 8 as char) - .repeat(input.len() - self.cursor_position), - ); - } - escaped = false; - } - [0x33, 0x3b] => { - let mut c4 = [0; 2]; - // TWO MORE! TODO: improve! - io::stdin().read_exact(&mut c4).unwrap(); - escaped = false; - } - _ => { - escaped = false; - } - } - } - // up arrow - 0x41 => { - if !self.history.is_empty() && history_entry_to_display != 0 { - if history_entry_to_display == -1 { - history_entry_to_display = (self.history.len() - 1) as i32; - input_stash = input.clone(); - } else if history_entry_to_display > 0 { - history_entry_to_display -= 1; - } - - self.erase_input(); - *input = - self.history[history_entry_to_display as usize].clone(); - self.cursor_position = input.len(); - self.echo(input); - } - escaped = false; - } - // down arrow - 0x42 => { - if history_entry_to_display != -1 { - self.erase_input(); - if self.history.len() - 1 > (history_entry_to_display as usize) - { - history_entry_to_display += 1; - *input = - self.history[history_entry_to_display as usize].clone(); - } else { - *input = input_stash.clone(); - history_entry_to_display = -1; - } - self.cursor_position = input.len(); - self.echo(input); - } - escaped = false; - } - // right arrow - 0x43 => { - if self.cursor_position < input.len() { - // move cursor right with `ESC[C` - self.echo("\x1b[C"); - self.cursor_position += 1; - } - escaped = false; - } - // left arrow - 0x44 => { - if self.cursor_position > 0 { - // move cursor left with `ESC[D` - self.echo("\x1b[D"); - self.cursor_position -= 1; - } - escaped = false; - } - // end key - 0x46 => { - self.get_cursor_to_end(input); - escaped = false; - } - // home key - 0x48 => { - self.get_cursor_to_beginning(); - escaped = false; - } - _ => { - escaped = false; - } - } - } - _ => { - escaped = false; - } - } - } else { - if c1 != 0x1b { - history_entry_to_display = -1; - } - match c1 { - // enter - 10 | 13 => { - self.echo("\n"); - self.cursor_position = 0; - *input = input.trim().to_string(); - return Ok(true); - } - // backspace - 127 => { - if !input.is_empty() && self.cursor_position > 0 { - self.echo("\x1b[D\x1b[P"); - input.remove(self.cursor_position - 1); - self.cursor_position -= 1; - } - } - // control codes - code if code < 32 => { - if code == 0x1b { - escaped = true; - } - // ignore rest for now - } - // regular characters - _ => { - if self.cursor_position == input.len() { - input.push(c1 as char); - self.echo(std::str::from_utf8(&[c1]).unwrap()); - } else if self.insert_mode { - // in insert mode, when cursor is in the middle, new character expand CLI - // instead of replacing charcter under cursor - input.insert(self.cursor_position, c1 as char); - - // for wasi target, we assume that hterm has enabled insert mode - #[cfg(target_os = "wasi")] - self.echo(std::str::from_utf8(&[c1]).unwrap()); - - #[cfg(not(target_os = "wasi"))] - self.echo(&format!("\x1b[@{}", std::str::from_utf8(&[c1]).unwrap())); - } else { - input.replace_range( - self.cursor_position..self.cursor_position + 1, - std::str::from_utf8(&[c1]).unwrap(), - ); - - self.echo(std::str::from_utf8(&[c1]).unwrap()); - } - - self.cursor_position += 1; - } - } - } - io::stdout().flush().unwrap(); } + + *input = self.cli.input.clone(); + Ok(true) } /// Expands input line with history expansion. fn history_expansion(&mut self, input: &str) -> HistoryExpansion { let mut processed = input.to_string(); - if let Some(last_command) = self.history.last() { + if let Some(last_command) = self.cli.history.last() { processed = processed.replace("!!", last_command); } // for eg. "!12", "!-2" @@ -995,12 +617,12 @@ impl Shell { let group_match = captures.get(1).unwrap().as_str(); let history_number = group_match.parse::().unwrap(); let history_number = if history_number < 0 { - (self.history.len() as i32 + history_number) as usize + (self.cli.history.len() as i32 + history_number) as usize } else { (history_number - 1) as usize }; // get that entry from history (if it exists) - if let Some(history_cmd) = self.history.get(history_number) { + if let Some(history_cmd) = self.cli.history.get(history_number) { // replace the match with the entry from history processed = processed.replace(full_match, history_cmd); } else { @@ -1022,6 +644,7 @@ impl Shell { // find history entry starting with the match if let Some(history_cmd) = self + .cli .history .iter() .rev() @@ -1053,7 +676,7 @@ impl Shell { } if PathBuf::from(&self.history_path).exists() { - self.history = fs::read_to_string(&self.history_path) + self.cli.history = fs::read_to_string(&self.history_path) .unwrap() .lines() .map(str::to_string) @@ -1114,8 +737,8 @@ impl Shell { .open(&self.history_path) { Ok(mut file) => { - if Some(&input) != self.history.last() { - self.history.push(input.clone()); + if Some(&input) != self.cli.history.last() { + self.cli.history.push(input.clone()); writeln!(file, "{}", &input).unwrap(); } } @@ -1309,7 +932,7 @@ impl Shell { #[cfg(target_os = "wasi")] { self.termios_mode = Some(termios_mode); - self.should_echo = (termios_mode.c_lflag & termios::ECHO) != 0; + self.cli.should_echo = (termios_mode.c_lflag & termios::ECHO) != 0; termios_mode.c_lflag |= termios::ISIG; termios_mode.c_lflag &= !(termios::ICANON | termios::ECHO); } @@ -1317,7 +940,7 @@ impl Shell { #[cfg(not(target_os = "wasi"))] { self.termios_mode = Some(termios_mode.clone()); - self.should_echo = termios_mode.local_flags.contains(termios::LocalFlags::ECHO); + self.cli.should_echo = termios_mode.local_flags.contains(termios::LocalFlags::ECHO); termios_mode.local_flags |= termios::LocalFlags::ISIG; termios_mode.local_flags &= !(termios::LocalFlags::ICANON | termios::LocalFlags::ECHO); } From 3af92248cddefaddafef42b22314c590b9097ac9 Mon Sep 17 00:00:00 2001 From: Krzysztof Juszczyk Date: Tue, 27 Feb 2024 16:52:25 +0100 Subject: [PATCH 4/8] [#55592] Fix handling diacritics in CLI --- src/cli.rs | 31 +++++++++++++++---------------- src/internals.rs | 2 +- src/shell_base.rs | 25 ++++++++++++++++--------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 819b38c..728759c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,14 +10,14 @@ use std::io::Write; use vte::{Params, Perform}; pub struct Cli { - pub history: Vec, + pub history: Vec>, pub should_echo: bool, pub cursor_position: usize, - pub input: String, + pub input: Vec, history_entry_to_display: i32, input_ready: bool, - input_stash: String, + input_stash: Vec, insert_mode: bool, } @@ -27,9 +27,9 @@ impl Cli { cursor_position: 0, history: Vec::new(), history_entry_to_display: -1, - input: String::new(), + input: Vec::new(), input_ready: false, - input_stash: String::new(), + input_stash: Vec::new(), insert_mode: true, should_echo, } @@ -87,7 +87,7 @@ impl Cli { impl Perform for Cli { fn print(&mut self, c: char) { - let byte = c as u8; + let byte = c as u16; match byte { // backspace 0x7f => { @@ -104,7 +104,8 @@ impl Perform for Cli { self.echo(&c.to_string()); } else if self.insert_mode { // in insert mode, when cursor is in the middle, new character expand CLI - // instead of replacing charcter under cursor + // instead of replacing character under cursor + self.input.insert(self.cursor_position, c); // for wasi target, we assume that hterm has enabled insert mode @@ -114,10 +115,7 @@ impl Perform for Cli { #[cfg(not(target_os = "wasi"))] self.echo(&format!("\x1b[@{}", c)); } else { - self.input.replace_range( - self.cursor_position..self.cursor_position + 1, - &c.to_string(), - ); + self.input[self.cursor_position] = c; #[cfg(target_os = "wasi")] self.echo(&format!("\x1b[P{}", c)); @@ -129,6 +127,7 @@ impl Perform for Cli { self.cursor_position += 1; } } + io::stdout().flush().unwrap(); } @@ -139,7 +138,6 @@ impl Perform for Cli { 0xa | 0xd => { self.echo("\n"); self.cursor_position = 0; - self.input = self.input.trim().to_string(); self.input_ready = true; } _ => {/* ignore for now */} @@ -148,6 +146,7 @@ impl Perform for Cli { } fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _c: char) { + /* ignore for now */ } @@ -181,7 +180,7 @@ impl Perform for Cli { self.input = self.history[self.history_entry_to_display as usize].clone(); self.cursor_position = self.input.len(); - self.echo(&self.input); + self.echo(&self.input.iter().collect::()); } } // DownArrow @@ -198,7 +197,7 @@ impl Perform for Cli { self.history_entry_to_display = -1; } self.cursor_position = self.input.len(); - self.echo(&self.input); + self.echo(&self.input.iter().collect::()); } } // RightArrow @@ -246,7 +245,7 @@ impl Perform for Cli { self.erase_input(); self.input = self.history[0].clone(); self.cursor_position = self.input.len(); - self.echo(&self.input); + self.echo(&self.input.iter().collect::()); } } // PageDown @@ -256,7 +255,7 @@ impl Perform for Cli { self.input = self.input_stash.clone(); self.history_entry_to_display = -1; self.cursor_position = self.input.len(); - self.echo(&self.input); + self.echo(&self.input.iter().collect::()); } } (_, _) => { diff --git a/src/internals.rs b/src/internals.rs index fa5a48e..20a0f41 100644 --- a/src/internals.rs +++ b/src/internals.rs @@ -110,7 +110,7 @@ fn history( output_device: &mut OutputDevice, ) -> Result { for (i, history_entry) in shell.cli.history.iter().enumerate() { - output_device.println(&format!("{}: {}", i + 1, history_entry)); + output_device.println(&format!("{}: {}", i + 1, history_entry.iter().collect::())); } Ok(EXIT_SUCCESS) } diff --git a/src/shell_base.rs b/src/shell_base.rs index 21f783b..526d5ec 100644 --- a/src/shell_base.rs +++ b/src/shell_base.rs @@ -596,7 +596,11 @@ impl Shell { } } - *input = self.cli.input.clone(); + *input = self.cli.input + .iter() + .collect::() + .trim() + .to_string(); Ok(true) } @@ -604,7 +608,7 @@ impl Shell { fn history_expansion(&mut self, input: &str) -> HistoryExpansion { let mut processed = input.to_string(); if let Some(last_command) = self.cli.history.last() { - processed = processed.replace("!!", last_command); + processed = processed.replace("!!", &last_command.iter().collect::()); } // for eg. "!12", "!-2" lazy_static! { @@ -624,7 +628,7 @@ impl Shell { // get that entry from history (if it exists) if let Some(history_cmd) = self.cli.history.get(history_number) { // replace the match with the entry from history - processed = processed.replace(full_match, history_cmd); + processed = processed.replace(full_match, &history_cmd.iter().collect::()); } else { return HistoryExpansion::EventNotFound(full_match.into()); } @@ -648,10 +652,10 @@ impl Shell { .history .iter() .rev() - .find(|entry| entry.starts_with(group_match)) + .find(|entry| entry.starts_with(&group_match.chars().collect::>())) { // replace the match with the entry from history - processed = processed.replace(full_match, history_cmd); + processed = processed.replace(full_match, &history_cmd.iter().collect::()); } else { return HistoryExpansion::EventNotFound(full_match.into()); } @@ -679,8 +683,10 @@ impl Shell { self.cli.history = fs::read_to_string(&self.history_path) .unwrap() .lines() - .map(str::to_string) - .collect(); + .map(|line| { + line.chars().collect::>() + }) + .collect::>>(); } let washrc_path = { @@ -737,8 +743,9 @@ impl Shell { .open(&self.history_path) { Ok(mut file) => { - if Some(&input) != self.cli.history.last() { - self.cli.history.push(input.clone()); + let vectored_input = input.chars().collect::>(); + if Some(&vectored_input) != self.cli.history.last() { + self.cli.history.push(vectored_input); writeln!(file, "{}", &input).unwrap(); } } From 23b21efa9a00d6e3ff7a87378fd6fb45988a3def Mon Sep 17 00:00:00 2001 From: Krzysztof Juszczyk Date: Wed, 28 Feb 2024 15:55:35 +0100 Subject: [PATCH 5/8] [#55592] Fix disabled echo after script execution --- src/shell_base.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/shell_base.rs b/src/shell_base.rs index 526d5ec..a380e9a 100644 --- a/src/shell_base.rs +++ b/src/shell_base.rs @@ -732,9 +732,17 @@ impl Shell { eprintln!("{event}: event not found"); } HistoryExpansion::Unchanged => { + if let Ok(true) = is_fd_tty(STDIN) { + self.restore_default_mode()?; + } + if let Err(error) = self.handle_input(&input) { eprintln!("{error:#?}"); }; + + if let Ok(true) = is_fd_tty(STDIN) { + self.enable_interpreter_mode()?; + } } } match OpenOptions::new() @@ -782,11 +790,6 @@ impl Shell { return Ok(EXIT_FAILURE); } - // restore termios - if let Ok(true) = is_fd_tty(STDIN) { - self.restore_default_mode()?; - } - let result: Result = if let Some(internal) = INTERNALS_MAP.get(command) { internal(self, args, &mut output_device) } else { @@ -890,10 +893,6 @@ impl Shell { } }; - if let Ok(true) = is_fd_tty(STDIN) { - self.enable_interpreter_mode()?; - } - output_device.flush()?; self.last_exit_status = if let Ok(exit_status) = result { From 9aa5152e3398a62f9bb69ea5aa3d579ebb5d87fc Mon Sep 17 00:00:00 2001 From: Krzysztof Juszczyk Date: Fri, 1 Mar 2024 12:01:33 +0100 Subject: [PATCH 6/8] [#55213] Apply cargo formater --- src/cli.rs | 16 +++++----------- src/internals.rs | 6 +++++- src/shell_base.rs | 14 +++----------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 728759c..fba4293 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -49,7 +49,6 @@ impl Cli { if !self.insert_mode { self.insert_mode = true; } - } fn echo(&self, output: &str) { @@ -140,13 +139,12 @@ impl Perform for Cli { self.cursor_position = 0; self.input_ready = true; } - _ => {/* ignore for now */} + _ => { /* ignore for now */ } } io::stdout().flush().unwrap(); } fn hook(&mut self, _params: &Params, _intermediates: &[u8], _ignore: bool, _c: char) { - /* ignore for now */ } @@ -177,8 +175,7 @@ impl Perform for Cli { } self.erase_input(); - self.input = - self.history[self.history_entry_to_display as usize].clone(); + self.input = self.history[self.history_entry_to_display as usize].clone(); self.cursor_position = self.input.len(); self.echo(&self.input.iter().collect::()); } @@ -187,8 +184,7 @@ impl Perform for Cli { (_, 'B') => { if self.history_entry_to_display != -1 { self.erase_input(); - if self.history.len() - 1 > (self.history_entry_to_display as usize) - { + if self.history.len() - 1 > (self.history_entry_to_display as usize) { self.history_entry_to_display += 1; self.input = self.history[self.history_entry_to_display as usize].clone(); @@ -258,9 +254,7 @@ impl Perform for Cli { self.echo(&self.input.iter().collect::()); } } - (_, _) => { - /* ignore for now */ - } + (_, _) => { /* ignore for now */ } } } else { /* ignore for now */ @@ -271,4 +265,4 @@ impl Perform for Cli { fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) { /* ignore for now */ } -} \ No newline at end of file +} diff --git a/src/internals.rs b/src/internals.rs index 20a0f41..d891c3f 100644 --- a/src/internals.rs +++ b/src/internals.rs @@ -110,7 +110,11 @@ fn history( output_device: &mut OutputDevice, ) -> Result { for (i, history_entry) in shell.cli.history.iter().enumerate() { - output_device.println(&format!("{}: {}", i + 1, history_entry.iter().collect::())); + output_device.println(&format!( + "{}: {}", + i + 1, + history_entry.iter().collect::() + )); } Ok(EXIT_SUCCESS) } diff --git a/src/shell_base.rs b/src/shell_base.rs index a380e9a..5793be5 100644 --- a/src/shell_base.rs +++ b/src/shell_base.rs @@ -589,18 +589,12 @@ impl Shell { while !self.cli.is_input_ready() { match self.reader.read_byte()? { - Some(byte) => { - vt_parser.advance(&mut self.cli, byte) - }, + Some(byte) => vt_parser.advance(&mut self.cli, byte), None => return Ok(false), } } - *input = self.cli.input - .iter() - .collect::() - .trim() - .to_string(); + *input = self.cli.input.iter().collect::().trim().to_string(); Ok(true) } @@ -683,9 +677,7 @@ impl Shell { self.cli.history = fs::read_to_string(&self.history_path) .unwrap() .lines() - .map(|line| { - line.chars().collect::>() - }) + .map(|line| line.chars().collect::>()) .collect::>>(); } From a77f322cacde4d94f202df2e14e11ba0fb976d1c Mon Sep 17 00:00:00 2001 From: Grzegorz Placzek Date: Fri, 1 Mar 2024 12:39:03 +0100 Subject: [PATCH 7/8] [#55213] Apply new cargo lints --- src/output_device.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/output_device.rs b/src/output_device.rs index 025e0aa..ef49905 100644 --- a/src/output_device.rs +++ b/src/output_device.rs @@ -71,7 +71,6 @@ impl<'a> OutputDevice<'a> { .create(true) .open(path)?, Some(Redirect::Append(_, path)) => OpenOptions::new() - .write(true) .append(true) .create(true) .open(path)?, From 1b6d86c7a52c07a0aa18c6d7cda28b6940302043 Mon Sep 17 00:00:00 2001 From: Grzegorz Placzek Date: Fri, 1 Mar 2024 12:39:43 +0100 Subject: [PATCH 8/8] [#55213] Update version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 277965f..fb8f45d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -699,7 +699,7 @@ dependencies = [ [[package]] name = "wash" -version = "0.1.0" +version = "0.1.3" dependencies = [ "clap", "color-eyre", diff --git a/Cargo.toml b/Cargo.toml index 0a9de79..252501a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ author_email='contact@antmicro.com' edition = "2018" name = "wash" -version = "0.1.0" +version = "0.1.3" license = "Apache-2.0" [dependencies]