aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Smeding <tom.smeding@gmail.com>2020-09-13 17:58:51 +0200
committerTom Smeding <tom.smeding@gmail.com>2021-01-28 22:20:12 +0100
commit5c7677e5fa134b60c7d4c29d3643125901a31fb8 (patch)
tree42bc74f3d2f257f9c013b986d4b5b421cd0fc361
parent7e169db30f3bc899c8708badf39d8ed268ef1968 (diff)
WIP rust client
-rw-r--r--rust/.gitignore3
-rw-r--r--rust/Cargo.toml13
-rw-r--r--rust/src/error.rs27
-rw-r--r--rust/src/main.rs483
4 files changed, 526 insertions, 0 deletions
diff --git a/rust/.gitignore b/rust/.gitignore
new file mode 100644
index 0000000..671fbc4
--- /dev/null
+++ b/rust/.gitignore
@@ -0,0 +1,3 @@
+target/
+Cargo.lock
+debug.txt
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
new file mode 100644
index 0000000..2c95d3c
--- /dev/null
+++ b/rust/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "tomsg-rustclient"
+version = "0.1.0"
+authors = ["Tom Smeding <tom.smeding@gmail.com>"]
+edition = "2018"
+
+[dependencies]
+tomsg-rs = { git = "https://github.com/lieuwex/tomsg-rs" }
+termion = "1.5"
+tokio = { version = "0.2", features = ["rt-threaded"] }
+futures = "0.3"
+once_cell = "1.4"
+unicode-width = "^0.1.8"
diff --git a/rust/src/error.rs b/rust/src/error.rs
new file mode 100644
index 0000000..f25bc5d
--- /dev/null
+++ b/rust/src/error.rs
@@ -0,0 +1,27 @@
+use std::io;
+
+pub trait IntoIOError {
+ fn ioerr(self) -> io::Error;
+ fn perror(self, parent: io::Error) -> io::Error;
+}
+
+// This impl bound is taken directly from the io::Error::new function.
+impl<E: Into<Box<dyn std::error::Error + Send + Sync>>> IntoIOError for E {
+ fn ioerr(self) -> io::Error {
+ io::Error::new(io::ErrorKind::Other, self)
+ }
+
+ fn perror(self, parent: io::Error) -> io::Error {
+ io::Error::new(parent.kind(), format!("{}: {}", self.into(), parent))
+ }
+}
+
+pub trait IntoIOResult<T> {
+ fn iores(self) -> io::Result<T>;
+}
+
+impl<T, E: IntoIOError> IntoIOResult<T> for Result<T, E> {
+ fn iores(self) -> io::Result<T> {
+ self.map_err(|e| e.ioerr())
+ }
+}
diff --git a/rust/src/main.rs b/rust/src/main.rs
new file mode 100644
index 0000000..2d08656
--- /dev/null
+++ b/rust/src/main.rs
@@ -0,0 +1,483 @@
+use std::fs::{File, OpenOptions};
+use std::collections::{HashMap, HashSet};
+use std::io::{self, Write};
+use std::sync::{Arc, Mutex};
+use std::convert::TryFrom;
+use futures::channel::mpsc;
+use futures::stream::StreamExt;
+use once_cell::sync::Lazy;
+use tokio::{runtime, task};
+use tokio::sync::Mutex as AsyncMutex;
+use termion::{clear, color, cursor};
+use termion::event::Key;
+use termion::raw::{IntoRawMode, RawTerminal};
+use termion::input::{TermRead, MouseTerminal};
+use termion::screen::AlternateScreen;
+use tomsg_rs::Connection;
+use tomsg_rs::connection;
+use tomsg_rs::PushMessage;
+use tomsg_rs::Command;
+use tomsg_rs::Word;
+use tomsg_rs::Line;
+use tomsg_rs::Reply;
+use tomsg_rs::Message;
+use tomsg_rs::Id;
+use unicode_width::UnicodeWidthChar;
+
+mod error;
+
+static DEBUG_FILE: Lazy<Mutex<File>> = Lazy::new(|| {
+ Mutex::new(
+ OpenOptions::new().write(true).truncate(true).create(true)
+ .open("debug.txt").unwrap())
+});
+
+fn debug_write(s: &str) {
+ writeln!(DEBUG_FILE.lock().unwrap(), "{}", s).unwrap();
+}
+
+macro_rules! debug {
+ ($($args:expr),*) => {
+ debug_write(&format!($($args),*));
+ }
+}
+
+fn draw_hori_bar<W: Write>(mut w: W, y: u16, x1: u16, x2: u16) -> io::Result<()> {
+ for x in x1..x2 {
+ write!(w, "{}─", cursor::Goto(x + 1, y + 1))?;
+ }
+ Ok(())
+}
+
+fn draw_vert_bar<W: Write>(mut w: W, x: u16, y1: u16, y2: u16) -> io::Result<()> {
+ for y in y1..y2 {
+ write!(w, "{}│", cursor::Goto(x + 1, y + 1))?;
+ }
+ Ok(())
+}
+
+fn wrap_text(s: &str, maxwid: u16) -> Vec<String> {
+ let mut iter = s.chars();
+ let mut lines = Vec::new();
+ let mut nextline = String::new();
+ let mut cursor = 0;
+ loop {
+ let c = match iter.next() {
+ Some(c) => c,
+ None => break,
+ };
+ if let Some(wid) = c.width().map(|w| u16::try_from(w).unwrap()) {
+ if cursor + wid > maxwid {
+ lines.push(nextline);
+ nextline = String::new();
+ cursor = 0;
+ }
+ nextline.push(c);
+ cursor += wid;
+ }
+ }
+ if nextline.len() > 0 {
+ lines.push(nextline);
+ }
+ lines
+}
+
+type Stdout = MouseTerminal<AlternateScreen<RawTerminal<io::Stdout>>>;
+
+#[derive(Debug, Copy, Clone)]
+struct Layout {
+ rlsepx: u16, // x-coordinate of room list / chat log separator bar
+ nlsepx: u16, // x-coordinate of chat log / nick list separator bar
+}
+
+impl Default for Layout {
+ fn default() -> Self {
+ Self { rlsepx: 5, nlsepx: 5 }
+ }
+}
+
+impl Layout {
+ fn compute(size: (u16, u16), state: &State) -> Self {
+ let rlsepx = u16::try_from(state.roomlist.iter().map(|s| s.as_str().len()).max().unwrap_or(5)).unwrap();
+ let room = state.rooms.get(&state.currentroom);
+ let maxnicklen = room.map(|r| r.members.iter().map(|s| s.as_str().len()).max().unwrap_or(5)).unwrap_or(5);
+ let nlsepx = size.0 - u16::min(size.0, u16::try_from(maxnicklen).unwrap() + 1);
+ Self { rlsepx, nlsepx }
+ }
+}
+
+struct App {
+ conn: Connection,
+ state: AsyncMutex<State>,
+}
+
+struct State {
+ stdout: Stdout,
+ termsize: (u16, u16),
+ layout: Layout,
+ roomlist: Vec<Word>,
+ rooms: HashMap<Word, RoomData>,
+ currentroom: Word,
+ msgs: HashMap<Id, Message>,
+}
+
+#[derive(Debug, Default)]
+struct RoomData {
+ members: Vec<Word>,
+ history: Vec<HItem>,
+ maxnicklen: usize,
+}
+
+#[derive(Debug)]
+enum HItem {
+ Message(Id),
+ Service(String),
+}
+
+impl App {
+ fn new(conn: Connection, stdout: Stdout) -> Self {
+ let termsize = (0, 0);
+ let state = State {
+ stdout,
+ termsize,
+ layout: Layout::default(),
+ roomlist: Vec::new(),
+ rooms: HashMap::new(),
+ currentroom: Word::try_from(String::new()).unwrap(),
+ msgs: HashMap::new(),
+ };
+ App {
+ conn,
+ state: AsyncMutex::new(state)
+ }
+ }
+
+ async fn fetch_rooms_list(self: &Arc<Self>) -> io::Result<()> {
+ debug!("fetch_rooms_list");
+ if let Ok(Reply::List(rooms)) = self.conn.send_command(Command::ListRooms).await? {
+ debug!("fetched room list: {:?}", rooms);
+ let newrooms = rooms.iter().collect::<HashSet<_>>();
+
+ {
+ let mut state = self.state.lock().await;
+ let toremove =
+ state.roomlist.iter()
+ .filter(|r| !newrooms.contains(r))
+ .cloned()
+ .collect::<HashSet<_>>();
+ state.roomlist.retain(|r| !toremove.contains(r));
+ state.rooms.retain(|r, _| !toremove.contains(r));
+ }
+
+ self.full_redraw().await?;
+
+ for room in &rooms {
+ self.enter_room(room).await?;
+ }
+ }
+ Ok(())
+ }
+
+ async fn enter_room(self: &Arc<Self>, room: &Word) -> io::Result<()> {
+ debug!("enter_room({})", room);
+
+ {
+ let mut state = self.state.lock().await;
+ if state.rooms.insert(room.clone(), RoomData::default()).is_none() {
+ state.roomlist.push(room.clone());
+
+ if state.currentroom.as_str().len() == 0 {
+ state.currentroom = room.clone();
+ }
+ }
+ }
+
+ self.full_redraw().await?;
+
+ let app = self.clone();
+ let room2 = room.clone();
+ task::spawn(async move {
+ if let Ok(Reply::List(members)) =
+ app.conn.send_command(Command::ListMembers { room_name: room2.clone() })
+ .await.unwrap() {
+ app.put_members(&room2, members).await.unwrap();
+ }
+ });
+ let app = self.clone();
+ let room2 = room.clone();
+ task::spawn(async move {
+ if let Ok(Reply::History(hist)) =
+ app.conn.send_command(Command::History { room_name: room2.clone(), count: 20 })
+ .await.unwrap() {
+ app.put_history(&room2, hist).await.unwrap();
+ }
+ });
+
+ Ok(())
+ }
+
+ async fn put_members(self: &Arc<Self>, room: &Word, ms: Vec<Word>) -> io::Result<()> {
+ debug!("put_members {} #{}", room, ms.len());
+ let mut state = self.state.lock().await;
+ state.rooms.get_mut(room).unwrap().members = ms;
+ state.full_redraw()
+ }
+
+ async fn add_member(self: &Arc<Self>, room: &Word, user: Word) -> io::Result<()> {
+ debug!("add_member {} {}", room, user);
+ let mut state = self.state.lock().await;
+ state.rooms.get_mut(room).unwrap().members.push(user);
+ state.full_redraw()
+ }
+
+ async fn put_history(self: &Arc<Self>, room: &Word, hist: Vec<Message>) -> io::Result<()> {
+ debug!("put_history {} #{}", room, hist.len());
+ let mut state = self.state.lock().await;
+ let ids = hist.iter().map(|m| m.id).collect::<Vec<_>>();
+ let replies = hist.iter().filter_map(|m| m.reply_on).collect::<Vec<_>>();
+
+ for msg in hist {
+ state.put_message(msg)?;
+ }
+
+ for replyid in replies {
+ if state.msgs.get(&replyid).is_none() {
+ let app = self.clone();
+ task::spawn(async move {
+ if let Ok(Reply::Message(msg2)) =
+ app.conn.send_command(Command::GetMessage(replyid)).await.unwrap() {
+ app.state.lock().await.put_message(msg2).unwrap();
+ }
+ });
+ }
+ }
+
+ for id in ids {
+ state.append_history(room, HItem::Message(id))?;
+ }
+
+ Ok(())
+ }
+
+ async fn append_history_item(self: &Arc<Self>, room: &Word, item: HItem) -> io::Result<()> {
+ let mut state = self.state.lock().await;
+ state.append_history(room, item)
+ }
+
+ async fn add_message(self: &Arc<Self>, msg: Message) -> io::Result<()> {
+ debug!("add_message {{id={} room={} user={}}}", msg.id, msg.roomname, msg.username);
+ let mut state = self.state.lock().await;
+
+ let roomname = msg.roomname.clone();
+ let msgid = msg.id;
+ let replyid = msg.reply_on;
+ state.put_message(msg)?;
+
+ if let Some(replyid) = replyid {
+ if state.msgs.get(&replyid).is_none() {
+ let app = self.clone();
+ task::spawn(async move {
+ if let Ok(Reply::Message(msg2)) =
+ app.conn.send_command(Command::GetMessage(replyid)).await.unwrap() {
+ app.state.lock().await.put_message(msg2).unwrap();
+ }
+ });
+ }
+ }
+
+ state.append_history(&roomname, HItem::Message(msgid))?;
+
+ Ok(())
+
+ }
+
+ async fn on_push(self: &Arc<Self>, pm: PushMessage) {
+ match pm {
+ PushMessage::Invite { room_name: room, inviter: _ } => {
+ self.enter_room(&room).await.unwrap();
+ }
+ PushMessage::Join { room_name: room, user } => {
+ self.add_member(&room, user.clone()).await.unwrap();
+ self.append_history_item(&room, HItem::Service(format!("Join: <{}>", user))).await.unwrap();
+ }
+ PushMessage::Message(msg) => {
+ self.add_message(msg).await.unwrap();
+ }
+ PushMessage::Online{ sessions: _, user: _ } => {}
+ }
+ }
+
+ async fn switch_buffer(self: &Arc<Self>, delta: isize) -> io::Result<()> {
+ {
+ let mut state = self.state.lock().await;
+ let current_index =
+ state.roomlist.iter().enumerate()
+ .filter(|&t| *t.1 == state.currentroom)
+ .map(|t| t.0)
+ .next().unwrap_or(0);
+ let index = (current_index as isize + delta)
+ .max(0).min(state.roomlist.len() as isize - 1);
+ state.currentroom = state.roomlist[index as usize].clone();
+ }
+ self.full_redraw().await
+ }
+
+ // Returns whether application should quit
+ async fn on_key(self: &Arc<Self>, key: Key) -> bool {
+ match key {
+ Key::Ctrl('c') => { return true; }
+ Key::Ctrl('l') => self.full_redraw().await.unwrap(),
+ Key::F(5) => self.switch_buffer(-1).await.unwrap(),
+ Key::F(6) => self.switch_buffer(1).await.unwrap(),
+ _ => debug!("{:?}", key)
+ }
+ false
+ }
+
+ async fn full_redraw(self: &Arc<Self>) -> io::Result<()> {
+ self.state.lock().await.full_redraw()
+ }
+}
+
+impl State {
+ fn full_redraw(&mut self) -> io::Result<()> {
+ self.termsize = termion::terminal_size()?;
+ self.layout = Layout::compute(self.termsize, &self);
+ print!("{}", clear::All);
+ draw_vert_bar(&mut self.stdout, self.layout.rlsepx, 0, self.termsize.1)?;
+ draw_vert_bar(&mut self.stdout, self.layout.nlsepx, 0, self.termsize.1 - 2)?;
+ draw_hori_bar(&mut self.stdout, self.termsize.1 - 2, self.layout.rlsepx + 1, self.termsize.0)?;
+
+ for (i, room) in self.roomlist.iter().enumerate() {
+ print!("{}", cursor::Goto(1, u16::try_from(i).unwrap() + 1));
+ if room == &self.currentroom {
+ print!("{}{}{}", color::Bg(color::Blue), room, color::Bg(color::Reset));
+ } else {
+ print!("{}", room);
+ }
+ }
+
+ if self.currentroom.as_str().len() > 0 {
+ let data = self.rooms.get(&self.currentroom).unwrap();
+
+ for (i, user) in data.members.iter().enumerate() {
+ print!("{}", cursor::Goto(self.layout.nlsepx + 2, u16::try_from(i).unwrap() + 1));
+ print!("{}", user);
+ }
+
+ let nicksepx = self.layout.rlsepx + u16::try_from(data.maxnicklen).unwrap() + 2;
+
+ draw_vert_bar(&mut self.stdout, nicksepx, 0, self.termsize.1 - 2)?;
+
+ let wid = self.layout.nlsepx - nicksepx - 2;
+ let mut y = self.termsize.1 - 3;
+ for item in data.history.iter().rev() {
+ let (prefix, text_to_format) = match item {
+ HItem::Message(id) => {
+ let msg = self.msgs.get(&id).unwrap();
+ (format!("<{}>", msg.username), msg.message.as_str())
+ }
+
+ HItem::Service(msg) => {
+ (String::from("--"), msg.as_str())
+ }
+ };
+
+ let lines = wrap_text(text_to_format, wid);
+ let mut done = false;
+ for (i, line) in lines.iter().enumerate().rev() {
+ if i == 0 {
+ print!("{}{}", cursor::Goto(self.layout.rlsepx + 2, y + 1), prefix);
+ }
+ print!("{}{}", cursor::Goto(nicksepx + 3, y + 1), line);
+ if y == 0 { done = true; break; }
+ y -= 1;
+ }
+ if done { break; }
+ }
+ }
+
+ print!("{}", cursor::Goto(self.layout.rlsepx + 2, self.termsize.1));
+
+ self.stdout.flush()
+ }
+
+ fn put_message(&mut self, msg: Message) -> io::Result<()> {
+ self.msgs.insert(msg.id, msg);
+ Ok(())
+ }
+
+ fn append_history(&mut self, room: &Word, item: HItem) -> io::Result<()> {
+ let mut data = self.rooms.get_mut(room).unwrap();
+ match &item {
+ HItem::Message(id) => {
+ let msg = self.msgs.get(&id).unwrap();
+ data.maxnicklen = data.maxnicklen.max(msg.username.as_str().len() + 2);
+ }
+ HItem::Service(_) => {
+ data.maxnicklen = data.maxnicklen.max(2);
+ }
+ }
+ self.rooms.get_mut(room).unwrap().history.push(item);
+ self.full_redraw()
+ }
+}
+
+async fn pushchan_thread(mut chan: mpsc::Receiver<PushMessage>, app: Arc<App>) {
+ loop {
+ match chan.next().await {
+ None => break,
+ Some(pm) => app.on_push(pm).await,
+ }
+ }
+}
+
+async fn async_main() -> io::Result<()> {
+ let addr = ("127.0.0.1", 29538);
+ let (conn, pushchan) = Connection::connect(connection::Type::Plain, addr).await?;
+
+ let user = Word::try_from(String::from("tom")).unwrap();
+ let pass = Line::try_from(String::from("kaas")).unwrap();
+
+ match conn.send_command(Command::Register { username: user.clone(), password: pass.clone() }).await? {
+ Ok(Reply::Ok) => {},
+ _ => {},
+ }
+
+ match conn.send_command(Command::Login { username: user, password: pass }).await? {
+ Ok(Reply::Ok) => {},
+ _ => {
+ eprintln!("Failed to login!");
+ return Ok(());
+ },
+ }
+
+ debug!("initializing");
+
+ let stdout = io::stdout().into_raw_mode()?;
+ let stdout = AlternateScreen::from(stdout);
+ let stdout = MouseTerminal::from(stdout);
+
+ let app = Arc::new(App::new(conn, stdout));
+
+ task::spawn(pushchan_thread(pushchan, app.clone()));
+
+ app.full_redraw().await?;
+
+ let app_clone = app.clone();
+ task::spawn(async move {
+ app_clone.fetch_rooms_list().await.unwrap();
+ });
+
+ for key in io::stdin().keys() {
+ if app.on_key(key?).await {
+ break;
+ }
+ }
+
+ Ok(())
+}
+
+fn main() -> io::Result<()> {
+ runtime::Runtime::new()?.block_on(async_main())
+}