Fix composer input cursor so trailing spaces are visible.

c06badb472e3 · AtlantisPleb · · parent 1fb228a72d9f

Fix composer input cursor so trailing spaces are visible.

wrap_input now wraps by character, preserving trailing spaces. The hardware cursor is placed after the real last character and Modifier::REVERSED is applied to the cursor cell, matching grok-build's textarea cursor approach.

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified crates/coder-lite/src/tui.rs

Diff

1 file changed, +23 -19

crates/coder-lite/src/tui.rs modified +23 -19

@@ -3,10 +3,11 @@

3 3
use ratatui::{
4 4
    Frame,
5 5
    layout::{Constraint, Direction, Layout, Position, Rect},
6
    style::Style,
6
    style::{Modifier, Style},
7 7
    text::{Line, Span, Text},
8 8
    widgets::{Block, Borders, Paragraph},
9 9
};
10
use unicode_width::UnicodeWidthStr;
10 11
use serde_json::Value;
11 12
use std::time::{SystemTime, UNIX_EPOCH};
12 13

@@ -194,30 +195,25 @@ fn wrap_input(text: &str, width: usize) -> Vec<String> {

194 195
    let mut lines = Vec::new();
195 196
196 197
    for paragraph in text.split('\n') {
197
        if paragraph.is_empty() {
198
            lines.push(String::new());
199
            continue;
200
        }
201
202 198
        let mut current = String::new();
203 199
        let mut current_width = 0;
204
        for word in paragraph.split_whitespace() {
205
            let word_width = word.chars().count();
206
            if current.is_empty() {
207
                current.push_str(word);
208
                current_width = word_width;
209
            } else if current_width + 1 + word_width <= width {
210
                current.push(' ');
211
                current.push_str(word);
212
                current_width += 1 + word_width;
200
        for c in paragraph.chars() {
201
            let w = unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
202
            if current_width + w <= width {
203
                current.push(c);
204
                current_width += w;
213 205
            } else {
214 206
                lines.push(current);
215
                current = word.to_string();
216
                current_width = word_width;
207
                current = c.to_string();
208
                current_width = w;
217 209
            }
218 210
        }
219 211
220
        if !current.is_empty() {
212
        if current.is_empty() {
213
            if lines.is_empty() {
214
                lines.push(String::new());
215
            }
216
        } else {
221 217
            lines.push(current);
222 218
        }
223 219
    }

@@ -328,9 +324,17 @@ impl CoderUi {

328 324
        frame.render_widget(input, input_area);
329 325
330 326
        let last_chunk = input_chunks.last().map(|s| s.as_str()).unwrap_or("");
331
        let cursor_x = input_area.x + 1 + 3 + last_chunk.chars().count() as u16;
327
        let last_prefix_width: u16 = 3;
328
        let cursor_x = input_area.x
329
            + 1
330
            + last_prefix_width
331
            + last_chunk.width().min(input_width as usize) as u16;
332 332
        let cursor_y = input_area.y + 1 + visible_input_lines.saturating_sub(1);
333 333
        frame.set_cursor_position(Position::new(cursor_x, cursor_y));
334
        let buf = frame.buffer_mut();
335
        if let Some(cell) = buf.cell_mut((cursor_x, cursor_y)) {
336
            cell.modifier.insert(Modifier::REVERSED);
337
        }
334 338
    }
335 339
336 340
    /// Calculate the scroll offset that keeps the viewport at the bottom

This page updates live while a promote is in flight · changelog