use crate::text_input::TextInput; use crate::DisplayLayout; use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::FuzzyMatcher; use zellij_tile::prelude::*; #[derive(Clone)] pub struct SearchResult { pub layout: DisplayLayout, pub original_index: usize, pub score: i64, pub matched_indices: Vec, } #[derive(Clone)] pub struct SearchState { filter_active: bool, filter_input: TextInput, typing_filter: bool, search_results: Vec, selected_search_index: usize, } impl Default for SearchState { fn default() -> Self { Self { filter_active: false, filter_input: TextInput::empty(), typing_filter: false, search_results: Vec::new(), selected_search_index: 0, } } } impl SearchState { pub fn new_in_search_mode() -> Self { Self { typing_filter: true, ..Self::default() } } pub fn fill_input_with_selected_match(&mut self) -> bool { if let Some(result) = self.search_results.get(self.selected_search_index) { let name = result.layout.name(); if self.filter_input.get_text() != name { self.filter_input.set_text(name); return true; } } false } pub fn is_active(&self) -> bool { self.filter_active } pub fn is_typing(&self) -> bool { self.typing_filter } pub fn start_typing(&mut self) { self.typing_filter = true; } pub fn get_filter_input(&self) -> &TextInput { &self.filter_input } pub fn get_filter_input_mut(&mut self) -> &mut TextInput { &mut self.filter_input } pub fn get_search_results(&self) -> &[SearchResult] { &self.search_results } pub fn get_selected_search_index(&self) -> usize { self.selected_search_index } pub fn set_selected_search_index(&mut self, index: usize) { self.selected_search_index = index; } pub fn update_filter( &mut self, display_layouts: &[DisplayLayout], base_x: usize, base_y: usize, ) { let filter_prompt = "Layout:"; let filter_text = self.filter_input.get_text(); // Clear results if filter is empty if filter_text.is_empty() && !self.typing_filter { self.search_results.clear(); self.selected_search_index = 0; return; } // Perform fuzzy matching let matcher = SkimMatcherV2::default(); let mut results: Vec = display_layouts .iter() .enumerate() .filter_map(|(index, layout)| { let name = layout.name(); matcher .fuzzy_indices(&name, filter_text) .map(|(score, indices)| SearchResult { layout: layout.clone(), original_index: index, score, matched_indices: indices, }) }) .collect(); // Sort by score descending (best matches first) results.sort_by(|a, b| b.score.cmp(&a.score)); self.search_results = results; if self.search_results.is_empty() { self.filter_active = false; } else { self.filter_active = true; } // Keep selection in bounds if self.selected_search_index >= self.search_results.len() { self.selected_search_index = self.search_results.len().saturating_sub(1); } // Show cursor when typing if self.typing_filter { let cursor_pos = self.filter_input.get_cursor_position(); show_cursor(Some(( filter_prompt.chars().count() + 1 + cursor_pos + base_x, base_y, ))); } else { show_cursor(None); } } pub fn clear_filter(&mut self) { self.filter_active = false; self.typing_filter = false; self.filter_input.clear(); self.search_results.clear(); self.selected_search_index = 0; show_cursor(None); } pub fn get_current_selected_original_index(&self) -> Option { self.search_results .get(self.selected_search_index) .map(|result| result.original_index) } pub fn render_filter_line(&self, base_x: usize, base_y: usize) { let filter_text_str = self.filter_input.get_text(); let filter_text = if self.typing_filter { let mut filter_line = Text::new(format!("Layout: {}", filter_text_str)).color_substring(2, "Layout:"); if !filter_text_str.is_empty() { filter_line = filter_line.color_last_substring(3, filter_text_str) } filter_line } else { Text::new(format!("Layout: {} ( - clear)", filter_text_str)) .color_substring(3, "") .color_substring(2, "Layout:") }; print_text_with_coordinates(filter_text, base_x, base_y, None, None); } pub fn get_matched_indices_for_visible( &self, hidden_above: usize, visible_count: usize, ) -> Vec>> { if !self.filter_active || self.search_results.is_empty() { return vec![None; visible_count]; } self.search_results .iter() .skip(hidden_above) .take(visible_count) .map(|result| Some(result.matched_indices.clone())) .collect() } }