diff options
author | Tom Smeding <tom.smeding@gmail.com> | 2020-09-14 23:02:00 +0200 |
---|---|---|
committer | Tom Smeding <tom.smeding@gmail.com> | 2021-01-28 22:20:12 +0100 |
commit | aba55fd071c6a8c7ef19e170dc54cd5a8a13854e (patch) | |
tree | bea18013d10acb559309b4492e4b0f4a6483d64b | |
parent | 5c7677e5fa134b60c7d4c29d3643125901a31fb8 (diff) |
rustclient: First prototype
-rw-r--r-- | rust/src/editor.rs | 81 | ||||
-rw-r--r-- | rust/src/main.rs | 138 |
2 files changed, 190 insertions, 29 deletions
diff --git a/rust/src/editor.rs b/rust/src/editor.rs new file mode 100644 index 0000000..7d83904 --- /dev/null +++ b/rust/src/editor.rs @@ -0,0 +1,81 @@ +use std::io::Write; +use termion::event::Key; + +#[derive(Debug, Default)] +pub struct Editor { + s: String, + left: usize, + cursor: usize, + wid: usize, +} + +impl Editor { + pub fn keypress(&mut self, key: Key) -> Option<String> { + let mut ret = None; + + match key { + Key::Char('\n') => { + ret = Some(String::new()); + std::mem::swap(ret.as_mut().unwrap(), &mut self.s); + self.cursor = 0; + } + + Key::Char(c) if !c.is_control() => { + self.s.insert(self.cursor, c); + self.cursor += 1; + } + + Key::Backspace if self.cursor > 0 => { + self.cursor -= 1; + self.s.remove(self.cursor); + } + + Key::Delete if self.s.len() > self.cursor => { + self.s.remove(self.cursor); + } + + Key::Home | Key::Ctrl('a') => { + self.cursor = 0; + } + + Key::End | Key::Ctrl('e') => { + self.cursor = self.s.len(); + } + + Key::Left | Key::Ctrl('b') if self.cursor > 0 => { + self.cursor -= 1; + } + + Key::Right | Key::Ctrl('f') if self.cursor < self.s.len() => { + self.cursor += 1; + } + + Key::Ctrl('u') => { + self.s.replace_range(0..self.cursor, ""); + self.cursor = 0; + } + + _ => { + print!("\x07"); + std::io::stdout().flush().unwrap(); + } + }; + + self.fix_left(); + ret + } + + pub fn set_wid(&mut self, wid: usize) { + self.wid = wid; + self.fix_left(); + } + + pub fn displayed(&self) -> (&str, usize) { + let endidx = (self.left + self.wid).min(self.s.len()); + (&self.s[self.left .. endidx], self.cursor - self.left) + } + + fn fix_left(&mut self) { + self.left = self.left.min(self.cursor).max(self.cursor + 1 - self.wid.min(self.cursor + 1)); + } +} diff --git a/rust/src/main.rs b/rust/src/main.rs index 2d08656..d2377c7 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -23,7 +23,10 @@ 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<Mutex<File>> = Lazy::new(|| { @@ -82,7 +85,17 @@ fn wrap_text(s: &str, maxwid: u16) -> Vec<String> { lines } +async fn send_command(conn: &Connection, cmd: Command) -> io::Result<Reply> { + 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<AlternateScreen<RawTerminal<io::Stdout>>>; +// type Stdout = MouseTerminal<io::Stdout>; #[derive(Debug, Copy, Clone)] struct Layout { @@ -108,6 +121,7 @@ impl Layout { struct App { conn: Connection, + username: Word, state: AsyncMutex<State>, } @@ -126,6 +140,7 @@ struct RoomData { members: Vec<Word>, history: Vec<HItem>, maxnicklen: usize, + editor: Editor, } #[derive(Debug)] @@ -135,7 +150,7 @@ enum HItem { } impl App { - fn new(conn: Connection, stdout: Stdout) -> Self { + fn new(conn: Connection, stdout: Stdout, username: Word) -> Self { let termsize = (0, 0); let state = State { stdout, @@ -148,13 +163,14 @@ impl App { }; App { conn, + username, 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? { + if let Reply::List(rooms) = send_command(&self.conn, Command::ListRooms).await? { debug!("fetched room list: {:?}", rooms); let newrooms = rooms.iter().collect::<HashSet<_>>(); @@ -198,8 +214,8 @@ impl App { 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() { + send_command(&app.conn, Command::ListMembers { room_name: room2.clone() }) + .await { app.put_members(&room2, members).await.unwrap(); } }); @@ -207,8 +223,8 @@ impl App { 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() { + send_command(&app.conn, Command::History { room_name: room2.clone(), count: 20 }) + .await { app.put_history(&room2, hist).await.unwrap(); } }); @@ -245,7 +261,7 @@ impl App { let app = self.clone(); task::spawn(async move { if let Ok(Reply::Message(msg2)) = - app.conn.send_command(Command::GetMessage(replyid)).await.unwrap() { + send_command(&app.conn, Command::GetMessage(replyid)).await { app.state.lock().await.put_message(msg2).unwrap(); } }); @@ -278,7 +294,7 @@ impl App { let app = self.clone(); task::spawn(async move { if let Ok(Reply::Message(msg2)) = - app.conn.send_command(Command::GetMessage(replyid)).await.unwrap() { + send_command(&app.conn, Command::GetMessage(replyid)).await { app.state.lock().await.put_message(msg2).unwrap(); } }); @@ -323,15 +339,76 @@ impl App { } // Returns whether application should quit - async fn on_key(self: &Arc<Self>, key: Key) -> bool { + async fn on_key(self: &Arc<Self>, key: Key) -> io::Result<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) + 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<Self>, line: &str) -> io::Result<bool> { + 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?; + } } - false + + Ok(false) } async fn full_redraw(self: &Arc<Self>) -> io::Result<()> { @@ -358,7 +435,7 @@ impl State { } if self.currentroom.as_str().len() > 0 { - let data = self.rooms.get(&self.currentroom).unwrap(); + 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)); @@ -395,9 +472,16 @@ impl State { } if done { break; } } - } - print!("{}", cursor::Goto(self.layout.rlsepx + 2, self.termsize.1)); + 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() } @@ -439,13 +523,10 @@ async fn async_main() -> io::Result<()> { 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) => {}, - _ => {}, - } + send_command(&conn, Command::Register { username: user.clone(), password: pass.clone() }).await?; - match conn.send_command(Command::Login { username: user, password: pass }).await? { - Ok(Reply::Ok) => {}, + match send_command(&conn, Command::Login { username: user.clone(), password: pass }).await? { + Reply::Ok => {}, _ => { eprintln!("Failed to login!"); return Ok(()); @@ -454,11 +535,10 @@ async fn async_main() -> io::Result<()> { debug!("initializing"); - let stdout = io::stdout().into_raw_mode()?; - let stdout = AlternateScreen::from(stdout); - let stdout = MouseTerminal::from(stdout); + 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)); + let app = Arc::new(App::new(conn, stdout, user)); task::spawn(pushchan_thread(pushchan, app.clone())); @@ -470,7 +550,7 @@ async fn async_main() -> io::Result<()> { }); for key in io::stdin().keys() { - if app.on_key(key?).await { + if app.on_key(key?).await? { break; } } |