diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-31 21:18:40 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-31 21:18:40 +0300 |
| commit | 8cf0e9b35b00f9dd91270fcd3931015d61ffd8a0 (patch) | |
| tree | 2b76b158344fdbe0de17c6abe98564a262266d2f /src/ui/dialogs.rs | |
| parent | 859185b491c5a80e888ba8f9dd213d2c35baa034 (diff) | |
fix: confirm implicit worktree group close
refs #369
Diffstat (limited to 'src/ui/dialogs.rs')
| -rw-r--r-- | src/ui/dialogs.rs | 174 |
1 files changed, 133 insertions, 41 deletions
diff --git a/src/ui/dialogs.rs b/src/ui/dialogs.rs index f7396fd8..31275282 100644 --- a/src/ui/dialogs.rs +++ b/src/ui/dialogs.rs @@ -10,7 +10,7 @@ use super::widgets::{ action_button_row_rects, centered_popup_rect, panel_contrast_fg, render_action_button, render_modal_header, render_modal_shell, render_panel_shell, ActionButtonSpec, }; -use crate::app::{AppState, Mode}; +use crate::app::{state::WorktreeOpenState, AppState, Mode}; fn truncate_text(text: &str, max_width: usize) -> String { let len = text.chars().count(); @@ -184,8 +184,11 @@ pub(crate) fn remove_worktree_button_rects(inner: Rect, force_confirmation: bool } pub(crate) fn open_existing_worktree_inner_rect(area: Rect, entry_count: usize) -> Option<Rect> { - let height = (entry_count as u16).saturating_add(5).clamp(8, 18); - centered_popup_rect(area, 78, height).map(|popup| { + let height = (entry_count as u16) + .saturating_mul(2) + .saturating_add(7) + .clamp(12, 26); + centered_popup_rect(area, 96, height).map(|popup| { Rect::new( popup.x + 1, popup.y + 1, @@ -195,8 +198,21 @@ pub(crate) fn open_existing_worktree_inner_rect(area: Rect, entry_count: usize) }) } -pub(crate) fn open_existing_worktree_visible_start(selected: usize, max_rows: usize) -> usize { - selected.saturating_sub(max_rows.saturating_sub(1)) +pub(crate) fn open_existing_worktree_max_visible_rows(inner: Rect) -> usize { + usize::from(inner.height.saturating_sub(5) / 2) +} + +pub(crate) fn open_existing_worktree_visible_start( + open: &WorktreeOpenState, + max_rows: usize, +) -> usize { + let filtered = open.filtered_indices(); + let selected = open.selected_entry_index().unwrap_or(open.selected); + let selected_pos = filtered + .iter() + .position(|idx| *idx == selected) + .unwrap_or(0); + selected_pos.saturating_sub(max_rows.saturating_sub(1)) } pub(crate) fn open_existing_worktree_button_rects(inner: Rect) -> (Rect, Rect) { @@ -408,11 +424,14 @@ pub(super) fn render_open_existing_worktree_overlay(app: &AppState, frame: &mut }; super::dim_background(frame, area); - let height = (open.entries.len() as u16).saturating_add(5).clamp(8, 18); - let Some(inner) = render_modal_shell(frame, area, 78, height, &app.palette) else { + let height = (open.entries.len() as u16) + .saturating_mul(2) + .saturating_add(7) + .clamp(12, 26); + let Some(inner) = render_modal_shell(frame, area, 96, height, &app.palette) else { return; }; - if inner.height < 6 { + if inner.height < 8 { return; } @@ -422,39 +441,29 @@ pub(super) fn render_open_existing_worktree_overlay(app: &AppState, frame: &mut "open worktree", &app.palette, ); + render_open_worktree_search( + app, + frame, + Rect::new(inner.x, inner.y + 1, inner.width, 1), + open, + ); frame.render_widget( - Paragraph::new(" existing checkouts").style(Style::default().fg(app.palette.overlay0)), - Rect::new(inner.x, inner.y.saturating_add(1), inner.width, 1), + Paragraph::new("─".repeat(inner.width as usize)) + .style(Style::default().fg(app.palette.surface1)), + Rect::new(inner.x, inner.y.saturating_add(2), inner.width, 1), ); - let max_rows = inner.height.saturating_sub(4) as usize; - let start = open_existing_worktree_visible_start(open.selected, max_rows); - for (visible_idx, (entry_idx, entry)) in open - .entries - .iter() - .enumerate() - .skip(start) - .take(max_rows) - .enumerate() - { - let selected = entry_idx == open.selected; - let y = inner.y.saturating_add(2 + visible_idx as u16); - let marker = if selected { "›" } else { " " }; - let branch = entry - .branch - .as_deref() - .unwrap_or(if entry.is_linked_worktree { - "detached" - } else { - "root" - }); - let open_label = if entry.already_open_ws_idx.is_some() { - " open" - } else { - "" + let filtered = open.filtered_indices(); + let max_rows = open_existing_worktree_max_visible_rows(inner); + let start = open_existing_worktree_visible_start(open, max_rows); + for (visible_idx, entry_idx) in filtered.iter().skip(start).take(max_rows).enumerate() { + let Some(entry) = open.entries.get(*entry_idx) else { + continue; }; - let label = format!("{marker} {branch}{open_label} {}", entry.path.display()); - let style = if selected { + let selected = Some(*entry_idx) == open.selected_entry_index(); + let y = inner.y.saturating_add(3 + (visible_idx as u16 * 2)); + let marker = if selected { "›" } else { " " }; + let row_style = if selected { Style::default() .fg(app.palette.text) .bg(app.palette.surface0) @@ -462,10 +471,51 @@ pub(super) fn render_open_existing_worktree_overlay(app: &AppState, frame: &mut } else { Style::default().fg(app.palette.subtext0) }; + let path_style = if selected { + Style::default() + .fg(app.palette.subtext0) + .bg(app.palette.surface0) + } else { + Style::default().fg(app.palette.overlay0) + }; + let status = entry.status_label(); + let title_width = inner + .width + .saturating_sub(status.len() as u16) + .saturating_sub(4) as usize; + let mut title = format!( + "{marker} {}", + truncate_text(&entry.display_name(), title_width) + ); + if !status.is_empty() { + let pad = inner + .width + .saturating_sub(title.chars().count() as u16) + .saturating_sub(status.len() as u16) + .max(1); + title.push_str(&" ".repeat(pad as usize)); + title.push_str(status); + } frame.render_widget( - Paragraph::new(truncate_text(&label, inner.width as usize)).style(style), + Paragraph::new(truncate_text(&title, inner.width as usize)).style(row_style), Rect::new(inner.x, y, inner.width, 1), ); + frame.render_widget( + Paragraph::new(truncate_text( + &format!(" {}", entry.path.display()), + inner.width as usize, + )) + .style(path_style), + Rect::new(inner.x, y.saturating_add(1), inner.width, 1), + ); + } + + if filtered.is_empty() { + frame.render_widget( + Paragraph::new(" no matching worktrees") + .style(Style::default().fg(app.palette.overlay0)), + Rect::new(inner.x, inner.y.saturating_add(3), inner.width, 1), + ); } if let Some(error) = &open.error { @@ -503,6 +553,47 @@ pub(super) fn render_open_existing_worktree_overlay(app: &AppState, frame: &mut ); } +fn render_open_worktree_search( + app: &AppState, + frame: &mut Frame, + area: Rect, + open: &WorktreeOpenState, +) { + let focus_style = if open.search_focused { + Style::default() + .fg(app.palette.accent) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(app.palette.overlay0) + }; + let filtered_count = open.filtered_indices().len(); + let count = if open.query.trim().is_empty() { + format!("{} checkouts", open.entries.len()) + } else { + format!("{filtered_count}/{} checkouts", open.entries.len()) + }; + let mut spans = vec![Span::styled(" / ", focus_style)]; + if open.query.trim().is_empty() { + spans.push(Span::styled( + "filter worktrees", + Style::default().fg(app.palette.overlay0), + )); + } else { + spans.push(Span::styled( + open.query.clone(), + Style::default().fg(app.palette.text), + )); + } + spans.push(Span::styled( + format!( + "{count:>width$}", + width = area.width.saturating_sub(18) as usize + ), + Style::default().fg(app.palette.overlay0), + )); + frame.render_widget(Paragraph::new(Line::from(spans)), area); +} + fn confirm_close_overlay_text(app: &AppState) -> (String, String) { let ws_name = app .workspaces @@ -608,8 +699,9 @@ pub(super) fn render_confirm_close_overlay(app: &AppState, frame: &mut Frame, ar Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), + Constraint::Length(1), ]) - .areas::<3>(inner); + .areas::<4>(inner); frame.render_widget(Paragraph::new(title_line), rows[0]); frame.render_widget(Paragraph::new(detail_line), rows[1]); @@ -639,7 +731,7 @@ pub(super) fn render_confirm_close_overlay(app: &AppState, frame: &mut Frame, ar } pub(crate) fn confirm_close_popup_rect(area: Rect) -> Option<Rect> { - centered_popup_rect(area, 44, 5) + centered_popup_rect(area, 64, 6) } pub(crate) fn confirm_close_button_rects(inner: Rect) -> (Rect, Rect) { @@ -656,7 +748,7 @@ pub(crate) fn confirm_close_button_rects(inner: Rect) -> (Rect, Rect) { }, ], 2, - 2, + 3, ); (rects[0], rects[1]) } |
