use crate::focus::FocusIntent;
use crate::runtime::{ActionOutcome, TuiEffect};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NavigationAction {
FocusNext,
FocusPrev,
Activate,
LeaveSection,
Quit,
NextBuffer,
PrevBuffer,
CloseBuffer,
NextPane,
PrevPane,
ClosePane,
SplitVertical,
SplitHorizontal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NavigationActionInfo {
pub action: NavigationAction,
pub name: &'static str,
pub label: &'static str,
pub description: &'static str,
pub category: &'static str,
}
impl NavigationAction {
pub const FOCUS: [NavigationAction; 5] = [
NavigationAction::FocusNext,
NavigationAction::FocusPrev,
NavigationAction::Activate,
NavigationAction::LeaveSection,
NavigationAction::Quit,
];
pub const WORKSPACE: [NavigationAction; 8] = [
NavigationAction::NextBuffer,
NavigationAction::PrevBuffer,
NavigationAction::CloseBuffer,
NavigationAction::NextPane,
NavigationAction::PrevPane,
NavigationAction::ClosePane,
NavigationAction::SplitVertical,
NavigationAction::SplitHorizontal,
];
pub fn all() -> impl Iterator<Item = NavigationAction> {
Self::FOCUS.into_iter().chain(Self::WORKSPACE)
}
pub fn infos() -> &'static [NavigationActionInfo] {
NAVIGATION_ACTION_INFOS
}
pub fn info(self) -> NavigationActionInfo {
NAVIGATION_ACTION_INFOS
.iter()
.copied()
.find(|info| info.action == self)
.expect("every NavigationAction has metadata")
}
pub fn as_name(&self) -> &str {
self.info().name
}
pub fn label(&self) -> &str {
self.info().label
}
pub fn description(&self) -> &str {
self.info().description
}
pub fn category(&self) -> &str {
self.info().category
}
pub fn from_name(name: &str) -> Option<Self> {
let normalized = name.replace('-', "_").to_ascii_lowercase();
match normalized.as_str() {
"focus_next" => Some(NavigationAction::FocusNext),
"focus_prev" => Some(NavigationAction::FocusPrev),
"activate" => Some(NavigationAction::Activate),
"leave_section" => Some(NavigationAction::LeaveSection),
"quit" => Some(NavigationAction::Quit),
"next_buffer" => Some(NavigationAction::NextBuffer),
"prev_buffer" | "previous_buffer" => Some(NavigationAction::PrevBuffer),
"close_buffer" => Some(NavigationAction::CloseBuffer),
"next_pane" => Some(NavigationAction::NextPane),
"prev_pane" | "previous_pane" => Some(NavigationAction::PrevPane),
"close_pane" => Some(NavigationAction::ClosePane),
"split_vertical" | "split_pane_vertical" => Some(NavigationAction::SplitVertical),
"split_horizontal" | "split_pane_horizontal" => Some(NavigationAction::SplitHorizontal),
_ => None,
}
}
pub fn to_effect<V, O, M>(self) -> TuiEffect<V, O, M> {
match self {
NavigationAction::FocusNext => TuiEffect::Focus(FocusIntent::Next),
NavigationAction::FocusPrev => TuiEffect::Focus(FocusIntent::Prev),
NavigationAction::Activate => TuiEffect::Focus(FocusIntent::Activate),
NavigationAction::LeaveSection => TuiEffect::Focus(FocusIntent::LeaveSection),
NavigationAction::Quit => TuiEffect::Quit,
NavigationAction::NextBuffer => TuiEffect::NextBuffer,
NavigationAction::PrevBuffer => TuiEffect::PreviousBuffer,
NavigationAction::CloseBuffer => TuiEffect::CloseBuffer,
NavigationAction::NextPane => TuiEffect::NextPane,
NavigationAction::PrevPane => TuiEffect::PreviousPane,
NavigationAction::ClosePane => TuiEffect::ClosePane,
NavigationAction::SplitVertical => {
TuiEffect::SplitPane(crate::navigation::PaneSplit::Vertical)
}
NavigationAction::SplitHorizontal => {
TuiEffect::SplitPane(crate::navigation::PaneSplit::Horizontal)
}
}
}
}
pub const NAVIGATION_ACTION_INFOS: &[NavigationActionInfo] = &[
NavigationActionInfo {
action: NavigationAction::FocusNext,
name: "focus_next",
label: "Focus next",
description: "Move focus to the next target.",
category: "Focus",
},
NavigationActionInfo {
action: NavigationAction::FocusPrev,
name: "focus_prev",
label: "Focus previous",
description: "Move focus to the previous target.",
category: "Focus",
},
NavigationActionInfo {
action: NavigationAction::Activate,
name: "activate",
label: "Activate",
description: "Activate the focused target.",
category: "Focus",
},
NavigationActionInfo {
action: NavigationAction::LeaveSection,
name: "leave_section",
label: "Leave section",
description: "Move focus out of the current section.",
category: "Focus",
},
NavigationActionInfo {
action: NavigationAction::Quit,
name: "quit",
label: "Quit",
description: "Request application shutdown.",
category: "Application",
},
NavigationActionInfo {
action: NavigationAction::NextBuffer,
name: "next_buffer",
label: "Next buffer",
description: "Switch to the next buffer.",
category: "Buffers",
},
NavigationActionInfo {
action: NavigationAction::PrevBuffer,
name: "prev_buffer",
label: "Previous buffer",
description: "Switch to the previous buffer.",
category: "Buffers",
},
NavigationActionInfo {
action: NavigationAction::CloseBuffer,
name: "close_buffer",
label: "Close buffer",
description: "Close the active buffer.",
category: "Buffers",
},
NavigationActionInfo {
action: NavigationAction::NextPane,
name: "next_pane",
label: "Next pane",
description: "Move to the next pane.",
category: "Panes",
},
NavigationActionInfo {
action: NavigationAction::PrevPane,
name: "prev_pane",
label: "Previous pane",
description: "Move to the previous pane.",
category: "Panes",
},
NavigationActionInfo {
action: NavigationAction::ClosePane,
name: "close_pane",
label: "Close pane",
description: "Close the active pane.",
category: "Panes",
},
NavigationActionInfo {
action: NavigationAction::SplitVertical,
name: "split_vertical",
label: "Split vertically",
description: "Split the active pane vertically.",
category: "Panes",
},
NavigationActionInfo {
action: NavigationAction::SplitHorizontal,
name: "split_horizontal",
label: "Split horizontally",
description: "Split the active pane horizontally.",
category: "Panes",
},
];
pub fn navigation_action_infos() -> &'static [NavigationActionInfo] {
NavigationAction::infos()
}
pub fn navigation_action_outcome<V, O, M>(action: NavigationAction) -> ActionOutcome<V, O, M> {
ActionOutcome::effect(action.to_effect())
}
pub fn try_standard_navigation_action<A, V, O, M>(
action: &A,
) -> Option<ActionOutcome<V, O, M>>
where
A: PartialEq + From<NavigationAction>,
{
for nav in NavigationAction::all() {
if *action == A::from(nav) {
return Some(navigation_action_outcome(nav));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TestAction {
Nav(NavigationAction),
Custom,
}
impl From<NavigationAction> for TestAction {
fn from(v: NavigationAction) -> Self {
TestAction::Nav(v)
}
}
#[test]
fn navigation_actions_round_trip_names() {
for action in NavigationAction::all() {
assert_eq!(NavigationAction::from_name(action.as_name()), Some(action));
}
}
#[test]
fn navigation_actions_have_public_metadata() {
assert_eq!(NavigationAction::FocusNext.label(), "Focus next");
assert_eq!(NavigationAction::SplitVertical.category(), "Panes");
assert_eq!(
NavigationAction::infos().len(),
NavigationAction::all().count()
);
}
#[test]
fn try_standard_navigation_action_dispatches_effects() {
let action = TestAction::Nav(NavigationAction::FocusNext);
let outcome: ActionOutcome<(), (), ()> =
try_standard_navigation_action(&action).expect("nav action");
assert_eq!(
outcome.effects,
vec![TuiEffect::Focus(FocusIntent::Next)]
);
assert!(
try_standard_navigation_action::<TestAction, (), (), ()>(&TestAction::Custom).is_none()
);
}
}