refactor(command): remove old Command enum and dispatch system

Remove the old JSON-based command infrastructure:

- Delete Command enum and CommandResult from types.rs
- Remove QuitCmd and InitBuffer command implementations
- Delete entire dispatch.rs file that handled command execution
- Remove Command type exports from mod.rs

The old system used a monolithic Command enum with serde serialization.
The new trait-based system is more flexible and doesn't require JSON
serialization for command execution.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
This commit is contained in:
Edward Langley
2026-04-04 10:56:35 -07:00
parent 1e8bc7a135
commit 909c20bcbd
3 changed files with 0 additions and 419 deletions

View File

@ -238,23 +238,6 @@ impl Cmd for EnterMode {
}
}
#[derive(Debug)]
pub struct QuitCmd;
impl Cmd for QuitCmd {
fn name(&self) -> &str {
"quit"
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
if ctx.dirty {
vec![effect::set_status(
"Unsaved changes! Use :wq to save+quit or :q! to force quit",
)]
} else {
vec![effect::change_mode(AppMode::Quit)]
}
}
}
#[derive(Debug)]
pub struct ForceQuit;
impl Cmd for ForceQuit {
@ -1364,28 +1347,6 @@ impl Cmd for PopChar {
}
}
/// Initialize a named buffer (set it to a value) and change mode.
#[derive(Debug)]
pub struct InitBuffer {
pub buffer: String,
pub value: String,
pub mode: AppMode,
}
impl Cmd for InitBuffer {
fn name(&self) -> &str {
"init-buffer"
}
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
vec![
Box::new(effect::SetBuffer {
name: self.buffer.clone(),
value: self.value.clone(),
}),
effect::change_mode(self.mode.clone()),
]
}
}
// ── Commit commands (mode-specific buffer consumers) ────────────────────────
/// Commit a cell edit: parse buffer, set cell, advance cursor, return to Normal.

View File

@ -1,254 +0,0 @@
use std::path::PathBuf;
use super::types::{CellValueArg, Command, CommandResult};
use crate::formula::parse_formula;
use crate::import::analyzer::{analyze_records, extract_array_at_path, FieldKind};
use crate::model::cell::{CellKey, CellValue};
use crate::model::Model;
use crate::persistence;
/// Execute a command against the model, returning a result.
/// This is the single authoritative mutation path used by both the TUI and headless modes.
pub fn dispatch(model: &mut Model, cmd: &Command) -> CommandResult {
match cmd {
Command::AddCategory { name } => match model.add_category(name) {
Ok(_) => CommandResult::ok_msg(format!("Category '{name}' added")),
Err(e) => CommandResult::err(e.to_string()),
},
Command::AddItem { category, item } => match model.category_mut(category) {
Some(cat) => {
cat.add_item(item);
CommandResult::ok()
}
None => CommandResult::err(format!("Category '{category}' not found")),
},
Command::AddItemInGroup {
category,
item,
group,
} => match model.category_mut(category) {
Some(cat) => {
cat.add_item_in_group(item, group);
CommandResult::ok()
}
None => CommandResult::err(format!("Category '{category}' not found")),
},
Command::SetCell { coords, value } => {
let kv: Vec<(String, String)> = coords
.iter()
.map(|pair| (pair[0].clone(), pair[1].clone()))
.collect();
// Validate all categories exist before mutating anything
for (cat_name, _) in &kv {
if model.category(cat_name).is_none() {
return CommandResult::err(format!("Category '{cat_name}' not found"));
}
}
// Ensure items exist within their categories
for (cat_name, item_name) in &kv {
model.category_mut(cat_name).unwrap().add_item(item_name);
}
let key = CellKey::new(kv);
let cell_value = match value {
CellValueArg::Number { number } => CellValue::Number(*number),
CellValueArg::Text { text } => CellValue::Text(text.clone()),
};
model.set_cell(key, cell_value);
CommandResult::ok()
}
Command::ClearCell { coords } => {
let kv: Vec<(String, String)> = coords
.iter()
.map(|pair| (pair[0].clone(), pair[1].clone()))
.collect();
let key = CellKey::new(kv);
model.clear_cell(&key);
CommandResult::ok()
}
Command::AddFormula {
raw,
target_category,
} => {
match parse_formula(raw, target_category) {
Ok(formula) => {
// Ensure the target item exists in the target category
let target = formula.target.clone();
let cat_name = formula.target_category.clone();
if let Some(cat) = model.category_mut(&cat_name) {
cat.add_item(&target);
}
model.add_formula(formula);
CommandResult::ok_msg(format!("Formula '{raw}' added"))
}
Err(e) => CommandResult::err(format!("Parse error: {e}")),
}
}
Command::RemoveFormula {
target,
target_category,
} => {
model.remove_formula(target, target_category);
CommandResult::ok()
}
Command::CreateView { name } => {
model.create_view(name);
CommandResult::ok()
}
Command::DeleteView { name } => match model.delete_view(name) {
Ok(_) => CommandResult::ok(),
Err(e) => CommandResult::err(e.to_string()),
},
Command::SwitchView { name } => match model.switch_view(name) {
Ok(_) => CommandResult::ok(),
Err(e) => CommandResult::err(e.to_string()),
},
Command::SetAxis { category, axis } => {
model.active_view_mut().set_axis(category, *axis);
CommandResult::ok()
}
Command::SetPageSelection { category, item } => {
model.active_view_mut().set_page_selection(category, item);
CommandResult::ok()
}
Command::ToggleGroup { category, group } => {
model
.active_view_mut()
.toggle_group_collapse(category, group);
CommandResult::ok()
}
Command::HideItem { category, item } => {
model.active_view_mut().hide_item(category, item);
CommandResult::ok()
}
Command::ShowItem { category, item } => {
model.active_view_mut().show_item(category, item);
CommandResult::ok()
}
Command::Save { path } => match persistence::save(model, std::path::Path::new(path)) {
Ok(_) => CommandResult::ok_msg(format!("Saved to {path}")),
Err(e) => CommandResult::err(e.to_string()),
},
Command::Load { path } => match persistence::load(std::path::Path::new(path)) {
Ok(mut loaded) => {
loaded.normalize_view_state();
*model = loaded;
CommandResult::ok_msg(format!("Loaded from {path}"))
}
Err(e) => CommandResult::err(e.to_string()),
},
Command::ExportCsv { path } => {
let view_name = model.active_view.clone();
match persistence::export_csv(model, &view_name, std::path::Path::new(path)) {
Ok(_) => CommandResult::ok_msg(format!("Exported to {path}")),
Err(e) => CommandResult::err(e.to_string()),
}
}
Command::ImportJson {
path,
model_name,
array_path,
} => import_headless(model, path, model_name.as_deref(), array_path.as_deref()),
}
}
fn import_headless(
model: &mut Model,
path: &PathBuf,
model_name: Option<&str>,
array_path: Option<&str>,
) -> CommandResult {
let is_csv = path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("csv"));
let records = if is_csv {
// Parse CSV file
match crate::import::csv_parser::parse_csv(path) {
Ok(recs) => recs,
Err(e) => return CommandResult::err(e.to_string()),
}
} else {
// Parse JSON file
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => return CommandResult::err(format!("Cannot read '{}': {e}", path.display())),
};
let value: serde_json::Value = match serde_json::from_str(&content) {
Ok(v) => v,
Err(e) => return CommandResult::err(format!("JSON parse error: {e}")),
};
if let Some(ap) = array_path.filter(|s| !s.is_empty()) {
match extract_array_at_path(&value, ap) {
Some(arr) => arr.clone(),
None => return CommandResult::err(format!("No array at path '{ap}'")),
}
} else if let Some(arr) = value.as_array() {
arr.clone()
} else {
let paths = crate::import::analyzer::find_array_paths(&value);
if let Some(first) = paths.first() {
match extract_array_at_path(&value, first) {
Some(arr) => arr.clone(),
None => return CommandResult::err("Could not extract records array"),
}
} else {
return CommandResult::err("No array found in JSON");
}
}
};
let proposals = analyze_records(&records);
// Build via ImportPipeline
let raw = if is_csv {
serde_json::Value::Array(records.clone())
} else {
// For JSON, we need the original parsed value
// Re-read and parse to get it (or pass it up from above)
serde_json::from_str(&std::fs::read_to_string(path).unwrap_or_default())
.unwrap_or(serde_json::Value::Array(records.clone()))
};
let pipeline = crate::import::wizard::ImportPipeline {
raw,
array_paths: vec![],
selected_path: array_path.unwrap_or("").to_string(),
records,
proposals: proposals
.into_iter()
.map(|mut p| {
p.accepted = p.kind != FieldKind::Label;
p
})
.collect(),
model_name: model_name.unwrap_or("Imported Model").to_string(),
formulas: vec![],
};
match pipeline.build_model() {
Ok(new_model) => {
*model = new_model;
CommandResult::ok_msg("Imported successfully")
}
Err(e) => CommandResult::err(e.to_string()),
}
}

View File

@ -1,126 +0,0 @@
use std::path::PathBuf;
use crate::view::Axis;
use serde::{Deserialize, Serialize};
/// All commands that can mutate a Model.
///
/// Serialized as `{"op": "<variant>", ...rest}` where `rest` contains
/// the variant's fields flattened into the same JSON object.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "op")]
pub enum Command {
/// Add a category (dimension).
AddCategory { name: String },
/// Add an item to a category.
AddItem { category: String, item: String },
/// Add an item inside a named group.
AddItemInGroup {
category: String,
item: String,
group: String,
},
/// Set a cell value. `coords` is a list of `[category, item]` pairs.
SetCell {
coords: Vec<[String; 2]>,
#[serde(flatten)]
value: CellValueArg,
},
/// Clear a cell.
ClearCell { coords: Vec<[String; 2]> },
/// Add or replace a formula.
/// `raw` is the full formula string, e.g. "Profit = Revenue - Cost".
/// `target_category` names the category that owns the formula target.
AddFormula {
raw: String,
target_category: String,
},
/// Remove a formula by its target name and category.
RemoveFormula {
target: String,
target_category: String,
},
/// Create a new view.
CreateView { name: String },
/// Delete a view.
DeleteView { name: String },
/// Switch the active view.
SwitchView { name: String },
/// Set the axis of a category in the active view.
SetAxis { category: String, axis: Axis },
/// Set the page-axis selection for a category.
SetPageSelection { category: String, item: String },
/// Toggle collapse of a group in the active view.
ToggleGroup { category: String, group: String },
/// Hide an item in the active view.
HideItem { category: String, item: String },
/// Show (un-hide) an item in the active view.
ShowItem { category: String, item: String },
/// Save the model to a file path.
Save { path: String },
/// Load a model from a file path (replaces current model).
Load { path: String },
/// Export the active view to CSV.
ExportCsv { path: String },
/// Import a JSON file via the analyzer (non-interactive, uses auto-detected proposals).
ImportJson {
path: PathBuf,
model_name: Option<String>,
/// Dot-path to the records array (empty = root)
array_path: Option<String>,
},
}
/// Inline value for SetCell
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CellValueArg {
Number { number: f64 },
Text { text: String },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandResult {
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl CommandResult {
pub fn ok() -> Self {
Self {
ok: true,
message: None,
}
}
pub fn ok_msg(msg: impl Into<String>) -> Self {
Self {
ok: true,
message: Some(msg.into()),
}
}
pub fn err(msg: impl Into<String>) -> Self {
Self {
ok: false,
message: Some(msg.into()),
}
}
}