Change UI to tui

This commit is contained in:
John Doty 2022-11-27 08:23:20 -08:00
parent 5780b7a4bf
commit dc374f0d20
3 changed files with 178 additions and 123 deletions

32
Cargo.lock generated
View file

@ -53,6 +53,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cc"
version = "1.0.73"
@ -173,6 +179,7 @@ dependencies = [
"tokio",
"tokio-stream",
"toml",
"tui",
]
[[package]]
@ -578,12 +585,37 @@ dependencies = [
"serde",
]
[[package]]
name = "tui"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
dependencies = [
"bitflags",
"cassowary",
"crossterm",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "unicode-ident"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
[[package]]
name = "unicode-segmentation"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"

View file

@ -19,6 +19,7 @@ thiserror = "1.0"
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"
toml = "0.5"
tui = "0.19"
[target.'cfg(target_os="linux")'.dependencies]
procfs = "0.14.1"

View file

@ -2,24 +2,31 @@ use super::{client_listen, config::ServerConfig};
use crate::message::PortDesc;
use anyhow::Result;
use crossterm::{
cursor::{MoveTo, RestorePosition, SavePosition},
event::{Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
execute, queue,
style::{Color, PrintStyledContent, Stylize},
execute,
terminal::{
disable_raw_mode, enable_raw_mode, size, Clear, ClearType,
DisableLineWrap, EnableLineWrap, EnterAlternateScreen,
LeaveAlternateScreen,
disable_raw_mode, enable_raw_mode, DisableLineWrap, EnableLineWrap,
EnterAlternateScreen, LeaveAlternateScreen,
},
};
use log::{error, info, Level, Metadata, Record};
use open;
use std::collections::vec_deque::VecDeque;
use std::collections::{HashMap, HashSet};
use std::io::{stdout, Write};
use std::io::stdout;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio_stream::StreamExt;
use tui::{
backend::{Backend, CrosstermBackend},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
text::Span,
widgets::{
Block, Borders, List, ListItem, ListState, Row, Table, TableState,
},
Frame, Terminal,
};
pub enum UIEvent {
Connected(u16),
@ -134,7 +141,7 @@ pub struct UI {
socks_port: Option<u16>,
lines: VecDeque<String>,
config: ServerConfig,
selection: usize,
selection: TableState,
running: bool,
show_logs: bool,
alternate_screen: bool,
@ -149,7 +156,7 @@ impl UI {
socks_port: None,
running: true,
show_logs: false,
selection: 0,
selection: TableState::default(),
lines: VecDeque::with_capacity(1024),
config,
alternate_screen: false,
@ -159,130 +166,137 @@ impl UI {
pub async fn run(&mut self) -> Result<()> {
let mut console_events = EventStream::new();
self.enter_alternate_screen()?;
let stdout = std::io::stdout();
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut last_connected = false;
self.running = true;
while self.running {
self.handle_events(&mut console_events).await;
let pos = terminal.get_cursor()?;
if last_connected != self.connected() {
terminal.clear()?;
last_connected = self.connected();
}
if self.connected() {
self.render_connected()?;
self.enable_raw_mode()?;
} else {
self.render_disconnected()?;
self.disable_raw_mode()?;
}
terminal.draw(|f| {
if self.connected() {
self.render_connected(f);
} else {
self.render_disconnected(f, pos);
}
})?;
}
Ok(())
}
fn render_disconnected(&mut self) -> Result<()> {
self.enter_alternate_screen()?;
self.disable_raw_mode()?;
let mut stdout = stdout();
fn render_disconnected<T: Backend>(
&mut self,
frame: &mut Frame<T>,
pos: (u16, u16),
) {
let size = frame.size();
let (columns, _) = size()?;
let columns: usize = columns.into();
let block = Block::default()
.title(Span::styled(
"Not Connected",
Style::default().fg(Color::Black).bg(Color::Red),
))
.borders(Borders::NONE);
execute!(
stdout,
SavePosition,
MoveTo(0, 0),
PrintStyledContent(
format!("{:^columns$}\r\n", "Not Connected")
.with(Color::Black)
.on(Color::Red)
),
RestorePosition,
)?;
Ok(())
frame.render_widget(block, size);
frame.set_cursor(pos.0, pos.1);
}
fn render_connected(&mut self) -> Result<()> {
self.enter_alternate_screen()?;
self.enable_raw_mode()?;
let mut stdout = stdout();
let (columns, rows) = size()?;
let columns: usize = columns.into();
queue!(stdout, Clear(ClearType::All), MoveTo(0, 0))?;
// List of open ports
// How wide are all the things?
let padding = 1;
let enabled_width = 1; // Just 1 character
let port_width = 5; // 5 characters for 16-bit number
let description_width = columns
- (padding
+ padding
+ enabled_width
+ padding
+ port_width
+ padding);
print!(
"{}",
format!(
" {enabled:>enabled_width$} {port:>port_width$} {description:<description_width$}\r\n",
enabled = "E",
port = "port",
description = "description"
)
.negative()
);
let max_ports: usize = if self.show_logs {
(rows / 2) - 1
fn render_connected<T: Backend>(&mut self, frame: &mut Frame<T>) {
let constraints = if self.show_logs {
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
} else {
rows - 2
}
.into();
vec![Constraint::Percentage(100)]
};
let ports = self.get_ui_ports();
for (index, port) in ports.into_iter().take(max_ports).enumerate() {
let listener = self.ports.get(&port).unwrap();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(frame.size());
let caret = if index == self.selection {
"\u{2B46}"
} else {
" "
};
let state = if listener.enabled { "+" } else { " " };
let desc: &str = match &listener.desc {
Some(port_desc) => &port_desc.desc,
None => "",
};
print!("{caret} {state:>enabled_width$} {port:port_width$} {desc:description_width$}\r\n");
}
// Log
self.render_ports(frame, chunks[0]);
if self.show_logs {
let hr: usize = ((rows / 2) - 2).into();
let start: usize = if self.lines.len() > hr {
self.lines.len() - hr
} else {
0
};
self.render_logs(frame, chunks[1]);
}
}
queue!(stdout, MoveTo(0, rows / 2))?;
print!("{}", format!("{:columns$}", " Log").negative());
for line in self.lines.range(start..) {
print!("{}\r\n", line);
}
fn render_ports<B: Backend>(&mut self, frame: &mut Frame<B>, size: Rect) {
let enabled_port_style = Style::default();
let disabled_port_style = Style::default().fg(Color::DarkGray);
let mut rows = Vec::new();
let ports = self.get_ui_ports();
let port_strings: Vec<_> =
ports.iter().map(|p| format!("{p}")).collect();
for (index, port) in ports.into_iter().enumerate() {
let listener = self.ports.get(&port).unwrap();
rows.push(
Row::new(vec![
&port_strings[index][..],
match &listener.desc {
Some(port_desc) => &port_desc.desc,
None => "",
},
])
.style(if listener.enabled {
enabled_port_style
} else {
disabled_port_style
}),
);
}
queue!(stdout, MoveTo(0, rows - 1))?;
print!(
"{}",
format!(
"{:columns$}",
" q - quit | l - toggle log | \u{2191}/\u{2193} - select port | e - enable/disable | <enter> - browse"
).negative()
);
stdout.flush()?;
Ok(())
// 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 port_list = Table::new(rows)
.header(Row::new(vec!["Port", "Description"]))
.block(Block::default().title("Ports").borders(Borders::ALL))
.column_spacing(1)
.widths(&widths)
.highlight_symbol(">> ");
frame.render_stateful_widget(port_list, size, &mut self.selection);
}
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();
let list = List::new(items)
.block(Block::default().title("Log").borders(Borders::ALL));
let mut list_state = ListState::default();
list_state.select(if self.lines.len() > 0 {
Some(self.lines.len() - 1)
} else {
None
});
frame.render_stateful_widget(list, size, &mut list_state);
}
fn connected(&self) -> bool {
@ -299,11 +313,7 @@ impl UI {
}
fn get_selected_port(&self) -> Option<u16> {
if self.selection < self.ports.len() {
Some(self.get_ui_ports()[self.selection])
} else {
None
}
self.selection.selected().map(|i| self.get_ui_ports()[i])
}
fn enable_disable_port(&mut self, port: u16) {
@ -378,15 +388,25 @@ impl UI {
}
KeyEvent { code: KeyCode::Up, .. }
| KeyEvent { code: KeyCode::Char('j'), .. } => {
if self.selection > 0 {
self.selection -= 1;
}
let index = match self.selection.selected() {
Some(i) => {
if i == 0 {
0
} else {
i - 1
}
}
None => 0,
};
self.selection.select(Some(index));
}
KeyEvent { code: KeyCode::Down, .. }
| KeyEvent { code: KeyCode::Char('k'), .. } => {
if self.selection != self.ports.len() - 1 {
self.selection += 1;
}
let index = match self.selection.selected() {
Some(i) => (i + 1).min(self.ports.len() - 1),
None => 0,
};
self.selection.select(Some(index));
}
KeyEvent { code: KeyCode::Enter, .. } => {
if let Some(p) = self.get_selected_port() {
@ -453,9 +473,11 @@ impl UI {
}
}
if self.selection >= self.ports.len() && self.ports.len() > 0 {
self.selection = self.ports.len() - 1;
}
let selected = match self.selection.selected() {
Some(i) => i.min(self.ports.len() - 1),
None => 0,
};
self.selection.select(Some(selected));
}
Some(UIEvent::ServerLine(line)) => {
while self.lines.len() >= 1024 {