From 3e3ac05b055717edcc061f5c6ae739b8fb906a48 Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Mon, 30 Mar 2026 23:30:57 -0700 Subject: [PATCH] refactor: cleanup entry point --- src/main.rs | 240 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 181 insertions(+), 59 deletions(-) diff --git a/src/main.rs b/src/main.rs index bef3947..4f476e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,10 @@ -mod model; -mod formula; -mod view; -mod ui; -mod import; -mod persistence; mod command; +mod formula; +mod import; +mod model; +mod persistence; +mod ui; +mod view; use std::io; use std::path::PathBuf; @@ -24,18 +24,24 @@ use ratatui::{ Frame, Terminal, }; +use model::Model; 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::view_panel::ViewPanel; +use ui::formula_panel::FormulaPanel; +use ui::grid::GridWidget; use ui::help::HelpWidget; use ui::import_wizard_ui::ImportWizardWidget; -use model::Model; +use ui::tile_bar::TileBar; +use ui::view_panel::ViewPanel; -fn main() -> Result<()> { - let args: Vec = std::env::args().collect(); +struct CmdLineArgs { + file_path: Option, + headless_cmds: Vec, + headless_script: Option, + import_path: Option, +} + +fn parse_args(args: Vec) -> Option { let mut file_path: Option = None; let mut headless_cmds: Vec = Vec::new(); let mut headless_script: Option = None; @@ -60,7 +66,7 @@ fn main() -> Result<()> { } "--help" | "-h" => { print_usage(); - return Ok(()); + return None; } arg if !arg.starts_with('-') => { file_path = Some(PathBuf::from(arg)); @@ -70,15 +76,33 @@ fn main() -> Result<()> { i += 1; } + return Some(CmdLineArgs { + file_path, + headless_cmds, + headless_script, + import_path, + }); +} + +fn main() -> Result<()> { + let args: Vec = 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 - 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() { let mut m = persistence::load(path) .with_context(|| format!("Failed to load {}", path.display()))?; m.normalize_view_state(); m } else { - let name = path.file_stem() + let name = path + .file_stem() .and_then(|s| s.to_str()) .unwrap_or("New Model") .to_string(); @@ -89,24 +113,35 @@ fn main() -> Result<()> { }; // Headless mode - if !headless_cmds.is_empty() || headless_script.is_some() { - return run_headless(&mut model, file_path, headless_cmds, headless_script); + if !cmd_line_args.headless_cmds.is_empty() || cmd_line_args.headless_script.is_some() { + 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 - 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) { - Err(e) => { eprintln!("Cannot read '{}': {e}", path.display()); return Ok(()); } - Ok(content) => match serde_json::from_str::(&content) { - Err(e) => { eprintln!("JSON parse error: {e}"); return Ok(()); } - Ok(json) => Some(json), + Err(e) => { + eprintln!("Cannot read '{}': {e}", path.display()); + return Ok(()); } + Ok(content) => match serde_json::from_str::(&content) { + Err(e) => { + eprintln!("JSON parse error: {e}"); + return Ok(()); + } + Ok(json) => Some(json), + }, } } else { None }; - run_tui(model, file_path, import_json) + run_tui(model, cmd_line_args.file_path, import_json) } fn run_headless( @@ -138,7 +173,9 @@ fn run_headless( } }; let result = command::dispatch(model, &parsed); - if !result.ok { exit_code = 1; } + if !result.ok { + exit_code = 1; + } println!("{}", serde_json::to_string(&result)?); } @@ -149,7 +186,11 @@ fn run_headless( std::process::exit(exit_code); } -fn run_tui(model: Model, file_path: Option, import_json: Option) -> Result<()> { +fn run_tui( + model: Model, + file_path: Option, + import_json: Option, +) -> Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); execute!(stdout, EnterAlternateScreen)?; @@ -228,7 +269,9 @@ fn draw(f: &mut Frame, app: &App) { fn draw_title(f: &mut Frame, area: Rect, app: &App) { 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(|n| n.to_str()) .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 line = format!("{title}{pad}{right}"); f.render_widget( - Paragraph::new(line) - .style(Style::default().fg(Color::Black).bg(Color::Blue).add_modifier(Modifier::BOLD)), + Paragraph::new(line).style( + Style::default() + .fg(Color::Black) + .bg(Color::Blue) + .add_modifier(Modifier::BOLD), + ), area, ); } @@ -254,30 +301,51 @@ fn draw_content(f: &mut Frame, area: Rect, app: &App) { .constraints([Constraint::Min(40), Constraint::Length(side_w)]) .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 panel_count = [app.formula_panel_open, app.category_panel_open, app.view_panel_open] - .iter().filter(|&&b| b).count() as u16; + let panel_count = [ + 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 mut y = side.y; if app.formula_panel_open { 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; } if app.category_panel_open { 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; } if app.view_panel_open { 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 { - 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 right = view_badge; - let pad = " ".repeat( - (area.width as usize) - .saturating_sub(left.len() + right.len()), - ); + let pad = " ".repeat((area.width as usize).saturating_sub(left.len() + right.len())); let line = format!("{left}{pad}{right}"); 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) { - 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}▌"); f.render_widget( 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) { - 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 x = area.x + area.width.saturating_sub(popup_w) / 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); f.render_widget(block, popup_area); f.render_widget( - Paragraph::new(format!("{buf}▌")) - .style(Style::default().fg(Color::Green)), + Paragraph::new(format!("{buf}▌")).style(Style::default().fg(Color::Green)), inner, ); } @@ -383,32 +455,82 @@ fn draw_welcome(f: &mut Frame, area: Rect, _app: &App) { f.render_widget(block, popup); 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()), - ("Getting started", Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD)), + ( + "Getting started", + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD), + ), ("", Style::default()), - (":import Import a JSON file", Style::default().fg(Color::Cyan)), - (":add-cat Add a category (dimension)", Style::default().fg(Color::Cyan)), - (":add-item Add an item to a category", Style::default().fg(Color::Cyan)), - (":formula Add a formula, e.g.:", Style::default().fg(Color::Cyan)), - (" Profit = Revenue - Cost", Style::default().fg(Color::Green)), - (":w Save your model", Style::default().fg(Color::Cyan)), + ( + ":import Import a JSON file", + Style::default().fg(Color::Cyan), + ), + ( + ":add-cat Add a category (dimension)", + Style::default().fg(Color::Cyan), + ), + ( + ":add-item Add an item to a category", + Style::default().fg(Color::Cyan), + ), + ( + ":formula Add a formula, e.g.:", + Style::default().fg(Color::Cyan), + ), + ( + " Profit = Revenue - Cost", + Style::default().fg(Color::Green), + ), + ( + ":w Save your model", + Style::default().fg(Color::Cyan), + ), ("", Style::default()), - ("Navigation", Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD)), + ( + "Navigation", + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::BOLD), + ), ("", 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()), - ("[ ] 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()), ]; 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( 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, + ), ); } }