aboutsummaryrefslogtreecommitdiff
path: root/src/app/state.rs
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-31 21:18:40 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-31 21:18:40 +0300
commit8cf0e9b35b00f9dd91270fcd3931015d61ffd8a0 (patch)
tree2b76b158344fdbe0de17c6abe98564a262266d2f /src/app/state.rs
parent859185b491c5a80e888ba8f9dd213d2c35baa034 (diff)
fix: confirm implicit worktree group close
refs #369
Diffstat (limited to 'src/app/state.rs')
-rw-r--r--src/app/state.rs98
1 files changed, 98 insertions, 0 deletions
diff --git a/src/app/state.rs b/src/app/state.rs
index a9c1b8a3..9466fcce 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -593,6 +593,48 @@ pub struct WorktreeOpenEntry {
pub already_open_ws_idx: Option<usize>,
}
+impl WorktreeOpenEntry {
+ pub(crate) fn display_name(&self) -> String {
+ self.branch.clone().unwrap_or_else(|| {
+ self.path
+ .file_name()
+ .and_then(|name| name.to_str())
+ .map(str::to_owned)
+ .unwrap_or_else(|| self.path.display().to_string())
+ })
+ }
+
+ pub(crate) fn status_label(&self) -> &'static str {
+ if self.already_open_ws_idx.is_some() {
+ "open"
+ } else if self.branch.is_some() {
+ ""
+ } else if self.is_linked_worktree {
+ "detached"
+ } else {
+ "root"
+ }
+ }
+
+ fn search_text(&self) -> String {
+ format!(
+ "{} {} {} {}",
+ self.display_name(),
+ self.path
+ .file_name()
+ .and_then(|name| name.to_str())
+ .unwrap_or_default(),
+ self.path.display(),
+ self.status_label()
+ )
+ .to_lowercase()
+ }
+
+ fn matches_query(&self, query: &str) -> bool {
+ text_matches_query(query, &self.search_text())
+ }
+}
+
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorktreeOpenState {
pub source_workspace_id: String,
@@ -603,9 +645,65 @@ pub struct WorktreeOpenState {
pub repo_name: String,
pub entries: Vec<WorktreeOpenEntry>,
pub selected: usize,
+ pub query: String,
+ pub search_focused: bool,
pub error: Option<String>,
}
+impl WorktreeOpenState {
+ pub(crate) fn filtered_indices(&self) -> Vec<usize> {
+ let query = self.query.trim();
+ self.entries
+ .iter()
+ .enumerate()
+ .filter_map(|(idx, entry)| {
+ (query.is_empty() || entry.matches_query(query)).then_some(idx)
+ })
+ .collect()
+ }
+
+ pub(crate) fn selected_entry_index(&self) -> Option<usize> {
+ let indices = self.filtered_indices();
+ if indices.contains(&self.selected) {
+ Some(self.selected)
+ } else {
+ indices.first().copied()
+ }
+ }
+
+ pub(crate) fn normalize_selection(&mut self) {
+ if let Some(selected) = self.selected_entry_index() {
+ self.selected = selected;
+ }
+ }
+
+ pub(crate) fn select_previous_filtered(&mut self) {
+ let indices = self.filtered_indices();
+ let Some(current) = self.selected_entry_index() else {
+ return;
+ };
+ let pos = indices.iter().position(|idx| *idx == current).unwrap_or(0);
+ self.selected = indices[pos.saturating_sub(1)];
+ }
+
+ pub(crate) fn select_next_filtered(&mut self) {
+ let indices = self.filtered_indices();
+ let Some(current) = self.selected_entry_index() else {
+ return;
+ };
+ let pos = indices.iter().position(|idx| *idx == current).unwrap_or(0);
+ self.selected = indices[(pos + 1).min(indices.len().saturating_sub(1))];
+ }
+}
+
+pub(crate) fn text_matches_query(query: &str, text: &str) -> bool {
+ let haystack = text.to_lowercase();
+ query
+ .to_lowercase()
+ .split_whitespace()
+ .all(|needle| haystack.contains(needle))
+}
+
/// Computed view geometry — derived from AppState + terminal size.
/// Updated before each render, consumed by render and mouse handling.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]