use crate::task::{LinkType, Status, Task};
use owo_colors::OwoColorize;
use std::io::IsTerminal;
use std::sync::OnceLock;
static GLOBAL: OnceLock<Display> = OnceLock::new();
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Display {
color: bool,
unicode: bool,
}
impl Display {
pub fn new(color: bool, unicode: bool) -> Self {
Display { color, unicode }
}
pub fn plain() -> Self {
Display::new(false, false)
}
pub fn styled() -> Self {
Display::new(true, true)
}
#[allow(clippy::fn_params_excessive_bools)]
pub fn resolve(plain: bool, no_color: bool, clicolor_zero: bool, is_tty: bool) -> Self {
let color = !plain && !no_color && !clicolor_zero && is_tty;
let unicode = !plain && !no_color && is_tty;
Display { color, unicode }
}
pub fn detect(plain: bool) -> Self {
let no_color = std::env::var_os("NO_COLOR").is_some();
let clicolor_zero = std::env::var("CLICOLOR")
.map(|v| v == "0")
.unwrap_or(false);
let is_tty = std::io::stdout().is_terminal();
Self::resolve(plain, no_color, clicolor_zero, is_tty)
}
pub fn use_color(&self) -> bool {
self.color
}
pub fn use_unicode(&self) -> bool {
self.unicode
}
pub fn prio_dot(&self, p: u8) -> String {
let dot = if self.unicode { "●" } else { "*" };
if !self.color {
return dot.to_string();
}
match p {
1 => dot.red().to_string(),
2 => dot.yellow().to_string(),
3 => dot.blue().to_string(),
_ => dot.bright_black().to_string(),
}
}
pub fn status_glyph(&self, s: &Status) -> &'static str {
if self.unicode {
match s {
Status::Open => "○",
Status::InProgress => "»",
Status::Review => "?",
Status::Blocked => "⌀",
Status::Closed => "✓",
Status::Deferred => "~",
Status::Unknown(_) => "·",
}
} else {
match s {
Status::Open => "[ ]",
Status::InProgress => "[>]",
Status::Review => "[?]",
Status::Blocked => "[!]",
Status::Closed => "[x]",
Status::Deferred => "[-]",
Status::Unknown(_) => "[.]",
}
}
}
pub fn status_word(&self, s: &Status) -> String {
let w = s.as_str();
if !self.color {
return w.to_string();
}
match s {
Status::Open => w.dimmed().to_string(),
Status::InProgress => w.yellow().to_string(),
Status::Review => w.cyan().to_string(),
Status::Blocked => w.magenta().to_string(),
Status::Closed => w.green().to_string(),
Status::Deferred => w.bright_black().to_string(),
Status::Unknown(_) => w.to_string(),
}
}
pub fn claimed_badge(&self, t: &Task, me: &str) -> &'static str {
match &t.claimed_by {
Some(c) if c == me => {
if self.unicode {
"★"
} else {
"*"
}
}
_ => "",
}
}
pub fn deps_badge(&self, t: &Task, all: &[Task]) -> &'static str {
let unmet = t.depends_on.iter().any(|dep_id| {
all.iter()
.any(|o| &o.id == dep_id && !matches!(o.status, Status::Closed))
});
if unmet {
if self.unicode {
"◆"
} else {
"D"
}
} else {
""
}
}
pub fn gates_badge(&self, t: &Task, all: &[Task]) -> &'static str {
let open = t.links.iter().any(|l| {
matches!(l.link_type, LinkType::Gates)
&& all
.iter()
.any(|o| o.id == l.target && !matches!(o.status, Status::Closed))
});
if open {
if self.unicode {
"⛓"
} else {
"G"
}
} else {
""
}
}
pub fn tree_prefix(&self, depth: usize, is_last: bool, ancestors: &[bool]) -> String {
if depth == 0 {
return String::new();
}
let (vbar, corner, tee, pad) = if self.unicode {
("│ ", "└─ ", "├─ ", " ")
} else {
("| ", "`- ", "|- ", " ")
};
let mut s = String::new();
for &anc_last in ancestors {
s.push_str(if anc_last { pad } else { vbar });
}
s.push_str(if is_last { corner } else { tee });
s
}
}
pub fn init(plain: bool) {
let _ = GLOBAL.set(Display::detect(plain));
}
pub fn global() -> Display {
GLOBAL.get().copied().unwrap_or_else(Display::plain)
}
#[cfg(test)]
#[path = "display_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "display_badge_tests.rs"]
mod badge_tests;