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 delete_flagged_files(&mut self) -> Result<()> {
216 let nb = self.flagged.len();
217 let output_socket = Args::parse().output_socket;
218 for pathbuf in self.flagged.content.iter() {
219 if pathbuf.is_dir() {
220 std::fs::remove_dir_all(pathbuf)?;
221 } else {
222 std::fs::remove_file(pathbuf)?;
223 }
224 if let Some(output_socket) = &output_socket {
225 nvim_inform_ipc(output_socket, NvimIPCAction::DELETE(pathbuf))?;
226 }
227 }
228 self.flagged.clear();
229 log_line!("Deleted {nb} flagged files");
230 Ok(())
231 }
232
233 pub fn clear_sudo_attributes(&mut self) -> Result<()> {
235 self.password_holder.reset();
236 drop_sudo_privileges()?;
237 self.sudo_command = None;
238 Ok(())
239 }
240
241 pub fn input_insert(&mut self, char: char) -> Result<()> {
243 self.input.insert(char);
244 Ok(())
245 }
246
247 pub fn refresh_shortcuts(
250 &mut self,
251 mount_points: &[&std::path::Path],
252 left_path: &std::path::Path,
253 right_path: &std::path::Path,
254 ) {
255 self.shortcut.refresh(mount_points, left_path, right_path)
256 }
257
258 pub fn completion_reset(&mut self) {
259 self.completion.reset();
260 }
261
262 pub fn completion_tab(&mut self) {
263 self.input.replace(self.completion.current_proposition())
264 }
265
266 pub fn len(&self, menu_mode: Menu) -> usize {
267 match menu_mode {
268 Menu::Navigate(navigate) => self.apply_method(navigate, |variant| variant.len()),
269 Menu::InputCompleted(_) => self.completion.len(),
270 Menu::NeedConfirmation(need_confirmation) if need_confirmation.use_flagged_files() => {
271 self.flagged.len()
272 }
273 Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => self.trash.len(),
274 Menu::NeedConfirmation(NeedConfirmation::BulkAction) => self.bulk.len(),
275 _ => 0,
276 }
277 }
278
279 pub fn index(&self, menu_mode: Menu) -> usize {
280 match menu_mode {
281 Menu::Navigate(navigate) => self.apply_method(navigate, |variant| variant.index()),
282 Menu::InputCompleted(_) => self.completion.index,
283 Menu::NeedConfirmation(need_confirmation) if need_confirmation.use_flagged_files() => {
284 self.flagged.index()
285 }
286 Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => self.trash.index(),
287 Menu::NeedConfirmation(NeedConfirmation::BulkAction) => self.bulk.index(),
288 _ => 0,
289 }
290 }
291
292 pub fn page_down(&mut self, navigate: Navigate) {
293 for _ in 0..10 {
294 self.next(navigate)
295 }
296 }
297
298 pub fn page_up(&mut self, navigate: Navigate) {
299 for _ in 0..10 {
300 self.prev(navigate)
301 }
302 }
303
304 pub fn completion_prev(&mut self, input_completed: InputCompleted) {
305 self.completion.prev();
306 self.window
307 .scroll_to(self.index(Menu::InputCompleted(input_completed)));
308 }
309
310 pub fn completion_next(&mut self, input_completed: InputCompleted) {
311 self.completion.next();
312 self.window
313 .scroll_to(self.index(Menu::InputCompleted(input_completed)));
314 }
315
316 pub fn next(&mut self, navigate: Navigate) {
317 self.apply_method_mut(navigate, |variant| variant.next());
318 self.window.scroll_to(self.index(Menu::Navigate(navigate)));
319 }
320
321 pub fn prev(&mut self, navigate: Navigate) {
322 self.apply_method_mut(navigate, |variant| variant.prev());
323 self.window.scroll_to(self.index(Menu::Navigate(navigate)));
324 }
325
326 pub fn set_index(&mut self, index: usize, navigate: Navigate) {
327 self.apply_method_mut(navigate, |variant| variant.set_index(index));
328 self.window.scroll_to(self.index(Menu::Navigate(navigate)))
329 }
330
331 pub fn select_last(&mut self, navigate: Navigate) {
332 let index = self.len(Menu::Navigate(navigate)).saturating_sub(1);
333 self.set_index(index, navigate);
334 }
335
336 fn apply_method_mut<F, T>(&mut self, navigate: Navigate, func: F) -> T
337 where
338 F: FnOnce(&mut dyn Selectable) -> T,
339 {
340 match navigate {
341 Navigate::CliApplication => func(&mut self.cli_applications),
342 Navigate::Compress => func(&mut self.compression),
343 Navigate::Mount => func(&mut self.mount),
344 Navigate::Context => func(&mut self.context),
345 Navigate::History => func(&mut self.history),
346 Navigate::Marks(_) => func(&mut self.marks),
347 Navigate::TempMarks(_) => func(&mut self.temp_marks),
348 Navigate::Shortcut => func(&mut self.shortcut),
349 Navigate::Trash => func(&mut self.trash),
350 Navigate::TuiApplication => func(&mut self.tui_applications),
351 Navigate::Cloud => func(&mut self.cloud),
352 Navigate::Picker => func(&mut self.picker),
353 Navigate::Flagged => func(&mut self.flagged),
354 }
355 }
356
357 fn apply_method<F, T>(&self, navigate: Navigate, func: F) -> T
358 where
359 F: FnOnce(&dyn Selectable) -> T,
360 {
361 match navigate {
362 Navigate::CliApplication => func(&self.cli_applications),
363 Navigate::Compress => func(&self.compression),
364 Navigate::Mount => func(&self.mount),
365 Navigate::Context => func(&self.context),
366 Navigate::History => func(&self.history),
367 Navigate::Marks(_) => func(&self.marks),
368 Navigate::TempMarks(_) => func(&self.temp_marks),
369 Navigate::Shortcut => func(&self.shortcut),
370 Navigate::Trash => func(&self.trash),
371 Navigate::TuiApplication => func(&self.tui_applications),
372 Navigate::Cloud => func(&self.cloud),
373 Navigate::Picker => func(&self.picker),
374 Navigate::Flagged => func(&self.flagged),
375 }
376 }
377
378 pub fn draw_navigate(&self, f: &mut Frame, rect: &Rect, navigate: Navigate) {
386 match navigate {
387 Navigate::Compress => self.compression.draw_menu(f, rect, &self.window),
388 Navigate::Shortcut => self.shortcut.draw_menu(f, rect, &self.window),
389 Navigate::Marks(_) => self.marks.draw_menu(f, rect, &self.window),
390 Navigate::TuiApplication => self.tui_applications.draw_menu(f, rect, &self.window),
391 Navigate::CliApplication => self.cli_applications.draw_menu(f, rect, &self.window),
392 Navigate::Mount => self.mount.draw_menu(f, rect, &self.window),
393 _ => unreachable!("{navigate} requires more information to be displayed."),
394 }
395 }
396
397 pub fn input_history_next(&mut self, tab: &mut Tab) -> Result<()> {
400 if !self.input_history.is_mode_logged(&tab.menu_mode) {
401 return Ok(());
402 }
403 self.input_history.next();
404 self.input_history_replace(tab)
405 }
406
407 pub fn input_history_prev(&mut self, tab: &mut Tab) -> Result<()> {
410 if !self.input_history.is_mode_logged(&tab.menu_mode) {
411 return Ok(());
412 }
413 self.input_history.prev();
414 self.input_history_replace(tab)
415 }
416
417 fn input_history_replace(&mut self, tab: &mut Tab) -> Result<()> {
418 let Some(history_element) = self.input_history.current() else {
419 return Ok(());
420 };
421 self.input.replace(history_element.content());
422 self.input_complete(tab)?;
423 Ok(())
424 }
425
426 impl_navigate_from_char!(shortcut_from_char, shortcut);
427 impl_navigate_from_char!(context_from_char, context);
428 impl_navigate_from_char!(tui_applications_from_char, tui_applications);
429 impl_navigate_from_char!(cli_applications_from_char, cli_applications);
430 impl_navigate_from_char!(compression_method_from_char, compression);
431}