use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::{
mpsc::{self, Sender, TryRecvError},
Arc,
};
use anyhow::{bail, Context, Result};
use clap::Parser;
use crossterm::event::{Event, KeyEvent};
use opendal::EntryMode;
use parking_lot::lock_api::Mutex;
use parking_lot::RawMutex;
use ratatui::layout::Size;
use sysinfo::Disks;
use walkdir::WalkDir;
use crate::app::{
ClickableLine, Footer, Header, InternalSettings, PreviewResponse, Previewer, Session, Tab,
ThumbnailManager,
};
use crate::common::{
build_dest_path, current_username, disk_space, filename_from_path, is_in_path, is_sudo_command,
path_to_string, row_to_window_index, set_current_dir, tilde, MountPoint,
};
use crate::config::{from_keyname, Bindings, START_FOLDER};
use crate::event::{ActionMap, FmEvents};
use crate::io::{
build_tokio_greper, execute_and_capture_output, execute_sudo_command_with_password,
execute_without_output, get_cloud_token_names, google_drive, reset_sudo_faillock, Args,
Internal, Kind, Opener, MIN_WIDTH_FOR_DUAL_PANE,
};
use crate::modes::{
append_files_to_shell_command, copy_move, parse_line_output, regex_flagger,
shell_command_parser, Content, ContentWindow, CopyMove, CursorOffset,
Direction as FuzzyDirection, Display, FileInfo, FileKind, FilterKind, FuzzyFinder, FuzzyKind,
InputCompleted, InputSimple, IsoDevice, Menu, MenuHolder, MountAction, MountCommands,
Mountable, Navigate, NeedConfirmation, PasswordKind, PasswordUsage, Permissions, PickerCaller,
Preview, PreviewBuilder, ReEnterMenu, Search, Selectable, Users, SAME_WINDOW_TOKEN,
};
use crate::{log_info, log_line};
pub enum Window {
Header,
Files,
Menu,
Footer,
}
#[derive(Default, Clone, Copy, Debug)]
pub enum Focus {
#[default]
LeftFile,
LeftMenu,
RightFile,
RightMenu,
}
impl Focus {
pub fn is_left(&self) -> bool {
matches!(self, Self::LeftMenu | Self::LeftFile)
}
pub fn is_file(&self) -> bool {
matches!(self, Self::LeftFile | Self::RightFile)
}
pub fn switch(&self) -> Self {
match self {
Self::LeftFile => Self::RightFile,
Self::LeftMenu => Self::RightFile,
Self::RightFile => Self::LeftFile,
Self::RightMenu => Self::LeftFile,
}
}
pub fn to_parent(&self) -> Self {
match self {
Self::LeftFile | Self::LeftMenu => Self::LeftFile,
Self::RightFile | Self::RightMenu => Self::RightFile,
}
}
pub fn index(&self) -> usize {
*self as usize
}
pub fn tab_index(&self) -> usize {
self.index() >> 1
}
pub fn is_left_menu(&self) -> bool {
matches!(self, Self::LeftMenu)
}
}
pub enum Direction {
RightToLeft,
LeftToRight,
}
impl Direction {
const fn source_dest(self) -> (usize, usize) {
match self {
Self::RightToLeft => (1, 0),
Self::LeftToRight => (0, 1),
}
}
}
pub struct Status {
pub tabs: [Tab; 2],
pub index: usize,
pub fuzzy: Option<FuzzyFinder<String>>,
pub menu: MenuHolder,
pub session: Session,
pub internal_settings: InternalSettings,
pub focus: Focus,
pub fm_sender: Arc<Sender<FmEvents>>,
preview_receiver: mpsc::Receiver<PreviewResponse>,
pub previewer: Previewer,
pub thumbnail_manager: Option<ThumbnailManager>,
}
impl Status {
pub fn arc_mutex_new(
size: Size,
opener: Opener,
binds: &Bindings,
fm_sender: Arc<Sender<FmEvents>>,
) -> Result<Arc<Mutex<RawMutex, Self>>> {
Ok(Arc::new(Mutex::new(Self::new(
size, opener, binds, fm_sender,
)?)))
}
fn new(
size: Size,
opener: Opener,
binds: &Bindings,
fm_sender: Arc<Sender<FmEvents>>,
) -> Result<Self> {
let fuzzy = None;
let index = 0;
let args = Args::parse();
let path = &START_FOLDER.get().context("Start folder should be set")?;
let start_dir = if path.is_dir() {
path
} else {
path.parent().context("")?
};
let disks = Disks::new_with_refreshed_list();
let session = Session::new(size.width);
let internal_settings = InternalSettings::new(opener, size, disks);
let menu = MenuHolder::new(start_dir, binds)?;
let focus = Focus::default();
let users_left = Users::default();
let users_right = users_left.clone();
let height = size.height as usize;
let tabs = [
Tab::new(&args, height, users_left)?,
Tab::new(&args, height, users_right)?,
];
let (previewer_sender, preview_receiver) = mpsc::channel();
let previewer = Previewer::new(previewer_sender);
let thumbnail_manager = None;
Ok(Self {
tabs,
index,
fuzzy,
menu,
session,
internal_settings,
focus,
fm_sender,
preview_receiver,
previewer,
thumbnail_manager,
})
}
pub fn current_tab(&self) -> &Tab {
&self.tabs[self.index]
}
pub fn current_tab_mut(&mut self) -> &mut Tab {
&mut self.tabs[self.index]
}
pub fn current_tab_path_str(&self) -> String {
self.current_tab().directory_str()
}
pub fn must_quit(&self) -> bool {
self.internal_settings.must_quit
}
pub fn focus_follow_index(&mut self) {
if (self.index == 0 && !self.focus.is_left()) || (self.index == 1 && self.focus.is_left()) {
self.focus = self.focus.switch();
}
}
pub fn set_focus_from_mode(&mut self) {
if self.index == 0 {
if self.tabs[0].menu_mode.is_nothing() {
self.focus = Focus::LeftFile;
} else {
self.focus = Focus::LeftMenu;
}
} else if self.tabs[1].menu_mode.is_nothing() {
self.focus = Focus::RightFile;
} else {
self.focus = Focus::RightMenu;
}
}
pub fn next(&mut self) {
if !self.session.dual() {
return;
}
self.index = 1 - self.index;
self.focus_follow_index();
}
pub fn select_tab_from_col(&mut self, col: u16) -> Result<()> {
if self.session.dual() {
if col < self.term_width() / 2 {
self.select_left();
} else {
self.select_right();
};
} else {
self.select_left();
}
Ok(())
}
fn window_from_row(&self, row: u16, height: u16) -> Window {
let win_height = if self.current_tab().menu_mode.is_nothing() {
height
} else {
height / 2
};
let w_index = row / win_height;
if w_index == 1 {
Window::Menu
} else if row == 1 {
Window::Header
} else if row == win_height - 2 {
Window::Footer
} else {
Window::Files
}
}
pub fn set_focus_from_pos(&mut self, row: u16, col: u16) -> Result<Window> {
self.select_tab_from_col(col)?;
let window = self.window_from_row(row, self.term_size().height);
self.set_focus_from_window_and_index(&window);
Ok(window)
}
pub fn click(&mut self, binds: &Bindings, row: u16, col: u16) -> Result<()> {
let window = self.set_focus_from_pos(row, col)?;
self.click_action_from_window(&window, row, col, binds)?;
Ok(())
}
fn has_clicked_on_second_pane_preview(&self) -> bool {
self.session.dual() && self.session.preview() && self.index == 1
}
fn click_action_from_window(
&mut self,
window: &Window,
row: u16,
col: u16,
binds: &Bindings,
) -> Result<()> {
match window {
Window::Header => self.header_action(col, binds),
Window::Files => {
if self.has_clicked_on_second_pane_preview() {
if let Preview::Tree(tree) = &self.tabs[1].preview {
let index = row_to_window_index(row) + self.tabs[1].window.top;
let path = &tree.path_from_index(index)?;
self.select_file_from_right_tree(path)?;
}
} else {
self.tab_select_row(row)?;
self.update_second_pane_for_preview()?;
}
Ok(())
}
Window::Footer => self.footer_action(col, binds),
Window::Menu => self.menu_action(row, col),
}
}
pub fn enter_from_preview(&mut self) -> Result<()> {
if let Preview::Tree(tree) = &self.tabs[1].preview {
let index = tree.displayable().index();
let path = &tree.path_from_index(index)?;
self.select_file_from_right_tree(path)?;
} else {
let filepath = self.tabs[self.focus.tab_index()].preview.filepath();
if filepath.exists() {
self.open_single_file(&filepath)?;
}
}
Ok(())
}
fn select_file_from_right_tree(&mut self, path: &Path) -> Result<()> {
self.tabs[0].cd_to_file(path)?;
self.index = 0;
self.focus = Focus::LeftFile;
self.update_second_pane_for_preview()
}
pub fn tab_select_row(&mut self, row: u16) -> Result<()> {
match self.current_tab().display_mode {
Display::Directory => self.current_tab_mut().normal_select_row(row),
Display::Tree => self.current_tab_mut().tree_select_row(row)?,
Display::Fuzzy => self.fuzzy_navigate(FuzzyDirection::Index(row))?,
_ => (),
}
Ok(())
}
#[rustfmt::skip]
fn set_focus_from_window_and_index(&mut self, window: &Window) {
self.focus = match (self.index == 0, window) {
(true, Window::Menu) => Focus::LeftMenu,
(true, _) => Focus::LeftFile,
(false, Window::Menu) => Focus::RightMenu,
(false, _) => Focus::RightFile
};
}
pub fn sync_tabs(&mut self, direction: Direction) -> Result<()> {
let (source, dest) = direction.source_dest();
self.tabs[dest].cd(&self.tabs[source]
.selected_path()
.context("No selected path")?)
}
pub fn second_window_height(&self) -> Result<usize> {
let height = self.term_size().height;
Ok((height / 2).saturating_sub(2) as usize)
}
fn menu_action(&mut self, row: u16, col: u16) -> Result<()> {
let second_window_height = self.second_window_height()?;
let row_offset = (row as usize).saturating_sub(second_window_height);
const OFFSET: usize =
ContentWindow::WINDOW_PADDING + ContentWindow::WINDOW_MARGIN_TOP_U16 as usize;
if row_offset >= OFFSET {
let index = row_offset - OFFSET + self.menu.window.top;
match self.current_tab().menu_mode {
Menu::Navigate(navigate) => match navigate {
Navigate::History => self.current_tab_mut().history.set_index(index),
navigate => self.menu.set_index(index, navigate),
},
Menu::InputCompleted(input_completed) => {
self.menu.completion.set_index(index);
if matches!(input_completed, InputCompleted::Search) {
self.follow_search()?;
}
}
Menu::NeedConfirmation(need_confirmation)
if need_confirmation.use_flagged_files() =>
{
self.menu.flagged.set_index(index)
}
Menu::NeedConfirmation(NeedConfirmation::EmptyTrash) => {
self.menu.trash.set_index(index)
}
Menu::NeedConfirmation(NeedConfirmation::BulkAction) => {
self.menu.bulk.set_index(index)
}
_ => (),
}
self.menu.window.scroll_to(index);
} else if row_offset == 3 && self.current_tab().menu_mode.is_input() {
let index =
col.saturating_sub(self.current_tab().menu_mode.cursor_offset() + 1) as usize;
self.menu.input.cursor_move(index);
}
Ok(())
}
pub fn select_left(&mut self) {
self.index = 0;
self.focus_follow_index();
}
pub fn select_right(&mut self) {
self.index = 1;
self.focus_follow_index();
}
pub fn refresh_shortcuts(&mut self) {
self.menu.refresh_shortcuts(
&self.internal_settings.mount_points_vec(),
self.tabs[0].current_directory_path(),
self.tabs[1].current_directory_path(),
);
}
pub fn disk_spaces_of_selected(&self) -> String {
disk_space(
&self.internal_settings.disks,
self.current_tab().current_directory_path(),
)
}
pub fn term_size(&self) -> Size {
self.internal_settings.term_size()
}
pub fn term_width(&self) -> u16 {
self.term_size().width
}
pub fn clear_preview_right(&mut self) {
if self.session.dual() && self.session.preview() && !self.tabs[1].preview.is_empty() {
self.tabs[1].preview = PreviewBuilder::empty()
}
}
pub fn refresh_view(&mut self) -> Result<()> {
self.refresh_status()?;
self.update_second_pane_for_preview()
}
pub fn reset_tabs_view(&mut self) -> Result<()> {
for tab in self.tabs.iter_mut() {
match tab.refresh_and_reselect_file() {
Ok(()) => (),
Err(error) => log_info!("reset_tabs_view error: {error}"),
}
}
Ok(())
}
pub fn leave_menu_mode(&mut self) -> Result<()> {
match self.current_tab().menu_mode {
Menu::InputSimple(InputSimple::Filter) => {
self.current_tab_mut().settings.reset_filter()
}
Menu::InputCompleted(InputCompleted::Cd) => self.current_tab_mut().cd_origin_path()?,
_ => (),
}
if self.reset_menu_mode()? {
self.current_tab_mut().refresh_view()?;
} else {
self.current_tab_mut().refresh_params();
}
self.current_tab_mut().reset_visual();
Ok(())
}
pub fn leave_preview(&mut self) -> Result<()> {
self.current_tab_mut().set_display_mode(Display::Directory);
self.current_tab_mut().refresh_and_reselect_file()
}
pub fn reset_menu_mode(&mut self) -> Result<bool> {
if self.current_tab().menu_mode.is_picker() {
if let Some(PickerCaller::Menu(menu)) = self.menu.picker.caller {
menu.reenter(self)?;
return Ok(false);
}
}
self.menu.reset();
let must_refresh = matches!(self.current_tab().display_mode, Display::Preview);
self.set_menu_mode(self.index, Menu::Nothing)?;
self.set_height_of_unfocused_menu()?;
Ok(must_refresh)
}
fn set_height_of_unfocused_menu(&mut self) -> Result<()> {
let unfocused_tab = &self.tabs[1 - self.index];
match unfocused_tab.menu_mode {
Menu::Nothing => (),
unfocused_mode => {
let len = self.menu.len(unfocused_mode);
let height = self.second_window_height()?;
self.menu.window = ContentWindow::new(len, height);
}
}
Ok(())
}
pub fn refresh_status(&mut self) -> Result<()> {
self.force_clear();
self.refresh_users()?;
self.refresh_tabs()?;
self.refresh_shortcuts();
Ok(())
}
pub fn force_clear(&mut self) {
self.internal_settings.force_clear();
}
pub fn should_be_cleared(&self) -> bool {
self.internal_settings.should_be_cleared()
}
pub fn refresh_users(&mut self) -> Result<()> {
self.tabs[0].users.update();
self.tabs[1].users = self.tabs[0].users.clone();
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()
}
pub fn resize(&mut self, width: u16, height: u16) -> Result<()> {
let couldnt_dual_but_want = self.couldnt_dual_but_want();
self.internal_settings.update_size(width, height);
if couldnt_dual_but_want {
self.set_dual_pane_if_wide_enough()?;
}
if !self.use_dual() {
self.select_left();
}
self.resize_all_windows(height)?;
self.refresh_status()
}
fn wide_enough_for_dual(&self) -> bool {
self.term_width() >= MIN_WIDTH_FOR_DUAL_PANE
}
fn couldnt_dual_but_want(&self) -> bool {
!self.wide_enough_for_dual() && self.session.dual()
}
fn use_dual(&self) -> bool {
self.wide_enough_for_dual() && self.session.dual()
}
fn left_window_width(&self) -> u16 {
if self.use_dual() {
self.term_width() / 2
} else {
self.term_width()
}
}
fn resize_all_windows(&mut self, height: u16) -> Result<()> {
let height_usize = height as usize;
self.tabs[0].set_height(height_usize);
self.tabs[1].set_height(height_usize);
self.fuzzy_resize(height_usize);
self.menu.resize(
self.tabs[self.index].menu_mode,
self.second_window_height()?,
);
Ok(())
}
pub fn update_second_pane_for_preview(&mut self) -> Result<()> {
if self.are_settings_requiring_dualpane_preview() {
if self.can_display_dualpane_preview() {
self.set_second_pane_for_preview()?;
} else {
self.tabs[1].preview = PreviewBuilder::empty();
}
}
Ok(())
}
fn are_settings_requiring_dualpane_preview(&self) -> bool {
self.index == 0 && self.session.dual() && self.session.preview()
}
fn can_display_dualpane_preview(&self) -> bool {
Session::display_wide_enough(self.term_width())
}
fn set_second_pane_for_preview(&mut self) -> Result<()> {
self.tabs[1].set_display_mode(Display::Preview);
self.tabs[1].menu_mode = Menu::Nothing;
let Ok((Some(fileinfo), line_index)) = self.get_correct_fileinfo_for_preview() else {
return Ok(());
};
log_info!("sending preview request");
self.previewer
.build(fileinfo.path.to_path_buf(), 1, line_index)?;
Ok(())
}
pub fn thumbnail_directory_video(&mut self) {
if !self.are_settings_requiring_dualpane_preview() || !self.can_display_dualpane_preview() {
return;
}
self.thumbnail_init_or_clear();
let videos = self.current_tab().directory.videos();
if videos.is_empty() {
return;
}
if let Some(thumbnail_manager) = &self.thumbnail_manager {
thumbnail_manager.enqueue(videos);
}
}
fn thumbnail_init_or_clear(&mut self) {
if self.thumbnail_manager.is_none() {
self.thumbnail_manager_init();
} else {
self.thumbnail_queue_clear();
}
}
fn thumbnail_manager_init(&mut self) {
self.thumbnail_manager = Some(ThumbnailManager::default());
}
pub fn thumbnail_queue_clear(&self) {
if let Some(thumbnail_manager) = &self.thumbnail_manager {
thumbnail_manager.clear()
}
}
pub fn check_preview(&mut self) -> Result<()> {
match self.preview_receiver.try_recv() {
Ok(preview_response) => self.attach_preview(preview_response)?,
Err(TryRecvError::Disconnected) => bail!("Previewer Disconnected"),
Err(TryRecvError::Empty) => (),
}
Ok(())
}
fn attach_preview(&mut self, preview_response: PreviewResponse) -> Result<()> {
let PreviewResponse {
path,
tab_index,
line_nr,
preview,
} = preview_response;
let compared_index = self.pick_correct_tab_from(tab_index)?;
if !self.preview_has_correct_path(compared_index, path.as_path())? {
return Ok(());
}
let tab = &mut self.tabs[tab_index];
tab.preview = preview;
tab.window.reset(tab.preview.len());
if let Some(line_nr) = line_nr {
tab.window.scroll_to(line_nr);
}
Ok(())
}
fn pick_correct_tab_from(&self, index: usize) -> Result<usize> {
if index == 1 && self.can_display_dualpane_preview() && self.session.preview() {
Ok(0)
} else {
Ok(index)
}
}
fn preview_has_correct_path(&self, compared_index: usize, path: &Path) -> Result<bool> {
let tab = &self.tabs[compared_index];
Ok(tab.display_mode.is_fuzzy()
|| tab.menu_mode.is_navigate()
|| tab.selected_path().context("No selected path")?.as_ref() == path)
}
fn get_correct_fileinfo_for_preview(&self) -> Result<(Option<FileInfo>, Option<usize>)> {
let left_tab = &self.tabs[0];
let users = &left_tab.users;
if left_tab.display_mode.is_fuzzy() {
let (opt_path, line_nr) =
parse_line_output(&self.fuzzy_current_selection().context("No selection")?)?;
Ok((FileInfo::new(&opt_path, users).ok(), line_nr))
} else if self.focus.is_left_menu() {
Ok((self.fileinfo_from_navigate(left_tab, users), None))
} else {
Ok((left_tab.current_file().ok(), None))
}
}
fn fileinfo_from_navigate(&self, tab: &Tab, users: &Users) -> Option<FileInfo> {
match tab.menu_mode {
Menu::Navigate(Navigate::History) => {
FileInfo::new(tab.history.content().get(tab.history.index())?, users).ok()
}
Menu::Navigate(Navigate::Shortcut) => {
let short = &self.menu.shortcut;
FileInfo::new(short.content().get(short.index())?, users).ok()
}
Menu::Navigate(Navigate::Marks(_)) => {
let (_, mark_path) = &self.menu.marks.content().get(self.menu.marks.index())?;
FileInfo::new(mark_path, users).ok()
}
Menu::Navigate(Navigate::Flagged) => {
FileInfo::new(self.menu.flagged.selected()?, users).ok()
}
_ => tab.current_file().ok(),
}
}
pub fn should_tabs_images_be_cleared(&self) -> bool {
self.tabs[0].settings.should_clear_image || self.tabs[1].settings.should_clear_image
}
pub fn set_tabs_images_cleared(&mut self) {
self.tabs[0].settings.should_clear_image = false;
self.tabs[1].settings.should_clear_image = false;
}
pub fn set_menu_mode(&mut self, index: usize, menu_mode: Menu) -> Result<()> {
self.set_menu_mode_no_refresh(index, menu_mode)?;
self.current_tab_mut().reset_visual();
self.refresh_status()
}
pub fn set_menu_mode_no_refresh(&mut self, index: usize, menu_mode: Menu) -> Result<()> {
if index > 1 {
return Ok(());
}
self.set_height_for_menu_mode(index, menu_mode)?;
self.tabs[index].menu_mode = menu_mode;
let len = self.menu.len(menu_mode);
let height = self.second_window_height()?;
self.menu.window = ContentWindow::new(len, height);
self.menu.window.scroll_to(self.menu.index(menu_mode));
self.set_focus_from_mode();
self.menu.input_history.filter_by_mode(menu_mode);
Ok(())
}
pub fn set_height_for_menu_mode(&mut self, index: usize, menu_mode: Menu) -> Result<()> {
let height = self.term_size().height;
let prim_window_height = if menu_mode.is_nothing() {
height
} else {
height / 2
};
self.tabs[index]
.window
.set_height(prim_window_height as usize);
self.tabs[index]
.window
.scroll_to(self.tabs[index].window.top);
Ok(())
}
pub fn set_dual_pane_if_wide_enough(&mut self) -> Result<()> {
if self.wide_enough_for_dual() {
self.session.set_dual(true);
} else {
self.select_left();
self.session.set_dual(false);
}
Ok(())
}
pub fn clear_flags_and_reset_view(&mut self) -> Result<()> {
self.menu.flagged.clear();
self.reset_tabs_view()
}
fn flagged_or_selected(&self) -> Vec<PathBuf> {
if self.menu.flagged.is_empty() {
let Some(path) = self.current_tab().selected_path() else {
return vec![];
};
vec![path.to_path_buf()]
} else {
self.menu.flagged.content().to_owned()
}
}
fn flagged_or_selected_relative_to(&self, here: &Path) -> Vec<PathBuf> {
self.flagged_or_selected()
.iter()
.filter_map(|abs_path| pathdiff::diff_paths(abs_path, here))
.filter(|f| !f.starts_with(".."))
.collect()
}
pub fn flagged_in_current_dir(&self) -> Vec<PathBuf> {
self.menu.flagged.in_dir(&self.current_tab().directory.path)
}
pub fn flag_all(&mut self) {
match self.current_tab().display_mode {
Display::Directory => {
self.tabs[self.index]
.directory
.content
.iter()
.filter(|file| file.filename.as_ref() != "." && file.filename.as_ref() != "..")
.for_each(|file| {
self.menu.flagged.push(file.path.to_path_buf());
});
}
Display::Tree => self.tabs[self.index].tree.flag_all(&mut self.menu.flagged),
_ => (),
}
}
pub fn reverse_flags(&mut self) {
log_info!("Reverse flags");
if !self.current_tab().display_mode.is_preview() {
self.tabs[self.index]
.directory
.content
.iter()
.for_each(|file| self.menu.flagged.toggle(&file.path));
}
}
pub fn toggle_flag_for_children(&mut self) {
let Some(path) = self.current_tab().selected_path() else {
return;
};
match self.current_tab().display_mode {
Display::Directory => self.toggle_flag_for_selected(),
Display::Tree => {
if !path.is_dir() {
self.menu.flagged.toggle(&path);
} else {
for entry in WalkDir::new(&path).into_iter().filter_map(|e| e.ok()) {
let p = entry.path();
if !p.is_dir() {
self.menu.flagged.toggle(p);
}
}
}
let _ = self.update_second_pane_for_preview();
}
Display::Preview => (),
Display::Fuzzy => (),
}
}
pub fn toggle_flag_for_selected(&mut self) {
let Some(path) = self.current_tab().selected_path() else {
return;
};
match self.current_tab().display_mode {
Display::Directory => {
self.menu.flagged.toggle(&path);
if !self.current_tab().directory.selected_is_last() {
self.tabs[self.index].normal_down_one_row();
}
let _ = self.update_second_pane_for_preview();
}
Display::Tree => {
self.menu.flagged.toggle(&path);
if !self.current_tab().tree.selected_is_last() {
self.current_tab_mut().tree_select_next();
}
let _ = self.update_second_pane_for_preview();
}
Display::Preview => self.menu.flagged.toggle(&path),
Display::Fuzzy => (),
}
if matches!(
self.current_tab().menu_mode,
Menu::Navigate(Navigate::Flagged)
) {
self.menu.window.set_len(self.menu.flagged.len());
}
}
pub fn jump_flagged(&mut self) -> Result<()> {
let Some(path) = self.menu.flagged.selected() else {
return Ok(());
};
let path = path.to_owned();
let tab = self.current_tab_mut();
tab.set_display_mode(Display::Directory);
tab.refresh_view()?;
tab.jump(path)?;
self.update_second_pane_for_preview()
}
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()?.to_owned();
self.cut_or_copy_files(cut_or_copy, sources, dest)
}
fn cut_or_copy_files(
&mut self,
cut_or_copy: CopyMove,
mut sources: Vec<PathBuf>,
dest: &Path,
) -> Result<()> {
if matches!(cut_or_copy, CopyMove::Move) {
sources = Self::remove_subdir_of_dest(sources, dest);
if sources.is_empty() {
return Ok(());
}
}
if self.is_simple_move(&cut_or_copy, &sources, dest) {
self.simple_move(&sources, dest)
} else {
self.complex_move(cut_or_copy, sources, dest)
}
}
fn remove_subdir_of_dest(mut sources: Vec<PathBuf>, dest: &Path) -> Vec<PathBuf> {
for index in (0..sources.len()).rev() {
if sources[index].starts_with(dest) {
log_info!("Cannot move to a subdirectory of itself");
log_line!("Cannot move to a subdirectory of itself");
sources.remove(index);
}
}
sources
}
fn is_simple_move(&self, cut_or_copy: &CopyMove, sources: &[PathBuf], dest: &Path) -> bool {
if cut_or_copy.is_copy() || sources.is_empty() {
return false;
}
let mount_points = self.internal_settings.mount_points_set();
let Some(source_mount_point) = sources[0].mount_point(&mount_points) else {
return false;
};
for source in sources {
if source.mount_point(&mount_points) != Some(source_mount_point) {
return false;
}
}
Some(source_mount_point) == dest.mount_point(&mount_points)
}
fn simple_move(&mut self, sources: &[PathBuf], dest: &Path) -> Result<()> {
for source in sources {
let filename = filename_from_path(source)?;
let dest = dest.to_path_buf().join(filename);
log_info!("simple_move {source:?} -> {dest:?}");
match std::fs::rename(source, &dest) {
Ok(()) => {
log_line!(
"Moved {source} to {dest}",
source = source.display(),
dest = dest.display()
);
self.rename_marks(source, dest)?;
}
Err(e) => {
log_info!("Error: {e:?}");
log_line!("Error: {e:?}")
}
}
}
self.clear_flags_and_reset_view()
}
fn complex_move(
&mut self,
cut_or_copy: CopyMove,
sources: Vec<PathBuf>,
dest: &Path,
) -> Result<()> {
let mut must_act_now = true;
if matches!(cut_or_copy, CopyMove::Copy) {
if !self.internal_settings.copy_file_queue.is_empty() {
log_info!("cut_or_copy_flagged_files: act later");
must_act_now = false;
}
self.internal_settings
.copy_file_queue
.push((sources.to_owned(), dest.to_path_buf()));
}
if must_act_now {
log_info!("cut_or_copy_flagged_files: act now");
let in_mem = copy_move(
cut_or_copy,
sources,
dest,
self.left_window_width(),
self.term_size().height,
Arc::clone(&self.fm_sender),
)?;
self.internal_settings.store_copy_progress(in_mem);
}
self.clear_flags_and_reset_view()
}
pub fn copy_next_file_in_queue(&mut self) -> Result<()> {
self.internal_settings
.copy_next_file_in_queue(self.fm_sender.clone(), self.left_window_width())
}
pub fn paste_input(&mut self, pasted: &str) -> Result<()> {
if self.focus.is_file() && self.current_tab().display_mode.is_fuzzy() {
let Some(fuzzy) = &mut self.fuzzy else {
return Ok(());
};
fuzzy.input.insert_string(pasted);
} else if !self.focus.is_file() && self.current_tab().menu_mode.is_input() {
self.menu.input.insert_string(pasted);
}
Ok(())
}
pub fn paste_pathes(&mut self, pasted: &str) -> Result<()> {
for pasted in pasted.split_whitespace() {
self.paste_path(pasted)?;
}
Ok(())
}
fn paste_path(&mut self, pasted: &str) -> Result<()> {
let pasted = Path::new(&pasted);
if !pasted.is_absolute() {
log_info!("pasted {pasted} isn't absolute.", pasted = pasted.display());
return Ok(());
}
if !pasted.exists() {
log_info!("pasted {pasted} doesn't exist.", pasted = pasted.display());
return Ok(());
}
let dest = self.current_tab().root_path().to_path_buf();
let Some(dest_filename) = build_dest_path(pasted, &dest) else {
return Ok(());
};
if dest_filename == pasted {
log_info!("pasted is same directory.");
return Ok(());
}
if dest_filename.exists() {
log_info!(
"pasted {dest_filename} already exists",
dest_filename = dest_filename.display()
);
return Ok(());
}
let sources = vec![pasted.to_path_buf()];
log_info!("pasted copy {sources:?} to {dest:?}");
if self.is_simple_move(&CopyMove::Move, &sources, &dest) {
self.simple_move(&sources, &dest)
} else {
self.complex_move(CopyMove::Copy, sources, &dest)
}
}
pub fn fuzzy_init(&mut self, kind: FuzzyKind) {
self.fuzzy = Some(FuzzyFinder::new(kind).set_height(self.current_tab().window.height));
}
fn fuzzy_drop(&mut self) {
self.fuzzy = None;
}
pub fn fuzzy_find_files(&mut self) -> Result<()> {
let Some(fuzzy) = &self.fuzzy else {
bail!("Fuzzy should be set");
};
let current_path = self.current_tab().current_directory_path().to_path_buf();
fuzzy.find_files(current_path);
Ok(())
}
pub fn fuzzy_help(&mut self, help: String) -> Result<()> {
let Some(fuzzy) = &self.fuzzy else {
bail!("Fuzzy should be set");
};
fuzzy.find_action(help);
Ok(())
}
pub fn fuzzy_find_lines(&mut self) -> Result<()> {
let Some(fuzzy) = &self.fuzzy else {
bail!("Fuzzy should be set");
};
let Some(tokio_greper) = build_tokio_greper() else {
log_info!("ripgrep & grep aren't in $PATH");
return Ok(());
};
fuzzy.find_line(tokio_greper);
Ok(())
}
fn fuzzy_current_selection(&self) -> Option<std::string::String> {
if let Some(fuzzy) = &self.fuzzy {
fuzzy.pick()
} else {
None
}
}
pub fn fuzzy_select(&mut self) -> Result<()> {
let Some(fuzzy) = &self.fuzzy else {
bail!("Fuzzy should be set");
};
if let Some(pick) = fuzzy.pick() {
match fuzzy.kind {
FuzzyKind::File => self.tabs[self.index].cd_to_file(Path::new(&pick))?,
FuzzyKind::Line => {
self.tabs[self.index].cd_to_file(&parse_line_output(&pick)?.0)?
}
FuzzyKind::Action => self.fuzzy_send_event(&pick)?,
}
} else {
log_info!("Fuzzy had nothing to pick from");
};
self.fuzzy_leave()
}
pub fn fuzzy_toggle_flag_selected(&mut self) -> Result<()> {
let Some(fuzzy) = &self.fuzzy else {
bail!("Fuzzy should be set");
};
if let Some(pick) = fuzzy.pick() {
if let FuzzyKind::File = fuzzy.kind {
self.menu.flagged.toggle(Path::new(&pick));
self.fuzzy_navigate(FuzzyDirection::Down)?;
}
} else {
log_info!("Fuzzy had nothing to select from");
};
Ok(())
}
pub fn fuzzy_open_file(&mut self) -> Result<()> {
let Some(fuzzy) = &self.fuzzy else {
bail!("Fuzzy should be set");
};
if let Some(pick) = fuzzy.pick() {
match fuzzy.kind {
FuzzyKind::File => {
self.open_single_file(Path::new(&pick))?;
}
FuzzyKind::Line => {
let (path, _) = parse_line_output(&pick)?;
self.open_single_file(&path)?;
}
_ => (),
}
} else {
log_info!("Fuzzy had nothing to select from");
};
Ok(())
}
fn fuzzy_send_event(&self, pick: &str) -> Result<()> {
if let Ok(key) = find_keybind_from_fuzzy(pick) {
self.fm_sender.send(FmEvents::Term(Event::Key(key)))?;
};
Ok(())
}
pub fn fuzzy_leave(&mut self) -> Result<()> {
self.fuzzy_drop();
self.current_tab_mut().set_display_mode(Display::Directory);
self.refresh_view()
}
pub fn fuzzy_backspace(&mut self) -> Result<()> {
let Some(fuzzy) = &mut self.fuzzy else {
bail!("Fuzzy should be set");
};
fuzzy.input.delete_char_left();
fuzzy.update_input(false);
Ok(())
}
pub fn fuzzy_delete(&mut self) -> Result<()> {
let Some(fuzzy) = &mut self.fuzzy else {
bail!("Fuzzy should be set");
};
fuzzy.input.delete_chars_right();
fuzzy.update_input(false);
Ok(())
}
pub fn fuzzy_left(&mut self) -> Result<()> {
let Some(fuzzy) = &mut self.fuzzy else {
bail!("Fuzzy should be set");
};
fuzzy.input.cursor_left();
Ok(())
}
pub fn fuzzy_right(&mut self) -> Result<()> {
let Some(fuzzy) = &mut self.fuzzy else {
bail!("Fuzzy should be set");
};
fuzzy.input.cursor_right();
Ok(())
}
pub fn fuzzy_start(&mut self) -> Result<()> {
self.fuzzy_navigate(FuzzyDirection::Start)
}
pub fn fuzzy_end(&mut self) -> Result<()> {
self.fuzzy_navigate(FuzzyDirection::End)
}
pub fn fuzzy_navigate(&mut self, direction: FuzzyDirection) -> Result<()> {
let Some(fuzzy) = &mut self.fuzzy else {
bail!("Fuzzy should be set");
};
fuzzy.navigate(direction);
if fuzzy.should_preview() {
self.update_second_pane_for_preview()?;
}
Ok(())
}
pub fn fuzzy_tick(&mut self) {
if let Some(fuzzy) = &mut self.fuzzy {
fuzzy.tick(false);
}
}
pub fn fuzzy_resize(&mut self, height: usize) {
if let Some(fuzzy) = &mut self.fuzzy {
fuzzy.resize(height)
}
}
pub fn input_history_next(&mut self) -> Result<()> {
if self.focus.is_file() {
return Ok(());
}
self.menu.input_history_next(&mut self.tabs[self.index])?;
if let Menu::InputCompleted(input_completed) = self.current_tab().menu_mode {
self.complete(input_completed)?;
}
Ok(())
}
pub fn input_history_prev(&mut self) -> Result<()> {
if self.focus.is_file() {
return Ok(());
}
self.menu.input_history_prev(&mut self.tabs[self.index])?;
if let Menu::InputCompleted(input_completed) = self.current_tab().menu_mode {
self.complete(input_completed)?;
}
Ok(())
}
pub fn input_and_complete(&mut self, input_completed: InputCompleted, c: char) -> Result<()> {
self.menu.input.insert(c);
self.complete(input_completed)
}
pub fn complete(&mut self, input_completed: InputCompleted) -> Result<()> {
match input_completed {
InputCompleted::Search => self.complete_search(),
_ => self.complete_non_search(),
}
}
pub fn complete_tab(&mut self, input_completed: InputCompleted) -> Result<()> {
self.menu.completion_tab();
self.complete_cd_move()?;
if matches!(input_completed, InputCompleted::Search) {
self.update_search()?;
self.search()?;
} else {
self.menu.input_complete(&mut self.tabs[self.index])?
}
Ok(())
}
fn complete_search(&mut self) -> Result<()> {
self.update_search()?;
self.search()?;
self.menu.input_complete(&mut self.tabs[self.index])
}
fn update_search(&mut self) -> Result<()> {
if let Ok(search) = Search::new(&self.menu.input.string()) {
self.current_tab_mut().search = search;
};
Ok(())
}
pub fn follow_search(&mut self) -> Result<()> {
let Some(proposition) = self.menu.completion.selected() else {
return Ok(());
};
let current_path = match self.current_tab().display_mode {
Display::Directory => self.current_tab().current_directory_path(),
Display::Tree => self.current_tab().tree.root_path(),
_ => {
return Ok(());
}
};
let mut full_path = current_path.to_path_buf();
full_path.push(proposition);
self.current_tab_mut().select_by_path(Arc::from(full_path));
Ok(())
}
fn complete_non_search(&mut self) -> Result<()> {
self.complete_cd_move()?;
self.menu.input_complete(&mut self.tabs[self.index])
}
pub fn complete_cd_move(&mut self) -> Result<()> {
if let Menu::InputCompleted(InputCompleted::Cd) = self.current_tab().menu_mode {
let input = self.menu.input.string();
if self.tabs[self.index].try_cd_to_file(input)? {
self.update_second_pane_for_preview()?;
}
}
Ok(())
}
pub fn input_regex(&mut self, char: char) -> Result<()> {
self.menu.input.insert(char);
self.flag_from_regex()?;
Ok(())
}
pub fn flag_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(),
_ => return Ok(()),
};
regex_flagger(&input, &paths, &mut self.menu.flagged)?;
if !self.menu.flagged.is_empty() {
self.tabs[self.index]
.go_to_file(self.menu.flagged.selected().context("no selected file")?);
}
Ok(())
}
pub fn open_selected_file(&mut self) -> Result<()> {
let path = self
.current_tab()
.selected_path()
.context("No selected path")?;
self.open_single_file(&path)
}
pub fn open_single_file(&mut self, path: &Path) -> Result<()> {
match self.internal_settings.opener.kind(path) {
Some(Kind::Internal(Internal::NotSupported)) => self.mount_iso_drive(),
Some(_) => self.internal_settings.open_single_file(path),
None => Ok(()),
}
}
pub fn open_flagged_files(&mut self) -> Result<()> {
self.internal_settings
.open_flagged_files(&self.menu.flagged)
}
fn ensure_iso_device_is_some(&mut self) -> Result<()> {
if self.menu.iso_device.is_none() {
let path = path_to_string(
&self
.current_tab()
.selected_path()
.context("No selected path")?,
);
self.menu.iso_device = Some(IsoDevice::from_path(path));
}
Ok(())
}
fn mount_iso_drive(&mut self) -> Result<()> {
if !self.menu.password_holder.has_sudo() {
self.ask_password(Some(MountAction::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(¤t_username()?, &mut self.menu.password_holder)? {
log_info!("iso mounter mounted {iso_device:?}");
log_line!("iso : {iso_device}");
let path = iso_device
.mountpoints
.clone()
.expect("mountpoint should be set");
self.current_tab_mut().cd(Path::new(&path))?;
};
self.menu.iso_device = None;
};
Ok(())
}
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(MountAction::UMOUNT), PasswordUsage::ISO)?;
} else {
iso_device.umount(¤t_username()?, &mut self.menu.password_holder)?;
};
}
self.menu.iso_device = None;
Ok(())
}
pub fn mount_encrypted_drive(&mut self) -> Result<()> {
if !self.menu.password_holder.has_sudo() {
self.ask_password(
Some(MountAction::MOUNT),
PasswordUsage::CRYPTSETUP(PasswordKind::SUDO),
)
} else if !self.menu.password_holder.has_cryptsetup() {
self.ask_password(
Some(MountAction::MOUNT),
PasswordUsage::CRYPTSETUP(PasswordKind::CRYPTSETUP),
)
} else {
let Some(Mountable::Encrypted(device)) = &self.menu.mount.selected() else {
return Ok(());
};
if let Ok(true) = device.mount(¤t_username()?, &mut self.menu.password_holder) {
self.go_to_encrypted_drive(device.uuid.clone())?;
}
Ok(())
}
}
pub fn umount_encrypted_drive(&mut self) -> Result<()> {
if !self.menu.password_holder.has_sudo() {
self.ask_password(
Some(MountAction::UMOUNT),
PasswordUsage::CRYPTSETUP(PasswordKind::SUDO),
)
} else {
let Some(Mountable::Encrypted(device)) = &self.menu.mount.selected() else {
log_info!("Cannot find Encrypted device");
return Ok(());
};
let success =
device.umount_close_crypto(¤t_username()?, &mut self.menu.password_holder)?;
log_info!("umount_encrypted_drive: {success}");
Ok(())
}
}
pub fn mount_normal_device(&mut self) -> Result<()> {
let Some(device) = self.menu.mount.selected() else {
return Ok(());
};
if device.is_mounted() {
return Ok(());
}
if device.is_crypto() {
return self.mount_encrypted_drive();
}
let Ok(success) = self.menu.mount.mount_selected_no_password() else {
return Ok(());
};
if success {
self.menu.mount.update(self.internal_settings.disks())?;
self.go_to_normal_drive()?;
return Ok(());
}
if !self.menu.password_holder.has_sudo() {
self.ask_password(Some(MountAction::MOUNT), PasswordUsage::DEVICE)
} else {
if let Ok(true) = self
.menu
.mount
.mount_selected(&mut self.menu.password_holder)
{
self.go_to_normal_drive()?;
}
Ok(())
}
}
pub fn go_to_normal_drive(&mut self) -> Result<()> {
let Some(path) = self.menu.mount.selected_mount_point() else {
return Ok(());
};
let tab = self.current_tab_mut();
tab.cd(&path)?;
tab.refresh_view()
}
pub fn go_to_mount_per_index(&mut self, c: char) -> Result<()> {
let Some(index) = c.to_digit(10) else {
return Ok(());
};
self.menu.mount.set_index(index.saturating_sub(1) as _);
self.go_to_normal_drive()
}
fn go_to_encrypted_drive(&mut self, uuid: Option<String>) -> Result<()> {
self.menu.mount.update(&self.internal_settings.disks)?;
let Some(mountpoint) = self.menu.mount.find_encrypted_by_uuid(uuid) else {
return Ok(());
};
log_info!("mountpoint {mountpoint}");
let tab = self.current_tab_mut();
tab.cd(Path::new(&mountpoint))?;
tab.refresh_view()
}
pub fn umount_normal_device(&mut self) -> Result<()> {
let Some(device) = self.menu.mount.selected() else {
return Ok(());
};
if !device.is_mounted() {
return Ok(());
}
if device.is_crypto() {
return self.umount_encrypted_drive();
}
let Ok(success) = self.menu.mount.umount_selected_no_password() else {
return Ok(());
};
if success {
self.menu.mount.update(self.internal_settings.disks())?;
return Ok(());
}
if !self.menu.password_holder.has_sudo() {
self.ask_password(Some(MountAction::UMOUNT), PasswordUsage::DEVICE)
} else {
self.menu
.mount
.umount_selected(&mut self.menu.password_holder)
}
}
pub fn eject_removable_device(&mut self) -> Result<()> {
if self.menu.mount.is_empty() {
return Ok(());
}
let success = self.menu.mount.eject_removable_device()?;
if success {
self.menu.mount.update(self.internal_settings.disks())?;
}
Ok(())
}
pub fn execute_shell_command_from_input(&mut self) -> Result<bool> {
let shell_command = self.menu.input.string();
self.execute_shell_command(shell_command, None, true)
}
pub fn execute_shell_command(
&mut self,
shell_command: String,
files: Option<Vec<String>>,
capture_output: bool,
) -> Result<bool> {
let command = append_files_to_shell_command(shell_command, files);
let Ok(args) = shell_command_parser(&command, self) else {
self.set_menu_mode(self.index, Menu::Nothing)?;
return Ok(true);
};
self.execute_parsed_command(args, command, capture_output)
}
fn execute_parsed_command(
&mut self,
mut args: Vec<String>,
shell_command: String,
capture_output: bool,
) -> Result<bool> {
let executable = args.remove(0);
if is_sudo_command(&executable) {
self.enter_sudo_mode(shell_command)?;
return Ok(false);
}
let params: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
if executable == *SAME_WINDOW_TOKEN {
self.internal_settings.open_in_window(¶ms)?;
return Ok(true);
}
if !is_in_path(&executable) {
log_line!("{executable} isn't in path.");
return Ok(true);
}
if capture_output {
match execute_and_capture_output(executable, ¶ms) {
Ok(output) => self.preview_command_output(output, shell_command),
Err(e) => {
log_info!("Error {e:?}");
log_line!("Command {shell_command} disn't finish properly");
}
}
Ok(true)
} else {
let _ = execute_without_output(executable, ¶ms);
Ok(true)
}
}
fn enter_sudo_mode(&mut self, shell_command: String) -> Result<()> {
self.menu.sudo_command = Some(shell_command);
self.ask_password(None, PasswordUsage::SUDOCOMMAND)?;
Ok(())
}
fn ask_password(
&mut self,
encrypted_action: Option<MountAction>,
password_dest: PasswordUsage,
) -> Result<()> {
log_info!("ask_password");
self.set_menu_mode(
self.index,
Menu::InputSimple(InputSimple::Password(encrypted_action, password_dest)),
)
}
pub fn execute_password_command(
&mut self,
action: Option<MountAction>,
dest: PasswordUsage,
) -> Result<()> {
let password = self.menu.input.string();
self.menu.input.reset();
if matches!(dest, PasswordUsage::CRYPTSETUP(PasswordKind::CRYPTSETUP)) {
self.menu.password_holder.set_cryptsetup(password)
} else {
self.menu.password_holder.set_sudo(password)
};
let sudo_command = self.menu.sudo_command.to_owned();
self.reset_menu_mode()?;
self.dispatch_password(action, dest, sudo_command)?;
Ok(())
}
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)?;
self.current_tab_mut().refresh_view()?;
self.reset_menu_mode()?;
self.refresh_status()
}
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_menu_mode()?;
self.refresh_status()
}
pub fn temp_marks_new(&mut self, c: char) -> Result<()> {
let Some(index) = c.to_digit(10) else {
return Ok(());
};
let path = self.current_tab_mut().directory.path.to_path_buf();
self.menu.temp_marks.set_mark(index as _, path);
self.current_tab_mut().refresh_view()?;
self.reset_menu_mode()?;
self.refresh_status()
}
pub fn temp_marks_erase(&mut self) -> Result<()> {
self.menu.temp_marks.erase_current_mark();
Ok(())
}
pub fn temp_marks_jump_char(&mut self, c: char) -> Result<()> {
let Some(index) = c.to_digit(10) else {
return Ok(());
};
if let Some(path) = self.menu.temp_marks.get_mark(index as _) {
self.tabs[self.index].cd(path)?;
}
self.current_tab_mut().refresh_view()?;
self.reset_menu_mode()?;
self.refresh_status()
}
pub fn confirm_delete_files(&mut self) -> Result<()> {
if self.menu.flagged.contains(self.current_tab().root_path()) {
log_info!("Can't delete current root path");
log_line!("Can't delete current root path");
return Ok(());
}
self.menu.delete_flagged_files()?;
self.reset_menu_mode()?;
self.clear_flags_and_reset_view()?;
self.refresh_status()
}
pub fn confirm_trash_empty(&mut self) -> Result<()> {
self.menu.trash.empty_trash()?;
self.reset_menu_mode()?;
self.clear_flags_and_reset_view()?;
Ok(())
}
pub fn bulk_ask_filenames(&mut self) -> Result<()> {
let flagged = self.flagged_in_current_dir();
let current_path = self.current_tab_path_str();
self.menu.bulk.ask_filenames(flagged, ¤t_path)?;
if let Some(temp_file) = self.menu.bulk.temp_file() {
self.open_single_file(&temp_file)?;
if self.internal_settings.opener.extension_use_term("txt") {
self.fm_sender.send(FmEvents::BulkExecute)?;
} else {
self.menu.bulk.watch_in_thread(self.fm_sender.clone())?;
}
}
Ok(())
}
pub fn bulk_execute(&mut self) -> Result<()> {
self.menu.bulk.get_new_names()?;
self.set_menu_mode(
self.index,
Menu::NeedConfirmation(NeedConfirmation::BulkAction),
)?;
Ok(())
}
pub fn confirm_bulk_action(&mut self) -> Result<()> {
if let (Some(renamed), Some(created)) = self.menu.bulk.execute()? {
for (old_path, new_path) in &renamed {
log_info!("old_path {old_path:?} -> new_path {new_path:?}");
self.rename_marks(old_path, new_path)?;
}
self.menu
.flagged
.update(renamed.into_iter().map(|(_, p)| p).collect());
self.menu.flagged.extend(created);
} else {
self.menu.flagged.clear();
};
self.reset_menu_mode()?;
self.reset_tabs_view()?;
Ok(())
}
fn run_sudo_command(&mut self, sudo_command: Option<String>) -> Result<()> {
let Some(sudo_command) = sudo_command else {
log_info!("No sudo_command received from args.");
return self.menu.clear_sudo_attributes();
};
self.set_menu_mode(self.index, Menu::Nothing)?;
reset_sudo_faillock()?;
let Some(command) = sudo_command.strip_prefix("sudo ") else {
log_info!("run_sudo_command cannot run {sudo_command}. It doesn't start with 'sudo '");
return self.menu.clear_sudo_attributes();
};
let args = shell_command_parser(command, self)?;
if args.is_empty() {
return self.menu.clear_sudo_attributes();
}
let Some(password) = self.menu.password_holder.sudo() else {
log_info!("run_sudo_command password isn't set");
return self.menu.clear_sudo_attributes();
};
let directory_of_selected = self.current_tab().directory_of_selected()?;
let (success, stdout, stderr) =
execute_sudo_command_with_password(&args, &password, directory_of_selected)?;
log_info!("sudo command execution. success: {success}");
self.menu.clear_sudo_attributes()?;
if !success {
log_line!("sudo command failed: {stderr}");
}
self.preview_command_output(stdout, sudo_command.to_owned());
Ok(())
}
#[rustfmt::skip]
pub fn dispatch_password(
&mut self,
action: Option<MountAction>,
dest: PasswordUsage,
sudo_command: Option<String>,
) -> Result<()> {
match (dest, action) {
(PasswordUsage::ISO, Some(MountAction::MOUNT)) => self.mount_iso_drive(),
(PasswordUsage::ISO, Some(MountAction::UMOUNT)) => self.umount_iso_drive(),
(PasswordUsage::CRYPTSETUP(_), Some(MountAction::MOUNT)) => self.mount_encrypted_drive(),
(PasswordUsage::CRYPTSETUP(_), Some(MountAction::UMOUNT)) => self.umount_encrypted_drive(),
(PasswordUsage::DEVICE, Some(MountAction::MOUNT)) => self.mount_normal_device(),
(PasswordUsage::DEVICE, Some(MountAction::UMOUNT)) => self.umount_normal_device(),
(PasswordUsage::SUDOCOMMAND, _) => self.run_sudo_command(sudo_command),
(_, _) => Ok(()),
}
}
pub fn preview_command_output(&mut self, output: String, command: String) {
log_info!("preview_command_output for {command}:\n{output}");
if output.is_empty() {
return;
}
let _ = self.reset_menu_mode();
self.current_tab_mut().set_display_mode(Display::Preview);
let preview = PreviewBuilder::cli_info(&output, command);
if let Preview::Text(text) = &preview {
log_info!("preview is Text with: {text:?}");
} else {
log_info!("preview is empty ? {empty}", empty = preview.is_empty());
}
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()
}
pub fn confirm(&mut self, c: char, confirmed_action: NeedConfirmation) -> Result<()> {
if c == 'y' {
if let Ok(must_leave) = self.match_confirmed_mode(confirmed_action) {
if must_leave {
return Ok(());
}
}
}
self.reset_menu_mode()?;
self.current_tab_mut().refresh_view()?;
Ok(())
}
fn match_confirmed_mode(&mut self, confirmed_action: NeedConfirmation) -> Result<bool> {
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(),
NeedConfirmation::BulkAction => self.confirm_bulk_action(),
NeedConfirmation::DeleteCloud => {
self.cloud_confirm_delete()?;
return Ok(true);
}
}?;
Ok(false)
}
pub fn header_action(&mut self, col: u16, binds: &Bindings) -> Result<()> {
if self.current_tab().display_mode.is_preview() {
return Ok(());
}
Header::new(self, self.current_tab())?
.action(col, !self.focus.is_left())
.matcher(self, binds)
}
pub fn footer_action(&mut self, col: u16, binds: &Bindings) -> Result<()> {
log_info!("footer clicked col {col}");
let is_right = self.index == 1;
let action = match self.current_tab().display_mode {
Display::Preview => return Ok(()),
Display::Tree | Display::Directory => {
let footer = Footer::new(self, self.current_tab())?;
footer.action(col, is_right).to_owned()
}
Display::Fuzzy => return Ok(()),
};
log_info!("action: {action}");
action.matcher(self, binds)
}
pub fn chmod(&mut self) -> Result<()> {
if self.menu.input.is_empty() || self.menu.flagged.is_empty() {
return Ok(());
}
let input_permission = &self.menu.input.string();
Permissions::set_permissions_of_flagged(input_permission, &self.menu.flagged)?;
self.reset_tabs_view()
}
pub fn set_mode_chmod(&mut self) -> Result<()> {
if self.current_tab_mut().directory.is_empty() {
return Ok(());
}
if self.menu.flagged.is_empty() {
self.toggle_flag_for_selected();
}
self.set_menu_mode(self.index, Menu::InputSimple(InputSimple::Chmod))?;
self.menu.replace_input_by_permissions();
Ok(())
}
pub fn run_custom_command(&mut self, string: &str) -> Result<()> {
self.execute_shell_command(string.to_owned(), None, true)?;
Ok(())
}
pub fn compress(&mut self) -> Result<()> {
let here = &self.current_tab().directory.path;
set_current_dir(here)?;
let files_with_relative_paths = self.flagged_or_selected_relative_to(here);
if files_with_relative_paths.is_empty() {
return Ok(());
}
match self
.menu
.compression
.compress(files_with_relative_paths, here)
{
Ok(()) => (),
Err(error) => log_info!("Error compressing files. Error: {error}"),
}
Ok(())
}
pub fn sort_by_char(&mut self, c: char) -> Result<()> {
self.current_tab_mut().sort(c)?;
self.menu.reset();
self.set_height_for_menu_mode(self.index, Menu::Nothing)?;
self.tabs[self.index].menu_mode = Menu::Nothing;
let len = self.menu.len(Menu::Nothing);
let height = self.second_window_height()?;
self.menu.window = ContentWindow::new(len, height);
self.focus = self.focus.to_parent();
Ok(())
}
pub fn canvas_width(&self) -> Result<u16> {
let full_width = self.term_width();
if self.session.dual() && full_width >= MIN_WIDTH_FOR_DUAL_PANE {
Ok(full_width / 2)
} else {
Ok(full_width)
}
}
fn search(&mut self) -> Result<()> {
let Some(search) = self.build_search_from_input() else {
self.current_tab_mut().search = Search::empty();
return Ok(());
};
self.search_and_update(search)
}
fn build_search_from_input(&self) -> Option<Search> {
let searched = &self.menu.input.string();
if searched.is_empty() {
return None;
}
Search::new(searched).ok()
}
fn search_and_update(&mut self, mut search: Search) -> Result<()> {
search.execute_search(self.current_tab_mut())?;
self.current_tab_mut().search = search;
self.update_second_pane_for_preview()
}
fn search_again(&mut self) -> Result<()> {
let search = self.current_tab().search.clone_with_regex();
self.search_and_update(search)
}
pub fn filter(&mut self) -> Result<()> {
let filter = FilterKind::from_input(&self.menu.input.string());
self.current_tab_mut().set_filter(filter)?;
self.search_again()
}
pub fn input_filter(&mut self, c: char) -> Result<()> {
self.menu.input_insert(c)?;
self.filter()
}
pub fn open_picker(&mut self) -> Result<()> {
if self.current_tab().menu_mode.is_picker() || self.menu.input_history.filtered_is_empty() {
return Ok(());
}
let menu = self.current_tab().menu_mode;
let content = self.menu.input_history.filtered_as_list();
self.menu.picker.set(
Some(PickerCaller::Menu(menu)),
menu.name_for_picker(),
content,
);
self.set_menu_mode(self.index, Menu::Navigate(Navigate::Picker))
}
pub fn cloud_load_config(&mut self) -> Result<()> {
let Some(picked) = self.menu.picker.selected() else {
log_info!("nothing selected");
return Ok(());
};
let Ok(cloud) = google_drive(picked) else {
log_line!("Invalid config file {picked}");
return Ok(());
};
self.menu.cloud = cloud;
self.set_menu_mode(self.index, Menu::Navigate(Navigate::Cloud))
}
pub fn cloud_open(&mut self) -> Result<()> {
if self.menu.cloud.is_set() {
self.set_menu_mode(self.index, Menu::Navigate(Navigate::Cloud))
} else {
self.cloud_picker()
}
}
fn cloud_picker(&mut self) -> Result<()> {
let content = get_cloud_token_names()?;
self.menu.picker.set(
Some(PickerCaller::Cloud),
Some("Pick a cloud provider".to_owned()),
content,
);
self.set_menu_mode(self.index, Menu::Navigate(Navigate::Picker))
}
pub fn cloud_disconnect(&mut self) -> Result<()> {
self.menu.cloud.disconnect();
self.cloud_open()
}
pub fn cloud_enter_delete_mode(&mut self) -> Result<()> {
self.set_menu_mode(
self.index,
Menu::NeedConfirmation(NeedConfirmation::DeleteCloud),
)
}
pub fn cloud_confirm_delete(&mut self) -> Result<()> {
self.menu.cloud.delete()?;
self.set_menu_mode(self.index, Menu::Navigate(Navigate::Cloud))?;
self.menu.cloud.refresh_current()?;
self.menu.window.scroll_to(self.menu.cloud.index);
Ok(())
}
pub fn cloud_update_metadata(&mut self) -> Result<()> {
self.menu.cloud.update_metadata()
}
pub fn cloud_enter_newdir_mode(&mut self) -> Result<()> {
self.set_menu_mode(self.index, Menu::InputSimple(InputSimple::CloudNewdir))?;
self.refresh_view()
}
pub fn cloud_create_newdir(&mut self, dirname: String) -> Result<()> {
self.menu.cloud.create_newdir(dirname)?;
self.menu.cloud.refresh_current()
}
fn get_normal_selected_file(&self) -> Option<FileInfo> {
let local_file = self.tabs[self.index].current_file().ok()?;
match local_file.file_kind {
FileKind::NormalFile => Some(local_file),
_ => None,
}
}
pub fn cloud_upload_selected_file(&mut self) -> Result<()> {
let Some(local_file) = self.get_normal_selected_file() else {
log_line!("Can only upload normal files.");
return Ok(());
};
self.menu.cloud.upload(&local_file)?;
self.menu.cloud.refresh_current()
}
pub fn cloud_enter_file_or_dir(&mut self) -> Result<()> {
if let Some(entry) = self.menu.cloud.selected() {
match entry.metadata().mode() {
EntryMode::Unknown => (),
EntryMode::FILE => self
.menu
.cloud
.download(self.current_tab().directory_of_selected()?)?,
EntryMode::DIR => {
self.menu.cloud.enter_selected()?;
self.cloud_set_content_window_len()?;
}
};
};
Ok(())
}
fn cloud_set_content_window_len(&mut self) -> Result<()> {
let len = self.menu.cloud.content.len();
let height = self.second_window_height()?;
self.menu.window = ContentWindow::new(len, height);
Ok(())
}
pub fn cloud_move_to_parent(&mut self) -> Result<()> {
self.menu.cloud.move_to_parent()?;
self.cloud_set_content_window_len()?;
Ok(())
}
pub fn toggle_flag_visual(&mut self) {
if self.current_tab().visual {
let Some(path) = self.current_tab().selected_path() else {
return;
};
self.menu.flagged.toggle(&path)
}
}
pub fn parse_ipc(&mut self, msg: String) -> Result<()> {
let mut split = msg.split_whitespace();
match split.next() {
Some("GO") => {
log_info!("Received IPC command GO");
if let Some(dest) = split.next() {
log_info!("Received IPC command GO to {dest}");
let dest = tilde(dest);
self.current_tab_mut().cd_to_file(dest.as_ref())?
}
}
Some("KEY") => {
log_info!("Received IPC command KEY");
if let Some(keyname) = split.next() {
if let Some(key) = from_keyname(keyname) {
self.fm_sender.send(FmEvents::Term(Event::Key(key)))?;
log_info!("Sent key event: {key:?}");
}
}
}
Some("ACTION") => {
log_info!("Received IPC command ACTION");
if let Some(action_str) = split.next() {
if let Ok(action) = ActionMap::from_str(action_str) {
log_info!("Sent action event: {action:?}");
self.fm_sender.send(FmEvents::Action(action))?;
}
}
}
Some(_unknown) => log_info!("Received unknown IPC command: {msg}"),
None => (),
};
Ok(())
}
pub fn rename_marks<P>(&mut self, old_path: &Path, new_path: P) -> Result<()>
where
P: AsRef<Path>,
{
log_info!("{old_path:?} -> {new_path:?}", new_path = new_path.as_ref());
self.menu.temp_marks.move_path(old_path, new_path.as_ref());
self.menu.marks.move_path(old_path, new_path.as_ref())
}
}
fn find_keybind_from_fuzzy(line: &str) -> Result<KeyEvent> {
let Some(keybind) = line.split(':').next() else {
bail!("No keybind found");
};
let Some(key) = from_keyname(keybind.trim()) else {
bail!("{keybind} isn't a valid Key name.");
};
Ok(key)
}