init commit, ui functional

This commit is contained in:
Phani Pavan K
2025-08-25 09:31:17 +05:30
parent f4bdff959e
commit d9b9925d0b
8 changed files with 1479 additions and 0 deletions

42
src/app/entry.rs Normal file
View File

@@ -0,0 +1,42 @@
use serde::{Deserialize, Serialize};
use std::{
error::Error,
fmt::Display,
io::ErrorKind,
num::{IntErrorKind, ParseIntError},
};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Entry {
fromIP: String,
fromPort: String,
toIP: String,
toPort: String,
}
impl Display for Entry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}:{} → {}:{}",
self.fromIP, self.fromPort, self.toIP, self.toPort
)
}
}
impl Entry {
pub fn new(fromIP: String, toIP: String, fromPort: String, toPort: String) -> Option<Self> {
if fromPort.parse::<i32>().is_ok_and(|a| a > 1 && a < 65535)
&& toPort.parse::<i32>().is_ok_and(|a| a > 1 && a < 65535)
{
Some(Entry {
fromIP,
toIP,
fromPort,
toPort,
})
} else {
None
}
}
}

108
src/app/mod.rs Normal file
View File

@@ -0,0 +1,108 @@
pub mod entry;
mod settings;
pub mod status;
use crate::app::{
entry::Entry,
settings::Settings,
status::{CurrentScreen, EditingField, EntryCreation},
};
pub struct AppState {
pub fromIP: String,
pub fromPort: String,
pub toIP: String,
pub toPort: String,
pub screen: CurrentScreen,
pub field: Option<EditingField>,
// pub current: Option<Entry>,
pub currentlyEditing: Option<EditingField>,
pub entries: Vec<Entry>,
pub confDir: String,
}
impl AppState {
pub fn new(confDir: String) -> Self {
let settings = Settings::new(&confDir);
AppState {
fromIP: String::new(),
fromPort: String::new(),
toIP: String::new(),
toPort: String::new(),
currentlyEditing: None,
screen: CurrentScreen::Main,
field: None,
entries: settings.entries,
confDir: confDir,
}
}
pub fn store(&mut self) -> EntryCreation {
match Entry::new(
self.fromIP.clone(),
self.toIP.clone(),
self.fromPort.clone(),
self.toPort.clone(),
) {
Some(entry) => {
self.entries.push(entry);
self.fromIP = String::new();
self.toIP = String::new();
self.fromPort = String::new();
self.toPort = String::new();
self.currentlyEditing = None;
EntryCreation::Success
}
_ => EntryCreation::PortValidationError,
}
}
pub fn startEditing(&mut self) {
if let Some(currentField) = &self.currentlyEditing {
match currentField {
EditingField::ToIP => {}
_ => self.currentlyEditing = Some(EditingField::FromIP),
}
} else {
self.currentlyEditing = Some(EditingField::FromIP);
}
}
pub fn nextField(&mut self) {
if let Some(currentField) = &self.currentlyEditing {
self.currentlyEditing = match currentField {
EditingField::FromIP => Some(EditingField::FromPort),
EditingField::FromPort => Some(EditingField::ToIP),
EditingField::ToIP => Some(EditingField::ToPort),
EditingField::ToPort => Some(EditingField::FromIP),
};
}
}
pub fn prevField(&mut self) {
if let Some(currentField) = &self.currentlyEditing {
self.currentlyEditing = match currentField {
EditingField::FromIP => Some(EditingField::ToPort),
EditingField::FromPort => Some(EditingField::FromIP),
EditingField::ToIP => Some(EditingField::FromPort),
EditingField::ToPort => Some(EditingField::ToIP),
};
}
}
pub fn print(&self) {
let tempEntry = &self.entries[0];
let res = serde_json::to_string(tempEntry).unwrap();
println!("{res}");
let resString = tempEntry.to_string();
println!("{resString}");
}
pub fn save(&self) {
let mut settings = Settings::new(&self.confDir);
settings.entries = self.entries.clone();
settings.save(&self.confDir);
}
}

38
src/app/settings.rs Normal file
View File

@@ -0,0 +1,38 @@
use crate::app::entry::Entry;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[derive(Debug, Serialize, Deserialize)]
pub struct Settings {
pub entries: Vec<Entry>,
}
impl Display for Settings {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Total Entries: {}", self.entries.len())
}
}
impl Settings {
pub fn new(config: &String) -> Self {
let data = std::fs::read_to_string(config);
match data {
Ok(data) => {
let settings =
serde_json::from_str::<Settings>(&data).expect("Settings file corrupted");
settings
}
Err(_) => {
let newSet = Settings { entries: vec![] };
let payload = serde_json::to_string_pretty(&newSet).unwrap();
std::fs::write(config, payload);
newSet
}
}
}
pub fn save(&self, config: &String) {
let payload = serde_json::to_string(self).unwrap();
std::fs::write(config, payload);
}
}

19
src/app/status.rs Normal file
View File

@@ -0,0 +1,19 @@
pub enum CurrentScreen {
Main,
Add,
Settings,
Exit,
}
pub enum EditingField {
FromIP,
FromPort,
ToIP,
ToPort,
}
pub enum EntryCreation {
Success,
PortValidationError,
IPValidationError,
}

164
src/main.rs Normal file
View File

@@ -0,0 +1,164 @@
#[allow(non_snake_case)]
mod app;
mod ui;
use crate::ui::ui;
use app::AppState;
use color_eyre::Result;
use ratatui::Terminal;
use ratatui::crossterm::event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers,
};
use ratatui::crossterm::execute;
use ratatui::crossterm::terminal::{
EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
};
use ratatui::prelude::{Backend, CrosstermBackend};
use std::io;
use crate::app::status::{CurrentScreen, EditingField};
fn main() -> Result<()> {
enable_raw_mode()?;
let mut stderr = io::stderr(); // This is a special case. Normally using stdout is fine
execute!(stderr, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stderr);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let mut app = AppState::new("./conf.json".to_string());
// app.load();
let res = runApp(&mut app, &mut terminal);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Ok(do_print) = res {
if do_print {
app.print();
}
} else if let Err(err) = res {
println!("{err:?}");
}
Ok(())
}
fn runApp<B: Backend>(app: &mut AppState, terminal: &mut Terminal<B>) -> Result<bool> {
loop {
terminal.draw(|f| ui(f, app))?; // from ui.rs
if let Event::Key(key) = event::read()? {
// println!("{key:?}");
if key.kind == event::KeyEventKind::Release {
continue;
}
match app.screen {
CurrentScreen::Settings => match key.code {
KeyCode::Home | KeyCode::Char('m') | KeyCode::Esc => {
app.screen = CurrentScreen::Main;
}
_ => {}
},
CurrentScreen::Main => match key.code {
KeyCode::Char('a') => {
app.screen = CurrentScreen::Add;
app.currentlyEditing = Some(EditingField::FromIP);
}
KeyCode::Char('q') | KeyCode::F(10) | KeyCode::Esc => {
app.screen = CurrentScreen::Exit
}
KeyCode::Char('s') | KeyCode::F(2) => app.screen = CurrentScreen::Settings,
_ => {}
},
CurrentScreen::Add => match (key.modifiers, key.code) {
(KeyModifiers::NONE, KeyCode::Enter) => {
if let Some(eF) = &app.currentlyEditing {
match eF {
EditingField::ToPort => {
app.store();
app.screen = CurrentScreen::Main;
app.currentlyEditing = None;
}
_ => app.nextField(),
}
}
}
(KeyModifiers::NONE, KeyCode::Backspace) => {
if let Some(eF) = &app.currentlyEditing {
match eF {
EditingField::FromIP => {
app.fromIP.pop();
}
EditingField::FromPort => {
app.fromPort.pop();
}
EditingField::ToIP => {
app.toIP.pop();
}
EditingField::ToPort => {
app.toPort.pop();
}
};
}
}
(KeyModifiers::NONE, KeyCode::Esc) => {
app.screen = CurrentScreen::Main;
app.currentlyEditing = None;
}
(KeyModifiers::NONE, KeyCode::Tab) => app.nextField(),
(KeyModifiers::SHIFT, KeyCode::Tab) => app.prevField(),
(m, KeyCode::Char(v)) => {
if let Some(e) = &app.currentlyEditing {
let mut isIP = false;
let opField = match e {
EditingField::FromIP => {
isIP = true;
&mut app.fromIP
}
EditingField::FromPort => &mut app.fromPort,
EditingField::ToIP => {
isIP = true;
&mut app.toIP
}
EditingField::ToPort => &mut app.toPort,
};
match v {
'l' => {
if isIP {
opField.clear();
opField.push_str("127.0.0.1");
}
}
'c' => opField.clear(),
'C' => {
app.fromIP.clear();
app.toIP.clear();
app.fromPort.clear();
app.toPort.clear();
}
_ => opField.push(v),
}
}
}
_ => {}
},
CurrentScreen::Exit => match key.code {
KeyCode::Enter => {
app.save();
return Ok(true);
}
KeyCode::Esc | KeyCode::Char('m') => app.screen = CurrentScreen::Main,
_ => {}
},
}
}
}
// app.run(terminal)
}

194
src/ui.rs Normal file
View File

@@ -0,0 +1,194 @@
#[allow(non_snake_case)]
use ratatui::{
Frame,
layout::{Constraint, Layout},
style::{Color, Style},
text::{Line, Span, Text},
widgets::{Block, Borders, List, ListItem, Paragraph},
};
use ratatui::{
layout::{Direction, Rect},
widgets::Wrap,
};
use crate::app::status::CurrentScreen;
use crate::app::{AppState, status::EditingField};
pub fn ui(frame: &mut Frame, app: &AppState) {
let chunks = Layout::default()
.direction(ratatui::layout::Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Min(3),
Constraint::Length(3),
])
.split(frame.area());
let footerChunks = Layout::default()
.direction(ratatui::layout::Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[2]);
let titleBlock = Block::default()
.borders(Borders::ALL)
.style(Style::default());
let title = Paragraph::new(Text::styled(
"Create new entry",
Style::default().fg(Color::Green),
))
.block(titleBlock);
frame.render_widget(title, chunks[0]);
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),
))));
}
let list = List::new(currentItems);
frame.render_widget(list, chunks[1]);
if let CurrentScreen::Exit = app.screen {
let exitPopup = Block::default()
.title("Save and Exit?")
.borders(Borders::ALL)
.style(Style::default().bg(Color::DarkGray));
let exitText = Text::styled(
"Save the current config and exit? (enter/esc)",
Style::default().fg(Color::Red),
);
let exitPara = Paragraph::new(exitText)
.block(exitPopup)
.wrap(Wrap { trim: false });
let area = centered_rect(60, 25, frame.area());
frame.render_widget(exitPara, area);
}
if let Some(edit) = &app.currentlyEditing {
let popup = Block::default()
.title("Add an entry")
.borders(Borders::NONE)
.style(Style::default().bg(Color::DarkGray));
let area = centered_rect(60, 25, frame.area());
frame.render_widget(popup, area);
let fields = Layout::default()
.direction(Direction::Horizontal)
.margin(1)
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(area);
let mut fromIPBlock = Block::default().title("From IP").borders(Borders::ALL);
let mut toIPBlock = Block::default().title("To IP").borders(Borders::ALL);
let mut fromPortBlock = Block::default().title("From Port").borders(Borders::ALL);
let mut toPortBlock = Block::default().title("To Port").borders(Borders::ALL);
let activeStyle = Style::default().bg(Color::LightYellow).fg(Color::Black);
match edit {
EditingField::FromIP => fromIPBlock = fromIPBlock.style(activeStyle),
EditingField::ToIP => toIPBlock = toIPBlock.style(activeStyle),
EditingField::FromPort => fromPortBlock = fromPortBlock.style(activeStyle),
EditingField::ToPort => toPortBlock = toPortBlock.style(activeStyle),
}
let fromIPPara = Paragraph::new(app.fromIP.clone()).block(fromIPBlock);
let toIPPara = Paragraph::new(app.toIP.clone()).block(toIPBlock);
let fromPortPara = Paragraph::new(app.fromPort.clone()).block(fromPortBlock);
let toPortPara = Paragraph::new(app.toPort.clone()).block(toPortBlock);
frame.render_widget(fromIPPara, fields[0]);
frame.render_widget(fromPortPara, fields[1]);
frame.render_widget(toIPPara, fields[2]);
frame.render_widget(toPortPara, fields[3]);
};
let screenHelp = 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::Settings => Span::styled("Settings", Style::default().fg(Color::Blue)),
}
.to_owned(),
Span::styled(" | ", Style::default().fg(Color::White)),
{
if let Some(editing) = &app.currentlyEditing {
let curEdit = match editing {
EditingField::FromIP => "From IP",
EditingField::ToIP => "To IP",
EditingField::FromPort => "From Port",
EditingField::ToPort => "To Port",
_ => "Something ",
};
Span::styled(
format!("Editing: {curEdit}"),
Style::default().fg(Color::Green),
)
} else {
Span::styled("Not Editing", Style::default().fg(Color::DarkGray))
}
},
];
let helpFooter =
Paragraph::new(Line::from(screenHelp)).block(Block::default().borders(Borders::ALL));
let keybinds = {
match app.screen {
CurrentScreen::Main => Span::styled(
"(a) add entry / (q) save and quit",
Style::default().fg(Color::Red),
),
CurrentScreen::Add => Span::styled(
"(l) localhost / (enter/tab) next field / (c) clear / (C) clear all / (esc) main menu",
Style::default().fg(Color::Red),
),
CurrentScreen::Settings => Span::styled("", Style::default().fg(Color::Red)),
CurrentScreen::Exit => Span::styled(
"(enter) save and exit / (esc) main menu",
Style::default().fg(Color::Red),
),
}
};
let keyBindFooter =
Paragraph::new(Line::from(keybinds)).block(Block::default().borders(Borders::ALL));
frame.render_widget(helpFooter, footerChunks[0]);
frame.render_widget(keyBindFooter, footerChunks[1]);
}
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
// Cut the given rectangle into three vertical pieces
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(r);
// Then cut the middle vertical piece into three width-wise pieces
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1] // Return the middle chunk
}