#![allow(dead_code)]
use std::fmt;
use crate::layout::Direction;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Action {
SplitWindow(Direction),
NewTab,
NextTab,
PrevTab,
KillPane,
CloseTab,
RenameTab(String),
SelectLayout(String),
Equalize,
Zoom,
Broadcast,
SelectPane(SelectDirection),
EnterMode(EnterModeKind),
LeaveMode,
Detach,
KillSession,
ReloadConfig,
Toggle(ToggleTarget),
CommandPalette,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum SelectDirection {
Up,
Down,
Left,
Right,
Next,
Prev,
Last,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum EnterModeKind {
Prefix,
Copy,
Resize,
PaneSelect,
Help,
CommandPalette,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ToggleTarget {
StatusBar,
Hints,
}
#[derive(Clone, Debug)]
pub struct ParseError(pub String);
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for ParseError {}
pub fn parse(s: &str) -> Result<Action, ParseError> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(ParseError("empty action".into()));
}
let parts: Vec<&str> = trimmed.split_whitespace().collect();
let head = parts[0];
let rest = &parts[1..];
match head {
"split-window" | "split" => {
let dir = if rest.first() == Some(&"-v") || rest.first() == Some(&"v") {
Direction::Vertical
} else if rest.first() == Some(&"horizontal") {
Direction::Horizontal
} else if rest.first() == Some(&"vertical") {
Direction::Vertical
} else {
Direction::Horizontal
};
Ok(Action::SplitWindow(dir))
}
"new-window" | "new-tab" => Ok(Action::NewTab),
"next-window" | "next-tab" => Ok(Action::NextTab),
"prev-window" | "prev-tab" | "previous-window" => Ok(Action::PrevTab),
"kill-pane" | "close-pane" => Ok(Action::KillPane),
"kill-window" | "close-tab" => Ok(Action::CloseTab),
"rename-window" | "rename-tab" => {
let name = rest.join(" ");
if name.is_empty() {
Err(ParseError("rename-tab requires a name".into()))
} else {
Ok(Action::RenameTab(name))
}
}
"select-layout" | "layout" => {
let spec = rest.join(" ");
if spec.is_empty() {
Err(ParseError("select-layout requires a spec".into()))
} else {
Ok(Action::SelectLayout(spec))
}
}
"equalize" | "even" => Ok(Action::Equalize),
"zoom" => Ok(Action::Zoom),
"broadcast" => Ok(Action::Broadcast),
"select-pane" => {
let dir = match rest.first().copied() {
Some("up") => SelectDirection::Up,
Some("down") => SelectDirection::Down,
Some("left") => SelectDirection::Left,
Some("right") => SelectDirection::Right,
Some("next") => SelectDirection::Next,
Some("prev") | Some("previous") => SelectDirection::Prev,
Some("last") => SelectDirection::Last,
_ => {
return Err(ParseError(
"select-pane requires up|down|left|right|next|prev|last".into(),
))
}
};
Ok(Action::SelectPane(dir))
}
"enter-mode" => {
let kind = match rest.first().copied() {
Some("prefix") => EnterModeKind::Prefix,
Some("copy") | Some("copy-mode") => EnterModeKind::Copy,
Some("resize") => EnterModeKind::Resize,
Some("pane-select") => EnterModeKind::PaneSelect,
Some("help") => EnterModeKind::Help,
Some("command-palette") | Some("palette") => EnterModeKind::CommandPalette,
_ => return Err(ParseError("enter-mode requires a mode name".into())),
};
Ok(Action::EnterMode(kind))
}
"leave-mode" => Ok(Action::LeaveMode),
"detach" => Ok(Action::Detach),
"kill-session" => Ok(Action::KillSession),
"reload-config" | "reload" => Ok(Action::ReloadConfig),
"toggle" => {
let kind = match rest.first().copied() {
Some("status-bar") | Some("statusbar") => ToggleTarget::StatusBar,
Some("hints") => ToggleTarget::Hints,
_ => return Err(ParseError("toggle requires status-bar|hints".into())),
};
Ok(Action::Toggle(kind))
}
"command-palette" | "palette" => Ok(Action::CommandPalette),
other => Err(ParseError(format!("unknown action: {other}"))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_split_aliases() {
assert_eq!(
parse("split").unwrap(),
Action::SplitWindow(Direction::Horizontal)
);
assert_eq!(
parse("split-window").unwrap(),
Action::SplitWindow(Direction::Horizontal)
);
assert_eq!(
parse("split -v").unwrap(),
Action::SplitWindow(Direction::Vertical)
);
assert_eq!(
parse("split vertical").unwrap(),
Action::SplitWindow(Direction::Vertical)
);
}
#[test]
fn parses_tab_aliases() {
assert_eq!(parse("new-tab").unwrap(), Action::NewTab);
assert_eq!(parse("new-window").unwrap(), Action::NewTab);
assert_eq!(parse("next-tab").unwrap(), Action::NextTab);
assert_eq!(parse("prev-tab").unwrap(), Action::PrevTab);
assert_eq!(parse("previous-window").unwrap(), Action::PrevTab);
}
#[test]
fn parses_select_pane_directions() {
assert_eq!(
parse("select-pane left").unwrap(),
Action::SelectPane(SelectDirection::Left)
);
assert_eq!(
parse("select-pane next").unwrap(),
Action::SelectPane(SelectDirection::Next)
);
assert!(parse("select-pane").is_err());
assert!(parse("select-pane diagonal").is_err());
}
#[test]
fn parses_enter_mode_variants() {
assert_eq!(
parse("enter-mode copy").unwrap(),
Action::EnterMode(EnterModeKind::Copy)
);
assert_eq!(
parse("enter-mode help").unwrap(),
Action::EnterMode(EnterModeKind::Help)
);
assert!(parse("enter-mode").is_err());
assert!(parse("enter-mode bogus").is_err());
}
#[test]
fn parses_toggle_variants() {
assert_eq!(
parse("toggle hints").unwrap(),
Action::Toggle(ToggleTarget::Hints)
);
assert_eq!(
parse("toggle status-bar").unwrap(),
Action::Toggle(ToggleTarget::StatusBar)
);
}
#[test]
fn parses_rename_with_multi_word_name() {
assert_eq!(
parse("rename-tab my pretty tab").unwrap(),
Action::RenameTab("my pretty tab".to_string())
);
assert!(parse("rename-tab").is_err());
}
#[test]
fn rejects_unknown_action() {
let e = parse("teleport").unwrap_err();
assert!(e.to_string().contains("unknown action"), "got: {e}");
}
#[test]
fn rejects_empty_action() {
assert!(parse("").is_err());
assert!(parse(" ").is_err());
}
}