Compare commits

...

2 commits

Author SHA1 Message Date
ad5d2f3b37 Add support for channels 2023-11-06 15:06:14 +01:00
6d2b16d472 Add bacon to shell.nix 2023-11-06 14:40:19 +01:00
2 changed files with 203 additions and 70 deletions

View file

@ -9,7 +9,7 @@
pkg-config
openssl
watchexec
bacon
libxkbcommon
libGL

View file

@ -1,10 +1,11 @@
use std::cell::RefCell;
use std::collections::HashMap;
use color_eyre::eyre::Result;
use futures::StreamExt;
use iced::theme::Theme;
use iced::widget::{column, container, scrollable, text, text_input};
use iced::{executor, Application, Color, Length, Settings};
use iced::widget::{column, container, mouse_area, row, scrollable, text, text_input};
use iced::{executor, Application, Background, Color, Length, Settings};
use once_cell::sync::Lazy;
use tokio::select;
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
@ -43,7 +44,12 @@ async fn main() -> Result<()> {
let config = irc::client::prelude::Config {
nickname: Some("cri".to_string()),
server: Some("vijf.life".to_string()),
channels: vec!["#h".to_string()],
channels: vec![
"#h".to_string(),
"#test".to_string(),
"#test1".to_string(),
"#test2".to_string(),
],
..Default::default()
};
@ -60,10 +66,35 @@ async fn main() -> Result<()> {
Ok(())
}
enum IrcMessage {
Join {
nickname: String,
},
Part {
nickname: String,
reason: Option<String>,
},
Nick {
old: String,
new: String,
},
Quit {
nickname: String,
reason: Option<String>,
},
Privmsg {
nickname: String,
message: String,
},
}
struct Cri {
message_rx: RefCell<Option<UnboundedReceiver<irc::proto::Message>>>,
message_log: Vec<irc::proto::Message>,
input_tx: RefCell<UnboundedSender<irc::proto::Message>>,
active_channel: Option<String>,
message_log: HashMap<Option<String>, Vec<IrcMessage>>,
input_value: String,
}
@ -73,24 +104,31 @@ struct CriFlags {
}
#[derive(Debug, Clone)]
enum Message {
enum UiMessage {
IrcMessageReceived(Box<irc::proto::Message>),
InputChanged(String),
InputSubmitted,
HandleChannelPress(Option<String>),
}
impl Application for Cri {
type Executor = executor::Default;
type Message = Message;
type Message = UiMessage;
type Theme = Theme;
type Flags = CriFlags;
fn new(flags: Self::Flags) -> (Self, iced::Command<Self::Message>) {
let mut message_log = HashMap::new();
message_log.insert(None, Vec::new());
(
Self {
message_rx: RefCell::new(Some(flags.message_rx)),
message_log: Vec::new(),
input_tx: RefCell::new(flags.input_tx),
active_channel: None,
message_log,
input_value: String::new(),
},
iced::Command::none(),
@ -103,18 +141,98 @@ impl Application for Cri {
fn update(&mut self, message: Self::Message) -> iced::Command<Self::Message> {
match 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();
UiMessage::IrcMessageReceived(message) => {
let message = *message;
self.message_log.push(message.clone());
self.input_tx.borrow().send(message.clone()).unwrap();
// TODO use actual nickname
let source_nickname = message.source_nickname().unwrap_or("cri").to_string();
match &message.command {
irc::proto::Command::JOIN(chanlist, _, _) => {
self.message_log
.entry(Some(chanlist.to_string()))
.or_insert_with(Vec::new)
.push(IrcMessage::Join {
nickname: source_nickname,
});
}
irc::proto::Command::PART(chanlist, comment) => {
self.message_log
.entry(Some(chanlist.to_string()))
.or_insert_with(Vec::new)
.push(IrcMessage::Part {
nickname: source_nickname,
reason: comment.clone(),
});
}
irc::proto::Command::NICK(new) => {
let channels = self.message_log.keys().cloned().collect::<Vec<_>>();
for channel in channels {
if channel.is_some() {
self.message_log
.get_mut(&channel)
.unwrap()
.push(IrcMessage::Nick {
old: source_nickname.clone(),
new: new.to_string(),
});
}
}
}
irc::proto::Command::QUIT(comment) => {
let channels = self.message_log.keys().cloned().collect::<Vec<_>>();
for channel in channels {
if channel.is_some() {
self.message_log
.get_mut(&channel)
.unwrap()
.push(IrcMessage::Quit {
nickname: source_nickname.clone(),
reason: comment.clone(),
});
}
}
}
irc::proto::Command::PRIVMSG(msgtarget, content) => {
let channel = message.response_target().unwrap_or(msgtarget);
self.message_log
.entry(Some(channel.to_string()))
.or_insert_with(Vec::new)
.push(IrcMessage::Privmsg {
nickname: source_nickname,
message: content.to_string(),
})
}
_ => (),
}
}
UiMessage::InputChanged(text) => self.input_value = text,
UiMessage::InputSubmitted => {
if let Some(active_channel) = &self.active_channel {
let command = irc::proto::Command::PRIVMSG(
active_channel.to_string(),
self.input_value.clone(),
);
let message: irc::proto::Message = command.into();
self.message_log
.get_mut(&self.active_channel)
.unwrap()
.push(IrcMessage::Privmsg {
nickname: String::from("cri"),
message: self.input_value.clone(),
});
self.input_tx.borrow().send(message.clone()).unwrap();
}
self.input_value.clear();
}
UiMessage::HandleChannelPress(channel) => self.active_channel = channel,
}
iced::Command::none()
}
@ -125,7 +243,7 @@ impl Application for Cri {
self.message_rx.take(),
move |mut receiver| async move {
let message = receiver.as_mut().unwrap().recv().await.unwrap();
(Message::IrcMessageReceived(Box::new(message)), receiver)
(UiMessage::IrcMessageReceived(Box::new(message)), receiver)
},
)
}
@ -133,61 +251,41 @@ impl Application for Cri {
fn view(&self) -> iced::Element<'_, Self::Message, iced::Renderer<Self::Theme>> {
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 _darker_grey = Color::new(0.498, 0.549, 0.553, 1.0);
let log = scrollable(column(
self.message_log
self.message_log[&self.active_channel]
.iter()
.flat_map(|message| {
// TODO use actual nickname
let source_nickname = message.source_nickname().unwrap_or("cri");
match message {
IrcMessage::Join { nickname } => {
Some(text(format!("* {nickname} joined the channel")).style(dark_green))
}
IrcMessage::Part { nickname, reason } => {
let reason = match reason {
Some(reason) => format!(" ({reason})"),
None => String::new(),
};
Some(
text(format!("* {nickname} left the channel{reason}"))
.style(dark_green),
)
}
IrcMessage::Nick { old, new } => {
Some(text(format!("* {old} changed their nickname to {new}")).style(dark_green))
}
IrcMessage::Quit { nickname, reason }=> {
let reason = match reason {
Some(reason) => format!(" ({reason})"),
None => String::new(),
};
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),
Some(text(format!("* {nickname} quit the server{reason}"))
.style(dark_green))
},
IrcMessage::Privmsg { nickname, message } => {
Some(text(format!("<{nickname}> {message}")))
}
}
})
.map(|element| element.into())
@ -198,10 +296,45 @@ impl Application for Cri {
let message_box = text_input("your magnum opus", &self.input_value)
.id(INPUT_ID.clone())
.on_input(Message::InputChanged)
.on_submit(Message::InputSubmitted);
.on_input(UiMessage::InputChanged)
.on_submit(UiMessage::InputSubmitted);
let content = column![log, message_box].height(Length::Fill);
let channels = column(
self.message_log
.keys()
.map(|channel| {
let channel_name = channel
.as_ref()
.unwrap_or(&String::from("Server"))
.to_string();
let text = text(channel_name);
let is_active = &self.active_channel == channel;
let container = container(text)
.style(move |_: &_| {
let background = if is_active {
Some(Background::Color(dark_grey))
} else {
None
};
container::Appearance {
background,
..Default::default()
}
})
.width(Length::Fill);
mouse_area(container)
.on_press(UiMessage::HandleChannelPress(channel.clone()))
.into()
})
.collect::<Vec<_>>(),
)
.height(Length::Fill)
.width(Length::Fixed(100.0));
let content = row![channels, column![log, message_box].height(Length::Fill)];
container(content).into()
}