148 lines
5.6 KiB
Rust
148 lines
5.6 KiB
Rust
use ratatui::{
|
|
layout::{Constraint, Direction, Layout},
|
|
style::{Color, Modifier, Style},
|
|
text::{Line, Span},
|
|
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
|
|
Frame,
|
|
};
|
|
|
|
use crate::tui::app::App;
|
|
|
|
pub fn draw(frame: &mut Frame, app: &App) {
|
|
let area = frame.size();
|
|
|
|
// Split into sidebar | main column
|
|
let columns = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints([Constraint::Length(22), Constraint::Min(0)])
|
|
.split(area);
|
|
|
|
draw_sidebar(frame, app, columns[0]);
|
|
draw_main(frame, app, columns[1]);
|
|
}
|
|
|
|
fn draw_sidebar(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
|
let rows = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([Constraint::Min(0), Constraint::Length(6)])
|
|
.split(area);
|
|
|
|
// ── Channel list ────────────────────────────────────────────────────
|
|
let items: Vec<ListItem> = app
|
|
.channels
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, (_, name))| {
|
|
let style = if i == app.selected_channel {
|
|
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
|
} else {
|
|
Style::default()
|
|
};
|
|
ListItem::new(format!("# {}", name)).style(style)
|
|
})
|
|
.collect();
|
|
|
|
let mut state = ListState::default();
|
|
state.select(Some(app.selected_channel));
|
|
|
|
let channels = List::new(items)
|
|
.block(
|
|
Block::default()
|
|
.title(" Channels ")
|
|
.borders(Borders::ALL)
|
|
.style(Style::default().fg(Color::Cyan)),
|
|
)
|
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
|
|
|
|
frame.render_stateful_widget(channels, rows[0], &mut state);
|
|
|
|
// ── Online users ─────────────────────────────────────────────────────
|
|
let user_items: Vec<ListItem> = app
|
|
.online_users
|
|
.iter()
|
|
.map(|(_, name)| ListItem::new(format!("● {}", name)))
|
|
.collect();
|
|
|
|
let online = List::new(user_items).block(
|
|
Block::default()
|
|
.title(" Online ")
|
|
.borders(Borders::ALL)
|
|
.style(Style::default().fg(Color::Green)),
|
|
);
|
|
frame.render_widget(online, rows[1]);
|
|
}
|
|
|
|
fn draw_main(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
|
let rows = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints([
|
|
Constraint::Length(1), // channel title bar
|
|
Constraint::Min(0), // messages
|
|
Constraint::Length(3), // input box
|
|
Constraint::Length(1), // status bar
|
|
])
|
|
.split(area);
|
|
|
|
// ── Title bar ────────────────────────────────────────────────────────
|
|
let title = app
|
|
.current_channel()
|
|
.map(|(_, name)| format!(" # {} ", name))
|
|
.unwrap_or_else(|| " No channel selected ".into());
|
|
|
|
let title_bar = Paragraph::new(title)
|
|
.style(Style::default().fg(Color::White).bg(Color::DarkGray).add_modifier(Modifier::BOLD));
|
|
frame.render_widget(title_bar, rows[0]);
|
|
|
|
// ── Messages ─────────────────────────────────────────────────────────
|
|
let msgs: Vec<ListItem> = app
|
|
.current_channel_messages()
|
|
.map(|deque| {
|
|
deque
|
|
.iter()
|
|
.map(|m| {
|
|
ListItem::new(Line::from(vec![
|
|
Span::styled(
|
|
format!("{} ", m.timestamp),
|
|
Style::default().fg(Color::DarkGray),
|
|
),
|
|
Span::styled(
|
|
format!("{}: ", m.author),
|
|
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
|
),
|
|
Span::raw(m.content.clone()),
|
|
]))
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let message_count = msgs.len();
|
|
let mut list_state = ListState::default();
|
|
if message_count > 0 {
|
|
list_state.select(Some(message_count - 1));
|
|
}
|
|
|
|
let messages = List::new(msgs)
|
|
.block(Block::default().borders(Borders::LEFT))
|
|
.highlight_style(Style::default());
|
|
frame.render_stateful_widget(messages, rows[1], &mut list_state);
|
|
|
|
// ── Input box ─────────────────────────────────────────────────────────
|
|
let input = Paragraph::new(app.input.as_str())
|
|
.block(
|
|
Block::default()
|
|
.borders(Borders::ALL)
|
|
.title(" Message (Enter to send) "),
|
|
);
|
|
frame.render_widget(input, rows[2]);
|
|
|
|
// ── Status bar ────────────────────────────────────────────────────────
|
|
let status_text = app
|
|
.status_msg
|
|
.as_deref()
|
|
.unwrap_or("Ctrl-Q: quit | ↑↓: channels | Esc: dismiss");
|
|
let status = Paragraph::new(status_text)
|
|
.style(Style::default().fg(Color::DarkGray));
|
|
frame.render_widget(status, rows[3]);
|
|
}
|