diff --git a/crates/owlen-tui/src/ui.rs b/crates/owlen-tui/src/ui.rs index 67abffa..cc9cb6d 100644 --- a/crates/owlen-tui/src/ui.rs +++ b/crates/owlen-tui/src/ui.rs @@ -101,12 +101,32 @@ fn render_editable_textarea( render_lines.push(Line::default()); } - let mut paragraph = Paragraph::new(render_lines).style(base_style); - + // If wrapping is enabled, we need to manually wrap the lines + // For now, we'll convert to plain text, wrap, and lose styling + // This ensures consistency with cursor calculation if wrap_lines { - paragraph = paragraph.wrap(Wrap { trim: false }); + let content_width = inner.width as usize; + let mut wrapped_lines: Vec = Vec::new(); + + for (row_idx, line) in render_lines.iter().enumerate() { + let line_text = line.to_string(); + let segments = wrap_line_segments(&line_text, content_width); + + for (seg_idx, segment) in segments.into_iter().enumerate() { + // For the line with the cursor, preserve the cursor line style + if row_idx == cursor.0 && seg_idx == 0 { + wrapped_lines.push(Line::from(segment).patch_style(cursor_line_style)); + } else { + wrapped_lines.push(Line::from(segment)); + } + } + } + + render_lines = wrapped_lines; } + let mut paragraph = Paragraph::new(render_lines).style(base_style); + let metrics = compute_cursor_metrics(lines_slice, cursor, mask_char, inner, wrap_lines); if let Some(ref metrics) = metrics { @@ -307,15 +327,48 @@ fn wrap_line_segments(line: &str, width: usize) -> Vec { return vec![String::new()]; } - let wrapped = wrap(line, Options::new(width).break_words(false)); - if wrapped.is_empty() { - vec![String::new()] - } else { - wrapped - .into_iter() - .map(|segment| segment.into_owned()) - .collect() + if line.is_empty() { + return vec![String::new()]; } + + // Manual wrapping that preserves all characters including spaces + let mut result = Vec::new(); + let mut current = String::new(); + let mut current_width = 0usize; + + for ch in line.chars() { + let ch_width = UnicodeWidthStr::width(ch.to_string().as_str()); + + // If adding this character would exceed width, wrap to next line + if current_width + ch_width > width { + if !current.is_empty() { + result.push(current); + current = String::new(); + current_width = 0; + } + // If even a single character is too wide, add it anyway to avoid infinite loop + if ch_width > width { + current.push(ch); + result.push(current); + current = String::new(); + current_width = 0; + continue; + } + } + + current.push(ch); + current_width += ch_width; + } + + if !current.is_empty() { + result.push(current); + } + + if result.is_empty() { + result.push(String::new()); + } + + result } fn render_header(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) { @@ -498,6 +551,7 @@ fn render_input(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) { if matches!(app.mode(), InputMode::Editing) { let mut textarea = app.textarea().clone(); textarea.set_block(input_block.clone()); + textarea.set_hard_tab_indent(false); render_editable_textarea(frame, area, &mut textarea, true); } else { // In non-editing mode, show the current input buffer content as read-only