mirror of
https://github.com/logos-messaging/libchat.git
synced 2026-08-26 07:31:09 +00:00
feat(chat-cli): wire up logos-delivery transport and switch to client API (#91)
Replace the direct use of `conversations::Context` with `client::ChatClient`, which is the intended public API for library consumers. Remove `MessageEnvelope` and the username-keyed session model. The envelope was never part of the wire protocol — sender identity was only tracked in the CLI's local state. Chats are now keyed by conversation ID; add `/nickname` as the user-facing replacement for named sessions. Add a logos-delivery (Waku) transport alongside the existing file transport. The active transport is selected at compile time: set `LOGOS_DELIVERY_LIB_DIR` to link liblogosdelivery, otherwise the file transport is used. Add logos-delivery as a Nix flake input and expose `.#logos-delivery` so the library can be built with `nix build` and referenced by `LOGOS_DELIVERY_LIB_DIR`. CI: rename `c-ffi-smoketest` to `smoketest`; add logos-delivery build step and a `--smoketest` invocation of chat-cli to verify startup.
This commit is contained in:
+35
-24
@@ -16,6 +16,8 @@ use ratatui::{
|
||||
widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
|
||||
};
|
||||
|
||||
use client::DeliveryService;
|
||||
|
||||
use crate::app::ChatApp;
|
||||
|
||||
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
|
||||
@@ -36,7 +38,7 @@ pub fn restore() -> io::Result<()> {
|
||||
}
|
||||
|
||||
/// Draw the UI.
|
||||
pub fn draw(frame: &mut Frame, app: &ChatApp) {
|
||||
pub fn draw<D: DeliveryService>(frame: &mut Frame, app: &ChatApp<D>) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
@@ -53,13 +55,16 @@ pub fn draw(frame: &mut Frame, app: &ChatApp) {
|
||||
draw_status(frame, app, chunks[3]);
|
||||
}
|
||||
|
||||
fn draw_header(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
||||
fn draw_header<D: DeliveryService>(frame: &mut Frame, app: &ChatApp<D>, area: Rect) {
|
||||
let title = match app.current_session() {
|
||||
Some(session) => format!(" 💬 Chat: {} ↔ {} ", app.user_name, session.remote_user),
|
||||
None => format!(
|
||||
" 💬 {} (no active chat - use /connect or /chats) ",
|
||||
app.user_name
|
||||
),
|
||||
Some(session) => {
|
||||
let id = &session.chat_id[..8.min(session.chat_id.len())];
|
||||
match &session.nickname {
|
||||
Some(name) => format!(" 💬 Chat: {} ↔ {name} ({id}) ", app.user_name),
|
||||
None => format!(" 💬 Chat: {} ↔ ({id}) ", app.user_name),
|
||||
}
|
||||
}
|
||||
None => format!(" 💬 {} — no active chat ", app.user_name),
|
||||
};
|
||||
|
||||
let header = Paragraph::new(title)
|
||||
@@ -73,10 +78,10 @@ fn draw_header(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
||||
frame.render_widget(header, area);
|
||||
}
|
||||
|
||||
fn draw_messages(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
||||
fn draw_messages<D: DeliveryService>(frame: &mut Frame, app: &ChatApp<D>, area: Rect) {
|
||||
let remote_name = app
|
||||
.current_session()
|
||||
.map(|s| s.remote_user.as_str())
|
||||
.map(|s| s.display_name())
|
||||
.unwrap_or("Them");
|
||||
|
||||
// Inner width: area minus borders (2) for wrapping long content.
|
||||
@@ -108,7 +113,7 @@ fn draw_messages(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
||||
let first_line_width = inner_width.saturating_sub(prefix_len).max(1);
|
||||
|
||||
// First line includes the prefix.
|
||||
let (first_chunk, rest) = if content.len() <= first_line_width {
|
||||
let (first_chunk, rest): (&str, &str) = if content.len() <= first_line_width {
|
||||
(content.as_str(), "")
|
||||
} else {
|
||||
content.split_at(first_line_width)
|
||||
@@ -121,7 +126,7 @@ fn draw_messages(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
||||
|
||||
// Continuation lines are indented to align with content.
|
||||
let indent = " ".repeat(prefix_len);
|
||||
let mut remaining = rest;
|
||||
let mut remaining: &str = rest;
|
||||
while !remaining.is_empty() {
|
||||
let chunk_width = inner_width.saturating_sub(prefix_len).max(1);
|
||||
let (chunk, tail) = if remaining.len() <= chunk_width {
|
||||
@@ -141,17 +146,25 @@ fn draw_messages(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
||||
.collect();
|
||||
|
||||
let title = match app.current_session() {
|
||||
Some(session) => format!(" Messages with {} ", session.remote_user),
|
||||
None => " Messages ".to_string(),
|
||||
Some(s) => match &s.nickname {
|
||||
Some(name) => format!(" Messages with {name} "),
|
||||
None => format!(" Messages ({}) ", &s.chat_id[..8.min(s.chat_id.len())]),
|
||||
},
|
||||
None => " Command output ".to_string(),
|
||||
};
|
||||
|
||||
let item_count = messages.len();
|
||||
let messages_widget =
|
||||
List::new(messages).block(Block::default().title(title).borders(Borders::ALL));
|
||||
|
||||
frame.render_widget(messages_widget, area);
|
||||
// Scroll so the last line is always visible (area height minus two borders).
|
||||
let visible = area.height.saturating_sub(2) as usize;
|
||||
let offset = item_count.saturating_sub(visible);
|
||||
let mut list_state = ratatui::widgets::ListState::default().with_offset(offset);
|
||||
frame.render_stateful_widget(messages_widget, area, &mut list_state);
|
||||
}
|
||||
|
||||
fn draw_input(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
||||
fn draw_input<D: DeliveryService>(frame: &mut Frame, app: &ChatApp<D>, area: Rect) {
|
||||
// Inner width: area minus borders (2).
|
||||
let inner_width = area.width.saturating_sub(2) as usize;
|
||||
let input_len = app.input.len();
|
||||
@@ -165,13 +178,11 @@ fn draw_input(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
||||
|
||||
let visible_input = &app.input[scroll_offset..];
|
||||
|
||||
let input = Paragraph::new(visible_input)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.block(
|
||||
Block::default()
|
||||
.title(" Input (Enter to send) ")
|
||||
.borders(Borders::ALL),
|
||||
);
|
||||
let input = Paragraph::new(visible_input).style(Style::default()).block(
|
||||
Block::default()
|
||||
.title(" Input (Enter to send) ")
|
||||
.borders(Borders::ALL),
|
||||
);
|
||||
|
||||
frame.render_widget(input, area);
|
||||
|
||||
@@ -180,7 +191,7 @@ fn draw_input(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
||||
frame.set_cursor_position((cursor_x, area.y + 1));
|
||||
}
|
||||
|
||||
fn draw_status(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
||||
fn draw_status<D: DeliveryService>(frame: &mut Frame, app: &ChatApp<D>, area: Rect) {
|
||||
let status = Paragraph::new(app.status.as_str())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(Block::default().title(" Status ").borders(Borders::ALL))
|
||||
@@ -190,7 +201,7 @@ fn draw_status(frame: &mut Frame, app: &ChatApp, area: Rect) {
|
||||
}
|
||||
|
||||
/// Handle keyboard events.
|
||||
pub fn handle_events(app: &mut ChatApp) -> io::Result<bool> {
|
||||
pub fn handle_events<D: DeliveryService>(app: &mut ChatApp<D>) -> io::Result<bool> {
|
||||
// Poll for events with a short timeout to allow checking incoming messages
|
||||
if event::poll(std::time::Duration::from_millis(100))?
|
||||
&& let Event::Key(key) = event::read()?
|
||||
|
||||
Reference in New Issue
Block a user