Implement state, action, and view layers for both list screens: - Issue List: keyset pagination, snapshot fence, filter DSL, label aggregation - MR List: mirrors Issue pattern with draft/reviewer/target branch filters - Migration 027: covering indexes for TUI list screen queries - Updated Msg types to use typed Page structs instead of raw Vec<Row> - 303 tests passing, clippy clean Beads: bd-3ei1, bd-2kr0, bd-3pm2
181 lines
5.7 KiB
Rust
181 lines
5.7 KiB
Rust
//! Command definitions — types for keybindings, screen filtering, and command metadata.
|
|
|
|
use ftui::{KeyCode, Modifiers};
|
|
|
|
use crate::message::Screen;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Key formatting
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Format a key code + modifiers as a human-readable string.
|
|
pub(crate) fn format_key(code: KeyCode, modifiers: Modifiers) -> String {
|
|
let mut parts = Vec::new();
|
|
if modifiers.contains(Modifiers::CTRL) {
|
|
parts.push("Ctrl");
|
|
}
|
|
if modifiers.contains(Modifiers::ALT) {
|
|
parts.push("Alt");
|
|
}
|
|
if modifiers.contains(Modifiers::SHIFT) {
|
|
parts.push("Shift");
|
|
}
|
|
let key_name = match code {
|
|
KeyCode::Char(c) => c.to_string(),
|
|
KeyCode::Enter => "Enter".to_string(),
|
|
KeyCode::Escape => "Esc".to_string(),
|
|
KeyCode::Tab => "Tab".to_string(),
|
|
KeyCode::Backspace => "Backspace".to_string(),
|
|
KeyCode::Delete => "Del".to_string(),
|
|
KeyCode::Up => "Up".to_string(),
|
|
KeyCode::Down => "Down".to_string(),
|
|
KeyCode::Left => "Left".to_string(),
|
|
KeyCode::Right => "Right".to_string(),
|
|
KeyCode::Home => "Home".to_string(),
|
|
KeyCode::End => "End".to_string(),
|
|
KeyCode::PageUp => "PgUp".to_string(),
|
|
KeyCode::PageDown => "PgDn".to_string(),
|
|
KeyCode::F(n) => format!("F{n}"),
|
|
_ => "?".to_string(),
|
|
};
|
|
parts.push(&key_name);
|
|
// We need to own the joined string.
|
|
let joined: String = parts.join("+");
|
|
joined
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// KeyCombo
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// A keybinding: either a single key or a two-key sequence.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
pub enum KeyCombo {
|
|
/// Single key press (e.g., `q`, `Esc`, `Ctrl+P`).
|
|
Single { code: KeyCode, modifiers: Modifiers },
|
|
/// Two-key sequence (e.g., `g` then `i` for go-to-issues).
|
|
Sequence {
|
|
first_code: KeyCode,
|
|
first_modifiers: Modifiers,
|
|
second_code: KeyCode,
|
|
second_modifiers: Modifiers,
|
|
},
|
|
}
|
|
|
|
impl KeyCombo {
|
|
/// Convenience: single key with no modifiers.
|
|
#[must_use]
|
|
pub const fn key(code: KeyCode) -> Self {
|
|
Self::Single {
|
|
code,
|
|
modifiers: Modifiers::NONE,
|
|
}
|
|
}
|
|
|
|
/// Convenience: single key with Ctrl modifier.
|
|
#[must_use]
|
|
pub const fn ctrl(code: KeyCode) -> Self {
|
|
Self::Single {
|
|
code,
|
|
modifiers: Modifiers::CTRL,
|
|
}
|
|
}
|
|
|
|
/// Convenience: g-prefix sequence (g + char).
|
|
#[must_use]
|
|
pub const fn g_then(c: char) -> Self {
|
|
Self::Sequence {
|
|
first_code: KeyCode::Char('g'),
|
|
first_modifiers: Modifiers::NONE,
|
|
second_code: KeyCode::Char(c),
|
|
second_modifiers: Modifiers::NONE,
|
|
}
|
|
}
|
|
|
|
/// Human-readable display string for this key combo.
|
|
#[must_use]
|
|
pub fn display(&self) -> String {
|
|
match self {
|
|
Self::Single { code, modifiers } => format_key(*code, *modifiers),
|
|
Self::Sequence {
|
|
first_code,
|
|
first_modifiers,
|
|
second_code,
|
|
second_modifiers,
|
|
} => {
|
|
let first = format_key(*first_code, *first_modifiers);
|
|
let second = format_key(*second_code, *second_modifiers);
|
|
format!("{first} {second}")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Whether this combo starts with the given key.
|
|
#[must_use]
|
|
pub fn starts_with(&self, code: &KeyCode, modifiers: &Modifiers) -> bool {
|
|
match self {
|
|
Self::Single {
|
|
code: c,
|
|
modifiers: m,
|
|
} => c == code && m == modifiers,
|
|
Self::Sequence {
|
|
first_code,
|
|
first_modifiers,
|
|
..
|
|
} => first_code == code && first_modifiers == modifiers,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ScreenFilter
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Specifies which screens a command is available on.
|
|
#[derive(Debug, Clone)]
|
|
pub enum ScreenFilter {
|
|
/// Available on all screens.
|
|
Global,
|
|
/// Available only on specific screens.
|
|
Only(Vec<Screen>),
|
|
}
|
|
|
|
impl ScreenFilter {
|
|
/// Whether the command is available on the given screen.
|
|
#[must_use]
|
|
pub fn matches(&self, screen: &Screen) -> bool {
|
|
match self {
|
|
Self::Global => true,
|
|
Self::Only(screens) => screens.contains(screen),
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CommandDef
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Unique command identifier.
|
|
pub type CommandId = &'static str;
|
|
|
|
/// A registered command with its keybinding, help text, and scope.
|
|
#[derive(Debug, Clone)]
|
|
pub struct CommandDef {
|
|
/// Unique identifier (e.g., "quit", "go_issues").
|
|
pub id: CommandId,
|
|
/// Human-readable label for palette and help overlay.
|
|
pub label: &'static str,
|
|
/// Keybinding (if any).
|
|
pub keybinding: Option<KeyCombo>,
|
|
/// Equivalent `lore` CLI command (for "Show CLI equivalent" feature).
|
|
pub cli_equivalent: Option<&'static str>,
|
|
/// Description for help overlay.
|
|
pub help_text: &'static str,
|
|
/// Short hint for status bar (e.g., "q:quit").
|
|
pub status_hint: &'static str,
|
|
/// Which screens this command is available on.
|
|
pub available_in: ScreenFilter,
|
|
/// Whether this command works in Text input mode.
|
|
pub available_in_text_mode: bool,
|
|
}
|