///|
/// ANSI styling for the CLI, in the palette PM2 uses through its color
/// library.
///
/// Colours are emitted only when standard output is a terminal, so piping
/// `pm3 list` into a file or another program prints plain text. `FORCE_COLOR`
/// overrides the terminal check (for tests and for `watch`-like consumers),
/// `NO_COLOR` turns colours off, and `--no-color` does the same for one
/// invocation. The escape sequences mirror PM2's exactly — bold cyan headers,
/// grey table borders, green/red/blue log prefixes — so the two tools' output
/// looks the same side by side.
///|
/// `isatty(3)` from the C library.
pub extern "C" fn pm3_isatty(fd : Int) -> Int = "pm3_isatty"
///|
/// Cached `colors_enabled()` answer: `-1` until computed, then `0` or `1`.
let colors_cache : Ref[Int] = Ref(-1)
///|
/// Enable or disable styling for the rest of this process.
pub fn set_colors_enabled(enabled : Bool) -> Unit {
colors_cache.val = if enabled { 1 } else { 0 }
}
///|
/// Whether output should be colorized.
///
/// `FORCE_COLOR` wins over everything: an empty value, `1` and `true` force
/// colours on, `0` and `false` force them off. Otherwise `NO_COLOR` (any
/// non-empty value) disables them, and the fallback is whether stdout is a
/// terminal.
pub fn colors_enabled() -> Bool {
if colors_cache.val >= 0 {
return colors_cache.val == 1
}
let enabled = match @env.get_env_var("FORCE_COLOR") {
Some(value) => {
let lowered = value.trim().to_lower().to_owned()
lowered != "0" && lowered != "false"
}
None =>
match @env.get_env_var("NO_COLOR") {
Some(value) if value.length() > 0 => false
_ => pm3_isatty(1) != 0
}
}
colors_cache.val = if enabled { 1 } else { 0 }
enabled
}
///|
/// Wrap `text` in an ANSI select and its matching reset, unless colours are
/// off.
fn ansi(text : String, open : String, close : String) -> String {
if !colors_enabled() {
return text
}
open + text + close
}
///|
/// Green text — PM2's colour for "online" and for stdout log lines.
pub fn style_green(text : String) -> String {
ansi(text, "\u{1b}[32m", "\u{1b}[39m")
}
///|
/// Red text — PM2's colour for stopped/errored states and stderr log lines.
pub fn style_red(text : String) -> String {
ansi(text, "\u{1b}[31m", "\u{1b}[39m")
}
///|
/// Blue text — PM2's colour for the daemon's own log lines.
pub fn style_blue(text : String) -> String {
ansi(text, "\u{1b}[34m", "\u{1b}[39m")
}
///|
/// Cyan text.
pub fn style_cyan(text : String) -> String {
ansi(text, "\u{1b}[36m", "\u{1b}[39m")
}
///|
/// Yellow text — PM2's colour for restarting/created states.
pub fn style_yellow(text : String) -> String {
ansi(text, "\u{1b}[33m", "\u{1b}[39m")
}
///|
/// Grey text — PM2 draws table borders and muted notes in grey.
pub fn style_gray(text : String) -> String {
ansi(text, "\u{1b}[90m", "\u{1b}[39m")
}
///|
/// Bold text.
pub fn style_bold(text : String) -> String {
ansi(text, "\u{1b}[1m", "\u{1b}[22m")
}
///|
/// Faint text.
pub fn style_dim(text : String) -> String {
ansi(text, "\u{1b}[2m", "\u{1b}[22m")
}
///|
/// Inverted text, for the banner PM2 draws around a title.
pub fn style_inverse(text : String) -> String {
ansi(text, "\u{1b}[7m", "\u{1b}[27m")
}
///|
/// Bold cyan, the style PM2 gives table headers and `pm3 show` keys.
pub fn bold_cyan(text : String) -> String {
style_bold(style_cyan(text))
}
///|
/// Bold grey, the style PM2 gives the `[TAILING] …` note.
pub fn bold_gray(text : String) -> String {
style_bold(style_gray(text))
}
///|
/// Bold green, the style PM2 gives the `online` status.
pub fn green_bold(text : String) -> String {
style_bold(style_green(text))
}
///|
/// Bold red, the style PM2 gives `errored` and friends.
pub fn red_bold(text : String) -> String {
style_bold(style_red(text))
}
///|
/// Bold blue, the style PM2 gives the `launching` status.
pub fn blue_bold(text : String) -> String {
style_bold(style_blue(text))
}
///|
/// Bold yellow, the style PM2 gives `restarting` and `created`.
pub fn yellow_bold(text : String) -> String {
style_bold(style_yellow(text))
}
///|
/// Bold inverse, the style PM2 gives the ` Describing process … ` banner.
pub fn bold_inverse(text : String) -> String {
style_bold(style_inverse(text))
}
///|
/// Bright-black background, the style PM2 gives search highlights in logs.
pub fn bg_black_bright(text : String) -> String {
ansi(text, "\u{1b}[100m", "\u{1b}[49m")
}
///|
/// PM2's `UxHelpers.colorizedMetric`: a zero stays plain, anything below
/// `warn` is green, up to `alert` bold yellow, above it bold red — the way
/// `pm2 ls` colours the `host metrics` footer.
pub fn colorized_metric(
value : Double,
warn : Double,
alert : Double,
text : String,
) -> String {
if value.is_nan() {
return "N/A"
}
if value == 0.0 {
return text
}
if value < warn {
style_green(text)
} else if value <= alert {
yellow_bold(text)
} else {
red_bold(text)
}
}
///|
/// The length of `text` as it appears on a terminal: ANSI escape sequences
/// contribute nothing.
pub fn visible_width(text : String) -> Int {
let mut width = 0
let mut i = 0
let length = text.length()
while i < length {
match text.get_char(i) {
Some('\u{1b}') => {
i = i + 1
if i < length && text.get_char(i) == Some('[') {
i = i + 1
let mut done = false
while i < length && !done {
match text.get_char(i) {
Some(c) => {
i = i + 1
// CSI ends at the first byte in the range `@` to `~`.
if c >= '@' && c <= '~' {
done = true
}
}
None => done = true
}
}
}
}
Some(_) => {
width = width + 1
i = i + 1
}
None => i = i + 1
}
}
width
}
///|
/// Pad `text` on the right with spaces until it is `width` terminal columns
/// wide. Unlike `pad_right`, ANSI escape sequences in `text` are not counted,
/// which is what lets `render_box` lay out pre-coloured cells.
pub fn pad_visible(text : String, width : Int, pad : Char) -> String {
let visible = visible_width(text)
if visible >= width {
return text
}
let buf = StringBuilder()
buf.write_string(text)
for _ in 0..<(width - visible) {
buf.write_char(pad)
}
buf.to_string()
}