diff --git a/src/handler.rs b/src/handler.rs index c5880ec..eac2428 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -26,6 +26,8 @@ pub struct Handler { terminal: Option>>, renderer: Renderer, selected: Option, + + rename_buffer: Option, } impl Handler { @@ -38,6 +40,7 @@ impl Handler { terminal: None, renderer: Default::default(), selected: None, + rename_buffer: None, } } @@ -78,7 +81,7 @@ impl Handler { trace!("redraw"); self.renderer.update(&self.tunnels, self.selected).await; terminal.draw(|frame| { - self.renderer.render(frame); + self.renderer.render(frame, &self.rename_buffer); })?; } else { warn!("Redraw called without valid terminal"); @@ -98,62 +101,102 @@ impl Handler { } async fn handle_input(&mut self, input: Input) -> std::io::Result { - match input { - 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"); + if self.rename_buffer.is_some() { + match input { + Input::Char(c) if c.is_alphanumeric() => { + self.rename_buffer + .as_mut() + .expect("input buffer should be some") + .push(c.to_ascii_lowercase()); } + Input::Backspace => { + self.rename_buffer + .as_mut() + .expect("input buffer should be some") + .pop(); + } + Input::Enter => { + debug!("Input accepted"); + if let Some(selected) = self.selected + && let Some(tunnel) = self.tunnels.get_mut(selected) + && let Some(buffer) = self.rename_buffer.take() + { + *tunnel = self.all_tunnels.rename_tunnel(tunnel.clone(), buffer).await; + } else { + warn!("Trying to rename invalid tunnel"); + } + } + Input::Esc => { + debug!("Input rejected"); + self.rename_buffer = None; + } + _ => return Ok(false), } - Input::Char('r') => { - let Some(selected) = self.selected else { - return Ok(false); - }; - - let Some(tunnel) = self.tunnels.get_mut(selected) else { - warn!("Trying to retry invalid tunnel"); - return Ok(false); - }; - - *tunnel = self.all_tunnels.retry_tunnel(tunnel.clone()).await; - } - Input::Delete => { - let Some(selected) = self.selected else { - return Ok(false); - }; - - if selected >= self.tunnels.len() { - warn!("Trying to delete tunnel out of bounds"); + debug!("Input: {:?}", self.rename_buffer); + } else { + match input { + Input::Char('q') => { + self.close()?; return Ok(false); } - - let tunnel = self.tunnels.remove(selected); - self.all_tunnels.remove_tunnel(tunnel).await; - - if self.tunnels.is_empty() { - self.selected = None; - } else { - self.selected = Some(min(self.tunnels.len() - 1, selected)); + 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::CtrlP => { - self.set_access_selection(TunnelAccess::Protected).await; - } - _ => { - return Ok(false); - } - }; + Input::Char('p') => { + if let Some(user) = self.user.clone() { + self.set_access_selection(TunnelAccess::Private(user)).await; + } else { + warn!("User not set"); + } + } + Input::Char('R') => { + let Some(selected) = self.selected else { + return Ok(false); + }; + + let Some(tunnel) = self.tunnels.get_mut(selected) else { + warn!("Trying to retry invalid tunnel"); + return Ok(false); + }; + + *tunnel = self.all_tunnels.retry_tunnel(tunnel.clone()).await; + } + Input::Char('r') => { + if self.selected.is_some() { + trace!("Renaming tunnel"); + self.rename_buffer = Some(String::new()); + } + } + Input::Delete => { + let Some(selected) = self.selected else { + return Ok(false); + }; + + if selected >= self.tunnels.len() { + warn!("Trying to delete tunnel out of bounds"); + return Ok(false); + } + + let tunnel = self.tunnels.remove(selected); + self.all_tunnels.remove_tunnel(tunnel).await; + + if self.tunnels.is_empty() { + self.selected = None; + } else { + self.selected = Some(min(self.tunnels.len() - 1, selected)); + } + } + Input::CtrlP => { + self.set_access_selection(TunnelAccess::Protected).await; + } + _ => { + return Ok(false); + } + }; + } Ok(true) } diff --git a/src/input.rs b/src/input.rs index 8949136..d212c47 100644 --- a/src/input.rs +++ b/src/input.rs @@ -8,6 +8,7 @@ pub enum Input { Delete, Esc, Enter, + Backspace, CtrlP, Other, } @@ -23,6 +24,7 @@ impl From<&[u8]> for Input { [13] => Input::Enter, // NOTE: Actual char is DLE, this happens to map to ctrl-p [16] => Input::CtrlP, + [127] => Input::Backspace, other => { trace!("{other:?}"); Input::Other diff --git a/src/tui.rs b/src/tui.rs index 51e5f3d..18e78b8 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,12 +1,14 @@ -use std::cmp; +use std::cmp::{self, max}; use futures::StreamExt; use ratatui::{ Frame, - layout::{Constraint, Flex, Layout, Rect}, + layout::{Constraint, Flex, Layout, Position, Rect}, style::{Style, Stylize as _}, text::{Line, Span, Text}, - widgets::{Block, BorderType, Cell, HighlightSpacing, Paragraph, Row, Table, TableState}, + widgets::{ + Block, BorderType, Cell, Clear, HighlightSpacing, Paragraph, Row, Table, TableState, + }, }; use unicode_width::UnicodeWidthStr; @@ -33,7 +35,7 @@ impl Renderer { self.table_state.select(index); } - fn compute_footer_text<'a>(&self, rect: Rect) -> (u16, Paragraph<'a>) { + pub fn compute_footer_text<'a>(&self, rect: Rect) -> (u16, Paragraph<'a>) { let width = rect.width as usize - 2; let commands = if self.table_state.selected().is_some() { @@ -43,11 +45,13 @@ impl Renderer { command("↓/j", "move down"), command("↑/k", "move up"), vec![], + command("del", "remove"), + command("r", "rename"), + command("shift-r", "retry"), + vec![], command("p", "make private"), command("ctrl-p", "make protected"), command("shift-p", "make public"), - command("del", "remove"), - command("r", "retry"), ] } else { vec![ @@ -89,7 +93,7 @@ impl Renderer { (height as u16, Paragraph::new(text).centered().block(block)) } - pub fn render(&mut self, frame: &mut Frame) { + pub fn render(&mut self, frame: &mut Frame, input: &Option) { self.render_title(frame, frame.area()); let mut area = frame.area().inner(ratatui::layout::Margin { @@ -104,6 +108,28 @@ impl Renderer { self.render_table(frame, chunks[0]); frame.render_widget(footer, chunks[1]); + + if let Some(input) = input { + self.render_rename(frame, area, input); + } + } + + pub fn render_rename(&self, frame: &mut Frame, area: Rect, input: &str) { + let vertical = Layout::vertical([Constraint::Length(3)]).flex(Flex::Center); + let horizontal = Layout::horizontal([Constraint::Max(max(20, input.width() as u16 + 4))]) + .flex(Flex::Center); + let [area] = vertical.areas(area); + let [area] = horizontal.areas(area); + + let title = Line::from("New name").centered(); + let block = Block::bordered().title(title); + let text = Paragraph::new(format!(" {input}")).block(block); + + frame.render_widget(Clear, area); + + frame.render_widget(text, area); + + frame.set_cursor_position(Position::new(area.x + input.width() as u16 + 2, area.y + 1)); } pub fn render_title(&self, frame: &mut Frame, rect: Rect) { diff --git a/src/tunnel.rs b/src/tunnel.rs index eb55bad..d8d8e6f 100644 --- a/src/tunnel.rs +++ b/src/tunnel.rs @@ -41,6 +41,7 @@ pub enum TunnelAccess { pub struct Tunnel { handle: Handle, name: String, + address: String, domain: Option, port: u32, access: Arc>, @@ -50,7 +51,7 @@ impl Tunnel { pub async fn open_tunnel(&self) -> Result, russh::Error> { trace!(tunnel = self.name, "Opening tunnel"); self.handle - .channel_open_forwarded_tcpip(&self.name, self.port, &self.name, self.port) + .channel_open_forwarded_tcpip(&self.address, self.port, &self.address, self.port) .await } @@ -92,9 +93,11 @@ impl Tunnels { port: u32, user: impl Into, ) -> Tunnel { + let address = name.into(); let mut tunnel = Tunnel { handle, - name: name.into(), + name: address.clone(), + address, domain: Some(self.domain.clone()), port, access: Arc::new(RwLock::new(TunnelAccess::Private(user.into()))), @@ -150,6 +153,14 @@ impl Tunnels { self.add_tunnel(tunnel).await } + + pub async fn rename_tunnel(&mut self, tunnel: Tunnel, name: impl Into) -> Tunnel { + let mut tunnel = self.remove_tunnel(tunnel).await; + tunnel.name = name.into(); + tunnel.domain = Some(self.domain.clone()); + + self.add_tunnel(tunnel).await + } } impl Service> for Tunnels {