feat(command): add new command structs and registry entries
Add new command structs for quitting and command execution. Introduce Quit and SaveAndQuit commands with dirty checks. Add ExecuteCommand for handling ':' input. Define effect_cmd for SetFormatCmd, ImportCmd, ExportCmd, WriteCmd, and HelpCmd. Register the new commands in the default command registry. Fix a buggy mode reset check that used Debug string matching. Co-Authored-By: fiddlerwoaroof/git-smart-commit (bartowski/nvidia_Nemotron-Cascade-2-30B-A3B-GGUF)
This commit is contained in:
@ -402,6 +402,39 @@ impl Cmd for ForceQuit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Quit with dirty check — refuses if unsaved changes exist.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Quit;
|
||||||
|
impl Cmd for Quit {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"q"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
if ctx.dirty {
|
||||||
|
vec![effect::set_status(
|
||||||
|
"Unsaved changes. Use :q! to force quit or :wq to save+quit.",
|
||||||
|
)]
|
||||||
|
} else {
|
||||||
|
vec![effect::change_mode(AppMode::Quit)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save then quit.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SaveAndQuit;
|
||||||
|
impl Cmd for SaveAndQuit {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"wq"
|
||||||
|
}
|
||||||
|
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
vec![
|
||||||
|
Box::new(effect::Save),
|
||||||
|
effect::change_mode(AppMode::Quit),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Cell operations ──────────────────────────────────────────────────────────
|
// ── Cell operations ──────────────────────────────────────────────────────────
|
||||||
// All cell commands take an explicit CellKey. The interactive spec fills it
|
// All cell commands take an explicit CellKey. The interactive spec fills it
|
||||||
// from ctx.cell_key(); the parser fills it from Cat/Item coordinate args.
|
// from ctx.cell_key(); the parser fills it from Cat/Item coordinate args.
|
||||||
@ -1576,6 +1609,9 @@ impl Cmd for HandleWizardKey {
|
|||||||
|
|
||||||
/// Execute the command in the "command" buffer (the `:` command line).
|
/// Execute the command in the "command" buffer (the `:` command line).
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
/// Execute the `:` command buffer by delegating to the command registry.
|
||||||
|
/// The `:` prompt is just another frontend to the scripting language —
|
||||||
|
/// same parser as `improvise script`.
|
||||||
pub struct ExecuteCommand;
|
pub struct ExecuteCommand;
|
||||||
impl Cmd for ExecuteCommand {
|
impl Cmd for ExecuteCommand {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
@ -1583,182 +1619,31 @@ impl Cmd for ExecuteCommand {
|
|||||||
}
|
}
|
||||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
let raw = ctx.buffers.get("command").cloned().unwrap_or_default();
|
let raw = ctx.buffers.get("command").cloned().unwrap_or_default();
|
||||||
let raw = raw.trim();
|
let raw = raw.trim().to_string();
|
||||||
let (cmd_name, rest) = raw
|
if raw.is_empty() {
|
||||||
.split_once(char::is_whitespace)
|
return vec![effect::change_mode(AppMode::Normal)];
|
||||||
.map(|(c, r)| (c, r.trim()))
|
}
|
||||||
.unwrap_or((raw, ""));
|
|
||||||
|
|
||||||
// Default: return to Normal
|
match crate::command::parse::parse_line_with(ctx.registry, &raw) {
|
||||||
|
Ok(cmds) => {
|
||||||
let mut effects: Vec<Box<dyn Effect>> = Vec::new();
|
let mut effects: Vec<Box<dyn Effect>> = Vec::new();
|
||||||
|
for cmd in cmds {
|
||||||
match cmd_name {
|
effects.extend(cmd.execute(ctx));
|
||||||
"q" | "quit" => {
|
|
||||||
if ctx.dirty {
|
|
||||||
effects.push(effect::set_status(
|
|
||||||
"Unsaved changes. Use :q! to force quit or :wq to save+quit.",
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
effects.push(effect::change_mode(AppMode::Quit));
|
|
||||||
}
|
}
|
||||||
}
|
// Return to Normal unless a command already changed mode
|
||||||
"q!" => {
|
if !effects.iter().any(|e| format!("{e:?}").contains("ChangeMode")) {
|
||||||
effects.push(effect::change_mode(AppMode::Quit));
|
|
||||||
}
|
|
||||||
"w" | "write" => {
|
|
||||||
if rest.is_empty() {
|
|
||||||
effects.push(Box::new(effect::Save));
|
|
||||||
} else {
|
|
||||||
effects.push(Box::new(effect::SaveAs(std::path::PathBuf::from(rest))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"wq" | "x" => {
|
|
||||||
effects.push(Box::new(effect::Save));
|
|
||||||
effects.push(effect::change_mode(AppMode::Quit));
|
|
||||||
}
|
|
||||||
"import" => {
|
|
||||||
if rest.is_empty() {
|
|
||||||
effects.push(effect::set_status("Usage: :import <path.json>"));
|
|
||||||
} else {
|
|
||||||
effects.push(Box::new(effect::StartImportWizard(rest.to_string())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"export" => {
|
|
||||||
let path = if rest.is_empty() { "export.csv" } else { rest };
|
|
||||||
effects.push(Box::new(effect::ExportCsv(std::path::PathBuf::from(path))));
|
|
||||||
}
|
|
||||||
"add-cat" | "add-category" | "cat" => {
|
|
||||||
if rest.is_empty() {
|
|
||||||
effects.push(effect::set_status("Usage: :add-cat <name>"));
|
|
||||||
} else {
|
|
||||||
effects.push(Box::new(effect::AddCategory(rest.to_string())));
|
|
||||||
effects.push(effect::mark_dirty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"add-item" | "item" => {
|
|
||||||
let mut parts = rest.splitn(2, char::is_whitespace);
|
|
||||||
let cat = parts.next().unwrap_or("").trim();
|
|
||||||
let item = parts.next().unwrap_or("").trim();
|
|
||||||
if cat.is_empty() || item.is_empty() {
|
|
||||||
effects.push(effect::set_status("Usage: :add-item <category> <item>"));
|
|
||||||
} else {
|
|
||||||
effects.push(Box::new(effect::AddItem {
|
|
||||||
category: cat.to_string(),
|
|
||||||
item: item.to_string(),
|
|
||||||
}));
|
|
||||||
effects.push(effect::mark_dirty());
|
|
||||||
effects.push(effect::set_status("Item added"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"add-items" | "items" => {
|
|
||||||
let mut parts = rest.splitn(2, char::is_whitespace);
|
|
||||||
let cat = parts.next().unwrap_or("").trim().to_string();
|
|
||||||
let items_str = parts.next().unwrap_or("").trim().to_string();
|
|
||||||
if cat.is_empty() || items_str.is_empty() {
|
|
||||||
effects.push(effect::set_status(
|
|
||||||
"Usage: :add-items <category> item1 item2 ...",
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
let items: Vec<&str> = items_str.split_whitespace().collect();
|
|
||||||
let count = items.len();
|
|
||||||
for item in &items {
|
|
||||||
effects.push(Box::new(effect::AddItem {
|
|
||||||
category: cat.clone(),
|
|
||||||
item: item.to_string(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
effects.push(effect::mark_dirty());
|
|
||||||
effects.push(effect::set_status(format!(
|
|
||||||
"Added {count} items to \"{cat}\".",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"formula" | "add-formula" => {
|
|
||||||
if rest.is_empty() {
|
|
||||||
effects.push(Box::new(effect::SetPanelOpen {
|
|
||||||
panel: Panel::Formula,
|
|
||||||
open: true,
|
|
||||||
}));
|
|
||||||
effects.push(effect::change_mode(AppMode::FormulaPanel));
|
|
||||||
return effects; // Don't set mode to Normal
|
|
||||||
} else {
|
|
||||||
let mut parts = rest.splitn(2, char::is_whitespace);
|
|
||||||
let cat = parts.next().unwrap_or("").trim();
|
|
||||||
let formula = parts.next().unwrap_or("").trim();
|
|
||||||
if cat.is_empty() || formula.is_empty() {
|
|
||||||
effects.push(effect::set_status(
|
|
||||||
"Usage: :formula <category> <Name = expr>",
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
effects.push(Box::new(effect::AddFormula {
|
|
||||||
raw: formula.to_string(),
|
|
||||||
target_category: cat.to_string(),
|
|
||||||
}));
|
|
||||||
effects.push(effect::mark_dirty());
|
|
||||||
effects.push(effect::set_status("Formula added"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"add-view" | "view" => {
|
|
||||||
let name = if rest.is_empty() {
|
|
||||||
format!("View {}", ctx.model.views.len() + 1)
|
|
||||||
} else {
|
|
||||||
rest.to_string()
|
|
||||||
};
|
|
||||||
effects.push(Box::new(effect::CreateView(name.clone())));
|
|
||||||
effects.push(Box::new(effect::SwitchView(name)));
|
|
||||||
effects.push(effect::mark_dirty());
|
|
||||||
}
|
|
||||||
"set-format" | "fmt" => {
|
|
||||||
if rest.is_empty() {
|
|
||||||
effects.push(effect::set_status(
|
|
||||||
"Usage: :set-format <fmt> e.g. ,.0 ,.2 .4",
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
effects.push(Box::new(effect::SetNumberFormat(rest.to_string())));
|
|
||||||
effects.push(effect::mark_dirty());
|
|
||||||
effects.push(effect::set_status(format!("Number format set to '{rest}'")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"show-item" | "show" => {
|
|
||||||
let mut parts = rest.splitn(2, char::is_whitespace);
|
|
||||||
let cat = parts.next().unwrap_or("").trim();
|
|
||||||
let item = parts.next().unwrap_or("").trim();
|
|
||||||
if cat.is_empty() || item.is_empty() {
|
|
||||||
effects.push(effect::set_status("Usage: :show-item <category> <item>"));
|
|
||||||
} else {
|
|
||||||
effects.push(Box::new(effect::ShowItem {
|
|
||||||
category: cat.to_string(),
|
|
||||||
item: item.to_string(),
|
|
||||||
}));
|
|
||||||
effects.push(effect::mark_dirty());
|
|
||||||
effects.push(effect::set_status(format!(
|
|
||||||
"Showed \"{item}\" in \"{cat}\""
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"help" | "h" => {
|
|
||||||
effects.push(effect::change_mode(AppMode::Help));
|
|
||||||
return effects; // Don't also set Normal
|
|
||||||
}
|
|
||||||
"" => {} // just pressed Enter with empty buffer
|
|
||||||
other => {
|
|
||||||
effects.push(effect::set_status(format!(
|
|
||||||
"Unknown command: :{other} (try :help)"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default: return to Normal (unless a command already set a different mode)
|
|
||||||
if !effects
|
|
||||||
.iter()
|
|
||||||
.any(|e| format!("{e:?}").contains("ChangeMode"))
|
|
||||||
{
|
|
||||||
effects.push(effect::change_mode(AppMode::Normal));
|
effects.push(effect::change_mode(AppMode::Normal));
|
||||||
}
|
}
|
||||||
|
|
||||||
effects
|
effects
|
||||||
}
|
}
|
||||||
|
Err(msg) => {
|
||||||
|
vec![
|
||||||
|
effect::set_status(format!(":{raw} — {msg}")),
|
||||||
|
effect::change_mode(AppMode::Normal),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Text buffer commands ─────────────────────────────────────────────────────
|
// ── Text buffer commands ─────────────────────────────────────────────────────
|
||||||
@ -2272,6 +2157,59 @@ effect_cmd!(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
effect_cmd!(
|
||||||
|
SetFormatCmd,
|
||||||
|
"set-format",
|
||||||
|
|args: &[String]| require_args("set-format", args, 1),
|
||||||
|
|args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
|
||||||
|
vec![
|
||||||
|
Box::new(effect::SetNumberFormat(args.join(" "))),
|
||||||
|
effect::mark_dirty(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
effect_cmd!(
|
||||||
|
ImportCmd,
|
||||||
|
"import",
|
||||||
|
|args: &[String]| require_args("import", args, 1),
|
||||||
|
|args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
|
||||||
|
vec![Box::new(effect::StartImportWizard(args[0].clone()))]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
effect_cmd!(
|
||||||
|
ExportCmd,
|
||||||
|
"export",
|
||||||
|
|_args: &[String]| -> Result<(), String> { Ok(()) },
|
||||||
|
|args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
|
||||||
|
let path = args.first().map(|s| s.as_str()).unwrap_or("export.csv");
|
||||||
|
vec![Box::new(effect::ExportCsv(std::path::PathBuf::from(path)))]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
effect_cmd!(
|
||||||
|
WriteCmd,
|
||||||
|
"w",
|
||||||
|
|_args: &[String]| -> Result<(), String> { Ok(()) },
|
||||||
|
|args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
|
||||||
|
if args.is_empty() {
|
||||||
|
vec![Box::new(effect::Save)]
|
||||||
|
} else {
|
||||||
|
vec![Box::new(effect::SaveAs(std::path::PathBuf::from(&args[0])))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
effect_cmd!(
|
||||||
|
HelpCmd,
|
||||||
|
"help",
|
||||||
|
|_args: &[String]| -> Result<(), String> { Ok(()) },
|
||||||
|
|_args: &Vec<String>, _ctx: &CmdContext| -> Vec<Box<dyn Effect>> {
|
||||||
|
vec![effect::change_mode(AppMode::Help)]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
effect_cmd!(
|
effect_cmd!(
|
||||||
LoadModelCmd,
|
LoadModelCmd,
|
||||||
"load",
|
"load",
|
||||||
@ -2354,6 +2292,11 @@ pub fn default_registry() -> CmdRegistry {
|
|||||||
r.register_pure(&LoadModelCmd(vec![]), LoadModelCmd::parse);
|
r.register_pure(&LoadModelCmd(vec![]), LoadModelCmd::parse);
|
||||||
r.register_pure(&ExportCsvCmd(vec![]), ExportCsvCmd::parse);
|
r.register_pure(&ExportCsvCmd(vec![]), ExportCsvCmd::parse);
|
||||||
r.register_pure(&ImportJsonCmd(vec![]), ImportJsonCmd::parse);
|
r.register_pure(&ImportJsonCmd(vec![]), ImportJsonCmd::parse);
|
||||||
|
r.register_pure(&SetFormatCmd(vec![]), SetFormatCmd::parse);
|
||||||
|
r.register_pure(&ImportCmd(vec![]), ImportCmd::parse);
|
||||||
|
r.register_pure(&ExportCmd(vec![]), ExportCmd::parse);
|
||||||
|
r.register_pure(&WriteCmd(vec![]), WriteCmd::parse);
|
||||||
|
r.register_pure(&HelpCmd(vec![]), HelpCmd::parse);
|
||||||
|
|
||||||
// ── Navigation (unified Move) ──────────────────────────────────────
|
// ── Navigation (unified Move) ──────────────────────────────────────
|
||||||
r.register(
|
r.register(
|
||||||
@ -2544,6 +2487,8 @@ pub fn default_registry() -> CmdRegistry {
|
|||||||
|
|
||||||
// ── Mode changes ─────────────────────────────────────────────────────
|
// ── Mode changes ─────────────────────────────────────────────────────
|
||||||
r.register_nullary(|| Box::new(ForceQuit));
|
r.register_nullary(|| Box::new(ForceQuit));
|
||||||
|
r.register_nullary(|| Box::new(Quit));
|
||||||
|
r.register_nullary(|| Box::new(SaveAndQuit));
|
||||||
r.register_nullary(|| Box::new(SaveCmd));
|
r.register_nullary(|| Box::new(SaveCmd));
|
||||||
r.register_nullary(|| Box::new(EnterSearchMode));
|
r.register_nullary(|| Box::new(EnterSearchMode));
|
||||||
r.register(
|
r.register(
|
||||||
|
|||||||
Reference in New Issue
Block a user