From c727994dbc5d33527b0ebf5d816bae24db25afac Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Wed, 4 Sep 2024 15:50:23 -0400 Subject: [PATCH 01/12] terminal: refactor --- kinode/src/terminal/mod.rs | 88 ++++++++++++++++++++---------------- kinode/src/terminal/utils.rs | 2 +- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/kinode/src/terminal/mod.rs b/kinode/src/terminal/mod.rs index 4203b20b..5eb9cc2f 100644 --- a/kinode/src/terminal/mod.rs +++ b/kinode/src/terminal/mod.rs @@ -21,24 +21,33 @@ pub mod utils; pub struct State { pub stdout: std::io::Stdout, + /// handle for writing to on-disk log (disabled by default, triggered by CTRL+L) pub log_writer: BufWriter, + /// in-memory searchable command history (default size: 1000) pub command_history: utils::CommandHistory, + /// terminal window width, 0 is leftmost column pub win_cols: u16, + /// terminal window height, 0 is topmost row pub win_rows: u16, - pub prompt_len: usize, + /// prompt for user input (e.g. "mynode.os > ") + pub prompt: &'static str, pub line_col: usize, pub cursor_col: u16, pub current_line: String, + /// flag representing whether we are in step-through mode (activated by CTRL+J, stepped by CTRL+S) pub in_step_through: bool, + /// flag representing whether we are in search mode (activated by CTRL+R, exited by CTRL+G) pub search_mode: bool, + /// depth of search mode (activated by CTRL+R, increased by CTRL+R) pub search_depth: usize, + /// flag representing whether we are in logging mode (activated by CTRL+L) pub logging_mode: bool, + /// verbosity mode (increased by CTRL+V) pub verbose_mode: u8, } -/* - * terminal driver - */ +/// main entry point for terminal process +/// called by main.rs pub async fn terminal( our: Identity, version: &str, @@ -50,12 +59,12 @@ pub async fn terminal( is_detached: bool, verbose_mode: u8, ) -> anyhow::Result<()> { - let (stdout, _maybe_raw_mode) = utils::startup(&our, version, is_detached)?; + let (stdout, _maybe_raw_mode) = utils::splash(&our, version, is_detached)?; let (win_cols, win_rows) = crossterm::terminal::size().expect("terminal: couldn't fetch size"); - let current_line = format!("{} > ", our.name); - let prompt_len: usize = our.name.len() + 3; + let prompt = Box::leak(format!("{} > ", our.name).into_boxed_str()); + let prompt_len: usize = prompt.len(); let cursor_col: u16 = prompt_len as u16; let line_col: usize = cursor_col as usize; @@ -99,10 +108,10 @@ pub async fn terminal( command_history, win_cols, win_rows, - prompt_len, + prompt, line_col, cursor_col, - current_line, + current_line: prompt.to_string(), in_step_through, search_mode, search_depth, @@ -229,7 +238,7 @@ fn handle_printout(printout: Printout, state: &mut State) -> anyhow::Result<()> cursor::MoveTo(0, state.win_rows), Print(utils::truncate_in_place( &state.current_line, - state.prompt_len, + state.prompt.len(), state.win_cols, (state.line_col, state.cursor_col) )), @@ -252,7 +261,7 @@ async fn handle_event( command_history, win_cols, win_rows, - prompt_len, + prompt, line_col, cursor_col, current_line, @@ -298,7 +307,7 @@ async fn handle_event( cursor::MoveTo(0, *win_rows), Print(utils::truncate_in_place( ¤t_line, - *prompt_len, + prompt.len(), *win_cols, (*line_col, *cursor_col) )), @@ -432,7 +441,7 @@ async fn handle_event( .. }) => { // go up one command in history - match command_history.get_prev(¤t_line[*prompt_len..]) { + match command_history.get_prev(¤t_line[prompt.len()..]) { Some(line) => { *current_line = format!("{} > {}", our.name, line); *line_col = current_line.len(); @@ -449,7 +458,7 @@ async fn handle_event( terminal::Clear(ClearType::CurrentLine), Print(utils::truncate_rightward( current_line, - *prompt_len, + prompt.len(), *win_cols )), )?; @@ -484,7 +493,7 @@ async fn handle_event( terminal::Clear(ClearType::CurrentLine), Print(utils::truncate_rightward( ¤t_line, - *prompt_len, + prompt.len(), *win_cols )), )?; @@ -497,14 +506,14 @@ async fn handle_event( modifiers: KeyModifiers::CONTROL, .. }) => { - *line_col = *prompt_len; - *cursor_col = *prompt_len as u16; + *line_col = prompt.len(); + *cursor_col = prompt.len() as u16; execute!( stdout, cursor::MoveTo(0, *win_rows), Print(utils::truncate_from_left( ¤t_line, - *prompt_len, + prompt.len(), *win_cols, *line_col )), @@ -529,7 +538,7 @@ async fn handle_event( cursor::MoveTo(0, *win_rows), Print(utils::truncate_from_right( ¤t_line, - *prompt_len, + prompt.len(), *win_cols, *line_col )), @@ -552,7 +561,7 @@ async fn handle_event( &our, &mut stdout, ¤t_line, - *prompt_len, + prompt.len(), (*win_cols, *win_rows), (*line_col, *cursor_col), command_history, @@ -575,8 +584,8 @@ async fn handle_event( cursor::MoveTo(0, *win_rows), terminal::Clear(ClearType::CurrentLine), Print(utils::truncate_in_place( - &format!("{} > {}", our.name, ¤t_line[*prompt_len..]), - *prompt_len, + &format!("{} > {}", our.name, ¤t_line[prompt.len()..]), + prompt.len(), *win_cols, (*line_col, *cursor_col) )), @@ -602,7 +611,7 @@ async fn handle_event( &our, &mut stdout, ¤t_line, - *prompt_len, + prompt.len(), (*win_cols, *win_rows), (*line_col, *cursor_col), command_history, @@ -616,7 +625,7 @@ async fn handle_event( terminal::Clear(ClearType::CurrentLine), Print(utils::truncate_in_place( ¤t_line, - *prompt_len, + prompt.len(), *win_cols, (*line_col, *cursor_col) )), @@ -627,7 +636,7 @@ async fn handle_event( // BACKSPACE: delete a single character at cursor // KeyCode::Backspace => { - if line_col == prompt_len { + if *line_col == prompt.len() { return Ok(false); } if *cursor_col as usize == *line_col { @@ -640,7 +649,7 @@ async fn handle_event( &our, &mut stdout, ¤t_line, - *prompt_len, + prompt.len(), (*win_cols, *win_rows), (*line_col, *cursor_col), command_history, @@ -654,7 +663,7 @@ async fn handle_event( terminal::Clear(ClearType::CurrentLine), Print(utils::truncate_in_place( ¤t_line, - *prompt_len, + prompt.len(), *win_cols, (*line_col, *cursor_col) )), @@ -674,7 +683,7 @@ async fn handle_event( &our, &mut stdout, ¤t_line, - *prompt_len, + prompt.len(), (*win_cols, *win_rows), (*line_col, *cursor_col), command_history, @@ -688,7 +697,7 @@ async fn handle_event( terminal::Clear(ClearType::CurrentLine), Print(utils::truncate_in_place( ¤t_line, - *prompt_len, + prompt.len(), *win_cols, (*line_col, *cursor_col) )), @@ -699,8 +708,8 @@ async fn handle_event( // LEFT: move cursor one spot left // KeyCode::Left => { - if *cursor_col as usize == *prompt_len { - if line_col == prompt_len { + if *cursor_col as usize == prompt.len() { + if *line_col == prompt.len() { // at the very beginning of the current typed line return Ok(false); } else { @@ -711,7 +720,7 @@ async fn handle_event( cursor::MoveTo(0, *win_rows), Print(utils::truncate_from_left( ¤t_line, - *prompt_len, + prompt.len(), *win_cols, *line_col )), @@ -746,7 +755,7 @@ async fn handle_event( cursor::MoveTo(0, *win_rows), Print(utils::truncate_from_right( ¤t_line, - *prompt_len, + prompt.len(), *win_cols, *line_col )), @@ -759,28 +768,27 @@ async fn handle_event( KeyCode::Enter => { // if we were in search mode, pull command from that let command = if !*search_mode { - current_line[*prompt_len..].to_string() + current_line[prompt.len()..].to_string() } else { command_history - .search(¤t_line[*prompt_len..], *search_depth) + .search(¤t_line[prompt.len()..], *search_depth) .unwrap_or_default() .to_string() }; - let next = format!("{} > ", our.name); execute!( stdout, cursor::MoveTo(0, *win_rows), terminal::Clear(ClearType::CurrentLine), Print(&format!("{} > {}", our.name, command)), Print("\r\n"), - Print(&next), + Print(&prompt), )?; *search_mode = false; *search_depth = 0; - *current_line = next; + *current_line = prompt.to_string(); command_history.add(command.clone()); - *cursor_col = *prompt_len as u16; - *line_col = *prompt_len; + *cursor_col = prompt.len() as u16; + *line_col = prompt.len(); KernelMessage::builder() .id(rand::random()) .source((our.name.as_str(), TERMINAL_PROCESS_ID.clone())) diff --git a/kinode/src/terminal/utils.rs b/kinode/src/terminal/utils.rs index baf30cb9..4316d12c 100644 --- a/kinode/src/terminal/utils.rs +++ b/kinode/src/terminal/utils.rs @@ -24,7 +24,7 @@ impl Drop for RawMode { } } -pub fn startup( +pub fn splash( our: &Identity, version: &str, is_detached: bool, From a97147452b5f79bfc7099b920fdf3be3ad00c851 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Wed, 4 Sep 2024 16:01:19 -0400 Subject: [PATCH 02/12] add --logging flag, use in terminal --- kinode/src/main.rs | 14 +++++++++++--- kinode/src/terminal/mod.rs | 7 ++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/kinode/src/main.rs b/kinode/src/main.rs index da7a4011..256e2fe1 100644 --- a/kinode/src/main.rs +++ b/kinode/src/main.rs @@ -77,6 +77,9 @@ async fn main() { let rpc = matches.get_one::("rpc"); let password = matches.get_one::("password"); + // logging mode is toggled at runtime by CTRL+L + let is_logging = *matches.get_one::("logging").unwrap(); + // detached determines whether terminal is interactive let detached = *matches.get_one::("detached").unwrap(); @@ -423,6 +426,7 @@ async fn main() { print_receiver, detached, verbose_mode, + is_logging, ) => { match quit { Ok(()) => { @@ -630,7 +634,7 @@ fn build_command() -> Command { .about("A General Purpose Sovereign Cloud Computing Platform") .arg(arg!([home] "Path to home directory").required(true)) .arg( - arg!(--port "Port to bind [default: first unbound at or above 8080]") + arg!(-p --port "Port to bind [default: first unbound at or above 8080]") .value_parser(value_parser!(u16)), ) .arg( @@ -644,17 +648,21 @@ fn build_command() -> Command { .value_parser(value_parser!(u16)), ) .arg( - arg!(--verbosity "Verbosity level: higher is more verbose") + arg!(-v --verbosity "Verbosity level: higher is more verbose") .default_value("0") .value_parser(value_parser!(u8)), ) + .arg( + arg!(-l --logging "Run in logging mode (toggled at runtime by CTRL+L): write all terminal output to .terminal_log file") + .action(clap::ArgAction::SetTrue), + ) .arg( arg!(--"reveal-ip" "If set to false, as an indirect node, always use routers to connect to other nodes.") .default_value("true") .value_parser(value_parser!(bool)), ) .arg( - arg!(--detached "Run in detached mode (don't accept input)") + arg!(-d --detached "Run in detached mode (don't accept input)") .action(clap::ArgAction::SetTrue), ) .arg(arg!(--rpc "Add a WebSockets RPC URL at boot")) diff --git a/kinode/src/terminal/mod.rs b/kinode/src/terminal/mod.rs index 5eb9cc2f..373ea1e6 100644 --- a/kinode/src/terminal/mod.rs +++ b/kinode/src/terminal/mod.rs @@ -58,6 +58,7 @@ pub async fn terminal( mut print_rx: PrintReceiver, is_detached: bool, verbose_mode: u8, + is_logging: bool, ) -> anyhow::Result<()> { let (stdout, _maybe_raw_mode) = utils::splash(&our, version, is_detached)?; @@ -68,12 +69,12 @@ pub async fn terminal( let cursor_col: u16 = prompt_len as u16; let line_col: usize = cursor_col as usize; - let in_step_through: bool = false; + let in_step_through = false; - let search_mode: bool = false; + let search_mode = false; let search_depth: usize = 0; - let logging_mode: bool = false; + let logging_mode = is_logging; // the terminal stores the most recent 1000 lines entered by user // in history. TODO should make history size adjustable. From e0751cdbf14f49a7e3e3021129fa4366c55c3790 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Thu, 5 Sep 2024 20:36:26 -0400 Subject: [PATCH 03/12] terminal: fix: use unicode graphemes to manage input line ALSO re-jigger 2 errors in terminal, app store --- Cargo.lock | 1 + kinode/Cargo.toml | 1 + kinode/packages/app_store/chain/src/lib.rs | 8 +- kinode/packages/terminal/terminal/src/lib.rs | 24 +- kinode/src/terminal/mod.rs | 439 ++++++++----------- kinode/src/terminal/utils.rs | 117 ++--- 6 files changed, 228 insertions(+), 362 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 773a4eb4..a2725a7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3573,6 +3573,7 @@ dependencies = [ "thiserror", "tokio", "tokio-tungstenite 0.21.0", + "unicode-segmentation", "url", "walkdir", "warp", diff --git a/kinode/Cargo.toml b/kinode/Cargo.toml index 3536f3de..a2fc1523 100644 --- a/kinode/Cargo.toml +++ b/kinode/Cargo.toml @@ -84,6 +84,7 @@ static_dir = "0.2.0" thiserror = "1.0" tokio = { version = "1.28", features = ["fs", "macros", "rt-multi-thread", "signal", "sync"] } tokio-tungstenite = { version = "0.21.0", features = ["native-tls"] } +unicode-segmentation = "1.11" url = "2.4.1" warp = "0.3.5" wasi-common = "19.0.1" diff --git a/kinode/packages/app_store/chain/src/lib.rs b/kinode/packages/app_store/chain/src/lib.rs index 88ea3e5d..deed3167 100644 --- a/kinode/packages/app_store/chain/src/lib.rs +++ b/kinode/packages/app_store/chain/src/lib.rs @@ -14,13 +14,12 @@ use kinode_process_lib::{ await_message, call_init, eth, get_blob, get_state, http, kernel_types as kt, kimap, print_to_terminal, println, timer, Address, Message, PackageId, Request, Response, }; +use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet}, str::FromStr, }; -use serde::{Deserialize, Serialize}; - wit_bindgen::generate!({ path: "target/wit", generate_unused_types: true, @@ -271,10 +270,7 @@ fn handle_eth_log(our: &Address, state: &mut State, log: eth::Log) -> anyhow::Re _ => Err(e), }, } - .map_err(|e| { - println!("Couldn't find {hash_note}: {e:?}"); - anyhow::anyhow!("metadata hash mismatch") - })?; + .map_err(|e| anyhow::anyhow!("Couldn't find {hash_note}: {e:?}"))?; match data { None => { diff --git a/kinode/packages/terminal/terminal/src/lib.rs b/kinode/packages/terminal/terminal/src/lib.rs index 8f29dcb9..78d30b2e 100644 --- a/kinode/packages/terminal/terminal/src/lib.rs +++ b/kinode/packages/terminal/terminal/src/lib.rs @@ -20,7 +20,7 @@ enum TerminalAction { #[derive(Debug, Serialize, Deserialize)] enum ScriptError { - UnknownName, + UnknownName(String), FailedToReadWasm, NoScriptsManifest, NoScriptInManifest, @@ -30,18 +30,16 @@ enum ScriptError { impl std::fmt::Display for ScriptError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - ScriptError::UnknownName => "script not found, either as an alias or process ID", - ScriptError::FailedToReadWasm => "failed to read script Wasm from VFS", - ScriptError::NoScriptsManifest => "no scripts manifest in package", - ScriptError::NoScriptInManifest => "script not in scripts.json file", - ScriptError::InvalidScriptsManifest => "could not parse scripts.json file", - ScriptError::KernelUnresponsive => "kernel unresponsive", + match self { + ScriptError::UnknownName(name) => { + write!(f, "'{name}' not found, either as an alias or process ID") } - ) + ScriptError::FailedToReadWasm => write!(f, "failed to read script Wasm from VFS"), + ScriptError::NoScriptsManifest => write!(f, "no scripts manifest in package"), + ScriptError::NoScriptInManifest => write!(f, "script not in scripts.json file"), + ScriptError::InvalidScriptsManifest => write!(f, "could not parse scripts.json file"), + ScriptError::KernelUnresponsive => write!(f, "kernel unresponsive"), + } } } @@ -187,7 +185,7 @@ fn parse_command(state: &mut TerminalState, line: String) -> Result<(), ScriptEr Some(process) => handle_run(&state.our, process, args.to_string()), None => match head.parse::() { Ok(pid) => handle_run(&state.our, &pid, args.to_string()), - Err(_) => Err(ScriptError::UnknownName), + Err(_) => Err(ScriptError::UnknownName(head.to_string())), }, } } diff --git a/kinode/src/terminal/mod.rs b/kinode/src/terminal/mod.rs index 373ea1e6..0b59fff3 100644 --- a/kinode/src/terminal/mod.rs +++ b/kinode/src/terminal/mod.rs @@ -16,24 +16,22 @@ use std::{ io::{BufWriter, Write}, }; use tokio::signal::unix::{signal, SignalKind}; +use unicode_segmentation::UnicodeSegmentation; pub mod utils; -pub struct State { +struct State { pub stdout: std::io::Stdout, /// handle for writing to on-disk log (disabled by default, triggered by CTRL+L) pub log_writer: BufWriter, - /// in-memory searchable command history (default size: 1000) + /// in-memory searchable command history that persists itself on disk (default size: 1000) pub command_history: utils::CommandHistory, /// terminal window width, 0 is leftmost column pub win_cols: u16, /// terminal window height, 0 is topmost row pub win_rows: u16, - /// prompt for user input (e.g. "mynode.os > ") - pub prompt: &'static str, - pub line_col: usize, - pub cursor_col: u16, - pub current_line: String, + /// the input line (bottom row) + pub current_line: CurrentLine, /// flag representing whether we are in step-through mode (activated by CTRL+J, stepped by CTRL+S) pub in_step_through: bool, /// flag representing whether we are in search mode (activated by CTRL+R, exited by CTRL+G) @@ -46,6 +44,94 @@ pub struct State { pub verbose_mode: u8, } +impl State { + fn display_current_input_line(&mut self) -> Result<(), std::io::Error> { + execute!( + self.stdout, + cursor::MoveTo(0, self.win_rows), + terminal::Clear(ClearType::CurrentLine), + style::SetForegroundColor(style::Color::Reset), + Print(self.current_line.prompt), + Print(utils::truncate_in_place( + &self.current_line.line, + self.win_cols as usize - self.current_line.prompt_len, + self.current_line.line_col, + self.current_line.cursor_col + )), + cursor::MoveTo( + self.current_line.prompt_len as u16 + self.current_line.cursor_col, + self.win_rows + ), + ) + } + + fn search(&mut self, our_name: &str) -> Result<(), std::io::Error> { + let search_prompt = format!("{} *", our_name); + let search_query = &self.current_line.line; + if let Some(result) = self.command_history.search(search_query, self.search_depth) { + let (result_underlined, u_end) = utils::underline(result, search_query); + let search_cursor_col = u_end + search_prompt.graphemes(true).count() as u16; + execute!( + self.stdout, + cursor::MoveTo(0, self.win_rows), + terminal::Clear(terminal::ClearType::CurrentLine), + style::SetForegroundColor(style::Color::Reset), + style::Print(&search_prompt), + style::Print(utils::truncate_in_place( + &result_underlined, + self.win_cols as usize - self.current_line.prompt_len, + self.current_line.line_col, + search_cursor_col + )), + cursor::MoveTo( + self.current_line.prompt_len as u16 + search_cursor_col, + self.win_rows + ), + ) + } else { + execute!( + self.stdout, + cursor::MoveTo(0, self.win_rows), + terminal::Clear(terminal::ClearType::CurrentLine), + style::SetForegroundColor(style::Color::Reset), + style::Print(&search_prompt), + style::Print(utils::truncate_in_place( + &format!("{}: no results", &self.current_line.line), + self.win_cols as usize - self.current_line.prompt_len, + self.current_line.line_col, + self.current_line.cursor_col + )), + cursor::MoveTo( + self.current_line.prompt_len as u16 + self.current_line.cursor_col, + self.win_rows + ), + ) + } + } +} + +struct CurrentLine { + /// prompt for user input (e.g. "mynode.os > ") + pub prompt: &'static str, + pub prompt_len: usize, + /// the grapheme index of the cursor in the current line + pub line_col: usize, + /// the column index of the cursor in the terminal window (not including prompt) + pub cursor_col: u16, + /// the line itself, which does not include the prompt + pub line: String, +} + +impl CurrentLine { + fn byte_index(&self) -> usize { + self.line + .grapheme_indices(true) + .nth(self.line_col) + .map(|(i, _)| i) + .unwrap_or_else(|| self.line.graphemes(true).count()) + } +} + /// main entry point for terminal process /// called by main.rs pub async fn terminal( @@ -64,10 +150,9 @@ pub async fn terminal( let (win_cols, win_rows) = crossterm::terminal::size().expect("terminal: couldn't fetch size"); - let prompt = Box::leak(format!("{} > ", our.name).into_boxed_str()); - let prompt_len: usize = prompt.len(); - let cursor_col: u16 = prompt_len as u16; - let line_col: usize = cursor_col as usize; + let (prompt, prompt_len) = utils::make_prompt(&our.name); + let cursor_col: u16 = 0; + let line_col: usize = 0; let in_step_through = false; @@ -109,10 +194,13 @@ pub async fn terminal( command_history, win_cols, win_rows, - prompt, - line_col, - cursor_col, - current_line: prompt.to_string(), + current_line: CurrentLine { + prompt, + prompt_len, + line_col, + cursor_col, + line: "".to_string(), + }, in_step_through, search_mode, search_depth, @@ -229,22 +317,10 @@ fn handle_printout(printout: Printout, state: &mut State) -> anyhow::Result<()> }), )?; for line in printout.content.lines() { - execute!(stdout, Print(format!("{}\r\n", line)),)?; + execute!(stdout, Print(format!("{line}\r\n")))?; } - // reset color and re-display the current input line - // re-place cursor where user had it at input line - execute!( - stdout, - style::ResetColor, - cursor::MoveTo(0, state.win_rows), - Print(utils::truncate_in_place( - &state.current_line, - state.prompt.len(), - state.win_cols, - (state.line_col, state.cursor_col) - )), - cursor::MoveTo(state.cursor_col, state.win_rows), - )?; + // re-display the current input line + state.display_current_input_line()?; Ok(()) } @@ -262,12 +338,8 @@ async fn handle_event( command_history, win_cols, win_rows, - prompt, - line_col, - cursor_col, current_line, in_step_through, - search_mode, search_depth, logging_mode, verbose_mode, @@ -297,23 +369,14 @@ async fn handle_event( .chars() .filter(|c| !c.is_control() && !c.is_ascii_control()) .collect::(); - current_line.insert_str(*line_col, &pasted); - *line_col = *line_col + pasted.len(); - *cursor_col = std::cmp::min( - line_col.to_owned().try_into().unwrap_or(*win_cols), + current_line + .line + .insert_str(current_line.byte_index(), &pasted); + current_line.line_col = current_line.line_col + pasted.graphemes(true).count(); + current_line.cursor_col = std::cmp::min( + current_line.line_col.try_into().unwrap_or(*win_cols), *win_cols, ); - execute!( - stdout, - cursor::MoveTo(0, *win_rows), - Print(utils::truncate_in_place( - ¤t_line, - prompt.len(), - *win_cols, - (*line_col, *cursor_col) - )), - cursor::MoveTo(*cursor_col, *win_rows), - )?; } // // CTRL+C, CTRL+D: turn off the node @@ -380,6 +443,7 @@ async fn handle_event( ) .send(&print_tx) .await; + return Ok(false); } // // CTRL+J: toggle debug mode -- makes system-level event loop step-through @@ -403,6 +467,7 @@ async fn handle_event( ) .send(&print_tx) .await; + return Ok(false); } // // CTRL+S: step through system-level event loop (when in step-through mode) @@ -413,6 +478,7 @@ async fn handle_event( .. }) => { let _ = debug_event_loop.send(DebugCommand::Step).await; + return Ok(false); } // // CTRL+L: toggle logging mode @@ -429,6 +495,7 @@ async fn handle_event( ) .send(&print_tx) .await; + return Ok(false); } // // UP / CTRL+P: go up one command in history @@ -442,27 +509,20 @@ async fn handle_event( .. }) => { // go up one command in history - match command_history.get_prev(¤t_line[prompt.len()..]) { + match command_history.get_prev(¤t_line.line) { Some(line) => { - *current_line = format!("{} > {}", our.name, line); - *line_col = current_line.len(); + current_line.line_col = line.graphemes(true).count(); + current_line.line = line; } None => { // the "no-no" ding print!("\x07"); } } - *cursor_col = std::cmp::min(current_line.len() as u16, *win_cols); - execute!( - stdout, - cursor::MoveTo(0, *win_rows), - terminal::Clear(ClearType::CurrentLine), - Print(utils::truncate_rightward( - current_line, - prompt.len(), - *win_cols - )), - )?; + current_line.cursor_col = + std::cmp::min(current_line.line.graphemes(true).count() as u16, *win_cols); + state.display_current_input_line()?; + return Ok(false); } // // DOWN / CTRL+N: go down one command in history @@ -479,25 +539,18 @@ async fn handle_event( // go down one command in history match command_history.get_next() { Some(line) => { - *current_line = format!("{} > {}", our.name, line); - *line_col = current_line.len(); + current_line.line_col = line.graphemes(true).count(); + current_line.line = line; } None => { // the "no-no" ding print!("\x07"); } } - *cursor_col = std::cmp::min(current_line.len() as u16, *win_cols); - execute!( - stdout, - cursor::MoveTo(0, *win_rows), - terminal::Clear(ClearType::CurrentLine), - Print(utils::truncate_rightward( - ¤t_line, - prompt.len(), - *win_cols - )), - )?; + current_line.cursor_col = + std::cmp::min(current_line.line.graphemes(true).count() as u16, *win_cols); + state.display_current_input_line()?; + return Ok(false); } // // CTRL+A: jump to beginning of line @@ -507,19 +560,11 @@ async fn handle_event( modifiers: KeyModifiers::CONTROL, .. }) => { - *line_col = prompt.len(); - *cursor_col = prompt.len() as u16; - execute!( - stdout, - cursor::MoveTo(0, *win_rows), - Print(utils::truncate_from_left( - ¤t_line, - prompt.len(), - *win_cols, - *line_col - )), - cursor::MoveTo(*cursor_col, *win_rows), - )?; + if state.search_mode { + return Ok(false); + } + current_line.line_col = 0; + current_line.cursor_col = 0; } // // CTRL+E: jump to end of line @@ -529,21 +574,12 @@ async fn handle_event( modifiers: KeyModifiers::CONTROL, .. }) => { - *line_col = current_line.len(); - *cursor_col = std::cmp::min( - line_col.to_owned().try_into().unwrap_or(*win_cols), - *win_cols, - ); - execute!( - stdout, - cursor::MoveTo(0, *win_rows), - Print(utils::truncate_from_right( - ¤t_line, - prompt.len(), - *win_cols, - *line_col - )), - )?; + if state.search_mode { + return Ok(false); + } + current_line.line_col = current_line.line.graphemes(true).count(); + current_line.cursor_col = + std::cmp::min(current_line.line.graphemes(true).count() as u16, *win_cols); } // // CTRL+R: enter search mode @@ -554,20 +590,10 @@ async fn handle_event( modifiers: KeyModifiers::CONTROL, .. }) => { - if *search_mode { + if state.search_mode { *search_depth += 1; } - *search_mode = true; - utils::execute_search( - &our, - &mut stdout, - ¤t_line, - prompt.len(), - (*win_cols, *win_rows), - (*line_col, *cursor_col), - command_history, - *search_depth, - )?; + state.search_mode = true; } // // CTRL+G: exit search mode @@ -578,20 +604,8 @@ async fn handle_event( .. }) => { // just show true current line as usual - *search_mode = false; + state.search_mode = false; *search_depth = 0; - execute!( - stdout, - cursor::MoveTo(0, *win_rows), - terminal::Clear(ClearType::CurrentLine), - Print(utils::truncate_in_place( - &format!("{} > {}", our.name, ¤t_line[prompt.len()..]), - prompt.len(), - *win_cols, - (*line_col, *cursor_col) - )), - cursor::MoveTo(*cursor_col, *win_rows), - )?; } // // KEY: handle keypress events @@ -602,165 +616,71 @@ async fn handle_event( // CHAR: write a single character // KeyCode::Char(c) => { - current_line.insert(*line_col, c); - if cursor_col < win_cols { - *cursor_col += 1; + current_line.line.insert(current_line.byte_index(), c); + if current_line.cursor_col < *win_cols { + current_line.cursor_col += 1; } - *line_col += 1; - if *search_mode { - utils::execute_search( - &our, - &mut stdout, - ¤t_line, - prompt.len(), - (*win_cols, *win_rows), - (*line_col, *cursor_col), - command_history, - *search_depth, - )?; - return Ok(false); - } - execute!( - stdout, - cursor::MoveTo(0, *win_rows), - terminal::Clear(ClearType::CurrentLine), - Print(utils::truncate_in_place( - ¤t_line, - prompt.len(), - *win_cols, - (*line_col, *cursor_col) - )), - cursor::MoveTo(*cursor_col, *win_rows), - )?; + current_line.line_col += 1; } // // BACKSPACE: delete a single character at cursor // KeyCode::Backspace => { - if *line_col == prompt.len() { + if current_line.line_col == 0 { return Ok(false); } - if *cursor_col as usize == *line_col { - *cursor_col -= 1; + if current_line.cursor_col as usize > 0 { + current_line.cursor_col -= 1; } - *line_col -= 1; - current_line.remove(*line_col); - if *search_mode { - utils::execute_search( - &our, - &mut stdout, - ¤t_line, - prompt.len(), - (*win_cols, *win_rows), - (*line_col, *cursor_col), - command_history, - *search_depth, - )?; - return Ok(false); - } - execute!( - stdout, - cursor::MoveTo(0, *win_rows), - terminal::Clear(ClearType::CurrentLine), - Print(utils::truncate_in_place( - ¤t_line, - prompt.len(), - *win_cols, - (*line_col, *cursor_col) - )), - cursor::MoveTo(*cursor_col, *win_rows), - )?; + current_line.line_col -= 1; + current_line.line.remove(current_line.byte_index()); } // // DELETE: delete a single character at right of cursor // KeyCode::Delete => { - if *line_col == current_line.len() { + if current_line.line_col == current_line.line.graphemes(true).count() { return Ok(false); } - current_line.remove(*line_col); - if *search_mode { - utils::execute_search( - &our, - &mut stdout, - ¤t_line, - prompt.len(), - (*win_cols, *win_rows), - (*line_col, *cursor_col), - command_history, - *search_depth, - )?; - return Ok(false); - } - execute!( - stdout, - cursor::MoveTo(0, *win_rows), - terminal::Clear(ClearType::CurrentLine), - Print(utils::truncate_in_place( - ¤t_line, - prompt.len(), - *win_cols, - (*line_col, *cursor_col) - )), - cursor::MoveTo(*cursor_col, *win_rows), - )?; + current_line.line.remove(current_line.byte_index()); } // // LEFT: move cursor one spot left // KeyCode::Left => { - if *cursor_col as usize == prompt.len() { - if *line_col == prompt.len() { + if current_line.cursor_col as usize == 0 { + if current_line.line_col == 0 { // at the very beginning of the current typed line return Ok(false); } else { // virtual scroll leftward through line - *line_col -= 1; - execute!( - stdout, - cursor::MoveTo(0, *win_rows), - Print(utils::truncate_from_left( - ¤t_line, - prompt.len(), - *win_cols, - *line_col - )), - cursor::MoveTo(*cursor_col, *win_rows), - )?; + current_line.line_col -= 1; } } else { // simply move cursor and line position left - execute!(stdout, cursor::MoveLeft(1),)?; - *cursor_col -= 1; - *line_col -= 1; + execute!(stdout, cursor::MoveLeft(1))?; + current_line.cursor_col -= 1; + current_line.line_col -= 1; + return Ok(false); } } // // RIGHT: move cursor one spot right // KeyCode::Right => { - if *line_col == current_line.len() { + if current_line.line_col == current_line.line.graphemes(true).count() { // at the very end of the current typed line return Ok(false); }; - if *cursor_col < (*win_cols - 1) { + if current_line.cursor_col < (*win_cols - 1) { // simply move cursor and line position right execute!(stdout, cursor::MoveRight(1))?; - *cursor_col += 1; - *line_col += 1; + current_line.cursor_col += 1; + current_line.line_col += 1; + return Ok(false); } else { // virtual scroll rightward through line - *line_col += 1; - execute!( - stdout, - cursor::MoveTo(0, *win_rows), - Print(utils::truncate_from_right( - ¤t_line, - prompt.len(), - *win_cols, - *line_col - )), - )?; + current_line.line_col += 1; } } // @@ -768,11 +688,11 @@ async fn handle_event( // KeyCode::Enter => { // if we were in search mode, pull command from that - let command = if !*search_mode { - current_line[prompt.len()..].to_string() + let command = if !state.search_mode { + current_line.line.clone() } else { command_history - .search(¤t_line[prompt.len()..], *search_depth) + .search(¤t_line.line, *search_depth) .unwrap_or_default() .to_string() }; @@ -780,16 +700,15 @@ async fn handle_event( stdout, cursor::MoveTo(0, *win_rows), terminal::Clear(ClearType::CurrentLine), - Print(&format!("{} > {}", our.name, command)), + Print(¤t_line.prompt), + Print(&command), Print("\r\n"), - Print(&prompt), )?; - *search_mode = false; + state.search_mode = false; *search_depth = 0; - *current_line = prompt.to_string(); - command_history.add(command.clone()); - *cursor_col = prompt.len() as u16; - *line_col = prompt.len(); + current_line.cursor_col = 0; + current_line.line_col = 0; + command_history.add(command.to_string()); KernelMessage::builder() .id(rand::random()) .source((our.name.as_str(), TERMINAL_PROCESS_ID.clone())) @@ -805,6 +724,7 @@ async fn handle_event( .unwrap() .send(&event_loop) .await; + current_line.line = "".to_string(); } _ => { // some keycode we don't care about, yet @@ -815,5 +735,10 @@ async fn handle_event( // some terminal event we don't care about, yet } } + if state.search_mode { + state.search(&our.name)?; + } else { + state.display_current_input_line()?; + } Ok(false) } diff --git a/kinode/src/terminal/utils.rs b/kinode/src/terminal/utils.rs index 4316d12c..3ca278cc 100644 --- a/kinode/src/terminal/utils.rs +++ b/kinode/src/terminal/utils.rs @@ -5,6 +5,7 @@ use std::{ fs::File, io::{BufWriter, Stdout, Write}, }; +use unicode_segmentation::UnicodeSegmentation; pub struct RawMode; impl RawMode { @@ -123,6 +124,17 @@ pub fn splash( )) } +/// produce command line prompt and its length +pub fn make_prompt(our_name: &str) -> (&'static str, usize) { + let prompt = Box::leak(format!("{} > ", our_name).into_boxed_str()); + ( + prompt, + UnicodeSegmentation::graphemes(prompt, true) + .collect::>() + .len(), + ) +} + pub fn cleanup(quit_msg: &str) { let stdout = std::io::stdout(); let mut stdout = stdout.lock(); @@ -202,7 +214,6 @@ impl CommandHistory { /// if depth = 0, find most recent command in history that contains the /// provided string. otherwise, skip the first matches. - /// yes this is O(n) to provide desired ordering, can revisit if slow pub fn search(&mut self, find: &str, depth: usize) -> Option<&str> { let mut skips = 0; if find.is_empty() { @@ -224,104 +235,38 @@ impl CommandHistory { } } -pub fn execute_search( - our: &Identity, - stdout: &mut std::io::StdoutLock, - current_line: &str, - prompt_len: usize, - (win_cols, win_rows): (u16, u16), - (line_col, cursor_col): (usize, u16), - command_history: &mut CommandHistory, - search_depth: usize, -) -> Result<(), std::io::Error> { - let search_query = ¤t_line[prompt_len..]; - if let Some(result) = command_history.search(search_query, search_depth) { - let (result_underlined, u_end) = underline(result, search_query); - let search_cursor_col = u_end + prompt_len as u16; - crossterm::execute!( - stdout, - crossterm::cursor::MoveTo(0, win_rows), - crossterm::terminal::Clear(crossterm::terminal::ClearType::CurrentLine), - crossterm::style::Print(truncate_in_place( - &format!("{} * {}", our.name, result_underlined), - prompt_len, - win_cols, - (line_col, search_cursor_col) - )), - crossterm::cursor::MoveTo(search_cursor_col, win_rows), - ) - } else { - crossterm::execute!( - stdout, - crossterm::cursor::MoveTo(0, win_rows), - crossterm::terminal::Clear(crossterm::terminal::ClearType::CurrentLine), - crossterm::style::Print(truncate_in_place( - &format!("{} * {}: no results", our.name, ¤t_line[prompt_len..]), - prompt_len, - win_cols, - (line_col, cursor_col) - )), - crossterm::cursor::MoveTo(cursor_col, win_rows), - ) - } -} - pub fn underline(s: &str, to_underline: &str) -> (String, u16) { // format result string to have query portion underlined let mut result = s.to_string(); let u_start = s.find(to_underline).unwrap(); - let u_end = u_start + to_underline.len(); + let u_end = u_start + to_underline.graphemes(true).count(); result.insert_str(u_end, "\x1b[24m"); result.insert_str(u_start, "\x1b[4m"); (result, u_end as u16) } -pub fn truncate_rightward(s: &str, prompt_len: usize, width: u16) -> String { - if s.len() <= width as usize { - // no adjustment to be made - return s.to_string(); - } - let sans_prompt = &s[prompt_len..]; - s[..prompt_len].to_string() + &sans_prompt[(s.len() - width as usize)..] -} - -/// print prompt, then as many chars as will fit in term starting from line_col -pub fn truncate_from_left(s: &str, prompt_len: usize, width: u16, line_col: usize) -> String { - if s.len() <= width as usize { - // no adjustment to be made - return s.to_string(); - } - s[..prompt_len].to_string() + &s[line_col..(width as usize - prompt_len + line_col)] -} - -/// print prompt, then as many chars as will fit in term leading up to line_col -pub fn truncate_from_right(s: &str, prompt_len: usize, width: u16, line_col: usize) -> String { - if s.len() <= width as usize { - // no adjustment to be made - return s.to_string(); - } - s[..prompt_len].to_string() + &s[(prompt_len + (line_col - width as usize))..line_col] -} - /// if line is wider than the terminal, truncate it intelligently, /// keeping the cursor in the same relative position. -pub fn truncate_in_place( - s: &str, - prompt_len: usize, - width: u16, - (line_col, cursor_col): (usize, u16), -) -> String { - if s.len() <= width as usize { +pub fn truncate_in_place(s: &str, term_width: usize, line_col: usize, cursor_col: u16) -> String { + let graphemes_count = s.graphemes(true).count(); + if graphemes_count <= term_width { // no adjustment to be made return s.to_string(); } - // always keep prompt at left - let prompt = &s[..prompt_len]; - // print as much of the command fits left of col_in_command before cursor_col, - // then fill out the rest up to width - let end = width as usize + line_col - cursor_col as usize; - if end > s.len() { - return s.to_string(); + + // input line is wider than terminal, clip start/end/both while keeping cursor + // in same relative position. + if (cursor_col as usize) == line_col { + // beginning of line is placed at left end, truncate everything past term_width + s.graphemes(true).take(term_width).collect::() + } else if (cursor_col as usize) < line_col { + // some amount of the line is to the left of the terminal, clip from the right + s.graphemes(true) + .skip(line_col - cursor_col as usize) + .take(term_width) + .collect::() + } else { + // this cannot occur + unreachable!() } - prompt.to_string() + &s[(prompt_len + line_col - cursor_col as usize)..end] } From 16142969c07b1aaed6df4b5761bf0c08bcf685b8 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Thu, 5 Sep 2024 21:16:53 -0400 Subject: [PATCH 04/12] terminal fix: vritual scroll bugfixes --- kinode/src/terminal/mod.rs | 56 +++++++++++++++++++++++------------- kinode/src/terminal/utils.rs | 30 ++++++++++++------- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/kinode/src/terminal/mod.rs b/kinode/src/terminal/mod.rs index 0b59fff3..3e9d865c 100644 --- a/kinode/src/terminal/mod.rs +++ b/kinode/src/terminal/mod.rs @@ -45,7 +45,7 @@ struct State { } impl State { - fn display_current_input_line(&mut self) -> Result<(), std::io::Error> { + fn display_current_input_line(&mut self, show_end: bool) -> Result<(), std::io::Error> { execute!( self.stdout, cursor::MoveTo(0, self.win_rows), @@ -54,9 +54,10 @@ impl State { Print(self.current_line.prompt), Print(utils::truncate_in_place( &self.current_line.line, - self.win_cols as usize - self.current_line.prompt_len, + self.win_cols - self.current_line.prompt_len as u16, self.current_line.line_col, - self.current_line.cursor_col + self.current_line.cursor_col, + show_end )), cursor::MoveTo( self.current_line.prompt_len as u16 + self.current_line.cursor_col, @@ -79,9 +80,10 @@ impl State { style::Print(&search_prompt), style::Print(utils::truncate_in_place( &result_underlined, - self.win_cols as usize - self.current_line.prompt_len, + self.win_cols - self.current_line.prompt_len as u16, self.current_line.line_col, - search_cursor_col + search_cursor_col, + false, )), cursor::MoveTo( self.current_line.prompt_len as u16 + search_cursor_col, @@ -97,9 +99,10 @@ impl State { style::Print(&search_prompt), style::Print(utils::truncate_in_place( &format!("{}: no results", &self.current_line.line), - self.win_cols as usize - self.current_line.prompt_len, + self.win_cols - self.current_line.prompt_len as u16, self.current_line.line_col, - self.current_line.cursor_col + self.current_line.cursor_col, + false, )), cursor::MoveTo( self.current_line.prompt_len as u16 + self.current_line.cursor_col, @@ -320,7 +323,7 @@ fn handle_printout(printout: Printout, state: &mut State) -> anyhow::Result<()> execute!(stdout, Print(format!("{line}\r\n")))?; } // re-display the current input line - state.display_current_input_line()?; + state.display_current_input_line(false)?; Ok(()) } @@ -375,7 +378,7 @@ async fn handle_event( current_line.line_col = current_line.line_col + pasted.graphemes(true).count(); current_line.cursor_col = std::cmp::min( current_line.line_col.try_into().unwrap_or(*win_cols), - *win_cols, + *win_cols - current_line.prompt_len as u16, ); } // @@ -508,6 +511,9 @@ async fn handle_event( modifiers: KeyModifiers::CONTROL, .. }) => { + if state.search_mode { + return Ok(false); + } // go up one command in history match command_history.get_prev(¤t_line.line) { Some(line) => { @@ -519,9 +525,11 @@ async fn handle_event( print!("\x07"); } } - current_line.cursor_col = - std::cmp::min(current_line.line.graphemes(true).count() as u16, *win_cols); - state.display_current_input_line()?; + current_line.cursor_col = std::cmp::min( + current_line.line.graphemes(true).count() as u16, + *win_cols - current_line.prompt_len as u16, + ); + state.display_current_input_line(true)?; return Ok(false); } // @@ -536,6 +544,9 @@ async fn handle_event( modifiers: KeyModifiers::CONTROL, .. }) => { + if state.search_mode { + return Ok(false); + } // go down one command in history match command_history.get_next() { Some(line) => { @@ -547,9 +558,11 @@ async fn handle_event( print!("\x07"); } } - current_line.cursor_col = - std::cmp::min(current_line.line.graphemes(true).count() as u16, *win_cols); - state.display_current_input_line()?; + current_line.cursor_col = std::cmp::min( + current_line.line.graphemes(true).count() as u16, + *win_cols - current_line.prompt_len as u16, + ); + state.display_current_input_line(true)?; return Ok(false); } // @@ -578,8 +591,10 @@ async fn handle_event( return Ok(false); } current_line.line_col = current_line.line.graphemes(true).count(); - current_line.cursor_col = - std::cmp::min(current_line.line.graphemes(true).count() as u16, *win_cols); + current_line.cursor_col = std::cmp::min( + current_line.line.graphemes(true).count() as u16, + *win_cols - current_line.prompt_len as u16, + ); } // // CTRL+R: enter search mode @@ -617,7 +632,7 @@ async fn handle_event( // KeyCode::Char(c) => { current_line.line.insert(current_line.byte_index(), c); - if current_line.cursor_col < *win_cols { + if (current_line.cursor_col + current_line.prompt_len as u16) < *win_cols { current_line.cursor_col += 1; } current_line.line_col += 1; @@ -672,7 +687,8 @@ async fn handle_event( // at the very end of the current typed line return Ok(false); }; - if current_line.cursor_col < (*win_cols - 1) { + if (current_line.cursor_col + current_line.prompt_len as u16) < (*win_cols - 1) + { // simply move cursor and line position right execute!(stdout, cursor::MoveRight(1))?; current_line.cursor_col += 1; @@ -738,7 +754,7 @@ async fn handle_event( if state.search_mode { state.search(&our.name)?; } else { - state.display_current_input_line()?; + state.display_current_input_line(false)?; } Ok(false) } diff --git a/kinode/src/terminal/utils.rs b/kinode/src/terminal/utils.rs index 3ca278cc..af41daad 100644 --- a/kinode/src/terminal/utils.rs +++ b/kinode/src/terminal/utils.rs @@ -127,12 +127,7 @@ pub fn splash( /// produce command line prompt and its length pub fn make_prompt(our_name: &str) -> (&'static str, usize) { let prompt = Box::leak(format!("{} > ", our_name).into_boxed_str()); - ( - prompt, - UnicodeSegmentation::graphemes(prompt, true) - .collect::>() - .len(), - ) + (prompt, prompt.graphemes(true).count()) } pub fn cleanup(quit_msg: &str) { @@ -247,23 +242,36 @@ pub fn underline(s: &str, to_underline: &str) -> (String, u16) { /// if line is wider than the terminal, truncate it intelligently, /// keeping the cursor in the same relative position. -pub fn truncate_in_place(s: &str, term_width: usize, line_col: usize, cursor_col: u16) -> String { +pub fn truncate_in_place( + s: &str, + term_width: u16, + line_col: usize, + cursor_col: u16, + show_end: bool, +) -> String { let graphemes_count = s.graphemes(true).count(); - if graphemes_count <= term_width { + if graphemes_count <= term_width as usize { // no adjustment to be made return s.to_string(); } // input line is wider than terminal, clip start/end/both while keeping cursor // in same relative position. - if (cursor_col as usize) == line_col { + if show_end { + // show end of line, truncate everything before + s.graphemes(true) + .skip(graphemes_count - term_width as usize) + .collect::() + } else if (cursor_col as usize) == line_col { // beginning of line is placed at left end, truncate everything past term_width - s.graphemes(true).take(term_width).collect::() + s.graphemes(true) + .take(term_width as usize) + .collect::() } else if (cursor_col as usize) < line_col { // some amount of the line is to the left of the terminal, clip from the right s.graphemes(true) .skip(line_col - cursor_col as usize) - .take(term_width) + .take(term_width as usize) .collect::() } else { // this cannot occur From 52e96c36b9b321a4db4c781eb10b2e591228a702 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Thu, 5 Sep 2024 22:00:17 -0400 Subject: [PATCH 05/12] terminal: fix special charater handling once and for all (?) --- kinode/src/terminal/mod.rs | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/kinode/src/terminal/mod.rs b/kinode/src/terminal/mod.rs index 3e9d865c..115cd875 100644 --- a/kinode/src/terminal/mod.rs +++ b/kinode/src/terminal/mod.rs @@ -131,7 +131,27 @@ impl CurrentLine { .grapheme_indices(true) .nth(self.line_col) .map(|(i, _)| i) - .unwrap_or_else(|| self.line.graphemes(true).count()) + .unwrap_or_else(|| self.line.len()) + } + + fn insert_char(&mut self, c: char) { + let byte_index = self.byte_index(); + self.line.insert(byte_index, c); + } + + fn insert_str(&mut self, s: &str) { + let byte_index = self.byte_index(); + self.line.insert_str(byte_index, s); + } + + fn delete_char(&mut self) { + let byte_index = self.byte_index(); + let next_grapheme = self.line[byte_index..] + .graphemes(true) + .next() + .map(|g| g.len()) + .unwrap_or(0); + self.line.drain(byte_index..byte_index + next_grapheme); } } @@ -372,9 +392,7 @@ async fn handle_event( .chars() .filter(|c| !c.is_control() && !c.is_ascii_control()) .collect::(); - current_line - .line - .insert_str(current_line.byte_index(), &pasted); + current_line.insert_str(&pasted); current_line.line_col = current_line.line_col + pasted.graphemes(true).count(); current_line.cursor_col = std::cmp::min( current_line.line_col.try_into().unwrap_or(*win_cols), @@ -631,7 +649,7 @@ async fn handle_event( // CHAR: write a single character // KeyCode::Char(c) => { - current_line.line.insert(current_line.byte_index(), c); + current_line.insert_char(c); if (current_line.cursor_col + current_line.prompt_len as u16) < *win_cols { current_line.cursor_col += 1; } @@ -648,7 +666,7 @@ async fn handle_event( current_line.cursor_col -= 1; } current_line.line_col -= 1; - current_line.line.remove(current_line.byte_index()); + current_line.delete_char(); } // // DELETE: delete a single character at right of cursor @@ -657,7 +675,7 @@ async fn handle_event( if current_line.line_col == current_line.line.graphemes(true).count() { return Ok(false); } - current_line.line.remove(current_line.byte_index()); + current_line.delete_char(); } // // LEFT: move cursor one spot left From 362741733d417c74712f0da4f719923bd8c9c366 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Fri, 6 Sep 2024 12:40:48 -0400 Subject: [PATCH 06/12] fix: improve resize events *slightly* (still not great) --- kinode/src/terminal/mod.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/kinode/src/terminal/mod.rs b/kinode/src/terminal/mod.rs index 115cd875..28cfbca4 100644 --- a/kinode/src/terminal/mod.rs +++ b/kinode/src/terminal/mod.rs @@ -380,8 +380,18 @@ async fn handle_event( // generally stable way. // Event::Resize(width, height) => { - *win_cols = width; + // this is critical at moment of resize not to double-up lines + execute!( + state.stdout, + cursor::MoveTo(0, height), + terminal::Clear(ClearType::CurrentLine) + )?; + *win_cols = width - 1; *win_rows = height; + if current_line.cursor_col + current_line.prompt_len as u16 > *win_cols { + current_line.cursor_col = *win_cols - current_line.prompt_len as u16; + current_line.line_col = current_line.cursor_col as usize; + } } // // PASTE: handle pasting of text from outside From 5180e243878589d848d5844abdbd853c34ad8a4f Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Tue, 10 Sep 2024 23:25:54 -0400 Subject: [PATCH 07/12] fix ctrl+R, use unicode-width to manage 2-wide graphemes --- Cargo.lock | 1 + kinode/Cargo.toml | 1 + kinode/src/terminal/mod.rs | 76 ++++++++++++++++++++++-------------- kinode/src/terminal/utils.rs | 18 ++++++--- 4 files changed, 60 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 996b50a5..d53477b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3586,6 +3586,7 @@ dependencies = [ "tokio", "tokio-tungstenite 0.21.0", "unicode-segmentation", + "unicode-width", "url", "walkdir", "warp", diff --git a/kinode/Cargo.toml b/kinode/Cargo.toml index cd4e1a44..cc76e55c 100644 --- a/kinode/Cargo.toml +++ b/kinode/Cargo.toml @@ -85,6 +85,7 @@ thiserror = "1.0" tokio = { version = "1.28", features = ["fs", "macros", "rt-multi-thread", "signal", "sync"] } tokio-tungstenite = { version = "0.21.0", features = ["native-tls"] } unicode-segmentation = "1.11" +unicode-width = "0.1.13" url = "2.4.1" warp = "0.3.5" wasi-common = "19.0.1" diff --git a/kinode/src/terminal/mod.rs b/kinode/src/terminal/mod.rs index c8aa2fb0..037f1c1d 100644 --- a/kinode/src/terminal/mod.rs +++ b/kinode/src/terminal/mod.rs @@ -70,8 +70,7 @@ impl State { let search_prompt = format!("{} *", our_name); let search_query = &self.current_line.line; if let Some(result) = self.command_history.search(search_query, self.search_depth) { - let (result_underlined, u_end) = utils::underline(result, search_query); - let search_cursor_col = u_end + search_prompt.graphemes(true).count() as u16; + let (result_underlined, search_cursor_col) = utils::underline(result, search_query); execute!( self.stdout, cursor::MoveTo(0, self.win_rows), @@ -134,6 +133,18 @@ impl CurrentLine { .unwrap_or_else(|| self.line.len()) } + fn current_char_left(&self) -> Option<&str> { + if self.line_col == 0 { + None + } else { + self.line.graphemes(true).nth(self.line_col - 1) + } + } + + fn current_char_right(&self) -> Option<&str> { + self.line.graphemes(true).nth(self.line_col) + } + fn insert_char(&mut self, c: char) { let byte_index = self.byte_index(); self.line.insert(byte_index, c); @@ -144,14 +155,17 @@ impl CurrentLine { self.line.insert_str(byte_index, s); } - fn delete_char(&mut self) { + /// returns the deleted character + fn delete_char(&mut self) -> String { let byte_index = self.byte_index(); let next_grapheme = self.line[byte_index..] .graphemes(true) .next() .map(|g| g.len()) .unwrap_or(0); - self.line.drain(byte_index..byte_index + next_grapheme); + self.line + .drain(byte_index..byte_index + next_grapheme) + .collect() } } @@ -403,7 +417,7 @@ async fn handle_event( .filter(|c| !c.is_control() && !c.is_ascii_control()) .collect::(); current_line.insert_str(&pasted); - current_line.line_col = current_line.line_col + pasted.graphemes(true).count(); + current_line.line_col = current_line.line_col + utils::display_width(&pasted); current_line.cursor_col = std::cmp::min( current_line.line_col.try_into().unwrap_or(*win_cols), *win_cols - current_line.prompt_len as u16, @@ -545,18 +559,17 @@ async fn handle_event( // go up one command in history match command_history.get_prev(¤t_line.line) { Some(line) => { - current_line.line_col = line.graphemes(true).count(); + let width = utils::display_width(&line); + current_line.line_col = width; current_line.line = line; + current_line.cursor_col = + std::cmp::min(width as u16, *win_cols - current_line.prompt_len as u16); } None => { // the "no-no" ding print!("\x07"); } } - current_line.cursor_col = std::cmp::min( - current_line.line.graphemes(true).count() as u16, - *win_cols - current_line.prompt_len as u16, - ); state.display_current_input_line(true)?; return Ok(false); } @@ -578,18 +591,17 @@ async fn handle_event( // go down one command in history match command_history.get_next() { Some(line) => { - current_line.line_col = line.graphemes(true).count(); + let width = utils::display_width(&line); + current_line.line_col = width; current_line.line = line; + current_line.cursor_col = + std::cmp::min(width as u16, *win_cols - current_line.prompt_len as u16); } None => { // the "no-no" ding print!("\x07"); } } - current_line.cursor_col = std::cmp::min( - current_line.line.graphemes(true).count() as u16, - *win_cols - current_line.prompt_len as u16, - ); state.display_current_input_line(true)?; return Ok(false); } @@ -618,11 +630,10 @@ async fn handle_event( if state.search_mode { return Ok(false); } - current_line.line_col = current_line.line.graphemes(true).count(); - current_line.cursor_col = std::cmp::min( - current_line.line.graphemes(true).count() as u16, - *win_cols - current_line.prompt_len as u16, - ); + let width = utils::display_width(¤t_line.line); + current_line.line_col = width; + current_line.cursor_col = + std::cmp::min(width as u16, *win_cols - current_line.prompt_len as u16); } // // CTRL+R: enter search mode @@ -661,7 +672,7 @@ async fn handle_event( KeyCode::Char(c) => { current_line.insert_char(c); if (current_line.cursor_col + current_line.prompt_len as u16) < *win_cols { - current_line.cursor_col += 1; + current_line.cursor_col += utils::display_width(&c.to_string()) as u16; } current_line.line_col += 1; } @@ -671,18 +682,17 @@ async fn handle_event( KeyCode::Backspace => { if current_line.line_col == 0 { return Ok(false); + } else { + current_line.line_col -= 1; + let c = current_line.delete_char(); + current_line.cursor_col -= utils::display_width(&c) as u16; } - if current_line.cursor_col as usize > 0 { - current_line.cursor_col -= 1; - } - current_line.line_col -= 1; - current_line.delete_char(); } // // DELETE: delete a single character at right of cursor // KeyCode::Delete => { - if current_line.line_col == current_line.line.graphemes(true).count() { + if current_line.line_col == utils::display_width(¤t_line.line) { return Ok(false); } current_line.delete_char(); @@ -702,7 +712,10 @@ async fn handle_event( } else { // simply move cursor and line position left execute!(stdout, cursor::MoveLeft(1))?; - current_line.cursor_col -= 1; + current_line.cursor_col -= current_line + .current_char_left() + .map_or_else(|| 1, |c| utils::display_width(&c)) + as u16; current_line.line_col -= 1; return Ok(false); } @@ -711,7 +724,7 @@ async fn handle_event( // RIGHT: move cursor one spot right // KeyCode::Right => { - if current_line.line_col == current_line.line.graphemes(true).count() { + if current_line.line_col == utils::display_width(¤t_line.line) { // at the very end of the current typed line return Ok(false); }; @@ -719,7 +732,10 @@ async fn handle_event( { // simply move cursor and line position right execute!(stdout, cursor::MoveRight(1))?; - current_line.cursor_col += 1; + current_line.cursor_col += current_line + .current_char_right() + .map_or_else(|| 1, |c| utils::display_width(&c)) + as u16; current_line.line_col += 1; return Ok(false); } else { diff --git a/kinode/src/terminal/utils.rs b/kinode/src/terminal/utils.rs index ca7b60de..2730b4ef 100644 --- a/kinode/src/terminal/utils.rs +++ b/kinode/src/terminal/utils.rs @@ -6,6 +6,7 @@ use std::{ io::{BufWriter, Stdout, Write}, }; use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; pub struct RawMode; impl RawMode { @@ -124,10 +125,14 @@ pub fn splash( )) } +pub fn display_width(s: &str) -> usize { + UnicodeWidthStr::width(s) +} + /// produce command line prompt and its length pub fn make_prompt(our_name: &str) -> (&'static str, usize) { let prompt = Box::leak(format!("{} > ", our_name).into_boxed_str()); - (prompt, prompt.graphemes(true).count()) + (prompt, display_width(prompt)) } pub fn cleanup(quit_msg: &str) { @@ -234,10 +239,11 @@ pub fn underline(s: &str, to_underline: &str) -> (String, u16) { // format result string to have query portion underlined let mut result = s.to_string(); let u_start = s.find(to_underline).unwrap(); - let u_end = u_start + to_underline.graphemes(true).count(); + let u_end = u_start + to_underline.len(); result.insert_str(u_end, "\x1b[24m"); result.insert_str(u_start, "\x1b[4m"); - (result, u_end as u16) + let cursor_end = display_width(&result[..u_end]); + (result, cursor_end as u16) } /// if line is wider than the terminal, truncate it intelligently, @@ -249,8 +255,8 @@ pub fn truncate_in_place( cursor_col: u16, show_end: bool, ) -> String { - let graphemes_count = s.graphemes(true).count(); - if graphemes_count <= term_width as usize { + let width = display_width(s); + if width <= term_width as usize { // no adjustment to be made return s.to_string(); } @@ -260,7 +266,7 @@ pub fn truncate_in_place( if show_end { // show end of line, truncate everything before s.graphemes(true) - .skip(graphemes_count - term_width as usize) + .skip(width - term_width as usize) .collect::() } else if (cursor_col as usize) == line_col { // beginning of line is placed at left end, truncate everything past term_width From bb875abd311f39b5995610d687acb68201c266d4 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Tue, 10 Sep 2024 23:55:40 -0400 Subject: [PATCH 08/12] fix conflation between line_col and cursor_col -- emojis working nicely --- kinode/src/terminal/mod.rs | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/kinode/src/terminal/mod.rs b/kinode/src/terminal/mod.rs index 037f1c1d..78443a49 100644 --- a/kinode/src/terminal/mod.rs +++ b/kinode/src/terminal/mod.rs @@ -404,7 +404,8 @@ async fn handle_event( *win_rows = height; if current_line.cursor_col + current_line.prompt_len as u16 > *win_cols { current_line.cursor_col = *win_cols - current_line.prompt_len as u16; - current_line.line_col = current_line.cursor_col as usize; + // can't do this because of wide graphemes :/ + // current_line.line_col = current_line.cursor_col as usize; } } // @@ -417,9 +418,9 @@ async fn handle_event( .filter(|c| !c.is_control() && !c.is_ascii_control()) .collect::(); current_line.insert_str(&pasted); - current_line.line_col = current_line.line_col + utils::display_width(&pasted); + current_line.line_col = current_line.line_col + pasted.graphemes(true).count(); current_line.cursor_col = std::cmp::min( - current_line.line_col.try_into().unwrap_or(*win_cols), + current_line.cursor_col + utils::display_width(&pasted) as u16, *win_cols - current_line.prompt_len as u16, ); } @@ -560,7 +561,7 @@ async fn handle_event( match command_history.get_prev(¤t_line.line) { Some(line) => { let width = utils::display_width(&line); - current_line.line_col = width; + current_line.line_col = line.graphemes(true).count(); current_line.line = line; current_line.cursor_col = std::cmp::min(width as u16, *win_cols - current_line.prompt_len as u16); @@ -592,7 +593,7 @@ async fn handle_event( match command_history.get_next() { Some(line) => { let width = utils::display_width(&line); - current_line.line_col = width; + current_line.line_col = line.graphemes(true).count(); current_line.line = line; current_line.cursor_col = std::cmp::min(width as u16, *win_cols - current_line.prompt_len as u16); @@ -630,10 +631,11 @@ async fn handle_event( if state.search_mode { return Ok(false); } - let width = utils::display_width(¤t_line.line); - current_line.line_col = width; - current_line.cursor_col = - std::cmp::min(width as u16, *win_cols - current_line.prompt_len as u16); + current_line.line_col = current_line.line.graphemes(true).count(); + current_line.cursor_col = std::cmp::min( + utils::display_width(¤t_line.line) as u16, + *win_cols - current_line.prompt_len as u16, + ); } // // CTRL+R: enter search mode @@ -692,7 +694,7 @@ async fn handle_event( // DELETE: delete a single character at right of cursor // KeyCode::Delete => { - if current_line.line_col == utils::display_width(¤t_line.line) { + if current_line.line_col == current_line.line.graphemes(true).count() { return Ok(false); } current_line.delete_char(); @@ -711,11 +713,12 @@ async fn handle_event( } } else { // simply move cursor and line position left - execute!(stdout, cursor::MoveLeft(1))?; - current_line.cursor_col -= current_line + let width = current_line .current_char_left() .map_or_else(|| 1, |c| utils::display_width(&c)) as u16; + execute!(stdout, cursor::MoveLeft(width))?; + current_line.cursor_col -= width; current_line.line_col -= 1; return Ok(false); } @@ -724,18 +727,19 @@ async fn handle_event( // RIGHT: move cursor one spot right // KeyCode::Right => { - if current_line.line_col == utils::display_width(¤t_line.line) { + if current_line.line_col == current_line.line.graphemes(true).count() { // at the very end of the current typed line return Ok(false); }; if (current_line.cursor_col + current_line.prompt_len as u16) < (*win_cols - 1) { // simply move cursor and line position right - execute!(stdout, cursor::MoveRight(1))?; - current_line.cursor_col += current_line + let width = current_line .current_char_right() .map_or_else(|| 1, |c| utils::display_width(&c)) as u16; + execute!(stdout, cursor::MoveRight(width))?; + current_line.cursor_col += width; current_line.line_col += 1; return Ok(false); } else { From 0696127ed1497e5a936bc29d80af3ade365fa733 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Wed, 11 Sep 2024 01:28:51 -0400 Subject: [PATCH 09/12] virtual scroll working...? --- kinode/src/terminal/mod.rs | 4 ++- kinode/src/terminal/utils.rs | 50 ++++++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/kinode/src/terminal/mod.rs b/kinode/src/terminal/mod.rs index 78443a49..bacfb2b4 100644 --- a/kinode/src/terminal/mod.rs +++ b/kinode/src/terminal/mod.rs @@ -719,7 +719,9 @@ async fn handle_event( as u16; execute!(stdout, cursor::MoveLeft(width))?; current_line.cursor_col -= width; - current_line.line_col -= 1; + if current_line.line_col != 0 { + current_line.line_col -= 1; + } return Ok(false); } } diff --git a/kinode/src/terminal/utils.rs b/kinode/src/terminal/utils.rs index 2730b4ef..25b4ec20 100644 --- a/kinode/src/terminal/utils.rs +++ b/kinode/src/terminal/utils.rs @@ -261,26 +261,54 @@ pub fn truncate_in_place( return s.to_string(); } + let graphemes_with_width = s.graphemes(true).map(|g| (g, display_width(g))); + + let adjusted_cursor_col = graphemes_with_width + .clone() + .take(cursor_col as usize) + .map(|(_, w)| w) + .sum::(); + // input line is wider than terminal, clip start/end/both while keeping cursor // in same relative position. - if show_end { + if show_end || cursor_col >= term_width { // show end of line, truncate everything before - s.graphemes(true) - .skip(width - term_width as usize) + let mut width = 0; + graphemes_with_width + .rev() + .take_while(|(_, w)| { + width += w; + width <= term_width as usize + }) + .map(|(g, _)| g) .collect::() - } else if (cursor_col as usize) == line_col { + .chars() + .rev() + .collect::() + } else if adjusted_cursor_col as usize == line_col { // beginning of line is placed at left end, truncate everything past term_width - s.graphemes(true) - .take(term_width as usize) + let mut width = 0; + graphemes_with_width + .take_while(|(_, w)| { + width += w; + width <= term_width as usize + }) + .map(|(g, _)| g) .collect::() - } else if (cursor_col as usize) < line_col { + } else if adjusted_cursor_col < line_col { // some amount of the line is to the left of the terminal, clip from the right - s.graphemes(true) - .skip(line_col - cursor_col as usize) - .take(term_width as usize) + // skip the difference between line_col and cursor_col *after adjusting for + // wide characters + let mut width = 0; + graphemes_with_width + .skip(line_col - adjusted_cursor_col) + .take_while(|(_, w)| { + width += w; + width <= term_width as usize + }) + .map(|(g, _)| g) .collect::() } else { - // this cannot occur unreachable!() } } From 5f5bdd3fa06229d219f42a62e2c399d7dfc030e6 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Wed, 11 Sep 2024 01:36:06 -0400 Subject: [PATCH 10/12] =?UTF-8?q?Soon=E2=84=A2=EF=B8=8F=20working=20(but?= =?UTF-8?q?=20actually=20now)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kinode/src/terminal/utils.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/kinode/src/terminal/utils.rs b/kinode/src/terminal/utils.rs index 25b4ec20..a464a137 100644 --- a/kinode/src/terminal/utils.rs +++ b/kinode/src/terminal/utils.rs @@ -239,9 +239,16 @@ pub fn underline(s: &str, to_underline: &str) -> (String, u16) { // format result string to have query portion underlined let mut result = s.to_string(); let u_start = s.find(to_underline).unwrap(); - let u_end = u_start + to_underline.len(); + let mut u_end = u_start + to_underline.len(); result.insert_str(u_end, "\x1b[24m"); result.insert_str(u_start, "\x1b[4m"); + // check if u_end is at a character boundary + loop { + if u_end == s.len() || s.is_char_boundary(u_end) { + break; + } + u_end += 1; + } let cursor_end = display_width(&result[..u_end]); (result, cursor_end as u16) } @@ -309,6 +316,18 @@ pub fn truncate_in_place( .map(|(g, _)| g) .collect::() } else { - unreachable!() + // show end of line, truncate everything before + let mut width = 0; + graphemes_with_width + .rev() + .take_while(|(_, w)| { + width += w; + width <= term_width as usize + }) + .map(|(g, _)| g) + .collect::() + .chars() + .rev() + .collect::() } } From ab57a7e39fc6a34da9804d2f48d20f59fe068409 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Thu, 12 Sep 2024 14:56:44 -0400 Subject: [PATCH 11/12] remove conditional allowing for non-char-boundary inserts --- kinode/src/terminal/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kinode/src/terminal/utils.rs b/kinode/src/terminal/utils.rs index a464a137..e1f77372 100644 --- a/kinode/src/terminal/utils.rs +++ b/kinode/src/terminal/utils.rs @@ -244,7 +244,7 @@ pub fn underline(s: &str, to_underline: &str) -> (String, u16) { result.insert_str(u_start, "\x1b[4m"); // check if u_end is at a character boundary loop { - if u_end == s.len() || s.is_char_boundary(u_end) { + if s.is_char_boundary(u_end) { break; } u_end += 1; From acfed3e55390c1d0103c6e2259bfe38293f726cf Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Thu, 12 Sep 2024 17:55:20 -0400 Subject: [PATCH 12/12] lol --- kinode/src/terminal/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kinode/src/terminal/utils.rs b/kinode/src/terminal/utils.rs index e1f77372..9674562b 100644 --- a/kinode/src/terminal/utils.rs +++ b/kinode/src/terminal/utils.rs @@ -244,7 +244,7 @@ pub fn underline(s: &str, to_underline: &str) -> (String, u16) { result.insert_str(u_start, "\x1b[4m"); // check if u_end is at a character boundary loop { - if s.is_char_boundary(u_end) { + if result.is_char_boundary(u_end) { break; } u_end += 1;