pub mod command;
pub mod summary;
pub mod tasks;
pub use command::{CommandRecord, CommandRunner, CommandStatus, CommandView, OutputLine, Stream};
pub use summary::{CompletionSummary, SummaryItem};
pub use tasks::{TaskId, TaskTree, TaskTreeView};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use rich::{Console, Style, StyleType, Text};
use crate::a11y::{AccessibilityPolicy, Status, SymbolSet};
pub const STYLES: &[(&str, &str)] = &[
("workflow.prompt", "dim"),
("workflow.command", "bold"),
("workflow.cwd", "dim"),
("workflow.stdout", "none"),
("workflow.stderr", "yellow"),
("workflow.gutter", "dim"),
("workflow.hidden", "dim italic"),
("workflow.duration", "dim"),
("workflow.guide", "dim"),
("workflow.status.pending", "dim"),
("workflow.status.running", "bold cyan"),
("workflow.status.ok", "bold green"),
("workflow.status.warning", "bold yellow"),
("workflow.status.error", "bold red"),
("workflow.status.skipped", "dim"),
("workflow.status.cancelled", "bold magenta"),
("workflow.task.label", "none"),
("workflow.task.running", "bold"),
("workflow.task.progress", "cyan"),
("workflow.task.note", "italic"),
("workflow.summary.title", "bold"),
("workflow.summary.counts", "none"),
("workflow.summary.next", "bold"),
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum State {
Failed,
Cancelled,
Warning,
Running,
Pending,
Succeeded,
Skipped,
}
impl State {
pub const ALL: [State; 7] = [
State::Failed,
State::Cancelled,
State::Warning,
State::Running,
State::Pending,
State::Succeeded,
State::Skipped,
];
pub fn is_finished(self) -> bool {
!matches!(self, State::Running | State::Pending)
}
pub fn is_problem(self) -> bool {
matches!(self, State::Failed | State::Cancelled)
}
pub fn status(self) -> Option<Status> {
match self {
State::Failed => Some(Status::Error),
State::Warning => Some(Status::Warning),
State::Pending => Some(Status::Pending),
State::Succeeded => Some(Status::Ok),
State::Skipped => Some(Status::Skipped),
State::Running | State::Cancelled => None,
}
}
pub fn word(self) -> &'static str {
match self.status() {
Some(status) => status.word(),
None if self == State::Running => "running",
None => "cancelled",
}
}
pub fn marker(self, set: SymbolSet) -> &'static str {
if let Some(status) = self.status() {
return status.symbol(set);
}
match (self, set) {
(State::Running, SymbolSet::Unicode) => "▶ running",
(State::Running, SymbolSet::Ascii) => "[RUN]",
(State::Running, SymbolSet::Words) => "running:",
(_, SymbolSet::Unicode) => "⊘ cancelled",
(_, SymbolSet::Ascii) => "[CANCEL]",
(_, SymbolSet::Words) => "cancelled:",
}
}
pub fn style_key(self) -> &'static str {
match self {
State::Failed => "workflow.status.error",
State::Cancelled => "workflow.status.cancelled",
State::Warning => "workflow.status.warning",
State::Running => "workflow.status.running",
State::Pending => "workflow.status.pending",
State::Succeeded => "workflow.status.ok",
State::Skipped => "workflow.status.skipped",
}
}
pub fn count_label(self, count: usize) -> String {
let word = match (self, count) {
(State::Failed, _) => "failed",
(State::Cancelled, _) => "cancelled",
(State::Warning, 1) => "warning",
(State::Warning, _) => "warnings",
(State::Running, _) => "running",
(State::Pending, _) => "pending",
(State::Succeeded, _) => "succeeded",
(State::Skipped, _) => "skipped",
};
format!("{count} {word}")
}
}
pub trait Clock: Send + Sync {
fn now(&self) -> Duration;
}
#[derive(Clone, Copy, Debug)]
pub struct SystemClock {
origin: Instant,
}
impl SystemClock {
pub fn new() -> Self {
SystemClock {
origin: Instant::now(),
}
}
}
impl Default for SystemClock {
fn default() -> Self {
Self::new()
}
}
impl Clock for SystemClock {
fn now(&self) -> Duration {
self.origin.elapsed()
}
}
#[derive(Clone, Debug, Default)]
pub struct ManualClock {
nanos: Arc<AtomicU64>,
}
impl ManualClock {
pub fn new() -> Self {
Self::default()
}
pub fn advance(&self, by: Duration) {
self.nanos.fetch_add(saturating_nanos(by), Ordering::SeqCst);
}
pub fn set(&self, to: Duration) {
self.nanos.store(saturating_nanos(to), Ordering::SeqCst);
}
}
fn saturating_nanos(d: Duration) -> u64 {
u64::try_from(d.as_nanos()).unwrap_or(u64::MAX)
}
impl Clock for ManualClock {
fn now(&self) -> Duration {
Duration::from_nanos(self.nanos.load(Ordering::SeqCst))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Look {
pub(crate) symbols: SymbolSet,
pub(crate) animate: bool,
}
impl Default for Look {
fn default() -> Self {
Look {
symbols: SymbolSet::Unicode,
animate: true,
}
}
}
impl Look {
pub(crate) fn from_policy(policy: &AccessibilityPolicy) -> Self {
Look {
symbols: policy.status_symbols,
animate: !(policy.no_animation || policy.reduced_motion || policy.screen_reader),
}
}
pub(crate) fn ascii(self) -> bool {
self.symbols != SymbolSet::Unicode
}
pub(crate) fn marker(self, state: State, elapsed: Duration) -> String {
if state == State::Running && self.animate && !self.ascii() {
let spinner = rich::spinner::Spinner::new("dots");
spinner.render(0.0);
let frame = spinner.render(elapsed.as_secs_f64());
return format!("{} running", frame.plain());
}
state.marker(self.symbols).to_string()
}
pub(crate) fn duration(self, d: Duration) -> String {
crate::format::duration_with(d, self.ascii())
}
pub(crate) fn ellipsis(self) -> &'static str {
if self.ascii() {
"..."
} else {
"…"
}
}
}
pub(crate) fn style(console: &Console, key: &str) -> Style {
let fallback = STYLES
.iter()
.find(|(name, _)| *name == key)
.map_or("none", |(_, spec)| spec);
crate::event::theme_style(console, key, fallback)
}
pub(crate) fn span(console: &Console, key: &str) -> Option<StyleType> {
let style = style(console, key);
(!style.is_null()).then(|| style.into())
}
pub(crate) fn render_texts(
console: &Console,
options: &rich::ConsoleOptions,
lines: &[Text],
) -> Vec<rich::Segment> {
if options.max_width == 0 || options.height == Some(0) {
return Vec::new();
}
let mut unbounded = options.clone();
unbounded.height = None;
let mut rows = Vec::new();
for line in lines {
rows.extend(console.render_lines(line, &unbounded, false));
}
if let Some(height) = options.height {
rows.truncate(height);
}
crate::event::flatten(rows)
}