diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b794d6..9c3f59e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
### next
- support for nextest output & default nextest job, bound by default to the 'n' key - Fix #196
+- hit 'f' to have tests "scoped" to the failures - Fix #214
- new `exports` structure in configuration. New `analysis` export bound by default to `ctrl-e`. The old syntax defining locations export is still supported but won't appear in documentations anymore.
- recognize panic location in test - Fix #208
- lines to ignore can be specified as a set of regular expressions in a `ignored_lines` field either in the job or at the top of the prefs or bacon.toml - Fix #223
diff --git a/README.md b/README.md
index e35e8ba..fdf0ccf 100644
--- a/README.md
+++ b/README.md
@@ -65,8 +65,14 @@ This will run against all targets like `check-all` does.
bacon test
+or `bacon nextest` if you're a nextest user.
+
![bacon test](doc/test.png)
+
+When there's a failure, hit f to restrict the job to the failing test.
+Hit esc to get back to all tests.
+
## define your own jobs
First create a `bacon.toml` file by running
diff --git a/defaults/default-prefs.toml b/defaults/default-prefs.toml
index 84d8387..c8d5cdd 100644
--- a/defaults/default-prefs.toml
+++ b/defaults/default-prefs.toml
@@ -75,10 +75,9 @@ path = "bacon-analysis.json"
# ctrl-q = "quit"
# q = "quit"
# F5 = "rerun"
-# s = "toggle-summary"
-# w = "toggle-wrap"
-# b = "toggle-backtrace"
-# f = "toggle-backtrace(full)"
+# alt-s = "toggle-summary"
+# alt-w = "toggle-wrap"
+# alt-b = "toggle-backtrace"
# Home = "scroll-to-top"
# End = "scroll-to-bottom"
# Up = "scroll-lines(-1)"
diff --git a/src/analysis/analyzer.rs b/src/analysis/analyzer.rs
index d0cfdd4..5df3908 100644
--- a/src/analysis/analyzer.rs
+++ b/src/analysis/analyzer.rs
@@ -148,7 +148,7 @@ impl Analyzer {
_ => {}
}
}
- for (key, failure) in failures.drain() {
+ for (key, failure) in &failures {
// if we know of a failure but there was no content, we add some
if failure.has_title {
continue;
@@ -156,7 +156,7 @@ impl Analyzer {
fails.push(Line {
item_idx: 0, // will be filled later
line_type: LineType::Title(Kind::TestFail),
- content: TLine::failed(&key),
+ content: TLine::failed(key),
});
fails.push(Line {
item_idx: 0,
@@ -181,11 +181,13 @@ impl Analyzer {
let mut stats = Stats::from(&lines);
stats.passed_tests = passed_tests;
debug!("stats: {:#?}", &stats);
+ let failure_keys = failures.keys().cloned().collect();
let report = Report {
lines,
stats,
suggest_backtrace,
output: Default::default(),
+ failure_keys,
};
Ok(report)
}
diff --git a/src/app.rs b/src/app.rs
index 99ca0b0..b1ae9c1 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -159,7 +159,6 @@ pub fn run(
event_source.unblock(false);
}
}
- info!("action: {action:?}");
if let Some(action) = action.take() {
debug!("requested action: {action:?}");
match action {
@@ -192,6 +191,15 @@ pub fn run(
task_executor.die();
task_executor = state.start_computation(&mut executor)?;
}
+ Internal::ScopeToFailures => {
+ if let Some(scope) = state.failures_scope() {
+ info!("scoping to failures: {scope:#?}");
+ next_job = Some(JobRef::Scope(scope));
+ break;
+ } else {
+ warn!("no available failures scope");
+ }
+ }
Internal::ToggleRawOutput => {
state.toggle_raw_output();
}
diff --git a/src/cli.rs b/src/cli.rs
index 353834c..e2b5f4d 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -129,7 +129,7 @@ pub fn run() -> anyhow::Result<()> {
let mut result = Ok(());
#[allow(clippy::while_let_loop)]
loop {
- let (job_name, job) = match job_stack.pick_job(&next_job) {
+ let (concrete_job_ref, job) = match job_stack.pick_job(&next_job) {
Err(e) => {
result = Err(e);
break;
@@ -139,7 +139,7 @@ pub fn run() -> anyhow::Result<()> {
break;
}
};
- let r = Mission::new(&location, job_name.to_string(), job, &settings)
+ let r = Mission::new(&location, concrete_job_ref, job, &settings)
.and_then(|mission| app::run(&mut w, mission, &event_source));
match r {
Ok(Some(job_ref)) => {
diff --git a/src/help_line.rs b/src/help_line.rs
index 4924677..f17bc73 100644
--- a/src/help_line.rs
+++ b/src/help_line.rs
@@ -10,6 +10,7 @@ pub struct HelpLine {
close_help: Option,
pause: Option,
unpause: Option,
+ scope: Option,
}
impl HelpLine {
@@ -48,6 +49,9 @@ impl HelpLine {
.shortest_internal_key(Internal::Unpause)
.or(kb.shortest_internal_key(Internal::TogglePause))
.map(|k| format!("*{k}* to unpause"));
+ let scope = kb
+ .shortest_internal_key(Internal::ScopeToFailures)
+ .map(|k| format!("*{k}* to scope to failures"));
Self {
quit,
toggle_summary,
@@ -58,6 +62,7 @@ impl HelpLine {
close_help,
pause,
unpause,
+ scope,
}
}
pub fn markdown(
@@ -70,6 +75,11 @@ impl HelpLine {
parts.push(s);
}
} else {
+ if state.can_be_scoped() {
+ if let Some(s) = &self.scope {
+ parts.push(s);
+ }
+ }
if state.auto_refresh.is_paused() {
if let Some(s) = &self.unpause {
parts.push(s);
diff --git a/src/internal.rs b/src/internal.rs
index e818acf..cc1b9b0 100644
--- a/src/internal.rs
+++ b/src/internal.rs
@@ -12,6 +12,7 @@ pub enum Internal {
Quit,
Refresh, // clear and rerun
ReRun,
+ ScopeToFailures,
Scroll(ScrollCommand),
ToggleBacktrace(&'static str),
ToggleRawOutput,
@@ -33,6 +34,7 @@ impl fmt::Display for Internal {
Self::Quit => write!(f, "quit"),
Self::Refresh => write!(f, "clear then run current job again"),
Self::ReRun => write!(f, "run current job again"),
+ Self::ScopeToFailures => write!(f, "scope to failures"),
Self::Scroll(scroll_command) => scroll_command.fmt(f),
Self::ToggleBacktrace(level) => write!(f, "toggle backtrace ({level})"),
Self::ToggleRawOutput => write!(f, "toggle raw output"),
@@ -57,6 +59,7 @@ impl std::str::FromStr for Internal {
"quit" => Ok(Self::Quit),
"refresh" => Ok(Self::Refresh),
"rerun" => Ok(Self::ReRun),
+ "scope-to-failures" => Ok(Self::ScopeToFailures),
"toggle-raw-output" => Ok(Self::ToggleRawOutput),
"toggle-backtrace" => Ok(Self::ToggleBacktrace("1")),
"toggle-backtrace(1)" => Ok(Self::ToggleBacktrace("1")),
diff --git a/src/job_ref.rs b/src/job_ref.rs
index d6621c2..5d54094 100644
--- a/src/job_ref.rs
+++ b/src/job_ref.rs
@@ -1,4 +1,6 @@
use {
+ crate::*,
+ lazy_regex::*,
serde::{
Deserialize,
Deserializer,
@@ -16,25 +18,66 @@ pub enum JobRef {
Initial,
Previous,
Concrete(ConcreteJobRef),
+ Scope(Scope),
}
impl JobRef {
pub fn from_job_name>(s: S) -> Self {
- Self::Concrete(ConcreteJobRef::Name(s.into()))
+ Self::Concrete(ConcreteJobRef::from_job_name(s))
}
}
/// A "concrete" job ref is one which can be used from the start, without
/// referring to the job stack
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub enum ConcreteJobRef {
+pub struct ConcreteJobRef {
+ pub name_or_alias: NameOrAlias,
+ pub scope: Scope,
+}
+
+impl ConcreteJobRef {
+ pub fn from_job_name>(s: S) -> Self {
+ Self {
+ name_or_alias: NameOrAlias::Name(s.into()),
+ scope: Default::default(),
+ }
+ }
+ pub fn badge_label(&self) -> String {
+ let mut s = String::new();
+ match &self.name_or_alias {
+ NameOrAlias::Name(name) => {
+ s.push_str(name);
+ }
+ NameOrAlias::Alias(alias) => {
+ s.push_str(alias);
+ }
+ }
+ if self.scope.has_tests() {
+ s.push_str(" (scoped)");
+ }
+ s
+ }
+ pub fn with_scope(
+ mut self,
+ scope: Scope,
+ ) -> Self {
+ self.scope = scope;
+ self
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum NameOrAlias {
Name(String),
Alias(String),
}
impl Default for ConcreteJobRef {
fn default() -> Self {
- Self::Name("check".to_string())
+ Self {
+ name_or_alias: NameOrAlias::Name("check".to_string()),
+ scope: Default::default(),
+ }
}
}
@@ -43,10 +86,14 @@ impl fmt::Display for ConcreteJobRef {
&self,
f: &mut fmt::Formatter,
) -> fmt::Result {
- match self {
- Self::Alias(alias) => write!(f, "alias:{alias}"),
- Self::Name(name) => write!(f, "{name}"),
+ match &self.name_or_alias {
+ NameOrAlias::Alias(alias) => write!(f, "alias:{alias}")?,
+ NameOrAlias::Name(name) => write!(f, "{name}")?,
+ }
+ if self.scope.has_tests() {
+ write!(f, "({})", self.scope.tests.join(","))?;
}
+ Ok(())
}
}
@@ -72,10 +119,27 @@ impl<'de> Deserialize<'de> for ConcreteJobRef {
impl From<&str> for ConcreteJobRef {
fn from(str_entry: &str) -> Self {
- if let Some(alias) = str_entry.strip_prefix("alias:") {
- ConcreteJobRef::Alias(alias.to_string())
+ let Some((_, alias_prefix, name_or_alias, scope)) =
+ regex_captures!(r"^(alias:)?([^\(\)]+)(?:\(([^\)]+)\))?$", str_entry,)
+ else {
+ warn!("unexpected job ref: {:?}", str_entry);
+ return Self::from_job_name(str_entry.to_string());
+ };
+ let name_or_alias = if alias_prefix.is_empty() {
+ NameOrAlias::Name(name_or_alias.to_string())
} else {
- ConcreteJobRef::Name(str_entry.to_string())
+ NameOrAlias::Alias(name_or_alias.to_string())
+ };
+ let scope = Scope {
+ tests: scope
+ .split(',')
+ .filter(|t| !t.trim().is_empty())
+ .map(|s| s.to_string())
+ .collect(),
+ };
+ Self {
+ name_or_alias,
+ scope,
}
}
}
@@ -89,18 +153,70 @@ impl fmt::Display for JobRef {
Self::Default => write!(f, "default"),
Self::Initial => write!(f, "initial"),
Self::Previous => write!(f, "previous"),
- Self::Concrete(concrete) => concrete.fmt(f),
+ Self::Scope(Scope { tests }) => write!(f, "scope:{}", tests.join(",")),
+ Self::Concrete(concrete) => write!(f, "{}", concrete),
}
}
}
impl From<&str> for JobRef {
- fn from(name: &str) -> Self {
- match name {
- "default" => Self::Default,
- "initial" => Self::Initial,
- "previous" => Self::Previous,
- _ => Self::Concrete(ConcreteJobRef::from(name)),
- }
+ fn from(s: &str) -> Self {
+ regex_switch!(s,
+ "^default$"i => Self::Default,
+ "^default$"i => Self::Default,
+ "^initial$"i => Self::Initial,
+ "^previous$"i => Self::Previous,
+ "^scope:(?.+)$" => Self::Scope(Scope {
+ tests: tests
+ .split(',')
+ .filter(|t| !t.trim().is_empty())
+ .map(|s| s.to_string())
+ .collect(),
+ }),
+ )
+ .unwrap_or_else(|| Self::Concrete(ConcreteJobRef::from(s)))
+ }
+}
+
+#[test]
+fn test_job_ref_string_round_trip() {
+ let job_refs = vec![
+ JobRef::Default,
+ JobRef::Initial,
+ JobRef::Previous,
+ JobRef::Concrete(ConcreteJobRef {
+ name_or_alias: NameOrAlias::Name("run".to_string()),
+ scope: Scope::default(),
+ }),
+ JobRef::Concrete(ConcreteJobRef {
+ name_or_alias: NameOrAlias::Name("nextest".to_string()),
+ scope: Scope {
+ tests: vec!["first::test".to_string(), "second_test".to_string()],
+ },
+ }),
+ JobRef::Concrete(ConcreteJobRef {
+ name_or_alias: NameOrAlias::Alias("my-check".to_string()),
+ scope: Scope::default(),
+ }),
+ JobRef::Concrete(ConcreteJobRef {
+ name_or_alias: NameOrAlias::Alias("my-test".to_string()),
+ scope: Scope {
+ tests: vec!["abc".to_string()],
+ },
+ }),
+ JobRef::Concrete(ConcreteJobRef {
+ name_or_alias: NameOrAlias::Name("nextest".to_string()),
+ scope: Scope {
+ tests: vec!["abc".to_string()],
+ },
+ }),
+ JobRef::Scope(Scope {
+ tests: vec!["first::test".to_string(), "second_test".to_string()],
+ }),
+ ];
+ for job_ref in job_refs {
+ let s = job_ref.to_string();
+ let job_ref2 = JobRef::from(s.as_str());
+ assert_eq!(job_ref, job_ref2);
}
}
diff --git a/src/job_stack.rs b/src/job_stack.rs
index 9173d4f..b6e6fd1 100644
--- a/src/job_stack.rs
+++ b/src/job_stack.rs
@@ -6,8 +6,8 @@ use {
},
};
-/// The stack of jobs that bacon ran, allowing
-/// to get back to the previous one
+/// The stack of jobs that bacon ran, allowing to get back to the previous one,
+/// or to scope the current one
pub struct JobStack<'c> {
settings: &'c Settings,
entries: Vec,
@@ -28,6 +28,9 @@ impl<'c> JobStack<'c> {
.unwrap_or(&self.settings.default_job)
}
+ /// Apply the job ref instruction to determine the job to run, updating the stack.
+ ///
+ /// When no job is returned, the application is supposed to quit.
pub fn pick_job(
&mut self,
job_ref: &JobRef,
@@ -37,19 +40,38 @@ impl<'c> JobStack<'c> {
JobRef::Default => self.settings.default_job.clone(),
JobRef::Initial => self.initial_job().clone(),
JobRef::Previous => {
- self.entries.pop();
+ let current = self.entries.pop();
match self.entries.pop() {
Some(concrete) => concrete,
+ None if current
+ .as_ref()
+ .map_or(false, |current| current.scope.has_tests()) =>
+ {
+ // rather than quitting, we assume the user wants to "unscope"
+ ConcreteJobRef {
+ name_or_alias: current.unwrap().name_or_alias,
+ scope: Scope::default(),
+ }
+ }
None => {
return Ok(None);
}
}
}
JobRef::Concrete(concrete) => concrete.clone(),
+ JobRef::Scope(scope) => match self.entries.last() {
+ Some(concrete) => ConcreteJobRef {
+ name_or_alias: concrete.name_or_alias.clone(),
+ scope: scope.clone(),
+ },
+ None => {
+ return Ok(None);
+ }
+ },
};
- let job = match &concrete {
- ConcreteJobRef::Alias(alias) => Job::from_alias(alias, self.settings),
- ConcreteJobRef::Name(name) => self
+ let job = match &concrete.name_or_alias {
+ NameOrAlias::Alias(alias) => Job::from_alias(alias, self.settings),
+ NameOrAlias::Name(name) => self
.settings
.jobs
.get(name)
diff --git a/src/keybindings.rs b/src/keybindings.rs
index 8db8ce9..281be70 100644
--- a/src/keybindings.rs
+++ b/src/keybindings.rs
@@ -38,6 +38,7 @@ impl Default for KeyBindings {
bindings.set(key!(PageUp), Internal::Scroll(ScrollCommand::Pages(-1)));
bindings.set(key!(PageDown), Internal::Scroll(ScrollCommand::Pages(1)));
bindings.set(key!(Space), Internal::Scroll(ScrollCommand::Pages(1)));
+ bindings.set(key!(f), Internal::ScopeToFailures);
bindings.set(key!(esc), Internal::Back);
bindings.set(key!(ctrl - d), JobRef::Default);
bindings.set(key!(i), JobRef::Initial);
diff --git a/src/lib.rs b/src/lib.rs
index ac34a38..e5c41ce 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -30,6 +30,7 @@ mod mission;
mod mission_location;
mod on_change_strategy;
mod report;
+mod scope;
mod scroll;
mod settings;
mod state;
@@ -71,6 +72,7 @@ pub use {
mission_location::*,
on_change_strategy::*,
report::*,
+ scope::*,
scroll::*,
settings::*,
state::*,
diff --git a/src/mission.rs b/src/mission.rs
index b1a90d7..89b49b9 100644
--- a/src/mission.rs
+++ b/src/mission.rs
@@ -21,7 +21,7 @@ static DEFAULT_WATCHES: &[&str] = &["src", "tests", "benches", "examples", "buil
#[derive(Debug)]
pub struct Mission<'s> {
pub location_name: String,
- pub job_name: String,
+ pub concrete_job_ref: ConcreteJobRef,
pub cargo_execution_directory: PathBuf,
pub workspace_root: PathBuf,
pub job: Job,
@@ -33,7 +33,7 @@ pub struct Mission<'s> {
impl<'s> Mission<'s> {
pub fn new(
location: &MissionLocation,
- job_name: String,
+ concrete_job_ref: ConcreteJobRef,
job: Job,
settings: &'s Settings,
) -> Result {
@@ -69,7 +69,7 @@ impl<'s> Mission<'s> {
files_to_watch.push(full_path.into());
}
} else {
- info!("missing {} : {:?}", dir, full_path);
+ debug!("missing {} : {:?}", dir, full_path);
}
}
}
@@ -84,7 +84,7 @@ impl<'s> Mission<'s> {
let cargo_execution_directory = location.package_directory.clone();
Ok(Mission {
location_name,
- job_name,
+ concrete_job_ref,
cargo_execution_directory,
workspace_root: location.workspace_root.clone(),
job,
@@ -146,10 +146,8 @@ impl<'s> Mission<'s> {
/// build (and doesn't call) the external cargo command
pub fn get_command(&self) -> Command {
- let expanded;
- let command = if self.job.expand_env_vars {
- expanded = self
- .job
+ let mut command = if self.job.expand_env_vars {
+ self.job
.command
.iter()
.map(|token| {
@@ -164,11 +162,26 @@ impl<'s> Mission<'s> {
})
.to_string()
})
- .collect();
- &expanded
+ .collect()
} else {
- &self.job.command
+ self.job.command.clone()
};
+
+ let scope = &self.concrete_job_ref.scope;
+ if scope.has_tests() && command.len() > 2 {
+ let tests = if command[0] == "cargo" && command[1] == "test" {
+ // Here we're going around a limitation of the vanilla cargo test:
+ // it can only be scoped to one test
+ &scope.tests[..1]
+ } else {
+ &scope.tests
+ };
+ for test in tests {
+ command.push(test.to_string());
+ }
+ }
+
+ info!("command: {command:#?}");
let mut tokens = command.iter();
let mut command = Command::new(
tokens.next().unwrap(), // implies a check in the job
diff --git a/src/report.rs b/src/report.rs
index f902fa6..d27a2ff 100644
--- a/src/report.rs
+++ b/src/report.rs
@@ -20,6 +20,7 @@ pub struct Report {
pub stats: Stats,
pub suggest_backtrace: bool,
pub output: CommandOutput,
+ pub failure_keys: Vec,
}
impl Report {
diff --git a/src/scope.rs b/src/scope.rs
new file mode 100644
index 0000000..360ef8a
--- /dev/null
+++ b/src/scope.rs
@@ -0,0 +1,11 @@
+/// A dynamic reduction of a job execution
+#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
+pub struct Scope {
+ pub tests: Vec,
+}
+
+impl Scope {
+ pub fn has_tests(&self) -> bool {
+ !self.tests.is_empty()
+ }
+}
diff --git a/src/settings.rs b/src/settings.rs
index 7636d8b..9ceb519 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -157,7 +157,7 @@ impl Settings {
if self.jobs.is_empty() {
bail!("Invalid configuration : no job found");
}
- if let ConcreteJobRef::Name(name) = &self.default_job {
+ if let NameOrAlias::Name(name) = &self.default_job.name_or_alias {
if !self.jobs.contains_key(name) {
bail!("Invalid configuration : default job ({name:?}) not found in jobs");
}
diff --git a/src/state.rs b/src/state.rs
index 9c3cb3e..0a37968 100644
--- a/src/state.rs
+++ b/src/state.rs
@@ -144,6 +144,19 @@ impl<'s> AppState<'s> {
pub fn has_report(&self) -> bool {
matches!(self.cmd_result, CommandResult::Report(_))
}
+ pub fn can_be_scoped(&self) -> bool {
+ self.cmd_result
+ .report()
+ .map_or(false, |report| report.stats.can_scope_tests())
+ }
+ pub fn failures_scope(&self) -> Option {
+ if !self.can_be_scoped() {
+ return None;
+ }
+ self.cmd_result.report().map(|report| Scope {
+ tests: report.failure_keys.clone(),
+ })
+ }
pub fn toggle_raw_output(&mut self) {
self.raw_output ^= true;
}
@@ -155,11 +168,12 @@ impl<'s> AppState<'s> {
cmd_result.reverse();
}
match &cmd_result {
- CommandResult::Report(_) => {
- debug!("GOT REPORT");
+ CommandResult::Report(report) => {
+ debug!("Got report");
+ info!("Stats: {:#?}", report.stats);
}
CommandResult::Failure(_) => {
- debug!("GOT FAILURE");
+ debug!("Got failure");
}
CommandResult::None => {
debug!("GOT NONE ???");
@@ -412,7 +426,8 @@ impl<'s> AppState<'s> {
let project_name = &self.mission.location_name;
t_line.add_badge(TString::badge(project_name, 255, 240));
// black over pink
- t_line.add_badge(TString::badge(&self.mission.job_name, 235, 204));
+ let job_label = self.mission.concrete_job_ref.badge_label();
+ t_line.add_badge(TString::badge(&job_label, 235, 204));
if let CommandResult::Report(report) = &self.cmd_result {
let stats = &report.stats;
if stats.errors > 0 {
diff --git a/src/stats.rs b/src/stats.rs
index c9b62a6..26b318e 100644
--- a/src/stats.rs
+++ b/src/stats.rs
@@ -44,4 +44,7 @@ impl Stats {
pub fn items(&self) -> usize {
self.warnings + self.errors + self.test_fails
}
+ pub fn can_scope_tests(&self) -> bool {
+ self.passed_tests > 0 && self.test_fails > 0
+ }
}
diff --git a/src/tty/mod.rs b/src/tty/mod.rs
new file mode 100644
index 0000000..c7b877e
--- /dev/null
+++ b/src/tty/mod.rs
@@ -0,0 +1,38 @@
+mod tline;
+mod tstring;
+
+pub const CSI_RESET: &str = "\u{1b}[0m\u{1b}[0m";
+pub const CSI_BOLD: &str = "\u{1b}[1m";
+pub const CSI_ITALIC: &str = "\u{1b}[3m";
+
+pub const CSI_GREEN: &str = "\u{1b}[32m";
+
+pub const CSI_RED: &str = "\u{1b}[31m";
+pub const CSI_BOLD_RED: &str = "\u{1b}[1m\u{1b}[38;5;9m";
+pub const CSI_BOLD_ORANGE: &str = "\u{1b}[1m\u{1b}[38;5;208m";
+
+/// Used for "Blocking"
+pub const CSI_BLUE: &str = "\u{1b}[1m\u{1b}[36m";
+
+#[cfg(windows)]
+pub const CSI_BOLD_YELLOW: &str = "\u{1b}[1m\u{1b}[38;5;11m";
+#[cfg(not(windows))]
+pub const CSI_BOLD_YELLOW: &str = "\u{1b}[1m\u{1b}[33m";
+
+#[cfg(windows)]
+pub const CSI_BOLD_BLUE: &str = "\u{1b}[1m\u{1b}[38;5;14m";
+#[cfg(not(windows))]
+pub const CSI_BOLD_BLUE: &str = "\u{1b}[1m\u{1b}[38;5;12m";
+
+#[cfg(windows)]
+pub const CSI_BOLD_4BIT_YELLOW: &str = "\u{1b}[1m\u{1b}[33m";
+
+#[cfg(windows)]
+pub const CSI_BOLD_WHITE: &str = "\u{1b}[1m\u{1b}[38;5;15m";
+
+static TAB_REPLACEMENT: &str = " ";
+
+pub use {
+ tline::*,
+ tstring::*,
+};
diff --git a/src/tty.rs b/src/tty/tline.rs
similarity index 58%
rename from src/tty.rs
rename to src/tty/tline.rs
index 667bf5f..37d5a89 100644
--- a/src/tty.rs
+++ b/src/tty/tline.rs
@@ -1,157 +1,13 @@
use {
+ super::*,
crate::*,
anyhow::*,
serde::{
Deserialize,
Serialize,
},
- std::{
- fmt::Write as _,
- io::Write,
- },
- termimad::StrFit,
};
-pub const CSI_RESET: &str = "\u{1b}[0m\u{1b}[0m";
-pub const CSI_BOLD: &str = "\u{1b}[1m";
-pub const CSI_ITALIC: &str = "\u{1b}[3m";
-
-pub const CSI_GREEN: &str = "\u{1b}[32m";
-
-pub const CSI_RED: &str = "\u{1b}[31m";
-pub const CSI_BOLD_RED: &str = "\u{1b}[1m\u{1b}[38;5;9m";
-pub const CSI_BOLD_ORANGE: &str = "\u{1b}[1m\u{1b}[38;5;208m";
-
-/// Used for "Blocking"
-pub const CSI_BLUE: &str = "\u{1b}[1m\u{1b}[36m";
-
-#[cfg(windows)]
-pub const CSI_BOLD_YELLOW: &str = "\u{1b}[1m\u{1b}[38;5;11m";
-#[cfg(not(windows))]
-pub const CSI_BOLD_YELLOW: &str = "\u{1b}[1m\u{1b}[33m";
-
-#[cfg(windows)]
-pub const CSI_BOLD_BLUE: &str = "\u{1b}[1m\u{1b}[38;5;14m";
-#[cfg(not(windows))]
-pub const CSI_BOLD_BLUE: &str = "\u{1b}[1m\u{1b}[38;5;12m";
-
-#[cfg(windows)]
-pub const CSI_BOLD_4BIT_YELLOW: &str = "\u{1b}[1m\u{1b}[33m";
-
-#[cfg(windows)]
-pub const CSI_BOLD_WHITE: &str = "\u{1b}[1m\u{1b}[38;5;15m";
-
-static TAB_REPLACEMENT: &str = " ";
-
-/// a simple representation of a colored and styled string.
-///
-/// Note that this works because of a few properties of
-/// cargo's output:
-/// - styles and colors are always reset on changes
-/// - they're always in the same order (bold then fg color)
-///
-/// A more generic parsing would have to:
-/// - parse the csi params (it's simple enough to map but takes code)
-/// - use a simple state machine to keep style (bold, italic, etc.),
-/// foreground color, and background color across tstrings
-#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
-pub struct TString {
- pub csi: String,
- pub raw: String,
-}
-impl TString {
- pub fn new>(
- csi: S,
- raw: S,
- ) -> Self {
- Self {
- csi: csi.into(),
- raw: raw.into(),
- }
- }
- /// colors are 8bits ansi values
- pub fn badge(
- con: &str,
- fg: u8,
- bg: u8,
- ) -> Self {
- Self {
- csi: format!("\u{1b}[1m\u{1b}[38;5;{}m\u{1b}[48;5;{}m", fg, bg),
- raw: format!(" {} ", con),
- }
- }
- pub fn num_badge(
- num: usize,
- cat: &str,
- fg: u8,
- bg: u8,
- ) -> Self {
- let raw = if num < 2 {
- format!(" {} {} ", num, cat)
- } else {
- format!(" {} {}s ", num, cat)
- };
- Self::badge(&raw, fg, bg)
- }
- pub fn push_csi(
- &mut self,
- params: &[i64],
- action: char,
- ) {
- self.csi.push('\u{1b}');
- self.csi.push('[');
- for (idx, p) in params.iter().enumerate() {
- let _ = write!(self.csi, "{}", p);
- if idx < params.len() - 1 {
- self.csi.push(';');
- }
- }
- self.csi.push(action);
- }
- pub fn draw(
- &self,
- w: &mut W,
- ) -> Result<()> {
- if self.csi.is_empty() {
- write!(w, "{}", &self.raw)?;
- } else {
- write!(w, "{}{}{}", &self.csi, &self.raw, CSI_RESET,)?;
- }
- Ok(())
- }
- /// draw the string but without taking more than cols_max cols.
- /// Return the number of cols written
- pub fn draw_in(
- &self,
- w: &mut W,
- cols_max: usize,
- ) -> Result {
- let fit = StrFit::make_cow(&self.raw, cols_max);
- if self.csi.is_empty() {
- write!(w, "{}", &fit.0)?;
- } else {
- write!(w, "{}{}{}", &self.csi, &fit.0, CSI_RESET)?;
- }
- Ok(fit.1)
- }
- pub fn starts_with(
- &self,
- csi: &str,
- raw: &str,
- ) -> bool {
- self.csi == csi && self.raw.starts_with(raw)
- }
- pub fn split_off(
- &mut self,
- at: usize,
- ) -> Self {
- Self {
- csi: self.csi.clone(),
- raw: self.raw.split_off(at),
- }
- }
-}
-
/// a simple representation of a line made of homogeneous parts.
///
/// Note that this only manages CSI and SGR components
diff --git a/src/tty/tstring.rs b/src/tty/tstring.rs
new file mode 100644
index 0000000..7686181
--- /dev/null
+++ b/src/tty/tstring.rs
@@ -0,0 +1,123 @@
+use {
+ super::*,
+ crate::*,
+ anyhow::*,
+ serde::{
+ Deserialize,
+ Serialize,
+ },
+ std::{
+ fmt::Write as _,
+ io::Write,
+ },
+ termimad::StrFit,
+};
+
+/// a simple representation of a colored and styled string.
+///
+/// Note that this works because of a few properties of
+/// cargo's output:
+/// - styles and colors are always reset on changes
+/// - they're always in the same order (bold then fg color)
+///
+/// A more generic parsing would have to:
+/// - parse the csi params (it's simple enough to map but takes code)
+/// - use a simple state machine to keep style (bold, italic, etc.),
+/// foreground color, and background color across tstrings
+#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct TString {
+ pub csi: String,
+ pub raw: String,
+}
+impl TString {
+ pub fn new>(
+ csi: S,
+ raw: S,
+ ) -> Self {
+ Self {
+ csi: csi.into(),
+ raw: raw.into(),
+ }
+ }
+ /// colors are 8bits ansi values
+ pub fn badge(
+ con: &str,
+ fg: u8,
+ bg: u8,
+ ) -> Self {
+ Self {
+ csi: format!("\u{1b}[1m\u{1b}[38;5;{}m\u{1b}[48;5;{}m", fg, bg),
+ raw: format!(" {} ", con),
+ }
+ }
+ pub fn num_badge(
+ num: usize,
+ cat: &str,
+ fg: u8,
+ bg: u8,
+ ) -> Self {
+ let raw = if num < 2 {
+ format!(" {} {} ", num, cat)
+ } else {
+ format!(" {} {}s ", num, cat)
+ };
+ Self::badge(&raw, fg, bg)
+ }
+ pub fn push_csi(
+ &mut self,
+ params: &[i64],
+ action: char,
+ ) {
+ self.csi.push('\u{1b}');
+ self.csi.push('[');
+ for (idx, p) in params.iter().enumerate() {
+ let _ = write!(self.csi, "{}", p);
+ if idx < params.len() - 1 {
+ self.csi.push(';');
+ }
+ }
+ self.csi.push(action);
+ }
+ pub fn draw(
+ &self,
+ w: &mut W,
+ ) -> Result<()> {
+ if self.csi.is_empty() {
+ write!(w, "{}", &self.raw)?;
+ } else {
+ write!(w, "{}{}{}", &self.csi, &self.raw, CSI_RESET,)?;
+ }
+ Ok(())
+ }
+ /// draw the string but without taking more than cols_max cols.
+ /// Return the number of cols written
+ pub fn draw_in(
+ &self,
+ w: &mut W,
+ cols_max: usize,
+ ) -> Result {
+ let fit = StrFit::make_cow(&self.raw, cols_max);
+ if self.csi.is_empty() {
+ write!(w, "{}", &fit.0)?;
+ } else {
+ write!(w, "{}{}{}", &self.csi, &fit.0, CSI_RESET)?;
+ }
+ Ok(fit.1)
+ }
+ pub fn starts_with(
+ &self,
+ csi: &str,
+ raw: &str,
+ ) -> bool {
+ self.csi == csi && self.raw.starts_with(raw)
+ }
+ pub fn split_off(
+ &mut self,
+ at: usize,
+ ) -> Self {
+ Self {
+ csi: self.csi.clone(),
+ raw: self.raw.split_off(at),
+ }
+ }
+}
diff --git a/website/docs/config.md b/website/docs/config.md
index 7d3d845..2d5327a 100644
--- a/website/docs/config.md
+++ b/website/docs/config.md
@@ -176,6 +176,7 @@ toggle-raw-output | | display the untransformed command output
toggle-backtrace(level) | b | enable rust backtrace, level is either `1` or `full`
toggle-summary | s | display results as abstracts
toggle-wrap | w | toggle line wrapping
+scope-to-failures | f | restrict job to test failure(s)
scroll-to-top | Home | scroll to top
scroll-to-bottom | End | scroll to bottom
scroll-lines(-1) | ↑ | move one line up
diff --git a/website/docs/index.md b/website/docs/index.md
index 15e7290..1ea87be 100644
--- a/website/docs/index.md
+++ b/website/docs/index.md
@@ -42,6 +42,9 @@ bacon test
![test](img/test.png)
+When there's a failure, hit f to restrict the job to the failing test.
+Hit esc to get back to all tests.
+
While in bacon, you can see Clippy warnings by hitting the c key. And you get back to your previous job with esc
You may also open the `cargo doc` in your browser with the d key.