use { super::*, crate::{ app::*, display::W, errors::ProgramError, launchable::Launchable, }, std::{ fs::OpenOptions, io::Write, path::PathBuf, }, }; /// Definition of how the user input should be interpreted /// to be executed in an external command. #[derive(Debug, Clone)] pub struct ExternalExecution { /// the pattern which will result in an executable string when /// completed with the args. /// This pattern may include names coming from the invocation /// pattern (like {my-arg}) and special names automatically filled by /// broot from the selection and application state: /// * {file} /// * {directory} /// * {parent} /// * {other-panel-file} /// * {other-panel-directory} /// * {other-panel-parent} pub exec_pattern: ExecPattern, /// how the external process must be launched pub exec_mode: ExternalExecutionMode, /// the working directory of the new process, or none if we don't /// want to set it pub working_dir: Option, } impl ExternalExecution { pub fn new( exec_pattern: ExecPattern, exec_mode: ExternalExecutionMode, ) -> Self { Self { exec_pattern, exec_mode, working_dir: None, } } pub fn with_working_dir(mut self, b: Option) -> Self { self.working_dir = b; self } /// goes from the external execution command to the CmdResult: /// - by executing the command if it can be executed from a subprocess /// - by building a command to be executed in parent shell in other cases pub fn to_cmd_result( &self, w: &mut W, builder: ExecutionStringBuilder<'_>, con: &AppContext, ) -> Result { match self.exec_mode { ExternalExecutionMode::FromParentShell => self.cmd_result_exec_from_parent_shell( builder, con, ), ExternalExecutionMode::LeaveBroot => self.cmd_result_exec_leave_broot( builder, con, ), ExternalExecutionMode::StayInBroot => self.cmd_result_exec_stay_in_broot( w, builder, con, ), } } fn working_dir_path( &self, builder: &ExecutionStringBuilder<'_>, ) -> Option { self.working_dir .as_ref() .map(|pattern| builder.path(pattern)) .filter(|pb| { if pb.exists() { true } else { warn!("workding dir doesn't exist: {:?}", pb); false } }) } /// build the cmd result as an executable which will be called /// from the parent shell (meaning broot must quit) fn cmd_result_exec_from_parent_shell( &self, builder: ExecutionStringBuilder<'_>, con: &AppContext, ) -> Result { if builder.sel_info.count_paths() > 1 { return Ok(CmdResult::error( "only verbs returning to broot on end can be executed on a multi-selection" )); } if let Some(ref export_path) = con.launch_args.outcmd { // Broot was probably launched as br. // the whole command is exported in the passed file let f = OpenOptions::new().append(true).open(export_path)?; writeln!(&f, "{}", builder.shell_exec_string(&self.exec_pattern))?; Ok(CmdResult::Quit) } else { Ok(CmdResult::error( "this verb needs broot to be launched as `br`. Try `broot --install` if necessary." )) } } /// build the cmd result as an executable which will be called in a process /// launched by broot at end of broot fn cmd_result_exec_leave_broot( &self, builder: ExecutionStringBuilder<'_>, con: &AppContext, ) -> Result { if builder.sel_info.count_paths() > 1 { return Ok(CmdResult::error( "only verbs returning to broot on end can be executed on a multi-selection" )); } let launchable = Launchable::program( builder.exec_token(&self.exec_pattern), self.working_dir_path(&builder), con, )?; Ok(CmdResult::from(launchable)) } /// build the cmd result as an executable which will be called in a process /// launched by broot fn cmd_result_exec_stay_in_broot( &self, w: &mut W, builder: ExecutionStringBuilder<'_>, con: &AppContext, ) -> Result { let working_dir_path = self.working_dir_path(&builder); match &builder.sel_info { SelInfo::None | SelInfo::One(_) => { // zero or one selection -> only one execution let launchable = Launchable::program( builder.exec_token(&self.exec_pattern), working_dir_path, con, )?; info!("Executing not leaving, launchable {:?}", launchable); if let Err(e) = launchable.execute(Some(w)) { warn!("launchable failed : {:?}", e); return Ok(CmdResult::error(e.to_string())); } } SelInfo::More(stage) => { // multiselection -> we must execute on all paths let sels = stage.paths().iter() .map(|path| Selection { path, line: 0, stype: SelectionType::from(path), is_exe: false, }); for sel in sels { let launchable = Launchable::program( builder.sel_exec_token(&self.exec_pattern, Some(sel)), working_dir_path.clone(), con, )?; if let Err(e) = launchable.execute(Some(w)) { warn!("launchable failed : {:?}", e); return Ok(CmdResult::error(e.to_string())); } } } } Ok(CmdResult::RefreshState { clear_cache: true }) } }