use std::io::{self, Write};
use std::sync::OnceLock;
use crate::Outcome;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cached {
Hit,
RemoteHit,
Miss,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Event {
Planned {
tasks: usize,
},
Started {
id: String,
checking: bool,
},
StartedContinuous {
id: String,
},
Cache {
id: String,
cached: Cached,
},
Finished {
id: String,
outcome: Outcome,
},
Skipped {
id: String,
},
Stopping {
id: String,
},
Stopped {
id: String,
},
}
impl std::fmt::Display for Event {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Planned { .. } => Ok(()),
Self::Started { id, checking: true } => write!(f, "qk: checking {id}"),
Self::Started {
id,
checking: false,
} => write!(f, "qk: running {id}"),
Self::StartedContinuous { id } => write!(f, "qk: started {id} (continuous)"),
Self::Cache {
id,
cached: Cached::Hit,
} => write!(f, "qk: cache hit {id}"),
Self::Cache {
id,
cached: Cached::RemoteHit,
} => write!(f, "qk: remote cache hit {id}"),
Self::Cache {
id,
cached: Cached::Miss,
} => write!(f, "qk: cache miss {id}"),
Self::Finished {
id,
outcome: Outcome::Success,
} => write!(f, "qk: finished {id}"),
Self::Finished {
id,
outcome: Outcome::Failed(code),
} => {
write!(f, "qk: failed {id} (exit {code})")
}
Self::Finished {
id,
outcome: Outcome::Cancelled,
} => write!(f, "qk: cancelled {id}"),
Self::Skipped { id } => write!(f, "qk: skipped {id} (dependency failed)"),
Self::Stopping { id } => write!(f, "qk: stopping {id} (no longer needed)"),
Self::Stopped { id } => write!(f, "qk: stopped {id}"),
}
}
}
pub trait Sink: Send + Sync {
fn event(&self, event: &Event);
fn warning(&self, line: &str);
fn output(&self, stderr: bool, bytes: &[u8]);
}
impl<T: Sink + ?Sized> Sink for std::sync::Arc<T> {
fn event(&self, event: &Event) {
(**self).event(event);
}
fn warning(&self, line: &str) {
(**self).warning(line);
}
fn output(&self, stderr: bool, bytes: &[u8]) {
(**self).output(stderr, bytes);
}
}
pub struct Lines;
impl Sink for Lines {
fn event(&self, event: &Event) {
let line = event.to_string();
if !line.is_empty() {
write_all(true, format!("{line}\n").as_bytes());
}
}
fn warning(&self, line: &str) {
write_all(true, format!("{line}\n").as_bytes());
}
fn output(&self, stderr: bool, bytes: &[u8]) {
write_all(stderr, bytes);
}
}
pub fn write_all(stderr: bool, bytes: &[u8]) {
let _ = if stderr {
io::stderr().lock().write_all(bytes)
} else {
io::stdout().lock().write_all(bytes)
};
}
static SINK: OnceLock<Box<dyn Sink>> = OnceLock::new();
pub fn install(sink: Box<dyn Sink>) {
let _ = SINK.set(sink);
}
fn sink() -> &'static dyn Sink {
SINK.get().map_or(&Lines, |sink| sink.as_ref())
}
pub fn event(event: Event) {
sink().event(&event);
}
pub fn warning(line: &str) {
sink().warning(line);
}
pub fn output(stderr: bool, bytes: &[u8]) {
if !bytes.is_empty() {
sink().output(stderr, bytes);
}
}