aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Smeding <tom.smeding@gmail.com>2020-09-14 23:02:00 +0200
committerTom Smeding <tom.smeding@gmail.com>2021-01-28 22:20:12 +0100
commitaba55fd071c6a8c7ef19e170dc54cd5a8a13854e (patch)
treebea18013d10acb559309b4492e4b0f4a6483d64b
parent5c7677e5fa134b60c7d4c29d3643125901a31fb8 (diff)
rustclient: First prototype
-rw-r--r--rust/src/editor.rs81
-rw-r--r--rust/src/main.rs138
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;
}
}