refactor: split cmd.rs

This commit is contained in:
Edward Langley
2026-04-09 02:25:23 -07:00
parent 767d524d4b
commit fd69126cdc
15 changed files with 4391 additions and 4300 deletions

189
src/command/cmd/mode.rs Normal file
View File

@ -0,0 +1,189 @@
use crate::ui::app::AppMode;
use crate::ui::effect::{self, Effect};
use super::core::{Cmd, CmdContext};
use super::grid::DrillIntoCell;
// ── Mode change commands ─────────────────────────────────────────────────────
#[derive(Debug)]
pub struct EnterMode(pub AppMode);
impl Cmd for EnterMode {
fn name(&self) -> &'static str {
"enter-mode"
}
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let mut effects: Vec<Box<dyn Effect>> = Vec::new();
// Clear the corresponding buffer when entering a text-entry mode
if let Some(mb) = self.0.minibuffer() {
effects.push(Box::new(effect::SetBuffer {
name: mb.buffer_key.to_string(),
value: String::new(),
}));
}
effects.push(effect::change_mode(self.0.clone()));
effects
}
}
#[derive(Debug)]
pub struct ForceQuit;
impl Cmd for ForceQuit {
fn name(&self) -> &'static str {
"force-quit"
}
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
vec![effect::change_mode(AppMode::Quit)]
}
}
/// 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)]
}
}
// ── Editing entry ───────────────────────────────────────────────────────
/// Enter editing mode with an initial buffer value.
#[derive(Debug)]
pub struct EnterEditMode {
pub initial_value: String,
}
impl Cmd for EnterEditMode {
fn name(&self) -> &'static str {
"enter-edit-mode"
}
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
vec![
Box::new(effect::SetBuffer {
name: "edit".to_string(),
value: self.initial_value.clone(),
}),
effect::change_mode(AppMode::editing()),
]
}
}
/// Smart dispatch for i/a: if the cursor is on an aggregated pivot cell
/// (categories on `Axis::None`, no records mode), drill into it instead of
/// editing. Otherwise enter edit mode with the current displayed value.
#[derive(Debug)]
pub struct EditOrDrill;
impl Cmd for EditOrDrill {
fn name(&self) -> &'static str {
"edit-or-drill"
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
// Only consider regular (non-virtual, non-label) categories on None
// as true aggregation. Virtuals like _Index/_Dim are always None in
// pivot mode and don't imply aggregation.
let regular_none = ctx.none_cats().iter().any(|c| {
ctx.model
.category(c)
.map(|cat| cat.kind.is_regular())
.unwrap_or(false)
});
// In records mode (synthetic key), always edit directly — no drilling.
let is_synthetic = ctx
.cell_key()
.as_ref()
.and_then(crate::view::synthetic_record_info)
.is_some();
let is_aggregated = !is_synthetic && regular_none;
if is_aggregated {
let Some(key) = ctx.cell_key().clone() else {
return vec![effect::set_status("cannot drill — no cell at cursor")];
};
return DrillIntoCell { key }.execute(ctx);
}
EnterEditMode {
initial_value: ctx.display_value.clone(),
}
.execute(ctx)
}
}
/// Thin command wrapper around the `EnterEditAtCursor` effect so it can
/// participate in `Binding::Sequence`.
#[derive(Debug)]
pub struct EnterEditAtCursorCmd;
impl Cmd for EnterEditAtCursorCmd {
fn name(&self) -> &'static str {
"enter-edit-at-cursor"
}
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
vec![Box::new(effect::EnterEditAtCursor)]
}
}
/// Enter export prompt mode.
#[derive(Debug)]
pub struct EnterExportPrompt;
impl Cmd for EnterExportPrompt {
fn name(&self) -> &'static str {
"enter-export-prompt"
}
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
vec![effect::change_mode(AppMode::export_prompt())]
}
}
/// Enter search mode.
#[derive(Debug)]
pub struct EnterSearchMode;
impl Cmd for EnterSearchMode {
fn name(&self) -> &'static str {
"search"
}
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
vec![
Box::new(effect::SetSearchMode(true)),
Box::new(effect::SetSearchQuery(String::new())),
]
}
}
/// Enter tile select mode.
#[derive(Debug)]
pub struct EnterTileSelect;
impl Cmd for EnterTileSelect {
fn name(&self) -> &'static str {
"enter-tile-select"
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let count = ctx.model.category_names().len();
if count > 0 {
vec![
Box::new(effect::SetTileCatIdx(0)),
effect::change_mode(AppMode::TileSelect),
]
} else {
vec![]
}
}
}