kptui: message entry should scroll horizontally
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user