diff --git a/src/main.rs b/src/main.rs index 5fa3fc8..58a9aff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ use std::cell::RefCell; +use std::collections::HashMap; use color_eyre::eyre::Result; use futures::StreamExt; use iced::theme::Theme; -use iced::widget::{column, container, scrollable, text, text_input}; -use iced::{executor, Application, Color, Length, Settings}; +use iced::widget::{column, container, mouse_area, row, scrollable, text, text_input}; +use iced::{executor, Application, Background, Color, Length, Settings}; use once_cell::sync::Lazy; use tokio::select; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; @@ -43,7 +44,12 @@ async fn main() -> Result<()> { let config = irc::client::prelude::Config { nickname: Some("cri".to_string()), server: Some("vijf.life".to_string()), - channels: vec!["#h".to_string()], + channels: vec![ + "#h".to_string(), + "#test".to_string(), + "#test1".to_string(), + "#test2".to_string(), + ], ..Default::default() }; @@ -60,10 +66,35 @@ async fn main() -> Result<()> { Ok(()) } +enum IrcMessage { + Join { + nickname: String, + }, + Part { + nickname: String, + reason: Option, + }, + Nick { + old: String, + new: String, + }, + Quit { + nickname: String, + reason: Option, + }, + Privmsg { + nickname: String, + message: String, + }, +} + struct Cri { message_rx: RefCell>>, - message_log: Vec, input_tx: RefCell>, + + active_channel: Option, + + message_log: HashMap, Vec>, input_value: String, } @@ -73,24 +104,31 @@ struct CriFlags { } #[derive(Debug, Clone)] -enum Message { +enum UiMessage { IrcMessageReceived(Box), InputChanged(String), InputSubmitted, + HandleChannelPress(Option), } impl Application for Cri { type Executor = executor::Default; - type Message = Message; + type Message = UiMessage; type Theme = Theme; type Flags = CriFlags; fn new(flags: Self::Flags) -> (Self, iced::Command) { + let mut message_log = HashMap::new(); + message_log.insert(None, Vec::new()); + ( Self { message_rx: RefCell::new(Some(flags.message_rx)), - message_log: Vec::new(), input_tx: RefCell::new(flags.input_tx), + + active_channel: None, + + message_log, input_value: String::new(), }, iced::Command::none(), @@ -103,18 +141,98 @@ impl Application for Cri { fn update(&mut self, message: Self::Message) -> iced::Command { match message { - Message::IrcMessageReceived(message) => self.message_log.push(*message), - Message::InputChanged(text) => self.input_value = text, - Message::InputSubmitted => { - let command = - irc::proto::Command::PRIVMSG("#h".to_string(), self.input_value.clone()); - let message: irc::proto::Message = command.into(); + UiMessage::IrcMessageReceived(message) => { + let message = *message; - self.message_log.push(message.clone()); - self.input_tx.borrow().send(message.clone()).unwrap(); + // TODO use actual nickname + let source_nickname = message.source_nickname().unwrap_or("cri").to_string(); + + match &message.command { + irc::proto::Command::JOIN(chanlist, _, _) => { + self.message_log + .entry(Some(chanlist.to_string())) + .or_insert_with(Vec::new) + .push(IrcMessage::Join { + nickname: source_nickname, + }); + } + irc::proto::Command::PART(chanlist, comment) => { + self.message_log + .entry(Some(chanlist.to_string())) + .or_insert_with(Vec::new) + .push(IrcMessage::Part { + nickname: source_nickname, + reason: comment.clone(), + }); + } + + + irc::proto::Command::NICK(new) => { + let channels = self.message_log.keys().cloned().collect::>(); + for channel in channels { + if channel.is_some() { + self.message_log + .get_mut(&channel) + .unwrap() + .push(IrcMessage::Nick { + old: source_nickname.clone(), + new: new.to_string(), + }); + } + } + } + + irc::proto::Command::QUIT(comment) => { + let channels = self.message_log.keys().cloned().collect::>(); + for channel in channels { + if channel.is_some() { + self.message_log + .get_mut(&channel) + .unwrap() + .push(IrcMessage::Quit { + nickname: source_nickname.clone(), + reason: comment.clone(), + }); + } + } + } + + irc::proto::Command::PRIVMSG(msgtarget, content) => { + let channel = message.response_target().unwrap_or(msgtarget); + + self.message_log + .entry(Some(channel.to_string())) + .or_insert_with(Vec::new) + .push(IrcMessage::Privmsg { + nickname: source_nickname, + message: content.to_string(), + }) + } + _ => (), + } + } + UiMessage::InputChanged(text) => self.input_value = text, + UiMessage::InputSubmitted => { + if let Some(active_channel) = &self.active_channel { + let command = irc::proto::Command::PRIVMSG( + active_channel.to_string(), + self.input_value.clone(), + ); + let message: irc::proto::Message = command.into(); + + self.message_log + .get_mut(&self.active_channel) + .unwrap() + .push(IrcMessage::Privmsg { + nickname: String::from("cri"), + message: self.input_value.clone(), + }); + self.input_tx.borrow().send(message.clone()).unwrap(); + } self.input_value.clear(); } + UiMessage::HandleChannelPress(channel) => self.active_channel = channel, } iced::Command::none() } @@ -125,7 +243,7 @@ impl Application for Cri { self.message_rx.take(), move |mut receiver| async move { let message = receiver.as_mut().unwrap().recv().await.unwrap(); - (Message::IrcMessageReceived(Box::new(message)), receiver) + (UiMessage::IrcMessageReceived(Box::new(message)), receiver) }, ) } @@ -133,61 +251,41 @@ impl Application for Cri { fn view(&self) -> iced::Element<'_, Self::Message, iced::Renderer> { let dark_green = Color::new(0.153, 0.682, 0.377, 1.0); let dark_grey = Color::new(0.584, 0.647, 0.651, 1.0); - let darker_grey = Color::new(0.498, 0.549, 0.553, 1.0); + let _darker_grey = Color::new(0.498, 0.549, 0.553, 1.0); let log = scrollable(column( - self.message_log + self.message_log[&self.active_channel] .iter() .flat_map(|message| { - // TODO use actual nickname - let source_nickname = message.source_nickname().unwrap_or("cri"); + match message { + IrcMessage::Join { nickname } => { + Some(text(format!("* {nickname} joined the channel")).style(dark_green)) + } + IrcMessage::Part { nickname, reason } => { + let reason = match reason { + Some(reason) => format!(" ({reason})"), + None => String::new(), + }; + Some( + text(format!("* {nickname} left the channel{reason}")) + .style(dark_green), + ) + } + IrcMessage::Nick { old, new } => { + Some(text(format!("* {old} changed their nickname to {new}")).style(dark_green)) + } + IrcMessage::Quit { nickname, reason }=> { + let reason = match reason { + Some(reason) => format!(" ({reason})"), + None => String::new(), + }; - match &message.command { - irc::proto::Command::NICK(nickname) => Some( - text(format!( - "* {} changed their nickname to {}", - source_nickname, nickname, - )) - .style(dark_green), - ), - irc::proto::Command::JOIN(chanlist, _, _) => Some( - text(format!( - "[{}] * {} joined the channel", - chanlist, source_nickname, - )) - .style(dark_green), - ), - irc::proto::Command::PART(chanlist, comment) => Some( - text(format!( - "[{}] * {} left the channel{}", - chanlist, - source_nickname, - comment - .as_ref() - .map(|c| format!(" ({})", c)) - .unwrap_or_default(), - )) - .style(dark_green), - ), - irc::proto::Command::QUIT(comment) => Some( - text(format!( - "* {} quit the server{}", - source_nickname, - comment - .as_ref() - .map(|c| format!(" ({})", c)) - .unwrap_or_default(), - )) - .style(dark_green), - ), - irc::proto::Command::PRIVMSG(msgtarget, content) => Some(text(format!( - "[{}] <{}> {}", - message.response_target().unwrap_or(msgtarget), - source_nickname, - content, - ))), - _ => None, - //_ => text(format!("{:?}", m.command)).style(dark_grey), + Some(text(format!("* {nickname} quit the server{reason}")) + .style(dark_green)) + }, + IrcMessage::Privmsg { nickname, message } => { + Some(text(format!("<{nickname}> {message}"))) + } } }) .map(|element| element.into()) @@ -198,10 +296,45 @@ impl Application for Cri { let message_box = text_input("your magnum opus", &self.input_value) .id(INPUT_ID.clone()) - .on_input(Message::InputChanged) - .on_submit(Message::InputSubmitted); + .on_input(UiMessage::InputChanged) + .on_submit(UiMessage::InputSubmitted); - let content = column![log, message_box].height(Length::Fill); + let channels = column( + self.message_log + .keys() + .map(|channel| { + let channel_name = channel + .as_ref() + .unwrap_or(&String::from("Server")) + .to_string(); + + let text = text(channel_name); + let is_active = &self.active_channel == channel; + let container = container(text) + .style(move |_: &_| { + let background = if is_active { + Some(Background::Color(dark_grey)) + } else { + None + }; + + container::Appearance { + background, + ..Default::default() + } + }) + .width(Length::Fill); + + mouse_area(container) + .on_press(UiMessage::HandleChannelPress(channel.clone())) + .into() + }) + .collect::>(), + ) + .height(Length::Fill) + .width(Length::Fixed(100.0)); + + let content = row![channels, column![log, message_box].height(Length::Fill)]; container(content).into() }