use std::io;
use alba_engine::RunEvent;
use owo_colors::{AnsiColors, OwoColorize};
use super::{LineSink, Renderer, format_duration, print_summary, status_label};
const PALETTE: [AnsiColors; 6] = [
AnsiColors::Cyan,
AnsiColors::Green,
AnsiColors::Yellow,
AnsiColors::Blue,
AnsiColors::Magenta,
AnsiColors::BrightCyan,
];
const SEPARATOR: &str = "\u{2502}";
pub struct InterleavedRenderer {
color: bool,
clear_between_runs: bool,
affected_by: Option<String>,
out: LineSink<io::Stdout>,
err: LineSink<io::Stderr>,
}
impl InterleavedRenderer {
pub fn new(color: bool, clear_between_runs: bool) -> Self {
Self {
color,
clear_between_runs,
affected_by: None,
out: LineSink::stdout(),
err: LineSink::stderr(),
}
}
fn line(&mut self, id: &str, text: &str) {
let prefix = if self.color {
id.color(color_for(id)).to_string()
} else {
id.to_string()
};
self.out.line(&format!("{prefix} {SEPARATOR} {text}"));
}
}
impl Renderer for InterleavedRenderer {
fn handle(&mut self, event: &RunEvent) {
if super::handle_watch_event(event, &mut self.out, &mut self.err, self.clear_between_runs) {
return;
}
match event {
RunEvent::BeamStarted { id } => self.line(&id.0, "started"),
RunEvent::BeamCached { id } => self.line(&id.0, "cached — replaying last output"),
RunEvent::BeamOutput { id, line, .. } => self.line(&id.0, &line.text),
RunEvent::BeamFinished {
id,
status,
duration,
} => {
let text = if matches!(status, alba_engine::BeamStatus::Cached) {
format!("cached in {} (original run)", format_duration(*duration))
} else {
format!("{} in {}", status_label(status), format_duration(*duration))
};
self.line(&id.0, &text);
}
RunEvent::RunFinished { summary } => {
print_summary(&mut self.err, summary, self.affected_by.as_deref());
}
RunEvent::RunStarted { affected_by, .. } => self.affected_by = affected_by.clone(),
RunEvent::WatchWaiting { .. } | RunEvent::WatchTriggered { .. } => {}
RunEvent::ProjectBroken { diagnostic } => {
self.err.line(diagnostic.trim_end());
}
}
}
}
fn color_for(id: &str) -> AnsiColors {
PALETTE[(fnv1a(id) % PALETTE.len() as u64) as usize]
}
fn fnv1a(text: &str) -> u64 {
const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
text.bytes().fold(OFFSET_BASIS, |hash, byte| {
(hash ^ u64::from(byte)).wrapping_mul(PRIME)
})
}
#[cfg(test)]
mod tests {
use super::*;
fn color_name(id: &str) -> String {
format!("{:?}", color_for(id))
}
#[test]
fn a_beam_id_always_gets_the_same_color() {
assert_eq!(color_name("build"), color_name("build"));
assert_eq!(color_name("api:test"), color_name("api:test"));
}
#[test]
fn different_ids_spread_across_the_palette() {
let ids = ["build", "test", "lint", "deploy", "docs", "bench"];
let used: std::collections::HashSet<_> = ids.iter().map(|id| color_name(id)).collect();
assert!(
used.len() > 1,
"a palette that collapses every id to one colour is useless"
);
}
}