1use std::os::unix::fs::MetadataExt;
2
3use anyhow::Result;
4use clap::Parser;
5use ratatui::layout::Rect;
6use ratatui::Frame;
7
8use crate::app::Tab;
9use crate::common::{index_from_a, INPUT_HISTORY_PATH};
10use crate::config::Bindings;
11use crate::io::{drop_sudo_privileges, InputHistory, OpendalContainer};
12use crate::io::{Args, DrawMenu};
13use crate::log_line;
14use crate::modes::{
15 nvim_inform_ipc, Bulk, CliApplications, Completion, Compresser, ContentWindow, ContextMenu,
16 Flagged, History, Input, InputCompleted, IsoDevice, Marks, Menu, Mount, Navigate,
17 NeedConfirmation, NvimIPCAction, PasswordHolder, Picker, Remote, Selectable, Shortcut,
18 TempMarks, Trash, TuiApplications, MAX_FILE_MODE,
19};
20
21macro_rules! impl_navigate_from_char {
22 ($name:ident, $field:ident) => {
23 #[doc = concat!(
24 "Navigates to the index in the `",
25 stringify!($field),
26 "` field based on the given character."
27 )]
28 pub fn $name(&mut self, c: char) -> bool {
29 let Some(index) = index_from_a(c) else {
30 return false;
31 };
32 if index < self.$field.len() {
33 self.$field.set_index(index);
34 self.window.scroll_to(index);
35 return true;
36 }
37 false
38 }
39 };
40}
41
42pub struct MenuHolder {
53 pub window: ContentWindow,
55 pub bulk: Bulk,
57 pub cli_applications: CliApplications,
59 pub cloud: OpendalContainer,
61 pub completion: Completion,
63 pub compression: Compresser,
65 pub context: ContextMenu,
67 pub flagged: Flagged,
69 pub input: Input,
71 pub input_history: InputHistory,
73 pub iso_device: Option<IsoDevice>,
75 pub marks: Marks,
77 pub temp_marks: TempMarks,
79 pub password_holder: PasswordHolder,
81 pub picker: Picker,
83 pub shortcut: Shortcut,
85 pub tui_applications: TuiApplications,
87 pub trash: Trash,
89 pub sudo_command: Option<String>,
91 pub history: History,
93 pub mount: Mount,
95}
96
97impl MenuHolder {
98 pub fn new(start_dir: &std::path::Path, binds: &Bindings) -> Result<Self> {
99 Ok(Self {
100 bulk: Bulk::default(),
101 cli_applications: CliApplications::default(),
102 cloud: OpendalContainer::default(),
103 completion: Completion::default(),
104 compression: Compresser::default(),
105 context: ContextMenu::default(),
106 flagged: Flagged::default(),
107 history: History::default(),
108 input: Input::default(),
109 input_history: InputHistory::load(INPUT_HISTORY_PATH)?,
110 iso_device: None,
111 marks: Marks::default(),
112 password_holder: PasswordHolder::default(),
113 picker: Picker::default(),
114 shortcut: Shortcut::empty(start_dir),
115 sudo_command: None,
116 temp_marks: TempMarks::default(),
117 trash: Trash::new(binds)?,
118 tui_applications: TuiApplications::default(),
119 window: ContentWindow::default(),
120 mount: Mount::default(),
121 })
122 }
123
124 pub fn reset(&mut self) {
125 self.input.reset();
126 self.completion.reset();
127 self.bulk.reset();
128 self.sudo_command = None;
129 }
130
131 pub fn resize(&mut self, menu_mode: Menu, height: usize) {
132 self.window.set_height(height);
133 if let Menu::Navigate(_) = menu_mode {
134 self.window.scroll_to(self.index(menu_mode))
135 }
136 }
137
138 pub fn replace_input_by_permissions(&mut self) {
147 let Some(flagged) = &self.flagged.content.first() else {
148 return;
149 };
150 let Ok(metadata) = flagged.metadata() else {
151 return;
152 };
153 let mode = metadata.mode() & MAX_FILE_MODE;
154 self.input.replace(&format!("{mode:o}"));
155 }
156
157 pub fn input_complete(&mut self, tab: &mut Tab) -> Result<()> {
159 self.fill_completion(tab);
160 self.window.reset(self.completion.len());
161 Ok(())
162 }
163
164 fn fill_completion(&mut self, tab: &mut Tab) {
165 match tab.menu_mode {
166 Menu::InputCompleted(InputCompleted::Cd) => self.completion.cd(
167 tab.current_directory_path()
168 .as_os_str()
169 .to_string_lossy()
170 .as_ref(),
171 &self.input.string(),
172 ),
173 Menu::InputCompleted(InputCompleted::Exec) => {
174 self.completion.exec(&self.input.string())
175 }
176 Menu::InputCompleted(InputCompleted::Search) => {
177 self.completion.search(tab.completion_search_files());
178 }
179 Menu::InputCompleted(InputCompleted::Action) => {
180 self.completion.action(&self.input.string())
181 }
182 _ => (),
183 }
184 }
185
186 pub fn mount_remote(&mut self, current_path: &str) {
191 let input = self.input.string();
192 if let Some(remote_builder) = Remote::from_input(input, current_path) {
193 remote_builder.mount();
194 }
195 self.input.reset();
196 }
197
198 pub fn remove_selected_flagged(&mut self) -> Result<()> {
200 self.flagged.remove_selected();
201 Ok(())
202 }
203
204 pub fn trash_delete_permanently(&mut self) -> Result<()> {
205 self.trash.delete_permanently()
206 }
207
208 pub fn trash_and_inform(&mut self) -> Result<()> {
212 self.trash.update()?;
213 let output_socket = Args::parse().output_socket;
214 while let Some(flagged) = self.flagged.content.pop() {
215 if self.trash_a_file(&flagged).is_ok() {
216 if let Some(output_socket) = &output_socket {
217 nvim_inform_ipc(output_socket, NvimIPCAction::DELETE(&flagged))?;
218 }
219 }
220 }
221 Ok(())
222 }
223
224 fn trash_a_file(&mut self, origin: &std::path::Path) -> Result<()> {
226 self.trash.trash(origin)?;
227 self.delete_mark(origin)
228 }
229
230 pub fn delete_flagged_files(&mut self) -> Result<()> {
238 let nb = self.flagged.len();
239 let output_socket = Args::parse().output_socket;
240 while let Some(pathbuf) = self.flagged.content.pop() {
241 if pathbuf.is_dir() {
242 std::fs::remove_dir_all(&pathbuf)?;
243 } else {
244 std::fs::remove_file(&pathbuf)?;
245 }
246 if let Some(output_socket) = &output_socket {
247 nvim_inform_ipc(output_socket, NvimIPCAction::DELETE(&pathbuf))?;
248 }
249 self.delete_mark(&pathbuf)?;
250 }
251 log_line!("Deleted {nb} flagged files");
252 Ok(())
253 }
254
255 pub fn clear_sudo_attributes(&mut self) -> Result<()> {
257 self.password_holder.reset();
258 drop_sudo_privileges()?;
259 self.sudo_command = None;
260 Ok(())
261 }
262
263 pub fn input_insert(&mut self, char: char) -> Result<()> {
265 self.input.insert(char);
266 Ok(())
267 }
268
269 pub fn refresh_shortcuts(
272 &mut self,
273 mount_points: &[&std::path::Path],
274 left_path: &std::path::Path,
275 right_path: &std::path::Path,
276 ) {
277 self.shortcut.refresh(mount_points, left_path, right_path)
278 }
279
280 pub fn completion_reset(&mut self) {
281 self.completion.reset();
282 }
283
284 pub fn completion_tab(&mut self) {
285 self.input.replace(self.completion.current_proposition())
286 }
287
288 pub fn len(&self, menu_mode: Menu) -> usize {
289 match menu_mode {
290 Menu::Navigate(navigate) => self.apply_method(navigate, |variant| variant.len()),
291 Menu::InputCompleted(_) => self.completion.len(),
292 Menu::NeedConfirmation(need_confirmation) if need_confirmation.use_flagged_files() => {
293 self.flagged.len()
294 }
295 Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => self.trash.len(),
296 Menu::NeedConfirmation(NeedConfirmation::BulkAction) => self.bulk.len(),
297 _ => 0,
298 }
299 }
300
301 pub fn index(&self, menu_mode: Menu) -> usize {
302 match menu_mode {
303 Menu::Navigate(navigate) => self.apply_method(navigate, |variant| variant.index()),
304 Menu::InputCompleted(_) => self.completion.index,
305 Menu::NeedConfirmation(need_confirmation) if need_confirmation.use_flagged_files() => {
306 self.flagged.index()
307 }
308 Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => self.trash.index(),
309 Menu::NeedConfirmation(NeedConfirmation::BulkAction) => self.bulk.index(),
310 _ => 0,
311 }
312 }
313
314 pub fn page_down(&mut self, navigate: Navigate) {
315 for _ in 0..10 {
316 self.next(navigate)
317 }
318 }
319
320 pub fn page_up(&mut self, navigate: Navigate) {
321 for _ in 0..10 {
322 self.prev(navigate)
323 }
324 }
325
326 pub fn completion_prev(&mut self, input_completed: InputCompleted) {
327 self.completion.prev();
328 self.window
329 .scroll_to(self.index(Menu::InputCompleted(input_completed)));
330 }
331
332 pub fn completion_next(&mut self, input_completed: InputCompleted) {
333 self.completion.next();
334 self.window
335 .scroll_to(self.index(Menu::InputCompleted(input_completed)));
336 }
337
338 pub fn next(&mut self, navigate: Navigate) {
339 self.apply_method_mut(navigate, |variant| variant.next());
340 self.window.scroll_to(self.index(Menu::Navigate(navigate)));
341 }
342
343 pub fn prev(&mut self, navigate: Navigate) {
344 self.apply_method_mut(navigate, |variant| variant.prev());
345 self.window.scroll_to(self.index(Menu::Navigate(navigate)));
346 }
347
348 pub fn set_index(&mut self, index: usize, navigate: Navigate) {
349 self.apply_method_mut(navigate, |variant| variant.set_index(index));
350 self.window.scroll_to(self.index(Menu::Navigate(navigate)))
351 }
352
353 pub fn select_last(&mut self, navigate: Navigate) {
354 let index = self.len(Menu::Navigate(navigate)).saturating_sub(1);
355 self.set_index(index, navigate);
356 }
357
358 fn apply_method_mut<F, T>(&mut self, navigate: Navigate, func: F) -> T
359 where
360 F: FnOnce(&mut dyn Selectable) -> T,
361 {
362 match navigate {
363 Navigate::CliApplication => func(&mut self.cli_applications),
364 Navigate::Compress => func(&mut self.compression),
365 Navigate::Mount => func(&mut self.mount),
366 Navigate::Context => func(&mut self.context),
367 Navigate::History => func(&mut self.history),
368 Navigate::Marks(_) => func(&mut self.marks),
369 Navigate::TempMarks(_) => func(&mut self.temp_marks),
370 Navigate::Shortcut => func(&mut self.shortcut),
371 Navigate::Trash => func(&mut self.trash),
372 Navigate::TuiApplication => func(&mut self.tui_applications),
373 Navigate::Cloud => func(&mut self.cloud),
374 Navigate::Picker => func(&mut self.picker),
375 Navigate::Flagged => func(&mut self.flagged),
376 }
377 }
378
379 fn apply_method<F, T>(&self, navigate: Navigate, func: F) -> T
380 where
381 F: FnOnce(&dyn Selectable) -> T,
382 {
383 match navigate {
384 Navigate::CliApplication => func(&self.cli_applications),
385 Navigate::Compress => func(&self.compression),
386 Navigate::Mount => func(&self.mount),
387 Navigate::Context => func(&self.context),
388 Navigate::History => func(&self.history),
389 Navigate::Marks(_) => func(&self.marks),
390 Navigate::TempMarks(_) => func(&self.temp_marks),
391 Navigate::Shortcut => func(&self.shortcut),
392 Navigate::Trash => func(&self.trash),
393 Navigate::TuiApplication => func(&self.tui_applications),
394 Navigate::Cloud => func(&self.cloud),
395 Navigate::Picker => func(&self.picker),
396 Navigate::Flagged => func(&self.flagged),
397 }
398 }
399
400 pub fn draw_navigate(&self, f: &mut Frame, rect: &Rect, navigate: Navigate) {
408 match navigate {
409 Navigate::Compress => self.compression.draw_menu(f, rect, &self.window),
410 Navigate::Shortcut => self.shortcut.draw_menu(f, rect, &self.window),
411 Navigate::Marks(_) => self.marks.draw_menu(f, rect, &self.window),
412 Navigate::TuiApplication => self.tui_applications.draw_menu(f, rect, &self.window),
413 Navigate::CliApplication => self.cli_applications.draw_menu(f, rect, &self.window),
414 Navigate::Mount => self.mount.draw_menu(f, rect, &self.window),
415 _ => unreachable!("{navigate} requires more information to be displayed."),
416 }
417 }
418
419 pub fn input_history_next(&mut self, tab: &mut Tab) -> Result<()> {
422 if !self.input_history.is_mode_logged(&tab.menu_mode) {
423 return Ok(());
424 }
425 self.input_history.next();
426 self.input_history_replace(tab)
427 }
428
429 pub fn input_history_prev(&mut self, tab: &mut Tab) -> Result<()> {
432 if !self.input_history.is_mode_logged(&tab.menu_mode) {
433 return Ok(());
434 }
435 self.input_history.prev();
436 self.input_history_replace(tab)
437 }
438
439 fn input_history_replace(&mut self, tab: &mut Tab) -> Result<()> {
440 let Some(history_element) = self.input_history.current() else {
441 return Ok(());
442 };
443 self.input.replace(history_element.content());
444 self.input_complete(tab)?;
445 Ok(())
446 }
447
448 pub fn delete_mark(&mut self, old_path: &std::path::Path) -> Result<()> {
449 crate::log_info!("Remove mark {old_path:?}");
450 self.temp_marks.remove_path(old_path);
451 self.marks.remove_path(old_path)
452 }
453
454 impl_navigate_from_char!(shortcut_from_char, shortcut);
455 impl_navigate_from_char!(context_from_char, context);
456 impl_navigate_from_char!(tui_applications_from_char, tui_applications);
457 impl_navigate_from_char!(cli_applications_from_char, cli_applications);
458 impl_navigate_from_char!(compression_method_from_char, compression);
459}