diff options
author | Tom Smeding <tom.smeding@gmail.com> | 2020-09-13 17:58:51 +0200 |
---|---|---|
committer | Tom Smeding <tom.smeding@gmail.com> | 2021-01-28 22:20:12 +0100 |
commit | 5c7677e5fa134b60c7d4c29d3643125901a31fb8 (patch) | |
tree | 42bc74f3d2f257f9c013b986d4b5b421cd0fc361 /rust | |
parent | 7e169db30f3bc899c8708badf39d8ed268ef1968 (diff) |
WIP rust client
Diffstat (limited to 'rust')
-rw-r--r-- | rust/.gitignore | 3 | ||||
-rw-r--r-- | rust/Cargo.toml | 13 | ||||
-rw-r--r-- | rust/src/error.rs | 27 | ||||
-rw-r--r-- | rust/src/main.rs | 483 |
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()) +} |