From 9c92bf4f3ea77254aa31566feefc515a00b3059a Mon Sep 17 00:00:00 2001 From: Erica Marigold Date: Wed, 29 Jan 2025 14:21:50 +0000 Subject: [PATCH] feat: forward ssh keystroke data to tui Makes controls usable by taking SSH keystroke data (encoded in the xterm format) and converting them to crossterm `KeyCode`s / `KeyEvent`s and sending them to the `Tui`. Also adds support for suspension of the `Tui` over the SSH connection. Some minor refactoring was also done. --- .config/config.json5 | 1 + src/app.rs | 138 +++++++++++++++++++----------- src/keycode.rs | 171 +++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/ssh.rs | 88 +++++++++++++------ src/{tui.rs => tui/mod.rs} | 90 +++++++++++-------- src/tui/status.rs | 25 ++++++ 7 files changed, 401 insertions(+), 113 deletions(-) create mode 100644 src/keycode.rs rename src/{tui.rs => tui/mod.rs} (70%) create mode 100644 src/tui/status.rs diff --git a/.config/config.json5 b/.config/config.json5 index c746239..824f185 100644 --- a/.config/config.json5 +++ b/.config/config.json5 @@ -4,6 +4,7 @@ "": "Quit", // Quit the application "": "Quit", // Another way to quit "": "Quit", // Yet another way to quit + "": "Quit", // Final way to quit "": "Suspend" // Suspend the application }, } diff --git a/src/app.rs b/src/app.rs index 978df4b..5ce7670 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,16 +1,21 @@ use std::sync::Arc; -use color_eyre::Result; -use crossterm::event::KeyEvent; +use color_eyre::{eyre, Result}; +use crossterm::event::{KeyCode, KeyEvent}; use ratatui::prelude::Rect; use serde::{Deserialize, Serialize}; -use tokio::sync::{mpsc, Mutex}; +use tokio::{ + sync::{mpsc, Mutex, RwLock}, + task::block_in_place, +}; +use tokio_util::sync::CancellationToken; use tracing::{debug, info}; use crate::{ action::Action, components::{fps::FpsCounter, home::Home, Component}, config::Config, + keycode::KeyCodeExt, tui::{Event, Terminal, Tui}, }; @@ -25,6 +30,7 @@ pub struct App { last_tick_key_events: Vec, action_tx: mpsc::UnboundedSender, action_rx: mpsc::UnboundedReceiver, + ssh_rx: mpsc::UnboundedReceiver>, } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -34,7 +40,11 @@ pub enum Mode { } impl App { - pub fn new(tick_rate: f64, frame_rate: f64) -> Result { + pub fn new( + tick_rate: f64, + frame_rate: f64, + ssh_rx: mpsc::UnboundedReceiver>, + ) -> Result { let (action_tx, action_rx) = mpsc::unbounded_channel(); Ok(Self { tick_rate, @@ -50,74 +60,104 @@ impl App { last_tick_key_events: Vec::new(), action_tx, action_rx, + ssh_rx, }) } - pub async fn run(&mut self, term: Arc>) -> Result<()> { - let mut tui = Tui::new(term)? - // .mouse(true) // uncomment this line to enable mouse support - .tick_rate(self.tick_rate) - .frame_rate(self.frame_rate); - - tokio::task::block_in_place(|| tui.enter())?; + pub async fn run( + &mut self, + term: Arc>, + tui: Arc>>, + ) -> Result<()> { + let mut tui = tui.write().await; + let mut tui = tui.get_or_insert( + Tui::new(term)? + // .mouse(true) // uncomment this line to enable mouse support + .tick_rate(self.tick_rate) + .frame_rate(self.frame_rate), + ); - for component in self.components.iter_mut() { - component - .try_lock()? - .register_action_handler(self.action_tx.clone())?; - } - for component in self.components.iter_mut() { - component - .try_lock()? - .register_config_handler(self.config.clone())?; - } - for component in self.components.iter_mut() { - component - .try_lock()? - .init(tui.terminal.try_lock()?.size()?)?; - } + // Blocking initialization logic for tui and components + block_in_place(|| { + tui.enter()?; + + for component in self.components.iter_mut() { + component + .try_lock()? + .register_action_handler(self.action_tx.clone())?; + } + + for component in self.components.iter_mut() { + component + .try_lock()? + .register_config_handler(self.config.clone())?; + } + + for component in self.components.iter_mut() { + component + .try_lock()? + .init(tui.terminal.try_lock()?.size()?)?; + } + + Ok::<_, eyre::Error>(()) + })?; let action_tx = self.action_tx.clone(); + let mut resume_tx: Option> = None; loop { self.handle_events(&mut tui).await?; // self.handle_actions(&mut tui)?; - tokio::task::block_in_place(|| self.handle_actions(&mut tui))?; + block_in_place(|| self.handle_actions(&mut tui))?; if self.should_suspend { - tui.suspend()?; + if let Some(ref tx) = resume_tx { + tx.cancel(); + resume_tx = None; + } else { + resume_tx = Some(tui.suspend().await?); + continue + } action_tx.send(Action::Resume)?; action_tx.send(Action::ClearScreen)?; // tui.mouse(true); - tui.enter()?; + block_in_place(|| tui.enter())?; } else if self.should_quit { - tui.stop()?; + block_in_place(|| tui.stop())?; break; } } - tui.exit()?; - Ok(()) + + block_in_place(|| tui.exit()) } async fn handle_events(&mut self, tui: &mut Tui) -> Result<()> { - let Some(event) = tui.next_event().await else { - return Ok(()); - }; - let action_tx = self.action_tx.clone(); - match event { - Event::Quit => action_tx.send(Action::Quit)?, - Event::Tick => action_tx.send(Action::Tick)?, - Event::Render => action_tx.send(Action::Render)?, - Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?, - Event::Key(key) => self.handle_key_event(key)?, - _ => {} - } - for component in self.components.iter_mut() { - if let Some(action) = component.try_lock()?.handle_events(Some(event.clone()))? { - action_tx.send(action)?; + tokio::select! { + Some(event) = tui.next_event() => { + // Wait for next event and fire required actions for components + let action_tx = self.action_tx.clone(); + match event { + Event::Quit => action_tx.send(Action::Quit)?, + Event::Tick => action_tx.send(Action::Tick)?, + Event::Render => action_tx.send(Action::Render)?, + Event::Resize(x, y) => action_tx.send(Action::Resize(x, y))?, + Event::Key(key) => block_in_place(|| self.handle_key_event(key))?, + _ => {} + }; + + for component in self.components.iter_mut() { + let mut component = component.try_lock()?; + if let Some(action) = block_in_place(|| component.handle_events(Some(event.clone())))? { + action_tx.send(action)?; + } + } + } + Some(ssh_data) = self.ssh_rx.recv() => { + // Receive keystroke data from SSH connection + let key_event = KeyCode::from_xterm_seq(&ssh_data[..]).into_key_event(); + block_in_place(|| self.handle_key_event(key_event))?; } } Ok(()) } - fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> { let action_tx = self.action_tx.clone(); let Some(keymap) = self.config.keybindings.get(&self.mode) else { @@ -130,7 +170,7 @@ impl App { } _ => { // If the key was not handled as a single key action, - // then consider it for multi-key combinations. + // then consider it for multi-key combinations self.last_tick_key_events.push(key); // Check for multi-key combinations diff --git a/src/keycode.rs b/src/keycode.rs new file mode 100644 index 0000000..09c9298 --- /dev/null +++ b/src/keycode.rs @@ -0,0 +1,171 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +pub const CTRL_C: char = 3 as char; +pub const CTRL_D: char = 4 as char; +pub const CTRL_Z: char = 26 as char; + +pub trait KeyCodeExt { + /// Create a [KeyCode] from a keystroke byte. + fn from(code: u8) -> KeyCode; + /// Create a [KeyCode] from a xterm byte sequence of keystroke data. + fn from_xterm_seq(seq: &[u8]) -> KeyCode; + /// Consume the [KeyCode], converting it into a [KeyEvent]. + fn into_key_event(self) -> KeyEvent; +} + +impl KeyCodeExt for KeyCode { + fn from(code: u8) -> KeyCode { + match code { + // Control sequences (non exhaustive) + 3 => Self::Char(CTRL_C), + 4 => Self::Char(CTRL_D), + 26 => Self::Char(CTRL_Z), + + // Common control characters + 8 => KeyCode::Backspace, + 9 => KeyCode::Tab, + 13 => KeyCode::Enter, + 27 => KeyCode::Esc, + + // Arrow keys + 37 => KeyCode::Left, + 38 => KeyCode::Up, + 39 => KeyCode::Right, + 40 => KeyCode::Down, + + // Navigation keys + 33 => KeyCode::PageUp, + 34 => KeyCode::PageDown, + 36 => KeyCode::Home, + 35 => KeyCode::End, + 45 => KeyCode::Insert, + 46 => KeyCode::Delete, + + // Menu key + 93 => KeyCode::Menu, + + // Printable ASCII characters + b' '..=b'~' => KeyCode::Char(code as char), + + // Special characters + 127 => KeyCode::Backspace, + + // Caps/Num/Scroll Lock + 20 => KeyCode::CapsLock, + 144 => KeyCode::NumLock, + 145 => KeyCode::ScrollLock, + + // Pause/Break + 19 => KeyCode::Pause, + + // Anything else + 0 | _ => KeyCode::Null, + } + } + + #[rustfmt::skip] + fn from_xterm_seq(seq: &[u8]) -> Self { + let codes = seq + .iter() + .map(|&b| ::from(b)) + .collect::>(); + + match codes.as_slice() { + [Self::Esc, Self::Char('['), Self::Char('A')] => Self::Up, + [Self::Esc, Self::Char('['), Self::Char('B')] => Self::Down, + [Self::Esc, Self::Char('['), Self::Char('C')] => Self::Right, + [Self::Esc, Self::Char('['), Self::Char('D')] => Self::Left, + [Self::Esc, Self::Char('['), Self::Char('1'), Self::Char('~')] => Self::Home, + [Self::Esc, Self::Char('['), Self::Char('4'), Self::Char('~')] => Self::End, + [Self::Esc, Self::Char('['), Self::Char('3'), Self::Char('~')] => Self::Delete, + [Self::Esc, Self::Char('['), Self::Char('5'), Self::Char('~')] => Self::PageUp, + [Self::Esc, Self::Char('['), Self::Char('6'), Self::Char('~')] => Self::PageDown, + [Self::Esc, Self::Char('O'), Self::Char('P')] => Self::F(1), + [Self::Esc, Self::Char('O'), Self::Char('Q')] => Self::F(2), + [Self::Esc, Self::Char('O'), Self::Char('R')] => Self::F(3), + [Self::Esc, Self::Char('O'), Self::Char('S')] => Self::F(4), + [Self::Esc, Self::Char('['), Self::Char('1'), Self::Char('5'), Self::Char('~')] => Self::F(5), + [Self::Esc, Self::Char('['), Self::Char('1'), Self::Char('7'), Self::Char('~')] => Self::F(6), + [Self::Esc, Self::Char('['), Self::Char('1'), Self::Char('8'), Self::Char('~')] => Self::F(7), + [Self::Esc, Self::Char('['), Self::Char('1'), Self::Char('9'), Self::Char('~')] => Self::F(8), + [Self::Esc, Self::Char('['), Self::Char('2'), Self::Char('0'), Self::Char('~')] => Self::F(9), + [Self::Esc, Self::Char('['), Self::Char('2'), Self::Char('1'), Self::Char('~')] => Self::F(10), + [Self::Esc, Self::Char('['), Self::Char('2'), Self::Char('3'), Self::Char('~')] => Self::F(11), + [Self::Esc, Self::Char('['), Self::Char('2'), Self::Char('4'), Self::Char('~')] => Self::F(12), + [single] => *single, + _ => KeyCode::Null, + } + } + + fn into_key_event(self) -> KeyEvent { + match self { + Self::Char(CTRL_C) => KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), + Self::Char(CTRL_D) => KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL), + Self::Char(CTRL_Z) => KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL), + other => KeyEvent::new(other, KeyModifiers::empty()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_keycode_from_control_chars() { + assert_eq!(::from(8), KeyCode::Backspace); + assert_eq!(::from(9), KeyCode::Tab); + assert_eq!(::from(13), KeyCode::Enter); + assert_eq!(::from(27), KeyCode::Esc); + } + + #[test] + fn test_keycode_from_printable_ascii() { + assert_eq!(::from(b'a'), KeyCode::Char('a')); + assert_eq!(::from(b'Z'), KeyCode::Char('Z')); + assert_eq!(::from(b'0'), KeyCode::Char('0')); + assert_eq!(::from(b'~'), KeyCode::Char('~')); + } + + #[test] + fn test_keycode_from_special_keys() { + assert_eq!(::from(20), KeyCode::CapsLock); + assert_eq!(::from(144), KeyCode::NumLock); + assert_eq!(::from(145), KeyCode::ScrollLock); + assert_eq!(::from(19), KeyCode::Pause); + assert_eq!(::from(0), KeyCode::Null); + } + + #[test] + fn test_keycode_from_invalid() { + assert_eq!(::from(255), KeyCode::Null); + assert_eq!(::from(200), KeyCode::Null); + } + + #[test] + fn test_keycode_from_seq() { + assert_eq!(::from_xterm_seq(&[65]), KeyCode::Char('A')); + assert_eq!(::from_xterm_seq(&[27]), KeyCode::Esc); + assert_eq!(::from_xterm_seq(&[0]), KeyCode::Null); + + assert_eq!(::from_xterm_seq(&[27, 91, 68]), KeyCode::Left); + assert_eq!(::from_xterm_seq(&[27, 91, 67]), KeyCode::Right); + assert_eq!(::from_xterm_seq(&[27, 91, 65]), KeyCode::Up); + assert_eq!(::from_xterm_seq(&[27, 91, 66]), KeyCode::Down); + + assert_eq!(::from_xterm_seq(&[27, 91, 53, 126]), KeyCode::PageUp); + assert_eq!(::from_xterm_seq(&[27, 91, 54, 126]), KeyCode::PageDown); + assert_eq!(::from_xterm_seq(&[27, 91, 51, 126]), KeyCode::Delete); + assert_eq!(::from_xterm_seq(&[27, 91, 52, 126]), KeyCode::End); + + assert_eq!(::from_xterm_seq(&[27, 91, 49, 56, 126]), KeyCode::F(7)); + assert_eq!(::from_xterm_seq(&[27, 91, 49, 57, 126]), KeyCode::F(8)); + } + + #[test] + fn test_into_key_event() { + let key_code = KeyCode::Char('a'); + let key_event = ::into_key_event(key_code); + assert_eq!(key_event, KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())); + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 905f570..b06b04c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ mod errors; mod logging; mod ssh; mod tui; +mod keycode; const SOCKET_ADDR: LazyLock = LazyLock::new(|| SocketAddr::from(([127, 0, 0, 1], 2222))); pub static SSH_CONFIG: OnceLock> = OnceLock::new(); diff --git a/src/ssh.rs b/src/ssh.rs index bac7e79..6ccf7ac 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -1,15 +1,22 @@ use std::{io::Write, net::SocketAddr, sync::Arc}; use async_trait::async_trait; +use color_eyre::eyre::eyre; use ratatui::prelude::CrosstermBackend; use russh::{ server::{Auth, Handle, Handler, Msg, Server, Session}, - Channel, ChannelId, CryptoVec, Pty, + Channel, ChannelId, CryptoVec, Pty, +}; +use tokio::{ + runtime::Handle as TokioHandle, + sync::{mpsc, Mutex, RwLock}, }; -use tokio::{runtime::Handle as TokioHandle, sync::Mutex}; use tracing::instrument; -use crate::{app::App, tui::Terminal}; +use crate::{ + app::App, + tui::{Terminal, Tui}, +}; #[derive(Debug)] pub struct TermWriter { @@ -27,6 +34,19 @@ impl TermWriter { inner: CryptoVec::new(), } } + + fn flush_inner(&mut self) -> std::io::Result<()> { + let handle = TokioHandle::current(); + handle.block_on(async move { + self.session + .data(self.channel.id(), self.inner.clone()) + .await + .map_err(|err| { + std::io::Error::other(String::from_iter(err.iter().map(|item| *item as char))) + }) + .and_then(|()| Ok(self.inner.clear())) + }) + } } impl Write for TermWriter { @@ -38,32 +58,29 @@ impl Write for TermWriter { #[instrument(skip(self), level = "trace")] fn flush(&mut self) -> std::io::Result<()> { - let handle = TokioHandle::current(); - handle.block_on(async move { - self.session - .data(self.channel.id(), self.inner.clone()) - .await - .map_err(|err| { - std::io::Error::other(String::from_iter(err.iter().map(|item| *item as char))) - })?; - - self.inner.clear(); - Ok(()) - }) + tokio::task::block_in_place(|| self.flush_inner()) } } -pub struct SshSession(Option>>); +pub struct SshSession { + app: Option>>, + ssh_tx: mpsc::UnboundedSender>, + tui: Arc>>, +} unsafe impl Send for SshSession {} impl SshSession { pub fn new() -> Self { - Self( - App::new(10f64, 60f64) + let (ssh_tx, ssh_rx) = mpsc::unbounded_channel(); + + Self { + app: App::new(10f64, 60f64, ssh_rx) .ok() .map(|app| Arc::new(Mutex::new(app))), - ) + tui: Arc::new(RwLock::new(None)), + ssh_tx, + } } } @@ -82,23 +99,26 @@ impl Handler for SshSession { channel: Channel, session: &mut Session, ) -> Result { - if let Some(app) = &self.0 { + if let Some(app) = &self.app { + let session_handle = session.handle(); + let channel_id = channel.id(); + let inner_app = Arc::clone(app); let term = Terminal::new(CrosstermBackend::new(TermWriter::new(session, channel)))?; let writer = Arc::new(Mutex::new(term)); - + let tui = Arc::clone(&self.tui); + tokio::task::spawn(async move { - inner_app.lock_owned().await.run(writer).await.unwrap(); + inner_app.lock_owned().await.run(writer, tui).await.unwrap(); + session_handle.close(channel_id).await.unwrap(); + session_handle.exit_status_request(channel_id, 0).await.unwrap(); }); return Ok(true); } - return Err(color_eyre::eyre::eyre!( - "Failed to initialize App for session" - )); + Err(eyre!("Failed to initialize App for session")) } - #[instrument(skip(self, _session), level = "trace")] async fn pty_request( &mut self, @@ -111,10 +131,23 @@ impl Handler for SshSession { modes: &[(Pty, u32)], _session: &mut Session, ) -> Result<(), Self::Error> { - tracing::info!("received pty request from channel {channel_id}"); + tracing::info!("Received pty request from channel {channel_id}"); tracing::debug!("dims: {col_width} * {row_height}, pixel: {pix_width} * {pix_height}"); Ok(()) } + + #[instrument(skip(self, _session), level = "trace")] + async fn data( + &mut self, + _channel: ChannelId, + data: &[u8], + _session: &mut Session, + ) -> Result<(), Self::Error> { + tracing::debug!("Received keystroke data from SSH: {:?}, sending", data); + self.ssh_tx + .send(data.to_vec()) + .map_err(|_| eyre!("Failed to send event keystroke data")) + } } #[derive(Default)] @@ -127,7 +160,6 @@ impl Server for SshServer { #[instrument(skip(self))] fn new_client(&mut self, peer_addr: Option) -> Self::Handler { let session = SshSession::new(); - // self.0.push((peer_addr.unwrap(), session)); session } } diff --git a/src/tui.rs b/src/tui/mod.rs similarity index 70% rename from src/tui.rs rename to src/tui/mod.rs index fc6e605..c073bb4 100644 --- a/src/tui.rs +++ b/src/tui/mod.rs @@ -1,4 +1,4 @@ -#![allow(dead_code)] // Remove this once you start using the code +#![allow(dead_code)] // TODO: Remove this once you start using the code use std::{sync::Arc, time::Duration}; @@ -14,10 +14,11 @@ use crossterm::{ use futures::{FutureExt, StreamExt}; use ratatui::backend::CrosstermBackend as Backend; use serde::{Deserialize, Serialize}; +use status::TuiStatus; use tokio::{ sync::{ mpsc::{self, UnboundedReceiver, UnboundedSender}, - Mutex, + Mutex, RwLock, }, task::JoinHandle, time::interval, @@ -27,6 +28,8 @@ use tracing::error; use crate::ssh::TermWriter; +pub(crate) mod status; + #[derive(Clone, Debug, Serialize, Deserialize)] pub enum Event { Init, @@ -50,6 +53,8 @@ pub struct Tui { pub cancellation_token: CancellationToken, pub event_rx: UnboundedReceiver, pub event_tx: UnboundedSender, + pub resume_tx: Option>, + pub status: Arc>, pub frame_rate: f64, pub tick_rate: f64, pub mouse: bool, @@ -65,6 +70,8 @@ impl Tui { cancellation_token: CancellationToken::new(), event_rx, event_tx, + resume_tx: Option::None, + status: Arc::new(RwLock::new(TuiStatus::Active)), frame_rate: 60.0, tick_rate: 4.0, mouse: false, @@ -96,6 +103,7 @@ impl Tui { self.cancel(); // Cancel any existing task self.cancellation_token = CancellationToken::new(); let event_loop = Self::event_loop( + self.status.clone(), self.event_tx.clone(), self.cancellation_token.clone(), self.tick_rate, @@ -107,6 +115,7 @@ impl Tui { } async fn event_loop( + status: Arc>, event_tx: UnboundedSender, cancellation_token: CancellationToken, tick_rate: f64, @@ -120,27 +129,33 @@ impl Tui { event_tx .send(Event::Init) .expect("failed to send init event"); + + let suspension_status = Arc::clone(&status); loop { + let _guard = suspension_status.read().await; let event = tokio::select! { _ = cancellation_token.cancelled() => { break; } _ = tick_interval.tick() => Event::Tick, _ = render_interval.tick() => Event::Render, - crossterm_event = event_stream.next().fuse() => match crossterm_event { - Some(Ok(event)) => match event { - CrosstermEvent::Key(key) if key.kind == KeyEventKind::Press => Event::Key(key), - CrosstermEvent::Mouse(mouse) => Event::Mouse(mouse), - CrosstermEvent::Resize(x, y) => Event::Resize(x, y), - CrosstermEvent::FocusLost => Event::FocusLost, - CrosstermEvent::FocusGained => Event::FocusGained, - CrosstermEvent::Paste(s) => Event::Paste(s), - _ => continue, // ignore other events + crossterm_event = event_stream.next().fuse() => { + match crossterm_event { + Some(Ok(event)) => match event { + CrosstermEvent::Key(key) if key.kind == KeyEventKind::Press => Event::Key(key), + CrosstermEvent::Mouse(mouse) => Event::Mouse(mouse), + CrosstermEvent::Resize(x, y) => Event::Resize(x, y), + CrosstermEvent::FocusLost => Event::FocusLost, + CrosstermEvent::FocusGained => Event::FocusGained, + CrosstermEvent::Paste(s) => Event::Paste(s), + _ => continue, // ignore other events + } + Some(Err(_)) => Event::Error, + None => break, // the event stream has stopped and will not produce any more events } - Some(Err(_)) => Event::Error, - None => break, // the event stream has stopped and will not produce any more events }, }; + if event_tx.send(event).is_err() { // the receiver has been dropped, so there's no point in continuing the loop break; @@ -180,6 +195,7 @@ impl Tui { } drop(term); + self.start(); Ok(()) } @@ -209,16 +225,32 @@ impl Tui { self.cancellation_token.cancel(); } - pub fn suspend(&mut self) -> Result<()> { - self.exit()?; - #[cfg(not(windows))] - signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; - Ok(()) - } + pub async fn suspend(&mut self) -> Result> { + // Exit the current Tui + tokio::task::block_in_place(|| self.exit())?; - pub fn resume(&mut self) -> Result<()> { - self.enter()?; - Ok(()) + // Update the status and initialize a cancellation token + let token = Arc::new(CancellationToken::new()); + let suspension = Arc::new(Mutex::new(())); + *self.status.write().await = TuiStatus::Suspended(Arc::clone(&suspension)); + + // Spawn a task holding on the lock until a notification interrupts it + let status = Arc::clone(&self.status); + let lock_release = Arc::clone(&token); + tokio::task::spawn(async move { + tokio::select! { + _ = lock_release.cancelled() => { + // Lock was released, update the status + *status.write().await = TuiStatus::Active; + } + _ = tokio::time::sleep(tokio::time::Duration::from_secs(u64::MAX)) => { + // Hold on to the lock until notified + let _ = suspension.lock().await; + } + } + }); + + Ok(token) } pub async fn next_event(&mut self) -> Option { @@ -226,20 +258,6 @@ impl Tui { } } -// impl Deref for Tui { -// type Target = ratatui::Terminal>; - -// fn deref(&self) -> &Self::Target { -// self.terminal.as_ref() -// } -// } - -// impl DerefMut for Tui { -// fn deref_mut(&mut self) -> &mut Self::Target { -// self.terminal.get_mut().unwrap() -// } -// } - impl Drop for Tui { fn drop(&mut self) { self.exit().unwrap(); diff --git a/src/tui/status.rs b/src/tui/status.rs new file mode 100644 index 0000000..c84bd8c --- /dev/null +++ b/src/tui/status.rs @@ -0,0 +1,25 @@ +use std::{future::{Future, IntoFuture}, pin::Pin, sync::Arc}; + +use futures::future; +use tokio::sync::Mutex; + +#[derive(Debug, Clone)] +pub enum TuiStatus { + Active, + Suspended(Arc>), +} + +impl IntoFuture for TuiStatus { + type Output = (); + type IntoFuture = Pin>>; + + fn into_future(self) -> Self::IntoFuture { + if let Self::Suspended(lock) = self { + return Box::pin(async move { + let _guard = lock.lock().await; + }); + } + + Box::pin(future::ready(())) + } +} \ No newline at end of file