Skip to main content

clankerdiff_ratatui/
input.rs

1use 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}