Added tunnel renaming
This commit is contained in:
parent
d944efc24a
commit
f7a79c8411
147
src/handler.rs
147
src/handler.rs
|
@ -26,6 +26,8 @@ pub struct Handler {
|
||||||
terminal: Option<Terminal<CrosstermBackend<TerminalHandle>>>,
|
terminal: Option<Terminal<CrosstermBackend<TerminalHandle>>>,
|
||||||
renderer: Renderer,
|
renderer: Renderer,
|
||||||
selected: Option<usize>,
|
selected: Option<usize>,
|
||||||
|
|
||||||
|
rename_buffer: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Handler {
|
impl Handler {
|
||||||
|
@ -38,6 +40,7 @@ impl Handler {
|
||||||
terminal: None,
|
terminal: None,
|
||||||
renderer: Default::default(),
|
renderer: Default::default(),
|
||||||
selected: None,
|
selected: None,
|
||||||
|
rename_buffer: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +81,7 @@ impl Handler {
|
||||||
trace!("redraw");
|
trace!("redraw");
|
||||||
self.renderer.update(&self.tunnels, self.selected).await;
|
self.renderer.update(&self.tunnels, self.selected).await;
|
||||||
terminal.draw(|frame| {
|
terminal.draw(|frame| {
|
||||||
self.renderer.render(frame);
|
self.renderer.render(frame, &self.rename_buffer);
|
||||||
})?;
|
})?;
|
||||||
} else {
|
} else {
|
||||||
warn!("Redraw called without valid terminal");
|
warn!("Redraw called without valid terminal");
|
||||||
|
@ -98,62 +101,102 @@ impl Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_input(&mut self, input: Input) -> std::io::Result<bool> {
|
async fn handle_input(&mut self, input: Input) -> std::io::Result<bool> {
|
||||||
match input {
|
if self.rename_buffer.is_some() {
|
||||||
Input::Char('q') => {
|
match input {
|
||||||
self.close()?;
|
Input::Char(c) if c.is_alphanumeric() => {
|
||||||
return Ok(false);
|
self.rename_buffer
|
||||||
}
|
.as_mut()
|
||||||
Input::Char('k') | Input::Up => self.previous_row(),
|
.expect("input buffer should be some")
|
||||||
Input::Char('j') | Input::Down => self.next_row(),
|
.push(c.to_ascii_lowercase());
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
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') => {
|
debug!("Input: {:?}", self.rename_buffer);
|
||||||
let Some(selected) = self.selected else {
|
} else {
|
||||||
return Ok(false);
|
match input {
|
||||||
};
|
Input::Char('q') => {
|
||||||
|
self.close()?;
|
||||||
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");
|
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
Input::Char('k') | Input::Up => self.previous_row(),
|
||||||
let tunnel = self.tunnels.remove(selected);
|
Input::Char('j') | Input::Down => self.next_row(),
|
||||||
self.all_tunnels.remove_tunnel(tunnel).await;
|
Input::Esc => self.selected = None,
|
||||||
|
Input::Char('P') => {
|
||||||
if self.tunnels.is_empty() {
|
self.set_access_selection(TunnelAccess::Public).await;
|
||||||
self.selected = None;
|
|
||||||
} else {
|
|
||||||
self.selected = Some(min(self.tunnels.len() - 1, selected));
|
|
||||||
}
|
}
|
||||||
}
|
Input::Char('p') => {
|
||||||
Input::CtrlP => {
|
if let Some(user) = self.user.clone() {
|
||||||
self.set_access_selection(TunnelAccess::Protected).await;
|
self.set_access_selection(TunnelAccess::Private(user)).await;
|
||||||
}
|
} else {
|
||||||
_ => {
|
warn!("User not set");
|
||||||
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::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)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ pub enum Input {
|
||||||
Delete,
|
Delete,
|
||||||
Esc,
|
Esc,
|
||||||
Enter,
|
Enter,
|
||||||
|
Backspace,
|
||||||
CtrlP,
|
CtrlP,
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
@ -23,6 +24,7 @@ impl From<&[u8]> for Input {
|
||||||
[13] => Input::Enter,
|
[13] => Input::Enter,
|
||||||
// NOTE: Actual char is DLE, this happens to map to ctrl-p
|
// NOTE: Actual char is DLE, this happens to map to ctrl-p
|
||||||
[16] => Input::CtrlP,
|
[16] => Input::CtrlP,
|
||||||
|
[127] => Input::Backspace,
|
||||||
other => {
|
other => {
|
||||||
trace!("{other:?}");
|
trace!("{other:?}");
|
||||||
Input::Other
|
Input::Other
|
||||||
|
|
40
src/tui.rs
40
src/tui.rs
|
@ -1,12 +1,14 @@
|
||||||
use std::cmp;
|
use std::cmp::{self, max};
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
layout::{Constraint, Flex, Layout, Rect},
|
layout::{Constraint, Flex, Layout, Position, Rect},
|
||||||
style::{Style, Stylize as _},
|
style::{Style, Stylize as _},
|
||||||
text::{Line, Span, Text},
|
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;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
@ -33,7 +35,7 @@ impl Renderer {
|
||||||
self.table_state.select(index);
|
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 width = rect.width as usize - 2;
|
||||||
|
|
||||||
let commands = if self.table_state.selected().is_some() {
|
let commands = if self.table_state.selected().is_some() {
|
||||||
|
@ -43,11 +45,13 @@ impl Renderer {
|
||||||
command("↓/j", "move down"),
|
command("↓/j", "move down"),
|
||||||
command("↑/k", "move up"),
|
command("↑/k", "move up"),
|
||||||
vec![],
|
vec![],
|
||||||
|
command("del", "remove"),
|
||||||
|
command("r", "rename"),
|
||||||
|
command("shift-r", "retry"),
|
||||||
|
vec![],
|
||||||
command("p", "make private"),
|
command("p", "make private"),
|
||||||
command("ctrl-p", "make protected"),
|
command("ctrl-p", "make protected"),
|
||||||
command("shift-p", "make public"),
|
command("shift-p", "make public"),
|
||||||
command("del", "remove"),
|
|
||||||
command("r", "retry"),
|
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
vec![
|
vec![
|
||||||
|
@ -89,7 +93,7 @@ impl Renderer {
|
||||||
(height as u16, Paragraph::new(text).centered().block(block))
|
(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());
|
self.render_title(frame, frame.area());
|
||||||
|
|
||||||
let mut area = frame.area().inner(ratatui::layout::Margin {
|
let mut area = frame.area().inner(ratatui::layout::Margin {
|
||||||
|
@ -104,6 +108,28 @@ impl Renderer {
|
||||||
|
|
||||||
self.render_table(frame, chunks[0]);
|
self.render_table(frame, chunks[0]);
|
||||||
frame.render_widget(footer, chunks[1]);
|
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) {
|
pub fn render_title(&self, frame: &mut Frame, rect: Rect) {
|
||||||
|
|
|
@ -41,6 +41,7 @@ pub enum TunnelAccess {
|
||||||
pub struct Tunnel {
|
pub struct Tunnel {
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
name: String,
|
name: String,
|
||||||
|
address: String,
|
||||||
domain: Option<String>,
|
domain: Option<String>,
|
||||||
port: u32,
|
port: u32,
|
||||||
access: Arc<RwLock<TunnelAccess>>,
|
access: Arc<RwLock<TunnelAccess>>,
|
||||||
|
@ -50,7 +51,7 @@ impl Tunnel {
|
||||||
pub async fn open_tunnel(&self) -> Result<Channel<Msg>, russh::Error> {
|
pub async fn open_tunnel(&self) -> Result<Channel<Msg>, russh::Error> {
|
||||||
trace!(tunnel = self.name, "Opening tunnel");
|
trace!(tunnel = self.name, "Opening tunnel");
|
||||||
self.handle
|
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
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,9 +93,11 @@ impl Tunnels {
|
||||||
port: u32,
|
port: u32,
|
||||||
user: impl Into<String>,
|
user: impl Into<String>,
|
||||||
) -> Tunnel {
|
) -> Tunnel {
|
||||||
|
let address = name.into();
|
||||||
let mut tunnel = Tunnel {
|
let mut tunnel = Tunnel {
|
||||||
handle,
|
handle,
|
||||||
name: name.into(),
|
name: address.clone(),
|
||||||
|
address,
|
||||||
domain: Some(self.domain.clone()),
|
domain: Some(self.domain.clone()),
|
||||||
port,
|
port,
|
||||||
access: Arc::new(RwLock::new(TunnelAccess::Private(user.into()))),
|
access: Arc::new(RwLock::new(TunnelAccess::Private(user.into()))),
|
||||||
|
@ -150,6 +153,14 @@ impl Tunnels {
|
||||||
|
|
||||||
self.add_tunnel(tunnel).await
|
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 {
|
impl Service<Request<Incoming>> for Tunnels {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user