From 8927ec8dafaaca3a14b55bb680b4f7f92fa1ed8b Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 7 Mar 2021 01:03:33 -0500 Subject: a bunch more improvements --- Cargo.toml | 2 +- examples/tmux.rs | 135 ++++++++++++++++++++++++++++++++++++++++++++++++------- src/lib.rs | 16 +++++-- 3 files changed, 133 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e692a4a..544d2a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" [dependencies] itoa = "0.4" terminal_size = "0.1" -vt100 = "0.10" +vt100 = "0.11" blocking = { version = "1.0", optional = true } futures-lite = { version = "1.11", optional = true } diff --git a/examples/tmux.rs b/examples/tmux.rs index eaff4c4..e6cd21e 100644 --- a/examples/tmux.rs +++ b/examples/tmux.rs @@ -45,6 +45,7 @@ enum Event { Output, WindowExit(usize), Command(Command), + Notification, } struct Window { @@ -53,10 +54,18 @@ struct Window { screen: vt100::Screen, } +#[derive(Clone)] +struct Notification { + text: String, + expiry: std::time::Instant, +} + struct State { windows: std::collections::BTreeMap, current_window: usize, next_window_id: usize, + notifications: std::collections::BTreeMap, + next_notification_id: usize, wevents: smol::channel::Sender, revents: smol::channel::Receiver, } @@ -68,6 +77,8 @@ impl State { windows: std::collections::BTreeMap::new(), current_window: 0, next_window_id: 0, + notifications: std::collections::BTreeMap::new(), + next_notification_id: 0, wevents: sender, revents: receiver, } @@ -81,7 +92,7 @@ impl State { self.windows.get_mut(&self.current_window).unwrap() } - fn next_window(&mut self) { + fn next_window(&mut self, ex: &smol::Executor<'_>) { self.current_window = self .windows .keys() @@ -90,6 +101,26 @@ impl State { .skip_while(|&id| id < self.current_window) .nth(1) .unwrap(); + self.notify( + ex, + &format!("switched to window {}", self.current_window), + ); + } + + fn notify(&mut self, ex: &smol::Executor<'_>, text: &str) { + let now = std::time::Instant::now(); + let expiry = now + std::time::Duration::from_secs(5); + let text = text.to_string(); + let notification = Notification { text, expiry }; + let id = self.next_notification_id; + self.next_notification_id += 1; + self.notifications.insert(id, notification); + let notify = self.wevents.clone(); + ex.spawn(async move { + smol::Timer::at(expiry).await; + notify.send(Event::Notification).await.unwrap(); + }) + .detach(); } fn spawn_input_task(&self, ex: &smol::Executor<'_>) { @@ -118,7 +149,7 @@ impl State { .detach(); } - async fn new_window( + fn new_window( &mut self, ex: &smol::Executor<'_>, notify: smol::channel::Sender, @@ -139,6 +170,7 @@ impl State { }; self.windows.insert(id, window); self.current_window = id; + self.notify(ex, &format!("created window {}", id)); ex.spawn(async move { let mut buf = [0_u8; 4096]; loop { @@ -218,20 +250,89 @@ impl State { return waiting_for_command; } - async fn redraw_current_window(&self, tm: &mut textmode::Textmode) { + async fn redraw_current_window(&mut self, tm: &mut textmode::Textmode) { let window = self.current_window(); tm.clear(); - tm.write(&window.vt.lock_arc().await.screen().contents_formatted()); + let new_screen = window.vt.lock_arc().await.screen().clone(); + tm.write(&new_screen.contents_formatted()); + self.draw_notifications(tm, &new_screen); tm.refresh().await.unwrap(); } async fn update_current_window(&mut self, tm: &mut textmode::Textmode) { - let window = self.current_window_mut(); + let window = self.current_window(); + let old_screen = window.screen.clone(); let new_screen = window.vt.lock_arc().await.screen().clone(); - let diff = new_screen.contents_diff(&window.screen); + let diff = new_screen.contents_diff(&old_screen); + self.clear_notifications(tm, &old_screen); tm.write(&diff); + self.draw_notifications(tm, &new_screen); tm.refresh().await.unwrap(); - window.screen = new_screen; + self.current_window_mut().screen = new_screen; + } + + fn clear_notifications( + &mut self, + tm: &mut textmode::Textmode, + screen: &vt100::Screen, + ) { + if self.notifications.is_empty() { + return; + } + + let reset_attrs = screen.attributes_formatted(); + let pos = screen.cursor_position(); + for (i, row) in screen + .rows_formatted(0, 80) + .enumerate() + .take(self.notifications.len()) + { + tm.move_to(i as u16, 0); + tm.reset_attributes(); + tm.clear_line(); + tm.write(&row); + } + tm.move_to(pos.0, pos.1); + tm.write(&reset_attrs); + } + + fn draw_notifications( + &mut self, + tm: &mut textmode::Textmode, + screen: &vt100::Screen, + ) { + if self.notifications.is_empty() { + return; + } + + let now = std::time::Instant::now(); + self.notifications = self + .notifications + .iter() + .map(|(k, v)| (*k, v.clone())) + .filter(|(_, v)| v.expiry >= now) + .collect(); + + if self.notifications.is_empty() { + return; + } + + let reset_attrs = screen.attributes_formatted(); + let pos = screen.cursor_position(); + tm.reset_attributes(); + tm.set_bgcolor(textmode::color::CYAN); + tm.set_fgcolor(textmode::color::WHITE); + for (i, notification) in self.notifications.values().enumerate() { + tm.move_to(i as u16, 0); + tm.clear_line(); + let str_len = notification.text.len(); + let spaces = 80 - str_len; + let prefix_spaces = spaces / 2; + tm.write(&vec![b' '; prefix_spaces]); + tm.write_str(¬ification.text); + } + tm.move_to(pos.0, pos.1); + tm.write(&reset_attrs); } } @@ -257,7 +358,7 @@ impl Tmux { mut state, } = self; - state.new_window(ex, state.wevents.clone()).await; + state.new_window(ex, state.wevents.clone()); state.spawn_input_task(ex); ex.run(async { @@ -276,6 +377,11 @@ impl Tmux { .unwrap(); } Ok(Event::WindowExit(id)) => { + // do this first because next_window breaks if + // current_window is greater than all existing windows + if state.current_window == id { + state.next_window(ex) + } let mut dropped_window = state.windows.remove(&id).unwrap(); // i can get_mut because at this point the future @@ -289,24 +395,23 @@ impl Tmux { if state.windows.is_empty() { break; } - if state.current_window == id { - state.next_window() - } + state.notify(ex, &format!("window {} exited", id)); state.redraw_current_window(&mut tm).await; } Ok(Event::Command(c)) => match c { Command::NewWindow => { - state - .new_window(&ex, state.wevents.clone()) - .await; + state.new_window(&ex, state.wevents.clone()); state.redraw_current_window(&mut tm).await; } Command::NextWindow => { - state.next_window(); + state.next_window(ex); state.redraw_current_window(&mut tm).await; } }, + Ok(Event::Notification) => { + state.update_current_window(&mut tm).await; + } Err(e) => { eprintln!("{}", e); break; diff --git a/src/lib.rs b/src/lib.rs index 4b09880..7f9aac4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,8 +27,8 @@ mod private { } pub trait TextmodeExt: private::TextmodeImpl { - fn cursor_position(&self) -> (u16, u16) { - self.next().screen().cursor_position() + fn screen(&self) -> &vt100::Screen { + self.next().screen() } fn write(&mut self, buf: &[u8]) { @@ -46,9 +46,9 @@ pub trait TextmodeExt: private::TextmodeImpl { fn move_to(&mut self, row: u16, col: u16) { self.write(b"\x1b["); - self.write_u16(row); + self.write_u16(row + 1); self.write(b";"); - self.write_u16(col); + self.write_u16(col + 1); self.write(b"H"); } @@ -56,6 +56,14 @@ pub trait TextmodeExt: private::TextmodeImpl { self.write(b"\x1b[2J"); } + fn clear_line(&mut self) { + self.write(b"\x1b[K"); + } + + fn reset_attributes(&mut self) { + self.write(b"\x1b[m"); + } + fn set_fgcolor(&mut self, color: vt100::Color) { match color { vt100::Color::Default => { -- cgit v1.2.3-54-g00ecf