aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2021-03-07 01:03:33 -0500
committerJesse Luehrs <doy@tozt.net>2021-03-07 01:03:33 -0500
commit8927ec8dafaaca3a14b55bb680b4f7f92fa1ed8b (patch)
tree2dfbdf496db8eac16e1b623809c0a5fdf8bd18ae
parentdbba3c9206f1e15b2b7a91066536efe57018831b (diff)
downloadtextmode-8927ec8dafaaca3a14b55bb680b4f7f92fa1ed8b.tar.gz
textmode-8927ec8dafaaca3a14b55bb680b4f7f92fa1ed8b.zip
a bunch more improvements
-rw-r--r--Cargo.toml2
-rw-r--r--examples/tmux.rs135
-rw-r--r--src/lib.rs16
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<usize, Window>,
current_window: usize,
next_window_id: usize,
+ notifications: std::collections::BTreeMap<usize, Notification>,
+ next_notification_id: usize,
wevents: smol::channel::Sender<Event>,
revents: smol::channel::Receiver<Event>,
}
@@ -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<Event>,
@@ -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(&notification.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 => {