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}