clankerdiff_ratatui/
input.rs1use crate::{
2 DiffReviewCommand, DiffReviewState, FocusPane, InputOutcome, InteractionPhase, KeyCode,
3 KeyEvent, MouseEvent, MouseEventKind, ReviewCommand, ReviewInput,
4 interaction::{self, ReviewWidget},
5 state::{RepositoryOperationStatus, RepositoryPrompt},
6};
7use clankerdiff_core::{CommentDraft, DiffReviewEvent, RepositoryAction};
8#[cfg(feature = "crossterm-backend")]
9use crossterm::event::Event;
10use ratatui::layout::Position;
11use std::convert::Infallible;
12
13#[cfg(feature = "crossterm-backend")]
14#[must_use]
15pub fn handle_crossterm_event(
16 state: &mut DiffReviewState,
17 event: Event,
18) -> InputOutcome<DiffReviewEvent> {
19 let Ok(outcome) = crate::crossterm_adapter::handle_event(state, event);
20 outcome
21}
22
23impl DiffReviewState {
24 #[must_use]
25 pub fn interaction_phase(&self) -> InteractionPhase {
26 if self.theme_picker.is_some() {
27 InteractionPhase::ThemePicker
28 } else if self.repository_prompt.is_some() {
29 InteractionPhase::RepositoryPrompt
30 } else if self.help {
31 InteractionPhase::Help
32 } else if self.session.draft().is_some() {
33 InteractionPhase::Draft
34 } else {
35 InteractionPhase::Browse
36 }
37 }
38
39 #[must_use]
40 pub fn handle_input(&mut self, input: ReviewInput) -> InputOutcome<DiffReviewEvent> {
41 let Ok(outcome) = interaction::handle_input(self, input);
42 outcome
43 }
44}
45
46impl ReviewWidget for DiffReviewState {
47 type Event = DiffReviewEvent;
48 type Error = Infallible;
49 type Draft = CommentDraft;
50
51 fn phase(&self) -> InteractionPhase {
52 self.interaction_phase()
53 }
54 fn handle_review_command(
55 &mut self,
56 command: ReviewCommand,
57 ) -> Result<InputOutcome<DiffReviewEvent>, Infallible> {
58 Ok(self.handle_command(command))
59 }
60 fn contains(&self, position: Position) -> bool {
61 self.hit_layout.drawer.contains(position) || self.hit_layout.patch.contains(position)
62 }
63 fn mark_dirty(&mut self) {
64 Self::mark_dirty(self);
65 }
66 fn draft_mut(&mut self) -> Option<&mut CommentDraft> {
67 self.session.draft_mut()
68 }
69 fn draft_changed(&mut self) {
70 self.request_follow();
71 }
72 fn paste_prompt(&mut self, text: &str) {
73 if let Some(RepositoryPrompt::Commit { message }) = &mut self.repository_prompt {
74 message.push_str(text);
75 }
76 }
77 fn handle_browse_key(
78 &mut self,
79 key: KeyEvent,
80 ) -> Result<InputOutcome<DiffReviewEvent>, Infallible> {
81 Ok(self
82 .command_for_key(key)
83 .map_or(InputOutcome::Ignored, |command| {
84 self.handle_command(command)
85 }))
86 }
87 fn handle_mouse(&mut self, mouse: MouseEvent) -> InputOutcome<DiffReviewEvent> {
88 self.handle_mouse(mouse)
89 }
90 fn handle_prompt_key(&mut self, key: KeyEvent) -> InputOutcome<DiffReviewEvent> {
91 self.handle_repository_prompt_key(key)
92 }
93}
94
95impl DiffReviewState {
96 fn handle_repository_prompt_key(&mut self, key: KeyEvent) -> InputOutcome<DiffReviewEvent> {
97 if key.code == KeyCode::Esc {
98 return self.handle_command(ReviewCommand::Cancel);
99 }
100 if !interaction::is_plain_key(key) {
101 return InputOutcome::Consumed;
102 }
103 let Some(prompt) = self.repository_prompt.as_mut() else {
104 return InputOutcome::Consumed;
105 };
106 let action = match prompt {
107 RepositoryPrompt::Commit { message } => match key.code {
108 KeyCode::Enter => {
109 let message = message.trim().to_owned();
110 if message.is_empty() {
111 self.set_repository_error("Commit message cannot be empty");
112 None
113 } else {
114 Some(RepositoryAction::Commit { message })
115 }
116 }
117 KeyCode::Backspace => {
118 message.pop();
119 None
120 }
121 KeyCode::Char(character) => {
122 message.push(character);
123 None
124 }
125 _ => None,
126 },
127 RepositoryPrompt::Discard { path, status } => match key.code {
128 KeyCode::Char('y' | 'Y') => Some(RepositoryAction::Discard {
129 path: path.clone(),
130 status: *status,
131 }),
132 KeyCode::Char('n' | 'N') => {
133 self.repository_prompt = None;
134 None
135 }
136 _ => None,
137 },
138 };
139 if let Some(action) = action {
140 self.repository_prompt = None;
141 return self.handle_command(DiffReviewCommand::RepositoryAction(action));
142 }
143 InputOutcome::Consumed
144 }
145
146 fn handle_mouse(&mut self, mouse: MouseEvent) -> InputOutcome<DiffReviewEvent> {
147 let position = Position::new(mouse.column, mouse.row);
148 match mouse.kind {
149 MouseEventKind::ScrollUp => self.scroll_at(position, -1),
150 MouseEventKind::ScrollDown => self.scroll_at(position, 1),
151 MouseEventKind::Down(_) => {
152 if self.hit_layout.drawer.contains(position) {
153 self.focus = FocusPane::Files;
154 let relative = usize::from(mouse.row.saturating_sub(self.hit_layout.drawer.y));
155 self.select_drawer_entry(self.drawer_scroll.saturating_add(relative));
156 if self.hit_layout.drawer_stage_column == Some(mouse.column)
157 && !matches!(self.repository_status, RepositoryOperationStatus::Pending)
158 {
159 return self.handle_command(DiffReviewCommand::ToggleStage);
160 }
161 } else if self.hit_layout.patch.contains(position) {
162 self.focus = FocusPane::Diff;
163 self.select_clicked_row(mouse.row);
164 }
165 }
166 _ => {}
167 }
168 InputOutcome::Consumed
169 }
170
171 fn scroll_at(&mut self, position: Position, direction: isize) {
172 let pane = if self.hit_layout.patch.contains(position) {
173 FocusPane::Diff
174 } else if self.hit_layout.drawer.contains(position) {
175 FocusPane::Files
176 } else {
177 self.focus
178 };
179 match pane {
180 FocusPane::Diff => self.move_row(direction),
181 FocusPane::Files => self.scroll_drawer(direction),
182 }
183 }
184}