diff options
| -rw-r--r-- | src/client/event.rs | 2 | ||||
| -rw-r--r-- | src/client/handler.rs | 39 | ||||
| -rw-r--r-- | src/client/mod.rs | 21 | ||||
| -rw-r--r-- | src/client/state.rs | 20 | ||||
| -rw-r--r-- | src/main.rs | 46 | ||||
| -rw-r--r-- | src/tui/app.rs | 14 | ||||
| -rw-r--r-- | src/tui/ui.rs | 24 |
7 files changed, 53 insertions, 113 deletions
diff --git a/src/client/event.rs b/src/client/event.rs index d34908d..26e3f86 100644 --- a/src/client/event.rs +++ b/src/client/event.rs @@ -14,7 +14,7 @@ pub enum Event { }, /// We joined a channel - Joined { channel: String }, + Joined { channel: String, nick: String }, /// We or someone else left a channel Parted { diff --git a/src/client/handler.rs b/src/client/handler.rs index 91aa5ea..6b61fed 100644 --- a/src/client/handler.rs +++ b/src/client/handler.rs @@ -61,8 +61,7 @@ pub fn handle(msg: IrcMessage, state: &mut ClientState, sender: &Sender) -> Vec< }) .collect(); - let ch = state.channel_mut(channel); - ch.members.extend(members.clone()); + state.channel.members.extend(members.clone()); events.push(Event::Names { channel: channel.clone(), @@ -74,7 +73,7 @@ pub fn handle(msg: IrcMessage, state: &mut ClientState, sender: &Sender) -> Vec< // --- 332: topic on join --- Command::Numeric(332) => { if let (Some(channel), Some(topic)) = (msg.params.get(1), msg.params.get(2)) { - state.channel_mut(channel).topic = Some(topic.clone()); + state.channel.topic = Some(topic.clone()); events.push(Event::Topic { channel: channel.clone(), topic: topic.clone(), @@ -86,15 +85,12 @@ pub fn handle(msg: IrcMessage, state: &mut ClientState, sender: &Sender) -> Vec< Command::Join => { let nick = nick_from_prefix(&msg.prefix); if let Some(channel) = msg.params.first() { - if nick == state.nick { - // We joined - state.channel_mut(channel); - events.push(Event::Joined { - channel: channel.clone(), - }); - } else { - // Someone else joined - state.channel_mut(channel).members.insert(nick); + events.push(Event::Joined { + channel: channel.clone(), + nick: nick.clone(), + }); + if nick != state.nick { + state.channel.members.insert(nick); } } } @@ -105,10 +101,8 @@ pub fn handle(msg: IrcMessage, state: &mut ClientState, sender: &Sender) -> Vec< let channel = msg.params.first().cloned().unwrap_or_default(); let reason = msg.params.get(1).cloned(); - if nick == state.nick { - state.remove_channel(&channel); - } else { - state.channel_mut(&channel).members.remove(&nick); + if nick != state.nick { + state.channel.members.remove(&nick); } events.push(Event::Parted { @@ -123,10 +117,7 @@ pub fn handle(msg: IrcMessage, state: &mut ClientState, sender: &Sender) -> Vec< let nick = nick_from_prefix(&msg.prefix); let reason = msg.params.first().cloned(); - // Remove them from all channels - for ch in state.channels.values_mut() { - ch.members.remove(&nick); - } + state.channel.members.remove(&nick); events.push(Event::Quit { nick, reason }); } @@ -141,10 +132,8 @@ pub fn handle(msg: IrcMessage, state: &mut ClientState, sender: &Sender) -> Vec< } // Update in all channels - for ch in state.channels.values_mut() { - if ch.members.remove(&old_nick) { - ch.members.insert(new_nick.clone()); - } + if state.channel.members.remove(&old_nick) { + state.channel.members.insert(new_nick.clone()); } events.push(Event::NickChanged { old_nick, new_nick }); @@ -168,7 +157,7 @@ pub fn handle(msg: IrcMessage, state: &mut ClientState, sender: &Sender) -> Vec< // --- TOPIC (live change) --- Command::Topic => { if let (Some(channel), Some(topic)) = (msg.params.first(), msg.params.get(1)) { - state.channel_mut(channel).topic = Some(topic.clone()); + state.channel.topic = Some(topic.clone()); events.push(Event::Topic { channel: channel.clone(), topic: topic.clone(), diff --git a/src/client/mod.rs b/src/client/mod.rs index 6d32017..f02f8e5 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,5 +1,4 @@ use tokio::sync::mpsc; -use tracing::info; use crate::client::event::Event; use crate::client::handler::handle; @@ -23,8 +22,6 @@ pub struct Config { pub realname: String, /// Optional server password pub password: Option<String>, - /// Channels to auto-join after registration - pub autojoin: Vec<String>, } /// The main IRC client. @@ -100,16 +97,6 @@ impl Client { let msg = self.inbox.recv().await?; let events = handle(msg, &mut self.state, &self.sender); - // Handle auto-join after registration - for event in &events { - if let Event::Connected { .. } = event { - for channel in &self.config.autojoin.clone() { - info!("Auto-joining {}", channel); - self.join(channel); - } - } - } - // Return the first event; re-queue the rest // (simple approach: process one at a time via recursive buffering) if let Some(first) = events.into_iter().next() { @@ -160,14 +147,6 @@ impl Client { let msg = self.inbox.try_recv().ok()?; let mut events = handle(msg, &mut self.state, &self.sender); - for event in &events { - if let Event::Connected { .. } = event { - for channel in &self.config.autojoin.clone() { - self.join(channel); - } - } - } - if !events.is_empty() { return Some(events.remove(0)); } diff --git a/src/client/state.rs b/src/client/state.rs index b987509..6945d4e 100644 --- a/src/client/state.rs +++ b/src/client/state.rs @@ -1,10 +1,10 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; /// The full state of a connected IRC client. #[derive(Debug, Default)] pub struct ClientState { pub nick: String, - pub channels: HashMap<String, Channel>, + pub channel: Channel, pub caps: HashSet<String>, pub server_name: Option<String>, pub reg: RegistrationState, @@ -17,20 +17,6 @@ impl ClientState { ..Default::default() } } - - pub fn channel(&self, name: &str) -> Option<&Channel> { - self.channels.get(&name.to_lowercase()) - } - - pub fn channel_mut(&mut self, name: &str) -> &mut Channel { - self.channels - .entry(name.to_lowercase()) - .or_insert_with(|| Channel::new(name)) - } - - pub fn remove_channel(&mut self, name: &str) { - self.channels.remove(&name.to_lowercase()); - } } /// State of the registration handshake. @@ -44,7 +30,7 @@ pub enum RegistrationState { } /// A joined channel and its current state. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Channel { pub name: String, pub members: HashSet<String>, diff --git a/src/main.rs b/src/main.rs index b7d4365..2759304 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,15 +11,12 @@ use ratatui::{Terminal, backend::CrosstermBackend}; use irc_client::client::event::Event as IrcEvent; use irc_client::client::{Client, Config}; use irc_client::proto::message::{Command, IrcMessage}; -use tui::app::{AppState, CHANNEL}; +use tui::app::AppState; use tui::ui; mod tui; -const NICK: &str = ""; - #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { - // ── Terminal setup ──────────────────────────────────────────────────── enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; @@ -29,7 +26,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { let result = run(&mut terminal).await; - // ── Restore terminal ────────────────────────────────────────────────── disable_raw_mode()?; execute!( terminal.backend_mut(), @@ -44,16 +40,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn run( terminal: &mut Terminal<CrosstermBackend<io::Stdout>>, ) -> Result<(), Box<dyn std::error::Error>> { - let mut app = AppState::new(NICK, CHANNEL); + let mut app = AppState::new(); - // ── Connect to IRC ──────────────────────────────────────────────────── + // Connect to IRC let config = Config { server: "irc.libera.chat:6667".to_string(), - nick: NICK.to_string(), + nick: "".to_string(), user: "speakez".to_string(), realname: "speakez".to_string(), password: None, - autojoin: vec![CHANNEL.to_string()], }; let mut client = Client::connect(config).await?; @@ -76,7 +71,9 @@ async fn run( (_, KeyCode::Enter) => { let text = app.take_input(); if !text.is_empty() { - handle_input(&text, &mut app, &mut client); + if handle_input(&text, &mut app, &mut client) { + break; + }; } } @@ -89,11 +86,6 @@ async fn run( (_, KeyCode::Right) => app.cursor_right(), (_, KeyCode::Home) => app.cursor = 0, (_, KeyCode::End) => app.cursor = app.input.len(), - - // Scroll - (_, KeyCode::PageUp) => app.scroll = app.scroll.saturating_add(5), - (_, KeyCode::PageDown) => app.scroll = app.scroll.saturating_sub(5), - _ => {} } } @@ -113,7 +105,7 @@ async fn run( } /// Handle a line entered in the input box. -fn handle_input(text: &str, app: &mut AppState, client: &mut Client) { +fn handle_input(text: &str, app: &mut AppState, client: &mut Client) -> bool { if let Some(cmd) = text.strip_prefix('/') { // It's a command let mut parts = cmd.splitn(2, ' '); @@ -123,6 +115,7 @@ fn handle_input(text: &str, app: &mut AppState, client: &mut Client) { match verb.as_str() { "JOIN" => { client.join(args.trim()); + app.channel = args.trim().to_string(); } "PART" => { let channel = if args.is_empty() { @@ -131,12 +124,14 @@ fn handle_input(text: &str, app: &mut AppState, client: &mut Client) { args.trim() }; client.part(channel, None); + app.channel = "".to_string(); } "NICK" => { client.nick(args.trim()); } "QUIT" => { client.send(IrcMessage::new(Command::Quit, vec![args.to_string()])); + return true; } "ME" => { // CTCP ACTION @@ -156,10 +151,13 @@ fn handle_input(text: &str, app: &mut AppState, client: &mut Client) { } } } else { - // Regular chat message to active channel - client.privmsg(&app.channel, text); - app.push_message(&app.nick.clone(), text, true); + if app.connected && !app.channel.is_empty() { + // Regular chat message to active channel + client.privmsg(&app.channel, text); + app.push_message(&app.nick.clone(), text, true); + } } + false } /// Apply an IRC event to the app state. @@ -172,8 +170,14 @@ fn handle_irc_event(event: IrcEvent, app: &mut AppState) { app.push_system(&format!("Connected to {} as {}", server, nick)); } - IrcEvent::Joined { channel } => { - app.push_system(&format!("You joined {}", channel)); + IrcEvent::Joined { channel, nick } => { + if nick == app.nick { + app.push_system(&format!("You joined {}", channel)); + } else { + app.push_system(&format!("{} joined {}", nick, channel)); + app.members.push(nick); + app.sort_members(); + } } IrcEvent::Message { diff --git a/src/tui/app.rs b/src/tui/app.rs index 1e7d021..0b5d77e 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -1,6 +1,3 @@ -/// Compile-time channel to join on startup -pub const CHANNEL: &str = "#speakez"; - /// A single chat message in the log #[derive(Clone)] pub struct ChatLine { @@ -28,8 +25,6 @@ pub struct AppState { pub input: String, /// Cursor position within `input` (byte index) pub cursor: usize, - /// Scroll offset from the bottom (0 = pinned to latest) - pub scroll: usize, /// Status line text (connection state, errors, etc.) pub status: String, /// Whether we've fully registered @@ -37,16 +32,15 @@ pub struct AppState { } impl AppState { - pub fn new(nick: impl Into<String>, channel: impl Into<String>) -> Self { + pub fn new() -> Self { Self { - nick: nick.into(), - channel: channel.into(), + nick: String::new(), + channel: String::new(), messages: Vec::new(), members: Vec::new(), input: String::new(), cursor: 0, - scroll: 0, - status: "Connecting...".into(), + status: "Set nick with /nick to connect.".into(), connected: false, } } diff --git a/src/tui/ui.rs b/src/tui/ui.rs index a38834a..4a243ad 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -90,37 +90,25 @@ fn draw_center(f: &mut Frame, area: Rect, state: &AppState) { } fn draw_chat_log(f: &mut Frame, area: Rect, state: &AppState) { - let inner_height = area.height.saturating_sub(2) as usize; // subtract borders - - // Build all rendered lines first so we can scroll from the bottom let lines: Vec<Line> = state .messages .iter() .map(|msg| render_chat_line(msg)) .collect(); - // Apply scroll offset - // scroll=0 means pinned to bottom (newest). scroll=N means N lines back from bottom. - let total = lines.len(); - let max_scroll = total.saturating_sub(inner_height); - let scroll = state.scroll.min(max_scroll); - let visible_start = total.saturating_sub(inner_height + scroll); - let visible: Vec<Line> = lines - .into_iter() - .skip(visible_start) - .take(inner_height) - .collect(); - let block = Block::default() .borders(Borders::ALL) .border_type(BorderType::Plain) .border_style(Style::default().fg(ORANGE)) .style(Style::default().bg(BG)); + let inner_height = area.height.saturating_sub(2) as usize; + let scroll = lines.len().saturating_sub(inner_height); f.render_widget( - Paragraph::new(Text::from(visible)) + Paragraph::new(Text::from(lines)) .block(block) - .wrap(Wrap { trim: false }), + .wrap(Wrap { trim: false }) + .scroll((scroll as u16, 0)), area, ); } @@ -232,7 +220,7 @@ fn draw_statusbar(f: &mut Frame, area: Rect, state: &AppState) { Span::styled(" │ ", Style::default().fg(FG)), Span::styled(&state.status, Style::default().fg(FG)), Span::styled(" │ ", Style::default().fg(FG)), - Span::styled("PgUp/Dn scroll Ctrl-C quit", Style::default().fg(FG)), + Span::styled("Ctrl-C quit", Style::default().fg(FG)), ]); f.render_widget(Paragraph::new(line).style(Style::default()), area); |
