use bcode_skill_models::{SkillId, SkillSummary};
use bmux_tui::prelude::Line as ListItem;
use bmux_tui::prelude::{Line, Span, Style};
use bmux_tui::style::Modifier;
use bmux_tui_components::selectable_list::SelectableListState as ListState;
use bmux_tui_components::text_input::TextInputState;
use super::filtered_list::FilteredListState;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkillPickerApp {
skills: Vec<SkillSummary>,
filter: TextInputState,
argument: TextInputState,
list: FilteredListState,
mode: SkillPickerMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SkillPickerMode {
Filter,
Argument,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SkillPickerAction {
Continue,
Invoke {
skill_id: SkillId,
arguments: String,
},
Activate(SkillId),
Deactivate(SkillId),
Help(SkillId),
Cancel,
}
impl SkillPickerApp {
#[must_use]
pub fn new(skills: Vec<SkillSummary>) -> Self {
let list = FilteredListState::new(skills.len());
Self {
skills,
filter: super::text_input_flow::empty_state(),
argument: super::text_input_flow::empty_state(),
list,
mode: SkillPickerMode::Filter,
}
}
pub const fn filter_mut(&mut self) -> &mut TextInputState {
&mut self.filter
}
#[must_use]
pub const fn argument(&self) -> &TextInputState {
&self.argument
}
pub const fn argument_mut(&mut self) -> &mut TextInputState {
&mut self.argument
}
#[must_use]
pub const fn mode(&self) -> SkillPickerMode {
self.mode
}
pub fn list_render_state(&mut self, viewport_height: u16) -> &mut ListState {
self.list.render_state(viewport_height)
}
#[must_use]
pub fn list_items(&self, muted: Style) -> Vec<ListItem> {
if self.list.indices().is_empty() {
return vec![empty_item("No matching skills.", muted)];
}
self.list
.indices()
.iter()
.map(|index| skill_item(&self.skills[*index], muted))
.collect()
}
#[must_use]
pub fn selected_skill_id(&self) -> Option<SkillId> {
let index = self.list.selected_source_index()?;
Some(self.skills[index].id.clone())
}
pub fn refresh_filter(&mut self) {
let query = self.filter.buffer().text().trim().to_ascii_lowercase();
let filtered_indices = self
.skills
.iter()
.enumerate()
.filter_map(|(index, skill)| skill_matches(skill, &query).then_some(index))
.collect();
self.list.replace_indices(filtered_indices);
}
pub fn select_next(&mut self) {
self.list.select_next();
}
pub fn select_previous(&mut self) {
self.list.select_previous();
}
pub const fn select_visible(&mut self, row: usize) -> bool {
self.list.select_visible(row)
}
pub const fn start_argument(&mut self) {
self.mode = SkillPickerMode::Argument;
}
}
fn skill_item(skill: &SkillSummary, muted: Style) -> ListItem {
let description = skill.description.as_deref().unwrap_or("no description");
Line::from_spans(vec![
Span::styled(
skill.id.to_string(),
Style::new().add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(description.to_owned(), muted),
])
}
fn skill_matches(skill: &SkillSummary, query: &str) -> bool {
query.is_empty()
|| skill.id.as_str().to_ascii_lowercase().contains(query)
|| skill.name.to_ascii_lowercase().contains(query)
|| skill
.description
.as_deref()
.is_some_and(|description| description.to_ascii_lowercase().contains(query))
}
fn empty_item(message: &str, muted: Style) -> ListItem {
Line::from_spans(vec![Span::styled(message.to_owned(), muted)])
}