Skip to main content

gwm/tui/state/
exec_picker.rs

1//! Exec profile picker state (issue #325).
2//!
3//! Pure state — no `App` / `Config` / PTY dependency beyond the sorted
4//! profile-name list handed in at open time. The `App` orchestrator owns
5//! resolving the highlighted profile to an argv (via
6//! [`crate::exec::resolve_exec_command`]); the run loop owns spawning the
7//! PTY overlay with that argv in the selected worktree's directory.
8//!
9//! The picker is deliberately tiny: a sorted list of `[exec.profiles.*]`
10//! names, a wrapping highlight, and the worktree path it was opened for.
11//! `Enter` accepts the highlight, `Esc` cancels — the `j`/`k`/arrow
12//! navigation lives in [`crate::tui::modal_keymap`] under
13//! `KeyContext::ExecPicker`.
14
15use std::path::{Path, PathBuf};
16
17/// Pure state for the exec profile picker. `Default` is an empty, closed
18/// picker (no profiles, no target, highlight at 0).
19#[derive(Debug, Default)]
20pub struct ExecPicker {
21  /// Exec profile names from `[exec.profiles.*]`, in the sorted order the
22  /// orchestrator hands them over (a `BTreeMap` key walk).
23  profiles: Vec<String>,
24  /// Highlighted row index. Always a valid index into `profiles` while
25  /// non-empty; the `next`/`prev` wrappers keep it in range.
26  selected: usize,
27  /// The worktree directory the picker was opened for. Captured at open so
28  /// `Enter` runs the profile in *that* worktree even if an auto-refresh
29  /// drifts the live selection while the overlay sits open (Codex #333).
30  cwd: Option<PathBuf>,
31}
32
33impl ExecPicker {
34  pub fn new() -> Self {
35    Self::default()
36  }
37
38  /// (Re)populate the picker from a list of profile names + the worktree it
39  /// targets, resetting the highlight to the top. Called by the
40  /// orchestrator's `enter_exec_picker` each time the overlay opens.
41  pub fn open(&mut self, profiles: Vec<String>, cwd: PathBuf) {
42    self.profiles = profiles;
43    self.selected = 0;
44    self.cwd = Some(cwd);
45  }
46
47  /// The worktree directory the picker was opened for, if any.
48  pub fn cwd(&self) -> Option<&Path> {
49    self.cwd.as_deref()
50  }
51
52  /// The profile names, in display order.
53  pub fn profiles(&self) -> &[String] {
54    &self.profiles
55  }
56
57  /// `true` when no profiles are loaded (the orchestrator refuses to open
58  /// the overlay in this state, but the picker stays well-defined).
59  pub fn is_empty(&self) -> bool {
60    self.profiles.is_empty()
61  }
62
63  /// The highlighted row index (clamped to `profiles`).
64  pub fn selected_index(&self) -> usize {
65    self.selected
66  }
67
68  /// The highlighted profile name, or `None` when the picker is empty.
69  pub fn selected_profile(&self) -> Option<&str> {
70    self.profiles.get(self.selected).map(String::as_str)
71  }
72
73  /// Move the highlight down one row, wrapping to the top. No-op when
74  /// empty.
75  pub fn next(&mut self) {
76    if self.profiles.is_empty() {
77      return;
78    }
79    self.selected = (self.selected + 1) % self.profiles.len();
80  }
81
82  /// Move the highlight up one row, wrapping to the bottom. No-op when
83  /// empty.
84  pub fn prev(&mut self) {
85    if self.profiles.is_empty() {
86      return;
87    }
88    self.selected = (self.selected + self.profiles.len() - 1) % self.profiles.len();
89  }
90}