ratatui_toolkit/widgets/code_diff/widget/methods/handle_key.rs
1//! Method to handle keyboard input.
2
3use crossterm::event::KeyCode;
4
5use crate::widgets::code_diff::code_diff::CodeDiff;
6
7impl CodeDiff {
8 /// Handles a keyboard event and returns whether it was consumed.
9 ///
10 /// # Key Bindings
11 ///
12 /// - `[` - Toggle sidebar visibility
13 /// - `Tab` - Switch focus between sidebar and diff
14 /// - `/` - Enter filter mode (sidebar only)
15 /// - `h` - Collapse expanded directory, or go to parent if collapsed (sidebar only, no-op on files)
16 /// - `l` - Expand collapsed directory, or show diff for files (sidebar only)
17 /// - `j` / `Down` - Navigate down (files in sidebar, scroll in diff)
18 /// - `k` / `Up` - Navigate up (files in sidebar, scroll in diff)
19 /// - `g` - Go to top (first file or top of diff)
20 /// - `G` - Go to bottom (last file or bottom of diff)
21 /// - `Space` / `Enter` - Toggle directory expand / select file (sidebar only)
22 /// - `H` / `<` - Decrease sidebar width
23 /// - `L` / `>` - Increase sidebar width
24 /// - `r` - Refresh diff from git
25 ///
26 /// # Filter Mode
27 ///
28 /// When filter mode is active (triggered by `/`):
29 /// - `Esc` - Clear filter and exit filter mode
30 /// - `Enter` - Exit filter mode (keep filter active)
31 /// - `Backspace` - Delete last character
32 /// - Any character - Append to filter
33 ///
34 /// # Arguments
35 ///
36 /// * `key` - The key code that was pressed
37 ///
38 /// # Returns
39 ///
40 /// `true` if the key was handled, `false` otherwise
41 ///
42 /// # Example
43 ///
44 /// ```rust
45 /// use crossterm::event::KeyCode;
46 /// use ratatui_toolkit::code_diff::{CodeDiff, DiffConfig};
47 ///
48 /// let mut diff = CodeDiff::new()
49 /// .with_config(DiffConfig::new().sidebar_enabled(true));
50 ///
51 /// // Toggle sidebar
52 /// diff.handle_key(KeyCode::Char('['));
53 ///
54 /// // Navigate
55 /// diff.handle_key(KeyCode::Char('j'));
56 ///
57 /// // Enter filter mode
58 /// diff.handle_key(KeyCode::Char('/'));
59 /// ```
60 pub fn handle_key(&mut self, key: KeyCode) -> bool {
61 // If in filter mode, delegate to file tree's filter handler
62 if self.file_tree.is_filter_mode() {
63 return self.file_tree.handle_filter_key(key);
64 }
65
66 match key {
67 // Toggle sidebar
68 KeyCode::Char('[') => {
69 self.toggle_sidebar();
70 true
71 }
72
73 // Focus switching - Tab only
74 KeyCode::Tab => {
75 if self.show_sidebar && self.config.sidebar_enabled {
76 self.toggle_focus();
77 }
78 true
79 }
80
81 // Vim-style: h = collapse directory (sidebar only)
82 // On expanded directory: collapse it
83 // On collapsed directory: go to parent
84 // On file: do nothing (files can't be collapsed)
85 KeyCode::Char('h') => {
86 if self.sidebar_focused && self.show_sidebar {
87 // Check if current selection is a directory
88 if let Some(is_dir) = self.file_tree.selected_is_dir() {
89 if is_dir {
90 // It's a directory - try to collapse it
91 // If already collapsed, go to parent
92 if !self.file_tree.collapse_selected() {
93 self.file_tree.go_to_parent();
94 }
95 }
96 // For files, do nothing - h only affects directories
97 }
98 true
99 } else {
100 false
101 }
102 }
103
104 // Vim-style: l = expand directory or show diff (sidebar only)
105 // On collapsed directory: expand it
106 // On expanded directory: descend (select first child) or show diff if no children
107 // On file: show the diff
108 KeyCode::Char('l') | KeyCode::Enter => {
109 if self.sidebar_focused && self.show_sidebar {
110 // Check if current selection is a directory
111 if let Some(is_dir) = self.file_tree.selected_is_dir() {
112 if is_dir {
113 // It's a directory - try to expand it
114 // If already expanded, we could descend but for now just expand
115 self.file_tree.expand_selected();
116 } else {
117 // It's a file - show the diff
118 self.sync_diff_from_selection();
119 }
120 }
121 true
122 } else {
123 false
124 }
125 }
126
127 // Navigation down
128 KeyCode::Char('j') | KeyCode::Down => {
129 if self.sidebar_focused && self.show_sidebar {
130 self.select_next_file();
131 } else {
132 self.scroll_down(1);
133 }
134 true
135 }
136
137 // Navigation up
138 KeyCode::Char('k') | KeyCode::Up => {
139 if self.sidebar_focused && self.show_sidebar {
140 self.select_prev_file();
141 } else {
142 self.scroll_up(1);
143 }
144 true
145 }
146
147 // Go to top
148 KeyCode::Char('g') => {
149 if self.sidebar_focused && self.show_sidebar {
150 self.file_tree.set_selected_index(0);
151 self.sync_diff_from_selection();
152 } else {
153 self.scroll_offset = 0;
154 }
155 true
156 }
157
158 // Go to bottom
159 KeyCode::Char('G') => {
160 if self.sidebar_focused && self.show_sidebar {
161 let count = self.file_tree.visible_count();
162 self.file_tree.set_selected_index(count.saturating_sub(1));
163 self.sync_diff_from_selection();
164 } else {
165 let total = self.total_lines();
166 self.scroll_offset = total.saturating_sub(1);
167 }
168 true
169 }
170
171 // Toggle directory expand (sidebar only)
172 KeyCode::Char(' ') => {
173 if self.sidebar_focused && self.show_sidebar {
174 self.file_tree.toggle_expand();
175 true
176 } else {
177 false
178 }
179 }
180
181 // Resize sidebar narrower
182 KeyCode::Char('H') | KeyCode::Char('<') => {
183 if self.config.sidebar_enabled {
184 self.resize_sidebar(-5);
185 }
186 true
187 }
188
189 // Resize sidebar wider
190 KeyCode::Char('L') | KeyCode::Char('>') => {
191 if self.config.sidebar_enabled {
192 self.resize_sidebar(5);
193 }
194 true
195 }
196
197 // Refresh diff from git
198 KeyCode::Char('r') => {
199 self.refresh();
200 true
201 }
202
203 // Enter filter mode (sidebar only)
204 KeyCode::Char('/') => {
205 if self.sidebar_focused && self.show_sidebar {
206 self.file_tree.enter_filter_mode();
207 true
208 } else {
209 false
210 }
211 }
212
213 _ => false,
214 }
215 }
216}