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