feat(tui): Phase 2 Issue List + MR List screens
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
This commit is contained in:
180
crates/lore-tui/src/commands/defs.rs
Normal file
180
crates/lore-tui/src/commands/defs.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
//! 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,
|
||||
}
|
||||
Reference in New Issue
Block a user