summaryrefslogtreecommitdiffstats
path: root/src/event_exec.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/event_exec.rs')
-rw-r--r--src/event_exec.rs285
1 files changed, 231 insertions, 54 deletions
diff --git a/src/event_exec.rs b/src/event_exec.rs
index 6327263..2b1b113 100644
--- a/src/event_exec.rs
+++ b/src/event_exec.rs
@@ -1,4 +1,5 @@
use std::borrow::Borrow;
+use std::fmt::Display;
use std::fs;
use std::path;
use std::str::FromStr;
@@ -11,23 +12,31 @@ use which::which;
use crate::action_map::ActionMap;
use crate::completion::InputCompleted;
use crate::config::Colors;
+use crate::constant_strings_paths::DIFF;
+use crate::constant_strings_paths::MEDIAINFO;
+use crate::constant_strings_paths::NITROGEN;
+use crate::constant_strings_paths::SSHFS_EXECUTABLE;
use crate::constant_strings_paths::{CONFIG_PATH, DEFAULT_DRAGNDROP};
-use crate::cryptsetup::BlockDeviceAction;
+use crate::cryptsetup::{lsblk_and_cryptsetup_installed, BlockDeviceAction};
use crate::fileinfo::FileKind;
use crate::filter::FilterKind;
use crate::log::read_log;
+use crate::log::write_log_line;
+use crate::mocp::is_mocp_installed;
use crate::mocp::Mocp;
use crate::mode::{InputSimple, MarkAction, Mode, Navigate, NeedConfirmation};
use crate::opener::{
- execute_and_capture_output_without_check, execute_in_child,
+ execute_and_capture_output, execute_and_capture_output_without_check, execute_in_child,
execute_in_child_without_output_with_path, InternalVariant,
};
use crate::password::{PasswordKind, PasswordUsage};
+use crate::preview::is_ext_image;
use crate::preview::Preview;
use crate::selectable_content::SelectableContent;
use crate::shell_parser::ShellCommandParser;
use crate::status::Status;
use crate::tab::Tab;
+use crate::utils::is_program_in_path;
use crate::utils::{
args_is_empty, disk_used_by_path, filename_from_path, is_sudo_command, open_in_current_neovim,
opt_mount_point, string_to_path,
@@ -73,7 +82,9 @@ impl EventAction {
match tab.mode {
Mode::Normal => {
- let Some(file) = tab.path_content.selected() else { return Ok(()) };
+ let Some(file) = tab.path_content.selected() else {
+ return Ok(());
+ };
let path = file.path.clone();
status.toggle_flag_on_path(&path);
status.selected().down_one_row();
@@ -100,7 +111,7 @@ impl EventAction {
.flagged
.push(status.tabs[status.index].selected().unwrap().path.clone());
};
- status.reset_tabs_view()
+ Ok(())
}
/// Enter JUMP mode, allowing to jump to any flagged file.
@@ -141,7 +152,12 @@ impl EventAction {
.directory_of_selected()?
.join(filename);
std::os::unix::fs::symlink(original_file, &link)?;
- info!(target: "special", "Symlink {link} links to {original_file}", original_file=original_file.display(), link=link.display());
+ let log_line = format!(
+ "Symlink {link} links to {original_file}",
+ original_file = original_file.display(),
+ link = link.display()
+ );
+ write_log_line(log_line);
}
status.clear_flags_and_reset_view()
}
@@ -156,8 +172,11 @@ impl EventAction {
/// Leave current mode to normal mode.
/// Reset the inputs and completion, reset the window, exit the preview.
pub fn reset_mode(tab: &mut Tab) -> Result<()> {
- tab.reset_mode();
- tab.refresh_view()
+ if tab.reset_mode() {
+ tab.refresh_view()
+ } else {
+ tab.refresh_params()
+ }
}
/// Enter a copy paste mode.
/// A confirmation is asked before copying all flagged files to
@@ -215,7 +234,9 @@ impl EventAction {
return Ok(());
}
let unmutable_tab = status.selected_non_mut();
- let Some(file_info) = unmutable_tab.selected() else { return Ok(()) };
+ let Some(file_info) = unmutable_tab.selected() else {
+ return Ok(());
+ };
match file_info.file_kind {
FileKind::NormalFile => {
let preview = Preview::new(
@@ -411,7 +432,7 @@ impl EventAction {
/// Basic folders (/, /dev... $HOME) and mount points (even impossible to
/// visit ones) are proposed.
pub fn shortcut(tab: &mut Tab) -> Result<()> {
- std::env::set_current_dir(tab.current_path())?;
+ std::env::set_current_dir(tab.current_directory_path())?;
tab.shortcut.update_git_root();
tab.set_mode(Mode::Navigate(Navigate::Shortcut));
Ok(())
@@ -427,8 +448,12 @@ impl EventAction {
};
let nvim_server = status.nvim_server.clone();
let tab = status.selected();
- let Some(fileinfo) = tab.selected() else { return Ok(()) };
- let Some(path_str) = fileinfo.path.to_str() else { return Ok(()) };
+ let Some(fileinfo) = tab.selected() else {
+ return Ok(());
+ };
+ let Some(path_str) = fileinfo.path.to_str() else {
+ return Ok(());
+ };
open_in_current_neovim(path_str, &nvim_server);
Ok(())
@@ -486,8 +511,13 @@ impl EventAction {
/// Executes a `dragon-drop` command on the selected file.
/// It obviously requires the `dragon-drop` command to be installed.
pub fn drag_n_drop(status: &mut Status) -> Result<()> {
- let tab = status.selected_non_mut();
- let Some(file) = tab.selected() else { return Ok(()) };
+ if !is_program_in_path(DEFAULT_DRAGNDROP) {
+ write_log_line(format!("{DEFAULT_DRAGNDROP} must be installed."));
+ return Ok(());
+ }
+ let Some(file) = status.selected_non_mut().selected() else {
+ return Ok(());
+ };
let path_str = file
.path
.to_str()
@@ -501,7 +531,9 @@ impl EventAction {
match tab.mode {
Mode::Tree => (),
_ => {
- let Some(searched) = tab.searched.clone() else { return Ok(()) };
+ let Some(searched) = tab.searched.clone() else {
+ return Ok(());
+ };
let next_index = (tab.path_content.index + 1) % tab.path_content.content.len();
tab.search_from(&searched, next_index);
}
@@ -680,6 +712,7 @@ impl EventAction {
must_reset_mode = false;
LeaveMode::password(status, kind, colors, dest, action)?
}
+ Mode::InputSimple(InputSimple::Remote) => LeaveMode::remote(status.selected())?,
Mode::Navigate(Navigate::Jump) => LeaveMode::jump(status)?,
Mode::Navigate(Navigate::History) => LeaveMode::history(status.selected())?,
Mode::Navigate(Navigate::Shortcut) => LeaveMode::shortcut(status.selected())?,
@@ -782,8 +815,14 @@ impl EventAction {
/// Display mediainfo details of an image
pub fn mediainfo(tab: &mut Tab) -> Result<()> {
+ if !is_program_in_path(MEDIAINFO) {
+ write_log_line(format!("{} isn't installed", MEDIAINFO));
+ return Ok(());
+ }
if let Mode::Normal | Mode::Tree = tab.mode {
- let Some(file_info) = tab.selected() else { return Ok(())};
+ let Some(file_info) = tab.selected() else {
+ return Ok(());
+ };
info!("selected {:?}", file_info);
tab.preview = Preview::mediainfo(&file_info.path)?;
tab.window.reset(tab.preview.len());
@@ -794,12 +833,20 @@ impl EventAction {
/// Display a diff between the first 2 flagged files or dir.
pub fn diff(status: &mut Status) -> Result<()> {
+ if !is_program_in_path(DIFF) {
+ write_log_line(format!("{DIFF} isn't installed"));
+ return Ok(());
+ }
if status.flagged.len() < 2 {
return Ok(());
};
if let Mode::Normal | Mode::Tree = status.selected_non_mut().mode {
- let first_path = &status.flagged.content[0].to_str().unwrap();
- let second_path = &status.flagged.content[1].to_str().unwrap();
+ let first_path = &status.flagged.content[0]
+ .to_str()
+ .context("Couldn't parse filename")?;
+ let second_path = &status.flagged.content[1]
+ .to_str()
+ .context("Couldn't parse filename")?;
status.selected().preview = Preview::diff(first_path, second_path)?;
let tab = status.selected();
tab.window.reset(tab.preview.len());
@@ -912,6 +959,10 @@ impl EventAction {
/// Enter the encrypted device menu, allowing the user to mount/umount
/// a luks encrypted device.
pub fn encrypted_drive(status: &mut Status) -> Result<()> {
+ if !lsblk_and_cryptsetup_installed() {
+ write_log_line("lsblk and cryptsetup must be installed.".to_owned());
+ return Ok(());
+ }
if status.encrypted_devices.is_empty() {
status.encrypted_devices.update()?;
}
@@ -958,18 +1009,46 @@ impl EventAction {
/// Set the current selected file as wallpaper with `nitrogen`.
/// Requires `nitrogen` to be installed.
pub fn set_wallpaper(tab: &Tab) -> Result<()> {
- let Some(path_str) = tab.path_content.selected_path_string() else { return Ok(()); };
- let _ = execute_in_child("nitrogen", &["--set-zoom-fill", "--save", &path_str]);
+ if !is_program_in_path(NITROGEN) {
+ write_log_line("nitrogen must be installed".to_owned());
+ return Ok(());
+ }
+ let Some(fileinfo) = tab.path_content.selected() else {
+ return Ok(());
+ };
+ if !is_ext_image(&fileinfo.extension) {
+ return Ok(());
+ }
+ let Some(path_str) = tab.path_content.selected_path_string() else {
+ return Ok(());
+ };
+ let _ = execute_in_child(NITROGEN, &["--set-zoom-fill", "--save", &path_str]);
Ok(())
}
/// Add a song or a folder to MOC playlist. Start it first...
pub fn mocp_add_to_playlist(tab: &Tab) -> Result<()> {
+ if !is_mocp_installed() {
+ write_log_line("mocp isn't installed".to_owned());
+ return Ok(());
+ }
Mocp::add_to_playlist(tab)
}
+ pub fn mocp_clear_playlist() -> Result<()> {
+ if !is_mocp_installed() {
+ write_log_line("mocp isn't installed".to_owned());
+ return Ok(());
+ }
+ Mocp::clear()
+ }
+
/// Add a song or a folder to MOC playlist. Start it first...
pub fn mocp_go_to_song(tab: &mut Tab) -> Result<()> {
+ if !is_mocp_installed() {
+ write_log_line("mocp isn't installed".to_owned());
+ return Ok(());
+ }
Mocp::go_to_song(tab)
}
@@ -977,23 +1056,35 @@ impl EventAction {
/// Starts the server if needed, preventing the output to fill the screen.
/// Then toggle play/pause
pub fn mocp_toggle_pause(status: &mut Status) -> Result<()> {
+ if !is_mocp_installed() {
+ write_log_line("mocp isn't installed".to_owned());
+ return Ok(());
+ }
Mocp::toggle_pause(status)
}
/// Skip to the next song in MOC
pub fn mocp_next() -> Result<()> {
+ if !is_mocp_installed() {
+ write_log_line("mocp isn't installed".to_owned());
+ return Ok(());
+ }
Mocp::next()
}
/// Go to the previous song in MOC
pub fn mocp_previous() -> Result<()> {
+ if !is_mocp_installed() {
+ write_log_line("mocp isn't installed".to_owned());
+ return Ok(());
+ }
Mocp::previous()
}
/// Execute a custom event on the selected file
- pub fn custom(status: &mut Status, string: String) -> Result<()> {
+ pub fn custom(status: &mut Status, string: &String) -> Result<()> {
info!("custom {string}");
- let parser = ShellCommandParser::new(&string);
+ let parser = ShellCommandParser::new(string);
let mut args = parser.compute(status)?;
let command = args.remove(0);
let args: Vec<&str> = args.iter().map(|s| &**s).collect();
@@ -1001,6 +1092,52 @@ impl EventAction {
info!("output {output}");
Ok(())
}
+
+ pub fn remote_mount(tab: &mut Tab) -> Result<()> {
+ tab.set_mode(Mode::InputSimple(InputSimple::Remote));
+ Ok(())
+ }
+}
+
+enum NodeCreation {
+ Newfile,
+ Newdir,
+}
+
+impl Display for NodeCreation {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Self::Newfile => write!(f, "file"),
+ Self::Newdir => write!(f, "directory"),
+ }
+ }
+}
+
+impl NodeCreation {
+ fn create(&self, tab: &mut Tab) -> Result<()> {
+ let path = tab
+ .path_content
+ .path
+ .join(sanitize_filename::sanitize(tab.input.string()));
+ if path.exists() {
+ write_log_line(format!(
+ "{self} {path} already exists",
+ path = path.display()
+ ));
+ } else {
+ match self {
+ Self::Newdir => {
+ fs::create_dir_all(&path)?;
+ }
+ Self::Newfile => {
+ fs::File::create(&path)?;
+ }
+ }
+ let log_line = format!("Created new {self}: {path}", path = path.display());
+ write_log_line(log_line);
+ }
+ tab.refresh_view()
+ }
}
/// Methods called when executing something with Enter key.
@@ -1050,7 +1187,10 @@ impl LeaveMode {
let len = status.selected_non_mut().path_content.content.len();
if let Some((ch, _)) = marks.selected() {
if let Some(path_str) = status.selected_non_mut().path_content_str() {
- status.marks.new_mark(*ch, path::PathBuf::from(path_str))?;
+ let p = path::PathBuf::from(path_str);
+ status.marks.new_mark(*ch, &p)?;
+ let log_line = format!("Saved mark {ch} -> {p}", p = p.display());
+ write_log_line(log_line);
}
status.selected().window.reset(len);
status.selected().input.reset();
@@ -1081,16 +1221,18 @@ impl LeaveMode {
/// Nothing is done if the user typed nothing or an invalid permission like
/// 955.
pub fn chmod(status: &mut Status) -> Result<()> {
- if status.selected().input.is_empty() {
+ if status.selected().input.is_empty() || status.flagged.is_empty() {
return Ok(());
}
- let permissions: u32 =
- u32::from_str_radix(&status.selected().input.string(), 8).unwrap_or(0_u32);
+ let input_permission = &status.selected().input.string();
+ let permissions: u32 = u32::from_str_radix(input_permission, 8).unwrap_or(0_u32);
if permissions <= Status::MAX_PERMISSIONS {
for path in status.flagged.content.iter() {
Status::set_permissions(path, permissions)?
}
- status.flagged.clear()
+ status.flagged.clear();
+ let log_line = format!("Changed permissions to {input_permission}");
+ write_log_line(log_line);
}
status.selected().refresh_view()?;
status.reset_tabs_view()
@@ -1106,7 +1248,9 @@ impl LeaveMode {
/// If the user selected a directory, we jump inside it.
/// Otherwise, we jump to the parent and select the file.
pub fn jump(status: &mut Status) -> Result<()> {
- let Some(jump_target) = status.flagged.selected() else { return Ok(()) };
+ let Some(jump_target) = status.flagged.selected() else {
+ return Ok(());
+ };
let jump_target = jump_target.to_owned();
let target_dir = match jump_target.parent() {
Some(parent) => parent,
@@ -1127,8 +1271,8 @@ impl LeaveMode {
}
/// Execute a shell command typed by the user.
- /// pipes and redirections aren't NotSupported
- /// expansions are supported
+ /// pipes and redirections aren't supported
+ /// but expansions are supported
pub fn shell(status: &mut Status) -> Result<bool> {
let shell_command = status.selected_non_mut().input.string();
let mut args = ShellCommandParser::new(&shell_command).compute(status)?;
@@ -1143,7 +1287,9 @@ impl LeaveMode {
status.ask_password(PasswordKind::SUDO, None, PasswordUsage::SUDOCOMMAND)?;
Ok(false)
} else {
- let Ok(executable) = which(executable) else { return Ok(true); };
+ let Ok(executable) = which(executable) else {
+ return Ok(true);
+ };
let current_directory = status
.selected_non_mut()
.directory_of_selected()?
@@ -1180,11 +1326,13 @@ impl LeaveMode {
original_path.display(),
new_path.display()
);
- info!(target: "special",
+ let log_line = format!(
"renaming: original: {} - new: {}",
original_path.display(),
new_path.display()
);
+ write_log_line(log_line);
+
fs::rename(original_path, new_path)?;
}
@@ -1195,15 +1343,7 @@ impl LeaveMode {
/// Nothing is done if the file already exists.
/// Filename is sanitized before processing.
pub fn newfile(tab: &mut Tab) -> Result<()> {
- let path = tab
- .path_content
- .path
- .join(sanitize_filename::sanitize(tab.input.string()));
- if !path.exists() {
- fs::File::create(&path)?;
- info!(target: "special", "New file: {path}", path=path.display());
- }
- tab.refresh_view()
+ NodeCreation::Newfile.create(tab)
}
/// Creates a new directory with input string as name.
@@ -1212,15 +1352,7 @@ impl LeaveMode {
/// ie. the user can create `newdir` or `newdir/newfolder`.
/// Directory name is sanitized before processing.
pub fn newdir(tab: &mut Tab) -> Result<()> {
- let path = tab
- .path_content
- .path
- .join(sanitize_filename::sanitize(tab.input.string()));
- if !path.exists() {
- fs::create_dir_all(&path)?;
- info!(target: "special", "New directory: {path}", path=path.display());
- }
- tab.refresh_view()
+ NodeCreation::Newdir.create(tab)
}
/// Tries to execute the selected file with an executable which is read
@@ -1353,15 +1485,22 @@ impl LeaveMode {
/// Compress the flagged files into an archive.
/// Compression method is chosen by the user.
/// The archive is created in the current directory and is named "archive.tar.??" or "archive.zip".
+ /// Files which are above the CWD are filtered out since they can't be added to an archive.
+ /// Archive creation depends on CWD so we ensure it's set to the selected tab.
fn compress(status: &mut Status) -> Result<()> {
- let cwd = std::env::current_dir()?;
- let files_with_relative_paths = status
+ let here = &status.selected_non_mut().path_content.path;
+ std::env::set_current_dir(here)?;
+ let files_with_relative_paths: Vec<path::PathBuf> = status
.flagged
.content
.iter()
- .filter_map(|abs_path| pathdiff::diff_paths(abs_path, &cwd))
+ .filter_map(|abs_path| pathdiff::diff_paths(abs_path, here))
+ .filter(|f| !f.starts_with(".."))
.collect();
- status.compression.compress(files_with_relative_paths)
+ if files_with_relative_paths.is_empty() {
+ return Ok(());
+ }
+ status.compression.compress(files_with_relative_paths, here)
}
/// Execute the selected command.
@@ -1369,7 +1508,9 @@ impl LeaveMode {
/// context.
pub fn command(status: &mut Status, colors: &Colors) -> Result<()> {
let command_str = status.selected_non_mut().completion.current_proposition();
- let Ok(command) = ActionMap::from_str(command_str) else { return Ok(()) };
+ let Ok(command) = ActionMap::from_str(command_str) else {
+ return Ok(());
+ };
command.matcher(status, colors)
}
@@ -1395,4 +1536,40 @@ impl LeaveMode {
tab.window.reset(tab.path_content.content.len());
Ok(())
}
+
+ /// Run sshfs with typed parameters to mount a remote directory in current directory.
+ /// sshfs should be reachable in path.
+ /// The user must type 3 arguments like this : `username hostname remote_path`.
+ /// If the user doesn't provide 3 arguments,
+ pub fn remote(tab: &mut Tab) -> Result<()> {
+ let user_hostname_remotepath_string = tab.input.string();
+ let strings: Vec<&str> = user_hostname_remotepath_string.split(' ').collect();
+ tab.input.reset();
+
+ if !is_program_in_path(SSHFS_EXECUTABLE) {
+ info!("{SSHFS_EXECUTABLE} isn't in path");
+ return Ok(());
+ }
+
+ if strings.len() != 3 {
+ info!(
+ "Wrong number of parameters for {SSHFS_EXECUTABLE}, expected 3, got {nb}",
+ nb = strings.len()
+ );
+ return Ok(());
+ };
+
+ let (username, hostname, remote_path) = (strings[0], strings[1], strings[2]);
+ let current_path: &str = tab
+ .current_directory_path()
+ .to_str()
+ .context("couldn't parse the path")?;
+ let first_arg = &format!("{username}@{hostname}:{remote_path}");
+ let command_output =
+ execute_and_capture_output(SSHFS_EXECUTABLE, &[first_arg, current_path]);
+ let log_line = format!("{SSHFS_EXECUTABLE} output {command_output:?}");
+ info!("{log_line}");
+ write_log_line(log_line);
+ Ok(())
+ }
}