diff --git a/src/ssh/renderer.rs b/src/ssh/renderer.rs index cb1e276..fdba7e8 100644 --- a/src/ssh/renderer.rs +++ b/src/ssh/renderer.rs @@ -1,5 +1,6 @@ use std::cmp::{self, max}; use std::io::Write as _; +use std::time::Duration; use futures::StreamExt; use git_version::git_version; @@ -11,17 +12,18 @@ use ratatui::widgets::{ Block, BorderType, Cell, Clear, HighlightSpacing, Paragraph, Row, Table, TableState, }; use ratatui::{Frame, Terminal}; +use tokio::select; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}; use tracing::error; use unicode_width::UnicodeWidthStr; use crate::io::TerminalHandle; -use crate::tunnel::Tunnel; +use crate::tunnel::{Tunnel, TunnelRow}; enum Message { Resize { width: u16, height: u16 }, Redraw, - Rows(Vec>>), + Rows(Vec), Select(Option), Rename(Option), Help(String), @@ -30,7 +32,7 @@ enum Message { struct RendererInner { state: TableState, - rows: Vec>>, + rows: Vec, input: Option, rx: UnboundedReceiver, } @@ -107,6 +109,22 @@ impl RendererInner { (height as u16, Paragraph::new(text).centered().block(block)) } + fn compute_widths(&mut self, rows: &Vec>>) -> Vec { + let table_header = Tunnel::header(); + std::iter::once(&table_header) + .chain(rows) + .map(|row| row.iter().map(|cell| cell.width() as u16)) + .fold(vec![0; table_header.len()], |acc, row| { + acc.into_iter() + .zip(row) + .map(|v| cmp::max(v.0, v.1)) + .collect() + }) + .into_iter() + .map(|c| Constraint::Length(c + 1)) + .collect() + } + fn render(&mut self, frame: &mut Frame) { self.render_title(frame, frame.area()); @@ -122,28 +140,7 @@ impl RendererInner { self.render_table(frame, chunks[0]); frame.render_widget(footer, chunks[1]); - - if let Some(input) = &self.input { - self.render_rename(frame, area, input); - } - } - - 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)); + self.render_rename(frame, area); } fn render_title(&self, frame: &mut Frame, rect: Rect) { @@ -152,28 +149,18 @@ impl RendererInner { frame.render_widget(title, rect); } - fn compute_widths(&mut self) -> Vec { - let table_header = Tunnel::header(); - std::iter::once(&table_header) - .chain(&self.rows) - .map(|row| row.iter().map(|cell| cell.width() as u16)) - .fold(vec![0; table_header.len()], |acc, row| { - acc.into_iter() - .zip(row) - .map(|v| cmp::max(v.0, v.1)) - .collect() - }) - .into_iter() - .map(|c| Constraint::Length(c + 1)) - .collect() - } - - pub fn render_table(&mut self, frame: &mut Frame<'_>, rect: Rect) { + fn render_table(&mut self, frame: &mut Frame<'_>, rect: Rect) { let highlight_style = Style::default().bold(); let header_style = Style::default().bold().reversed(); let row_style = Style::default(); - let rows = self.rows.iter().map(|row| { + let r = self + .rows + .iter() + .map(From::from) + .collect::>>>(); + + let rows = r.iter().map(|row| { row.iter() .cloned() .map(Cell::from) @@ -195,7 +182,7 @@ impl RendererInner { .rows(rows) .flex(Flex::Start) .column_spacing(3) - .widths(self.compute_widths()) + .widths(self.compute_widths(&r)) .row_highlight_style(highlight_style) .highlight_symbol(Line::from("> ")) .highlight_spacing(HighlightSpacing::Always); @@ -203,36 +190,70 @@ impl RendererInner { frame.render_stateful_widget(t, rect, &mut self.state); } + fn render_rename(&self, frame: &mut Frame, area: Rect) { + if let Some(input) = &self.input { + 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 async fn start( &mut self, mut terminal: Terminal>, ) -> std::io::Result<()> { - while let Some(message) = self.rx.recv().await { - match message { - Message::Resize { width, height } => { - let rect = Rect::new(0, 0, width, height); + loop { + select! { + message = self.rx.recv() => { + let Some(message) = message else { + break; + }; - terminal.resize(rect)?; + match message { + Message::Resize { width, height } => { + let rect = Rect::new(0, 0, width, height); + + terminal.resize(rect)?; + } + Message::Select(selected) => self.state.select(selected), + Message::Rename(input) => self.input = input, + Message::Rows(rows) => self.rows = rows, + Message::Redraw => { + terminal.draw(|frame| { + self.render(frame); + })?; + } + Message::Help(message) => { + let writer = terminal.backend_mut().writer_mut(); + writer.leave_alternate_screen()?; + writer.write_all(message.as_bytes())?; + writer.flush()?; + + break; + } + Message::Close => { + break; + } + } } - Message::Select(selected) => self.state.select(selected), - Message::Rename(input) => self.input = input, - Message::Rows(rows) => self.rows = rows, - Message::Redraw => { + _ = tokio::time::sleep(Duration::from_secs(1)) => { terminal.draw(|frame| { self.render(frame); })?; } - Message::Help(message) => { - let writer = terminal.backend_mut().writer_mut(); - writer.leave_alternate_screen()?; - writer.write_all(message.as_bytes())?; - writer.flush()?; - - break; - } - Message::Close => { - break; - } } } diff --git a/src/tunnel/mod.rs b/src/tunnel/mod.rs index 3c92248..bc9a73a 100644 --- a/src/tunnel/mod.rs +++ b/src/tunnel/mod.rs @@ -8,6 +8,7 @@ use registry::RegistryEntry; use russh::server::Handle; use tokio::sync::{RwLock, RwLockReadGuard}; use tracing::trace; +pub use tui::TunnelRow; use crate::io::{Stats, TrackStats}; diff --git a/src/tunnel/tui.rs b/src/tunnel/tui.rs index dc423f7..965ab5d 100644 --- a/src/tunnel/tui.rs +++ b/src/tunnel/tui.rs @@ -1,9 +1,33 @@ use std::ops::Deref; +use std::sync::Arc; use ratatui::style::Stylize; use ratatui::text::Span; use super::{Tunnel, TunnelAccess}; +use crate::io::Stats; + +pub struct TunnelRow { + name: Span<'static>, + access: Span<'static>, + port: Span<'static>, + address: Span<'static>, + stats: Arc, +} + +impl From<&TunnelRow> for Vec> { + fn from(row: &TunnelRow) -> Self { + vec![ + row.name.clone(), + row.access.clone(), + row.port.clone(), + row.address.clone(), + row.stats.connections().to_string().into(), + row.stats.rx().to_string().into(), + row.stats.tx().to_string().into(), + ] + } +} impl Tunnel { pub fn header() -> Vec> { @@ -18,7 +42,7 @@ impl Tunnel { ] } - pub async fn to_row(tunnel: &Tunnel) -> Vec> { + pub async fn to_row(tunnel: &Tunnel) -> TunnelRow { let access = match tunnel.inner.access.read().await.deref() { TunnelAccess::Private(owner) => owner.clone().yellow(), TunnelAccess::Protected => "PROTECTED".blue(), @@ -30,14 +54,12 @@ impl Tunnel { .map(|address| format!("http://{address}").into()) .unwrap_or("FAILED".red()); - vec![ - tunnel.registry_entry.get_name().to_owned().into(), + TunnelRow { + name: tunnel.registry_entry.get_name().to_string().into(), access, - tunnel.inner.port.to_string().into(), + port: tunnel.inner.port.to_string().into(), address, - tunnel.inner.stats.connections().to_string().into(), - tunnel.inner.stats.rx().to_string().into(), - tunnel.inner.stats.tx().to_string().into(), - ] + stats: tunnel.inner.stats.clone(), + } } }