handle * ip, ui refac, fix #1 to handle .conf
Some checks failed
/ Quality Check (push) Failing after 1m5s
/ Build (push) Successful in 1m7s

This commit is contained in:
Phani Pavan K
2025-10-26 11:45:08 +05:30
parent 9314e97c60
commit f83d6038c8
7 changed files with 158 additions and 124 deletions

View File

@@ -29,7 +29,14 @@ impl Entry {
toPort: String, toPort: String,
) -> Result<Self, EntryValError> { ) -> Result<Self, EntryValError> {
let ip = Regex::new("^(?:25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\\.(?:25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\\.(?:25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\\.(?:25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])$").unwrap(); let ip = Regex::new("^(?:25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\\.(?:25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\\.(?:25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])\\.(?:25[0-5]|2[0-4][0-9]|1?[0-9]?[0-9])$").unwrap();
if !ip.is_match(fromIP.trim()) {
let newFromIP = if fromIP.eq("*") {
"0.0.0.0".to_string()
} else {
fromIP
};
if !ip.is_match(newFromIP.trim()) {
Err(EntryValError::FromIPValError) Err(EntryValError::FromIPValError)
} else if !fromPort.parse::<i32>().is_ok_and(|a| a > 1 && a < 65535) { } else if !fromPort.parse::<i32>().is_ok_and(|a| a > 1 && a < 65535) {
Err(EntryValError::FromPortValError) Err(EntryValError::FromPortValError)
@@ -39,7 +46,7 @@ impl Entry {
Err(EntryValError::ToPortValError) Err(EntryValError::ToPortValError)
} else { } else {
Ok(Entry { Ok(Entry {
fromIP, fromIP: newFromIP,
toIP, toIP,
fromPort: fromPort.trim_start_matches('0').to_string(), fromPort: fromPort.trim_start_matches('0').to_string(),
toPort: toPort.trim_start_matches('0').to_string(), toPort: toPort.trim_start_matches('0').to_string(),

View File

@@ -3,6 +3,8 @@ pub mod entry;
mod settings; mod settings;
pub mod status; pub mod status;
use std::{thread::sleep, time::Duration};
use crate::app::{ use crate::app::{
entry::Entry, entry::Entry,
settings::Settings, settings::Settings,
@@ -102,13 +104,13 @@ impl AppState {
} }
} }
pub fn print(&self) { // pub fn print(&self) {
let tempEntry = &self.entries[0]; // let tempEntry = &self.entries[0];
let res = serde_json::to_string(tempEntry).unwrap(); // let res = serde_json::to_string(tempEntry).unwrap();
println!("{res}"); // println!("{res}");
let resString = tempEntry.to_string(); // let resString = tempEntry.to_string();
println!("{resString}"); // println!("{resString}");
} // }
pub fn saveConfigToScript(&self) { pub fn saveConfigToScript(&self) {
let mut outputString: String = String::new(); let mut outputString: String = String::new();
@@ -120,6 +122,12 @@ impl AppState {
)) ))
} }
outputString.push_str("\nwait"); outputString.push_str("\nwait");
// for o in 0..1 {
// println!("test {o}");
// sleep(Duration::from_secs(1));
// }
println!("Writing config to system");
sleep(Duration::from_millis(100));
let _ = std::fs::write("./forward.sh", outputString); let _ = std::fs::write("./forward.sh", outputString);
} }

View File

@@ -2,6 +2,9 @@
mod app; mod app;
mod ui; mod ui;
use crate::app::status::{
AppStatus, CurrentScreen, EditingField, EntryValError, entryValError2Field,
};
use crate::ui::ui; use crate::ui::ui;
use app::AppState; use app::AppState;
use color_eyre::Result; use color_eyre::Result;
@@ -14,12 +17,9 @@ use ratatui::crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
}; };
use ratatui::prelude::{Backend, CrosstermBackend}; use ratatui::prelude::{Backend, CrosstermBackend};
use std::env::var;
use std::io; use std::io;
use crate::app::status::{
AppStatus, CurrentScreen, EditingField, EntryValError, entryValError2Field,
};
fn main() -> Result<()> { fn main() -> Result<()> {
enable_raw_mode()?; enable_raw_mode()?;
let mut stderr = io::stderr(); // This is a special case. Normally using stdout is fine let mut stderr = io::stderr(); // This is a special case. Normally using stdout is fine
@@ -27,10 +27,13 @@ fn main() -> Result<()> {
let backend = CrosstermBackend::new(stderr); let backend = CrosstermBackend::new(stderr);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
let configHome = var("XDG_CONFIG_HOME")
.or_else(|_| var("HOME").map(|home| format!("{}/.config", home)))
.unwrap_or("~/.config".to_string());
// create app and run it // create app and run it
let mut app = AppState::new("./conf.json".to_string()); let mut appSettings = AppState::new(format!("{configHome}/steckbrett.conf"));
// app.load(); // app.load();
let res = runApp(&mut app, &mut terminal); let _ = runApp(&mut appSettings, &mut terminal);
disable_raw_mode()?; disable_raw_mode()?;
execute!( execute!(
terminal.backend_mut(), terminal.backend_mut(),
@@ -39,17 +42,15 @@ fn main() -> Result<()> {
)?; )?;
terminal.show_cursor()?; terminal.show_cursor()?;
if let Ok(do_print) = res { // match res {
if do_print { // Ok(ent) => println!("{ent} entries "),
app.print(); // _ => {}
} // }
} else if let Err(err) = res {
println!("{err:?}");
}
Ok(()) Ok(())
} }
fn runApp<B: Backend>(app: &mut AppState, terminal: &mut Terminal<B>) -> Result<bool> { fn runApp<B: Backend>(app: &mut AppState, terminal: &mut Terminal<B>) -> Result<usize> {
loop { loop {
terminal.draw(|f| ui(f, app))?; // from ui.rs terminal.draw(|f| ui(f, app))?; // from ui.rs
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
@@ -86,6 +87,7 @@ fn runApp<B: Backend>(app: &mut AppState, terminal: &mut Terminal<B>) -> Result<
app.saveConfigToScript(); app.saveConfigToScript();
app.saveConfigToSettingsFile(); app.saveConfigToSettingsFile();
app.appStatus = AppStatus::Saved; app.appStatus = AppStatus::Saved;
terminal.clear()?;
} }
} }
KeyCode::F(2) => app.screen = CurrentScreen::Settings, KeyCode::F(2) => app.screen = CurrentScreen::Settings,
@@ -184,9 +186,9 @@ fn runApp<B: Backend>(app: &mut AppState, terminal: &mut Terminal<B>) -> Result<
}, },
CurrentScreen::Exit => match key.code { CurrentScreen::Exit => match key.code {
KeyCode::Enter => { KeyCode::Enter => {
app.saveConfigToSettingsFile(); // app.saveConfigToSettingsFile();
app.saveConfigToScript(); // app.saveConfigToScript();
return Ok(true); return Ok(app.settings.entries.len());
} }
KeyCode::Esc | KeyCode::Char('m') => app.screen = CurrentScreen::Main, KeyCode::Esc | KeyCode::Char('m') => app.screen = CurrentScreen::Main,
_ => {} _ => {}

72
src/ui/entryTable.rs Normal file
View File

@@ -0,0 +1,72 @@
use crate::app::{
entry::Entry,
status::{AppStatus, CurrentScreen},
};
use ratatui::{
layout::Constraint,
style::{Color, Modifier, Style},
text::Text,
widgets::{Block, Borders, Cell, Row, Table},
};
pub fn getTableElement<'a>(
entries: &Vec<Entry>,
curScreen: &CurrentScreen,
appStatus: &AppStatus,
isHashDifferent: bool,
) -> Table<'static> {
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 = 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 curScreen {
CurrentScreen::Delete => Color::Yellow,
_ => Color::DarkGray,
};
let tableBorderColor = match appStatus {
AppStatus::Saved => Color::LightGreen,
AppStatus::Welcome => Color::White,
_ => {
if isHashDifferent {
Color::LightYellow
} else {
Color::White
}
}
};
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())
.style(Style::default().fg(tableBorderColor)),
)
.row_highlight_style(
Style::default()
.add_modifier(Modifier::REVERSED)
.fg(selectedColor),
);
table
}

25
src/ui/exitPrompt.rs Normal file
View File

@@ -0,0 +1,25 @@
use ratatui::{
style::{Color, Style},
text::{Line, Text},
widgets::{Block, Borders, Paragraph, Wrap},
};
pub fn getExitPara() -> Paragraph<'static> {
let exitPopup = Block::default()
.title("Exit Window")
.borders(Borders::ALL)
.style(Style::default().bg(Color::DarkGray))
.border_style(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 })
.style(Style::default().fg(Color::White));
exitPara
}

View File

@@ -1,13 +1,15 @@
#![allow(non_snake_case)] #![allow(non_snake_case)]
mod centeredRect; mod centeredRect;
mod entryTable;
mod exitPrompt;
mod textHints; mod textHints;
use ratatui::{ use ratatui::{
Frame, Frame,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style}, style::{Color, Style},
text::{Line, Span, Text}, text::{Line, Span, Text},
widgets::{Block, Borders, Cell, Paragraph, Row, Table, Wrap}, widgets::{Block, Borders, Paragraph},
}; };
use crate::app::status::{AppStatus, CurrentScreen, EntryValError}; use crate::app::status::{AppStatus, CurrentScreen, EntryValError};
@@ -48,104 +50,22 @@ pub fn ui(frame: &mut Frame, app: &mut AppState) {
frame.render_widget(title, titleBodyChunks[0]); frame.render_widget(title, titleBodyChunks[0]);
// Create and add body
// let mut currentItems = Vec::<ListItem>::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 // table drawing
let headers = ["No.", "From IP", "From Port", "-->", "To IP", "To Port"] let table = entryTable::getTableElement(
.into_iter() &app.entries,
.map(Cell::from) &app.screen,
.collect::<Row>() &app.appStatus,
.style(Style::default().fg(Color::White).bg(Color::Black)) app.isHashDifferent(),
.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 tableBorderColor = match app.appStatus {
AppStatus::Saved => Color::LightGreen,
AppStatus::Welcome => Color::White,
_ => {
if app.isHashDifferent() {
Color::LightYellow
} else {
Color::White
}
}
};
// if app.appStatus == AppStatus::Saved {
// Color::LightGreen
// } else if app.appStatus == AppStatus::Welcome {
// Color::White
// } else {
// if app.isHashDifferent() {
// Color::LightYellow
// } else {
// Color::White
// }
// };
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())
.style(Style::default().fg(tableBorderColor)),
)
.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); 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() frame.render_widget(
.title("Exit Window") exitPrompt::getExitPara(),
.borders(Borders::ALL) centered_rect(60, 25, titleBodyChunks[1]),
.style(Style::default().bg(Color::DarkGray)) );
.border_style(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 })
.style(Style::default().fg(Color::White));
let area = centered_rect(60, 25, titleBodyChunks[1]);
frame.render_widget(exitPara, area);
} }
// Editing screen popup // Editing screen popup
@@ -154,7 +74,7 @@ pub fn ui(frame: &mut Frame, app: &mut 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(75, 20, titleBodyChunks[1]); let area = centered_rect(75, 30, titleBodyChunks[1]);
frame.render_widget(popup, area); frame.render_widget(popup, area);
let fields = Layout::default() let fields = Layout::default()

View File

@@ -27,16 +27,16 @@ pub mod hints {
pub fn exitHints<'a>() -> Text<'a> { pub fn exitHints<'a>() -> Text<'a> {
Text::from(vec![ Text::from(vec![
Line::from("(enter) save and exit").style(Style::default().fg(Color::Red)), Line::from("(enter) Exit").style(Style::default().fg(Color::Red)),
Line::from("(esc) main menu").style(Style::default().fg(Color::LightBlue)), Line::from("(esc) Main menu").style(Style::default().fg(Color::LightBlue)),
]) ])
} }
pub fn delHints<'a>() -> Text<'a> { pub fn delHints<'a>() -> Text<'a> {
Text::from(vec![ Text::from(vec![
Line::from("(↑/↓) Scroll").style(Style::default().fg(Color::White)), Line::from("(↑/↓) Scroll").style(Style::default().fg(Color::White)),
Line::from("(enter) delete selection").style(Style::default().fg(Color::Magenta)), Line::from("(enter) Delete selection").style(Style::default().fg(Color::Magenta)),
Line::from("(esc) main menu").style(Style::default().fg(Color::LightBlue)), Line::from("(esc) Main menu").style(Style::default().fg(Color::LightBlue)),
]) ])
} }