From e0751cdbf14f49a7e3e3021129fa4366c55c3790 Mon Sep 17 00:00:00 2001 From: dr-frmr Date: Thu, 5 Sep 2024 20:36:26 -0400 Subject: [PATCH] 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] }