feat(records): implement records mode for data entry

Implement a new "Records" mode for data entry.
- Add `RecordsNormal` and `RecordsEditing` to `AppMode` and `ModeKey` .
- `DataStore` now uses `IndexMap` and supports `sort_by_key()` to ensure
  deterministic row order.
- `ToggleRecordsMode` command now sorts data and switches to
  `RecordsNormal` .
- `EnterEditMode` command now respects records editing variants.
- `RecordsNormal` mode includes a new `o` keybinding to add a record row.
- `RecordsEditing` mode inherits from `Editing` and adds an `Esc` binding
  to return to `RecordsNormal` .
- Added `SortData` effect to trigger data sorting.
- Updated UI to display "RECORDS" and "RECORDS INSERT" mode names and
  styles.
- Updated keymaps, command registry, and view navigation to support these
  new modes.
- Added comprehensive tests for records mode behavior, including sorting
  and boundary conditions for Tab/Enter.

Co-Authored-By: fiddlerwoaroof/git-smart-commit (gemma-4-26B-A4B-it-UD-Q5_K_XL.gguf)
This commit is contained in:
Edward Langley
2026-04-15 21:32:35 -07:00
parent ded35f705c
commit 030865a0ff
10 changed files with 350 additions and 32 deletions

View File

@ -61,6 +61,8 @@ pub enum ModeKey {
CommandMode,
SearchMode,
ImportWizard,
RecordsNormal,
RecordsEditing,
}
impl ModeKey {
@ -80,6 +82,8 @@ impl ModeKey {
AppMode::ExportPrompt { .. } => Some(ModeKey::ExportPrompt),
AppMode::CommandMode { .. } => Some(ModeKey::CommandMode),
AppMode::ImportWizard => Some(ModeKey::ImportWizard),
AppMode::RecordsNormal => Some(ModeKey::RecordsNormal),
AppMode::RecordsEditing { .. } => Some(ModeKey::RecordsEditing),
_ => None,
}
}
@ -450,14 +454,9 @@ impl KeymapSet {
normal.bind(KeyCode::Char('z'), none, "toggle-group-under-cursor");
normal.bind(KeyCode::Char('H'), none, "hide-selected-row-item");
// Drill into aggregated cell / view history / add row
// Drill into aggregated cell / view history
normal.bind(KeyCode::Char('>'), none, "drill-into-cell");
normal.bind(KeyCode::Char('<'), none, "view-back");
normal.bind_seq(
KeyCode::Char('o'),
none,
vec![("add-record-row", vec![]), ("enter-edit-at-cursor", vec![])],
);
// Records mode toggle and prune toggle
normal.bind(KeyCode::Char('R'), none, "toggle-records-mode");
@ -484,7 +483,17 @@ impl KeymapSet {
z_map.bind(KeyCode::Char('Z'), none, "wq");
normal.bind_prefix(KeyCode::Char('Z'), none, Arc::new(z_map));
set.insert(ModeKey::Normal, Arc::new(normal));
let normal = Arc::new(normal);
set.insert(ModeKey::Normal, normal.clone());
// ── Records normal mode (inherits from normal) ────────────────────
let mut rn = Keymap::with_parent(normal);
rn.bind_seq(
KeyCode::Char('o'),
none,
vec![("add-record-row", vec![]), ("enter-edit-at-cursor", vec![])],
);
set.insert(ModeKey::RecordsNormal, Arc::new(rn));
// ── Help mode ────────────────────────────────────────────────────
let mut help = Keymap::new();
@ -754,7 +763,20 @@ impl KeymapSet {
);
ed.bind_args(KeyCode::Backspace, none, "pop-char", vec!["edit".into()]);
ed.bind_any_char("append-char", vec!["edit".into()]);
set.insert(ModeKey::Editing, Arc::new(ed));
let ed = Arc::new(ed);
set.insert(ModeKey::Editing, ed.clone());
// ── Records editing mode (inherits from editing) ──────────────────
let mut re = Keymap::with_parent(ed);
re.bind_seq(
KeyCode::Esc,
none,
vec![
("clear-buffer", vec!["edit".into()]),
("enter-mode", vec!["records-normal".into()]),
],
);
set.insert(ModeKey::RecordsEditing, Arc::new(re));
// ── Formula edit ─────────────────────────────────────────────────
let mut fe = Keymap::new();
@ -1106,6 +1128,8 @@ mod tests {
ModeKey::CommandMode,
ModeKey::SearchMode,
ModeKey::ImportWizard,
ModeKey::RecordsNormal,
ModeKey::RecordsEditing,
];
for mode in &expected_modes {
assert!(