diff --git a/src/command/cmd.rs b/src/command/cmd.rs index 999729a..55c75c5 100644 --- a/src/command/cmd.rs +++ b/src/command/cmd.rs @@ -413,6 +413,30 @@ impl Cmd for ScrollRows { } } +#[derive(Debug)] +pub struct PageScroll { + pub direction: i32, // 1 for down, -1 for up + pub cursor: CursorState, +} +impl Cmd for PageScroll { + fn name(&self) -> &'static str { + "page-scroll" + } + fn execute(&self, _ctx: &CmdContext) -> Vec> { + let delta = (self.cursor.visible_rows as i32 * 3 / 4).max(1) * self.direction; + let row_max = self.cursor.row_count.saturating_sub(1) as i32; + let nr = (self.cursor.row as i32 + delta).clamp(0, row_max) as usize; + viewport_effects( + nr, + self.cursor.col, + self.cursor.row_offset, + self.cursor.col_offset, + self.cursor.visible_rows, + self.cursor.visible_cols, + ) + } +} + // ── Mode change commands ───────────────────────────────────────────────────── #[derive(Debug)] @@ -746,6 +770,26 @@ impl Cmd for AddRecordRow { } } +/// Vim-style 'o': add a new record row below cursor and enter edit mode. +#[derive(Debug)] +pub struct OpenRecordRow; +impl Cmd for OpenRecordRow { + fn name(&self) -> &'static str { + "open-record-row" + } + fn execute(&self, ctx: &CmdContext) -> Vec> { + let is_records = ctx.cell_key.as_ref() + .and_then(|k| crate::view::synthetic_record_info(k)) + .is_some(); + if !is_records { + return vec![effect::set_status("open-record-row only works in records mode")]; + } + let mut effects = AddRecordRow.execute(ctx); + effects.push(Box::new(effect::EnterEditAtCursor)); + effects + } +} + /// Typewriter-style advance: move down, wrap to top of next column at bottom. #[derive(Debug)] pub struct EnterAdvance { @@ -1971,6 +2015,36 @@ impl Cmd for CommitCellEdit { } } +/// Tab in editing: commit cell, move right, re-enter edit mode (Excel-style). +#[derive(Debug)] +pub struct CommitAndAdvanceRight { + pub key: CellKey, + pub value: String, + pub cursor: CursorState, +} +impl Cmd for CommitAndAdvanceRight { + fn name(&self) -> &'static str { + "commit-and-advance-right" + } + fn execute(&self, _ctx: &CmdContext) -> Vec> { + let mut effects: Vec> = Vec::new(); + commit_cell_value(&self.key, &self.value, &mut effects); + // Move right instead of down + let col_max = self.cursor.col_count.saturating_sub(1); + let nc = (self.cursor.col + 1).min(col_max); + effects.extend(viewport_effects( + self.cursor.row, + nc, + self.cursor.row_offset, + self.cursor.col_offset, + self.cursor.visible_rows, + self.cursor.visible_cols, + )); + effects.push(Box::new(effect::EnterEditAtCursor)); + effects + } +} + /// Commit a formula from the formula edit buffer. #[derive(Debug)] pub struct CommitFormula; @@ -2555,6 +2629,25 @@ pub fn default_registry() -> CmdRegistry { })) }, ); + r.register( + &PageScroll { direction: 0, cursor: CursorState::default() }, + |args| { + require_args("page-scroll", args, 1)?; + let dir = args[0].parse::().map_err(|e| e.to_string())?; + Ok(Box::new(PageScroll { + direction: dir, + cursor: CursorState::default(), + })) + }, + |args, ctx| { + require_args("page-scroll", args, 1)?; + let dir = args[0].parse::().map_err(|e| e.to_string())?; + Ok(Box::new(PageScroll { + direction: dir, + cursor: CursorState::from_ctx(ctx), + })) + }, + ); r.register( &EnterAdvance { cursor: CursorState::default() }, |_| { @@ -2817,6 +2910,7 @@ pub fn default_registry() -> CmdRegistry { Box::new(DeleteFormulaAtCursor) }); r.register_nullary(|| Box::new(AddRecordRow)); + r.register_nullary(|| Box::new(OpenRecordRow)); r.register_nullary(|| Box::new(TogglePruneEmpty)); r.register_nullary(|| Box::new(ToggleRecordsMode)); r.register_nullary(|| Box::new(CycleAxisAtCursor)); @@ -2870,35 +2964,36 @@ pub fn default_registry() -> CmdRegistry { &CommitCellEdit { key: CellKey::new(vec![]), value: String::new(), - records_edit: None, }, |args| { - // parse: commit-cell-edit ... if args.len() < 2 { return Err("commit-cell-edit requires a value and coords".into()); } Ok(Box::new(CommitCellEdit { key: parse_cell_key_from_args(&args[1..]), value: args[0].clone(), - records_edit: None, })) }, |_args, ctx| { let value = read_buffer(ctx, "edit"); - // In records mode, stage the edit instead of writing to the model - if let Some(col_name) = &ctx.records_col { - let record_idx = ctx.selected.0; - return Ok(Box::new(CommitCellEdit { - key: CellKey::new(vec![]), // ignored in records mode - value, - records_edit: Some((record_idx, col_name.clone())), - })); - } let key = ctx.cell_key.clone().ok_or("no cell at cursor")?; - Ok(Box::new(CommitCellEdit { + Ok(Box::new(CommitCellEdit { key, value })) + }, + ); + r.register( + &CommitAndAdvanceRight { + key: CellKey::new(vec![]), + value: String::new(), + cursor: CursorState::default(), + }, + |_| Err("commit-and-advance-right requires context".into()), + |_args, ctx| { + let value = read_buffer(ctx, "edit"); + let key = ctx.cell_key.clone().ok_or("no cell at cursor")?; + Ok(Box::new(CommitAndAdvanceRight { key, value, - records_edit: None, + cursor: CursorState::from_ctx(ctx), })) }, ); diff --git a/src/command/keymap.rs b/src/command/keymap.rs index e84357e..d5564c7 100644 --- a/src/command/keymap.rs +++ b/src/command/keymap.rs @@ -266,10 +266,14 @@ impl KeymapSet { normal.bind(KeyCode::Char('G'), none, "jump-last-row"); normal.bind(KeyCode::Char('0'), none, "jump-first-col"); normal.bind(KeyCode::Char('$'), none, "jump-last-col"); + normal.bind(KeyCode::Home, none, "jump-first-col"); + normal.bind(KeyCode::End, none, "jump-last-col"); // Scroll normal.bind_args(KeyCode::Char('d'), ctrl, "scroll-rows", vec!["5".into()]); normal.bind_args(KeyCode::Char('u'), ctrl, "scroll-rows", vec!["-5".into()]); + normal.bind_args(KeyCode::PageDown, none, "page-scroll", vec!["1".into()]); + normal.bind_args(KeyCode::PageUp, none, "page-scroll", vec!["-1".into()]); // Cell operations normal.bind(KeyCode::Char('x'), none, "clear-cell"); @@ -357,7 +361,7 @@ impl KeymapSet { // Drill into aggregated cell / view history / add row normal.bind(KeyCode::Char('>'), none, "drill-into-cell"); normal.bind(KeyCode::Char('<'), none, "view-back"); - normal.bind(KeyCode::Char('o'), none, "add-record-row"); + normal.bind(KeyCode::Char('o'), none, "open-record-row"); // Records mode toggle and prune toggle normal.bind(KeyCode::Char('R'), none, "toggle-records-mode"); @@ -560,6 +564,7 @@ impl KeymapSet { let mut ed = Keymap::new(); ed.bind_args(KeyCode::Esc, none, "enter-mode", vec!["normal".into()]); ed.bind(KeyCode::Enter, none, "commit-cell-edit"); + ed.bind(KeyCode::Tab, none, "commit-and-advance-right"); 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)); diff --git a/src/ui/app.rs b/src/ui/app.rs index d06da3b..41888e9 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -253,7 +253,7 @@ impl App { pub fn hint_text(&self) -> &'static str { match &self.mode { AppMode::Normal => "hjkl:nav i:edit R:records P:prune F/C/V:panels T:tiles [:]:page >:drill ::cmd", - AppMode::Editing { .. } => "Enter:commit Esc:cancel", + AppMode::Editing { .. } => "Enter:commit Tab:commit+right Esc:cancel", AppMode::FormulaPanel => "n:new d:delete jk:nav Esc:back", AppMode::FormulaEdit { .. } => "Enter:save Esc:cancel — type: Name = expression", AppMode::CategoryPanel => "jk:nav Space:cycle-axis n:new-cat a:add-items d:delete Esc:back",