diff --git a/src/keys.rs b/src/keys.rs new file mode 100644 index 0000000..eacbb7d --- /dev/null +++ b/src/keys.rs @@ -0,0 +1,22 @@ +#[derive(Debug)] +pub enum Input { + Char(char), + Up, + Down, + Esc, + Enter, + Other, +} + +impl From<&[u8]> for Input { + fn from(value: &[u8]) -> Self { + match value { + [c] if c.is_ascii_graphic() => Input::Char(*c as char), + [27] => Input::Esc, + [27, 91, 65] => Input::Up, + [27, 91, 66] => Input::Down, + [13] => Input::Enter, + _ => Input::Other, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 3427ef4..2bfeb80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod animals; pub mod auth; pub mod helper; +pub mod keys; pub mod ssh; pub mod terminal; pub mod tui; diff --git a/src/ssh.rs b/src/ssh.rs index 868707c..11e12ab 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -12,6 +12,7 @@ use tokio::net::ToSocketAddrs; use tracing::{debug, trace, warn}; use crate::{ + keys::Input, terminal::TerminalHandle, tui::Renderer, tunnel::{Tunnel, TunnelAccess, Tunnels}, @@ -25,6 +26,7 @@ pub struct Handler { terminal: Option>>, renderer: Renderer, + selected: Option, } impl Handler { @@ -33,12 +35,13 @@ impl Handler { all_tunnels, tunnels: IndexMap::new(), user: None, - terminal: Default::default(), + terminal: None, renderer: Default::default(), + selected: None, } } - async fn set_access(&mut self, access: TunnelAccess) { + async fn set_access_all(&mut self, access: TunnelAccess) { for (_address, tunnel) in &self.tunnels { if let Some(tunnel) = tunnel { tunnel.set_access(access.clone()).await; @@ -46,7 +49,7 @@ impl Handler { } } - pub async fn resize(&mut self, width: u32, height: u32) -> std::io::Result<()> { + async fn resize(&mut self, width: u32, height: u32) -> std::io::Result<()> { let rect = Rect { x: 0, y: 0, @@ -73,10 +76,11 @@ impl Handler { Ok(()) } - pub async fn redraw(&mut self) -> std::io::Result<()> { + async fn redraw(&mut self) -> std::io::Result<()> { if let Some(terminal) = &mut self.terminal { trace!("redraw"); self.renderer.update_table(&self.tunnels).await; + self.renderer.select(self.selected); terminal.draw(|frame| { self.renderer.render(frame); })?; @@ -87,12 +91,37 @@ impl Handler { Ok(()) } - pub async fn handle_input(&mut self, input: char) -> std::io::Result { + async fn set_access_selection(&mut self, access: TunnelAccess) { + if let Some(selected) = self.selected { + if let Some((_, Some(tunnel))) = self.tunnels.get_index_mut(selected) { + tunnel.set_access(access).await; + } else { + warn!("Selection was invalid"); + } + } else { + self.set_access_all(access).await; + } + } + + async fn handle_input(&mut self, input: Input) -> std::io::Result { match input { - 'q' => { + Input::Char('q') => { self.close()?; return Ok(false); } + Input::Char('k') | Input::Up => self.previous_row(), + Input::Char('j') | Input::Down => self.next_row(), + Input::Esc => self.selected = None, + Input::Char('P') => { + self.set_access_selection(TunnelAccess::Public).await; + } + Input::Char('p') => { + if let Some(user) = self.user.clone() { + self.set_access_selection(TunnelAccess::Private(user)).await; + } else { + warn!("User not set"); + } + } _ => { return Ok(false); } @@ -100,6 +129,34 @@ impl Handler { Ok(true) } + + fn next_row(&mut self) { + let i = match self.selected { + Some(i) => { + if i < self.tunnels.len() - 1 { + i + 1 + } else { + i + } + } + None => 0, + }; + self.selected = Some(i); + } + + fn previous_row(&mut self) { + let i = match self.selected { + Some(i) => { + if i > 0 { + i - 1 + } else { + i + } + } + None => self.tunnels.len() - 1, + }; + self.selected = Some(i); + } } /// Quickly create http tunnels for development @@ -150,13 +207,10 @@ impl russh::server::Handler for Handler { data: &[u8], _session: &mut Session, ) -> Result<(), Self::Error> { - let Some(input) = data.first().cloned() else { - return Ok(()); - }; + let input: Input = data.into(); + trace!(?input, "data"); - trace!(input, "data"); - - if self.handle_input(input as char).await? { + if self.handle_input(input).await? { self.redraw().await?; } @@ -179,7 +233,7 @@ impl russh::server::Handler for Handler { debug!("{args:?}"); if args.public { trace!("Making tunnels public"); - self.set_access(TunnelAccess::Public).await; + self.set_access_all(TunnelAccess::Public).await; self.redraw().await?; } } diff --git a/src/tui.rs b/src/tui.rs index bb9af63..05a2630 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -5,7 +5,7 @@ use indexmap::IndexMap; use ratatui::{ Frame, layout::{Constraint, Flex, Rect}, - style::{Color, Style, Stylize as _}, + style::{Style, Stylize as _}, text::{Line, Span}, widgets::{Cell, HighlightSpacing, Row, Table, TableState}, }; @@ -61,7 +61,8 @@ impl Renderer { "{} ({})", std::env!("CARGO_PKG_NAME"), std::env!("CARGO_PKG_VERSION") - ); + ) + .bold(); let title = Line::from(title).centered(); frame.render_widget(title, rect); } @@ -82,7 +83,7 @@ impl Renderer { } pub fn render_table(&mut self, frame: &mut Frame<'_>, rect: Rect) { - let highlight_style = Style::default().bg(Color::Blue); + let highlight_style = Style::default().bold(); let header_style = Style::default().bold().reversed(); let row_style = Style::default(); @@ -116,4 +117,8 @@ impl Renderer { frame.render_stateful_widget(t, rect, &mut self.table_state); } + + pub fn select(&mut self, index: Option) { + self.table_state.select(index); + } } diff --git a/src/tunnel.rs b/src/tunnel.rs index 320c5bb..923cee4 100644 --- a/src/tunnel.rs +++ b/src/tunnel.rs @@ -60,6 +60,10 @@ impl Tunnel { pub async fn set_access(&self, access: TunnelAccess) { *self.access.write().await = access; } + + pub async fn is_public(&self) -> bool { + matches!(*self.access.read().await, TunnelAccess::Public) + } } #[derive(Debug, Clone)]