Added tunnel renaming

This commit is contained in:
Dreaded_X 2025-04-13 01:53:46 +02:00
parent d944efc24a
commit f7a79c8411
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
4 changed files with 143 additions and 61 deletions

View File

@ -26,6 +26,8 @@ pub struct Handler {
terminal: Option<Terminal<CrosstermBackend<TerminalHandle>>>,
renderer: Renderer,
selected: Option<usize>,
rename_buffer: Option<String>,
}
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<bool> {
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)
}

View File

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

View File

@ -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<String>) {
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) {

View File

@ -41,6 +41,7 @@ pub enum TunnelAccess {
pub struct Tunnel {
handle: Handle,
name: String,
address: String,
domain: Option<String>,
port: u32,
access: Arc<RwLock<TunnelAccess>>,
@ -50,7 +51,7 @@ impl Tunnel {
pub async fn open_tunnel(&self) -> Result<Channel<Msg>, 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<String>,
) -> 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<String>) -> 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<Request<Incoming>> for Tunnels {