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}