274 lines
7.0 KiB
Rust
274 lines
7.0 KiB
Rust
//! Types that flow between the menu engine and frontends.
|
|
//!
|
|
//! - [`Action`]: commands sent into the menu (keypresses,
|
|
//! script actions).
|
|
//! - [`MenuEvent`]: notifications sent out to subscribers
|
|
//! (state changes, terminal events).
|
|
//! - [`ViewState`]: snapshot of what the frontend should
|
|
//! render right now.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use serde::ser::SerializeSeq;
|
|
use serde::Serialize;
|
|
use serde_json::Value;
|
|
|
|
use crate::hook::HookResponse;
|
|
|
|
/// Input mode. Insert mode sends keystrokes to the filter,
|
|
/// normal mode uses vim-style navigation keybinds.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub enum Mode {
|
|
#[default]
|
|
Insert,
|
|
Normal,
|
|
Visual,
|
|
}
|
|
|
|
/// A command the menu should process. Frontends and headless
|
|
/// scripts both produce these. The menu loop consumes them
|
|
/// sequentially.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum Action {
|
|
UpdateFilter(String),
|
|
MoveUp(usize),
|
|
MoveDown(usize),
|
|
MoveToTop,
|
|
MoveToBottom,
|
|
PageUp(usize),
|
|
PageDown(usize),
|
|
HalfPageUp(usize),
|
|
HalfPageDown(usize),
|
|
SetMode(Mode),
|
|
ToggleSelect,
|
|
SelectRange { start: usize, end: usize },
|
|
ClearSelections,
|
|
SelectAll,
|
|
UndoSelection,
|
|
RedoSelection,
|
|
Confirm,
|
|
Quicklist,
|
|
Cancel,
|
|
Resize { height: u16 },
|
|
AddItems(Vec<Value>),
|
|
ReplaceItems(Vec<Value>),
|
|
RemoveItems(Vec<usize>),
|
|
ProcessHookResponse(HookResponse),
|
|
CloseMenu,
|
|
StreamingDone,
|
|
}
|
|
|
|
/// Modifier keys held during an action. Serializes as an
|
|
/// array of strings (e.g. `["shift", "ctrl"]`). Empty
|
|
/// modifiers serialize as `[]`.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub struct Modifiers {
|
|
pub shift: bool,
|
|
pub ctrl: bool,
|
|
pub alt: bool,
|
|
}
|
|
|
|
impl Modifiers {
|
|
/// True when no modifiers are held.
|
|
pub fn is_empty(&self) -> bool {
|
|
!self.shift && !self.ctrl && !self.alt
|
|
}
|
|
}
|
|
|
|
impl Serialize for Modifiers {
|
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
let count = self.shift as usize + self.ctrl as usize + self.alt as usize;
|
|
let mut seq = serializer.serialize_seq(Some(count))?;
|
|
if self.shift {
|
|
seq.serialize_element("shift")?;
|
|
}
|
|
if self.ctrl {
|
|
seq.serialize_element("ctrl")?;
|
|
}
|
|
if self.alt {
|
|
seq.serialize_element("alt")?;
|
|
}
|
|
seq.end()
|
|
}
|
|
}
|
|
|
|
/// An [`Action`] paired with the modifier keys that were
|
|
/// held when it was triggered. Frontends produce these,
|
|
/// the menu engine consumes them.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct ModifiedAction {
|
|
pub action: Action,
|
|
pub modifiers: Modifiers,
|
|
}
|
|
|
|
impl ModifiedAction {
|
|
/// Wrap an action with default (empty) modifiers.
|
|
pub fn new(action: Action) -> Self {
|
|
Self {
|
|
action,
|
|
modifiers: Modifiers::default(),
|
|
}
|
|
}
|
|
|
|
/// Wrap an action with specific modifiers.
|
|
pub fn with_modifiers(action: Action, modifiers: Modifiers) -> Self {
|
|
Self { action, modifiers }
|
|
}
|
|
}
|
|
|
|
/// Broadcast from the menu loop to all subscribers
|
|
/// (frontends, tests).
|
|
#[derive(Debug, Clone)]
|
|
pub enum MenuEvent {
|
|
StateChanged(ViewState),
|
|
Selected(Vec<(Value, usize)>),
|
|
Quicklist(Vec<Value>),
|
|
Cancelled,
|
|
}
|
|
|
|
/// Snapshot of the menu's visible state. Sent on every state
|
|
/// change so frontends can render without querying back into
|
|
/// the engine.
|
|
#[must_use]
|
|
#[derive(Debug, Clone)]
|
|
pub struct ViewState {
|
|
pub visible_items: Vec<VisibleItem>,
|
|
/// Index of the cursor within `visible_items`
|
|
/// (not the full list).
|
|
pub cursor: usize,
|
|
pub filter_text: Arc<str>,
|
|
pub total_items: usize,
|
|
pub total_filtered: usize,
|
|
pub mode: Mode,
|
|
pub selection_count: usize,
|
|
pub multi_enabled: bool,
|
|
/// True while stdin is still streaming items in.
|
|
pub streaming: bool,
|
|
/// Column headers for table mode. None when not in
|
|
/// table mode.
|
|
pub columns: Option<Vec<ColumnHeader>>,
|
|
/// Monotonically increasing counter. Each call to
|
|
/// `build_view_state()` bumps this, so frontends can
|
|
/// detect duplicate broadcasts and skip redundant redraws.
|
|
pub generation: u64,
|
|
}
|
|
|
|
/// Column header with display name and computed width for
|
|
/// table mode rendering.
|
|
#[must_use]
|
|
#[derive(Debug, Clone)]
|
|
pub struct ColumnHeader {
|
|
pub display_name: String,
|
|
pub width: u16,
|
|
}
|
|
|
|
/// A single item in the current viewport window. Has the
|
|
/// display label pre-resolved and the position in the
|
|
/// filtered list.
|
|
#[must_use]
|
|
#[derive(Debug, Clone)]
|
|
pub struct VisibleItem {
|
|
pub label: String,
|
|
pub formatted_text: Option<String>,
|
|
pub index: usize,
|
|
pub selected: bool,
|
|
/// Column cell values for table mode. None when not in
|
|
/// table mode.
|
|
pub column_values: Option<Vec<String>>,
|
|
}
|
|
|
|
/// Final outcome of [`crate::menu::MenuRunner::run`]. The
|
|
/// user either picked something or bailed.
|
|
#[must_use]
|
|
#[derive(Debug)]
|
|
pub enum MenuResult {
|
|
Selected {
|
|
items: Vec<(Value, usize)>,
|
|
filter_text: String,
|
|
modifiers: Modifiers,
|
|
},
|
|
Quicklist {
|
|
items: Vec<(Value, usize)>,
|
|
filter_text: String,
|
|
modifiers: Modifiers,
|
|
},
|
|
Cancelled {
|
|
modifiers: Modifiers,
|
|
},
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn modifiers_empty_serializes_as_empty_array() {
|
|
let mods = Modifiers::default();
|
|
let json = serde_json::to_value(&mods).unwrap_or_default();
|
|
assert_eq!(json, json!([]));
|
|
}
|
|
|
|
#[test]
|
|
fn modifiers_single_serializes() {
|
|
let mods = Modifiers {
|
|
shift: true,
|
|
ctrl: false,
|
|
alt: false,
|
|
};
|
|
let json = serde_json::to_value(&mods).unwrap_or_default();
|
|
assert_eq!(json, json!(["shift"]));
|
|
}
|
|
|
|
#[test]
|
|
fn modifiers_multiple_serializes() {
|
|
let mods = Modifiers {
|
|
shift: true,
|
|
ctrl: true,
|
|
alt: false,
|
|
};
|
|
let json = serde_json::to_value(&mods).unwrap_or_default();
|
|
assert_eq!(json, json!(["shift", "ctrl"]));
|
|
}
|
|
|
|
#[test]
|
|
fn modifiers_all_serializes() {
|
|
let mods = Modifiers {
|
|
shift: true,
|
|
ctrl: true,
|
|
alt: true,
|
|
};
|
|
let json = serde_json::to_value(&mods).unwrap_or_default();
|
|
assert_eq!(json, json!(["shift", "ctrl", "alt"]));
|
|
}
|
|
|
|
#[test]
|
|
fn modifiers_is_empty() {
|
|
assert!(Modifiers::default().is_empty());
|
|
assert!(!Modifiers {
|
|
shift: true,
|
|
ctrl: false,
|
|
alt: false
|
|
}
|
|
.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn modified_action_new_has_empty_modifiers() {
|
|
let ma = ModifiedAction::new(Action::Confirm);
|
|
assert!(ma.modifiers.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn modified_action_with_modifiers() {
|
|
let mods = Modifiers {
|
|
shift: true,
|
|
ctrl: false,
|
|
alt: false,
|
|
};
|
|
let ma = ModifiedAction::with_modifiers(Action::Confirm, mods);
|
|
assert!(ma.modifiers.shift);
|
|
assert!(!ma.modifiers.ctrl);
|
|
}
|
|
}
|