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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
use {
super::*,
crate::{
app::*,
display::W,
errors::ProgramError,
launchable::Launchable,
},
std::{
fs::OpenOptions,
io::Write,
path::PathBuf,
},
};
pub static MULTI_SELECTION_ERROR: &str =
"Only verbs returning to broot on end or merging selections can be executed on multi-selection";
/// 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>,
/// whether we need to switch to the normal terminal for
/// the duration of the execution of the process
pub switch_terminal: bool,
/// whether the tree must be refreshed after the verb is executed
pub refresh_after: bool,
}
impl ExternalExecution {
pub fn new(
exec_pattern: ExecPattern,
exec_mode: ExternalExecutionMode,
) -> Self {
Self {
exec_pattern,
exec_mode,
working_dir: None,
switch_terminal: true, // by default we switch
refresh_after: true, // by default we refresh
}
}
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: ExecutionBuilder<'_>,
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: &ExecutionBuilder<'_>,
con: &AppContext,
) -> Option<PathBuf> {
self.working_dir
.as_ref()
.map(|pattern| builder.path(pattern, con))
.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,
mut builder: ExecutionBuilder<'_>,
con: &AppContext,
) -> Result<CmdResult, ProgramError> {
if builder.sel_info.count_paths() > 1 {
let coarity = self.exec_pattern.coarity();
debug!("coarity of the command is {:?}", coarity);
if coarity == CommandCoarity::PerSelection {
return Ok(CmdResult::error(MULTI_SELECTION_ERROR));
}
}
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, con))?;
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: ExecutionBuilder<'_>,
con: &AppContext,
) -> Result<CmdResult, ProgramError> {
if builder.sel_info.count_paths() > 1 {
if self.exec_pattern.coarity() == CommandCoarity::PerSelection {
return Ok(CmdResult::error(MULTI_SELECTION_ERROR));
}
}
let launchable = Launchable::program(
builder.exec_token(&self.exec_pattern, con),
self.working_dir_path(&builder, con),
self.switch_terminal,
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,
mut builder: ExecutionBuilder<'_>,
con: &AppContext,
) -> Result<CmdResult, ProgramError> {
let working_dir_path = self.working_dir_path(&builder, con);
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, con),
working_dir_path,
self.switch_terminal,
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 -> what we do depends on the coarity of the command
let coarity = self.exec_pattern.coarity();
info!("coarity of the command is {:#?}", coarity);
match coarity {
CommandCoarity::PerSelection => {
// we execute once per selection
let sels = stage.paths().iter().map(|path| Selection {
path,
line: 0,
stype: SelectionType::from(path),
is_exe: false,
});
let n = sels.len();
for (i, sel) in sels.enumerate() {
let launchable = Launchable::program(
builder.sel_exec_token(&self.exec_pattern, Some(sel), con),
working_dir_path.clone(),
self.switch_terminal,
con,
)?;
let i = i + 1;
info!("Executing not leaving launchable {i}/{n}: {launchable:#?}");
if let Err(e) = launchable.execute(Some(w)) {
warn!("launchable failed : {:#?}", e);
return Ok(CmdResult::error(e.to_string()));
}
}
}
CommandCoarity::Merged => {
// we execute once as the arguments are merging the selection
let launchable = Launchable::program(
builder.exec_token(&self.exec_pattern, con),
working_dir_path.clone(),
self.switch_terminal,
con,
)?;
info!("Executing not leaving, merged launchable {:#?}", launchable);
if let Err(e) = launchable.execute(Some(w)) {
warn!("launchable failed : {:?}", e);
return Ok(CmdResult::error(e.to_string()));
}
}
}
}
}
if self.refresh_after {
Ok(CmdResult::RefreshState { clear_cache: true })
} else {
Ok(CmdResult::Keep)
}
}
}