tui: fix cursor desync by scanning rendered buffer
Instead of simulating ratatui's word wrapping algorithm, scan the rendered buffer to find the actual cursor position. This correctly handles word wrapping, unicode widths, and any other rendering nuances that ratatui applies. The old code computed wrapped_height() and cursor position based on simple character counting, which diverged from ratatui's WordWrapper that respects word boundaries. Now we render first, then walk the buffer counting visible characters until we reach self.cursor. This is O(area) but the input area is small (typically < 200 cells), so it's negligible.
This commit is contained in:
parent
5308c8e3a4
commit
ec79d60fbd
4 changed files with 52 additions and 133 deletions
137
Cargo.lock
generated
137
Cargo.lock
generated
|
|
@ -339,12 +339,6 @@ dependencies = [
|
|||
"capnp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
|
|
@ -447,20 +441,6 @@ version = "1.0.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.9.0"
|
||||
|
|
@ -1350,7 +1330,7 @@ version = "0.2.24"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
||||
dependencies = [
|
||||
"unicode-width 0.2.0",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1446,8 +1426,6 @@ version = "0.15.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
|
|
@ -1810,15 +1788,6 @@ version = "1.70.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
|
|
@ -2007,15 +1976,6 @@ dependencies = [
|
|||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.16.3"
|
||||
|
|
@ -2648,13 +2608,14 @@ dependencies = [
|
|||
"glob",
|
||||
"json5",
|
||||
"libc",
|
||||
"ratatui 0.30.0",
|
||||
"ratatui",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tiktoken-rs",
|
||||
"tokio",
|
||||
"tui-markdown",
|
||||
"unicode-width",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
|
|
@ -2701,7 +2662,7 @@ dependencies = [
|
|||
"paste",
|
||||
"peg",
|
||||
"poc-agent",
|
||||
"ratatui 0.29.0",
|
||||
"ratatui",
|
||||
"rayon",
|
||||
"redb",
|
||||
"regex",
|
||||
|
|
@ -3055,27 +3016,6 @@ dependencies = [
|
|||
"rand 0.9.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"cassowary",
|
||||
"compact_str 0.8.1",
|
||||
"crossterm 0.28.1",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools 0.13.0",
|
||||
"lru 0.12.5",
|
||||
"paste",
|
||||
"strum 0.26.3",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate 1.1.0",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.30.0"
|
||||
|
|
@ -3097,17 +3037,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"compact_str 0.9.0",
|
||||
"compact_str",
|
||||
"hashbrown 0.16.1",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"itertools",
|
||||
"kasuari",
|
||||
"lru 0.16.3",
|
||||
"strum 0.27.2",
|
||||
"lru",
|
||||
"strum",
|
||||
"thiserror 2.0.18",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate 2.0.1",
|
||||
"unicode-width 0.2.0",
|
||||
"unicode-truncate",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3152,13 +3092,13 @@ dependencies = [
|
|||
"hashbrown 0.16.1",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools 0.14.0",
|
||||
"itertools",
|
||||
"line-clipping",
|
||||
"ratatui-core",
|
||||
"strum 0.27.2",
|
||||
"strum",
|
||||
"time",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.0",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3751,35 +3691,13 @@ version = "0.11.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros 0.26.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
dependencies = [
|
||||
"strum_macros 0.27.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.117",
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4366,7 +4284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e766339aabad4528c3fccddf4acf03bc2b7ae6642def41e43c7af1a11f183122"
|
||||
dependencies = [
|
||||
"ansi-to-tui",
|
||||
"itertools 0.14.0",
|
||||
"itertools",
|
||||
"pretty_assertions",
|
||||
"pulldown-cmark",
|
||||
"ratatui-core",
|
||||
|
|
@ -4414,39 +4332,22 @@ version = "1.12.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-truncate"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-truncate"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"itertools",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.0",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
|
|
|
|||
|
|
@ -32,3 +32,4 @@ figment = { version = "0.10", features = ["env"] }
|
|||
json5 = "0.4"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
tui-markdown = "0.3"
|
||||
unicode-width = "0.2.2"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
// Uses ratatui + crossterm. The App struct holds all TUI state and
|
||||
// handles rendering. Input is processed from crossterm key events.
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use crossterm::{
|
||||
event::{EnableMouseCapture, DisableMouseCapture, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind, MouseButton},
|
||||
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
|
|
@ -16,6 +17,7 @@ use crossterm::{
|
|||
};
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span},
|
||||
|
|
@ -816,21 +818,36 @@ impl App {
|
|||
let input_para = Paragraph::new(input_lines).wrap(Wrap { trim: false });
|
||||
frame.render_widget(input_para, input_area);
|
||||
|
||||
// Cursor position: walk through text up to cursor, tracking visual row/col
|
||||
let cursor_text = format!("{}{}", prompt, &self.input[..self.cursor]);
|
||||
let w = input_area.width as usize;
|
||||
let cursor_lines: Vec<&str> = cursor_text.split('\n').collect();
|
||||
let n = cursor_lines.len();
|
||||
let mut visual_row = 0u16;
|
||||
for line in &cursor_lines[..n - 1] {
|
||||
visual_row += wrapped_height(line, w) as u16;
|
||||
// Cursor position: scan the rendered buffer to find where the cursor should be.
|
||||
// This matches ratatui's actual word wrapping instead of trying to simulate it.
|
||||
let buffer = frame.buffer_mut();
|
||||
let mut char_count = 0usize;
|
||||
let mut cursor_x = input_area.x;
|
||||
let mut cursor_y = input_area.y;
|
||||
|
||||
// Walk through the rendered buffer, counting characters until we reach the cursor position
|
||||
for y in input_area.y..input_area.y + input_area.height {
|
||||
for x in input_area.x..input_area.x + input_area.width {
|
||||
if let Some(cell) = buffer.cell((x, y)) {
|
||||
let symbol = cell.symbol();
|
||||
// Count visible characters (skip zero-width and empty)
|
||||
if !symbol.is_empty() {
|
||||
let width = symbol.width();
|
||||
if char_count + width > self.cursor {
|
||||
// Found the cursor position
|
||||
cursor_x = x;
|
||||
cursor_y = y;
|
||||
break;
|
||||
}
|
||||
// Use unicode display width to match ratatui's wrapping
|
||||
let last_width = ratatui::text::Line::raw(cursor_lines[n - 1]).width();
|
||||
let col = if w > 0 { last_width % w } else { last_width };
|
||||
visual_row += if w > 0 { (last_width / w) as u16 } else { 0 };
|
||||
let cursor_x = col as u16 + input_area.x;
|
||||
let cursor_y = visual_row + input_area.y;
|
||||
char_count += width;
|
||||
}
|
||||
}
|
||||
}
|
||||
if cursor_x != input_area.x || cursor_y != input_area.y {
|
||||
break; // Found it
|
||||
}
|
||||
}
|
||||
|
||||
if cursor_y < input_area.y + input_area.height {
|
||||
frame.set_cursor_position((cursor_x, cursor_y));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ poc-agent = { path = "../poc-agent" }
|
|||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
redb = "2"
|
||||
log = "0.4"
|
||||
ratatui = "0.29"
|
||||
ratatui = "0.30"
|
||||
skillratings = "0.28"
|
||||
crossterm = { version = "0.28", features = ["event-stream"] }
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue