Skip to main content

diffs_rs/app/
controls.rs

1use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, MouseButton, MouseEventKind};
2
3use super::{App, Layout, sidebar::Focus};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum InputEffect {
7    Ignored,
8    Redraw,
9    Highlight,
10}
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Command {
14    Down,
15    Up,
16    PageDown,
17    PageUp,
18    FirstSource,
19    LastSource,
20    NextFile,
21    PreviousFile,
22    NextHunk,
23    PreviousHunk,
24    Left,
25    Right,
26    SwitchSide,
27    ToggleLayout,
28    ToggleWrap,
29    ToggleNumbers,
30    ToggleTheme,
31    Files,
32    Help,
33    Back,
34    Activate,
35    ToggleFolder,
36    TreeStart,
37    TreeEnd,
38    SelectLines,
39    ClearSelection,
40}
41
42impl Command {
43    pub fn from_key(key: KeyEvent) -> Option<Self> {
44        if key.kind == KeyEventKind::Release {
45            return None;
46        }
47        Some(match key.code {
48            KeyCode::Down | KeyCode::Char('j') => Self::Down,
49            KeyCode::Up | KeyCode::Char('k') => Self::Up,
50            KeyCode::PageDown => Self::PageDown,
51            KeyCode::PageUp => Self::PageUp,
52            KeyCode::Char('g') => Self::FirstSource,
53            KeyCode::Char('G') => Self::LastSource,
54            KeyCode::Char('n') => Self::NextFile,
55            KeyCode::Char('N') => Self::PreviousFile,
56            KeyCode::Char(']') => Self::NextHunk,
57            KeyCode::Char('[') => Self::PreviousHunk,
58            KeyCode::Left => Self::Left,
59            KeyCode::Right => Self::Right,
60            KeyCode::Tab => Self::SwitchSide,
61            KeyCode::Char('u') => Self::ToggleLayout,
62            KeyCode::Char('w') => Self::ToggleWrap,
63            KeyCode::Char('l') => Self::ToggleNumbers,
64            KeyCode::Char('t') => Self::ToggleTheme,
65            KeyCode::Char('f') => Self::Files,
66            KeyCode::Char('?') => Self::Help,
67            KeyCode::Esc => Self::Back,
68            KeyCode::Enter => Self::Activate,
69            KeyCode::Char(' ') => Self::ToggleFolder,
70            KeyCode::Home => Self::TreeStart,
71            KeyCode::End => Self::TreeEnd,
72            _ => return None,
73        })
74    }
75}
76
77impl App {
78    pub fn handle_event(&mut self, event: Event) -> anyhow::Result<InputEffect> {
79        let mut highlight = true;
80        match event {
81            Event::Resize(w, h) => {
82                self.prepare_view(ratatui::layout::Rect::new(0, 0, w, h))?;
83            }
84            Event::Key(key) => {
85                return Command::from_key(key)
86                    .map_or(Ok(InputEffect::Ignored), |command| self.execute(command));
87            }
88            Event::Mouse(mouse) if !self.help => {
89                let point = ratatui::layout::Position::new(mouse.column, mouse.row);
90                if self.sidebar.area.contains(point) {
91                    highlight = false;
92                    match mouse.kind {
93                        MouseEventKind::ScrollDown => self.sidebar.scroll(3),
94                        MouseEventKind::ScrollUp => self.sidebar.scroll(-3),
95                        MouseEventKind::Down(MouseButton::Left) => {
96                            self.sidebar.focus = Focus::Files;
97                            if mouse.row > self.sidebar.area.y {
98                                let index = self.sidebar.top
99                                    + usize::from(mouse.row - self.sidebar.area.y - 1);
100                                if let Some(row) = self.sidebar.tree.rows().get(index) {
101                                    self.sidebar.cursor = Some(row.node);
102                                    highlight = self.activate_tree();
103                                }
104                            }
105                        }
106                        _ => return Ok(InputEffect::Ignored),
107                    }
108                } else if self.sidebar.diff.contains(point) {
109                    match mouse.kind {
110                        MouseEventKind::ScrollDown => self.scroll(3)?,
111                        MouseEventKind::ScrollUp => self.scroll(-3)?,
112                        MouseEventKind::Down(MouseButton::Left) => {
113                            self.sidebar.focus = Focus::Diff;
114                            let y = mouse.row - self.sidebar.diff.y + 1;
115                            if let Some((position, _)) =
116                                self.visible.iter().find(|(_, row)| *row == y)
117                            {
118                                self.select(*position, mouse.column - self.sidebar.diff.x)?;
119                                self.expand_context()?;
120                            }
121                        }
122                        _ => return Ok(InputEffect::Ignored),
123                    }
124                } else {
125                    return Ok(InputEffect::Ignored);
126                }
127            }
128            _ => return Ok(InputEffect::Ignored),
129        }
130        Ok(if highlight {
131            InputEffect::Highlight
132        } else {
133            InputEffect::Redraw
134        })
135    }
136
137    pub fn execute(&mut self, command: Command) -> anyhow::Result<InputEffect> {
138        self.sidebar.notice = None;
139        let mut highlight = true;
140        match command {
141            Command::Help => {
142                self.help = !self.help;
143                highlight = false;
144            }
145            Command::Back if self.help => {
146                self.help = false;
147                highlight = false;
148            }
149            _ if self.help => return Ok(InputEffect::Ignored),
150            Command::Files => self.files_command()?,
151            command if self.sidebar.focused() => {
152                highlight = false;
153                match command {
154                    Command::Back | Command::SwitchSide => self.sidebar.focus = Focus::Diff,
155                    Command::Down => self.sidebar.move_cursor(1),
156                    Command::Up => self.sidebar.move_cursor(-1),
157                    Command::PageDown => self.sidebar.move_cursor(self.sidebar.page() as isize),
158                    Command::PageUp => self.sidebar.move_cursor(-(self.sidebar.page() as isize)),
159                    Command::TreeStart => self.sidebar.move_cursor(isize::MIN),
160                    Command::TreeEnd => self.sidebar.move_cursor(isize::MAX),
161                    Command::Right => self.sidebar.right(),
162                    Command::Left => self.sidebar.left(),
163                    Command::Activate => highlight = self.activate_tree(),
164                    Command::ToggleFolder => {
165                        if let Some(id) = self.sidebar.cursor {
166                            self.sidebar.toggle(id);
167                        }
168                    }
169                    _ => return Ok(InputEffect::Ignored),
170                }
171            }
172            _ if self.sidebar.diff.is_empty() => return Ok(InputEffect::Ignored),
173            Command::Down => self.move_cursor(1)?,
174            Command::Up => self.move_cursor(-1)?,
175            Command::PageDown => self.move_cursor(self.height.saturating_sub(4).max(1) as isize)?,
176            Command::PageUp => {
177                self.move_cursor(-(self.height.saturating_sub(4).max(1) as isize))?
178            }
179            Command::FirstSource => self.jump(false)?,
180            Command::LastSource => self.jump(true)?,
181            Command::NextHunk => self.jump_section(true, false),
182            Command::PreviousHunk => self.jump_section(false, false),
183            Command::NextFile => self.jump_section(true, true),
184            Command::PreviousFile => self.jump_section(false, true),
185            Command::Right if !self.wrap => self.move_horizontal(true)?,
186            Command::Left if !self.wrap => self.move_horizontal(false)?,
187            Command::SwitchSide => self.switch_side(),
188            Command::ToggleLayout => {
189                let layout = if self.split() {
190                    Layout::Unified
191                } else {
192                    Layout::Split
193                };
194                self.reflow(layout, self.width, self.height)?;
195            }
196            Command::ToggleWrap => self.toggle_wrap()?,
197            Command::ToggleNumbers => self.toggle_numbers()?,
198            Command::ToggleTheme => self.toggle_theme()?,
199            Command::Activate => highlight = self.expand_context()?,
200            _ => return Ok(InputEffect::Ignored),
201        }
202        Ok(if highlight {
203            InputEffect::Highlight
204        } else {
205            InputEffect::Redraw
206        })
207    }
208}