Improved input handling and allow user to change tunnel access in interface

This commit is contained in:
Dreaded_X 2025-04-10 15:56:15 +02:00
parent 750713b6b0
commit 321e5842d3
Signed by: Dreaded_X
GPG Key ID: 5A0CBFE3C3377FAA
5 changed files with 102 additions and 16 deletions

22
src/keys.rs Normal file
View File

@ -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,
}
}
}

View File

@ -3,6 +3,7 @@
pub mod animals; pub mod animals;
pub mod auth; pub mod auth;
pub mod helper; pub mod helper;
pub mod keys;
pub mod ssh; pub mod ssh;
pub mod terminal; pub mod terminal;
pub mod tui; pub mod tui;

View File

@ -12,6 +12,7 @@ use tokio::net::ToSocketAddrs;
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
use crate::{ use crate::{
keys::Input,
terminal::TerminalHandle, terminal::TerminalHandle,
tui::Renderer, tui::Renderer,
tunnel::{Tunnel, TunnelAccess, Tunnels}, tunnel::{Tunnel, TunnelAccess, Tunnels},
@ -25,6 +26,7 @@ pub struct Handler {
terminal: Option<Terminal<CrosstermBackend<TerminalHandle>>>, terminal: Option<Terminal<CrosstermBackend<TerminalHandle>>>,
renderer: Renderer, renderer: Renderer,
selected: Option<usize>,
} }
impl Handler { impl Handler {
@ -33,12 +35,13 @@ impl Handler {
all_tunnels, all_tunnels,
tunnels: IndexMap::new(), tunnels: IndexMap::new(),
user: None, user: None,
terminal: Default::default(), terminal: None,
renderer: Default::default(), 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 { for (_address, tunnel) in &self.tunnels {
if let Some(tunnel) = tunnel { if let Some(tunnel) = tunnel {
tunnel.set_access(access.clone()).await; 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 { let rect = Rect {
x: 0, x: 0,
y: 0, y: 0,
@ -73,10 +76,11 @@ impl Handler {
Ok(()) 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 { if let Some(terminal) = &mut self.terminal {
trace!("redraw"); trace!("redraw");
self.renderer.update_table(&self.tunnels).await; self.renderer.update_table(&self.tunnels).await;
self.renderer.select(self.selected);
terminal.draw(|frame| { terminal.draw(|frame| {
self.renderer.render(frame); self.renderer.render(frame);
})?; })?;
@ -87,12 +91,37 @@ impl Handler {
Ok(()) Ok(())
} }
pub async fn handle_input(&mut self, input: char) -> std::io::Result<bool> { 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<bool> {
match input { match input {
'q' => { Input::Char('q') => {
self.close()?; self.close()?;
return Ok(false); 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); return Ok(false);
} }
@ -100,6 +129,34 @@ impl Handler {
Ok(true) 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 /// Quickly create http tunnels for development
@ -150,13 +207,10 @@ impl russh::server::Handler for Handler {
data: &[u8], data: &[u8],
_session: &mut Session, _session: &mut Session,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
let Some(input) = data.first().cloned() else { let input: Input = data.into();
return Ok(()); trace!(?input, "data");
};
trace!(input, "data"); if self.handle_input(input).await? {
if self.handle_input(input as char).await? {
self.redraw().await?; self.redraw().await?;
} }
@ -179,7 +233,7 @@ impl russh::server::Handler for Handler {
debug!("{args:?}"); debug!("{args:?}");
if args.public { if args.public {
trace!("Making tunnels public"); trace!("Making tunnels public");
self.set_access(TunnelAccess::Public).await; self.set_access_all(TunnelAccess::Public).await;
self.redraw().await?; self.redraw().await?;
} }
} }

View File

@ -5,7 +5,7 @@ use indexmap::IndexMap;
use ratatui::{ use ratatui::{
Frame, Frame,
layout::{Constraint, Flex, Rect}, layout::{Constraint, Flex, Rect},
style::{Color, Style, Stylize as _}, style::{Style, Stylize as _},
text::{Line, Span}, text::{Line, Span},
widgets::{Cell, HighlightSpacing, Row, Table, TableState}, widgets::{Cell, HighlightSpacing, Row, Table, TableState},
}; };
@ -61,7 +61,8 @@ impl Renderer {
"{} ({})", "{} ({})",
std::env!("CARGO_PKG_NAME"), std::env!("CARGO_PKG_NAME"),
std::env!("CARGO_PKG_VERSION") std::env!("CARGO_PKG_VERSION")
); )
.bold();
let title = Line::from(title).centered(); let title = Line::from(title).centered();
frame.render_widget(title, rect); frame.render_widget(title, rect);
} }
@ -82,7 +83,7 @@ impl Renderer {
} }
pub fn render_table(&mut self, frame: &mut Frame<'_>, rect: Rect) { 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 header_style = Style::default().bold().reversed();
let row_style = Style::default(); let row_style = Style::default();
@ -116,4 +117,8 @@ impl Renderer {
frame.render_stateful_widget(t, rect, &mut self.table_state); frame.render_stateful_widget(t, rect, &mut self.table_state);
} }
pub fn select(&mut self, index: Option<usize>) {
self.table_state.select(index);
}
} }

View File

@ -60,6 +60,10 @@ impl Tunnel {
pub async fn set_access(&self, access: TunnelAccess) { pub async fn set_access(&self, access: TunnelAccess) {
*self.access.write().await = access; *self.access.write().await = access;
} }
pub async fn is_public(&self) -> bool {
matches!(*self.access.read().await, TunnelAccess::Public)
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]