summaryrefslogtreecommitdiffstats
path: root/src/verb/external_execution.rs
blob: 626dc6c5d86e5ea1b6eeba5caf00c7c8eb634941 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
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<String>,
}

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<String>) -> 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<CmdResult, ProgramError> {
        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<PathBuf> {
        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<CmdResult, ProgramError> {
        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<CmdResult, ProgramError> {
        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<CmdResult, ProgramError> {
        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 })
    }
}