refactor: cleanup entry point
This commit is contained in:
240
src/main.rs
240
src/main.rs
@ -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,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user