use bcode_plugin_sdk::path::display_from_current_dir;
use bcode_worktree_models::WorktreeInfo;
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 WorktreePickerApp {
worktrees: Vec<WorktreeInfo>,
filter: TextInputState,
list: FilteredListState,
status: String,
}
impl WorktreePickerApp {
#[must_use]
pub fn new(worktrees: Vec<WorktreeInfo>) -> Self {
let list = FilteredListState::new(worktrees.len());
Self {
worktrees,
filter: super::text_input_flow::empty_state(),
list,
status: "Select a worktree or Esc to cancel".to_owned(),
}
}
pub const fn filter_mut(&mut self) -> &mut TextInputState {
&mut self.filter
}
pub fn list_render_state(&mut self, viewport_height: u16) -> &mut ListState {
self.list.render_state(viewport_height)
}
#[must_use]
pub fn status(&self) -> &str {
&self.status
}
pub fn set_status(&mut self, status: String) {
self.status = status;
}
#[must_use]
pub fn list_items(&self, muted: Style) -> Vec<ListItem> {
if self.list.indices().is_empty() {
return vec![empty_item("No matching worktrees", muted)];
}
self.list
.indices()
.iter()
.map(|index| worktree_item(&self.worktrees[*index], muted))
.collect()
}
#[must_use]
pub fn selected_worktree(&self) -> Option<&WorktreeInfo> {
let index = self.list.selected_source_index()?;
self.worktrees.get(index)
}
pub const fn select_visible(&mut self, row: usize) -> bool {
self.list.select_visible(row)
}
pub fn select_previous(&mut self) {
self.list.select_previous();
}
pub fn select_next(&mut self) {
self.list.select_next();
}
pub fn refresh_filter(&mut self) {
let query = self.filter.buffer().text().trim().to_ascii_lowercase();
let indices = self
.worktrees
.iter()
.enumerate()
.filter_map(|(index, worktree)| worktree_matches(worktree, &query).then_some(index))
.collect::<Vec<_>>();
self.list.replace_indices(indices);
}
}
fn worktree_matches(worktree: &WorktreeInfo, query: &str) -> bool {
if query.trim().is_empty() {
return true;
}
let haystack = format!(
"{} {} {}",
display_from_current_dir(&worktree.path),
worktree.branch.as_deref().unwrap_or(""),
worktree.commit.as_deref().unwrap_or("")
)
.to_ascii_lowercase();
haystack.contains(query)
}
fn worktree_item(worktree: &WorktreeInfo, muted: Style) -> ListItem {
let marker = if worktree.is_main { "main" } else { "linked" };
let branch = worktree.branch.as_deref().unwrap_or("<detached>");
let commit = worktree.commit.as_deref().unwrap_or("-");
Line::from_spans(vec![
Span::styled(branch.to_owned(), Style::new().add_modifier(Modifier::BOLD)),
Span::raw(" "),
Span::styled(marker.to_owned(), muted),
Span::raw(" "),
Span::styled(commit.to_owned(), muted),
Span::raw(" "),
Span::raw(display_from_current_dir(&worktree.path).to_string()),
])
}
fn empty_item(message: &str, muted: Style) -> ListItem {
Line::from_spans(vec![Span::styled(message.to_owned(), muted)])
}