fix cursor tracking: use char indices instead of byte for multi-byte utf-8 support

This commit is contained in:
Niko Marmeladkov 2026-07-15 22:23:24 +03:00
parent 66ca52c187
commit b161924bbc
Signed by untrusted user who does not match committer: Niko
GPG key ID: E3B955F9442D44E3

View file

@ -124,12 +124,17 @@ fn follow_log(path: &str, tx: mpsc::Sender<String>) {
}
}
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
}