pub mod browse_maps;
pub mod browse_routes;
pub mod clean;
pub mod compile;
pub mod extract;
pub mod file_browser;
pub mod home;
pub mod optimize;
pub mod vrp;
use ratatui::Frame;
use crate::app::{App, View};
pub fn draw_panel(f: &mut Frame, title: &str, area: ratatui::layout::Rect) -> ratatui::layout::Rect {
let block = ratatui::widgets::Block::default()
.title(format!(" {} ", title))
.borders(ratatui::widgets::Borders::ALL)
.border_style(ratatui::style::Style::default().fg(ratatui::style::Color::Cyan));
let inner = block.inner(area);
f.render_widget(block, area);
inner
}
pub fn draw(f: &mut Frame, app: &App) {
let chunks = ratatui::layout::Layout::default()
.direction(ratatui::layout::Direction::Vertical)
.constraints([
ratatui::layout::Constraint::Length(3), ratatui::layout::Constraint::Min(10), ratatui::layout::Constraint::Length(8), ratatui::layout::Constraint::Length(1), ])
.split(f.area());
draw_header(f, chunks[0]);
draw_main(f, app, chunks[1]);
draw_logs(f, app, chunks[2]);
draw_footer(f, app, chunks[3]);
}
fn draw_header(f: &mut Frame, area: ratatui::layout::Rect) {
let title = format!(" rmpca - Route Optimization TUI [v{}] ", env!("CARGO_PKG_VERSION"));
let block = ratatui::widgets::Block::default()
.borders(ratatui::widgets::Borders::ALL)
.border_style(ratatui::style::Style::default().fg(ratatui::style::Color::Cyan));
let paragraph = ratatui::widgets::Paragraph::new(ratatui::text::Span::styled(
title,
ratatui::style::Style::default().fg(ratatui::style::Color::Cyan),
))
.block(block)
.alignment(ratatui::layout::Alignment::Center);
f.render_widget(paragraph, area);
}
fn draw_main(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
match app.current_view {
View::Home => home::draw(f, app, area),
View::Extract => extract::draw(f, app, area),
View::Compile => compile::draw(f, app, area),
View::Optimize => optimize::draw(f, app, area),
View::Vrp => vrp::draw(f, app, area),
View::BrowseMaps => browse_maps::draw(f, app, area),
View::BrowseRoutes => browse_routes::draw(f, app, area),
View::FileBrowser => file_browser::draw(f, app, area),
View::Help => draw_help(f, area),
View::Clean => clean::draw(f, app, area),
}
}
fn draw_logs(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
let block = ratatui::widgets::Block::default()
.title(" Logs ")
.borders(ratatui::widgets::Borders::ALL)
.border_style(ratatui::style::Style::default().fg(ratatui::style::Color::DarkGray));
let inner = block.inner(area);
f.render_widget(block, area);
let visible = inner.height as usize;
let entries: Vec<ratatui::text::Line> = app
.log_entries
.iter()
.rev()
.take(visible)
.rev()
.map(|entry| {
let ts_style = ratatui::style::Style::default().fg(ratatui::style::Color::Cyan);
let level_style = match entry.level {
crate::app::LogLevel::Info => {
ratatui::style::Style::default().fg(ratatui::style::Color::Cyan)
}
crate::app::LogLevel::Success => {
ratatui::style::Style::default().fg(ratatui::style::Color::Green)
}
crate::app::LogLevel::Warn => {
ratatui::style::Style::default().fg(ratatui::style::Color::Yellow)
}
crate::app::LogLevel::Error => {
ratatui::style::Style::default().fg(ratatui::style::Color::Red)
}
};
ratatui::text::Line::from(vec![
ratatui::text::Span::styled(format!("{} ", entry.timestamp), ts_style),
ratatui::text::Span::styled(format!("[{}] ", entry.level), level_style),
ratatui::text::Span::raw(entry.message.clone()),
])
})
.collect();
let paragraph = ratatui::widgets::Paragraph::new(entries);
f.render_widget(paragraph, inner);
}
fn draw_footer(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
let text = match app.current_view {
View::Home | View::Extract | View::Optimize => {
"[q] Quit [Esc] Home [h/F1] Help [↑↓] Navigate [Enter] Select"
}
View::Vrp => {
"[Esc] Home [I] Input [W] Waypoints [V] Vehicles [A] Algo [D] Depot [Enter] Run VRP"
}
View::Compile => "[q] Quit [Esc] Home [I] Input file [O] Output file [Enter] Compile",
View::BrowseMaps => {
"[↑↓] Navigate [Enter] Select -> Optimize [d] Delete [r] Refresh [Esc] Home"
}
View::BrowseRoutes => "[↑↓] Navigate [Enter] View [d] Delete [r] Refresh [Esc] Home",
View::FileBrowser => {
"[↑↓] Navigate [Enter] Select [Backspace] Parent [h] Toggle hidden [Esc] Cancel"
}
View::Help => "[Esc] Home [q] Quit",
View::Clean => {
"[Esc] Home [I] Input file [O] Output file [Space] Toggle [Enter] Run clean"
}
};
let paragraph = ratatui::widgets::Paragraph::new(ratatui::text::Span::styled(
text,
ratatui::style::Style::default()
.fg(ratatui::style::Color::DarkGray)
.add_modifier(ratatui::style::Modifier::REVERSED),
));
f.render_widget(paragraph, area);
}
fn draw_help(f: &mut Frame, area: ratatui::layout::Rect) {
let block = ratatui::widgets::Block::default()
.title(" Help ")
.borders(ratatui::widgets::Borders::ALL)
.border_style(ratatui::style::Style::default().fg(ratatui::style::Color::Cyan));
let lines = vec![
ratatui::text::Line::from(""),
ratatui::text::Line::from(ratatui::text::Span::styled(
"rmpca — Route Optimization TUI",
ratatui::style::Style::default()
.fg(ratatui::style::Color::Cyan)
.add_modifier(ratatui::style::Modifier::BOLD),
)),
ratatui::text::Line::from(""),
ratatui::text::Line::from("Global Keys:"),
ratatui::text::Line::from(" [q] Quit application"),
ratatui::text::Line::from(" [Esc] Return to home"),
ratatui::text::Line::from(" [h/F1] Toggle this help"),
ratatui::text::Line::from(" [↑↓] Navigate menus"),
ratatui::text::Line::from(" [Enter] Select / confirm"),
ratatui::text::Line::from(""),
ratatui::text::Line::from("Workflow Steps:"),
ratatui::text::Line::from(" 1. Extract road data from OSM or Overture"),
ratatui::text::Line::from(" 2. Compile GeoJSON into binary .rmp format"),
ratatui::text::Line::from(" 3. Clean GeoJSON (repair, dedupe, simplify)"),
ratatui::text::Line::from(" 4. Optimize route with turn penalties"),
ratatui::text::Line::from(" 5. Browse cached maps and saved routes"),
ratatui::text::Line::from(""),
ratatui::text::Line::from("Core algorithms: CPP (Eulerian circuit), TSP (2-opt),"),
ratatui::text::Line::from("VRP (OR-Tools), haversine distance, bearing-based turns."),
];
let paragraph = ratatui::widgets::Paragraph::new(lines).block(block);
f.render_widget(paragraph, area);
}
pub fn draw_input_prompt(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
if !app.input_mode.active {
return;
}
let label = match app.input_mode.field {
crate::app::InputField::BoundingBox => "Bounding Box (min_lon,min_lat,max_lon,max_lat)",
crate::app::InputField::InputFile => "Input GeoJSON file path",
crate::app::InputField::OutputFile => "Output .rmp file path",
crate::app::InputField::CacheFile => "Cache map file path",
crate::app::InputField::RouteFile => "Route input file path",
crate::app::InputField::LeftTurnPenalty => "Left turn penalty",
crate::app::InputField::RightTurnPenalty => "Right turn penalty",
crate::app::InputField::UTurnPenalty => "U-turn penalty",
crate::app::InputField::DepotCoordinates => "Depot coordinates (lat,lon)",
crate::app::InputField::NumVehicles => "Number of vehicles",
crate::app::InputField::SolverId => {
"Solver ID (clarke_wright, sweep, two_opt, or_opt, default)"
}
crate::app::InputField::CleanInputFile => "Clean input GeoJSON file path",
crate::app::InputField::CleanOutputFile => "Clean output GeoJSON file path",
crate::app::InputField::VrpInputFile => "VRP input .rmp file path",
crate::app::InputField::VrpOutputDir => "VRP output directory",
crate::app::InputField::VrpCsvFile => "VRP coordinates CSV file path",
crate::app::InputField::VrpAlgorithm => "VRP algorithm (greedy|savings|local_search|simulated_annealing)",
crate::app::InputField::VrpCapacity => "VRP vehicle capacity",
crate::app::InputField::VrpWaypointsFile => "VRP waypoints file path (.json)",
crate::app::InputField::VrpDepot => "VRP depot (lat,lon)",
};
let popup_area = ratatui::layout::Rect {
x: area.x + 2,
y: area.y + area.height.saturating_sub(4),
width: area.width.saturating_sub(4),
height: 3,
};
let input_block = ratatui::widgets::Block::default()
.title(format!(" {} ", label))
.borders(ratatui::widgets::Borders::ALL)
.border_style(ratatui::style::Style::default().fg(ratatui::style::Color::Yellow));
let display = format!("{}█", app.input_mode.buffer);
let paragraph = ratatui::widgets::Paragraph::new(ratatui::text::Span::styled(
display,
ratatui::style::Style::default().fg(ratatui::style::Color::White),
))
.block(input_block);
f.render_widget(paragraph, popup_area);
}
pub fn draw_selectable_list(
f: &mut Frame,
area: ratatui::layout::Rect,
title: &str,
items: &[String],
selection: usize,
) {
let block = ratatui::widgets::Block::default()
.title(format!(" {} ({}) ", title, items.len()))
.borders(ratatui::widgets::Borders::ALL)
.border_style(ratatui::style::Style::default().fg(ratatui::style::Color::Cyan));
let inner = block.inner(area);
f.render_widget(block, area);
if items.is_empty() {
return;
}
let list_items: Vec<ratatui::widgets::ListItem> = items
.iter()
.enumerate()
.map(|(i, name)| {
let style = if i == selection {
ratatui::style::Style::default()
.fg(ratatui::style::Color::Yellow)
.add_modifier(ratatui::style::Modifier::BOLD)
} else {
ratatui::style::Style::default().fg(ratatui::style::Color::White)
};
let prefix = if i == selection { " > " } else { " " };
ratatui::widgets::ListItem::new(ratatui::text::Span::styled(
format!("{}{}", prefix, name),
style,
))
})
.collect();
let list = ratatui::widgets::List::new(list_items);
f.render_widget(list, inner);
}
pub fn draw_empty_placeholder(
f: &mut ratatui::Frame,
area: ratatui::layout::Rect,
title: &str,
empty_msg: &str,
action_msg: &str,
) {
let block = ratatui::widgets::Block::default()
.title(format!(" {} ", title))
.borders(ratatui::widgets::Borders::ALL)
.border_style(ratatui::style::Style::default().fg(ratatui::style::Color::Cyan));
let inner = block.inner(area);
f.render_widget(block, area);
let lines = vec![
ratatui::text::Line::from(""),
ratatui::text::Line::from(empty_msg.to_string()),
ratatui::text::Line::from(""),
ratatui::text::Line::from(ratatui::text::Span::styled(
action_msg.to_string(),
ratatui::style::Style::default().fg(ratatui::style::Color::DarkGray),
)),
ratatui::text::Line::from(""),
ratatui::text::Line::from(ratatui::text::Span::styled(
"(press Esc to return home)",
ratatui::style::Style::default().fg(ratatui::style::Color::DarkGray),
)),
];
let paragraph = ratatui::widgets::Paragraph::new(lines);
f.render_widget(paragraph, inner);
}