diff --git a/Cargo.lock b/Cargo.lock index cdc32fd..fb8f45d 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,9 +676,30 @@ 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" +version = "0.1.3" dependencies = [ "clap", "color-eyre", @@ -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..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] @@ -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/cli.rs b/src/cli.rs new file mode 100644 index 0000000..fba4293 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,268 @@ +/* + * 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: Vec, + + history_entry_to_display: i32, + input_ready: bool, + input_stash: Vec, + 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: Vec::new(), + input_ready: false, + input_stash: Vec::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 u16; + 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 character 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[self.cursor_position] = c; + + #[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_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.iter().collect::()); + } + } + // 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.iter().collect::()); + } + } + // 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.iter().collect::()); + } + } + // 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.iter().collect::()); + } + } + (_, _) => { /* 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 */ + } +} diff --git a/src/internals.rs b/src/internals.rs index 3349c60..d891c3f 100644 --- a/src/internals.rs +++ b/src/internals.rs @@ -109,8 +109,12 @@ fn history( _args: &mut [String], output_device: &mut OutputDevice, ) -> Result { - for (i, history_entry) in shell.history.iter().enumerate() { - output_device.println(&format!("{}: {}", i + 1, history_entry)); + for (i, history_entry) in shell.cli.history.iter().enumerate() { + output_device.println(&format!( + "{}: {}", + i + 1, + history_entry.iter().collect::() + )); } Ok(EXIT_SUCCESS) } 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/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/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)?, diff --git a/src/shell_base.rs b/src/shell_base.rs index 570767b..5793be5 100644 --- a/src/shell_base.rs +++ b/src/shell_base.rs @@ -29,6 +29,9 @@ use wasi; #[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; @@ -501,24 +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, - should_echo: bool, - cursor_position: usize, - insert_mode: bool, termios_mode: Option, - reader: InternalReader, } 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", @@ -535,17 +532,16 @@ impl Shell { 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 { @@ -579,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) } @@ -596,270 +583,26 @@ 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] => { - 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; - } - } - } - // 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; - } - _ => { - println!("WE HAVE UNKNOWN CONTROL CODE '[' + {}", c2); - 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.iter().collect::().trim().to_string(); + 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() { - processed = processed.replace("!!", last_command); + if let Some(last_command) = self.cli.history.last() { + processed = processed.replace("!!", &last_command.iter().collect::()); } // for eg. "!12", "!-2" lazy_static! { @@ -872,14 +615,14 @@ 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); + processed = processed.replace(full_match, &history_cmd.iter().collect::()); } else { return HistoryExpansion::EventNotFound(full_match.into()); } @@ -899,13 +642,14 @@ impl Shell { // find history entry starting with the match if let Some(history_cmd) = self + .cli .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()); } @@ -930,11 +674,11 @@ 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) - .collect(); + .map(|line| line.chars().collect::>()) + .collect::>>(); } let washrc_path = { @@ -980,9 +724,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() @@ -991,8 +743,9 @@ impl Shell { .open(&self.history_path) { Ok(mut file) => { - if Some(&input) != self.history.last() { - self.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(); } } @@ -1029,11 +782,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 { @@ -1137,10 +885,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 { @@ -1186,7 +930,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); } @@ -1194,7 +938,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); }