added colours, select and delete ops, refac
Some checks failed
/ Quality Check (push) Failing after 1m41s
/ Build (push) Successful in 1m36s

This commit is contained in:
Phani Pavan K
2025-10-19 13:06:50 +05:30
parent d4d2c5e3eb
commit d713cc8fa3
6 changed files with 218 additions and 50 deletions

View File

@@ -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,
]
}
} }

View File

@@ -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());
}
} }

View File

@@ -2,6 +2,7 @@ pub enum CurrentScreen {
Main, Main,
Add, Add,
Settings, Settings,
Delete,
Exit, Exit,
} }

View File

@@ -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();

View File

@@ -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
View 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)))
}
}