use ratatui_action::id::ActionId;
use ratatui_action::input::ActionInput;
use ratatui_action::invocation::{ActionArgs, ActionInvocation, InvocationSource};
use ratatui_action::spec::{ActionSpec, Availability};
use crate::event::{MoveSelection, PaletteEvent, PaletteMode};
use crate::shortcut::ShortcutLabels;
use crate::view::{PaletteRow, PaletteView};
use crate::{matching, selection};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PaletteState {
query: String,
selected: Option<usize>,
mode: PaletteMode,
args: ActionArgs,
}
impl Default for PaletteState {
fn default() -> Self {
Self {
query: String::new(),
selected: None,
mode: PaletteMode::Searching,
args: ActionArgs::new(),
}
}
}
impl PaletteState {
pub fn new() -> Self {
Self::default()
}
pub fn query(&self) -> &str {
&self.query
}
pub fn selected(&self) -> Option<usize> {
self.selected
}
pub fn mode(&self) -> &PaletteMode {
&self.mode
}
pub fn open(&mut self, actions: &[ActionSpec]) -> PaletteEvent {
self.mode = PaletteMode::Searching;
self.args = ActionArgs::new();
self.clamp_selection(actions);
PaletteEvent::Opened
}
pub fn close(&mut self) -> PaletteEvent {
self.mode = PaletteMode::Searching;
self.args = ActionArgs::new();
PaletteEvent::Closed
}
pub fn cancel(&mut self) -> PaletteEvent {
self.cancel_events();
PaletteEvent::Cancelled
}
pub fn cancel_events(&mut self) -> Vec<PaletteEvent> {
let clears_preview = matches!(self.mode, PaletteMode::CollectingInput { .. });
self.query.clear();
self.selected = None;
self.mode = PaletteMode::Searching;
self.args = ActionArgs::new();
let mut events = Vec::new();
if clears_preview {
events.push(PaletteEvent::PreviewChanged(None));
}
events.push(PaletteEvent::Cancelled);
events
}
pub fn set_query(&mut self, query: impl Into<String>, actions: &[ActionSpec]) {
self.mode = PaletteMode::Searching;
self.args = ActionArgs::new();
self.query = query.into();
self.select_first(actions);
}
pub fn push_query_char(&mut self, character: char, actions: &[ActionSpec]) {
if matches!(self.current_input(actions), Some(ActionInput::Text { .. })) {
self.query.push(character);
return;
}
self.mode = PaletteMode::Searching;
self.args = ActionArgs::new();
self.query.push(character);
self.select_first(actions);
}
pub fn pop_query_char(&mut self, actions: &[ActionSpec]) -> Option<char> {
if matches!(self.current_input(actions), Some(ActionInput::Text { .. })) {
return self.query.pop();
}
self.mode = PaletteMode::Searching;
self.args = ActionArgs::new();
let character = self.query.pop();
self.select_first(actions);
character
}
pub fn move_selection(
&mut self,
movement: MoveSelection,
actions: &[ActionSpec],
) -> Option<PaletteEvent> {
let row_count = self.current_row_count(actions);
self.selected = selection::move_selection(self.selected, row_count, movement);
self.preview_event(actions)
}
pub fn view(&self, actions: &[ActionSpec]) -> PaletteView {
self.view_with_shortcuts(actions, &ShortcutLabels::new())
}
pub fn view_with_shortcuts(
&self,
actions: &[ActionSpec],
shortcuts: &ShortcutLabels,
) -> PaletteView {
let rows = match &self.mode {
PaletteMode::Searching => self
.filtered_actions(actions)
.into_iter()
.map(|action| PaletteRow::from_action_with_shortcuts(action, shortcuts))
.collect(),
PaletteMode::CollectingInput {
action,
input_index,
} => self.input_rows(actions, action, *input_index),
};
PaletteView {
prompt: self.prompt(actions),
query: match &self.mode {
PaletteMode::Searching => self.query.clone(),
PaletteMode::CollectingInput { .. }
if matches!(self.current_input(actions), Some(ActionInput::Text { .. })) =>
{
self.query.clone()
}
PaletteMode::CollectingInput { .. } => String::new(),
},
rows,
selected: self.selected,
mode: self.mode.clone(),
}
}
pub fn accept(&mut self, actions: &[ActionSpec]) -> Option<PaletteEvent> {
if matches!(self.mode, PaletteMode::CollectingInput { .. }) {
return self.accept_input(actions);
}
let action = self.selected_action(actions)?;
match action.availability() {
Availability::Enabled if action.inputs().is_empty() => {
let invocation =
ActionInvocation::new(action.id().clone(), InvocationSource::Palette);
self.query.clear();
self.select_first(actions);
Some(PaletteEvent::Invoke(invocation))
}
Availability::Enabled
if action
.inputs()
.first()
.filter(|input| is_collectible_input(input))
.is_some() =>
{
self.mode = PaletteMode::CollectingInput {
action: action.id().clone(),
input_index: 0,
};
self.query.clear();
self.selected = selection::selected_for_input(action.inputs().first());
self.preview_event(actions)
}
Availability::Enabled => None,
Availability::Disabled { .. } | Availability::Hidden => None,
}
}
pub fn preview_event(&self, actions: &[ActionSpec]) -> Option<PaletteEvent> {
let (action, input, value) = self.selected_input_value(actions)?;
let mut args = self.args.clone();
args.insert(input.id().clone(), value);
let invocation =
ActionInvocation::with_args(action.id().clone(), args, InvocationSource::Palette);
Some(PaletteEvent::PreviewChanged(Some(invocation)))
}
fn accept_input(&mut self, actions: &[ActionSpec]) -> Option<PaletteEvent> {
let (action, input, value) = self.current_input_value(actions)?;
let input_id = input.id().clone();
let action_id = action.id().clone();
self.args.insert(input_id, value);
let PaletteMode::CollectingInput { input_index, .. } = self.mode else {
return None;
};
let next_index = input_index + 1;
if next_index < action.inputs().len() {
self.mode = PaletteMode::CollectingInput {
action: action_id,
input_index: next_index,
};
self.query.clear();
self.selected = selection::selected_for_input(action.inputs().get(next_index));
return self.preview_event(actions);
}
let invocation = ActionInvocation::with_args(
action_id,
std::mem::take(&mut self.args),
InvocationSource::Palette,
);
self.mode = PaletteMode::Searching;
self.query.clear();
Some(PaletteEvent::Invoke(invocation))
}
fn selected_action<'a>(&self, actions: &'a [ActionSpec]) -> Option<&'a ActionSpec> {
let selected = self.selected?;
self.filtered_actions(actions).get(selected).copied()
}
fn clamp_selection(&mut self, actions: &[ActionSpec]) {
let row_count = self.current_row_count(actions);
self.selected = match (self.selected, row_count) {
(_, 0) => None,
(Some(selected), row_count) if selected < row_count => Some(selected),
_ => Some(0),
};
}
fn select_first(&mut self, actions: &[ActionSpec]) {
self.selected = (self.current_row_count(actions) > 0).then_some(0);
}
fn filtered_actions<'a>(&self, actions: &'a [ActionSpec]) -> Vec<&'a ActionSpec> {
matching::filtered_actions(actions, &self.query)
}
fn current_row_count(&self, actions: &[ActionSpec]) -> usize {
match &self.mode {
PaletteMode::Searching => self.filtered_actions(actions).len(),
PaletteMode::CollectingInput {
action,
input_index,
} => self.input_rows(actions, action, *input_index).len(),
}
}
fn prompt(&self, actions: &[ActionSpec]) -> String {
match &self.mode {
PaletteMode::Searching => ">".into(),
PaletteMode::CollectingInput { .. } => self
.current_input(actions)
.map_or_else(|| ">".into(), |input| format!("{}:", input.label())),
}
}
fn current_input<'a>(&self, actions: &'a [ActionSpec]) -> Option<&'a ActionInput> {
let PaletteMode::CollectingInput {
action,
input_index,
} = &self.mode
else {
return None;
};
actions
.iter()
.find(|candidate| candidate.id() == action)?
.inputs()
.get(*input_index)
}
fn input_rows(
&self,
actions: &[ActionSpec],
action: &ActionId,
input_index: usize,
) -> Vec<PaletteRow> {
let Some(input) = actions
.iter()
.find(|candidate| candidate.id() == action)
.and_then(|action| action.inputs().get(input_index))
else {
return Vec::new();
};
match input {
ActionInput::Choice { .. } => input
.choices()
.unwrap_or_default()
.iter()
.map(|choice| PaletteRow::from_choice(action, input, choice))
.collect(),
ActionInput::Bool { .. } => vec![
PaletteRow::from_bool(action, input, true),
PaletteRow::from_bool(action, input, false),
],
ActionInput::Text { .. } => Vec::new(),
}
}
fn current_input_value<'a>(
&self,
actions: &'a [ActionSpec],
) -> Option<(&'a ActionSpec, &'a ActionInput, String)> {
let PaletteMode::CollectingInput {
action,
input_index,
} = &self.mode
else {
return None;
};
let action = actions.iter().find(|candidate| candidate.id() == action)?;
let input = action.inputs().get(*input_index)?;
match input {
ActionInput::Text { .. } => Some((action, input, self.query.clone())),
ActionInput::Choice { .. } | ActionInput::Bool { .. } => {
self.selected_input_value(actions)
}
}
}
fn selected_input_value<'a>(
&self,
actions: &'a [ActionSpec],
) -> Option<(&'a ActionSpec, &'a ActionInput, String)> {
let PaletteMode::CollectingInput {
action,
input_index,
} = &self.mode
else {
return None;
};
let selected = self.selected?;
let action = actions.iter().find(|candidate| candidate.id() == action)?;
let input = action.inputs().get(*input_index)?;
match input {
ActionInput::Choice { choices, .. } => choices
.get(selected)
.map(|choice| (action, input, choice.value().to_string())),
ActionInput::Bool { .. } => match selected {
0 => Some((action, input, "true".into())),
1 => Some((action, input, "false".into())),
_ => None,
},
ActionInput::Text { .. } => None,
}
}
}
fn is_collectible_input(input: &ActionInput) -> bool {
match input {
ActionInput::Text { .. } | ActionInput::Bool { .. } => true,
ActionInput::Choice { choices, .. } => !choices.is_empty(),
}
}
#[cfg(test)]
#[path = "state_tests.rs"]
mod state_tests;