update composer + user message styling (#4240)
Changes: - the composer and user messages now have a colored background that stretches the entire width of the terminal. - the prompt character was changed from a cyan `▌` to a bold `›`. - the "working" shimmer now follows the "dark gray" color of the terminal, better matching the terminal's color scheme | Terminal + Background | Screenshot | |------------------------------|------------| | iTerm with dark bg | <img width="810" height="641" alt="Screenshot 2025-09-25 at 11 44 52 AM" src="https://github.com/user-attachments/assets/1317e579-64a9-4785-93e6-98b0258f5d92" /> | | iTerm with light bg | <img width="845" height="540" alt="Screenshot 2025-09-25 at 11 46 29 AM" src="https://github.com/user-attachments/assets/e671d490-c747-4460-af0b-3f8d7f7a6b8e" /> | | iTerm with color bg | <img width="825" height="564" alt="Screenshot 2025-09-25 at 11 47 12 AM" src="https://github.com/user-attachments/assets/141cda1b-1164-41d5-87da-3be11e6a3063" /> | | Terminal.app with dark bg | <img width="577" height="367" alt="Screenshot 2025-09-25 at 11 45 22 AM" src="https://github.com/user-attachments/assets/93fc4781-99f7-4ee7-9c8e-3db3cd854fe5" /> | | Terminal.app with light bg | <img width="577" height="367" alt="Screenshot 2025-09-25 at 11 46 04 AM" src="https://github.com/user-attachments/assets/19bf6a3c-91e0-447b-9667-b8033f512219" /> | | Terminal.app with color bg | <img width="577" height="367" alt="Screenshot 2025-09-25 at 11 45 50 AM" src="https://github.com/user-attachments/assets/dd7c4b5b-342e-4028-8140-f4e65752bd0b" /> |
This commit is contained in:
@@ -2,7 +2,6 @@ use std::fmt;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
|
||||
use crate::tui;
|
||||
use crate::wrapping::word_wrap_lines_borrowed;
|
||||
use crossterm::Command;
|
||||
use crossterm::cursor::MoveTo;
|
||||
@@ -14,7 +13,10 @@ use crossterm::style::SetAttribute;
|
||||
use crossterm::style::SetBackgroundColor;
|
||||
use crossterm::style::SetColors;
|
||||
use crossterm::style::SetForegroundColor;
|
||||
use crossterm::terminal::Clear;
|
||||
use crossterm::terminal::ClearType;
|
||||
use ratatui::layout::Size;
|
||||
use ratatui::prelude::Backend;
|
||||
use ratatui::style::Color;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::text::Line;
|
||||
@@ -22,24 +24,16 @@ use ratatui::text::Span;
|
||||
|
||||
/// Insert `lines` above the viewport using the terminal's backend writer
|
||||
/// (avoids direct stdout references).
|
||||
pub(crate) fn insert_history_lines(terminal: &mut tui::Terminal, lines: Vec<Line>) {
|
||||
let mut out = std::io::stdout();
|
||||
insert_history_lines_to_writer(terminal, &mut out, lines);
|
||||
}
|
||||
|
||||
/// Like `insert_history_lines`, but writes ANSI to the provided writer. This
|
||||
/// is intended for testing where a capture buffer is used instead of stdout.
|
||||
pub fn insert_history_lines_to_writer<B, W>(
|
||||
terminal: &mut crate::custom_terminal::Terminal<B>,
|
||||
writer: &mut W,
|
||||
lines: Vec<Line>,
|
||||
) where
|
||||
B: ratatui::backend::Backend,
|
||||
W: Write,
|
||||
pub fn insert_history_lines<B>(terminal: &mut crate::custom_terminal::Terminal<B>, lines: Vec<Line>)
|
||||
where
|
||||
B: Backend + Write,
|
||||
{
|
||||
let screen_size = terminal.backend().size().unwrap_or(Size::new(0, 0));
|
||||
|
||||
let mut area = terminal.viewport_area;
|
||||
let mut should_update_area = false;
|
||||
let last_cursor_pos = terminal.last_known_cursor_pos;
|
||||
let writer = terminal.backend_mut();
|
||||
|
||||
// Pre-wrap lines using word-aware wrapping so terminal scrollback sees the same
|
||||
// formatting as the TUI. This avoids character-level hard wrapping by the terminal.
|
||||
@@ -67,7 +61,7 @@ pub fn insert_history_lines_to_writer<B, W>(
|
||||
|
||||
let cursor_top = area.top().saturating_sub(1);
|
||||
area.y += scroll_amount;
|
||||
terminal.set_viewport_area(area);
|
||||
should_update_area = true;
|
||||
cursor_top
|
||||
} else {
|
||||
area.top().saturating_sub(1)
|
||||
@@ -97,6 +91,21 @@ pub fn insert_history_lines_to_writer<B, W>(
|
||||
|
||||
for line in wrapped {
|
||||
queue!(writer, Print("\r\n")).ok();
|
||||
queue!(
|
||||
writer,
|
||||
SetColors(Colors::new(
|
||||
line.style
|
||||
.fg
|
||||
.map(std::convert::Into::into)
|
||||
.unwrap_or(CColor::Reset),
|
||||
line.style
|
||||
.bg
|
||||
.map(std::convert::Into::into)
|
||||
.unwrap_or(CColor::Reset)
|
||||
))
|
||||
)
|
||||
.ok();
|
||||
queue!(writer, Clear(ClearType::UntilNewLine)).ok();
|
||||
// Merge line-level style into each span so that ANSI colors reflect
|
||||
// line styles (e.g., blockquotes with green fg).
|
||||
let merged_spans: Vec<Span> = line
|
||||
@@ -113,14 +122,12 @@ pub fn insert_history_lines_to_writer<B, W>(
|
||||
queue!(writer, ResetScrollRegion).ok();
|
||||
|
||||
// Restore the cursor position to where it was before we started.
|
||||
queue!(
|
||||
writer,
|
||||
MoveTo(
|
||||
terminal.last_known_cursor_pos.x,
|
||||
terminal.last_known_cursor_pos.y
|
||||
)
|
||||
)
|
||||
.ok();
|
||||
queue!(writer, MoveTo(last_cursor_pos.x, last_cursor_pos.y)).ok();
|
||||
|
||||
let _ = writer;
|
||||
if should_update_area {
|
||||
terminal.set_viewport_area(area);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -275,9 +282,9 @@ where
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::markdown_render::render_markdown_text;
|
||||
use crate::test_backend::VT100Backend;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Color;
|
||||
use vt100::Parser;
|
||||
|
||||
#[test]
|
||||
fn writes_bold_then_regular_spans() {
|
||||
@@ -312,7 +319,7 @@ mod tests {
|
||||
// Set up a small off-screen terminal
|
||||
let width: u16 = 40;
|
||||
let height: u16 = 10;
|
||||
let backend = ratatui::backend::TestBackend::new(width, height);
|
||||
let backend = VT100Backend::new(width, height);
|
||||
let mut term = crate::custom_terminal::Terminal::with_options(backend).expect("terminal");
|
||||
// Place viewport on the last line so history inserts scroll upward
|
||||
let viewport = Rect::new(0, height - 1, width, 1);
|
||||
@@ -321,17 +328,12 @@ mod tests {
|
||||
// Build a blockquote-like line: apply line-level green style and prefix "> "
|
||||
let mut line: Line<'static> = Line::from(vec!["> ".into(), "Hello world".into()]);
|
||||
line = line.style(Color::Green);
|
||||
let mut ansi: Vec<u8> = Vec::new();
|
||||
insert_history_lines_to_writer(&mut term, &mut ansi, vec![line]);
|
||||
|
||||
// Parse ANSI using vt100 and assert at least one non-default fg color appears
|
||||
let mut parser = Parser::new(height, width, 0);
|
||||
parser.process(&ansi);
|
||||
insert_history_lines(&mut term, vec![line]);
|
||||
|
||||
let mut saw_colored = false;
|
||||
'outer: for row in 0..height {
|
||||
for col in 0..width {
|
||||
if let Some(cell) = parser.screen().cell(row, col)
|
||||
if let Some(cell) = term.backend().vt100().screen().cell(row, col)
|
||||
&& cell.has_contents()
|
||||
&& cell.fgcolor() != vt100::Color::Default
|
||||
{
|
||||
@@ -351,7 +353,7 @@ mod tests {
|
||||
// Force wrapping by using a narrow viewport width and a long blockquote line.
|
||||
let width: u16 = 20;
|
||||
let height: u16 = 8;
|
||||
let backend = ratatui::backend::TestBackend::new(width, height);
|
||||
let backend = VT100Backend::new(width, height);
|
||||
let mut term = crate::custom_terminal::Terminal::with_options(backend).expect("terminal");
|
||||
// Viewport is the last line so history goes directly above it.
|
||||
let viewport = Rect::new(0, height - 1, width, 1);
|
||||
@@ -364,13 +366,10 @@ mod tests {
|
||||
]);
|
||||
line = line.style(Color::Green);
|
||||
|
||||
let mut ansi: Vec<u8> = Vec::new();
|
||||
insert_history_lines_to_writer(&mut term, &mut ansi, vec![line]);
|
||||
insert_history_lines(&mut term, vec![line]);
|
||||
|
||||
// Parse and inspect the final screen buffer.
|
||||
let mut parser = Parser::new(height, width, 0);
|
||||
parser.process(&ansi);
|
||||
let screen = parser.screen();
|
||||
let screen = term.backend().vt100().screen();
|
||||
|
||||
// Collect rows that are non-empty; these should correspond to our wrapped lines.
|
||||
let mut non_empty_rows: Vec<u16> = Vec::new();
|
||||
@@ -418,7 +417,7 @@ mod tests {
|
||||
fn vt100_colored_prefix_then_plain_text_resets_color() {
|
||||
let width: u16 = 40;
|
||||
let height: u16 = 6;
|
||||
let backend = ratatui::backend::TestBackend::new(width, height);
|
||||
let backend = VT100Backend::new(width, height);
|
||||
let mut term = crate::custom_terminal::Terminal::with_options(backend).expect("terminal");
|
||||
let viewport = Rect::new(0, height - 1, width, 1);
|
||||
term.set_viewport_area(viewport);
|
||||
@@ -429,12 +428,9 @@ mod tests {
|
||||
Span::raw("Hello world"),
|
||||
]);
|
||||
|
||||
let mut ansi: Vec<u8> = Vec::new();
|
||||
insert_history_lines_to_writer(&mut term, &mut ansi, vec![line]);
|
||||
insert_history_lines(&mut term, vec![line]);
|
||||
|
||||
let mut parser = Parser::new(height, width, 0);
|
||||
parser.process(&ansi);
|
||||
let screen = parser.screen();
|
||||
let screen = term.backend().vt100().screen();
|
||||
|
||||
// Find the first non-empty row; verify first three cells are colored, following cells default.
|
||||
'rows: for row in 0..height {
|
||||
@@ -483,35 +479,17 @@ mod tests {
|
||||
|
||||
let width: u16 = 60;
|
||||
let height: u16 = 12;
|
||||
let backend = ratatui::backend::TestBackend::new(width, height);
|
||||
let backend = VT100Backend::new(width, height);
|
||||
let mut term = crate::custom_terminal::Terminal::with_options(backend).expect("terminal");
|
||||
let viewport = ratatui::layout::Rect::new(0, height - 1, width, 1);
|
||||
term.set_viewport_area(viewport);
|
||||
|
||||
let mut ansi: Vec<u8> = Vec::new();
|
||||
insert_history_lines_to_writer(&mut term, &mut ansi, lines);
|
||||
insert_history_lines(&mut term, lines);
|
||||
|
||||
let mut parser = Parser::new(height, width, 0);
|
||||
parser.process(&ansi);
|
||||
let screen = parser.screen();
|
||||
let screen = term.backend().vt100().screen();
|
||||
|
||||
// Reconstruct screen rows as strings to locate the 3rd level line.
|
||||
let mut rows: Vec<String> = Vec::with_capacity(height as usize);
|
||||
for row in 0..height {
|
||||
let mut s = String::with_capacity(width as usize);
|
||||
for col in 0..width {
|
||||
if let Some(cell) = screen.cell(row, col) {
|
||||
if let Some(ch) = cell.contents().chars().next() {
|
||||
s.push(ch);
|
||||
} else {
|
||||
s.push(' ');
|
||||
}
|
||||
} else {
|
||||
s.push(' ');
|
||||
}
|
||||
}
|
||||
rows.push(s.trim_end().to_string());
|
||||
}
|
||||
let rows: Vec<String> = screen.rows(0, width).collect();
|
||||
|
||||
let needle = "1. Third level (ordered)";
|
||||
let row_idx = rows
|
||||
|
||||
Reference in New Issue
Block a user