tui_file_explorer/explorer/
keys.rs1use super::{background::AfterLoad, *};
2
3impl ExplorerCommand {
4 #[must_use]
6 pub fn from_key(key: KeyEvent) -> Option<Self> {
7 Keymap::builtin().resolve(key)
8 }
9}
10
11impl PromptKind {
14 fn outcome(self, from: PathBuf, to: PathBuf) -> ExplorerOutcome {
15 match self {
16 Self::Rename => ExplorerOutcome::RenameCompleted { from, to },
17 Self::Touch => ExplorerOutcome::TouchCreated(to),
18 Self::Mkdir => ExplorerOutcome::MkdirCreated(to),
19 }
20 }
21}
22
23impl FileExplorer {
24 fn handle_prompt_key(&mut self, key: KeyEvent) -> ExplorerOutcome {
26 let Some(prompt) = self.prompt.as_mut() else {
27 return ExplorerOutcome::Pending;
28 };
29 match key.code {
30 KeyCode::Char(c)
32 if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT =>
33 {
34 prompt.text.push(c);
35 }
36 KeyCode::Backspace => {
37 prompt.text.pop();
38 }
39 KeyCode::Enter => return self.commit_prompt(),
40 KeyCode::Esc => self.prompt = None,
42 _ => {}
44 }
45 ExplorerOutcome::Pending
46 }
47
48 fn commit_prompt(&mut self) -> ExplorerOutcome {
50 let Some(Prompt { kind, text }) = self.prompt.take() else {
51 return ExplorerOutcome::Pending;
52 };
53 let name = text.trim();
54 if name.is_empty() {
55 return ExplorerOutcome::Pending;
56 }
57
58 let target = self.current_dir.join(name);
59 let mut from = target.clone();
60 let result = match kind {
61 PromptKind::Rename => {
62 let Some(src) = self.entries.get(self.cursor).map(|e| e.path.clone()) else {
64 return ExplorerOutcome::Pending;
65 };
66 from = src.clone();
67 self.filesystem.rename(&src, &target)
68 }
69 PromptKind::Touch => self.filesystem.create_file(&target),
70 PromptKind::Mkdir => self.filesystem.create_dir_all(&target),
71 };
72
73 match result {
74 Ok(()) => {
75 self.reload_with(AfterLoad {
76 select: Some(target.clone()),
77 cursor: None,
78 });
79 kind.outcome(from, target)
80 }
81 Err(e) => {
82 self.status = format!("{} failed: {e}", kind.verb());
83 ExplorerOutcome::Pending
84 }
85 }
86 }
87
88 pub fn go_to(&mut self, path: &Path) {
93 let (dir, select) = if self.filesystem.is_dir(path) {
94 (path.to_path_buf(), None)
95 } else {
96 match path.parent() {
97 Some(parent) => (parent.to_path_buf(), Some(path)),
98 None => return,
99 }
100 };
101 self.current_dir = dir;
102 self.search_active = false;
103 self.search_query.clear();
104 self.marked.clear();
105 self.cursor = 0;
106 self.scroll_offset = 0;
107 self.reload_with(AfterLoad {
108 select: select.map(Path::to_path_buf),
109 cursor: None,
110 });
111 }
112
113 pub fn select_path(&mut self, path: &Path) {
115 if let Some(idx) = self.entries.iter().position(|e| e.path == path) {
116 self.cursor = idx;
117 }
118 }
119
120 fn reload_keep_cursor(&mut self) {
122 self.reload();
124 }
125
126 fn reload_from_top(&mut self) {
128 self.cursor = 0;
129 self.scroll_offset = 0;
130 self.reload();
131 }
132}
133
134impl FileExplorer {
137 pub fn handle_command(&mut self, command: ExplorerCommand) -> ExplorerOutcome {
142 self.handle_key(self.keymap.canonical_key(command))
143 }
144
145 pub fn handle_key(&mut self, key: KeyEvent) -> ExplorerOutcome {
147 press_only!(key, ExplorerOutcome::Pending);
148
149 if self.prompt.is_some() {
151 return self.handle_prompt_key(key);
152 }
153
154 if self.search_active {
158 match key.code {
159 KeyCode::Char(c) if key.modifiers.is_empty() => {
160 self.search_query.push(c);
161 self.reload_from_top();
162 return ExplorerOutcome::Pending;
163 }
164 KeyCode::Backspace => {
165 if self.search_query.is_empty() {
166 self.search_active = false;
168 } else {
169 self.search_query.pop();
170 self.reload_from_top();
171 }
172 return ExplorerOutcome::Pending;
173 }
174 KeyCode::Esc => {
176 self.search_active = false;
177 self.search_query.clear();
178 self.reload_from_top();
179 return ExplorerOutcome::Pending;
180 }
181 _ => {}
182 }
183 }
184
185 match self.keymap.resolve(key) {
186 Some(command) => self.execute(command),
187 None => ExplorerOutcome::Unhandled,
188 }
189 }
190
191 fn execute(&mut self, command: ExplorerCommand) -> ExplorerOutcome {
193 use ExplorerCommand as C;
194 match command {
195 C::Dismiss => return ExplorerOutcome::Dismissed,
196 C::MoveUp => self.move_up(),
197 C::MoveDown => self.move_down(),
198 C::PageUp => (0..self.page_size.min(self.entries.len())).for_each(|_| self.move_up()),
199 C::PageDown => {
200 (0..self.page_size.min(self.entries.len())).for_each(|_| self.move_down())
201 }
202 C::Top => {
203 self.cursor = 0;
204 self.scroll_offset = 0;
205 }
206 C::Bottom => {
207 if !self.entries.is_empty() {
208 self.cursor = self.entries.len() - 1;
209 }
210 }
211 C::Ascend => self.ascend(),
212 C::Navigate => return self.navigate(),
214 C::Confirm => return self.confirm(),
215 C::ToggleHidden => {
216 self.show_hidden = !self.show_hidden;
217 self.reload_keep_cursor();
218 }
219 C::ToggleSizes => self.show_sizes = !self.show_sizes,
220 C::ToggleDirSizes => self.show_dir_sizes = !self.show_dir_sizes,
221 C::Search => self.search_active = true,
222 C::CycleSort => {
223 self.sort_mode = self.sort_mode.next();
224 self.reload_keep_cursor();
225 }
226 C::ToggleMark => self.toggle_mark(),
227 C::NewDirectory => self.open_prompt(PromptKind::Mkdir, ""),
228 C::NewFile => self.open_prompt(PromptKind::Touch, ""),
229 C::Rename => {
231 if let Some(entry) = self.entries.get(self.cursor) {
232 let name = entry.name.clone();
233 self.open_prompt(PromptKind::Rename, name);
234 }
235 }
236 }
237 ExplorerOutcome::Pending
238 }
239
240 pub(crate) fn move_up(&mut self) {
243 if self.cursor > 0 {
244 self.cursor -= 1;
245 }
246 self.clamp_cursor();
248 }
249
250 pub(crate) fn move_down(&mut self) {
251 let last = self.entries.len().saturating_sub(1);
252 if !self.entries.is_empty() && self.cursor < last {
253 self.cursor += 1;
254 }
255 self.clamp_cursor();
256 }
257
258 pub(crate) fn clamp_cursor(&mut self) {
262 let max = self.entries.len().saturating_sub(1);
263 if self.cursor > max {
264 self.cursor = max;
265 }
266 if self.scroll_offset > self.cursor {
267 self.scroll_offset = self.cursor;
268 }
269 }
270
271 pub(crate) fn ascend(&mut self) {
272 if let Some(parent) = self.current_dir.parent().map(std::path::Path::to_path_buf) {
273 let prev = self.current_dir.clone();
274 self.current_dir = parent;
275 self.cursor = 0;
276 self.scroll_offset = 0;
277 self.search_active = false;
279 self.search_query.clear();
280 self.marked.clear();
281 self.reload_with(AfterLoad {
283 select: Some(prev),
284 cursor: None,
285 });
286 } else {
287 self.status = "Already at the filesystem root.".to_string();
289 }
290 }
291
292 fn navigate(&mut self) -> ExplorerOutcome {
298 let Some(entry) = self.entries.get(self.cursor) else {
299 return ExplorerOutcome::Pending;
300 };
301
302 if entry.is_dir {
303 let path = entry.path.clone();
304 self.search_active = false;
305 self.search_query.clear();
306 self.marked.clear();
307 self.navigate_to(path);
308 } else {
309 self.move_down();
310 }
311 ExplorerOutcome::Pending
312 }
313
314 fn confirm(&mut self) -> ExplorerOutcome {
315 let Some(entry) = self.entries.get(self.cursor) else {
316 return ExplorerOutcome::Pending;
317 };
318
319 if entry.is_dir {
320 if matches!(
321 self.selection_mode,
322 SelectionMode::Directories | SelectionMode::Any
323 ) {
324 return ExplorerOutcome::Selected(entry.path.clone());
325 }
326 let path = entry.path.clone();
327 self.search_active = false;
329 self.search_query.clear();
330 self.marked.clear();
331 self.navigate_to(path);
332 ExplorerOutcome::Pending
333 } else if self.selection_mode == SelectionMode::Directories {
334 self.status = "Only directories can be selected.".into();
335 ExplorerOutcome::Pending
336 } else {
337 ExplorerOutcome::Selected(entry.path.clone())
340 }
341 }
342}