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