Private
Public Access
1
0

kptui: message entry should scroll horizontally

This commit is contained in:
2026-02-11 14:43:04 -08:00
parent 6ccef24512
commit 9a3c808095

View File

@@ -113,13 +113,22 @@ fn view_mode(width: u16, has_active_conversation: bool, requested: ViewMode) ->
fn ui(frame: &mut Frame, app: &AppState, requested_view: ViewMode) {
let area = frame.area();
let mode = view_mode(area.width, app.active_conversation_id.is_some(), requested_view);
let mode = view_mode(
area.width,
app.active_conversation_id.is_some(),
requested_view,
);
let show_input = matches!(mode, ViewMode::Chat | ViewMode::Split) && app.active_conversation_id.is_some();
let show_input =
matches!(mode, ViewMode::Chat | ViewMode::Split) && app.active_conversation_id.is_some();
let chunks = if show_input {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Length(3), Constraint::Length(1)])
.constraints([
Constraint::Min(1),
Constraint::Length(3),
Constraint::Length(1),
])
.split(area)
} else {
Layout::default()
@@ -149,12 +158,13 @@ fn ui(frame: &mut Frame, app: &AppState, requested_view: ViewMode) {
}
if let Some(input_area) = input_area {
render_input(frame, app, input_area);
let input_scroll_x = render_input(frame, app, input_area);
if app.focus == Focus::Input {
let cursor_col = visual_width_u16(app.input.as_str());
let mut x = input_area
.x
.saturating_add(1)
.saturating_add(app.input.len() as u16);
.saturating_add(cursor_col.saturating_sub(input_scroll_x));
let max_x = input_area
.x
.saturating_add(input_area.width.saturating_sub(2));
@@ -184,7 +194,10 @@ fn render_conversations(frame: &mut Frame, app: &AppState, area: Rect, in_split:
String::new()
};
let header = Line::from(vec![
Span::styled(c.title.clone(), Style::default().add_modifier(Modifier::BOLD)),
Span::styled(
c.title.clone(),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(unread),
]);
let preview = Line::from(Span::styled(
@@ -234,7 +247,10 @@ fn render_transcript(frame: &mut Frame, app: &AppState, area: Rect, in_split: bo
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string());
lines.push(Line::from(vec![
Span::styled(message.sender.clone(), Style::default().add_modifier(Modifier::BOLD)),
Span::styled(
message.sender.clone(),
Style::default().add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(ts, Style::default().fg(Color::DarkGray)),
]));
@@ -268,23 +284,27 @@ fn render_transcript(frame: &mut Frame, app: &AppState, area: Rect, in_split: bo
frame.render_widget(paragraph, area);
}
fn render_input(frame: &mut Frame, app: &AppState, area: Rect) {
fn render_input(frame: &mut Frame, app: &AppState, area: Rect) -> u16 {
let title = if app.focus == Focus::Input {
"Reply (Enter to send)"
} else {
"Reply (press i to type)"
};
let inner_width = area.width.saturating_sub(2).max(1);
let cursor_col = visual_width_u16(app.input.as_str());
let input_scroll_x = cursor_col.saturating_sub(inner_width.saturating_sub(1));
let input = Paragraph::new(app.input.as_str())
.block(Block::default().borders(Borders::ALL).title(title));
.block(Block::default().borders(Borders::ALL).title(title))
.scroll((0, input_scroll_x));
frame.render_widget(input, area);
input_scroll_x
}
fn render_status(frame: &mut Frame, app: &AppState, area: Rect, mode: ViewMode) {
let mut parts = vec![
format!(
"{} convs",
app.conversations.len()
),
format!("{} convs", app.conversations.len()),
match mode {
ViewMode::Split => "split".to_string(),
ViewMode::List => "list".to_string(),
@@ -295,7 +315,10 @@ fn render_status(frame: &mut Frame, app: &AppState, area: Rect, mode: ViewMode)
parts.push(app.status.clone());
}
let line = parts.join(" | ");
frame.render_widget(Paragraph::new(line).block(Block::default().borders(Borders::TOP)), area);
frame.render_widget(
Paragraph::new(line).block(Block::default().borders(Borders::TOP)),
area,
);
}
fn main() -> Result<()> {
@@ -322,7 +345,9 @@ fn main() -> Result<()> {
res
}
fn run_app(terminal: &mut ratatui::Terminal<ratatui::backend::CrosstermBackend<std::io::Stdout>>) -> Result<()> {
fn run_app(
terminal: &mut ratatui::Terminal<ratatui::backend::CrosstermBackend<std::io::Stdout>>,
) -> Result<()> {
let (request_tx, request_rx) = mpsc::channel::<daemon::Request>();
let (event_tx, event_rx) = mpsc::channel::<daemon::Event>();
let _worker = daemon::spawn_worker(request_rx, event_tx);
@@ -361,14 +386,12 @@ fn run_app(terminal: &mut ratatui::Terminal<ratatui::backend::CrosstermBackend<s
app.selected_conversation_id = Some(id);
} else {
app.selected_idx = 0;
app.selected_conversation_id =
Some(app.conversations[0].id.clone());
app.selected_conversation_id = Some(app.conversations[0].id.clone());
}
} else {
app.selected_idx = app.selected_idx.min(app.conversations.len() - 1);
app.selected_conversation_id = Some(
app.conversations[app.selected_idx].id.clone(),
);
app.selected_conversation_id =
Some(app.conversations[app.selected_idx].id.clone());
}
}
daemon::Event::Messages {
@@ -428,9 +451,7 @@ fn run_app(terminal: &mut ratatui::Terminal<ratatui::backend::CrosstermBackend<s
if app.active_conversation_id.as_deref() == Some(conversation_id.as_str()) {
if !app.refresh_messages_in_flight {
request_tx
.send(daemon::Request::RefreshMessages {
conversation_id,
})
.send(daemon::Request::RefreshMessages { conversation_id })
.ok();
app.refresh_messages_in_flight = true;
}
@@ -493,9 +514,21 @@ fn run_app(terminal: &mut ratatui::Terminal<ratatui::backend::CrosstermBackend<s
if app.active_conversation_id.is_some() {
requested_view = ViewMode::Chat;
if let Some(cid) = app.active_conversation_id.clone() {
request_tx.send(daemon::Request::MarkRead { conversation_id: cid.clone() }).ok();
request_tx.send(daemon::Request::SyncConversation { conversation_id: cid.clone() }).ok();
request_tx.send(daemon::Request::RefreshMessages { conversation_id: cid }).ok();
request_tx
.send(daemon::Request::MarkRead {
conversation_id: cid.clone(),
})
.ok();
request_tx
.send(daemon::Request::SyncConversation {
conversation_id: cid.clone(),
})
.ok();
request_tx
.send(daemon::Request::RefreshMessages {
conversation_id: cid,
})
.ok();
app.refresh_messages_in_flight = true;
}
}
@@ -540,9 +573,21 @@ fn run_app(terminal: &mut ratatui::Terminal<ratatui::backend::CrosstermBackend<s
app.open_selected_conversation();
requested_view = ViewMode::Chat;
if let Some(cid) = app.active_conversation_id.clone() {
request_tx.send(daemon::Request::MarkRead { conversation_id: cid.clone() }).ok();
request_tx.send(daemon::Request::SyncConversation { conversation_id: cid.clone() }).ok();
request_tx.send(daemon::Request::RefreshMessages { conversation_id: cid }).ok();
request_tx
.send(daemon::Request::MarkRead {
conversation_id: cid.clone(),
})
.ok();
request_tx
.send(daemon::Request::SyncConversation {
conversation_id: cid.clone(),
})
.ok();
request_tx
.send(daemon::Request::RefreshMessages {
conversation_id: cid,
})
.ok();
app.refresh_messages_in_flight = true;
}
} else {
@@ -644,7 +689,11 @@ fn scroll_down(app: &mut AppState, amount: u16, max_scroll: u16) {
}
fn transcript_inner_width(size: Size, app: &AppState, requested_view: ViewMode) -> u16 {
let mode = view_mode(size.width, app.active_conversation_id.is_some(), requested_view);
let mode = view_mode(
size.width,
app.active_conversation_id.is_some(),
requested_view,
);
let outer_width = match mode {
ViewMode::Split => {
let left_width = (size.width / 3).clamp(24, 40);
@@ -666,10 +715,11 @@ fn visual_line_count(s: &str, inner_width: u16) -> u16 {
((w + iw - 1) / iw).min(u16::MAX as usize) as u16
}
fn transcript_content_visual_lines(
messages: &[daemon::ChatMessage],
inner_width: u16,
) -> u16 {
fn visual_width_u16(s: &str) -> u16 {
s.width().min(u16::MAX as usize) as u16
}
fn transcript_content_visual_lines(messages: &[daemon::ChatMessage], inner_width: u16) -> u16 {
if messages.is_empty() {
return 1;
}
@@ -699,7 +749,11 @@ fn transcript_content_visual_lines(
}
fn transcript_viewport_height(size: Size, app: &AppState, requested_view: ViewMode) -> u16 {
let mode = view_mode(size.width, app.active_conversation_id.is_some(), requested_view);
let mode = view_mode(
size.width,
app.active_conversation_id.is_some(),
requested_view,
);
let show_input =
matches!(mode, ViewMode::Chat | ViewMode::Split) && app.active_conversation_id.is_some();