refactor: cleanup entry point

This commit is contained in:
Edward Langley
2026-03-30 23:30:57 -07:00
parent 4fb97c89ed
commit 3e3ac05b05

View File

@ -1,10 +1,10 @@
mod model;
mod formula;
mod view;
mod ui;
mod import;
mod persistence;
mod command; mod command;
mod formula;
mod import;
mod model;
mod persistence;
mod ui;
mod view;
use std::io; use std::io;
use std::path::PathBuf; use std::path::PathBuf;
@ -24,18 +24,24 @@ use ratatui::{
Frame, Terminal, Frame, Terminal,
}; };
use model::Model;
use ui::app::{App, AppMode}; use ui::app::{App, AppMode};
use ui::grid::GridWidget;
use ui::tile_bar::TileBar;
use ui::formula_panel::FormulaPanel;
use ui::category_panel::CategoryPanel; use ui::category_panel::CategoryPanel;
use ui::view_panel::ViewPanel; use ui::formula_panel::FormulaPanel;
use ui::grid::GridWidget;
use ui::help::HelpWidget; use ui::help::HelpWidget;
use ui::import_wizard_ui::ImportWizardWidget; use ui::import_wizard_ui::ImportWizardWidget;
use model::Model; use ui::tile_bar::TileBar;
use ui::view_panel::ViewPanel;
fn main() -> Result<()> { struct CmdLineArgs {
let args: Vec<String> = std::env::args().collect(); file_path: Option<PathBuf>,
headless_cmds: Vec<String>,
headless_script: Option<PathBuf>,
import_path: Option<PathBuf>,
}
fn parse_args(args: Vec<String>) -> Option<CmdLineArgs> {
let mut file_path: Option<PathBuf> = None; let mut file_path: Option<PathBuf> = None;
let mut headless_cmds: Vec<String> = Vec::new(); let mut headless_cmds: Vec<String> = Vec::new();
let mut headless_script: Option<PathBuf> = None; let mut headless_script: Option<PathBuf> = None;
@ -60,7 +66,7 @@ fn main() -> Result<()> {
} }
"--help" | "-h" => { "--help" | "-h" => {
print_usage(); print_usage();
return Ok(()); return None;
} }
arg if !arg.starts_with('-') => { arg if !arg.starts_with('-') => {
file_path = Some(PathBuf::from(arg)); file_path = Some(PathBuf::from(arg));
@ -70,15 +76,33 @@ fn main() -> Result<()> {
i += 1; i += 1;
} }
return Some(CmdLineArgs {
file_path,
headless_cmds,
headless_script,
import_path,
});
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().collect();
let maybe_cmd_line_args = parse_args(args);
if maybe_cmd_line_args.is_none() {
return Ok(());
}
let cmd_line_args = maybe_cmd_line_args.unwrap();
// Load or create model // Load or create model
let mut model = if let Some(ref path) = file_path { let mut model = if let Some(ref path) = cmd_line_args.file_path {
if path.exists() { if path.exists() {
let mut m = persistence::load(path) let mut m = persistence::load(path)
.with_context(|| format!("Failed to load {}", path.display()))?; .with_context(|| format!("Failed to load {}", path.display()))?;
m.normalize_view_state(); m.normalize_view_state();
m m
} else { } else {
let name = path.file_stem() let name = path
.file_stem()
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
.unwrap_or("New Model") .unwrap_or("New Model")
.to_string(); .to_string();
@ -89,24 +113,35 @@ fn main() -> Result<()> {
}; };
// Headless mode // Headless mode
if !headless_cmds.is_empty() || headless_script.is_some() { if !cmd_line_args.headless_cmds.is_empty() || cmd_line_args.headless_script.is_some() {
return run_headless(&mut model, file_path, headless_cmds, headless_script); return run_headless(
&mut model,
cmd_line_args.file_path,
cmd_line_args.headless_cmds,
cmd_line_args.headless_script,
);
} }
// Pre-TUI import: parse JSON and open wizard // Pre-TUI import: parse JSON and open wizard
let import_json = if let Some(ref path) = import_path { let import_json = if let Some(ref path) = cmd_line_args.import_path {
match std::fs::read_to_string(path) { match std::fs::read_to_string(path) {
Err(e) => { eprintln!("Cannot read '{}': {e}", path.display()); return Ok(()); } Err(e) => {
Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) { eprintln!("Cannot read '{}': {e}", path.display());
Err(e) => { eprintln!("JSON parse error: {e}"); return Ok(()); } return Ok(());
Ok(json) => Some(json),
} }
Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
Err(e) => {
eprintln!("JSON parse error: {e}");
return Ok(());
}
Ok(json) => Some(json),
},
} }
} else { } else {
None None
}; };
run_tui(model, file_path, import_json) run_tui(model, cmd_line_args.file_path, import_json)
} }
fn run_headless( fn run_headless(
@ -138,7 +173,9 @@ fn run_headless(
} }
}; };
let result = command::dispatch(model, &parsed); let result = command::dispatch(model, &parsed);
if !result.ok { exit_code = 1; } if !result.ok {
exit_code = 1;
}
println!("{}", serde_json::to_string(&result)?); println!("{}", serde_json::to_string(&result)?);
} }
@ -149,7 +186,11 @@ fn run_headless(
std::process::exit(exit_code); std::process::exit(exit_code);
} }
fn run_tui(model: Model, file_path: Option<PathBuf>, import_json: Option<serde_json::Value>) -> Result<()> { fn run_tui(
model: Model,
file_path: Option<PathBuf>,
import_json: Option<serde_json::Value>,
) -> Result<()> {
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = io::stdout(); let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?; execute!(stdout, EnterAlternateScreen)?;
@ -228,7 +269,9 @@ fn draw(f: &mut Frame, app: &App) {
fn draw_title(f: &mut Frame, area: Rect, app: &App) { fn draw_title(f: &mut Frame, area: Rect, app: &App) {
let dirty = if app.dirty { " [+]" } else { "" }; let dirty = if app.dirty { " [+]" } else { "" };
let file = app.file_path.as_ref() let file = app
.file_path
.as_ref()
.and_then(|p| p.file_name()) .and_then(|p| p.file_name())
.and_then(|n| n.to_str()) .and_then(|n| n.to_str())
.map(|n| format!(" ({n})")) .map(|n| format!(" ({n})"))
@ -238,8 +281,12 @@ fn draw_title(f: &mut Frame, area: Rect, app: &App) {
let pad = " ".repeat((area.width as usize).saturating_sub(title.len() + right.len())); let pad = " ".repeat((area.width as usize).saturating_sub(title.len() + right.len()));
let line = format!("{title}{pad}{right}"); let line = format!("{title}{pad}{right}");
f.render_widget( f.render_widget(
Paragraph::new(line) Paragraph::new(line).style(
.style(Style::default().fg(Color::Black).bg(Color::Blue).add_modifier(Modifier::BOLD)), Style::default()
.fg(Color::Black)
.bg(Color::Blue)
.add_modifier(Modifier::BOLD),
),
area, area,
); );
} }
@ -254,30 +301,51 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) {
.constraints([Constraint::Min(40), Constraint::Length(side_w)]) .constraints([Constraint::Min(40), Constraint::Length(side_w)])
.split(area); .split(area);
f.render_widget(GridWidget::new(&app.model, &app.mode, &app.search_query), chunks[0]); f.render_widget(
GridWidget::new(&app.model, &app.mode, &app.search_query),
chunks[0],
);
let side = chunks[1]; let side = chunks[1];
let panel_count = [app.formula_panel_open, app.category_panel_open, app.view_panel_open] let panel_count = [
.iter().filter(|&&b| b).count() as u16; app.formula_panel_open,
app.category_panel_open,
app.view_panel_open,
]
.iter()
.filter(|&&b| b)
.count() as u16;
let ph = side.height / panel_count.max(1); let ph = side.height / panel_count.max(1);
let mut y = side.y; let mut y = side.y;
if app.formula_panel_open { if app.formula_panel_open {
let a = Rect::new(side.x, y, side.width, ph); let a = Rect::new(side.x, y, side.width, ph);
f.render_widget(FormulaPanel::new(&app.model, &app.mode, app.formula_cursor), a); f.render_widget(
FormulaPanel::new(&app.model, &app.mode, app.formula_cursor),
a,
);
y += ph; y += ph;
} }
if app.category_panel_open { if app.category_panel_open {
let a = Rect::new(side.x, y, side.width, ph); let a = Rect::new(side.x, y, side.width, ph);
f.render_widget(CategoryPanel::new(&app.model, &app.mode, app.cat_panel_cursor), a); f.render_widget(
CategoryPanel::new(&app.model, &app.mode, app.cat_panel_cursor),
a,
);
y += ph; y += ph;
} }
if app.view_panel_open { if app.view_panel_open {
let a = Rect::new(side.x, y, side.width, ph); let a = Rect::new(side.x, y, side.width, ph);
f.render_widget(ViewPanel::new(&app.model, &app.mode, app.view_panel_cursor), a); f.render_widget(
ViewPanel::new(&app.model, &app.mode, app.view_panel_cursor),
a,
);
} }
} else { } else {
f.render_widget(GridWidget::new(&app.model, &app.mode, &app.search_query), area); f.render_widget(
GridWidget::new(&app.model, &app.mode, &app.search_query),
area,
);
} }
} }
@ -320,10 +388,7 @@ fn draw_status(f: &mut Frame, area: Rect, app: &App) {
let left = format!(" {mode_badge}{search_part} {msg}"); let left = format!(" {mode_badge}{search_part} {msg}");
let right = view_badge; let right = view_badge;
let pad = " ".repeat( let pad = " ".repeat((area.width as usize).saturating_sub(left.len() + right.len()));
(area.width as usize)
.saturating_sub(left.len() + right.len()),
);
let line = format!("{left}{pad}{right}"); let line = format!("{left}{pad}{right}");
let badge_style = match &app.mode { let badge_style = match &app.mode {
@ -337,7 +402,11 @@ fn draw_status(f: &mut Frame, area: Rect, app: &App) {
} }
fn draw_command_bar(f: &mut Frame, area: Rect, app: &App) { fn draw_command_bar(f: &mut Frame, area: Rect, app: &App) {
let buf = if let AppMode::CommandMode { buffer } = &app.mode { buffer.as_str() } else { "" }; let buf = if let AppMode::CommandMode { buffer } = &app.mode {
buffer.as_str()
} else {
""
};
let line = format!(":{buf}"); let line = format!(":{buf}");
f.render_widget( f.render_widget(
Paragraph::new(line).style(Style::default().fg(Color::White).bg(Color::Black)), Paragraph::new(line).style(Style::default().fg(Color::White).bg(Color::Black)),
@ -346,7 +415,11 @@ fn draw_command_bar(f: &mut Frame, area: Rect, app: &App) {
} }
fn draw_export_prompt(f: &mut Frame, area: Rect, app: &App) { fn draw_export_prompt(f: &mut Frame, area: Rect, app: &App) {
let buf = if let AppMode::ExportPrompt { buffer } = &app.mode { buffer.as_str() } else { "" }; let buf = if let AppMode::ExportPrompt { buffer } = &app.mode {
buffer.as_str()
} else {
""
};
let popup_w = 64u16.min(area.width); let popup_w = 64u16.min(area.width);
let x = area.x + area.width.saturating_sub(popup_w) / 2; let x = area.x + area.width.saturating_sub(popup_w) / 2;
let y = area.y + area.height / 2; let y = area.y + area.height / 2;
@ -360,8 +433,7 @@ fn draw_export_prompt(f: &mut Frame, area: Rect, app: &App) {
let inner = block.inner(popup_area); let inner = block.inner(popup_area);
f.render_widget(block, popup_area); f.render_widget(block, popup_area);
f.render_widget( f.render_widget(
Paragraph::new(format!("{buf}")) Paragraph::new(format!("{buf}")).style(Style::default().fg(Color::Green)),
.style(Style::default().fg(Color::Green)),
inner, inner,
); );
} }
@ -383,32 +455,82 @@ fn draw_welcome(f: &mut Frame, area: Rect, _app: &App) {
f.render_widget(block, popup); f.render_widget(block, popup);
let lines: &[(&str, Style)] = &[ let lines: &[(&str, Style)] = &[
("Multi-dimensional data modeling — in your terminal.", Style::default().fg(Color::White)), (
"Multi-dimensional data modeling — in your terminal.",
Style::default().fg(Color::White),
),
("", Style::default()), ("", Style::default()),
("Getting started", Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD)), (
"Getting started",
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::BOLD),
),
("", Style::default()), ("", Style::default()),
(":import <file.json> Import a JSON file", Style::default().fg(Color::Cyan)), (
(":add-cat <name> Add a category (dimension)", Style::default().fg(Color::Cyan)), ":import <file.json> Import a JSON file",
(":add-item <cat> <name> Add an item to a category", Style::default().fg(Color::Cyan)), Style::default().fg(Color::Cyan),
(":formula <cat> <expr> Add a formula, e.g.:", Style::default().fg(Color::Cyan)), ),
(" Profit = Revenue - Cost", Style::default().fg(Color::Green)), (
(":w <file.improv> Save your model", Style::default().fg(Color::Cyan)), ":add-cat <name> Add a category (dimension)",
Style::default().fg(Color::Cyan),
),
(
":add-item <cat> <name> Add an item to a category",
Style::default().fg(Color::Cyan),
),
(
":formula <cat> <expr> Add a formula, e.g.:",
Style::default().fg(Color::Cyan),
),
(
" Profit = Revenue - Cost",
Style::default().fg(Color::Green),
),
(
":w <file.improv> Save your model",
Style::default().fg(Color::Cyan),
),
("", Style::default()), ("", Style::default()),
("Navigation", Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD)), (
"Navigation",
Style::default()
.fg(Color::Blue)
.add_modifier(Modifier::BOLD),
),
("", Style::default()), ("", Style::default()),
("F C V Open panels (Formulas/Categories/Views)", Style::default()), (
("T Tile-select: pivot rows ↔ cols ↔ page", Style::default()), "F C V Open panels (Formulas/Categories/Views)",
Style::default(),
),
(
"T Tile-select: pivot rows ↔ cols ↔ page",
Style::default(),
),
("i Enter Edit a cell", Style::default()), ("i Enter Edit a cell", Style::default()),
("[ ] Cycle the page-axis filter", Style::default()), (
("? or :help Full key reference", Style::default()), "[ ] Cycle the page-axis filter",
Style::default(),
),
(
"? or :help Full key reference",
Style::default(),
),
(":q Quit", Style::default()), (":q Quit", Style::default()),
]; ];
for (i, (text, style)) in lines.iter().enumerate() { for (i, (text, style)) in lines.iter().enumerate() {
if i >= inner.height as usize { break; } if i >= inner.height as usize {
break;
}
f.render_widget( f.render_widget(
Paragraph::new(*text).style(*style), Paragraph::new(*text).style(*style),
Rect::new(inner.x + 1, inner.y + i as u16, inner.width.saturating_sub(2), 1), Rect::new(
inner.x + 1,
inner.y + i as u16,
inner.width.saturating_sub(2),
1,
),
); );
} }
} }