summaryrefslogtreecommitdiffstats
path: root/src/app/status.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/status.rs')
-rw-r--r--src/app/status.rs995
1 files changed, 995 insertions, 0 deletions
diff --git a/src/app/status.rs b/src/app/status.rs
new file mode 100644
index 0000000..823aaa5
--- /dev/null
+++ b/src/app/status.rs
@@ -0,0 +1,995 @@
+use std::fs;
+use std::path::Path;
+use std::sync::Arc;
+
+use anyhow::{anyhow, Context, Result};
+use clap::Parser;
+use skim::SkimItem;
+use sysinfo::{Disk, RefreshKind, System, SystemExt};
+use tuikit::prelude::{from_keyname, Event};
+use tuikit::term::Term;
+
+use crate::app::ClickableLine;
+use crate::app::Footer;
+use crate::app::Header;
+use crate::app::InternalSettings;
+use crate::app::Session;
+use crate::app::Tab;
+use crate::common::{args_is_empty, is_sudo_command, path_to_string};
+use crate::common::{current_username, disk_space, filename_from_path, is_program_in_path};
+use crate::config::Bindings;
+use crate::io::Internal;
+use crate::io::Kind;
+use crate::io::Opener;
+use crate::io::MIN_WIDTH_FOR_DUAL_PANE;
+use crate::io::{execute_and_capture_output_with_path, Args};
+use crate::io::{
+ execute_and_capture_output_without_check, execute_sudo_command_with_password,
+ reset_sudo_faillock,
+};
+use crate::modes::CopyMove;
+use crate::modes::Display;
+use crate::modes::Edit;
+use crate::modes::FileKind;
+use crate::modes::InputSimple;
+use crate::modes::IsoDevice;
+use crate::modes::Menu;
+use crate::modes::MountCommands;
+use crate::modes::MountRepr;
+use crate::modes::NeedConfirmation;
+use crate::modes::PasswordKind;
+use crate::modes::PasswordUsage;
+use crate::modes::Permissions;
+use crate::modes::Preview;
+use crate::modes::SelectableContent;
+use crate::modes::ShellCommandParser;
+use crate::modes::Skimer;
+use crate::modes::Tree;
+use crate::modes::Users;
+use crate::modes::{copy_move, regex_matcher};
+use crate::modes::{BlockDeviceAction, Navigate};
+use crate::{log_info, log_line};
+
+pub enum Window {
+ Header,
+ Files,
+ Menu,
+ Footer,
+}
+
+/// Holds every mutable parameter of the application itself, except for
+/// the "display" information.
+/// It holds 2 tabs (left & right), even if only one can be displayed sometimes.
+/// It knows which tab is selected, which files are flagged,
+/// which jump target is selected, a cache of normal file colors,
+/// if we have to display one or two tabs and if all details are shown or only
+/// the filename.
+/// Mutation of this struct are mostly done externally, by the event crate :
+/// `crate::event_exec`.
+pub struct Status {
+ /// Vector of `Tab`, each of them are displayed in a separate tab.
+ pub tabs: [Tab; 2],
+ /// Index of the current selected tab
+ pub index: usize,
+
+ skimer: Option<Result<Skimer>>,
+
+ /// Navigable menu
+ pub menu: Menu,
+ /// Display settings
+ pub display_settings: Session,
+ /// Interna settings
+ pub internal_settings: InternalSettings,
+}
+
+impl Status {
+ /// Creates a new status for the application.
+ /// It requires most of the information (arguments, configuration, height
+ /// of the terminal, the formated help string).
+ pub fn new(height: usize, term: Arc<Term>, opener: Opener) -> Result<Self> {
+ let skimer = None;
+ let index = 0;
+
+ let args = Args::parse();
+ let path = std::fs::canonicalize(Path::new(&args.path))?;
+ let start_dir = if path.is_dir() {
+ &path
+ } else {
+ path.parent().context("")?
+ };
+ let sys = System::new_with_specifics(RefreshKind::new().with_disks());
+ let display_settings = Session::new(term.term_size()?.0);
+ let mut internal_settings = InternalSettings::new(opener, term, sys);
+ let mount_points = internal_settings.mount_points();
+ let menu = Menu::new(start_dir, &mount_points)?;
+
+ let users_left = Users::new();
+ let users_right = users_left.clone();
+
+ let tabs = [
+ Tab::new(&args, height, users_left)?,
+ Tab::new(&args, height, users_right)?,
+ ];
+ Ok(Self {
+ tabs,
+ index,
+ skimer,
+ menu,
+ display_settings,
+ internal_settings,
+ })
+ }
+
+ /// Returns a non mutable reference to the selected tab.
+ pub fn current_tab(&self) -> &Tab {
+ &self.tabs[self.index]
+ }
+
+ /// Returns a mutable reference to the selected tab.
+ pub fn current_tab_mut(&mut self) -> &mut Tab {
+ &mut self.tabs[self.index]
+ }
+
+ /// Returns a string representing the current path in the selected tab.
+ pub fn current_tab_path_str(&self) -> String {
+ self.current_tab().directory_str()
+ }
+
+ /// True if a quit event was registered in the selected tab.
+ pub fn must_quit(&self) -> bool {
+ self.internal_settings.must_quit
+ }
+
+ /// Select the other tab if two are displayed. Does nother otherwise.
+ pub fn next(&mut self) {
+ if !self.display_settings.dual() {
+ return;
+ }
+ self.index = 1 - self.index
+ }
+
+ /// Select the other tab if two are displayed. Does nother otherwise.
+ pub fn prev(&mut self) {
+ self.next()
+ }
+
+ /// Select the left or right tab depending on where the user clicked.
+ pub fn select_tab_from_col(&mut self, col: u16) -> Result<()> {
+ let (width, _) = self.term_size()?;
+ if self.display_settings.dual() {
+ if (col as usize) < width / 2 {
+ self.select_left();
+ } else {
+ self.select_right();
+ };
+ } else {
+ self.select_left();
+ }
+ Ok(())
+ }
+
+ pub fn window_from_row(&self, row: u16, height: usize) -> Window {
+ let win_height = if matches!(self.current_tab().edit_mode, Edit::Nothing) {
+ height
+ } else {
+ height / 2
+ };
+ log_info!("clicked row {row}, height {height}, win_height {win_height}");
+ let w_index = row as usize / win_height;
+ if w_index == 1 {
+ Window::Menu
+ } else if row == 1 {
+ Window::Header
+ } else if row as usize == win_height - 2 {
+ Window::Footer
+ } else {
+ Window::Files
+ }
+ }
+
+ pub fn click(&mut self, row: u16, col: u16, binds: &Bindings) -> Result<()> {
+ let window = self.window_from_row(row, self.term_size()?.1);
+ self.select_tab_from_col(col)?;
+ let (_, current_height) = self.term_size()?;
+ match window {
+ Window::Menu => self.menu_action(row, current_height),
+ Window::Header => self.header_action(col, binds)?,
+ Window::Footer => self.footer_action(col, binds)?,
+ Window::Files => {
+ self.current_tab_mut().select_row(row, current_height)?;
+ self.update_second_pane_for_preview()?;
+ }
+ };
+ Ok(())
+ }
+
+ fn menu_action(&mut self, row: u16, height: usize) {
+ let second_window_height = height / 2 + (height % 2);
+ let offset = row as usize - second_window_height;
+ if offset >= 4 {
+ let index = offset - 4;
+ match self.current_tab().edit_mode {
+ Edit::Navigate(navigate) => match navigate {
+ Navigate::Bulk => self.menu.bulk_set_index(index),
+ Navigate::CliApplication => self.menu.cli_applications.set_index(index),
+ Navigate::Compress => self.menu.compression.set_index(index),
+ Navigate::Context => self.menu.context.set_index(index),
+ Navigate::EncryptedDrive => self.menu.encrypted_devices.set_index(index),
+ Navigate::History => self.current_tab_mut().history.set_index(index),
+ Navigate::Jump => self.menu.flagged.set_index(index),
+ Navigate::Marks(_) => self.menu.marks.set_index(index),
+ Navigate::RemovableDevices => self.menu.removable_set_index(index),
+ Navigate::Shortcut => self.menu.shortcut.set_index(index),
+ Navigate::Trash => self.menu.trash.set_index(index),
+ Navigate::TuiApplication => self.menu.tui_applications.set_index(index),
+ },
+ Edit::InputCompleted(_) => self.menu.completion.set_index(index),
+ _ => (),
+ }
+ }
+ }
+
+ pub fn select_left(&mut self) {
+ self.index = 0;
+ }
+
+ pub fn select_right(&mut self) {
+ self.index = 1;
+ }
+
+ /// Refresh every disk information.
+ /// It also refreshes the disk list, which is usefull to detect removable medias.
+ /// It may be very slow...
+ /// There's surelly a better way, like doing it only once in a while or on
+ /// demand.
+ pub fn refresh_shortcuts(&mut self) {
+ self.menu
+ .refresh_shortcuts(&self.internal_settings.mount_points());
+ }
+
+ /// Returns an array of Disks
+ pub fn disks(&self) -> &[Disk] {
+ self.internal_settings.sys.disks()
+ }
+
+ /// Returns a the disk spaces for the selected tab..
+ pub fn disk_spaces_of_selected(&self) -> String {
+ disk_space(self.disks(), self.current_tab().current_path())
+ }
+
+ /// Returns the sice of the terminal (width, height)
+ pub fn term_size(&self) -> Result<(usize, usize)> {
+ self.internal_settings.term_size()
+ }
+
+ /// Refresh the current view, reloading the files. Move the selection to top.
+ pub fn refresh_view(&mut self) -> Result<()> {
+ self.menu.encrypted_devices.update()?;
+ self.refresh_status()?;
+ self.update_second_pane_for_preview()
+ }
+
+ /// Reset the view of every tab.
+ pub fn reset_tabs_view(&mut self) -> Result<()> {
+ for tab in self.tabs.iter_mut() {
+ tab.refresh_and_reselect_file()?
+ }
+ Ok(())
+ }
+
+ fn reset_edit_mode(&mut self) {
+ self.menu.completion.reset();
+ self.current_tab_mut().reset_edit_mode();
+ }
+ /// Refresh the existing users.
+
+ /// Reset the selected tab view to the default.
+ pub fn refresh_status(&mut self) -> Result<()> {
+ self.force_clear();
+ self.refresh_users()?;
+ self.refresh_tabs()?;
+ Ok(())
+ }
+
+ /// Set a "force clear" flag to true, which will reset the display.
+ /// It's used when some command or whatever may pollute the terminal.
+ /// We ensure to clear it before displaying again.
+ pub fn force_clear(&mut self) {
+ self.internal_settings.force_clear();
+ }
+
+ pub fn refresh_users(&mut self) -> Result<()> {
+ let users = Users::new();
+ self.tabs[0].users = users.clone();
+ self.tabs[1].users = users;
+ Ok(())
+ }
+
+ pub fn refresh_tabs(&mut self) -> Result<()> {
+ self.menu.input.reset();
+ self.menu.completion.reset();
+ self.tabs[0].refresh_and_reselect_file()?;
+ self.tabs[1].refresh_and_reselect_file()
+ }
+
+ /// When a rezise event occurs, we may hide the second panel if the width
+ /// isn't sufficiant to display enough information.
+ /// We also need to know the new height of the terminal to start scrolling
+ /// up or down.
+ pub fn resize(&mut self, width: usize, height: usize) -> Result<()> {
+ self.set_dual_pane_if_wide_enough(width)?;
+ self.tabs[0].set_height(height);
+ self.tabs[1].set_height(height);
+ self.refresh_status()
+ }
+
+ /// Drop the current tree, replace it with an empty one.
+ pub fn remove_tree(&mut self) -> Result<()> {
+ self.current_tab_mut().tree = Tree::default();
+ Ok(())
+ }
+
+ /// Check if the second pane should display a preview and force it.
+ pub fn update_second_pane_for_preview(&mut self) -> Result<()> {
+ if self.index == 0 && self.display_settings.preview() {
+ self.set_second_pane_for_preview()?;
+ };
+ Ok(())
+ }
+
+ /// Force preview the selected file of the first pane in the second pane.
+ /// Doesn't check if it has do.
+ pub fn set_second_pane_for_preview(&mut self) -> Result<()> {
+ if !Session::display_wide_enough(self.term_size()?.0) {
+ self.tabs[1].preview = Preview::empty();
+ return Ok(());
+ }
+
+ self.tabs[1].set_display_mode(Display::Preview);
+ self.tabs[1].set_edit_mode(Edit::Nothing);
+ let fileinfo = self.tabs[0]
+ .current_file()
+ .context("force preview: No file to select")?;
+ let preview = match fileinfo.file_kind {
+ FileKind::Directory => Preview::directory(&fileinfo, &self.tabs[0].users),
+ _ => Preview::file(&fileinfo),
+ };
+ self.tabs[1].preview = preview.unwrap_or_default();
+
+ self.tabs[1].window.reset(self.tabs[1].preview.len());
+ Ok(())
+ }
+
+ /// Set dual pane if the term is big enough
+ pub fn set_dual_pane_if_wide_enough(&mut self, width: usize) -> Result<()> {
+ if width < MIN_WIDTH_FOR_DUAL_PANE {
+ self.select_left();
+ self.display_settings.set_dual(false);
+ } else {
+ self.display_settings.set_dual(true);
+ }
+ Ok(())
+ }
+
+ /// Empty the flagged files, reset the view of every tab.
+ pub fn clear_flags_and_reset_view(&mut self) -> Result<()> {
+ self.menu.flagged.clear();
+ self.reset_tabs_view()
+ }
+
+ /// Returns a vector of path of files which are both flagged and in current
+ /// directory.
+ /// It's necessary since the user may have flagged files OUTSIDE of current
+ /// directory before calling Bulkrename.
+ /// It may be confusing since the same filename can be used in
+ /// different places.
+ pub fn flagged_in_current_dir(&self) -> Vec<&Path> {
+ self.menu
+ .flagged
+ .in_current_dir(&self.current_tab().directory.path)
+ }
+
+ /// Flag all files in the current directory.
+ pub fn flag_all(&mut self) {
+ self.tabs[self.index]
+ .directory
+ .content
+ .iter()
+ .for_each(|file| {
+ self.menu.flagged.push(file.path.to_path_buf());
+ });
+ }
+
+ /// Reverse every flag in _current_ directory. Flagged files in other
+ /// directory aren't affected.
+ pub fn reverse_flags(&mut self) {
+ self.tabs[self.index]
+ .directory
+ .content
+ .iter()
+ .for_each(|file| self.menu.flagged.toggle(&file.path));
+ }
+
+ /// Flag the selected file if any
+ pub fn toggle_flag_for_selected(&mut self) {
+ let tab = self.current_tab();
+
+ if matches!(tab.edit_mode, Edit::Nothing) && !matches!(tab.display_mode, Display::Preview) {
+ let Ok(file) = tab.current_file() else {
+ return;
+ };
+ self.menu.flagged.toggle(&file.path);
+ self.current_tab_mut().normal_down_one_row();
+ };
+ }
+
+ /// Execute a move or a copy of the flagged files to current directory.
+ /// A progress bar is displayed (invisible for small files) and a notification
+ /// is sent every time, even for 0 bytes files...
+ pub fn cut_or_copy_flagged_files(&mut self, cut_or_copy: CopyMove) -> Result<()> {
+ let sources = self.menu.flagged.content.clone();
+
+ let dest = &self.current_tab().directory_of_selected()?;
+
+ copy_move(
+ cut_or_copy,
+ sources,
+ dest,
+ Arc::clone(&self.internal_settings.term),
+ )?;
+ self.clear_flags_and_reset_view()
+ }
+
+ fn skim_init(&mut self) {
+ self.skimer = Some(Skimer::new(Arc::clone(&self.internal_settings.term)));
+ }
+
+ /// Replace the tab content with the first result of skim.
+ /// It calls skim, reads its output, then update the tab content.
+ pub fn skim_output_to_tab(&mut self) -> Result<()> {
+ self.skim_init();
+ let _ = self._skim_output_to_tab();
+ self.drop_skim()
+ }
+
+ fn _skim_output_to_tab(&mut self) -> Result<()> {
+ let Some(Ok(skimer)) = &self.skimer else {
+ return Ok(());
+ };
+ let skim = skimer.search_filename(&self.current_tab().directory_str());
+ let Some(output) = skim.first() else {
+ return Ok(());
+ };
+ self._update_tab_from_skim_output(output)
+ }
+
+ /// Replace the tab content with the first result of skim.
+ /// It calls skim, reads its output, then update the tab content.
+ /// The output is splited at `:` since we only care about the path, not the line number.
+ pub fn skim_line_output_to_tab(&mut self) -> Result<()> {
+ self.skim_init();
+ let _ = self._skim_line_output_to_tab();
+ self.drop_skim()
+ }
+
+ fn _skim_line_output_to_tab(&mut self) -> Result<()> {
+ let Some(Ok(skimer)) = &self.skimer else {
+ return Ok(());
+ };
+ let skim = skimer.search_line_in_file(&self.current_tab().directory_str());
+ let Some(output) = skim.first() else {
+ return Ok(());
+ };
+ self._update_tab_from_skim_line_output(output)
+ }
+
+ /// Run a command directly from help.
+ /// Search a command in skim, if it's a keybinding, run it directly.
+ /// If the result can't be parsed, nothing is done.
+ pub fn skim_find_keybinding_and_run(&mut self, help: String) -> Result<()> {
+ self.skim_init();
+ if let Ok(key) = self._skim_find_keybinding(help) {
+ let _ = self.internal_settings.term.send_event(Event::Key(key));
+ };
+ self.drop_skim()
+ }
+
+ fn _skim_find_keybinding(&mut self, help: String) -> Result<tuikit::prelude::Key> {
+ let Some(Ok(skimer)) = &mut self.skimer else {
+ return Err(anyhow!("Skim isn't initialised"));
+ };
+ let skim = skimer.search_in_text(&help);
+ let Some(output) = skim.first() else {
+ return Err(anyhow!("Skim hasn't sent anything"));
+ };
+ let line = output.output().into_owned();
+ let Some(keybind) = line.split(':').next() else {
+ return Err(anyhow!("No keybind found"));
+ };
+ let Some(keyname) = parse_keyname(keybind) else {
+ return Err(anyhow!("No keyname found for {keybind}"));
+ };
+ let Some(key) = from_keyname(&keyname) else {
+ return Err(anyhow!("{keyname} isn't a valid Key name."));
+ };
+ Ok(key)
+ }
+
+ fn _update_tab_from_skim_line_output(&mut self, skim_output: &Arc<dyn SkimItem>) -> Result<()> {
+ let output_str = skim_output.output().to_string();
+ let Some(filename) = output_str.split(':').next() else {
+ return Ok(());
+ };
+ let path = fs::canonicalize(filename)?;
+ self._replace_path_by_skim_output(path)
+ }
+
+ fn _update_tab_from_skim_output(&mut self, skim_output: &Arc<dyn SkimItem>) -> Result<()> {
+ let path = fs::canonicalize(skim_output.output().to_string())?;
+ self._replace_path_by_skim_output(path)
+ }
+
+ fn _replace_path_by_skim_output(&mut self, path: std::path::PathBuf) -> Result<()> {
+ let tab = self.current_tab_mut();
+ if path.is_file() {
+ let Some(parent) = path.parent() else {
+ return Ok(());
+ };
+ tab.cd(parent)?;
+ let filename = filename_from_path(&path)?;
+ tab.search_from(filename, 0);
+ } else if path.is_dir() {
+ tab.cd(&path)?;
+ }
+ Ok(())
+ }
+
+ fn drop_skim(&mut self) -> Result<()> {
+ self.skimer = None;
+ Ok(())
+ }
+
+ pub fn execute_bulk(&self) -> Result<()> {
+ if let Some(bulk) = &self.menu.bulk {
+ bulk.execute_bulk(self)?;
+ }
+ Ok(())
+ }
+
+ pub fn input_regex(&mut self, char: char) -> Result<()> {
+ self.menu.input.insert(char);
+ self.select_from_regex()?;
+ Ok(())
+ }
+
+ /// Flag every file matching a typed regex.
+ pub fn select_from_regex(&mut self) -> Result<()> {
+ let input = self.menu.input.string();
+ if input.is_empty() {
+ return Ok(());
+ }
+ let paths = match self.current_tab().display_mode {
+ Display::Directory => self.tabs[self.index].directory.paths(),
+ Display::Tree => self.tabs[self.index].tree.paths(),
+ Display::Preview => return Ok(()),
+ };
+ regex_matcher(&input, &paths, &mut self.menu.flagged)?;
+ Ok(())
+ }
+
+ /// Open a the selected file with its opener
+ pub fn open_selected_file(&mut self) -> Result<()> {
+ let path = self.current_tab().current_file()?.path;
+ match self.internal_settings.opener.kind(&path) {
+ Some(Kind::Internal(Internal::NotSupported)) => {
+ let _ = self.mount_iso_drive();
+ }
+ Some(_) => {
+ let _ = self.internal_settings.opener.open_single(&path);
+ }
+ None => (),
+ }
+ Ok(())
+ }
+
+ /// Open every flagged file with their respective opener.
+ pub fn open_flagged_files(&mut self) -> Result<()> {
+ self.internal_settings
+ .opener
+ .open_multiple(self.menu.flagged.content())
+ }
+
+ fn ensure_iso_device_is_some(&mut self) -> Result<()> {
+ if self.menu.iso_device.is_none() {
+ let path = path_to_string(&self.current_tab().current_file()?.path);
+ self.menu.iso_device = Some(IsoDevice::from_path(path));
+ }
+ Ok(())
+ }
+
+ /// Mount the currently selected file (which should be an .iso file) to
+ /// `/run/media/$CURRENT_USER/fm_iso`
+ /// Ask a sudo password first if needed. It should always be the case.
+ fn mount_iso_drive(&mut self) -> Result<()> {
+ if !self.menu.password_holder.has_sudo() {
+ self.ask_password(Some(BlockDeviceAction::MOUNT), PasswordUsage::ISO)?;
+ } else {
+ self.ensure_iso_device_is_some()?;
+ let Some(ref mut iso_device) = self.menu.iso_device else {
+ return Ok(());
+ };
+ if iso_device.mount(&current_username()?, &mut self.menu.password_holder)? {
+ log_info!("iso mounter mounted {iso_device:?}");
+ log_line!("iso : {}", iso_device.as_string()?);
+ let path = iso_device.mountpoints.clone().context("no mount point")?;
+ self.current_tab_mut().cd(&path)?;
+ };
+ self.menu.iso_device = None;
+ };
+
+ Ok(())
+ }
+
+ /// Currently unused.
+ /// Umount an iso device.
+ pub fn umount_iso_drive(&mut self) -> Result<()> {
+ if let Some(ref mut iso_device) = self.menu.iso_device {
+ if !self.menu.password_holder.has_sudo() {
+ self.ask_password(Some(BlockDeviceAction::UMOUNT), PasswordUsage::ISO)?;
+ } else {
+ iso_device.umount(&current_username()?, &mut self.menu.password_holder)?;
+ };
+ }
+ Ok(())
+ }
+
+ /// Mount the selected encrypted device. Will ask first for sudo password and
+ /// passphrase.
+ /// Those passwords are always dropped immediatly after the commands are run.
+ pub fn mount_encrypted_drive(&mut self) -> Result<()> {
+ let Some(device) = self.menu.encrypted_devices.selected() else {
+ return Ok(());
+ };
+ if device.is_mounted() {
+ return Ok(());
+ }
+ if !self.menu.password_holder.has_sudo() {
+ self.ask_password(
+ Some(BlockDeviceAction::MOUNT),
+ PasswordUsage::CRYPTSETUP(PasswordKind::SUDO),
+ )
+ } else if !self.menu.password_holder.has_cryptsetup() {
+ self.ask_password(
+ Some(BlockDeviceAction::MOUNT),
+ PasswordUsage::CRYPTSETUP(PasswordKind::CRYPTSETUP),
+ )
+ } else {
+ self.menu
+ .encrypted_devices
+ .mount_selected(&mut self.menu.password_holder)
+ }
+ }
+
+ /// Move to the selected crypted device mount point.
+ pub fn go_to_encrypted_drive(&mut self) -> Result<()> {
+ let Some(path) = self.menu.find_encrypted_drive_mount_point() else {
+ return Ok(());
+ };
+ let tab = self.current_tab_mut();
+ tab.cd(&path)?;
+ tab.refresh_view()
+ }
+
+ /// Unmount the selected device.
+ /// Will ask first for a sudo password which is immediatly forgotten.
+ pub fn umount_encrypted_drive(&mut self) -> Result<()> {
+ let Some(device) = self.menu.encrypted_devices.selected() else {
+ return Ok(());
+ };
+ if !device.is_mounted() {
+ return Ok(());
+ }
+ if !self.menu.password_holder.has_sudo() {
+ self.ask_password(
+ Some(BlockDeviceAction::UMOUNT),
+ PasswordUsage::CRYPTSETUP(PasswordKind::SUDO),
+ )
+ } else {
+ self.menu
+ .encrypted_devices
+ .umount_selected(&mut self.menu.password_holder)
+ }
+ }
+
+ pub fn go_to_removable(&mut self) -> Result<()> {
+ let Some(path) = self.menu.find_removable_mount_point() else {
+ return Ok(());
+ };
+ self.current_tab_mut().cd(&path)?;
+ self.current_tab_mut().refresh_view()
+ }
+
+ pub fn parse_shell_command(&mut self) -> Result<bool> {
+ let shell_command = self.menu.input.string();
+ let mut args = ShellCommandParser::new(&shell_command).compute(self)?;
+ log_info!("command {shell_command} args: {args:?}");
+ if args_is_empty(&args) {
+ self.current_tab_mut().set_edit_mode(Edit::Nothing);
+ return Ok(true);
+ }
+ let executable = args.remove(0);
+ if is_sudo_command(&executable) {
+ self.menu.sudo_command = Some(shell_command);
+ self.ask_password(None, PasswordUsage::SUDOCOMMAND)?;
+ Ok(false)
+ } else {
+ if !is_program_in_path(&executable) {
+ return Ok(true);
+ }
+ let current_directory = self.current_tab().directory_of_selected()?.to_owned();
+ let params: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
+ if let Ok(output) =
+ execute_and_capture_output_with_path(executable, current_directory, &params)
+ {
+ self.preview_command_output(output, shell_command);
+ }
+ // self.current_tab_mut().set_edit_mode(Edit::Nothing);
+ Ok(true)
+ }
+ }
+
+ /// Ask for a password of some kind (sudo or device passphrase).
+ fn ask_password(
+ &mut self,
+ encrypted_action: Option<BlockDeviceAction>,
+ password_dest: PasswordUsage,
+ ) -> Result<()> {
+ log_info!("event ask password");
+ self.current_tab_mut()
+ .set_edit_mode(Edit::InputSimple(InputSimple::Password(
+ encrypted_action,
+ password_dest,
+ )));
+ Ok(())
+ }
+
+ pub fn execute_password_command(&mut self) -> Result<()> {
+ match self.current_tab().edit_mode {
+ Edit::InputSimple(InputSimple::Password(action, dest)) => {
+ self._execute_password_command(action, dest)?;
+ }
+ _ => {
+ return Err(anyhow!(
+ "execute_password_command: edit_mode should be `InputSimple::Password`"
+ ))
+ }
+ }
+ Ok(())
+ }
+
+ fn _execute_password_command(
+ &mut self,
+ action: Option<BlockDeviceAction>,
+ dest: PasswordUsage,
+ ) -> Result<()> {
+ let password = self.menu.input.string();
+ self.menu.input.reset();
+ match dest {
+ PasswordUsage::CRYPTSETUP(PasswordKind::CRYPTSETUP) => {
+ self.menu.password_holder.set_cryptsetup(password)
+ }
+ _ => self.menu.password_holder.set_sudo(password),
+ };
+ self.reset_edit_mode();
+ self.dispatch_password(dest, action)
+ }
+
+ /// Execute a new mark, saving it to a config file for futher use.
+ pub fn marks_new(&mut self, c: char) -> Result<()> {
+ let path = self.current_tab_mut().directory.path.clone();
+ self.menu.marks.new_mark(c, &path)?;
+ {
+ let tab: &mut Tab = self.current_tab_mut();
+ tab.refresh_view()
+ }?;
+ self.reset_edit_mode();
+ self.refresh_status()
+ }
+
+ /// Execute a jump to a mark, moving to a valid path.
+ /// If the saved path is invalid, it does nothing but reset the view.
+ pub fn marks_jump_char(&mut self, c: char) -> Result<()> {
+ if let Some(path) = self.menu.marks.get(c) {
+ self.current_tab_mut().cd(&path)?;
+ }
+ self.current_tab_mut().refresh_view()?;
+ self.reset_edit_mode();
+ self.refresh_status()
+ }
+
+ /// Recursively delete all flagged files.
+ pub fn confirm_delete_files(&mut self) -> Result<()> {
+ self.menu.delete_flagged_files()?;
+ self.reset_edit_mode();
+ self.clear_flags_and_reset_view()?;
+ self.refresh_status()
+ }
+
+ /// Empty the trash folder permanently.
+ pub fn confirm_trash_empty(&mut self) -> Result<()> {
+ self.menu.trash.empty_trash()?;
+ self.reset_edit_mode();
+ self.clear_flags_and_reset_view()?;
+ Ok(())
+ }
+
+ fn run_sudo_command(&mut self) -> Result<()> {
+ self.current_tab_mut().set_edit_mode(Edit::Nothing);
+ reset_sudo_faillock()?;
+ let Some(sudo_command) = self.menu.sudo_command.to_owned() else {
+ return self.menu.clear_sudo_attributes();
+ };
+ let args = ShellCommandParser::new(&sudo_command).compute(self)?;
+ if args.is_empty() {
+ return self.menu.clear_sudo_attributes();
+ }
+ let (_, output, _) = execute_sudo_command_with_password(
+ &args[1..],
+ self.menu
+ .password_holder
+ .sudo()
+ .as_ref()
+ .context("sudo password isn't set")?,
+ self.current_tab().directory_of_selected()?,
+ )?;
+ self.menu.clear_sudo_attributes()?;
+ self.preview_command_output(output, sudo_command.to_owned());
+ // self.refresh_status()
+ Ok(())
+ }
+
+ pub fn dispatch_password(
+ &mut self,
+ dest: PasswordUsage,
+ action: Option<BlockDeviceAction>,
+ ) -> Result<()> {
+ match dest {
+ PasswordUsage::ISO => match action {
+ Some(BlockDeviceAction::MOUNT) => self.mount_iso_drive(),
+ Some(BlockDeviceAction::UMOUNT) => self.umount_iso_drive(),
+ None => Ok(()),
+ },
+ PasswordUsage::CRYPTSETUP(_) => match action {
+ Some(BlockDeviceAction::MOUNT) => self.mount_encrypted_drive(),
+ Some(BlockDeviceAction::UMOUNT) => self.umount_encrypted_drive(),
+ None => Ok(()),
+ },
+ PasswordUsage::SUDOCOMMAND => self.run_sudo_command(),
+ }
+ }
+
+ pub fn preview_command_output(&mut self, output: String, command: String) {
+ log_info!("output {output}");
+ self.current_tab_mut().reset_edit_mode();
+ self.current_tab_mut().set_display_mode(Display::Preview);
+ let preview = Preview::cli_info(&output, command);
+ self.current_tab_mut().window.reset(preview.len());
+ self.current_tab_mut().preview = preview;
+ }
+
+ pub fn update_nvim_listen_address(&mut self) {
+ self.internal_settings.update_nvim_listen_address()
+ }
+
+ /// Execute a command requiring a confirmation (Delete, Move or Copy).
+ /// The action is only executed if the user typed the char `y`
+ pub fn confirm(&mut self, c: char, confirmed_action: NeedConfirmation) -> Result<()> {
+ if c == 'y' {
+ let _ = self.match_confirmed_mode(confirmed_action);
+ }
+ self.reset_edit_mode();
+ self.current_tab_mut().refresh_view()?;
+
+ Ok(())
+ }
+
+ fn match_confirmed_mode(&mut self, confirmed_action: NeedConfirmation) -> Result<()> {
+ match confirmed_action {
+ NeedConfirmation::Delete => self.confirm_delete_files(),
+ NeedConfirmation::Move => self.cut_or_copy_flagged_files(CopyMove::Move),
+ NeedConfirmation::Copy => self.cut_or_copy_flagged_files(CopyMove::Copy),
+ NeedConfirmation::EmptyTrash => self.confirm_trash_empty(),
+ }
+ }
+
+ pub fn header_action(&mut self, col: u16, binds: &Bindings) -> Result<()> {
+ if matches!(self.current_tab().display_mode, Display::Preview) {
+ return Ok(());
+ }
+ let is_right = self.index == 1;
+ let header = Header::new(self, self.current_tab())?;
+ let action = header.action(col as usize, is_right);
+ action.matcher(self, binds)
+ }
+
+ pub fn footer_action(&mut self, col: u16, binds: &Bindings) -> Result<()> {
+ log_info!("footer clicked col {col}");
+ if matches!(self.current_tab().display_mode, Display::Preview) {
+ return Ok(());
+ }
+ let is_right = self.inde