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
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
use {
crate::*,
schemars::JsonSchema,
serde::Deserialize,
std::{
collections::HashMap,
path::PathBuf,
},
};
/// One of the possible jobs that bacon can run
#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema)]
pub struct Job {
/// Whether to consider that we can have a success
/// when we have test failures
pub allow_failures: Option<bool>,
/// Whether to consider that we can have a success
/// when we have warnings. This is especially useful
/// for "cargo run" jobs
pub allow_warnings: Option<bool>,
/// The analyzer interpreting the output of the command, the
/// standard cargo dedicated one if not provided
pub analyzer: Option<AnalyzerRef>,
/// Whether gitignore rules must be applied
pub apply_gitignore: Option<bool>,
/// Whether to wait for the computation to finish before
/// to display it on screen
///
/// This is true by default. Set it to false if you want
/// the previous computation result to be replaced with
/// the new one as soon as it starts.
pub background: Option<bool>,
/// The tokens making the command to execute (first one
/// is the executable).
#[serde(default)]
pub command: Vec<String>,
/// Whether to apply the default watch list, which is
/// `["src", "tests", "benches", "examples", "build.rs"]`
///
/// This is true by default. Set it to false if you want
/// to watch nothing, or only the directories you set in
/// `watch`.
pub default_watch: Option<bool>,
/// Env vars to set for this job execution
#[serde(default)]
pub env: HashMap<String, String>,
/// Whether to expand environment variables in the command
pub expand_env_vars: Option<bool>,
/// Whether to insert extraneous arguments provided by bacon or end users
///
/// Eg: --all-features or anything after -- in bacon incantation
pub extraneous_args: Option<bool>,
/// Minimum delay to wait before restarting the job after a change is detected.
pub grace_period: Option<Period>,
/// Whether to hide the scrollbar
pub hide_scrollbar: Option<bool>,
/// A list of glob patterns to ignore.
/// Patterns starting with `!` are negations that force-include
/// matching paths, overriding other ignore rules (including .gitignore).
#[serde(default)]
pub ignore: Vec<String>,
/// Patterns of lines which should be ignored. Patterns of
/// the prefs or bacon.toml can be overridden at the job
pub ignored_lines: Option<Vec<LinePattern>>,
/// A kill command. If not provided, SIGKILL is used.
pub kill: Option<Vec<String>>,
/// Whether we need to capture stdout too (stderr is
/// always captured)
pub need_stdout: Option<bool>,
/// How to handle changes: either immediately kill the current job
/// then restart it, or wait for the current job to finish before
/// restarting it.
pub on_change_strategy: Option<OnChangeStrategy>,
/// The optional action to run when it's not a success
#[serde(default)]
pub on_failure: Option<Action>,
/// The optional action to run when there's no
/// error, warning or test failures
/// (depending on whether `allow_warnings` is `true` or `false`)
///
/// Could be made a vec in the future but that would involve
/// explaining subtleties like the fact that those actions stop
/// after the first one ending the mission or doing a refresh
#[serde(default)]
pub on_success: Option<Action>,
/// Whether to display how many files triggered the current run.
pub show_changes_count: Option<bool>,
/// Whether to show the error code of commands.
/// This is normally automatic, depending on warnings and errors found by the analyzers, but
/// some commands may want to force it on (eg `cargo run` to show the error code of the binary
/// being run even when there are rustc warnings).
pub show_command_error_code: Option<bool>,
/// Color overrides applied to the UI while the job runs.
#[serde(default)]
pub skin: BaconSkin,
/// Notification sounds
#[serde(default)]
pub sound: SoundConfig,
/// A list of directories that will be watched if the job
/// is run on a package.
/// src, examples, tests, and benches are implicitly included
/// unless you `set default_watch` to false.
pub watch: Option<Vec<String>>,
/// An optional working directory for the job command, which
/// would override the package directory.
pub workdir: Option<PathBuf>,
/// Where to anchor the scroll when the output changes and the user didn't scroll up manually.
///
/// When not specified, default will be 'first', as bacon usually displays the most important
/// item on top. Setting it to 'auto' will make bacon use 'first' when the command errored
/// (eg compilation errors) and 'last' otherwise, which is useful for commands like `cargo run`
/// where you want to see the last output when it runs successfully.
pub scroll_anchor: Option<ScrollAnchor>,
}
static DEFAULT_ARGS: &[&str] = &["--color", "always"];
impl Job {
/// Build a `Job` for a cargo alias
pub fn from_alias(
alias_name: &str,
settings: &Settings,
) -> Self {
let mut command = vec!["cargo".to_string(), alias_name.to_string()];
if let Some(additional_args) = settings.additional_alias_args.as_ref() {
for arg in additional_args {
command.push(arg.clone());
}
} else {
for &arg in DEFAULT_ARGS {
command.push(arg.to_string());
}
}
Self {
command,
..Default::default()
}
}
pub fn allow_failures(&self) -> bool {
self.allow_failures.unwrap_or(false)
}
pub fn allow_warnings(&self) -> bool {
self.allow_warnings.unwrap_or(false)
}
pub fn background(&self) -> bool {
self.background.unwrap_or(true)
}
pub fn default_watch(&self) -> bool {
self.default_watch.unwrap_or(true)
}
pub fn expand_env_vars(&self) -> bool {
self.expand_env_vars.unwrap_or(true)
}
pub fn hide_scrollbar(&self) -> bool {
self.hide_scrollbar.unwrap_or(false)
}
pub fn need_stdout(&self) -> bool {
self.need_stdout.unwrap_or(false)
}
pub fn extraneous_args(&self) -> bool {
self.extraneous_args.unwrap_or(true)
}
pub fn show_changes_count(&self) -> bool {
self.show_changes_count.unwrap_or(false)
}
pub fn grace_period(&self) -> Period {
self.grace_period
.unwrap_or(std::time::Duration::from_millis(15).into())
}
pub fn on_change_strategy(&self) -> OnChangeStrategy {
self.on_change_strategy
.unwrap_or(OnChangeStrategy::WaitThenRestart)
}
pub fn scroll_anchor(&self) -> ScrollAnchor {
self.scroll_anchor.unwrap_or(ScrollAnchor::First)
}
pub fn apply(
&mut self,
job: &Job,
) {
if let Some(b) = job.allow_failures {
self.allow_failures = Some(b);
}
if let Some(b) = job.allow_warnings {
self.allow_warnings = Some(b);
}
if let Some(v) = job.analyzer {
self.analyzer = Some(v);
}
if let Some(b) = job.apply_gitignore {
self.apply_gitignore = Some(b);
}
if let Some(b) = job.background {
self.background = Some(b);
}
if !job.command.is_empty() {
self.command.clone_from(&job.command);
}
if let Some(b) = job.default_watch {
self.default_watch = Some(b);
}
for (k, v) in &job.env {
self.env.insert(k.clone(), v.clone());
}
if let Some(b) = job.expand_env_vars {
self.expand_env_vars = Some(b);
}
if let Some(b) = job.extraneous_args {
self.extraneous_args = Some(b);
}
if let Some(b) = job.hide_scrollbar {
self.hide_scrollbar = Some(b);
}
for v in &job.ignore {
if !self.ignore.contains(v) {
self.ignore.push(v.clone());
}
}
if let Some(v) = job.ignored_lines.as_ref() {
self.ignored_lines = Some(v.clone());
}
if let Some(v) = job.kill.as_ref() {
self.kill = Some(v.clone());
}
if let Some(b) = job.need_stdout {
self.need_stdout = Some(b);
}
if let Some(v) = job.on_change_strategy {
self.on_change_strategy = Some(v);
}
if let Some(v) = job.on_success.as_ref() {
self.on_success = Some(v.clone());
}
if let Some(v) = job.grace_period {
self.grace_period = Some(v);
}
if let Some(v) = job.on_failure.as_ref() {
self.on_failure = Some(v.clone());
}
if let Some(v) = job.watch.as_ref() {
self.watch = Some(v.clone());
}
if let Some(b) = job.show_changes_count {
self.show_changes_count = Some(b);
}
if let Some(b) = job.show_command_error_code {
self.show_command_error_code = Some(b);
}
self.sound.apply(&job.sound);
if let Some(p) = job.workdir.as_ref() {
self.workdir = Some(p.clone());
}
if let Some(v) = job.scroll_anchor {
self.scroll_anchor = Some(v);
}
self.skin.apply(job.skin);
}
}
#[test]
fn test_job_apply() {
use std::str::FromStr;
let mut base_job = Job::default();
let job_to_apply = Job {
allow_failures: Some(true),
allow_warnings: Some(false),
analyzer: Some(AnalyzerRef::Nextest),
apply_gitignore: Some(false),
background: Some(false),
command: vec!["cargo".to_string(), "test".to_string()],
default_watch: Some(false),
env: vec![("RUST_LOG".to_string(), "debug".to_string())]
.into_iter()
.collect(),
expand_env_vars: Some(false),
extraneous_args: Some(false),
hide_scrollbar: Some(true),
ignore: vec![
"special-target".to_string(),
"generated".to_string(),
"!myfile.txt".to_string(),
],
ignored_lines: Some(vec![LinePattern::from_str("half-error.*").unwrap()]),
kill: Some(vec!["die".to_string()]),
need_stdout: Some(true),
grace_period: Some(Period::from_str("20ms").unwrap()),
on_change_strategy: Some(OnChangeStrategy::KillThenRestart),
on_success: Some(Action::from_str("refresh").unwrap()),
on_failure: Some(Action::from_str("play-sound(name=car-horn)").unwrap()),
watch: Some(vec!["src".to_string(), "tests".to_string()]),
show_changes_count: Some(true),
show_command_error_code: Some(true),
sound: SoundConfig {
enabled: Some(true),
base_volume: Some(Volume::from_str("50").unwrap()),
},
workdir: Some(PathBuf::from("/path/to/workdir")),
skin: Default::default(),
scroll_anchor: Some(ScrollAnchor::Last),
};
base_job.apply(&job_to_apply);
dbg!(&base_job);
assert_eq!(&base_job, &job_to_apply);
}