1use crate::command::{GitCommand, GitWatchCommand};
2use crate::view::filterable_list::FilterableList;
3use crate::view::selection::Direction;
4use acp_utils::notifications::WorkspaceMoveTarget;
5use agent_client_protocol::schema::v2::SessionId;
6use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
7use std::path::PathBuf;
8
9const COMPOSED_MODIFIERS: KeyModifiers = KeyModifiers::CONTROL
10 .union(KeyModifiers::ALT)
11 .union(KeyModifiers::SUPER)
12 .union(KeyModifiers::HYPER)
13 .union(KeyModifiers::META);
14
15pub(crate) fn is_composed_char(key: KeyEvent) -> bool {
16 matches!(key.code, KeyCode::Char(_)) && key.modifiers.intersects(COMPOSED_MODIFIERS)
17}
18
19pub(crate) fn is_press(key: KeyEvent) -> bool {
20 matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat)
21}
22
23#[derive(Debug)]
24pub enum SessionPickerOutput {
25 Close,
26 Resume { session_id: SessionId, cwd: PathBuf },
27 Preview(String),
28}
29
30#[derive(Debug)]
31pub enum WorkspacePickerOutput {
32 Close,
33 Move { target: WorkspaceMoveTarget },
34}
35
36#[derive(Debug)]
37pub enum SettingsOutput {
38 Close,
39 SetConfigOption { config_id: String, value: String },
40 SetTheme(String),
41 AuthenticateServer(String),
42 AuthenticateProvider(String),
43}
44
45#[derive(Debug)]
46pub enum ElicitationOutput {
47 Close,
48}
49
50#[derive(Debug)]
54pub enum ReviewOutcome {
55 Cancelled,
56 Submitted(String),
57}
58
59#[derive(Debug)]
60pub enum GitReviewOutput {
61 SetTheme(String),
62 Outcome(ReviewOutcome),
63 Task(GitCommand),
64 Watch(GitWatchCommand),
65}
66
67#[derive(Debug)]
68pub enum ArtifactReviewOutput {
69 Approved,
70 Outcome(ReviewOutcome),
71 SetTheme(String),
72}
73
74#[derive(Debug)]
75pub enum RootOutput {
76 Session(SessionPickerOutput),
77 Workspace(WorkspacePickerOutput),
78 Settings(SettingsOutput),
79 Elicitation(ElicitationOutput),
80 GitReview(GitReviewOutput),
81 ArtifactReview(ArtifactReviewOutput),
82}
83
84pub enum UiEvent {
85 Key(KeyEvent),
86 Paste(String),
87 Mouse(MouseAction, (u16, u16)),
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91pub enum MouseAction {
92 ScrollUp,
93 ScrollDown,
94 Click,
95}
96
97impl MouseAction {
98 pub fn from_event(kind: crossterm::event::MouseEventKind) -> Option<Self> {
99 use crossterm::event::MouseEventKind;
100 match kind {
101 MouseEventKind::ScrollUp => Some(Self::ScrollUp),
102 MouseEventKind::ScrollDown => Some(Self::ScrollDown),
103 MouseEventKind::Down(_) => Some(Self::Click),
104 _ => None,
105 }
106 }
107
108 pub fn direction(self) -> Option<Direction> {
110 match self {
111 Self::ScrollUp => Some(Direction::Backward),
112 Self::ScrollDown => Some(Direction::Forward),
113 Self::Click => None,
114 }
115 }
116}
117
118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120pub(crate) enum Nav {
121 Close,
123 Activate,
125 Clicked,
127 Moved,
129 Unhandled,
131}
132
133impl<T> FilterableList<T> {
134 pub(crate) fn on_nav_event(&mut self, event: &UiEvent) -> Nav {
139 match event {
140 UiEvent::Key(key) if is_press(*key) => self.on_nav_key(*key),
141 UiEvent::Key(_) | UiEvent::Paste(_) => Nav::Unhandled,
142 UiEvent::Mouse(action, (_, row)) => match action.direction() {
143 Some(direction) => {
144 self.step(direction);
145 Nav::Moved
146 }
147 None if self.select_at(*row) => Nav::Clicked,
148 None => Nav::Unhandled,
149 },
150 }
151 }
152
153 fn on_nav_key(&mut self, key: KeyEvent) -> Nav {
154 match key.code {
155 KeyCode::Esc => Nav::Close,
156 KeyCode::Enter => Nav::Activate,
157 KeyCode::Up => {
158 self.step(Direction::Backward);
159 Nav::Moved
160 }
161 KeyCode::Down => {
162 self.step(Direction::Forward);
163 Nav::Moved
164 }
165 KeyCode::Backspace => {
166 self.pop_query_char();
167 Nav::Moved
168 }
169 KeyCode::Char(c) if !c.is_control() && !is_composed_char(key) => {
170 self.push_query_char(c);
171 Nav::Moved
172 }
173 _ => Nav::Unhandled,
174 }
175 }
176}
177
178pub(crate) fn one<T>(action: Option<T>) -> Vec<T> {
179 action.into_iter().collect()
180}