Compare commits
3 commits
15a6cfe4e2
...
1f72560438
Author | SHA1 | Date | |
---|---|---|---|
1f72560438 | |||
d22b1c3a34 | |||
9c92bf4f3e |
7 changed files with 398 additions and 121 deletions
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
138
src/app.rs
138
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<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
171
src/keycode.rs
Normal 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()));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
100
src/ssh.rs
100
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<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,24 +99,27 @@ 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")]
|
||||
#[instrument(skip(self, session), level = "trace")]
|
||||
async fn pty_request(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
|
@ -109,11 +129,30 @@ impl Handler for SshSession {
|
|||
pix_width: u32,
|
||||
pix_height: u32,
|
||||
modes: &[(Pty, u32)],
|
||||
session: &mut Session,
|
||||
) -> Result<(), Self::Error> {
|
||||
tracing::info!("Received pty request from channel {channel_id}; terminal: {term}");
|
||||
tracing::debug!("dims: {col_width} * {row_height}, pixel: {pix_width} * {pix_height}");
|
||||
|
||||
if !term.contains("xterm") {
|
||||
session.channel_failure(channel_id)?;
|
||||
return Err(eyre!("Unsupported terminal type: {term}"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self, _session), level = "trace")]
|
||||
async fn data(
|
||||
&mut self,
|
||||
_channel: ChannelId,
|
||||
data: &[u8],
|
||||
_session: &mut Session,
|
||||
) -> Result<(), Self::Error> {
|
||||
tracing::info!("received pty request from channel {channel_id}");
|
||||
tracing::debug!("dims: {col_width} * {row_height}, pixel: {pix_width} * {pix_height}");
|
||||
Ok(())
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,7 +166,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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
@ -7,17 +7,17 @@ use crossterm::{
|
|||
cursor,
|
||||
event::{
|
||||
DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
|
||||
Event as CrosstermEvent, EventStream, KeyEvent, KeyEventKind, MouseEvent,
|
||||
KeyEvent, MouseEvent,
|
||||
},
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
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 +27,8 @@ use tracing::error;
|
|||
|
||||
use crate::ssh::TermWriter;
|
||||
|
||||
pub(crate) mod status;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum Event {
|
||||
Init,
|
||||
|
@ -50,6 +52,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 +69,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 +102,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,12 +114,12 @@ impl Tui {
|
|||
}
|
||||
|
||||
async fn event_loop(
|
||||
status: Arc<RwLock<TuiStatus>>,
|
||||
event_tx: UnboundedSender<Event>,
|
||||
cancellation_token: CancellationToken,
|
||||
tick_rate: f64,
|
||||
frame_rate: f64,
|
||||
) {
|
||||
let mut event_stream = EventStream::new();
|
||||
let mut tick_interval = interval(Duration::from_secs_f64(1.0 / tick_rate));
|
||||
let mut render_interval = interval(Duration::from_secs_f64(1.0 / frame_rate));
|
||||
|
||||
|
@ -120,27 +127,18 @@ 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
|
||||
}
|
||||
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 +178,7 @@ impl Tui {
|
|||
}
|
||||
|
||||
drop(term);
|
||||
|
||||
self.start();
|
||||
Ok(())
|
||||
}
|
||||
|
@ -209,16 +208,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 +241,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
25
src/tui/status.rs
Normal 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(()))
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue