Compare commits

...

4 commits

Author SHA1 Message Date
99d377d4ce 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.
2023-03-21 23:17:16 -07:00
34340e2575 Simplify centering math, don't crash if the rectangle clips
Also, tests
2023-03-21 23:07:08 -07:00
Brandon W Maister
290dcff9b6 tui: Add a help popup 2023-03-21 22:53:35 -07:00
Brandon W Maister
381c008665 tui: Add a fwd column with a ✓ to show that ports are being forwarded
I didn't realize that everything was forwarded by default, this makes it more
obvious.
2023-03-21 22:53:35 -07:00

View file

@ -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<B: Backend>(&mut self, frame: &mut Frame<B>, size: Rect) {
@ -248,6 +254,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 +272,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)
@ -278,6 +288,58 @@ impl UI {
frame.render_stateful_widget(port_list, size, &mut self.selection);
}
fn render_help<B: Backend>(&mut self, frame: &mut Frame<B>) {
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", "Quit"]),
Row::new(vec!["? / h", "Show this help text"]),
Row::new(vec!["l", "Show fwd's logs"]),
];
let help_intro = 4;
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("Keys").borders(Borders::TOP));
let exp = Paragraph::new(
"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);
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<B: Backend>(&mut self, frame: &mut Frame<B>, size: Rect) {
let items: Vec<_> =
self.lines.iter().map(|l| ListItem::new(&l[..])).collect();
@ -362,6 +424,37 @@ impl UI {
fn handle_console_event(
&mut self,
ev: Option<Result<Event, std::io::Error>>,
) {
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<Result<Event, std::io::Error>>,
) {
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<Result<Event, std::io::Error>>,
) {
match ev {
Some(Ok(Event::Key(ev))) => match ev {
@ -374,6 +467,10 @@ impl UI {
| KeyEvent { code: KeyCode::Char('q'), .. } => {
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;
}
@ -506,6 +603,28 @@ 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 left = r.left()
+ if width_chars > r.width {
0
} else {
(r.width - width_chars) / 2
};
let top = r.top()
+ if height_chars > r.height {
0
} else {
(r.height - height_chars) / 2
};
let width = width_chars.min(r.width);
let height = height_chars.min(r.height);
Rect::new(left, top, width, height)
}
#[cfg(test)]
mod tests {
use super::*;
@ -669,4 +788,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);
}
}