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.
This commit is contained in:
Erica Marigold 2025-01-29 14:21:50 +00:00
parent 15a6cfe4e2
commit 9c92bf4f3e
Signed by: DevComp
GPG key ID: 429EF1C337871656
7 changed files with 401 additions and 113 deletions

View file

@ -4,6 +4,7 @@
"<q>": "Quit", // Quit the application
"<Ctrl-d>": "Quit", // Another way to quit
"<Ctrl-c>": "Quit", // Yet another way to quit
"<Esc>": "Quit", // Final way to quit
"<Ctrl-z>": "Suspend" // Suspend the application
},
}

View file

@ -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<KeyEvent>,
action_tx: mpsc::UnboundedSender<Action>,
action_rx: mpsc::UnboundedReceiver<Action>,
ssh_rx: mpsc::UnboundedReceiver<Vec<u8>>,
}
#[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<Self> {
pub fn new(
tick_rate: f64,
frame_rate: f64,
ssh_rx: mpsc::UnboundedReceiver<Vec<u8>>,
) -> Result<Self> {
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<Mutex<Terminal>>) -> 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<Mutex<Terminal>>,
tui: Arc<RwLock<Option<Tui>>>,
) -> 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<Arc<CancellationToken>> = 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

171
src/keycode.rs Normal file
View file

@ -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| <Self as KeyCodeExt>::from(b))
.collect::<Vec<Self>>();
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!(<KeyCode as KeyCodeExt>::from(8), KeyCode::Backspace);
assert_eq!(<KeyCode as KeyCodeExt>::from(9), KeyCode::Tab);
assert_eq!(<KeyCode as KeyCodeExt>::from(13), KeyCode::Enter);
assert_eq!(<KeyCode as KeyCodeExt>::from(27), KeyCode::Esc);
}
#[test]
fn test_keycode_from_printable_ascii() {
assert_eq!(<KeyCode as KeyCodeExt>::from(b'a'), KeyCode::Char('a'));
assert_eq!(<KeyCode as KeyCodeExt>::from(b'Z'), KeyCode::Char('Z'));
assert_eq!(<KeyCode as KeyCodeExt>::from(b'0'), KeyCode::Char('0'));
assert_eq!(<KeyCode as KeyCodeExt>::from(b'~'), KeyCode::Char('~'));
}
#[test]
fn test_keycode_from_special_keys() {
assert_eq!(<KeyCode as KeyCodeExt>::from(20), KeyCode::CapsLock);
assert_eq!(<KeyCode as KeyCodeExt>::from(144), KeyCode::NumLock);
assert_eq!(<KeyCode as KeyCodeExt>::from(145), KeyCode::ScrollLock);
assert_eq!(<KeyCode as KeyCodeExt>::from(19), KeyCode::Pause);
assert_eq!(<KeyCode as KeyCodeExt>::from(0), KeyCode::Null);
}
#[test]
fn test_keycode_from_invalid() {
assert_eq!(<KeyCode as KeyCodeExt>::from(255), KeyCode::Null);
assert_eq!(<KeyCode as KeyCodeExt>::from(200), KeyCode::Null);
}
#[test]
fn test_keycode_from_seq() {
assert_eq!(<KeyCode as KeyCodeExt>::from_xterm_seq(&[65]), KeyCode::Char('A'));
assert_eq!(<KeyCode as KeyCodeExt>::from_xterm_seq(&[27]), KeyCode::Esc);
assert_eq!(<KeyCode as KeyCodeExt>::from_xterm_seq(&[0]), KeyCode::Null);
assert_eq!(<KeyCode as KeyCodeExt>::from_xterm_seq(&[27, 91, 68]), KeyCode::Left);
assert_eq!(<KeyCode as KeyCodeExt>::from_xterm_seq(&[27, 91, 67]), KeyCode::Right);
assert_eq!(<KeyCode as KeyCodeExt>::from_xterm_seq(&[27, 91, 65]), KeyCode::Up);
assert_eq!(<KeyCode as KeyCodeExt>::from_xterm_seq(&[27, 91, 66]), KeyCode::Down);
assert_eq!(<KeyCode as KeyCodeExt>::from_xterm_seq(&[27, 91, 53, 126]), KeyCode::PageUp);
assert_eq!(<KeyCode as KeyCodeExt>::from_xterm_seq(&[27, 91, 54, 126]), KeyCode::PageDown);
assert_eq!(<KeyCode as KeyCodeExt>::from_xterm_seq(&[27, 91, 51, 126]), KeyCode::Delete);
assert_eq!(<KeyCode as KeyCodeExt>::from_xterm_seq(&[27, 91, 52, 126]), KeyCode::End);
assert_eq!(<KeyCode as KeyCodeExt>::from_xterm_seq(&[27, 91, 49, 56, 126]), KeyCode::F(7));
assert_eq!(<KeyCode as KeyCodeExt>::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 = <KeyCode as KeyCodeExt>::into_key_event(key_code);
assert_eq!(key_event, KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty()));
}
}

View file

@ -14,6 +14,7 @@ mod errors;
mod logging;
mod ssh;
mod tui;
mod keycode;
const SOCKET_ADDR: LazyLock<SocketAddr> = LazyLock::new(|| SocketAddr::from(([127, 0, 0, 1], 2222)));
pub static SSH_CONFIG: OnceLock<Arc<Config>> = OnceLock::new();

View file

@ -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<Arc<Mutex<App>>>);
pub struct SshSession {
app: Option<Arc<Mutex<App>>>,
ssh_tx: mpsc::UnboundedSender<Vec<u8>>,
tui: Arc<RwLock<Option<Tui>>>,
}
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<Msg>,
session: &mut Session,
) -> Result<bool, Self::Error> {
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<SocketAddr>) -> Self::Handler {
let session = SshSession::new();
// self.0.push((peer_addr.unwrap(), session));
session
}
}

View file

@ -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<Event>,
pub event_tx: UnboundedSender<Event>,
pub resume_tx: Option<UnboundedSender<()>>,
pub status: Arc<RwLock<TuiStatus>>,
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<RwLock<TuiStatus>>,
event_tx: UnboundedSender<Event>,
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<Arc<CancellationToken>> {
// 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<Event> {
@ -226,20 +258,6 @@ impl Tui {
}
}
// impl Deref for Tui {
// type Target = ratatui::Terminal<Backend<TermWriter>>;
// 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();

25
src/tui/status.rs Normal file
View file

@ -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<Mutex<()>>),
}
impl IntoFuture for TuiStatus {
type Output = ();
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;
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(()))
}
}