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",
"iced",
"irc",
"once_cell",
"tokio",
]

View file

@ -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"] }

View file

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

View file

@ -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<irc::proto::Message>) -> 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<text_input::Id> = 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<irc::proto::Message>,
mut input_rx: UnboundedReceiver<irc::proto::Message>,
) -> 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::<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(())
}
struct Cri {
pub receiver: RefCell<Option<UnboundedReceiver<irc::proto::Message>>>,
pub irc_message: String,
message_rx: RefCell<Option<UnboundedReceiver<irc::proto::Message>>>,
message_log: Vec<irc::proto::Message>,
input_tx: RefCell<UnboundedSender<irc::proto::Message>>,
input_value: String,
}
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 {
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::Message>) {
(
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<Self::Message> {
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<Self::Message> {
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<Self::Theme>> {
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::<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()
}
}