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 PlanReviewOutput {
69 Outcome(ReviewOutcome),
70 SetTheme(String),
71}
72
73#[derive(Debug)]
74pub enum RootOutput {
75 Session(SessionPickerOutput),
76 Workspace(WorkspacePickerOutput),
77 Settings(SettingsOutput),
78 Elicitation(ElicitationOutput),
79 GitReview(GitReviewOutput),
80 PlanReview(PlanReviewOutput),
81}
82
83pub enum UiEvent {
84 Key(KeyEvent),
85 Paste(String),
86 Mouse(MouseAction, (u16, u16)),
87}
88
89#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub enum MouseAction {
91 ScrollUp,
92 ScrollDown,
93 Click,
94}
95
96impl MouseAction {
97 pub fn from_event(kind: crossterm::event::MouseEventKind) -> Option<Self> {
98 use crossterm::event::MouseEventKind;
99 match kind {
100 MouseEventKind::ScrollUp => Some(Self::ScrollUp),
101 MouseEventKind::ScrollDown => Some(Self::ScrollDown),
102 MouseEventKind::Down(_) => Some(Self::Click),
103 _ => None,
104 }
105 }
106
107 pub fn direction(self) -> Option<Direction> {
109 match self {
110 Self::ScrollUp => Some(Direction::Backward),
111 Self::ScrollDown => Some(Direction::Forward),
112 Self::Click => None,
113 }
114 }
115}
116
117#[derive(Clone, Copy, Debug, Eq, PartialEq)]
119pub(crate) enum Nav {
120 Close,
122 Activate,
124 Clicked,
126 Moved,
128 Unhandled,
130}
131
132impl<T> FilterableList<T> {
133 pub(crate) fn on_nav_event(&mut self, event: &UiEvent) -> Nav {
138 match event {
139 UiEvent::Key(key) if is_press(*key) => self.on_nav_key(*key),
140 UiEvent::Key(_) | UiEvent::Paste(_) => Nav::Unhandled,
141 UiEvent::Mouse(action, (_, row)) => match action.direction() {
142 Some(direction) => {
143 self.step(direction);
144 Nav::Moved
145 }
146 None if self.select_at(*row) => Nav::Clicked,
147 None => Nav::Unhandled,
148 },
149 }
150 }
151
152 fn on_nav_key(&mut self, key: KeyEvent) -> Nav {
153 match key.code {
154 KeyCode::Esc => Nav::Close,
155 KeyCode::Enter => Nav::Activate,
156 KeyCode::Up => {
157 self.step(Direction::Backward);
158 Nav::Moved
159 }
160 KeyCode::Down => {
161 self.step(Direction::Forward);
162 Nav::Moved
163 }
164 KeyCode::Backspace => {
165 self.pop_query_char();
166 Nav::Moved
167 }
168 KeyCode::Char(c) if !c.is_control() && !is_composed_char(key) => {
169 self.push_query_char(c);
170 Nav::Moved
171 }
172 _ => Nav::Unhandled,
173 }
174 }
175}
176
177pub(crate) fn one<T>(action: Option<T>) -> Vec<T> {
178 action.into_iter().collect()
179}