From 381c008665cf4089b4fd46a495f8d6a877a061a8 Mon Sep 17 00:00:00 2001 From: Brandon W Maister Date: Sat, 18 Feb 2023 14:04:56 -0800 Subject: [PATCH 1/4] =?UTF-8?q?tui:=20Add=20a=20`fwd`=20column=20with=20a?= =?UTF-8?q?=20=E2=9C=93=20to=20show=20that=20ports=20are=20being=20forward?= =?UTF-8?q?ed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I didn't realize that everything was forwarded by default, this makes it more obvious. --- src/client/ui.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index 3a1f3d4..e53efe7 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -248,6 +248,7 @@ impl UI { let listener = self.ports.get(&port).unwrap(); rows.push( Row::new(vec![ + if listener.enabled { " ✓ " } else { "" }, &port_strings[index][..], match &listener.desc { Some(port_desc) => &port_desc.desc, @@ -265,11 +266,14 @@ impl UI { // TODO: I don't know how to express the lengths I want here. // That last length is extremely wrong but guaranteed to work I // guess. - let widths = - vec![Constraint::Length(5), Constraint::Length(size.width)]; + let widths = vec![ + Constraint::Length(3), + Constraint::Length(5), + Constraint::Length(size.width), + ]; let port_list = Table::new(rows) - .header(Row::new(vec!["Port", "Description"])) + .header(Row::new(vec!["fwd", "Port", "Description"])) .block(Block::default().title("Ports").borders(Borders::ALL)) .column_spacing(1) .widths(&widths) From 290dcff9b63aab6a5337baa61a36980fc1d8da3a Mon Sep 17 00:00:00 2001 From: Brandon W Maister Date: Sat, 18 Feb 2023 15:06:46 -0800 Subject: [PATCH 2/4] tui: Add a help popup --- src/client/ui.rs | 110 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 3 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index e53efe7..515395f 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -19,10 +19,11 @@ use tokio::sync::oneshot; use tokio_stream::StreamExt; use tui::{ backend::{Backend, CrosstermBackend}, - layout::{Constraint, Direction, Layout, Rect}, + layout::{Constraint, Direction, Layout, Margin, Rect}, style::{Color, Style}, widgets::{ - Block, Borders, List, ListItem, ListState, Row, Table, TableState, + Block, Borders, Clear, List, ListItem, ListState, Paragraph, Row, + Table, TableState, }, Frame, Terminal, }; @@ -148,6 +149,7 @@ pub struct UI { selection: TableState, running: bool, show_logs: bool, + show_help: bool, alternate_screen: bool, raw_mode: bool, } @@ -160,6 +162,7 @@ impl UI { socks_port: None, running: true, show_logs: false, + show_help: false, selection: TableState::default(), lines: VecDeque::with_capacity(1024), config, @@ -234,6 +237,9 @@ impl UI { if self.show_logs { self.render_logs(frame, chunks[1]); } + if self.show_help { + self.render_help(frame); + } } fn render_ports(&mut self, frame: &mut Frame, size: Rect) { @@ -282,6 +288,60 @@ impl UI { frame.render_stateful_widget(port_list, size, &mut self.selection); } + fn render_help(&mut self, frame: &mut Frame) { + let keybindings = vec![ + Row::new(vec!["↑ / k", "Move cursor up"]), + Row::new(vec!["↓ / j", "Move cursor down"]), + Row::new(vec!["e", "enable/disable forwarding"]), + Row::new(vec!["RET", "Open port in web browser"]), + Row::new(vec!["ESC / q", "exit"]), + Row::new(vec!["? / h", "Show this help text"]), + Row::new(vec!["l", "Show fwd's logs"]), + ]; + + let help_intro = 2; + let border_lines = 3; + + let help_popup_area = centered_rect( + 65, + keybindings.len() as u16 + help_intro + border_lines, + frame.size(), + ); + let inner_area = + help_popup_area.inner(&Margin { vertical: 1, horizontal: 1 }); + + let constraints = vec![ + Constraint::Length(help_intro), + Constraint::Length(inner_area.height - help_intro), + ]; + let help_parts = Layout::default() + .direction(Direction::Vertical) + .constraints(constraints) + .split(inner_area); + + let keybindings = Table::new(keybindings) + .widths(&[Constraint::Length(7), Constraint::Length(40)]) + .column_spacing(1) + .block( + Block::default().title("key bindings").borders(Borders::TOP), + ); + + let exp = Paragraph::new( + "fwd forwards all ports discovered on the target when it starts.", + ); + + // outer box + frame.render_widget(Clear, help_popup_area); //this clears out the background + let helpbox = Block::default().title("help").borders(Borders::ALL); + frame.render_widget(helpbox, help_popup_area); + + // explanation + frame.render_widget(exp, help_parts[0]); + + // keybindings + frame.render_widget(keybindings, help_parts[1]); + } + fn render_logs(&mut self, frame: &mut Frame, size: Rect) { let items: Vec<_> = self.lines.iter().map(|l| ListItem::new(&l[..])).collect(); @@ -376,7 +436,17 @@ impl UI { } KeyEvent { code: KeyCode::Esc, .. } | KeyEvent { code: KeyCode::Char('q'), .. } => { - self.running = false; + // it's natural to press q to get out of a help screen, so + // don't shut down if users do ?q + if self.show_help { + self.show_help = false; + } else { + self.running = false; + } + } + KeyEvent { code: KeyCode::Char('?'), .. } + | KeyEvent { code: KeyCode::Char('h'), .. } => { + self.show_help = !self.show_help; } KeyEvent { code: KeyCode::Char('l'), .. } => { self.show_logs = !self.show_logs; @@ -510,6 +580,40 @@ impl Drop for UI { } } +/// helper function to create a centered rect using up certain percentage of the available rect `r` +fn centered_rect(width_chars: u16, height_chars: u16, r: Rect) -> Rect { + let height_percent = + (height_chars as f64 / r.height as f64 * 100.0).ceil() as u16; + let height_diff = (100 - height_percent) / 2; + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage(height_diff), + Constraint::Percentage(height_percent), + Constraint::Percentage(height_diff), + ] + .as_ref(), + ) + .split(r); + + let width_percent = + (width_chars as f64 / r.width as f64 * 100.0).ceil() as u16; + let width_diff = (100 - width_percent) / 2; + + Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage(width_diff), + Constraint::Percentage(width_percent), + Constraint::Percentage(width_diff), + ] + .as_ref(), + ) + .split(popup_layout[1])[1] +} + #[cfg(test)] mod tests { use super::*; From 34340e2575590c156a12b87dc5211cd623e7d45e Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 21 Mar 2023 23:07:08 -0700 Subject: [PATCH 3/4] Simplify centering math, don't crash if the rectangle clips Also, tests --- src/client/ui.rs | 71 +++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index 515395f..899ab62 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -582,36 +582,24 @@ impl Drop for UI { /// helper function to create a centered rect using up certain percentage of the available rect `r` fn centered_rect(width_chars: u16, height_chars: u16, r: Rect) -> Rect { - let height_percent = - (height_chars as f64 / r.height as f64 * 100.0).ceil() as u16; - let height_diff = (100 - height_percent) / 2; - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage(height_diff), - Constraint::Percentage(height_percent), - Constraint::Percentage(height_diff), - ] - .as_ref(), - ) - .split(r); + let left = r.left() + + if width_chars > r.width { + 0 + } else { + (r.width - width_chars) / 2 + }; - let width_percent = - (width_chars as f64 / r.width as f64 * 100.0).ceil() as u16; - let width_diff = (100 - width_percent) / 2; + let top = r.top() + + if height_chars > r.height { + 0 + } else { + (r.height - height_chars) / 2 + }; - Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage(width_diff), - Constraint::Percentage(width_percent), - Constraint::Percentage(width_diff), - ] - .as_ref(), - ) - .split(popup_layout[1])[1] + let width = width_chars.min(r.width); + let height = height_chars.min(r.height); + + Rect::new(left, top, width, height) } #[cfg(test)] @@ -777,4 +765,31 @@ mod tests { drop(sender); } + + #[test] + fn test_centered_rect() { + // Normal old centering. + let frame = Rect::new(0, 0, 128, 128); + let centered = centered_rect(10, 10, frame); + assert_eq!(centered.left(), (128 - centered.width) / 2); + assert_eq!(centered.top(), (128 - centered.height) / 2); + assert_eq!(centered.width, 10); + assert_eq!(centered.height, 10); + + // Clip the width and height to the box + let frame = Rect::new(0, 0, 5, 5); + let centered = centered_rect(10, 10, frame); + assert_eq!(centered.left(), 0); + assert_eq!(centered.top(), 0); + assert_eq!(centered.width, 5); + assert_eq!(centered.height, 5); + + // Deal with non zero-zero origins. + let frame = Rect::new(10, 10, 128, 128); + let centered = centered_rect(10, 10, frame); + assert_eq!(centered.left(), 10 + (128 - centered.width) / 2); + assert_eq!(centered.top(), 10 + (128 - centered.height) / 2); + assert_eq!(centered.width, 10); + assert_eq!(centered.height, 10); + } } From 99d377d4ce115da639ec59daaeb4b3f5e5a303b3 Mon Sep 17 00:00:00 2001 From: John Doty Date: Tue, 21 Mar 2023 23:17:16 -0700 Subject: [PATCH 4/4] Make help modal I don't like that ENTER and the arrow keys still manipulate the list while help is showing. Ignore other keypresses while the help screen is shown. Also, make the spelling/capitalization a little cleaner. --- src/client/ui.rs | 53 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/src/client/ui.rs b/src/client/ui.rs index 899ab62..ec0ac73 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs @@ -292,14 +292,14 @@ impl UI { let keybindings = vec![ Row::new(vec!["↑ / k", "Move cursor up"]), Row::new(vec!["↓ / j", "Move cursor down"]), - Row::new(vec!["e", "enable/disable forwarding"]), + Row::new(vec!["e", "Enable/disable forwarding"]), Row::new(vec!["RET", "Open port in web browser"]), - Row::new(vec!["ESC / q", "exit"]), + Row::new(vec!["ESC / q", "Quit"]), Row::new(vec!["? / h", "Show this help text"]), Row::new(vec!["l", "Show fwd's logs"]), ]; - let help_intro = 2; + let help_intro = 4; let border_lines = 3; let help_popup_area = centered_rect( @@ -322,17 +322,15 @@ impl UI { let keybindings = Table::new(keybindings) .widths(&[Constraint::Length(7), Constraint::Length(40)]) .column_spacing(1) - .block( - Block::default().title("key bindings").borders(Borders::TOP), - ); + .block(Block::default().title("Keys").borders(Borders::TOP)); let exp = Paragraph::new( - "fwd forwards all ports discovered on the target when it starts.", + "fwd automatically listens for connections on the same ports\nas the target, and forwards connections on those ports to the\ntarget.", ); // outer box frame.render_widget(Clear, help_popup_area); //this clears out the background - let helpbox = Block::default().title("help").borders(Borders::ALL); + let helpbox = Block::default().title("Help").borders(Borders::ALL); frame.render_widget(helpbox, help_popup_area); // explanation @@ -426,6 +424,37 @@ impl UI { fn handle_console_event( &mut self, ev: Option>, + ) { + if self.show_help { + self.handle_console_event_help(ev) + } else { + self.handle_console_event_main(ev) + } + } + + fn handle_console_event_help( + &mut self, + ev: Option>, + ) { + match ev { + Some(Ok(Event::Key(ev))) => match ev { + KeyEvent { code: KeyCode::Esc, .. } + | KeyEvent { code: KeyCode::Char('q'), .. } + | KeyEvent { code: KeyCode::Char('?'), .. } + | KeyEvent { code: KeyCode::Char('h'), .. } => { + self.show_help = false; + } + _ => (), + }, + Some(Ok(_)) => (), // Don't care about this event... + Some(Err(_)) => (), // Hmmmmmm.....? + None => (), // ....no events? what? + } + } + + fn handle_console_event_main( + &mut self, + ev: Option>, ) { match ev { Some(Ok(Event::Key(ev))) => match ev { @@ -436,13 +465,7 @@ impl UI { } KeyEvent { code: KeyCode::Esc, .. } | KeyEvent { code: KeyCode::Char('q'), .. } => { - // it's natural to press q to get out of a help screen, so - // don't shut down if users do ?q - if self.show_help { - self.show_help = false; - } else { - self.running = false; - } + self.running = false; } KeyEvent { code: KeyCode::Char('?'), .. } | KeyEvent { code: KeyCode::Char('h'), .. } => {