TUIのratatuiを0.30.0にした
This commit is contained in:
parent
46526ed262
commit
f2aaa3683f
932
Cargo.lock
generated
932
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -6,7 +6,8 @@ license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
protocol = { path = "../protocol" }
|
protocol = { path = "../protocol" }
|
||||||
ratatui = "0.29"
|
ratatui = "0.30.0"
|
||||||
crossterm = "0.28"
|
crossterm = "0.28"
|
||||||
tokio = { version = "1.49", features = ["rt-multi-thread", "macros", "net", "io-util", "sync", "time"] }
|
tokio = { version = "1.49", features = ["rt-multi-thread", "macros", "net", "io-util", "sync", "time"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
tui-scrollview = "0.6.4"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use protocol::{Event, Method};
|
use protocol::{Event, Method};
|
||||||
|
use tui_scrollview::ScrollViewState;
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub pod_name: String,
|
pub pod_name: String,
|
||||||
|
|
@ -13,7 +14,7 @@ pub struct App {
|
||||||
pub current_tool: Option<String>,
|
pub current_tool: Option<String>,
|
||||||
pub input: String,
|
pub input: String,
|
||||||
pub cursor: usize,
|
pub cursor: usize,
|
||||||
pub scroll: u16,
|
pub scroll_state: ScrollViewState,
|
||||||
pub quit: bool,
|
pub quit: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,7 +48,7 @@ impl App {
|
||||||
current_tool: None,
|
current_tool: None,
|
||||||
input: String::new(),
|
input: String::new(),
|
||||||
cursor: 0,
|
cursor: 0,
|
||||||
scroll: 0,
|
scroll_state: ScrollViewState::new(),
|
||||||
quit: false,
|
quit: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -230,24 +231,15 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_up(&mut self) {
|
pub fn scroll_up(&mut self) {
|
||||||
self.scroll = self.scroll.saturating_sub(3);
|
self.scroll_state.scroll_up();
|
||||||
|
self.scroll_state.scroll_up();
|
||||||
|
self.scroll_state.scroll_up();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll_down(&mut self) {
|
pub fn scroll_down(&mut self) {
|
||||||
self.scroll = self.scroll.saturating_add(3);
|
self.scroll_state.scroll_down();
|
||||||
}
|
self.scroll_state.scroll_down();
|
||||||
|
self.scroll_state.scroll_down();
|
||||||
/// Total visible lines (for rendering the in-progress text as part of output).
|
|
||||||
pub fn display_lines(&self) -> Vec<(&MessageKind, &str)> {
|
|
||||||
let mut lines: Vec<(&MessageKind, &str)> = self
|
|
||||||
.messages
|
|
||||||
.iter()
|
|
||||||
.map(|m| (&m.kind, m.content.as_str()))
|
|
||||||
.collect();
|
|
||||||
if !self.current_text.is_empty() {
|
|
||||||
lines.push((&MessageKind::Assistant, &self.current_text));
|
|
||||||
}
|
|
||||||
lines
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_history(&mut self, items: &[serde_json::Value]) {
|
fn restore_history(&mut self, items: &[serde_json::Value]) {
|
||||||
|
|
@ -312,8 +304,7 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn scroll_to_bottom(&mut self) {
|
fn scroll_to_bottom(&mut self) {
|
||||||
// Will be clamped during rendering
|
self.scroll_state.scroll_to_bottom();
|
||||||
self.scroll = u16::MAX;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use ratatui::layout::{Alignment, Constraint, Layout, Position};
|
use ratatui::layout::{Alignment, Constraint, Layout, Position, Rect, Size};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Paragraph, Wrap};
|
use ratatui::widgets::{Block, Padding, Paragraph, Wrap};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
use tui_scrollview::{ScrollView, ScrollbarVisibility};
|
||||||
|
|
||||||
use crate::app::{fmt_tokens, App, MessageKind};
|
use crate::app::{fmt_tokens, App, MessageKind};
|
||||||
|
|
||||||
|
|
@ -21,35 +22,111 @@ pub fn draw(frame: &mut Frame, app: &mut App) {
|
||||||
draw_input(frame, app, chunks[3]);
|
draw_input(frame, app, chunks[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_messages(frame: &mut Frame, app: &mut App, area: ratatui::layout::Rect) {
|
fn draw_messages(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||||
let display = app.display_lines();
|
let width = area.width;
|
||||||
|
let padded_inner = width.saturating_sub(1); // content width inside Block::padding(left=1)
|
||||||
|
|
||||||
let lines: Vec<Line> = display
|
// Build segments: (is_padded, lines, wrapped_height)
|
||||||
.iter()
|
struct Seg<'a> {
|
||||||
.flat_map(|(kind, content)| {
|
lines: Vec<Line<'a>>,
|
||||||
let style = kind_style(kind);
|
padded: bool,
|
||||||
let align = if matches!(kind, MessageKind::TurnStats) {
|
height: u16,
|
||||||
Alignment::Right
|
|
||||||
} else {
|
|
||||||
Alignment::Left
|
|
||||||
};
|
|
||||||
content
|
|
||||||
.lines()
|
|
||||||
.map(move |l| Line::from(Span::styled(l.to_owned(), style)).alignment(align))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let total = lines.len() as u16;
|
|
||||||
let max_scroll = total.saturating_sub(area.height);
|
|
||||||
if app.scroll > max_scroll {
|
|
||||||
app.scroll = max_scroll;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let paragraph = Paragraph::new(lines)
|
let mut segs: Vec<Seg> = Vec::new();
|
||||||
.wrap(Wrap { trim: false })
|
let mut content: Vec<Line> = Vec::new();
|
||||||
.scroll((app.scroll, 0));
|
|
||||||
|
|
||||||
frame.render_widget(paragraph, area);
|
macro_rules! flush_content {
|
||||||
|
() => {
|
||||||
|
if !content.is_empty() {
|
||||||
|
let h = wrapped_height(&content, padded_inner);
|
||||||
|
segs.push(Seg { lines: std::mem::take(&mut content), padded: true, height: h });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for msg in &app.messages {
|
||||||
|
let style = kind_style(&msg.kind);
|
||||||
|
match msg.kind {
|
||||||
|
MessageKind::TurnHeader => {
|
||||||
|
flush_content!();
|
||||||
|
if !segs.is_empty() {
|
||||||
|
segs.push(Seg { lines: vec![Line::raw("")], padded: false, height: 1 });
|
||||||
|
}
|
||||||
|
let lines = vec![Line::from(Span::styled(msg.content.clone(), style))];
|
||||||
|
segs.push(Seg { lines, padded: false, height: 1 });
|
||||||
|
}
|
||||||
|
MessageKind::TurnStats => {
|
||||||
|
flush_content!();
|
||||||
|
let lines: Vec<Line> = msg.content.lines()
|
||||||
|
.map(|l| Line::from(Span::styled(l.to_owned(), style)).alignment(Alignment::Right))
|
||||||
|
.collect();
|
||||||
|
let h = wrapped_height(&lines, padded_inner);
|
||||||
|
segs.push(Seg { lines, padded: true, height: h });
|
||||||
|
segs.push(Seg { lines: vec![Line::raw("")], padded: false, height: 1 });
|
||||||
|
}
|
||||||
|
MessageKind::User => {
|
||||||
|
for l in msg.content.lines() {
|
||||||
|
content.push(Line::from(Span::styled(l.to_owned(), style)));
|
||||||
|
}
|
||||||
|
content.push(Line::raw(""));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
for l in msg.content.lines() {
|
||||||
|
content.push(Line::from(Span::styled(l.to_owned(), style)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-progress streaming text
|
||||||
|
if !app.current_text.is_empty() {
|
||||||
|
let style = kind_style(&MessageKind::Assistant);
|
||||||
|
for l in app.current_text.lines() {
|
||||||
|
content.push(Line::from(Span::styled(l.to_owned(), style)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flush_content!();
|
||||||
|
|
||||||
|
// Total content height
|
||||||
|
let total_height: u16 = segs.iter().map(|s| s.height).sum();
|
||||||
|
|
||||||
|
// Build ScrollView
|
||||||
|
let mut sv = ScrollView::new(Size::new(width, total_height.max(1)))
|
||||||
|
.horizontal_scrollbar_visibility(ScrollbarVisibility::Never);
|
||||||
|
|
||||||
|
let mut y: u16 = 0;
|
||||||
|
for seg in segs {
|
||||||
|
let rect = Rect::new(0, y, width, seg.height);
|
||||||
|
if seg.padded {
|
||||||
|
sv.render_widget(
|
||||||
|
Paragraph::new(seg.lines)
|
||||||
|
.block(Block::default().padding(Padding::left(1)))
|
||||||
|
.wrap(Wrap { trim: false }),
|
||||||
|
rect,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sv.render_widget(Paragraph::new(seg.lines), rect);
|
||||||
|
}
|
||||||
|
y += seg.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.render_stateful_widget(sv, area, &mut app.scroll_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estimate the number of visual rows after wrapping.
|
||||||
|
fn wrapped_height(lines: &[Line], avail_width: u16) -> u16 {
|
||||||
|
if avail_width == 0 {
|
||||||
|
return lines.len() as u16;
|
||||||
|
}
|
||||||
|
lines
|
||||||
|
.iter()
|
||||||
|
.map(|line| {
|
||||||
|
let w = line.width() as u16;
|
||||||
|
if w == 0 { 1 } else { w.div_ceil(avail_width) }
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_separator(frame: &mut Frame, area: ratatui::layout::Rect) {
|
fn draw_separator(frame: &mut Frame, area: ratatui::layout::Rect) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user