diff --git a/src/cri.rs b/src/cri.rs new file mode 100644 index 0000000..a1744c4 --- /dev/null +++ b/src/cri.rs @@ -0,0 +1,464 @@ +use std::{cell::RefCell, collections::HashMap}; + +use chrono::{DateTime, Local}; +use iced::{ + alignment::{Horizontal, Vertical}, + executor, + widget::{column, container, mouse_area, row, text, text_input}, + Application, Background, Color, Element, Length, Theme, +}; +use irc::proto::{message::Tag, CapSubCommand, Capability, Command as IrcCommand, Response}; +use once_cell::sync::Lazy; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + +use crate::{ + cri_flags::CriFlags, + irc_message::MessageDetail, + message_log::MessageLog, + ui_message::{self, UiMessage}, +}; + +static INPUT_ID: Lazy = Lazy::new(text_input::Id::unique); + +pub struct Cri { + message_rx: RefCell>>, + input_tx: RefCell>, + message_log: MessageLog, + input_value: String, + nickname: String, + capabilities: Vec, +} + +impl Cri { + fn send_message(&mut self, input_value: &str) { + let active_channel = self.message_log.active_channel.clone(); + if let Some(active_channel) = &active_channel { + let command = IrcCommand::PRIVMSG(active_channel.into(), input_value.into()); + let message: irc::proto::Message = command.into(); + self.input_tx.borrow().send(message.clone()).unwrap(); + + let echo_message = self + .capabilities + .contains(&Capability::EchoMessage.as_ref().into()); + if !echo_message { + self.message_log.on_privmsg( + &self.nickname, + active_channel, + &self.nickname, + input_value, + None, + &Local::now(), + ); + } + } + } + + fn send_command(&mut self, command: &str) { + let mut tokens = command.split_whitespace(); + + let command = tokens.next().unwrap(); + match command { + "/join" | "/j" => self.handle_join_command(&mut tokens), + "/part" | "/p" => self.handle_part_command(&mut tokens), + "/query" | "/q" => self.handle_query_command(tokens), + "/list" => self.handle_list_command(), + _ => (), + } + } + + fn handle_part_command(&mut self, tokens: &mut std::str::SplitWhitespace<'_>) { + let channel = tokens + .next() + .map(String::from) + .or_else(|| self.message_log.active_channel.clone()); + if channel.is_none() { + // TODO error message + return; + } + + let channel = channel.unwrap(); + if !channel.starts_with('#') { + // TODO error message + return; + } + + let reason = tokens.collect::>().join(" "); + let reason = if reason.is_empty() { + None + } else { + Some(reason) + }; + + self.input_tx + .borrow() + .send(IrcCommand::PART(channel, reason).into()) + .unwrap(); + } + + fn handle_join_command(&mut self, tokens: &mut std::str::SplitWhitespace<'_>) { + let channel = tokens + .next() + .map(String::from) + .or_else(|| self.message_log.active_channel.clone()); + if channel.is_none() { + // TODO error message + return; + } + + let channel = channel.unwrap(); + if !channel.starts_with('#') { + // TODO error message + return; + } + + self.input_tx + .borrow() + .send(IrcCommand::JOIN(channel.clone(), tokens.next().map(String::from), None).into()) + .unwrap(); + self.message_log.set_active(Some(channel)); + } + + fn handle_query_command(&mut self, mut tokens: std::str::SplitWhitespace<'_>) { + self.message_log + .set_active(Some(tokens.next().unwrap().into())); + } + + fn handle_list_command(&self) { + self.input_tx + .borrow() + .send(IrcCommand::LIST(None, None).into()) + .unwrap() + } + + fn on_join( + &mut self, + chanlist: &str, + source_nickname: &String, + timestamp: DateTime, + message_id: Option<&str>, + ) { + let already_joined = self.message_log.has_channel(dbg!(chanlist)); + self.message_log + .on_join(chanlist, source_nickname, ×tamp, message_id); + + if !already_joined + && source_nickname == &self.nickname + && self.capabilities.contains(&"draft/chathistory".into()) + { + self.input_tx + .borrow() + .send( + IrcCommand::Raw( + "CHATHISTORY".into(), + vec![ + "LATEST".into(), + chanlist.into(), + "*".into(), + 100.to_string(), + ], + ) + .into(), + ) + .unwrap(); + } + } +} + +impl Application for Cri { + type Executor = executor::Default; + type Message = UiMessage; + type Theme = Theme; + type Flags = CriFlags; + + fn new(flags: Self::Flags) -> (Self, iced::Command) { + ( + Self { + message_rx: RefCell::new(Some(flags.message_rx)), + input_tx: RefCell::new(flags.input_tx), + message_log: MessageLog::new(), + input_value: String::new(), + nickname: "cri".into(), // TODO take default value from config + capabilities: Vec::new(), + }, + iced::Command::none(), + ) + } + + fn title(&self) -> String { + "cri".into() + } + + fn update(&mut self, message: Self::Message) -> iced::Command { + match message { + ui_message::UiMessage::IrcMessageReceived(message) => { + // TODO use actual nickname + let source_nickname: String = + message.source_nickname().unwrap_or(&self.nickname).into(); + + let tags_map = message + .tags + .clone() + .unwrap_or_default() + .iter() + .map(|Tag(k, v)| (k.clone(), v.clone())) + .collect::>(); + + let timestamp = tags_map + .get(&"time".to_owned()) + .cloned() + .flatten() + .and_then(|time| DateTime::parse_from_rfc3339(&time).ok()) + .map(DateTime::into) + .unwrap_or_else(Local::now); + + let message_id = tags_map.get("msgid").cloned().flatten(); + let message_id = message_id.as_deref(); + + match &message.command { + IrcCommand::CAP(_, CapSubCommand::ACK, capability, _) => { + let capability = capability.as_ref().unwrap(); + self.capabilities.push(capability.clone()); + } + + IrcCommand::JOIN(chanlist, _, _) => { + self.on_join(chanlist, &source_nickname, timestamp, message_id); + } + + IrcCommand::PART(chanlist, comment) => { + self.message_log.on_part( + chanlist, + &source_nickname, + comment.as_deref(), + ×tamp, + message_id, + ); + } + + IrcCommand::NICK(new) => { + self.message_log + .on_nick(&source_nickname, new, ×tamp, message_id); + } + + IrcCommand::QUIT(comment) => { + self.message_log.on_quit( + &source_nickname, + comment.as_deref(), + ×tamp, + message_id, + ); + } + + IrcCommand::PRIVMSG(msgtarget, content) + | IrcCommand::NOTICE(msgtarget, content) => { + let channel: String = message.response_target().unwrap_or(msgtarget).into(); + self.message_log.on_privmsg( + &self.nickname, + &channel, + &source_nickname, + content, + message_id, + ×tamp, + ); + } + + IrcCommand::Response(Response::RPL_WELCOME, args) => { + self.nickname = args[0].clone() + } + IrcCommand::Response(Response::RPL_NAMREPLY, args) => { + let channel = &args[2]; + let names: Vec<_> = args[3].split_ascii_whitespace().collect(); + self.message_log.on_names_reply(channel, names); + } + IrcCommand::Response(Response::RPL_TOPIC, args) => { + let channel = &args[1]; + let topic = &args[2]; + self.message_log.on_topic(channel, topic); + } + + IrcCommand::BATCH(tag, subcommand, params) => { + self.message_log.on_batch( + &self.nickname, + tag, + subcommand.as_ref(), + params.as_ref(), + ×tamp, + ); + } + + _ => self.message_log.on_other(&message.to_string()), + } + } + ui_message::UiMessage::InputChanged(text) => self.input_value = text, + ui_message::UiMessage::InputSubmitted => { + if self.input_value.starts_with("//") { + self.send_message(&self.input_value.clone()[1..]) + } else if self.input_value.starts_with('/') { + self.send_command(&self.input_value.clone()) + } else { + self.send_message(&self.input_value.clone()); + } + + self.input_value.clear(); + } + ui_message::UiMessage::HandleChannelPress(channel) => { + self.message_log.set_active(channel) + } + ui_message::UiMessage::None => (), + } + iced::Command::none() + } + + fn subscription(&self) -> iced::Subscription { + iced::subscription::unfold( + "irc message", + self.message_rx.take(), + move |mut receiver| async move { + if let Some(message) = receiver.as_mut().unwrap().recv().await { + ( + ui_message::UiMessage::IrcMessageReceived(Box::new(message)), + receiver, + ) + } else { + (ui_message::UiMessage::None, receiver) + } + }, + ) + } + + fn view(&self) -> iced::Element<'_, Self::Message, iced::Renderer> { + let dark_grey = Color::new(0.58, 0.65, 0.65, 1.0); + let light_blue = Color::new(0.26, 0.62, 0.85, 1.0); + let light_red = Color::new(0.99, 0.36, 0.40, 1.0); + + let log = self.message_log.view(&self.nickname); + + let message_box = text_input("your magnum opus", &self.input_value) + .id(INPUT_ID.clone()) + .on_input(ui_message::UiMessage::InputChanged) + .on_submit(ui_message::UiMessage::InputSubmitted); + + let channels = column( + self.message_log + .get_all() + .iter() + .map(|(channel_name, channel)| { + let is_active = self.message_log.active_channel == **channel_name; + let label = match channel_name { + None => "Server", + Some(channel_name) => channel_name, + }; + + let text_color = if is_active { Some(Color::WHITE) } else { None }; + let nickname_color = if is_active { Color::WHITE } else { light_blue }; + let no_message_color = if is_active { Color::WHITE } else { dark_grey }; + + let text_size = 14.0; + let last_message = container( + channel + .messages + .iter() + .rev() + .find_map(|m| -> Option> { + match &m.detail { + MessageDetail::Privmsg { nickname, message } => Some( + row![ + text(format!("{nickname}: ")) + .style(nickname_color) + .size(text_size), + text(message).size(text_size) + ] + .into(), + ), + MessageDetail::Join { nickname } => Some( + row![ + text(nickname).style(nickname_color).size(text_size), + text(" joined").style(no_message_color).size(text_size) + ] + .into(), + ), + _ => None, + } + }) + .unwrap_or( + text(if channel_name.is_some() { + "No messages" + } else { + "Server-related messages" + }) + .style(no_message_color) + .size(text_size) + .into(), + ), + ) + .padding([2, 0]); + + let unread_events = channel.unread_events; + let unread_messages = channel.unread_messages; + let unread_highlights = channel.unread_highlights; + let unread_total = unread_events + unread_messages + unread_highlights; + + let unread_indicator = container( + container( + text(if unread_total > 0 { + unread_total.to_string() + } else { + String::new() + }) + .style(Color::WHITE) + .size(12), + ) + .width(20) + .height(20) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .style(move |_: &_| container::Appearance { + background: Some(Background::Color(if unread_highlights > 0 { + light_red + } else if unread_messages > 0 { + light_blue + } else if unread_events > 0 { + dark_grey + } else { + Color::TRANSPARENT + })), + border_radius: 12.0.into(), + ..Default::default() + }), + ) + .padding([4, 4]) + .align_y(Vertical::Center) + .height(Length::Fill); + + let container = container(row![ + column![text(label), last_message].width(Length::Fill), + unread_indicator + ]) + .style(move |_: &_| container::Appearance { + background: match is_active { + true => Some(Background::Color(light_blue)), + false => None, + }, + text_color, + ..Default::default() + }) + .padding([4, 4]) + .align_y(Vertical::Center) + .width(Length::Fill) + .height(54); + + mouse_area(container) + .on_press(ui_message::UiMessage::HandleChannelPress( + (*channel_name).clone(), + )) + .into() + }) + .collect::>(), + ) + .height(Length::Fill) + .width(Length::Fixed(200.0)); + + let content = row![channels, column![log, message_box].height(Length::Fill)]; + + container(content).into() + } +} diff --git a/src/cri_flags.rs b/src/cri_flags.rs new file mode 100644 index 0000000..3a922a4 --- /dev/null +++ b/src/cri_flags.rs @@ -0,0 +1,6 @@ +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + +pub struct CriFlags { + pub message_rx: UnboundedReceiver, + pub input_tx: UnboundedSender, +} diff --git a/src/main.rs b/src/main.rs index a040903..3ed61ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,15 @@ +mod cri; +mod cri_flags; mod irc_handler; mod irc_message; mod message_log; +mod ui_message; -use chrono::{DateTime, Local}; use color_eyre::eyre::Result; -use iced::{ - alignment::{Horizontal, Vertical}, - executor, - theme::Theme, - widget::{column, container, mouse_area, row, text, text_input}, - Application, Background, Color, Element, Length, Settings, -}; -use irc::proto::{message::Tag, CapSubCommand, Capability, Command as IrcCommand, Response}; -use irc_message::MessageDetail; -use message_log::MessageLog; -use once_cell::sync::Lazy; -use std::{cell::RefCell, collections::HashMap}; -use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use iced::{Application, Settings}; +use tokio::sync::mpsc::{self}; -static INPUT_ID: Lazy = Lazy::new(text_input::Id::unique); +use crate::{cri::Cri, cri_flags::CriFlags}; #[tokio::main] async fn main() -> Result<()> { @@ -39,453 +30,3 @@ async fn main() -> Result<()> { Ok(()) } - -struct CriFlags { - pub message_rx: UnboundedReceiver, - pub input_tx: UnboundedSender, -} - -#[derive(Debug, Clone)] -pub enum UiMessage { - IrcMessageReceived(Box), - InputChanged(String), - InputSubmitted, - HandleChannelPress(Option), - None, -} - -struct Cri { - message_rx: RefCell>>, - input_tx: RefCell>, - message_log: MessageLog, - input_value: String, - nickname: String, - capabilities: Vec, -} - -impl Cri { - fn send_message(&mut self, input_value: &str) { - let active_channel = self.message_log.active_channel.clone(); - if let Some(active_channel) = &active_channel { - let command = IrcCommand::PRIVMSG(active_channel.into(), input_value.into()); - let message: irc::proto::Message = command.into(); - self.input_tx.borrow().send(message.clone()).unwrap(); - - let echo_message = self - .capabilities - .contains(&Capability::EchoMessage.as_ref().into()); - if !echo_message { - self.message_log.on_privmsg( - &self.nickname, - active_channel, - &self.nickname, - input_value, - None, - &Local::now(), - ); - } - } - } - - fn send_command(&mut self, command: &str) { - let mut tokens = command.split_whitespace(); - - let command = tokens.next().unwrap(); - match command { - "/join" | "/j" => self.handle_join_command(&mut tokens), - "/part" | "/p" => self.handle_part_command(&mut tokens), - "/query" | "/q" => self.handle_query_command(tokens), - "/list" => self.handle_list_command(), - _ => (), - } - } - - fn handle_part_command(&mut self, tokens: &mut std::str::SplitWhitespace<'_>) { - let channel = tokens - .next() - .map(String::from) - .or_else(|| self.message_log.active_channel.clone()); - if channel.is_none() { - // TODO error message - return; - } - - let channel = channel.unwrap(); - if !channel.starts_with('#') { - // TODO error message - return; - } - - let reason = tokens.collect::>().join(" "); - let reason = if reason.is_empty() { - None - } else { - Some(reason) - }; - - self.input_tx - .borrow() - .send(IrcCommand::PART(channel, reason).into()) - .unwrap(); - } - - fn handle_join_command(&mut self, tokens: &mut std::str::SplitWhitespace<'_>) { - let channel = tokens - .next() - .map(String::from) - .or_else(|| self.message_log.active_channel.clone()); - if channel.is_none() { - // TODO error message - return; - } - - let channel = channel.unwrap(); - if !channel.starts_with('#') { - // TODO error message - return; - } - - self.input_tx - .borrow() - .send(IrcCommand::JOIN(channel.clone(), tokens.next().map(String::from), None).into()) - .unwrap(); - self.message_log.set_active(Some(channel)); - } - - fn handle_query_command(&mut self, mut tokens: std::str::SplitWhitespace<'_>) { - self.message_log - .set_active(Some(tokens.next().unwrap().into())); - } - - fn handle_list_command(&self) { - self.input_tx - .borrow() - .send(IrcCommand::LIST(None, None).into()) - .unwrap() - } - - fn on_join( - &mut self, - chanlist: &str, - source_nickname: &String, - timestamp: DateTime, - message_id: Option<&str>, - ) { - let already_joined = self.message_log.has_channel(dbg!(chanlist)); - self.message_log - .on_join(chanlist, source_nickname, ×tamp, message_id); - - if !already_joined - && source_nickname == &self.nickname - && self.capabilities.contains(&"draft/chathistory".into()) - { - self.input_tx - .borrow() - .send( - IrcCommand::Raw( - "CHATHISTORY".into(), - vec![ - "LATEST".into(), - chanlist.into(), - "*".into(), - 100.to_string(), - ], - ) - .into(), - ) - .unwrap(); - } - } -} - -impl Application for Cri { - type Executor = executor::Default; - type Message = UiMessage; - type Theme = Theme; - type Flags = CriFlags; - - fn new(flags: Self::Flags) -> (Self, iced::Command) { - ( - Self { - message_rx: RefCell::new(Some(flags.message_rx)), - input_tx: RefCell::new(flags.input_tx), - message_log: MessageLog::new(), - input_value: String::new(), - nickname: "cri".into(), // TODO take default value from config - capabilities: Vec::new(), - }, - iced::Command::none(), - ) - } - - fn title(&self) -> String { - "cri".into() - } - - fn update(&mut self, message: Self::Message) -> iced::Command { - match message { - UiMessage::IrcMessageReceived(message) => { - // TODO use actual nickname - let source_nickname: String = - message.source_nickname().unwrap_or(&self.nickname).into(); - - let tags_map = message - .tags - .clone() - .unwrap_or_default() - .iter() - .map(|Tag(k, v)| (k.clone(), v.clone())) - .collect::>(); - - let timestamp = tags_map - .get(&"time".to_owned()) - .cloned() - .flatten() - .and_then(|time| DateTime::parse_from_rfc3339(&time).ok()) - .map(DateTime::into) - .unwrap_or_else(Local::now); - - let message_id = tags_map.get("msgid").cloned().flatten(); - let message_id = message_id.as_deref(); - - match &message.command { - IrcCommand::CAP(_, CapSubCommand::ACK, capability, _) => { - let capability = capability.as_ref().unwrap(); - self.capabilities.push(capability.clone()); - } - - IrcCommand::JOIN(chanlist, _, _) => { - self.on_join(chanlist, &source_nickname, timestamp, message_id); - } - - IrcCommand::PART(chanlist, comment) => { - self.message_log.on_part( - chanlist, - &source_nickname, - comment.as_deref(), - ×tamp, - message_id, - ); - } - - IrcCommand::NICK(new) => { - self.message_log - .on_nick(&source_nickname, new, ×tamp, message_id); - } - - IrcCommand::QUIT(comment) => { - self.message_log.on_quit( - &source_nickname, - comment.as_deref(), - ×tamp, - message_id, - ); - } - - IrcCommand::PRIVMSG(msgtarget, content) - | IrcCommand::NOTICE(msgtarget, content) => { - let channel: String = message.response_target().unwrap_or(msgtarget).into(); - self.message_log.on_privmsg( - &self.nickname, - &channel, - &source_nickname, - content, - message_id, - ×tamp, - ); - } - - IrcCommand::Response(Response::RPL_WELCOME, args) => { - self.nickname = args[0].clone() - } - IrcCommand::Response(Response::RPL_NAMREPLY, args) => { - let channel = &args[2]; - let names: Vec<_> = args[3].split_ascii_whitespace().collect(); - self.message_log.on_names_reply(channel, names); - } - IrcCommand::Response(Response::RPL_TOPIC, args) => { - let channel = &args[1]; - let topic = &args[2]; - self.message_log.on_topic(channel, topic); - } - - IrcCommand::BATCH(tag, subcommand, params) => { - self.message_log.on_batch( - &self.nickname, - tag, - subcommand.as_ref(), - params.as_ref(), - ×tamp, - ); - } - - _ => self.message_log.on_other(&message.to_string()), - } - } - UiMessage::InputChanged(text) => self.input_value = text, - UiMessage::InputSubmitted => { - if self.input_value.starts_with("//") { - self.send_message(&self.input_value.clone()[1..]) - } else if self.input_value.starts_with('/') { - self.send_command(&self.input_value.clone()) - } else { - self.send_message(&self.input_value.clone()); - } - - self.input_value.clear(); - } - UiMessage::HandleChannelPress(channel) => self.message_log.set_active(channel), - UiMessage::None => (), - } - iced::Command::none() - } - - fn subscription(&self) -> iced::Subscription { - iced::subscription::unfold( - "irc message", - self.message_rx.take(), - move |mut receiver| async move { - if let Some(message) = receiver.as_mut().unwrap().recv().await { - (UiMessage::IrcMessageReceived(Box::new(message)), receiver) - } else { - (UiMessage::None, receiver) - } - }, - ) - } - - fn view(&self) -> iced::Element<'_, Self::Message, iced::Renderer> { - let dark_grey = Color::new(0.58, 0.65, 0.65, 1.0); - let light_blue = Color::new(0.26, 0.62, 0.85, 1.0); - let light_red = Color::new(0.99, 0.36, 0.40, 1.0); - - let log = self.message_log.view(&self.nickname); - - let message_box = text_input("your magnum opus", &self.input_value) - .id(INPUT_ID.clone()) - .on_input(UiMessage::InputChanged) - .on_submit(UiMessage::InputSubmitted); - - let channels = column( - self.message_log - .get_all() - .iter() - .map(|(channel_name, channel)| { - let is_active = self.message_log.active_channel == **channel_name; - let label = match channel_name { - None => "Server", - Some(channel_name) => channel_name, - }; - - let text_color = if is_active { Some(Color::WHITE) } else { None }; - let nickname_color = if is_active { Color::WHITE } else { light_blue }; - let no_message_color = if is_active { Color::WHITE } else { dark_grey }; - - let text_size = 14.0; - let last_message = container( - channel - .messages - .iter() - .rev() - .find_map(|m| -> Option> { - match &m.detail { - MessageDetail::Privmsg { nickname, message } => Some( - row![ - text(format!("{nickname}: ")) - .style(nickname_color) - .size(text_size), - text(message).size(text_size) - ] - .into(), - ), - MessageDetail::Join { nickname } => Some( - row![ - text(nickname).style(nickname_color).size(text_size), - text(" joined").style(no_message_color).size(text_size) - ] - .into(), - ), - _ => None, - } - }) - .unwrap_or( - text(if channel_name.is_some() { - "No messages" - } else { - "Server-related messages" - }) - .style(no_message_color) - .size(text_size) - .into(), - ), - ) - .padding([2, 0]); - - let unread_events = channel.unread_events; - let unread_messages = channel.unread_messages; - let unread_highlights = channel.unread_highlights; - let unread_total = unread_events + unread_messages + unread_highlights; - - let unread_indicator = container( - container( - text(if unread_total > 0 { - unread_total.to_string() - } else { - String::new() - }) - .style(Color::WHITE) - .size(12), - ) - .width(20) - .height(20) - .align_x(Horizontal::Center) - .align_y(Vertical::Center) - .style(move |_: &_| container::Appearance { - background: Some(Background::Color(if unread_highlights > 0 { - light_red - } else if unread_messages > 0 { - light_blue - } else if unread_events > 0 { - dark_grey - } else { - Color::TRANSPARENT - })), - border_radius: 12.0.into(), - ..Default::default() - }), - ) - .padding([4, 4]) - .align_y(Vertical::Center) - .height(Length::Fill); - - let container = container(row![ - column![text(label), last_message].width(Length::Fill), - unread_indicator - ]) - .style(move |_: &_| container::Appearance { - background: match is_active { - true => Some(Background::Color(light_blue)), - false => None, - }, - text_color, - ..Default::default() - }) - .padding([4, 4]) - .align_y(Vertical::Center) - .width(Length::Fill) - .height(54); - - mouse_area(container) - .on_press(UiMessage::HandleChannelPress((*channel_name).clone())) - .into() - }) - .collect::>(), - ) - .height(Length::Fill) - .width(Length::Fixed(200.0)); - - let content = row![channels, column![log, message_box].height(Length::Fill)]; - - container(content).into() - } -} diff --git a/src/message_log.rs b/src/message_log.rs index 52d6a66..cf7cda0 100644 --- a/src/message_log.rs +++ b/src/message_log.rs @@ -292,7 +292,7 @@ impl<'a> MessageLog { channel.unread_highlights = 0; } - pub fn view(&self, current_nickname: &str) -> Container<'_, crate::UiMessage> { + pub fn view(&self, current_nickname: &str) -> Container<'_, crate::ui_message::UiMessage> { let lighter_grey = Color::new(0.93, 0.94, 0.95, 1.0); let dark_grey = Color::new(0.58, 0.65, 0.65, 1.0); let lighter_green = Color::new(0.94, 0.99, 0.87, 1.0); diff --git a/src/ui_message.rs b/src/ui_message.rs new file mode 100644 index 0000000..10722cb --- /dev/null +++ b/src/ui_message.rs @@ -0,0 +1,10 @@ +use irc::proto::Message as IrcMessage; + +#[derive(Debug, Clone)] +pub enum UiMessage { + IrcMessageReceived(Box), + InputChanged(String), + InputSubmitted, + HandleChannelPress(Option), + None, +}