Move all types to their own modules

This commit is contained in:
Sijmen 2023-11-23 11:53:18 +01:00
parent 8d2e832671
commit b0dcb6b170
Signed by: vijfhoek
GPG key ID: DAF7821E067D9C48
5 changed files with 487 additions and 466 deletions

464
src/cri.rs Normal file
View file

@ -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<text_input::Id> = Lazy::new(text_input::Id::unique);
pub struct Cri {
message_rx: RefCell<Option<UnboundedReceiver<irc::proto::Message>>>,
input_tx: RefCell<UnboundedSender<irc::proto::Message>>,
message_log: MessageLog,
input_value: String,
nickname: String,
capabilities: Vec<String>,
}
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::<Vec<_>>().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<Local>,
message_id: Option<&str>,
) {
let already_joined = self.message_log.has_channel(dbg!(chanlist));
self.message_log
.on_join(chanlist, source_nickname, &timestamp, 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>) {
(
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<Self::Message> {
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::<HashMap<_, _>>();
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(),
&timestamp,
message_id,
);
}
IrcCommand::NICK(new) => {
self.message_log
.on_nick(&source_nickname, new, &timestamp, message_id);
}
IrcCommand::QUIT(comment) => {
self.message_log.on_quit(
&source_nickname,
comment.as_deref(),
&timestamp,
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,
&timestamp,
);
}
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(),
&timestamp,
);
}
_ => 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<Self::Message> {
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<Self::Theme>> {
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<Element<_, _>> {
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::<Vec<_>>(),
)
.height(Length::Fill)
.width(Length::Fixed(200.0));
let content = row![channels, column![log, message_box].height(Length::Fill)];
container(content).into()
}
}

6
src/cri_flags.rs Normal file
View file

@ -0,0 +1,6 @@
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
pub struct CriFlags {
pub message_rx: UnboundedReceiver<irc::proto::Message>,
pub input_tx: UnboundedSender<irc::proto::Message>,
}

View file

@ -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<text_input::Id> = 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<irc::proto::Message>,
pub input_tx: UnboundedSender<irc::proto::Message>,
}
#[derive(Debug, Clone)]
pub enum UiMessage {
IrcMessageReceived(Box<irc::proto::Message>),
InputChanged(String),
InputSubmitted,
HandleChannelPress(Option<String>),
None,
}
struct Cri {
message_rx: RefCell<Option<UnboundedReceiver<irc::proto::Message>>>,
input_tx: RefCell<UnboundedSender<irc::proto::Message>>,
message_log: MessageLog,
input_value: String,
nickname: String,
capabilities: Vec<String>,
}
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::<Vec<_>>().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<Local>,
message_id: Option<&str>,
) {
let already_joined = self.message_log.has_channel(dbg!(chanlist));
self.message_log
.on_join(chanlist, source_nickname, &timestamp, 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>) {
(
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<Self::Message> {
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::<HashMap<_, _>>();
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(),
&timestamp,
message_id,
);
}
IrcCommand::NICK(new) => {
self.message_log
.on_nick(&source_nickname, new, &timestamp, message_id);
}
IrcCommand::QUIT(comment) => {
self.message_log.on_quit(
&source_nickname,
comment.as_deref(),
&timestamp,
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,
&timestamp,
);
}
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(),
&timestamp,
);
}
_ => 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<Self::Message> {
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<Self::Theme>> {
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<Element<_, _>> {
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::<Vec<_>>(),
)
.height(Length::Fill)
.width(Length::Fixed(200.0));
let content = row![channels, column![log, message_box].height(Length::Fill)];
container(content).into()
}
}

View file

@ -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);

10
src/ui_message.rs Normal file
View file

@ -0,0 +1,10 @@
use irc::proto::Message as IrcMessage;
#[derive(Debug, Clone)]
pub enum UiMessage {
IrcMessageReceived(Box<IrcMessage>),
InputChanged(String),
InputSubmitted,
HandleChannelPress(Option<String>),
None,
}