From b161924bbcb03850af5faee87dab50a2108af99a Mon Sep 17 00:00:00 2001 From: Niko Marmeladkov Date: Wed, 15 Jul 2026 22:23:24 +0300 Subject: [PATCH] fix cursor tracking: use char indices instead of byte for multi-byte utf-8 support --- src/attach.rs | 64 ++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/src/attach.rs b/src/attach.rs index 61b14b7..4a50550 100644 --- a/src/attach.rs +++ b/src/attach.rs @@ -124,12 +124,17 @@ fn follow_log(path: &str, tx: mpsc::Sender) { } } +fn byte_at(s: &str, char_idx: usize) -> usize { + s.char_indices().nth(char_idx).map(|(i, _)| i).unwrap_or(s.len()) +} + fn adjust_scroll(input: &str, cursor: usize, scroll: &mut usize, cols: usize) { let max_visible = cols.saturating_sub(2); if max_visible == 0 { return; } let threshold = (max_visible as f64 * 0.9) as usize; + let nchars = input.chars().count(); if cursor > *scroll + threshold { *scroll = cursor - threshold; @@ -137,8 +142,8 @@ fn adjust_scroll(input: &str, cursor: usize, scroll: &mut usize, cols: usize) { if cursor < *scroll { *scroll = cursor; } - if *scroll + max_visible > input.len() { - *scroll = input.len().saturating_sub(max_visible); + if *scroll + max_visible > nchars { + *scroll = nchars.saturating_sub(max_visible); } } @@ -178,18 +183,12 @@ fn draw_all( let prompt = "> "; let max_visible = cols.saturating_sub(prompt.len()); - let visible_start = scroll; - let visible_end = (scroll + max_visible).min(input.len()); - let visible = if scroll < input.len() { - &input[visible_start..visible_end] - } else { - "" - }; + let visible: String = input.chars().skip(scroll).take(max_visible).collect(); out.push_str(&format!("\x1b[{};1H", rows)); out.push_str(GRAY_BG); out.push_str(prompt); - out.push_str(visible); + out.push_str(&visible); out.push_str("\x1b[K"); out.push_str(RESET); @@ -389,8 +388,8 @@ pub fn run_attach(name: &str, full: bool, stream: UnixStream) { } if let Some(idx) = history_idx { input = history[idx].clone(); - cursor = input.len(); - scroll = input.len().saturating_sub(cols.saturating_sub(2)); + cursor = input.chars().count(); + scroll = input.chars().count().saturating_sub(cols.saturating_sub(2)); } adjust_scroll(&input, cursor, &mut scroll, cols); } @@ -399,7 +398,7 @@ pub fn run_attach(name: &str, full: bool, stream: UnixStream) { if idx + 1 < history.len() { history_idx = Some(idx + 1); input = history[idx + 1].clone(); - cursor = input.len(); + cursor = input.chars().count(); } else { history_idx = None; input.clear(); @@ -449,13 +448,6 @@ pub fn run_attach(name: &str, full: bool, stream: UnixStream) { } } -fn prev_char_boundary(s: &str, cursor: usize) -> usize { - if cursor == 0 { - return 0; - } - s.floor_char_boundary(cursor - 1) -} - fn process_input_byte( b: u8, input: &mut String, @@ -473,9 +465,11 @@ fn process_input_byte( 0x0e => return InputAction::HistoryNext, 0x08 | 0x7f => { if *cursor > 0 { - let start = prev_char_boundary(input, *cursor); - input.drain(start..*cursor); - *cursor = start; + *cursor -= 1; + let byte_start = byte_at(input, *cursor); + let c = input[byte_start..].chars().next().unwrap(); + let n = c.len_utf8(); + input.drain(byte_start..byte_start + n); } adjust_scroll(input, *cursor, scroll, cols); return InputAction::None; @@ -486,7 +480,7 @@ fn process_input_byte( return InputAction::None; } 0x05 => { - *cursor = input.len(); + *cursor = input.chars().count(); adjust_scroll(input, *cursor, scroll, cols); return InputAction::None; } @@ -497,7 +491,7 @@ fn process_input_byte( return InputAction::None; } 0x0b => { - input.truncate(*cursor); + input.truncate(byte_at(input, *cursor)); return InputAction::None; } _ => {} @@ -514,9 +508,9 @@ fn process_input_byte( if Some(utf8_buf.len()) == *utf8_expected { if let Ok(s) = std::str::from_utf8(&utf8_buf) { for c in s.chars() { - let n = c.len_utf8(); - input.insert(*cursor, c); - *cursor += n; + let byte_pos = byte_at(input, *cursor); + input.insert(byte_pos, c); + *cursor += 1; } adjust_scroll(input, *cursor, scroll, cols); } @@ -528,7 +522,8 @@ fn process_input_byte( *utf8_expected = None; } } else if b >= 0x20 { - input.insert(*cursor, b as char); + let byte_pos = byte_at(input, *cursor); + input.insert(byte_pos, b as char); *cursor += 1; adjust_scroll(input, *cursor, scroll, cols); } @@ -558,7 +553,7 @@ fn process_escape( } if let Some(idx) = *history_idx { *input = history[idx].clone(); - *cursor = input.len(); + *cursor = input.chars().count(); } adjust_scroll(input, *cursor, scroll, cols); InputAction::None @@ -569,7 +564,7 @@ fn process_escape( if idx + 1 < history.len() { *history_idx = Some(idx + 1); *input = history[idx + 1].clone(); - *cursor = input.len(); + *cursor = input.chars().count(); } else { *history_idx = None; input.clear(); @@ -581,7 +576,8 @@ fn process_escape( } b'C' => { // Right arrow - if *cursor < input.len() { + let nchars = input.chars().count(); + if *cursor < nchars { *cursor += 1; } adjust_scroll(input, *cursor, scroll, cols); @@ -589,7 +585,7 @@ fn process_escape( } b'D' => { // Left arrow - *cursor = prev_char_boundary(input, *cursor); + *cursor = cursor.saturating_sub(1); adjust_scroll(input, *cursor, scroll, cols); InputAction::None } @@ -601,7 +597,7 @@ fn process_escape( } b'F' => { // End - *cursor = input.len(); + *cursor = input.chars().count(); adjust_scroll(input, *cursor, scroll, cols); InputAction::None }