diff --git a/Cargo.lock b/Cargo.lock index b7a69a1..6bf1058 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,6 +472,7 @@ dependencies = [ "futures", "iced", "irc", + "once_cell", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 83024de..bffbb58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,5 @@ color-eyre = "0.6.2" futures = "0.3.29" iced = { version = "0.10.0", features = ["tokio"] } irc = "0.15.0" +once_cell = "1.18.0" tokio = { version = "1.33.0", features = ["full"] } diff --git a/shell.nix b/shell.nix index 8e7eff1..25ca55b 100644 --- a/shell.nix +++ b/shell.nix @@ -2,12 +2,15 @@ pkgs.mkShell rec { buildInputs = with pkgs; [ cargo + clippy clang llvmPackages.bintools - watchexec + pkg-config openssl + watchexec + libxkbcommon libGL diff --git a/src/main.rs b/src/main.rs index 7b680d4..0c2cd7e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,59 +1,82 @@ use std::cell::RefCell; -use futures::prelude::*; -use irc::client::prelude::*; - use color_eyre::eyre::Result; +use futures::StreamExt; use iced::theme::Theme; -use iced::widget::{container, text}; -use iced::{executor, Application, Settings}; +use iced::widget::{column, container, scrollable, text, text_input}; +use iced::{executor, Application, Color, Length, Settings}; +use once_cell::sync::Lazy; +use tokio::select; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; -async fn irc_loop(sender: UnboundedSender) -> Result<()> { - let config = Config { - nickname: Some("cri".to_string()), - server: Some("vijf.life".to_string()), - channels: vec!["#h".to_string()], - ..Config::default() - }; +static INPUT_ID: Lazy = Lazy::new(text_input::Id::unique); - let mut client = Client::from_config(config).await?; +async fn irc_loop( + mut client: irc::client::Client, + message_tx: UnboundedSender, + mut input_rx: UnboundedReceiver, +) -> Result<()> { client.identify()?; + let mut irc_stream = client.stream()?; - let mut stream = client.stream()?; - - while let Some(message) = stream.next().await.transpose()? { - sender.send(message)?; + loop { + select! { + val = irc_stream.next() => { + if let Some(message) = val.transpose()? { + message_tx.send(message)?; + } + } + val = input_rx.recv() => { + client.send(val.unwrap())?; + } + } } - - Ok(()) } #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; - let (sender, receiver) = mpsc::unbounded_channel::(); + let (message_tx, message_rx) = mpsc::unbounded_channel::(); + let (input_tx, input_rx) = mpsc::unbounded_channel::(); - tokio::task::spawn(irc_loop(sender)); + let config = irc::client::prelude::Config { + nickname: Some("cri".to_string()), + server: Some("vijf.life".to_string()), + channels: vec!["#h".to_string()], + ..Default::default() + }; - Cri::run(Settings::with_flags(CriFlags { receiver }))?; + let client = irc::client::Client::from_config(config).await?; + let task = tokio::task::spawn(irc_loop(client, message_tx, input_rx)); + + Cri::run(Settings::with_flags(CriFlags { + message_rx, + input_tx, + }))?; + + task.abort(); Ok(()) } struct Cri { - pub receiver: RefCell>>, - pub irc_message: String, + message_rx: RefCell>>, + message_log: Vec, + input_tx: RefCell>, + input_value: String, } struct CriFlags { - pub receiver: UnboundedReceiver, + pub message_rx: UnboundedReceiver, + pub input_tx: UnboundedSender, } -#[derive(Debug)] +#[derive(Debug, Clone)] enum Message { IrcMessageReceived(irc::proto::Message), + InputChanged(String), + InputSubmitted, } impl Application for Cri { @@ -65,8 +88,10 @@ impl Application for Cri { fn new(flags: Self::Flags) -> (Self, iced::Command) { ( Self { - receiver: RefCell::new(Some(flags.receiver)), - irc_message: String::new(), + message_rx: RefCell::new(Some(flags.message_rx)), + message_log: Vec::new(), + input_tx: RefCell::new(flags.input_tx), + input_value: String::new(), }, iced::Command::none(), ) @@ -78,9 +103,17 @@ impl Application for Cri { fn update(&mut self, message: Self::Message) -> iced::Command { match message { - Message::IrcMessageReceived(message) => { - let message = message.to_string().replace("\r", ""); - self.irc_message += &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(); + + self.message_log.push(message.clone()); + self.input_tx.borrow().send(message.clone()).unwrap(); + + self.input_value.clear(); } } iced::Command::none() @@ -89,7 +122,7 @@ impl Application for Cri { fn subscription(&self) -> iced::Subscription { iced::subscription::unfold( "irc message", - self.receiver.take(), + self.message_rx.take(), move |mut receiver| async move { let message = receiver.as_mut().unwrap().recv().await.unwrap(); (Message::IrcMessageReceived(message), receiver) @@ -98,7 +131,78 @@ impl Application for Cri { } fn view(&self) -> iced::Element<'_, Self::Message, iced::Renderer> { - let log = text(self.irc_message.clone()); - container(log).into() + 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 log = scrollable(column( + self.message_log + .iter() + .flat_map(|message| { + // TODO use actual nickname + let source_nickname = message.source_nickname().unwrap_or("cri"); + + 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), + } + }) + .map(|element| element.into()) + .collect::>(), + )) + .height(Length::Fill) + .width(Length::Fill); + + let message_box = text_input("your magnum opus", &self.input_value) + .id(INPUT_ID.clone()) + .on_input(Message::InputChanged) + .on_submit(Message::InputSubmitted); + + let content = column![log, message_box].height(Length::Fill); + + container(content).into() } }