diff options
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()) +}  | 
