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

300
src/command/cmd/grid.rs Normal file
View File

@ -0,0 +1,300 @@
use crate::model::cell::CellValue;
use crate::ui::effect::{self, Effect};
use crate::view::AxisEntry;
use super::core::{Cmd, CmdContext};
// ── Grid operations ─────────────────────────────────────────────────────
/// Toggle the row or column group collapse under the cursor.
#[derive(Debug)]
pub struct ToggleGroupAtCursor {
pub is_row: bool,
}
impl Cmd for ToggleGroupAtCursor {
fn name(&self) -> &'static str {
if self.is_row {
"toggle-group-under-cursor"
} else {
"toggle-col-group-under-cursor"
}
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let lookup = if self.is_row {
ctx.layout.row_group_for(ctx.selected.0)
} else {
ctx.layout.col_group_for(ctx.selected.1)
};
let Some((cat, group)) = lookup else {
return vec![];
};
vec![
Box::new(effect::ToggleGroup {
category: cat,
group,
}),
effect::mark_dirty(),
]
}
}
/// Hide the row item at the cursor.
#[derive(Debug)]
pub struct HideSelectedRowItem;
impl Cmd for HideSelectedRowItem {
fn name(&self) -> &'static str {
"hide-selected-row-item"
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let Some(cat_name) = ctx.layout.row_cats.first().cloned() else {
return vec![];
};
let sel_row = ctx.selected.0;
let Some(items) = ctx
.layout
.row_items
.iter()
.filter_map(|e| {
if let AxisEntry::DataItem(v) = e {
Some(v)
} else {
None
}
})
.nth(sel_row)
else {
return vec![];
};
let item_name = items[0].clone();
vec![
Box::new(effect::HideItem {
category: cat_name,
item: item_name,
}),
effect::mark_dirty(),
]
}
}
/// Navigate back or forward in view history.
#[derive(Debug)]
pub struct ViewNavigate {
pub forward: bool,
}
impl Cmd for ViewNavigate {
fn name(&self) -> &'static str {
if self.forward {
"view-forward"
} else {
"view-back"
}
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
if self.forward {
if ctx.view_forward_stack.is_empty() {
vec![effect::set_status("No forward view")]
} else {
vec![Box::new(effect::ViewForward)]
}
} else {
if ctx.view_back_stack.is_empty() {
vec![effect::set_status("No previous view")]
} else {
vec![
Box::new(effect::ApplyAndClearDrill),
Box::new(effect::ViewBack),
]
}
}
}
}
/// Drill down into an aggregated cell: create a _Drill view with _Index on
/// Row and _Dim on Column (records/long-format view). Fixed coordinates
/// from the drilled cell become page filters.
#[derive(Debug)]
pub struct DrillIntoCell {
pub key: crate::model::cell::CellKey,
}
impl Cmd for DrillIntoCell {
fn name(&self) -> &'static str {
"drill-into-cell"
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let drill_name = "_Drill".to_string();
let mut effects: Vec<Box<dyn Effect>> = Vec::new();
// Capture the records snapshot NOW (before we switch views).
let records: Vec<(crate::model::cell::CellKey, crate::model::cell::CellValue)> =
if self.key.0.is_empty() {
ctx.model
.data
.iter_cells()
.map(|(k, v)| (k, v.clone()))
.collect()
} else {
ctx.model
.data
.matching_cells(&self.key.0)
.into_iter()
.map(|(k, v)| (k, v.clone()))
.collect()
};
let n = records.len();
// Freeze the snapshot in the drill state
effects.push(Box::new(effect::StartDrill(records)));
// Create (or replace) the drill view
effects.push(Box::new(effect::CreateView(drill_name.clone())));
effects.push(Box::new(effect::SwitchView(drill_name)));
// Records mode: _Index on Row, _Dim on Column
effects.push(Box::new(effect::SetAxis {
category: "_Index".to_string(),
axis: crate::view::Axis::Row,
}));
effects.push(Box::new(effect::SetAxis {
category: "_Dim".to_string(),
axis: crate::view::Axis::Column,
}));
// Fixed coords (from drilled cell) -> Page with that value as filter
let fixed_cats: std::collections::HashSet<String> =
self.key.0.iter().map(|(c, _)| c.clone()).collect();
for (cat, item) in &self.key.0 {
effects.push(Box::new(effect::SetAxis {
category: cat.clone(),
axis: crate::view::Axis::Page,
}));
effects.push(Box::new(effect::SetPageSelection {
category: cat.clone(),
item: item.clone(),
}));
}
// Previously-aggregated categories (none_cats) stay on Axis::None so
// they don't filter records; they'll appear as columns in records mode.
// Skip virtual categories — we already set _Index/_Dim above.
for cat in ctx.none_cats() {
if fixed_cats.contains(cat) || cat.starts_with('_') {
continue;
}
effects.push(Box::new(effect::SetAxis {
category: cat.clone(),
axis: crate::view::Axis::None,
}));
}
effects.push(effect::set_status(format!("Drilled into cell: {n} rows")));
effects
}
}
/// Toggle pruning of empty rows/columns in the current view.
#[derive(Debug)]
pub struct TogglePruneEmpty;
impl Cmd for TogglePruneEmpty {
fn name(&self) -> &'static str {
"toggle-prune-empty"
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let currently_on = ctx.model.active_view().prune_empty;
vec![
Box::new(effect::TogglePruneEmpty),
effect::set_status(if currently_on {
"Showing all rows/columns"
} else {
"Hiding empty rows/columns"
}),
]
}
}
/// Toggle between records mode and pivot mode using the view stack.
/// Entering records mode creates a `_Records` view and switches to it.
/// Leaving records mode navigates back to the previous view.
#[derive(Debug)]
pub struct ToggleRecordsMode;
impl Cmd for ToggleRecordsMode {
fn name(&self) -> &'static str {
"toggle-records-mode"
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let is_records = ctx.layout.is_records_mode();
if is_records {
// Navigate back to the previous view (restores original axes)
return vec![Box::new(effect::ViewBack), effect::set_status("Pivot mode")];
}
let mut effects: Vec<Box<dyn Effect>> = Vec::new();
let records_name = "_Records".to_string();
// Create (or replace) a _Records view and switch to it
effects.push(Box::new(effect::CreateView(records_name.clone())));
effects.push(Box::new(effect::SwitchView(records_name)));
// _Index on Row, _Dim on Column, everything else -> None
effects.push(Box::new(effect::SetAxis {
category: "_Index".to_string(),
axis: crate::view::Axis::Row,
}));
effects.push(Box::new(effect::SetAxis {
category: "_Dim".to_string(),
axis: crate::view::Axis::Column,
}));
for name in ctx.model.categories.keys() {
if name != "_Index" && name != "_Dim" {
effects.push(Box::new(effect::SetAxis {
category: name.clone(),
axis: crate::view::Axis::None,
}));
}
}
effects.push(effect::set_status("Records mode"));
effects
}
}
/// In records mode, add a new row with an empty value. The new cell gets
/// coords from the current page filters. In pivot mode, this is a no-op.
#[derive(Debug)]
pub struct AddRecordRow;
impl Cmd for AddRecordRow {
fn name(&self) -> &'static str {
"add-record-row"
}
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
let is_records = ctx
.cell_key()
.as_ref()
.and_then(crate::view::synthetic_record_info)
.is_some();
if !is_records {
return vec![effect::set_status(
"add-record-row only works in records mode",
)];
}
// Build a CellKey from the current page filters
let view = ctx.model.active_view();
let page_cats: Vec<String> = view
.categories_on(crate::view::Axis::Page)
.into_iter()
.map(String::from)
.collect();
let coords: Vec<(String, String)> = page_cats
.iter()
.map(|cat| {
let sel = view.page_selection(cat).unwrap_or("").to_string();
(cat.clone(), sel)
})
.filter(|(_, v)| !v.is_empty())
.collect();
let key = crate::model::cell::CellKey::new(coords);
vec![
Box::new(effect::SetCell(key, CellValue::Number(0.0))),
effect::mark_dirty(),
effect::set_status("Added new record row"),
]
}
}