added colours, select and delete ops, refac
This commit is contained in:
@@ -39,9 +39,21 @@ impl Entry {
|
|||||||
Ok(Entry {
|
Ok(Entry {
|
||||||
fromIP,
|
fromIP,
|
||||||
toIP,
|
toIP,
|
||||||
fromPort,
|
fromPort: fromPort.trim_start_matches('0').to_string(),
|
||||||
toPort,
|
toPort: toPort.trim_start_matches('0').to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn array(&self, idx: usize) -> Vec<String> {
|
||||||
|
let d = self.clone();
|
||||||
|
vec![
|
||||||
|
format!("{}", idx),
|
||||||
|
d.fromIP,
|
||||||
|
d.fromPort,
|
||||||
|
String::new(),
|
||||||
|
d.toIP,
|
||||||
|
d.toPort,
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ pub mod entry;
|
|||||||
mod settings;
|
mod settings;
|
||||||
|
|
||||||
pub mod status;
|
pub mod status;
|
||||||
|
use ratatui::widgets::TableState;
|
||||||
|
|
||||||
use crate::app::{
|
use crate::app::{
|
||||||
entry::Entry,
|
entry::Entry,
|
||||||
settings::Settings,
|
settings::Settings,
|
||||||
@@ -18,6 +20,7 @@ pub struct AppState {
|
|||||||
pub currentlyEditing: Option<EditingField>,
|
pub currentlyEditing: Option<EditingField>,
|
||||||
pub entries: Vec<Entry>,
|
pub entries: Vec<Entry>,
|
||||||
pub confDir: String,
|
pub confDir: String,
|
||||||
|
pub tableState: TableState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@@ -32,6 +35,7 @@ impl AppState {
|
|||||||
screen: CurrentScreen::Main,
|
screen: CurrentScreen::Main,
|
||||||
entries: settings.entries,
|
entries: settings.entries,
|
||||||
confDir,
|
confDir,
|
||||||
|
tableState: TableState::default().with_selected(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +53,8 @@ impl AppState {
|
|||||||
self.fromPort = String::new();
|
self.fromPort = String::new();
|
||||||
self.toPort = String::new();
|
self.toPort = String::new();
|
||||||
self.currentlyEditing = None;
|
self.currentlyEditing = None;
|
||||||
|
self.tableState
|
||||||
|
.select(Some(self.entries.len() - 1 as usize));
|
||||||
EntryCreation::Success
|
EntryCreation::Success
|
||||||
}
|
}
|
||||||
_ => EntryCreation::PortValidationError,
|
_ => EntryCreation::PortValidationError,
|
||||||
@@ -104,4 +109,40 @@ impl AppState {
|
|||||||
settings.entries = self.entries.clone();
|
settings.entries = self.entries.clone();
|
||||||
settings.save(&self.confDir);
|
settings.save(&self.confDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn nextRow(&mut self) {
|
||||||
|
let i = match self.tableState.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i >= self.entries.len() - 1 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
self.tableState.select(Some(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prevRow(&mut self) {
|
||||||
|
let i = match self.tableState.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i == 0 {
|
||||||
|
self.entries.len() - 1
|
||||||
|
} else {
|
||||||
|
i - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
self.tableState.select(Some(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delCur(&mut self) {
|
||||||
|
if self.entries.is_empty() {
|
||||||
|
self.screen = CurrentScreen::Main;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.entries.remove(self.tableState.selected().unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pub enum CurrentScreen {
|
|||||||
Main,
|
Main,
|
||||||
Add,
|
Add,
|
||||||
Settings,
|
Settings,
|
||||||
|
Delete,
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
16
src/main.rs
16
src/main.rs
@@ -63,6 +63,13 @@ fn runApp<B: Backend>(app: &mut AppState, terminal: &mut Terminal<B>) -> Result<
|
|||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
CurrentScreen::Delete => match key.code {
|
||||||
|
KeyCode::Up => app.nextRow(),
|
||||||
|
KeyCode::Down => app.prevRow(),
|
||||||
|
KeyCode::Enter => app.delCur(),
|
||||||
|
KeyCode::Esc => app.screen = CurrentScreen::Main,
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
CurrentScreen::Main => match key.code {
|
CurrentScreen::Main => match key.code {
|
||||||
KeyCode::Char('a') => {
|
KeyCode::Char('a') => {
|
||||||
app.screen = CurrentScreen::Add;
|
app.screen = CurrentScreen::Add;
|
||||||
@@ -72,6 +79,9 @@ fn runApp<B: Backend>(app: &mut AppState, terminal: &mut Terminal<B>) -> Result<
|
|||||||
app.screen = CurrentScreen::Exit
|
app.screen = CurrentScreen::Exit
|
||||||
}
|
}
|
||||||
KeyCode::Char('s') | KeyCode::F(2) => app.screen = CurrentScreen::Settings,
|
KeyCode::Char('s') | KeyCode::F(2) => app.screen = CurrentScreen::Settings,
|
||||||
|
KeyCode::Char('d') => app.screen = CurrentScreen::Delete,
|
||||||
|
KeyCode::Up => app.nextRow(),
|
||||||
|
KeyCode::Down => app.prevRow(),
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
CurrentScreen::Add => match (key.modifiers, key.code) {
|
CurrentScreen::Add => match (key.modifiers, key.code) {
|
||||||
@@ -134,6 +144,12 @@ fn runApp<B: Backend>(app: &mut AppState, terminal: &mut Terminal<B>) -> Result<
|
|||||||
opField.push_str("127.0.0.1");
|
opField.push_str("127.0.0.1");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
'o' => {
|
||||||
|
if isIP {
|
||||||
|
opField.clear();
|
||||||
|
opField.push_str("0.0.0.0");
|
||||||
|
}
|
||||||
|
}
|
||||||
'c' => opField.clear(),
|
'c' => opField.clear(),
|
||||||
'C' => {
|
'C' => {
|
||||||
app.fromIP.clear();
|
app.fromIP.clear();
|
||||||
|
|||||||
147
src/ui/mod.rs
147
src/ui/mod.rs
@@ -1,19 +1,21 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
mod centeredRect;
|
mod centeredRect;
|
||||||
|
mod textHints;
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
layout::{Constraint, Direction, Layout},
|
layout::{Constraint, Direction, Layout},
|
||||||
style::{Color, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::{Line, Span, Text},
|
text::{Line, Span, Text},
|
||||||
widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
|
widgets::{Block, Borders, Cell, Paragraph, Row, Table, Wrap},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::app::status::CurrentScreen;
|
use crate::app::status::CurrentScreen;
|
||||||
use crate::app::{AppState, status::EditingField};
|
use crate::app::{AppState, status::EditingField};
|
||||||
use crate::ui::centeredRect::centered_rect;
|
use crate::ui::centeredRect::centered_rect;
|
||||||
|
use crate::ui::textHints::hints;
|
||||||
|
|
||||||
pub fn ui(frame: &mut Frame, app: &AppState) {
|
pub fn ui(frame: &mut Frame, app: &mut AppState) {
|
||||||
// Split entire body into left title+body and right screen+keybinds chunks
|
// Split entire body into left title+body and right screen+keybinds chunks
|
||||||
let screenChunks = Layout::default()
|
let screenChunks = Layout::default()
|
||||||
.direction(ratatui::layout::Direction::Horizontal)
|
.direction(ratatui::layout::Direction::Horizontal)
|
||||||
@@ -47,32 +49,74 @@ pub fn ui(frame: &mut Frame, app: &AppState) {
|
|||||||
frame.render_widget(title, titleBodyChunks[0]);
|
frame.render_widget(title, titleBodyChunks[0]);
|
||||||
|
|
||||||
// Create and add body
|
// Create and add body
|
||||||
let mut currentItems = Vec::<ListItem>::new();
|
// let mut currentItems = Vec::<ListItem>::new();
|
||||||
for (i, e) in app.entries.iter().enumerate() {
|
// for (i, e) in app.entries.iter().enumerate() {
|
||||||
currentItems.push(ListItem::new(Line::from(Span::styled(
|
// currentItems.push(ListItem::new(Line::from(Span::styled(
|
||||||
format!("{i}. {e}"),
|
// format!("{i}. {e}"),
|
||||||
Style::default().fg(Color::Yellow),
|
// Style::default().fg(Color::Yellow),
|
||||||
))));
|
// ))));
|
||||||
}
|
// }
|
||||||
let list = List::new(currentItems).block(Block::default().borders(Borders::all()));
|
|
||||||
frame.render_widget(list, titleBodyChunks[1]);
|
// table drawing
|
||||||
|
let headers = ["No.", "From IP", "From Port", "-->", "To IP", "To Port"]
|
||||||
|
.into_iter()
|
||||||
|
.map(Cell::from)
|
||||||
|
.collect::<Row>()
|
||||||
|
.style(Style::default().fg(Color::White).bg(Color::Black))
|
||||||
|
.height(2);
|
||||||
|
|
||||||
|
let dataRows = app.entries.iter().enumerate().map(|(i, d)| {
|
||||||
|
let item = d.array(i + 1);
|
||||||
|
item.into_iter()
|
||||||
|
.map(|content| Cell::from(Text::from(format!("\n{content}\n"))))
|
||||||
|
.collect::<Row>()
|
||||||
|
.style(Style::new().fg(Color::White).bg(Color::Black))
|
||||||
|
.height(2)
|
||||||
|
});
|
||||||
|
let selectedColor = match app.screen {
|
||||||
|
CurrentScreen::Delete => Color::Yellow,
|
||||||
|
_ => Color::DarkGray,
|
||||||
|
};
|
||||||
|
let table = Table::new(
|
||||||
|
dataRows,
|
||||||
|
[
|
||||||
|
Constraint::Percentage(4),
|
||||||
|
Constraint::Percentage(28),
|
||||||
|
Constraint::Percentage(18),
|
||||||
|
Constraint::Percentage(4),
|
||||||
|
Constraint::Percentage(28),
|
||||||
|
Constraint::Percentage(18),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.header(headers)
|
||||||
|
.block(Block::default().borders(Borders::all()))
|
||||||
|
.row_highlight_style(
|
||||||
|
Style::default()
|
||||||
|
.add_modifier(Modifier::REVERSED)
|
||||||
|
.fg(selectedColor),
|
||||||
|
);
|
||||||
|
|
||||||
|
// let list = List::new(currentItems).block(Block::default().borders(Borders::all()));
|
||||||
|
frame.render_stateful_widget(table, titleBodyChunks[1], &mut app.tableState);
|
||||||
|
|
||||||
// Renter exit prompt
|
// Renter exit prompt
|
||||||
if let CurrentScreen::Exit = app.screen {
|
if let CurrentScreen::Exit = app.screen {
|
||||||
let exitPopup = Block::default()
|
let exitPopup = Block::default()
|
||||||
.title("Save and Exit?")
|
.title("Exit Window")
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.style(Style::default().bg(Color::DarkGray));
|
.style(Style::default().bg(Color::DarkGray))
|
||||||
|
.border_style(Style::default().fg(Color::Red));
|
||||||
|
|
||||||
let exitText = Text::styled(
|
let exitText = Text::from(vec![
|
||||||
"Save the current config and exit? (enter/esc)",
|
Line::from("Exit the app?"),
|
||||||
Style::default().fg(Color::Red),
|
Line::from("ANY UNSAVED CHANGES WILL BE DISCARDED."),
|
||||||
);
|
Line::from("Press (s) to save from the main screen."),
|
||||||
|
]);
|
||||||
let exitPara = Paragraph::new(exitText)
|
let exitPara = Paragraph::new(exitText)
|
||||||
.block(exitPopup)
|
.block(exitPopup)
|
||||||
.wrap(Wrap { trim: false });
|
.wrap(Wrap { trim: false });
|
||||||
|
|
||||||
let area = centered_rect(60, 25, frame.area());
|
let area = centered_rect(60, 25, titleBodyChunks[1]);
|
||||||
frame.render_widget(exitPara, area);
|
frame.render_widget(exitPara, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +126,7 @@ pub fn ui(frame: &mut Frame, app: &AppState) {
|
|||||||
.title("Add an entry")
|
.title("Add an entry")
|
||||||
.borders(Borders::NONE)
|
.borders(Borders::NONE)
|
||||||
.style(Style::default().bg(Color::DarkGray));
|
.style(Style::default().bg(Color::DarkGray));
|
||||||
let area = centered_rect(60, 25, titleBodyChunks[1]);
|
let area = centered_rect(75, 20, titleBodyChunks[1]);
|
||||||
frame.render_widget(popup, area);
|
frame.render_widget(popup, area);
|
||||||
|
|
||||||
let fields = Layout::default()
|
let fields = Layout::default()
|
||||||
@@ -123,14 +167,30 @@ pub fn ui(frame: &mut Frame, app: &AppState) {
|
|||||||
|
|
||||||
// --------------------------------
|
// --------------------------------
|
||||||
// Current page title and status
|
// Current page title and status
|
||||||
|
let mut borderColor = Color::White;
|
||||||
|
let _ = borderColor; // to suppress warning, remove later
|
||||||
let screenHeader = vec![
|
let screenHeader = vec![
|
||||||
match app.screen {
|
match app.screen {
|
||||||
CurrentScreen::Main => Span::styled("Main Screen", Style::default().fg(Color::Green)),
|
CurrentScreen::Main => {
|
||||||
CurrentScreen::Add => Span::styled("Add entry", Style::default().fg(Color::Yellow)),
|
borderColor = Color::LightBlue;
|
||||||
CurrentScreen::Exit => {
|
Span::styled("Main Screen", Style::default().fg(Color::LightBlue))
|
||||||
Span::styled("Exit Screen", Style::default().fg(Color::LightRed))
|
}
|
||||||
|
CurrentScreen::Add => {
|
||||||
|
borderColor = Color::LightGreen;
|
||||||
|
Span::styled("Add entry", Style::default().fg(Color::Green))
|
||||||
|
}
|
||||||
|
CurrentScreen::Exit => {
|
||||||
|
borderColor = Color::LightRed;
|
||||||
|
Span::styled("Exit Screen", Style::default().fg(Color::Red))
|
||||||
|
}
|
||||||
|
CurrentScreen::Settings => {
|
||||||
|
borderColor = Color::LightBlue;
|
||||||
|
Span::styled("Settings", Style::default().fg(Color::Blue))
|
||||||
|
}
|
||||||
|
CurrentScreen::Delete => {
|
||||||
|
borderColor = Color::LightMagenta;
|
||||||
|
Span::styled("Delete Selection", Style::default().fg(Color::Magenta))
|
||||||
}
|
}
|
||||||
CurrentScreen::Settings => Span::styled("Settings", Style::default().fg(Color::Blue)),
|
|
||||||
}
|
}
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
Span::styled(" | ", Style::default().fg(Color::White)),
|
Span::styled(" | ", Style::default().fg(Color::White)),
|
||||||
@@ -152,35 +212,28 @@ pub fn ui(frame: &mut Frame, app: &AppState) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let screenHeaderPara = Paragraph::new(Line::from(screenHeader))
|
let screenHeaderPara = Paragraph::new(Line::from(screenHeader)).block(
|
||||||
.block(Block::default().borders(Borders::TOP | Borders::LEFT | Borders::RIGHT));
|
Block::default()
|
||||||
|
.borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
|
||||||
|
.style(Style::default().fg(borderColor)),
|
||||||
|
);
|
||||||
|
|
||||||
// Keybinds Entry
|
// Keybinds Entry
|
||||||
let keybinds = {
|
let keybinds = {
|
||||||
match app.screen {
|
match app.screen {
|
||||||
CurrentScreen::Main => Text::from(vec![
|
CurrentScreen::Main => hints::mainHints(),
|
||||||
Line::from("(a) add entry").style(Style::default().fg(Color::Green)),
|
CurrentScreen::Add => hints::addHints(),
|
||||||
Line::from("(q) save and quit").style(Style::default().fg(Color::Green)),
|
CurrentScreen::Settings => hints::settingsHints(),
|
||||||
]),
|
CurrentScreen::Delete => hints::delHints(),
|
||||||
CurrentScreen::Add => Text::from(vec![
|
CurrentScreen::Exit => hints::exitHints(),
|
||||||
Line::from("(l) localhost"),
|
|
||||||
Line::from("(enter) next field"),
|
|
||||||
Line::from("(c) clear"),
|
|
||||||
Line::from("(C) clear all"),
|
|
||||||
Line::from("(esc) main menu"),
|
|
||||||
]),
|
|
||||||
CurrentScreen::Settings => {
|
|
||||||
Text::from(Line::from("").style(Style::default().fg(Color::Red)))
|
|
||||||
}
|
|
||||||
CurrentScreen::Exit => Text::from(vec![
|
|
||||||
Line::from("(enter) save and exit").style(Style::default().fg(Color::Red)),
|
|
||||||
Line::from("(esc) main menu").style(Style::default().fg(Color::Red)),
|
|
||||||
]),
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let keyBindFooter = Paragraph::new(keybinds)
|
let keyBindFooter = Paragraph::new(keybinds).block(
|
||||||
.block(Block::default().borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT));
|
Block::default()
|
||||||
|
.borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT)
|
||||||
|
.style(Style::default().fg(borderColor)),
|
||||||
|
);
|
||||||
|
|
||||||
frame.render_widget(screenHeaderPara, headerKeybindChunks[0]);
|
frame.render_widget(screenHeaderPara, headerKeybindChunks[0]);
|
||||||
frame.render_widget(keyBindFooter, headerKeybindChunks[1]);
|
frame.render_widget(keyBindFooter, headerKeybindChunks[1]);
|
||||||
|
|||||||
45
src/ui/textHints.rs
Normal file
45
src/ui/textHints.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
pub mod hints {
|
||||||
|
use ratatui::{
|
||||||
|
style::{Color, Style},
|
||||||
|
text::{Line, Text},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn mainHints<'a>() -> Text<'a> {
|
||||||
|
Text::from(vec![
|
||||||
|
Line::from("(a) Add entry").style(Style::default().fg(Color::Green)),
|
||||||
|
Line::from("(d) Delete entry").style(Style::default().fg(Color::Magenta)),
|
||||||
|
Line::from("(q) Quit").style(Style::default().fg(Color::Red)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addHints<'a>() -> Text<'a> {
|
||||||
|
Text::from(vec![
|
||||||
|
Line::from("(l) 127.0.0.1"),
|
||||||
|
Line::from("(o) 0.0.0.0"),
|
||||||
|
Line::from("(enter) next field"),
|
||||||
|
Line::from("(c) clear"),
|
||||||
|
Line::from("(C) clear all"),
|
||||||
|
Line::from("(esc) main menu").style(Style::default().fg(Color::LightBlue)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exitHints<'a>() -> Text<'a> {
|
||||||
|
Text::from(vec![
|
||||||
|
Line::from("(enter) save and exit").style(Style::default().fg(Color::Red)),
|
||||||
|
Line::from("(esc) main menu").style(Style::default().fg(Color::LightBlue)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delHints<'a>() -> Text<'a> {
|
||||||
|
Text::from(vec![
|
||||||
|
Line::from("(up) move up"),
|
||||||
|
Line::from("(down) move down"),
|
||||||
|
Line::from("(enter) delete selection").style(Style::default().fg(Color::Magenta)),
|
||||||
|
Line::from("(esc) main menu").style(Style::default().fg(Color::LightBlue)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn settingsHints<'a>() -> Text<'a> {
|
||||||
|
Text::from(Line::from("").style(Style::default().fg(Color::Red)))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user