use crossterm::event::{KeyCode, KeyModifiers};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Normal,
Leader,
Picker,
Command,
Confirm,
}
pub const DEFAULT_LEADER: (KeyCode, KeyModifiers) = (KeyCode::Char(' '), KeyModifiers::CONTROL);
pub fn parse_leader(spec: &str) -> Option<(KeyCode, KeyModifiers)> {
let rest = spec.trim().to_ascii_lowercase();
let rest = rest.strip_prefix("ctrl-")?;
let code = match rest {
"space" => KeyCode::Char(' '),
_ => {
let mut chars = rest.chars();
let c = chars.next()?;
if chars.next().is_some() {
return None; }
KeyCode::Char(c)
}
};
Some((code, KeyModifiers::CONTROL))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Command {
SpawnAgent,
NewSession,
CloseAgent,
SplitH,
SplitV,
Unsplit,
Autonomy,
KillDone,
ToggleSessions,
ToggleSubagents,
KeysHelp,
Quit,
}
pub const COMMANDS: &[(&str, Command)] = &[
("spawn subagent", Command::SpawnAgent),
("new session", Command::NewSession),
("close agent", Command::CloseAgent),
("split horizontal", Command::SplitH),
("split vertical", Command::SplitV),
("unsplit", Command::Unsplit),
("autonomy cycle", Command::Autonomy),
("kill done", Command::KillDone),
("toggle sessions", Command::ToggleSessions),
("toggle subagents", Command::ToggleSubagents),
("keys help", Command::KeysHelp),
("quit", Command::Quit),
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LeaderAction {
FocusSession(usize),
NextActionable,
NewSession,
Palette,
Close,
Autonomy,
Attach,
KeysHelp,
}
pub const LEADER_LEAVES: &[(KeyCode, &str, LeaderAction)] = &[
(
KeyCode::Tab,
"focus next actionable subagent",
LeaderAction::NextActionable,
),
(
KeyCode::Char('n'),
"new session (harness picker)",
LeaderAction::NewSession,
),
(KeyCode::Char('p'), "command palette", LeaderAction::Palette),
(
KeyCode::Char('c'),
"close the focused pane",
LeaderAction::Close,
),
(
KeyCode::Char('a'),
"cycle its autonomy tier",
LeaderAction::Autonomy,
),
(
KeyCode::Char('t'),
"attach: drive it natively",
LeaderAction::Attach,
),
(KeyCode::Char('?'), "keys help", LeaderAction::KeysHelp),
];
pub(super) fn leader_action(code: KeyCode) -> Option<LeaderAction> {
if let KeyCode::Char(c @ '1'..='9') = code {
return Some(LeaderAction::FocusSession((c as usize) - ('1' as usize)));
}
LEADER_LEAVES
.iter()
.find(|(key, _, _)| *key == code)
.map(|&(_, _, action)| action)
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PaletteState {
pub input: String,
pub selected: usize,
}
impl PaletteState {
pub fn matches(&self) -> Vec<(&'static str, Command)> {
COMMANDS
.iter()
.copied()
.filter(|(name, _)| fuzzy_match(name, &self.input))
.collect()
}
}
fn fuzzy_match(haystack: &str, needle: &str) -> bool {
let mut hay = haystack.chars().flat_map(char::to_lowercase);
needle
.chars()
.flat_map(char::to_lowercase)
.all(|n| hay.any(|h| h == n))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PickerPurpose {
Subagent,
Session,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PickerState {
pub agents: Vec<String>,
pub selected: usize,
pub purpose: PickerPurpose,
}