summaryrefslogtreecommitdiff
path: root/src/client/shell/worktree_overlays.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-01 15:48:22 +0300
committerGitHub <noreply@github.com>2026-09-01 15:48:22 +0300
commit207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch)
treed7b037ecdf629224a048c841d11659efdc23507d /src/client/shell/worktree_overlays.rs
parent8a6d697308162874a236c84aac0b0f8c7ac01f83 (diff)
refactor: render the shell in the client (#3487)
* feat: add opt-in client-rendered shell * feat: add client-owned shell mouse controls * feat: complete client-owned shell chrome * feat: add client-owned shell settings and notifications * feat: run client shell custom commands * feat: render popup terminals in client shell * feat: add client-owned scrollback and copy mode * fix: preserve client worktree trust defaults * feat: render startup diagnostics in client shell * feat: render onboarding in client shell * feat: render product announcements in client shell * feat: render release notes in client shell * feat: render mobile switcher in client shell * feat: render kitty graphics in client shell * fix: preserve client shell lifecycle coherence * refactor: route client shell commands through endpoint connections * refactor: make the client-rendered shell the default * refactor: remove monolithic launch mode * refactor: complete client-rendered shell cutover * perf: retain client pane surface updates * perf: scale retained pane surface updates * perf: remove remote pane input latency * test: make client shell checks platform-independent * fix: prevent client timer starvation * test: fix client shell platform gates * test: accept retained surfaces in cross-area checks * test: make client mode assertions platform-neutral * fix: badge only outdated integrations
Diffstat (limited to 'src/client/shell/worktree_overlays.rs')
-rw-r--r--src/client/shell/worktree_overlays.rs410
1 files changed, 410 insertions, 0 deletions
diff --git a/src/client/shell/worktree_overlays.rs b/src/client/shell/worktree_overlays.rs
new file mode 100644
index 00000000..a2b5c65f
--- /dev/null
+++ b/src/client/shell/worktree_overlays.rs
@@ -0,0 +1,410 @@
+use super::*;
+
+pub(super) fn render_worktree_create_overlay(
+ b: &mut Buffer,
+ create: &ClientWorktreeCreateOverlay,
+ p: &Palette,
+) -> Option<OverlayRender> {
+ let popup = popup(b.area, 68, 12)?;
+ let inner = panel(b, popup, p.accent, p.panel_bg)?;
+ put_text(
+ b,
+ inner.x,
+ inner.y,
+ inner.width,
+ "new worktree",
+ Style::default()
+ .fg(p.text)
+ .bg(p.panel_bg)
+ .add_modifier(Modifier::BOLD),
+ );
+ put_text(
+ b,
+ inner.x,
+ inner.y + 2,
+ inner.width,
+ " branch",
+ Style::default().fg(p.overlay0).bg(p.panel_bg),
+ );
+ let input = Rect::new(inner.x, inner.y + 3, inner.width, 1);
+ b.set_style(input, Style::default().fg(p.text).bg(p.surface0));
+ put_text(
+ b,
+ input.x,
+ input.y,
+ input.width,
+ &format!(" {}", create.branch),
+ Style::default().fg(p.text).bg(p.surface0),
+ );
+ put_text(
+ b,
+ inner.x,
+ inner.y + 5,
+ inner.width,
+ " checkout",
+ Style::default().fg(p.overlay0).bg(p.panel_bg),
+ );
+ put_text(
+ b,
+ inner.x,
+ inner.y + 6,
+ inner.width,
+ &format!(" {}", create.checkout_path),
+ Style::default().fg(p.subtext0).bg(p.panel_bg),
+ );
+ if create.creating {
+ put_text(
+ b,
+ inner.x,
+ inner.y + 8,
+ inner.width,
+ " creating…",
+ Style::default().fg(p.accent).bg(p.panel_bg),
+ );
+ } else if let Some(error) = create.error.as_deref() {
+ put_text(
+ b,
+ inner.x,
+ inner.y + 8,
+ inner.width,
+ &format!(" {error}"),
+ Style::default().fg(p.red).bg(p.panel_bg),
+ );
+ }
+ let buttons = row(inner, &[20, 12], 2, 9);
+ let [primary, cancel] = buttons.as_slice() else {
+ return None;
+ };
+ button(
+ b,
+ *primary,
+ " ↵ create and open ",
+ Style::default()
+ .fg(contrast(p))
+ .bg(p.accent)
+ .add_modifier(Modifier::BOLD),
+ );
+ button(
+ b,
+ *cancel,
+ " esc cancel ",
+ Style::default()
+ .fg(p.text)
+ .bg(p.surface0)
+ .add_modifier(Modifier::BOLD),
+ );
+ Some(OverlayRender {
+ primary: *primary,
+ clear: Rect::default(),
+ cancel: *cancel,
+ navigator_popup: Rect::default(),
+ navigator_search: Rect::default(),
+ navigator_rows: Vec::new(),
+ worktree_search: Rect::default(),
+ worktree_rows: Vec::new(),
+ cursor: (!create.creating).then(|| crate::protocol::CursorState {
+ x: (input.x + 1 + display_width(&create.branch)).min(input.right() - 1),
+ y: input.y,
+ visible: true,
+ shape: 0,
+ }),
+ ..OverlayRender::default()
+ })
+}
+
+pub(super) fn render_worktree_open_overlay(
+ b: &mut Buffer,
+ open: &ClientWorktreeOpenOverlay,
+ p: &Palette,
+) -> Option<OverlayRender> {
+ let popup_height = (open.entries.len().saturating_mul(2) + 7).clamp(12, 26) as u16;
+ let popup = popup(b.area, 96, popup_height)?;
+ let inner = panel(b, popup, p.accent, p.panel_bg)?;
+ put_text(
+ b,
+ inner.x,
+ inner.y,
+ inner.width,
+ "open worktree",
+ Style::default()
+ .fg(p.text)
+ .bg(p.panel_bg)
+ .add_modifier(Modifier::BOLD),
+ );
+ let search = Rect::new(inner.x, inner.y + 1, inner.width, 1);
+ let filtered = open.filtered_indices();
+ put_text(
+ b,
+ search.x,
+ search.y,
+ search.width,
+ &if open.search_focused || !open.query.is_empty() {
+ format!(" / {}", open.query)
+ } else {
+ " / filter worktrees".to_owned()
+ },
+ Style::default()
+ .fg(if open.search_focused {
+ p.text
+ } else {
+ p.overlay0
+ })
+ .bg(p.panel_bg),
+ );
+ let count = if filtered.len() == open.entries.len() {
+ format!("{} checkouts", open.entries.len())
+ } else {
+ format!("{}/{} checkouts", filtered.len(), open.entries.len())
+ };
+ put_right_text(
+ b,
+ search,
+ search.y,
+ &count,
+ Style::default().fg(p.overlay0).bg(p.panel_bg),
+ );
+ put_text(
+ b,
+ inner.x,
+ inner.y + 2,
+ inner.width,
+ &"─".repeat(inner.width as usize),
+ Style::default().fg(p.surface1).bg(p.panel_bg),
+ );
+ let body = Rect::new(
+ inner.x,
+ inner.y + 3,
+ inner.width,
+ inner.height.saturating_sub(6),
+ );
+ let visible_count = (body.height / 2).max(1) as usize;
+ let selected_position = filtered
+ .iter()
+ .position(|index| *index == open.selected)
+ .unwrap_or(0);
+ let start = selected_position
+ .saturating_sub(visible_count.saturating_sub(1))
+ .min(filtered.len().saturating_sub(visible_count));
+ let mut row_hits = Vec::new();
+ for (visible, entry_index) in filtered
+ .iter()
+ .copied()
+ .skip(start)
+ .take(visible_count)
+ .enumerate()
+ {
+ let entry = &open.entries[entry_index];
+ let rect = Rect::new(body.x, body.y + visible as u16 * 2, body.width, 2);
+ row_hits.push((rect, entry_index));
+ let selected = entry_index == open.selected;
+ let style = if selected {
+ Style::default().fg(contrast(p)).bg(p.accent)
+ } else {
+ Style::default().fg(p.text).bg(p.panel_bg)
+ };
+ b.set_style(rect, style);
+ put_text(
+ b,
+ rect.x,
+ rect.y,
+ rect.width,
+ &format!(" {}", entry.label),
+ style.add_modifier(Modifier::BOLD),
+ );
+ let status = entry.status_label();
+ if !status.is_empty() {
+ put_right_text(b, rect, rect.y, status, style);
+ }
+ put_text(
+ b,
+ rect.x,
+ rect.y + 1,
+ rect.width,
+ &format!(" {}", entry.path),
+ if selected {
+ style
+ } else {
+ Style::default().fg(p.overlay0).bg(p.panel_bg)
+ },
+ );
+ }
+ if filtered.is_empty() {
+ put_text(
+ b,
+ body.x,
+ body.y,
+ body.width,
+ " no matching worktrees",
+ Style::default().fg(p.overlay0).bg(p.panel_bg),
+ );
+ }
+ if open.opening {
+ put_text(
+ b,
+ inner.x,
+ inner.bottom() - 3,
+ inner.width,
+ " opening…",
+ Style::default().fg(p.accent).bg(p.panel_bg),
+ );
+ } else if let Some(error) = open.error.as_deref() {
+ put_text(
+ b,
+ inner.x,
+ inner.bottom() - 3,
+ inner.width,
+ &format!(" {error}"),
+ Style::default().fg(p.red).bg(p.panel_bg),
+ );
+ }
+ let buttons = row(inner, &[10, 12], 2, inner.height.saturating_sub(1));
+ let [primary, cancel] = buttons.as_slice() else {
+ return None;
+ };
+ button(
+ b,
+ *primary,
+ " ↵ open ",
+ Style::default()
+ .fg(contrast(p))
+ .bg(p.accent)
+ .add_modifier(Modifier::BOLD),
+ );
+ button(
+ b,
+ *cancel,
+ " esc cancel ",
+ Style::default()
+ .fg(p.text)
+ .bg(p.surface0)
+ .add_modifier(Modifier::BOLD),
+ );
+ Some(OverlayRender {
+ primary: *primary,
+ clear: Rect::default(),
+ cancel: *cancel,
+ navigator_popup: Rect::default(),
+ navigator_search: Rect::default(),
+ navigator_rows: Vec::new(),
+ worktree_search: search,
+ worktree_rows: row_hits,
+ cursor: (open.search_focused && !open.opening).then(|| crate::protocol::CursorState {
+ x: (search.x + 3 + display_width(&open.query)).min(search.right() - 1),
+ y: search.y,
+ visible: true,
+ shape: 0,
+ }),
+ ..OverlayRender::default()
+ })
+}
+
+pub(super) fn render_worktree_remove_overlay(
+ b: &mut Buffer,
+ remove: &ClientWorktreeRemoveOverlay,
+ p: &Palette,
+) -> Option<OverlayRender> {
+ let popup = popup(b.area, 72, 10)?;
+ let inner = panel(b, popup, p.red, p.panel_bg)?;
+ put_text(
+ b,
+ inner.x,
+ inner.y,
+ inner.width,
+ " delete worktree checkout?",
+ Style::default()
+ .fg(p.red)
+ .bg(p.panel_bg)
+ .add_modifier(Modifier::BOLD),
+ );
+ put_text(
+ b,
+ inner.x,
+ inner.y + 1,
+ inner.width,
+ " This removes the checkout folder:",
+ Style::default().fg(p.text).bg(p.panel_bg),
+ );
+ put_text(
+ b,
+ inner.x,
+ inner.y + 2,
+ inner.width,
+ &format!(" {}", remove.path),
+ Style::default().fg(p.subtext0).bg(p.panel_bg),
+ );
+ put_text(
+ b,
+ inner.x,
+ inner.y + 3,
+ inner.width,
+ " The branch is not deleted. The Herdr workspace will close.",
+ Style::default().fg(p.text).bg(p.panel_bg),
+ );
+ if remove.force_confirmation {
+ put_text(
+ b,
+ inner.x,
+ inner.y + 4,
+ inner.width,
+ " Dirty or untracked files will be permanently deleted.",
+ Style::default().fg(p.red).bg(p.panel_bg),
+ );
+ }
+ if remove.removing {
+ put_text(
+ b,
+ inner.x,
+ inner.y + 5,
+ inner.width,
+ " removing…",
+ Style::default().fg(p.accent).bg(p.panel_bg),
+ );
+ } else if let Some(error) = remove.error.as_deref() {
+ put_text(
+ b,
+ inner.x,
+ inner.y + 5,
+ inner.width,
+ &format!(" {error}"),
+ Style::default().fg(p.red).bg(p.panel_bg),
+ );
+ }
+ let buttons = row(inner, &[18, 12], 2, 7);
+ let [primary, cancel] = buttons.as_slice() else {
+ return None;
+ };
+ button(
+ b,
+ *primary,
+ if remove.force_confirmation {
+ " ↵ delete anyway "
+ } else {
+ " ↵ remove "
+ },
+ Style::default()
+ .fg(contrast(p))
+ .bg(p.red)
+ .add_modifier(Modifier::BOLD),
+ );
+ button(
+ b,
+ *cancel,
+ " esc cancel ",
+ Style::default()
+ .fg(p.text)
+ .bg(p.surface0)
+ .add_modifier(Modifier::BOLD),
+ );
+ Some(OverlayRender {
+ primary: *primary,
+ clear: Rect::default(),
+ cancel: *cancel,
+ navigator_popup: Rect::default(),
+ navigator_search: Rect::default(),
+ navigator_rows: Vec::new(),
+ worktree_search: Rect::default(),
+ worktree_rows: Vec::new(),
+ cursor: None,
+ ..OverlayRender::default()
+ })
+}