Add basic messaging

This commit is contained in:
Sijmen 2023-11-06 12:32:42 +01:00
parent 18a5892371
commit dd55f8c623
4 changed files with 144 additions and 35 deletions

1
Cargo.lock generated
View file

@ -472,6 +472,7 @@ dependencies = [
"futures", "futures",
"iced", "iced",
"irc", "irc",
"once_cell",
"tokio", "tokio",
] ]

View file

@ -10,4 +10,5 @@ color-eyre = "0.6.2"
futures = "0.3.29" futures = "0.3.29"
iced = { version = "0.10.0", features = ["tokio"] } iced = { version = "0.10.0", features = ["tokio"] }
irc = "0.15.0" irc = "0.15.0"
once_cell = "1.18.0"
tokio = { version = "1.33.0", features = ["full"] } tokio = { version = "1.33.0", features = ["full"] }

View file

@ -2,12 +2,15 @@
pkgs.mkShell rec { pkgs.mkShell rec {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
cargo cargo
clippy
clang clang
llvmPackages.bintools llvmPackages.bintools
watchexec
pkg-config pkg-config
openssl openssl
watchexec
libxkbcommon libxkbcommon
libGL libGL

View file

@ -1,59 +1,82 @@
use std::cell::RefCell; use std::cell::RefCell;
use futures::prelude::*;
use irc::client::prelude::*;
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use futures::StreamExt;
use iced::theme::Theme; use iced::theme::Theme;
use iced::widget::{container, text}; use iced::widget::{column, container, scrollable, text, text_input};
use iced::{executor, Application, Settings}; use iced::{executor, Application, Color, Length, Settings};
use once_cell::sync::Lazy;
use tokio::select;
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
async fn irc_loop(sender: UnboundedSender<irc::proto::Message>) -> Result<()> { static INPUT_ID: Lazy<text_input::Id> = Lazy::new(text_input::Id::unique);
let config = Config {
nickname: Some("cri".to_string()),
server: Some("vijf.life".to_string()),
channels: vec!["#h".to_string()],
..Config::default()
};
let mut client = Client::from_config(config).await?; async fn irc_loop(
mut client: irc::client::Client,
message_tx: UnboundedSender<irc::proto::Message>,
mut input_rx: UnboundedReceiver<irc::proto::Message>,
) -> Result<()> {
client.identify()?; client.identify()?;
let mut irc_stream = client.stream()?;
let mut stream = client.stream()?; loop {
select! {
while let Some(message) = stream.next().await.transpose()? { val = irc_stream.next() => {
sender.send(message)?; if let Some(message) = val.transpose()? {
message_tx.send(message)?;
}
}
val = input_rx.recv() => {
client.send(val.unwrap())?;
}
}
} }
Ok(())
} }
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
color_eyre::install()?; color_eyre::install()?;
let (sender, receiver) = mpsc::unbounded_channel::<irc::proto::Message>(); let (message_tx, message_rx) = mpsc::unbounded_channel::<irc::proto::Message>();
let (input_tx, input_rx) = mpsc::unbounded_channel::<irc::proto::Message>();
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(()) Ok(())
} }
struct Cri { struct Cri {
pub receiver: RefCell<Option<UnboundedReceiver<irc::proto::Message>>>, message_rx: RefCell<Option<UnboundedReceiver<irc::proto::Message>>>,
pub irc_message: String, message_log: Vec<irc::proto::Message>,
input_tx: RefCell<UnboundedSender<irc::proto::Message>>,
input_value: String,
} }
struct CriFlags { struct CriFlags {
pub receiver: UnboundedReceiver<irc::proto::Message>, pub message_rx: UnboundedReceiver<irc::proto::Message>,
pub input_tx: UnboundedSender<irc::proto::Message>,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
enum Message { enum Message {
IrcMessageReceived(irc::proto::Message), IrcMessageReceived(irc::proto::Message),
InputChanged(String),
InputSubmitted,
} }
impl Application for Cri { impl Application for Cri {
@ -65,8 +88,10 @@ impl Application for Cri {
fn new(flags: Self::Flags) -> (Self, iced::Command<Self::Message>) { fn new(flags: Self::Flags) -> (Self, iced::Command<Self::Message>) {
( (
Self { Self {
receiver: RefCell::new(Some(flags.receiver)), message_rx: RefCell::new(Some(flags.message_rx)),
irc_message: String::new(), message_log: Vec::new(),
input_tx: RefCell::new(flags.input_tx),
input_value: String::new(),
}, },
iced::Command::none(), iced::Command::none(),
) )
@ -78,9 +103,17 @@ impl Application for Cri {
fn update(&mut self, message: Self::Message) -> iced::Command<Self::Message> { fn update(&mut self, message: Self::Message) -> iced::Command<Self::Message> {
match message { match message {
Message::IrcMessageReceived(message) => { Message::IrcMessageReceived(message) => self.message_log.push(message),
let message = message.to_string().replace("\r", ""); Message::InputChanged(text) => self.input_value = text,
self.irc_message += &message; 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() iced::Command::none()
@ -89,7 +122,7 @@ impl Application for Cri {
fn subscription(&self) -> iced::Subscription<Self::Message> { fn subscription(&self) -> iced::Subscription<Self::Message> {
iced::subscription::unfold( iced::subscription::unfold(
"irc message", "irc message",
self.receiver.take(), self.message_rx.take(),
move |mut receiver| async move { move |mut receiver| async move {
let message = receiver.as_mut().unwrap().recv().await.unwrap(); let message = receiver.as_mut().unwrap().recv().await.unwrap();
(Message::IrcMessageReceived(message), receiver) (Message::IrcMessageReceived(message), receiver)
@ -98,7 +131,78 @@ impl Application for Cri {
} }
fn view(&self) -> iced::Element<'_, Self::Message, iced::Renderer<Self::Theme>> { fn view(&self) -> iced::Element<'_, Self::Message, iced::Renderer<Self::Theme>> {
let log = text(self.irc_message.clone()); let dark_green = Color::new(0.153, 0.682, 0.377, 1.0);
container(log).into() 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::<Vec<_>>(),
))
.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()
} }
} }