feat(core): Add input modes and half-page cursor movement.
Some checks failed
CI / Check (macos-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Clippy (strict) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled

This commit is contained in:
2026-03-13 22:56:30 -04:00
parent 6a4cc85285
commit d9ed49e7d9
8 changed files with 795 additions and 54 deletions

View File

@@ -8,7 +8,7 @@ use std::sync::Arc;
use tokio::sync::{broadcast, mpsc};
use crate::error::PiklError;
use crate::event::{Action, MenuEvent, MenuResult, ViewState, VisibleItem};
use crate::event::{Action, MenuEvent, MenuResult, Mode, ViewState, VisibleItem};
use crate::model::traits::Menu;
use crate::navigation::Viewport;
use serde_json::Value;
@@ -35,6 +35,7 @@ pub struct MenuRunner<M: Menu> {
menu: M,
viewport: Viewport,
filter_text: Arc<str>,
mode: Mode,
action_rx: mpsc::Receiver<Action>,
event_tx: broadcast::Sender<MenuEvent>,
}
@@ -55,6 +56,7 @@ impl<M: Menu> MenuRunner<M> {
menu,
viewport: Viewport::new(),
filter_text: Arc::from(""),
mode: Mode::default(),
action_rx,
event_tx,
};
@@ -100,6 +102,7 @@ impl<M: Menu> MenuRunner<M> {
filter_text: Arc::clone(&self.filter_text),
total_items: self.menu.total(),
total_filtered: self.menu.filtered_count(),
mode: self.mode,
}
}
@@ -158,6 +161,18 @@ impl<M: Menu> MenuRunner<M> {
self.viewport.set_height(height as usize);
ActionOutcome::Broadcast
}
Action::HalfPageUp(n) => {
self.viewport.half_page_up(n);
ActionOutcome::Broadcast
}
Action::HalfPageDown(n) => {
self.viewport.half_page_down(n);
ActionOutcome::Broadcast
}
Action::SetMode(m) => {
self.mode = m;
ActionOutcome::Broadcast
}
Action::AddItems(values) => {
self.menu.add_raw(values);
self.run_filter();
@@ -166,6 +181,12 @@ impl<M: Menu> MenuRunner<M> {
}
}
/// Set the initial mode before running the event loop.
/// Used by `--start-mode` CLI flag.
pub fn set_initial_mode(&mut self, mode: Mode) {
self.mode = mode;
}
/// Run the menu event loop. Consumes actions and
/// broadcasts events.
///
@@ -617,6 +638,73 @@ mod tests {
));
}
// -- New action variant tests --
#[test]
fn apply_half_page_down() {
let (mut m, _tx) = test_menu();
m.run_filter();
m.apply_action(Action::Resize { height: 4 });
let outcome = m.apply_action(Action::HalfPageDown(1));
assert!(matches!(outcome, ActionOutcome::Broadcast));
assert_eq!(m.viewport.cursor(), 2); // 4/2 = 2
}
#[test]
fn apply_half_page_up() {
let (mut m, _tx) = test_menu();
m.run_filter();
m.apply_action(Action::Resize { height: 4 });
m.apply_action(Action::HalfPageDown(1));
let outcome = m.apply_action(Action::HalfPageUp(1));
assert!(matches!(outcome, ActionOutcome::Broadcast));
assert_eq!(m.viewport.cursor(), 0);
}
#[test]
fn apply_set_mode_normal() {
let mut m = ready_menu();
let outcome = m.apply_action(Action::SetMode(Mode::Normal));
assert!(matches!(outcome, ActionOutcome::Broadcast));
assert_eq!(m.build_view_state().mode, Mode::Normal);
}
#[test]
fn apply_set_mode_insert() {
let mut m = ready_menu();
m.apply_action(Action::SetMode(Mode::Normal));
let outcome = m.apply_action(Action::SetMode(Mode::Insert));
assert!(matches!(outcome, ActionOutcome::Broadcast));
assert_eq!(m.build_view_state().mode, Mode::Insert);
}
#[test]
fn apply_set_mode_preserves_filter() {
let mut m = ready_menu();
m.apply_action(Action::UpdateFilter("al".to_string()));
let count_before = m.menu.filtered_count();
let filter_before = m.filter_text.clone();
m.apply_action(Action::SetMode(Mode::Normal));
assert_eq!(&*m.filter_text, &*filter_before);
assert_eq!(m.menu.filtered_count(), count_before);
}
#[test]
fn set_initial_mode_works() {
let (mut m, _tx) = test_menu();
m.set_initial_mode(Mode::Normal);
m.run_filter();
assert_eq!(m.build_view_state().mode, Mode::Normal);
}
#[test]
fn view_state_mode_after_switch() {
let mut m = ready_menu();
m.apply_action(Action::SetMode(Mode::Normal));
let vs = m.build_view_state();
assert_eq!(vs.mode, Mode::Normal);
}
#[tokio::test]
async fn actions_process_in_order_add_items_then_filter_then_confirm() {
// AddItems + filter + confirm, all back-to-back.