diff options
author | Jesse Luehrs <doy@tozt.net> | 2021-03-13 14:39:14 -0500 |
---|---|---|
committer | Jesse Luehrs <doy@tozt.net> | 2021-03-13 14:39:14 -0500 |
commit | f0c07dfee8b65a36033943f888c1da5f8ec96098 (patch) | |
tree | 4382c61dc122c639b4cd062abdd42a43b2cc5555 | |
parent | 5244bfcd2b67ab6bf808750326de1e08e52c4f2f (diff) | |
download | textmode-f0c07dfee8b65a36033943f888c1da5f8ec96098.tar.gz textmode-f0c07dfee8b65a36033943f888c1da5f8ec96098.zip |
clean up examples to make a bare `cargo test` work
-rw-r--r-- | examples/async.rs | 32 | ||||
-rw-r--r-- | examples/basic.rs | 34 | ||||
-rw-r--r-- | examples/tmux.rs | 390 | ||||
-rw-r--r-- | examples/tmux_impl/mod.rs | 379 | ||||
-rw-r--r-- | tests/basic.rs | 2 | ||||
l--------- | tests/fixtures/bin/src/bin/async.rs | 1 |
6 files changed, 424 insertions, 414 deletions
diff --git a/examples/async.rs b/examples/async.rs deleted file mode 100644 index 52c1edb..0000000 --- a/examples/async.rs +++ /dev/null @@ -1,32 +0,0 @@ -use textmode::Textmode as _; - -async fn run( - tm: &mut textmode::Output, - input: &mut textmode::Input, -) -> textmode::Result<()> { - tm.move_to(5, 5); - tm.write_str("foo"); - input.read_key().await?; - tm.refresh().await?; - input.read_key().await?; - - tm.move_to(8, 8); - tm.set_fgcolor(textmode::color::GREEN); - tm.write_str("bar"); - tm.move_to(11, 11); - tm.set_fgcolor(vt100::Color::Default); - tm.write_str("baz"); - input.read_key().await?; - tm.refresh().await?; - input.read_key().await?; - Ok(()) -} - -fn main() { - smol::block_on(async { - let mut input = textmode::Input::new().await.unwrap(); - let mut tm = textmode::Output::new().await.unwrap(); - let e = run(&mut tm, &mut input).await; - e.unwrap(); - }); -} diff --git a/examples/basic.rs b/examples/basic.rs index 18d86a9..49f56bc 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,5 +1,39 @@ use textmode::Textmode as _; +#[cfg(feature = "async")] +async fn run( + tm: &mut textmode::Output, + input: &mut textmode::Input, +) -> textmode::Result<()> { + tm.move_to(5, 5); + tm.write_str("foo"); + input.read_key().await?; + tm.refresh().await?; + input.read_key().await?; + + tm.move_to(8, 8); + tm.set_fgcolor(textmode::color::GREEN); + tm.write_str("bar"); + tm.move_to(11, 11); + tm.set_fgcolor(vt100::Color::Default); + tm.write_str("baz"); + input.read_key().await?; + tm.refresh().await?; + input.read_key().await?; + Ok(()) +} + +#[cfg(feature = "async")] +fn main() { + smol::block_on(async { + let mut input = textmode::Input::new().await.unwrap(); + let mut tm = textmode::Output::new().await.unwrap(); + let e = run(&mut tm, &mut input).await; + e.unwrap(); + }); +} + +#[cfg(not(feature = "async"))] fn main() { let mut tm = textmode::blocking::Output::new().unwrap(); let mut input = textmode::blocking::Input::new().unwrap(); diff --git a/examples/tmux.rs b/examples/tmux.rs index c3e16b4..5eebee4 100644 --- a/examples/tmux.rs +++ b/examples/tmux.rs @@ -1,389 +1,19 @@ -use pty_process::Command as _; -use smol::io::{AsyncReadExt as _, AsyncWriteExt as _}; -use textmode::Textmode as _; - -enum Command { - NewWindow, - NextWindow, -} - -enum Event { - Input(textmode::Key), - Output, - WindowExit(usize), - Command(Command), - Notification, -} - -struct Window { - child: std::sync::Arc<pty_process::smol::Child>, - vt: std::sync::Arc<smol::lock::Mutex<vt100::Parser>>, - 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>, -} - -impl State { - fn new() -> Self { - let (sender, receiver) = smol::channel::unbounded(); - Self { - 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, - } - } - - fn current_window(&self) -> &Window { - &self.windows[&self.current_window] - } - - fn current_window_mut(&mut self) -> &mut Window { - self.windows.get_mut(&self.current_window).unwrap() - } - - fn next_window(&mut self, ex: &smol::Executor<'_>) { - self.current_window = self - .windows - .keys() - .copied() - .cycle() - .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<'_>, - mut input: textmode::Input, - ) { - let notify = self.wevents.clone(); - ex.spawn(async move { - let mut waiting_for_command = false; - input.parse_utf8(false); - input.parse_meta(false); - input.parse_special_keys(false); - loop { - input.parse_single(waiting_for_command); - match input.read_key().await { - Ok(Some(key)) => { - if waiting_for_command { - waiting_for_command = false; - match key { - textmode::Key::Ctrl(b'n') => { - notify - .send(Event::Input(key)) - .await - .unwrap(); - } - textmode::Key::Byte(b'c') => { - notify - .send(Event::Command( - Command::NewWindow, - )) - .await - .unwrap(); - } - textmode::Key::Byte(b'n') => { - notify - .send(Event::Command( - Command::NextWindow, - )) - .await - .unwrap(); - } - _ => {} // ignore - } - } else { - match key { - textmode::Key::Ctrl(b'n') => { - waiting_for_command = true; - } - _ => { - notify - .send(Event::Input(key)) - .await - .unwrap(); - } - } - } - } - Ok(None) => { - break; - } - Err(e) => { - eprintln!("{}", e); - break; - } - } - } - }) - .detach(); - } - - fn new_window( - &mut self, - ex: &smol::Executor<'_>, - notify: smol::channel::Sender<Event>, - ) { - let child = smol::process::Command::new("zsh") - .spawn_pty(Some(&pty_process::Size::new(24, 80))) - .unwrap(); - let child = std::sync::Arc::new(child); - let vt = vt100::Parser::default(); - let screen = vt.screen().clone(); - let vt = std::sync::Arc::new(smol::lock::Mutex::new(vt)); - let id = self.next_window_id; - self.next_window_id += 1; - let window = Window { - child: child.clone(), - vt: vt.clone(), - screen, - }; - 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 { - match child.pty().read(&mut buf).await { - Ok(bytes) => { - vt.lock_arc().await.process(&buf[..bytes]); - notify.send(Event::Output).await.unwrap(); - } - Err(e) => { - // EIO means that the process closed the other - // end of the pty - if e.raw_os_error() != Some(libc::EIO) { - eprintln!("pty read failed: {:?}", e); - } - notify.send(Event::WindowExit(id)).await.unwrap(); - break; - } - } - } - }) - .detach(); - } - - async fn redraw_current_window(&mut self, tm: &mut textmode::Output) { - let window = self.current_window(); - tm.clear(); - let new_screen = window.vt.lock_arc().await.screen().clone(); - tm.write(&new_screen.state_formatted()); - self.draw_notifications(tm, &new_screen); - tm.refresh().await.unwrap(); - } - - async fn update_current_window(&mut self, tm: &mut textmode::Output) { - 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.state_diff(&old_screen); - self.clear_notifications(tm, &old_screen); - tm.write(&diff); - self.draw_notifications(tm, &new_screen); - tm.refresh().await.unwrap(); - self.current_window_mut().screen = new_screen; - } - - fn clear_notifications( - &mut self, - tm: &mut textmode::Output, - 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::Output, - 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); - } -} - -#[must_use] -struct Tmux { - input: textmode::Input, - tm: textmode::Output, - state: State, -} - -impl Tmux { - async fn new() -> Self { - let input = textmode::Input::new().await.unwrap(); - let tm = textmode::Output::new().await.unwrap(); - let state = State::new(); - Self { input, tm, state } - } - - async fn run(self, ex: &smol::Executor<'_>) { - let Self { - input, - mut tm, - mut state, - } = self; - - state.spawn_input_task(ex, input); - - ex.run(async { - state.new_window(ex, state.wevents.clone()); - - loop { - match state.revents.recv().await { - Ok(Event::Output) => { - state.update_current_window(&mut tm).await; - } - Ok(Event::Input(key)) => { - state - .current_window() - .child - .pty() - .write_all(&key.into_bytes()) - .await - .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 - // holding the other copy of child has already been - // dropped - std::sync::Arc::get_mut(&mut dropped_window.child) - .unwrap() - .status() - .await - .unwrap(); - if state.windows.is_empty() { - break; - } - 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()); - state.redraw_current_window(&mut tm).await; - } - Command::NextWindow => { - 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; - } - } - } - }) - .await; - } -} +#[cfg(feature = "async")] +mod tmux_impl; +#[cfg(feature = "async")] async fn async_main(ex: &smol::Executor<'_>) { - let tmux = Tmux::new().await; + let tmux = tmux_impl::Tmux::new().await; tmux.run(ex).await; } +#[cfg(feature = "async")] fn main() { let ex = smol::Executor::new(); smol::block_on(async { async_main(&ex).await }) } + +#[cfg(not(feature = "async"))] +fn main() { + panic!("tmux example requires feature async") +} diff --git a/examples/tmux_impl/mod.rs b/examples/tmux_impl/mod.rs new file mode 100644 index 0000000..de026e4 --- /dev/null +++ b/examples/tmux_impl/mod.rs @@ -0,0 +1,379 @@ +use pty_process::Command as _; +use smol::io::{AsyncReadExt as _, AsyncWriteExt as _}; +use textmode::Textmode as _; + +enum Command { + NewWindow, + NextWindow, +} + +enum Event { + Input(textmode::Key), + Output, + WindowExit(usize), + Command(Command), + Notification, +} + +struct Window { + child: std::sync::Arc<pty_process::smol::Child>, + vt: std::sync::Arc<smol::lock::Mutex<vt100::Parser>>, + 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>, +} + +impl State { + fn new() -> Self { + let (sender, receiver) = smol::channel::unbounded(); + Self { + 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, + } + } + + fn current_window(&self) -> &Window { + &self.windows[&self.current_window] + } + + fn current_window_mut(&mut self) -> &mut Window { + self.windows.get_mut(&self.current_window).unwrap() + } + + fn next_window(&mut self, ex: &smol::Executor<'_>) { + self.current_window = self + .windows + .keys() + .copied() + .cycle() + .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<'_>, + mut input: textmode::Input, + ) { + let notify = self.wevents.clone(); + ex.spawn(async move { + let mut waiting_for_command = false; + input.parse_utf8(false); + input.parse_meta(false); + input.parse_special_keys(false); + loop { + input.parse_single(waiting_for_command); + match input.read_key().await { + Ok(Some(key)) => { + if waiting_for_command { + waiting_for_command = false; + match key { + textmode::Key::Ctrl(b'n') => { + notify + .send(Event::Input(key)) + .await + .unwrap(); + } + textmode::Key::Byte(b'c') => { + notify + .send(Event::Command( + Command::NewWindow, + )) + .await + .unwrap(); + } + textmode::Key::Byte(b'n') => { + notify + .send(Event::Command( + Command::NextWindow, + )) + .await + .unwrap(); + } + _ => {} // ignore + } + } else { + match key { + textmode::Key::Ctrl(b'n') => { + waiting_for_command = true; + } + _ => { + notify + .send(Event::Input(key)) + .await + .unwrap(); + } + } + } + } + Ok(None) => { + break; + } + Err(e) => { + eprintln!("{}", e); + break; + } + } + } + }) + .detach(); + } + + fn new_window( + &mut self, + ex: &smol::Executor<'_>, + notify: smol::channel::Sender<Event>, + ) { + let child = smol::process::Command::new("zsh") + .spawn_pty(Some(&pty_process::Size::new(24, 80))) + .unwrap(); + let child = std::sync::Arc::new(child); + let vt = vt100::Parser::default(); + let screen = vt.screen().clone(); + let vt = std::sync::Arc::new(smol::lock::Mutex::new(vt)); + let id = self.next_window_id; + self.next_window_id += 1; + let window = Window { + child: child.clone(), + vt: vt.clone(), + screen, + }; + 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 { + match child.pty().read(&mut buf).await { + Ok(bytes) => { + vt.lock_arc().await.process(&buf[..bytes]); + notify.send(Event::Output).await.unwrap(); + } + Err(e) => { + // EIO means that the process closed the other + // end of the pty + if e.raw_os_error() != Some(libc::EIO) { + eprintln!("pty read failed: {:?}", e); + } + notify.send(Event::WindowExit(id)).await.unwrap(); + break; + } + } + } + }) + .detach(); + } + + async fn redraw_current_window(&mut self, tm: &mut textmode::Output) { + let window = self.current_window(); + tm.clear(); + let new_screen = window.vt.lock_arc().await.screen().clone(); + tm.write(&new_screen.state_formatted()); + self.draw_notifications(tm, &new_screen); + tm.refresh().await.unwrap(); + } + + async fn update_current_window(&mut self, tm: &mut textmode::Output) { + 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.state_diff(&old_screen); + self.clear_notifications(tm, &old_screen); + tm.write(&diff); + self.draw_notifications(tm, &new_screen); + tm.refresh().await.unwrap(); + self.current_window_mut().screen = new_screen; + } + + fn clear_notifications( + &mut self, + tm: &mut textmode::Output, + 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::Output, + 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); + } +} + +#[must_use] +pub struct Tmux { + input: textmode::Input, + tm: textmode::Output, + state: State, +} + +impl Tmux { + pub async fn new() -> Self { + let input = textmode::Input::new().await.unwrap(); + let tm = textmode::Output::new().await.unwrap(); + let state = State::new(); + Self { input, tm, state } + } + + pub async fn run(self, ex: &smol::Executor<'_>) { + let Self { + input, + mut tm, + mut state, + } = self; + + state.spawn_input_task(ex, input); + + ex.run(async { + state.new_window(ex, state.wevents.clone()); + + loop { + match state.revents.recv().await { + Ok(Event::Output) => { + state.update_current_window(&mut tm).await; + } + Ok(Event::Input(key)) => { + state + .current_window() + .child + .pty() + .write_all(&key.into_bytes()) + .await + .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 + // holding the other copy of child has already been + // dropped + std::sync::Arc::get_mut(&mut dropped_window.child) + .unwrap() + .status() + .await + .unwrap(); + if state.windows.is_empty() { + break; + } + 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()); + state.redraw_current_window(&mut tm).await; + } + Command::NextWindow => { + 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; + } + } + } + }) + .await; + } +} diff --git a/tests/basic.rs b/tests/basic.rs index c18477b..e0856fb 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -25,7 +25,7 @@ fn test_basic() { #[test] fn test_async() { - let mut fixture = fixtures::Fixture::new("async"); + let mut fixture = fixtures::Fixture::new("basic"); fixture.features("async"); fixture.build().run(&[], |pty| { pty.write_all(b"a").unwrap(); diff --git a/tests/fixtures/bin/src/bin/async.rs b/tests/fixtures/bin/src/bin/async.rs deleted file mode 120000 index ff890ad..0000000 --- a/tests/fixtures/bin/src/bin/async.rs +++ /dev/null @@ -1 +0,0 @@ -../../../../../examples/async.rs
\ No newline at end of file |