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; use crate::editor::Editor; use crate::error::IntoIOError; mod editor; mod error; static DEBUG_FILE: Lazy> = 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(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(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 { 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 } async fn send_command(conn: &Connection, cmd: Command) -> io::Result { match conn.send_command(cmd).await { Ok(Ok(reply)) => Ok(reply), Ok(Err(connection::CloseReason::EOF)) => Err("EOF on server connection".ioerr()), Ok(Err(connection::CloseReason::Err(err))) => Err(format!("Error from server: {}", err).ioerr()), Err(err) => Err(err), } } type Stdout = MouseTerminal>>; // type Stdout = MouseTerminal; #[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, username: Word, state: AsyncMutex, } struct State { stdout: Stdout, termsize: (u16, u16), layout: Layout, roomlist: Vec, rooms: HashMap, currentroom: Word, msgs: HashMap, } #[derive(Debug, Default)] struct RoomData { members: Vec, history: Vec, maxnicklen: usize, editor: Editor, } #[derive(Debug)] enum HItem { Message(Id), Service(String), } impl App { fn new(conn: Connection, stdout: Stdout, username: Word) -> 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, username, state: AsyncMutex::new(state) } } async fn fetch_rooms_list(self: &Arc) -> io::Result<()> { debug!("fetch_rooms_list"); if let Reply::List(rooms) = send_command(&self.conn, Command::ListRooms).await? { debug!("fetched room list: {:?}", rooms); let newrooms = rooms.iter().collect::>(); { let mut state = self.state.lock().await; let toremove = state.roomlist.iter() .filter(|r| !newrooms.contains(r)) .cloned() .collect::>(); 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, 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)) = send_command(&app.conn, Command::ListMembers { room_name: room2.clone() }) .await { 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)) = send_command(&app.conn, Command::History { room_name: room2.clone(), count: 20 }) .await { app.put_history(&room2, hist).await.unwrap(); } }); Ok(()) } async fn put_members(self: &Arc, room: &Word, ms: Vec) -> 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, 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, room: &Word, hist: Vec) -> io::Result<()> { debug!("put_history {} #{}", room, hist.len()); let mut state = self.state.lock().await; let ids = hist.iter().map(|m| m.id).collect::>(); let replies = hist.iter().filter_map(|m| m.reply_on).collect::>(); 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)) = send_command(&app.conn, Command::GetMessage(replyid)).await { 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, room: &Word, item: HItem) -> io::Result<()> { let mut state = self.state.lock().await; state.append_history(room, item) } async fn add_message(self: &Arc, 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)) = send_command(&app.conn, Command::GetMessage(replyid)).await { app.state.lock().await.put_message(msg2).unwrap(); } }); } } state.append_history(&roomname, HItem::Message(msgid))?; Ok(()) } async fn on_push(self: &Arc, 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, 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, key: Key) -> io::Result { match key { Key::Ctrl('c') => { return Ok(true); } Key::Ctrl('l') => self.full_redraw().await?, Key::F(5) => self.switch_buffer(-1).await?, Key::F(6) => self.switch_buffer(1).await?, _ => { let submitted = { let mut state = self.state.lock().await; let state = &mut *state; let data = state.rooms.get_mut(&state.currentroom).unwrap(); data.editor.keypress(key) }; if let Some(submitted) = submitted { if self.handle_input(&submitted).await? { return Ok(true); } } self.full_redraw().await?; } } Ok(false) } // Returns whether application should quit async fn handle_input(self: &Arc, line: &str) -> io::Result { let parsed = if line.starts_with("//") { Err(&line[1..]) } else if line.starts_with("/") { match line.find(' ') { Some(idx) => Ok((&line[1..idx], &line[idx+1..])), None => Ok((&line[1..], "")) } } else { Err(line) }; match parsed { Err(msg) => { let msg = msg.to_string(); let app = self.clone(); task::spawn(async move { let room = app.state.lock().await.currentroom.clone(); let line = Line::try_from(msg).unwrap(); if let Ok(Reply::Number(id)) = send_command(&app.conn, Command::Send { room_name: room.clone(), reply_on: None, message: line.clone() }).await { app.add_message(Message { id: Id::try_from(id).unwrap(), reply_on: None, roomname: room, username: app.username.clone(), timestamp: std::time::SystemTime::now(), message: line, }).await.unwrap(); } }); } Ok(("quit", _)) | Ok(("exit", _)) => { return Ok(true); } Ok((cmd, _)) => { let room = self.state.lock().await.currentroom.clone(); self.append_history_item(&room, HItem::Service(format!("Unknown command: '{}'", cmd))).await?; } } Ok(false) } async fn full_redraw(self: &Arc) -> 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_mut(&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; } } data.editor.set_wid(usize::from(self.termsize.0 - self.layout.rlsepx - 1)); let (editor_str, editor_cursor) = data.editor.displayed(); let editor_cursor = u16::try_from(editor_cursor).unwrap(); print!("{}{}{}", cursor::Goto(self.layout.rlsepx + 2, self.termsize.1), editor_str, cursor::Goto(self.layout.rlsepx + 2 + editor_cursor, 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, app: Arc) { 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(); send_command(&conn, Command::Register { username: user.clone(), password: pass.clone() }).await?; match send_command(&conn, Command::Login { username: user.clone(), password: pass }).await? { Reply::Ok => {}, _ => { eprintln!("Failed to login!"); return Ok(()); }, } debug!("initializing"); let stdout = MouseTerminal::from(AlternateScreen::from(io::stdout().into_raw_mode()?)); // let stdout = MouseTerminal::from(io::stdout()); let app = Arc::new(App::new(conn, stdout, user)); 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()) }