Change UI to tui
This commit is contained in:
parent
5780b7a4bf
commit
dc374f0d20
3 changed files with 178 additions and 123 deletions
32
Cargo.lock
generated
32
Cargo.lock
generated
|
|
@ -53,6 +53,12 @@ version = "1.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
|
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cassowary"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.73"
|
version = "1.0.73"
|
||||||
|
|
@ -173,6 +179,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"toml",
|
"toml",
|
||||||
|
"tui",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -578,12 +585,37 @@ dependencies = [
|
||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
|
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]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.10.0+wasi-snapshot-preview1"
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ thiserror = "1.0"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
|
tui = "0.19"
|
||||||
|
|
||||||
[target.'cfg(target_os="linux")'.dependencies]
|
[target.'cfg(target_os="linux")'.dependencies]
|
||||||
procfs = "0.14.1"
|
procfs = "0.14.1"
|
||||||
|
|
|
||||||
268
src/client/ui.rs
268
src/client/ui.rs
|
|
@ -2,24 +2,31 @@ use super::{client_listen, config::ServerConfig};
|
||||||
use crate::message::PortDesc;
|
use crate::message::PortDesc;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor::{MoveTo, RestorePosition, SavePosition},
|
|
||||||
event::{Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
|
event::{Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
|
||||||
execute, queue,
|
execute,
|
||||||
style::{Color, PrintStyledContent, Stylize},
|
|
||||||
terminal::{
|
terminal::{
|
||||||
disable_raw_mode, enable_raw_mode, size, Clear, ClearType,
|
disable_raw_mode, enable_raw_mode, DisableLineWrap, EnableLineWrap,
|
||||||
DisableLineWrap, EnableLineWrap, EnterAlternateScreen,
|
EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
LeaveAlternateScreen,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use log::{error, info, Level, Metadata, Record};
|
use log::{error, info, Level, Metadata, Record};
|
||||||
use open;
|
use open;
|
||||||
use std::collections::vec_deque::VecDeque;
|
use std::collections::vec_deque::VecDeque;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::io::{stdout, Write};
|
use std::io::stdout;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tokio_stream::StreamExt;
|
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 {
|
pub enum UIEvent {
|
||||||
Connected(u16),
|
Connected(u16),
|
||||||
|
|
@ -134,7 +141,7 @@ pub struct UI {
|
||||||
socks_port: Option<u16>,
|
socks_port: Option<u16>,
|
||||||
lines: VecDeque<String>,
|
lines: VecDeque<String>,
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
selection: usize,
|
selection: TableState,
|
||||||
running: bool,
|
running: bool,
|
||||||
show_logs: bool,
|
show_logs: bool,
|
||||||
alternate_screen: bool,
|
alternate_screen: bool,
|
||||||
|
|
@ -149,7 +156,7 @@ impl UI {
|
||||||
socks_port: None,
|
socks_port: None,
|
||||||
running: true,
|
running: true,
|
||||||
show_logs: false,
|
show_logs: false,
|
||||||
selection: 0,
|
selection: TableState::default(),
|
||||||
lines: VecDeque::with_capacity(1024),
|
lines: VecDeque::with_capacity(1024),
|
||||||
config,
|
config,
|
||||||
alternate_screen: false,
|
alternate_screen: false,
|
||||||
|
|
@ -159,130 +166,137 @@ impl UI {
|
||||||
|
|
||||||
pub async fn run(&mut self) -> Result<()> {
|
pub async fn run(&mut self) -> Result<()> {
|
||||||
let mut console_events = EventStream::new();
|
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;
|
self.running = true;
|
||||||
while self.running {
|
while self.running {
|
||||||
self.handle_events(&mut console_events).await;
|
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() {
|
if self.connected() {
|
||||||
self.render_connected()?;
|
self.enable_raw_mode()?;
|
||||||
} else {
|
} 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_disconnected(&mut self) -> Result<()> {
|
fn render_disconnected<T: Backend>(
|
||||||
self.enter_alternate_screen()?;
|
&mut self,
|
||||||
self.disable_raw_mode()?;
|
frame: &mut Frame<T>,
|
||||||
let mut stdout = stdout();
|
pos: (u16, u16),
|
||||||
|
) {
|
||||||
|
let size = frame.size();
|
||||||
|
|
||||||
let (columns, _) = size()?;
|
let block = Block::default()
|
||||||
let columns: usize = columns.into();
|
.title(Span::styled(
|
||||||
|
"Not Connected",
|
||||||
|
Style::default().fg(Color::Black).bg(Color::Red),
|
||||||
|
))
|
||||||
|
.borders(Borders::NONE);
|
||||||
|
|
||||||
execute!(
|
frame.render_widget(block, size);
|
||||||
stdout,
|
|
||||||
SavePosition,
|
frame.set_cursor(pos.0, pos.1);
|
||||||
MoveTo(0, 0),
|
|
||||||
PrintStyledContent(
|
|
||||||
format!("{:^columns$}\r\n", "Not Connected")
|
|
||||||
.with(Color::Black)
|
|
||||||
.on(Color::Red)
|
|
||||||
),
|
|
||||||
RestorePosition,
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_connected(&mut self) -> Result<()> {
|
fn render_connected<T: Backend>(&mut self, frame: &mut Frame<T>) {
|
||||||
self.enter_alternate_screen()?;
|
let constraints = if self.show_logs {
|
||||||
self.enable_raw_mode()?;
|
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||||
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
|
|
||||||
} else {
|
} else {
|
||||||
rows - 2
|
vec![Constraint::Percentage(100)]
|
||||||
}
|
};
|
||||||
.into();
|
|
||||||
|
|
||||||
let ports = self.get_ui_ports();
|
let chunks = Layout::default()
|
||||||
for (index, port) in ports.into_iter().take(max_ports).enumerate() {
|
.direction(Direction::Vertical)
|
||||||
let listener = self.ports.get(&port).unwrap();
|
.constraints(constraints)
|
||||||
|
.split(frame.size());
|
||||||
|
|
||||||
let caret = if index == self.selection {
|
self.render_ports(frame, chunks[0]);
|
||||||
"\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
|
|
||||||
if self.show_logs {
|
if self.show_logs {
|
||||||
let hr: usize = ((rows / 2) - 2).into();
|
self.render_logs(frame, chunks[1]);
|
||||||
let start: usize = if self.lines.len() > hr {
|
}
|
||||||
self.lines.len() - hr
|
}
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
queue!(stdout, MoveTo(0, rows / 2))?;
|
fn render_ports<B: Backend>(&mut self, frame: &mut Frame<B>, size: Rect) {
|
||||||
print!("{}", format!("{:columns$}", " Log").negative());
|
let enabled_port_style = Style::default();
|
||||||
for line in self.lines.range(start..) {
|
let disabled_port_style = Style::default().fg(Color::DarkGray);
|
||||||
print!("{}\r\n", line);
|
|
||||||
}
|
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))?;
|
// TODO: I don't know how to express the lengths I want here.
|
||||||
print!(
|
// That last length is extremely wrong but guaranteed to work I
|
||||||
"{}",
|
// guess.
|
||||||
format!(
|
let widths =
|
||||||
"{:columns$}",
|
vec![Constraint::Length(5), Constraint::Length(size.width)];
|
||||||
" q - quit | l - toggle log | \u{2191}/\u{2193} - select port | e - enable/disable | <enter> - browse"
|
|
||||||
).negative()
|
let port_list = Table::new(rows)
|
||||||
);
|
.header(Row::new(vec!["Port", "Description"]))
|
||||||
stdout.flush()?;
|
.block(Block::default().title("Ports").borders(Borders::ALL))
|
||||||
Ok(())
|
.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 {
|
fn connected(&self) -> bool {
|
||||||
|
|
@ -299,11 +313,7 @@ impl UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_selected_port(&self) -> Option<u16> {
|
fn get_selected_port(&self) -> Option<u16> {
|
||||||
if self.selection < self.ports.len() {
|
self.selection.selected().map(|i| self.get_ui_ports()[i])
|
||||||
Some(self.get_ui_ports()[self.selection])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enable_disable_port(&mut self, port: u16) {
|
fn enable_disable_port(&mut self, port: u16) {
|
||||||
|
|
@ -378,15 +388,25 @@ impl UI {
|
||||||
}
|
}
|
||||||
KeyEvent { code: KeyCode::Up, .. }
|
KeyEvent { code: KeyCode::Up, .. }
|
||||||
| KeyEvent { code: KeyCode::Char('j'), .. } => {
|
| KeyEvent { code: KeyCode::Char('j'), .. } => {
|
||||||
if self.selection > 0 {
|
let index = match self.selection.selected() {
|
||||||
self.selection -= 1;
|
Some(i) => {
|
||||||
}
|
if i == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
i - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
self.selection.select(Some(index));
|
||||||
}
|
}
|
||||||
KeyEvent { code: KeyCode::Down, .. }
|
KeyEvent { code: KeyCode::Down, .. }
|
||||||
| KeyEvent { code: KeyCode::Char('k'), .. } => {
|
| KeyEvent { code: KeyCode::Char('k'), .. } => {
|
||||||
if self.selection != self.ports.len() - 1 {
|
let index = match self.selection.selected() {
|
||||||
self.selection += 1;
|
Some(i) => (i + 1).min(self.ports.len() - 1),
|
||||||
}
|
None => 0,
|
||||||
|
};
|
||||||
|
self.selection.select(Some(index));
|
||||||
}
|
}
|
||||||
KeyEvent { code: KeyCode::Enter, .. } => {
|
KeyEvent { code: KeyCode::Enter, .. } => {
|
||||||
if let Some(p) = self.get_selected_port() {
|
if let Some(p) = self.get_selected_port() {
|
||||||
|
|
@ -453,9 +473,11 @@ impl UI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.selection >= self.ports.len() && self.ports.len() > 0 {
|
let selected = match self.selection.selected() {
|
||||||
self.selection = self.ports.len() - 1;
|
Some(i) => i.min(self.ports.len() - 1),
|
||||||
}
|
None => 0,
|
||||||
|
};
|
||||||
|
self.selection.select(Some(selected));
|
||||||
}
|
}
|
||||||
Some(UIEvent::ServerLine(line)) => {
|
Some(UIEvent::ServerLine(line)) => {
|
||||||
while self.lines.len() >= 1024 {
|
while self.lines.len() >= 1024 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue