diff --git a/src/app/entry.rs b/src/app/entry.rs index caa4dbb..60ecf58 100644 --- a/src/app/entry.rs +++ b/src/app/entry.rs @@ -39,9 +39,21 @@ impl Entry { Ok(Entry { fromIP, toIP, - fromPort, - toPort, + fromPort: fromPort.trim_start_matches('0').to_string(), + toPort: toPort.trim_start_matches('0').to_string(), }) } } + + pub fn array(&self, idx: usize) -> Vec { + let d = self.clone(); + vec![ + format!("{}", idx), + d.fromIP, + d.fromPort, + String::new(), + d.toIP, + d.toPort, + ] + } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 9b1d9f6..6d4b35d 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -3,6 +3,8 @@ pub mod entry; mod settings; pub mod status; +use ratatui::widgets::TableState; + use crate::app::{ entry::Entry, settings::Settings, @@ -18,6 +20,7 @@ pub struct AppState { pub currentlyEditing: Option, pub entries: Vec, pub confDir: String, + pub tableState: TableState, } impl AppState { @@ -32,6 +35,7 @@ impl AppState { screen: CurrentScreen::Main, entries: settings.entries, confDir, + tableState: TableState::default().with_selected(0), } } @@ -49,7 +53,8 @@ impl AppState { self.fromPort = String::new(); self.toPort = String::new(); self.currentlyEditing = None; - + self.tableState + .select(Some(self.entries.len() - 1 as usize)); EntryCreation::Success } _ => EntryCreation::PortValidationError, @@ -104,4 +109,40 @@ impl AppState { settings.entries = self.entries.clone(); 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()); + } } diff --git a/src/app/status.rs b/src/app/status.rs index ed86c38..46b1f6f 100644 --- a/src/app/status.rs +++ b/src/app/status.rs @@ -2,6 +2,7 @@ pub enum CurrentScreen { Main, Add, Settings, + Delete, Exit, } diff --git a/src/main.rs b/src/main.rs index 56816a7..2454ac8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,6 +63,13 @@ fn runApp(app: &mut AppState, terminal: &mut Terminal) -> 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 { KeyCode::Char('a') => { app.screen = CurrentScreen::Add; @@ -72,6 +79,9 @@ fn runApp(app: &mut AppState, terminal: &mut Terminal) -> Result< app.screen = CurrentScreen::Exit } 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) { @@ -134,6 +144,12 @@ fn runApp(app: &mut AppState, terminal: &mut Terminal) -> Result< opField.push_str("127.0.0.1"); } } + 'o' => { + if isIP { + opField.clear(); + opField.push_str("0.0.0.0"); + } + } 'c' => opField.clear(), 'C' => { app.fromIP.clear(); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d98337e..8314392 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,19 +1,21 @@ #![allow(non_snake_case)] mod centeredRect; +mod textHints; use ratatui::{ Frame, layout::{Constraint, Direction, Layout}, - style::{Color, Style}, + style::{Color, Modifier, Style}, 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::{AppState, status::EditingField}; 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 let screenChunks = Layout::default() .direction(ratatui::layout::Direction::Horizontal) @@ -47,32 +49,74 @@ pub fn ui(frame: &mut Frame, app: &AppState) { frame.render_widget(title, titleBodyChunks[0]); // Create and add body - let mut currentItems = Vec::::new(); - for (i, e) in app.entries.iter().enumerate() { - currentItems.push(ListItem::new(Line::from(Span::styled( - format!("{i}. {e}"), - Style::default().fg(Color::Yellow), - )))); - } - let list = List::new(currentItems).block(Block::default().borders(Borders::all())); - frame.render_widget(list, titleBodyChunks[1]); + // let mut currentItems = Vec::::new(); + // for (i, e) in app.entries.iter().enumerate() { + // currentItems.push(ListItem::new(Line::from(Span::styled( + // format!("{i}. {e}"), + // Style::default().fg(Color::Yellow), + // )))); + // } + + // table drawing + let headers = ["No.", "From IP", "From Port", "-->", "To IP", "To Port"] + .into_iter() + .map(Cell::from) + .collect::() + .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::() + .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 if let CurrentScreen::Exit = app.screen { let exitPopup = Block::default() - .title("Save and Exit?") + .title("Exit Window") .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( - "Save the current config and exit? (enter/esc)", - Style::default().fg(Color::Red), - ); + let exitText = Text::from(vec![ + Line::from("Exit the app?"), + Line::from("ANY UNSAVED CHANGES WILL BE DISCARDED."), + Line::from("Press (s) to save from the main screen."), + ]); let exitPara = Paragraph::new(exitText) .block(exitPopup) .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); } @@ -82,7 +126,7 @@ pub fn ui(frame: &mut Frame, app: &AppState) { .title("Add an entry") .borders(Borders::NONE) .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); let fields = Layout::default() @@ -123,14 +167,30 @@ pub fn ui(frame: &mut Frame, app: &AppState) { // -------------------------------- // Current page title and status + let mut borderColor = Color::White; + let _ = borderColor; // to suppress warning, remove later let screenHeader = vec![ match app.screen { - CurrentScreen::Main => Span::styled("Main Screen", Style::default().fg(Color::Green)), - CurrentScreen::Add => Span::styled("Add entry", Style::default().fg(Color::Yellow)), - CurrentScreen::Exit => { - Span::styled("Exit Screen", Style::default().fg(Color::LightRed)) + CurrentScreen::Main => { + borderColor = Color::LightBlue; + Span::styled("Main Screen", Style::default().fg(Color::LightBlue)) + } + 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(), 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)) - .block(Block::default().borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)); + let screenHeaderPara = Paragraph::new(Line::from(screenHeader)).block( + Block::default() + .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT) + .style(Style::default().fg(borderColor)), + ); // Keybinds Entry let keybinds = { match app.screen { - CurrentScreen::Main => Text::from(vec![ - Line::from("(a) add entry").style(Style::default().fg(Color::Green)), - Line::from("(q) save and quit").style(Style::default().fg(Color::Green)), - ]), - CurrentScreen::Add => Text::from(vec![ - 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)), - ]), + CurrentScreen::Main => hints::mainHints(), + CurrentScreen::Add => hints::addHints(), + CurrentScreen::Settings => hints::settingsHints(), + CurrentScreen::Delete => hints::delHints(), + CurrentScreen::Exit => hints::exitHints(), } }; - let keyBindFooter = Paragraph::new(keybinds) - .block(Block::default().borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT)); + let keyBindFooter = Paragraph::new(keybinds).block( + Block::default() + .borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT) + .style(Style::default().fg(borderColor)), + ); frame.render_widget(screenHeaderPara, headerKeybindChunks[0]); frame.render_widget(keyBindFooter, headerKeybindChunks[1]); diff --git a/src/ui/textHints.rs b/src/ui/textHints.rs new file mode 100644 index 0000000..7d5e3cc --- /dev/null +++ b/src/ui/textHints.rs @@ -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))) + } +}