codetether_agent/tui/ui/chat_view/cursor.rs
1//! Input-area cursor placement.
2//!
3//! [`place_cursor`] positions the terminal cursor inside the input rectangle
4//! accounting for multi-line text and Unicode wide characters.
5
6use ratatui::{Frame, layout::Rect};
7use unicode_width::UnicodeWidthStr;
8
9use crate::tui::app::state::App;
10
11/// Set the terminal cursor inside the input `area`.
12///
13/// Accounts for newlines (row offset) and wide characters (column offset)
14/// so the cursor visually lands at [`AppState::input_cursor`].
15///
16/// # Examples
17///
18/// ```rust,no_run
19/// // Requires a live terminal backend; cannot run in doc tests.
20/// use codetether_agent::tui::ui::chat_view::cursor::place_cursor;
21/// # fn demo(f: &mut ratatui::Frame, app: &codetether_agent::tui::app::state::App) {
22/// let area = ratatui::layout::Rect::new(0, 0, 40, 3);
23/// place_cursor(f, app, area);
24/// # }
25/// ```
26pub fn place_cursor(f: &mut Frame, app: &App, area: Rect) {
27 let text = &app.state.input;
28 let cursor_byte = text
29 .char_indices()
30 .nth(app.state.input_cursor)
31 .map(|(i, _)| i)
32 .unwrap_or(text.len());
33 let before = &text[..cursor_byte];
34 let row = before.matches('\n').count();
35 let last_nl = before.rfind('\n').map(|i| i + 1).unwrap_or(0);
36 let col = before[last_nl..].width();
37 let x = area
38 .x
39 .saturating_add(1)
40 .saturating_add(col as u16)
41 .min(area.x.saturating_add(area.width.saturating_sub(2)));
42 let y = area
43 .y
44 .saturating_add(1)
45 .saturating_add(row as u16)
46 .min(area.y.saturating_add(area.height.saturating_sub(2)));
47 f.set_cursor_position((x, y));
48}