use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::FuzzyMatcher; use humantime::format_duration; use crate::ui::components::render_resurrection_toggle; use std::time::Duration; use zellij_tile::shim::*; #[derive(Debug, Default)] pub struct ResurrectableSessions { pub all_resurrectable_sessions: Vec<(String, Duration)>, pub selected_index: Option, pub selected_search_index: Option, pub search_results: Vec, pub is_searching: bool, pub search_term: String, pub delete_all_dead_sessions_warning: bool, } impl ResurrectableSessions { pub fn update(&mut self, mut list: Vec<(String, Duration)>) { list.sort_by(|a, b| a.1.cmp(&b.1)); self.all_resurrectable_sessions = list; } pub fn render(&self, rows: usize, columns: usize) { if self.delete_all_dead_sessions_warning { self.render_delete_all_sessions_warning(rows, columns); return; } render_resurrection_toggle(columns, true); let search_indication = Text::new(format!("> {}_", self.search_term)).color_range(1, ..); let table_rows = rows.saturating_sub(3); let table_columns = columns; let table = if self.is_searching { self.render_search_results(table_rows, columns) } else { self.render_all_entries(table_rows, columns) }; print_text_with_coordinates(search_indication, 0, 0, None, None); print_table_with_coordinates(table, 0, 1, Some(table_columns), Some(table_rows)); self.render_controls_line(rows); } fn render_search_results(&self, table_rows: usize, _table_columns: usize) -> Table { let mut table = Table::new().add_row(vec![" ", " ", " "]); // skip the title row let (first_row_index_to_render, last_row_index_to_render) = self.range_to_render( table_rows, self.search_results.len(), self.selected_search_index, ); for i in first_row_index_to_render..last_row_index_to_render { if let Some(search_result) = self.search_results.get(i) { let is_selected = Some(i) == self.selected_search_index; let mut table_cells = vec![ self.render_session_name( &search_result.session_name, Some(search_result.indices.clone()), ), self.render_ctime(&search_result.ctime), self.render_more_indication_or_enter_as_needed( i, first_row_index_to_render, last_row_index_to_render, self.search_results.len(), is_selected, ), ]; if is_selected { table_cells = table_cells.drain(..).map(|t| t.selected()).collect(); } table = table.add_styled_row(table_cells); } } table } fn render_all_entries(&self, table_rows: usize, _table_columns: usize) -> Table { let mut table = Table::new().add_row(vec![" ", " ", " "]); // skip the title row let (first_row_index_to_render, last_row_index_to_render) = self.range_to_render( table_rows, self.all_resurrectable_sessions.len(), self.selected_index, ); for i in first_row_index_to_render..last_row_index_to_render { if let Some(session) = self.all_resurrectable_sessions.get(i) { let is_selected = Some(i) == self.selected_index; let mut table_cells = vec![ self.render_session_name(&session.0, None), self.render_ctime(&session.1), self.render_more_indication_or_enter_as_needed( i, first_row_index_to_render, last_row_index_to_render, self.all_resurrectable_sessions.len(), is_selected, ), ]; if is_selected { table_cells = table_cells.drain(..).map(|t| t.selected()).collect(); } table = table.add_styled_row(table_cells); } } table } fn render_delete_all_sessions_warning(&self, rows: usize, columns: usize) { if rows == 0 || columns == 0 { return; } let session_count = self.all_resurrectable_sessions.len(); let session_count_len = session_count.to_string().chars().count(); let warning_description_text = format!("This will delete {} resurrectable sessions", session_count,); let confirmation_text = "Are you sure? (y/n)"; let warning_y_location = (rows / 2).saturating_sub(1); let confirmation_y_location = (rows / 2) + 1; let warning_x_location = columns.saturating_sub(warning_description_text.chars().count()) / 2; let confirmation_x_location = columns.saturating_sub(confirmation_text.chars().count()) / 2; print_text_with_coordinates( Text::new(warning_description_text).color_range(0, 17..18 + session_count_len), warning_x_location, warning_y_location, None, None, ); print_text_with_coordinates( Text::new(confirmation_text).color_indices(2, vec![15, 17]), confirmation_x_location, confirmation_y_location, None, None, ); } fn range_to_render( &self, table_rows: usize, results_len: usize, selected_index: Option, ) -> (usize, usize) { if table_rows <= results_len { let row_count_to_render = table_rows.saturating_sub(1); // 1 for the title let first_row_index_to_render = selected_index .unwrap_or(0) .saturating_sub(row_count_to_render / 2); let last_row_index_to_render = first_row_index_to_render + row_count_to_render; (first_row_index_to_render, last_row_index_to_render) } else { let first_row_index_to_render = 0; let last_row_index_to_render = results_len; (first_row_index_to_render, last_row_index_to_render) } } fn render_session_name(&self, session_name: &str, indices: Option>) -> Text { let text = Text::new(&session_name).color_range(0, ..); match indices { Some(indices) => text.color_indices(1, indices), None => text, } } fn render_ctime(&self, ctime: &Duration) -> Text { let duration = format_duration(ctime.clone()).to_string(); let duration_parts = duration.split_whitespace(); let mut formatted_duration = String::new(); for part in duration_parts { if !part.ends_with('s') { if !formatted_duration.is_empty() { formatted_duration.push(' '); } formatted_duration.push_str(part); } } if formatted_duration.is_empty() { formatted_duration.push_str("<1m"); } let duration_len = formatted_duration.chars().count(); Text::new(format!("Created {} ago", formatted_duration)).color_range(2, 8..9 + duration_len) } fn render_more_indication_or_enter_as_needed( &self, i: usize, first_row_index_to_render: usize, last_row_index_to_render: usize, results_len: usize, is_selected: bool, ) -> Text { if is_selected { Text::new(format!(" - Resurrect Session")).color_range(3, 0..7) } else if i == first_row_index_to_render && i > 0 { Text::new(format!("+ {} more", first_row_index_to_render)).color_range(1, ..) } else if i == last_row_index_to_render.saturating_sub(1) && last_row_index_to_render < results_len { Text::new(format!( "+ {} more", results_len.saturating_sub(last_row_index_to_render) )) .color_range(1, ..) } else { Text::new(" ") } } fn render_controls_line(&self, rows: usize) { let controls_line = Text::new(format!( "Help: <↓↑> - Navigate, - Delete Session, - Delete all sessions" )) .color_range(3, 6..10) .color_range(3, 23..29) .color_range(3, 47..56); print_text_with_coordinates(controls_line, 0, rows.saturating_sub(1), None, None); } pub fn move_selection_down(&mut self) { if self.is_searching { if let Some(selected_index) = self.selected_search_index.as_mut() { if *selected_index == self.search_results.len().saturating_sub(1) { *selected_index = 0; } else { *selected_index = *selected_index + 1; } } else { self.selected_search_index = Some(0); } } else { if let Some(selected_index) = self.selected_index.as_mut() { if *selected_index == self.all_resurrectable_sessions.len().saturating_sub(1) { *selected_index = 0; } else { *selected_index = *selected_index + 1; } } else { self.selected_index = Some(0); } } } pub fn move_selection_up(&mut self) { if self.is_searching { if let Some(selected_index) = self.selected_search_index.as_mut() { if *selected_index == 0 { *selected_index = self.search_results.len().saturating_sub(1); } else { *selected_index = selected_index.saturating_sub(1); } } else { self.selected_search_index = Some(self.search_results.len().saturating_sub(1)); } } else { if let Some(selected_index) = self.selected_index.as_mut() { if *selected_index == 0 { *selected_index = self.all_resurrectable_sessions.len().saturating_sub(1); } else { *selected_index = selected_index.saturating_sub(1); } } else { self.selected_index = Some(self.all_resurrectable_sessions.len().saturating_sub(1)); } } } pub fn get_selected_session_name(&self) -> Option { if self.is_searching { self.selected_search_index .and_then(|i| self.search_results.get(i)) .map(|search_result| search_result.session_name.clone()) } else { self.selected_index .and_then(|i| self.all_resurrectable_sessions.get(i)) .map(|session_name_and_creation_time| session_name_and_creation_time.0.clone()) } } pub fn delete_selected_session(&mut self) { self.selected_index .and_then(|i| { if self.all_resurrectable_sessions.len() > i { // optimistic update if i == 0 { self.selected_index = None; } else if i == self.all_resurrectable_sessions.len().saturating_sub(1) { self.selected_index = Some(i.saturating_sub(1)); } Some(self.all_resurrectable_sessions.remove(i)) } else { None } }) .map(|session_name_and_creation_time| { delete_dead_session(&session_name_and_creation_time.0) }); } fn delete_all_sessions(&mut self) { // optimistic update self.all_resurrectable_sessions = vec![]; self.delete_all_dead_sessions_warning = false; delete_all_dead_sessions(); } pub fn show_delete_all_sessions_warning(&mut self) { self.delete_all_dead_sessions_warning = true; } pub fn handle_character(&mut self, character: char) { if self.delete_all_dead_sessions_warning && character == 'y' { self.delete_all_sessions(); } else if self.delete_all_dead_sessions_warning && character == 'n' { self.delete_all_dead_sessions_warning = false; } else { self.search_term.push(character); self.update_search_term(); } } pub fn handle_backspace(&mut self) { self.search_term.pop(); self.update_search_term(); } fn update_search_term(&mut self) { let mut matches = vec![]; let matcher = SkimMatcherV2::default().use_cache(true); for (session_name, ctime) in &self.all_resurrectable_sessions { if let Some((score, indices)) = matcher.fuzzy_indices(&session_name, &self.search_term) { matches.push(SearchResult { session_name: session_name.to_owned(), ctime: ctime.clone(), score, indices, }); } } matches.sort_by(|a, b| b.score.cmp(&a.score)); self.search_results = matches; self.is_searching = !self.search_term.is_empty(); self.selected_search_index = Some(0); } } #[derive(Debug)] pub struct SearchResult { score: i64, indices: Vec, session_name: String, ctime: Duration, }