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:
@ -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)]
|
#[derive(Debug)]
|
||||||
pub struct ForceQuit;
|
pub struct ForceQuit;
|
||||||
impl Cmd for 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 commands (mode-specific buffer consumers) ────────────────────────
|
||||||
|
|
||||||
/// Commit a cell edit: parse buffer, set cell, advance cursor, return to Normal.
|
/// Commit a cell edit: parse buffer, set cell, advance cursor, return to Normal.
|
||||||
|
|||||||
@ -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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user