// Standard library: ANSI terminal color and style codes. // // `colors_enabled` only checks NO_COLOR / FORCE_COLOR / TERM via IO.get_env — // there is no isatty/TTY check, so piped output is not auto-disabled unless // NO_COLOR is set. This is a deliberate scope cut (see // implementations/ansi-colors.md in the plans repo), not an oversight. use lib::io {} open IO {get_env} pub type Color { black, red, green, yellow, blue, magenta, cyan, white, bright_black, bright_red, bright_green, bright_yellow, bright_blue, bright_magenta, bright_cyan, bright_white, color256 (code : U8), true_color (r : U8) (g : U8) (b : U8) } /// Structural equality on `Color`, variant by variant; `color256` compares /// its code and `true_color` all three channels. pub def Color.beq (a b : Color) : Bool := match a { black => match b { black => true, _ => false }, red => match b { red => true, _ => false }, green => match b { green => true, _ => false }, yellow => match b { yellow => true, _ => false }, blue => match b { blue => true, _ => false }, magenta => match b { magenta => true, _ => false }, cyan => match b { cyan => true, _ => false }, white => match b { white => true, _ => false }, bright_black => match b { bright_black => true, _ => false }, bright_red => match b { bright_red => true, _ => false }, bright_green => match b { bright_green => true, _ => false }, bright_yellow => match b { bright_yellow => true, _ => false }, bright_blue => match b { bright_blue => true, _ => false }, bright_magenta => match b { bright_magenta => true, _ => false }, bright_cyan => match b { bright_cyan => true, _ => false }, bright_white => match b { bright_white => true, _ => false }, color256 ca => match b { color256 cb => ca == cb, _ => false }, true_color ra ga ba => match b { true_color rb gb bb => ra == rb && ga == gb && ba == bb, _ => false } } instance BEq Color { def beq (a b : Color) : Bool := Color.beq a b } type Style { bold, dim, italic, underline, blink, reverse, hidden, strikethrough } type Modifier { fg (color : Color), bg (color : Color), style (s : Style), reset } pub def color_fg_code (c : Color) : String := match c { black => "30", red => "31", green => "32", yellow => "33", blue => "34", magenta => "35", cyan => "36", white => "37", bright_black => "90", bright_red => "91", bright_green => "92", bright_yellow => "93", bright_blue => "94", bright_magenta => "95", bright_cyan => "96", bright_white => "97", color256 code => "38;5;" ++ U8.to_string code, true_color r g b => "38;2;" ++ U8.to_string r ++ ";" ++ U8.to_string g ++ ";" ++ U8.to_string b } pub def color_bg_code (c : Color) : String := match c { black => "40", red => "41", green => "42", yellow => "43", blue => "44", magenta => "45", cyan => "46", white => "47", bright_black => "100", bright_red => "101", bright_green => "102", bright_yellow => "103", bright_blue => "104", bright_magenta => "105", bright_cyan => "106", bright_white => "107", color256 code => "48;5;" ++ U8.to_string code, true_color r g b => "48;2;" ++ U8.to_string r ++ ";" ++ U8.to_string g ++ ";" ++ U8.to_string b } def style_code (s : Style) : String := match s { bold => "1", dim => "2", italic => "3", underline => "4", blink => "5", reverse => "7", hidden => "8", strikethrough => "9" } /// Constructs an ANSI escape sequence for a single modifier. def escape (m : Modifier) : String := match m { fg c => "[" ++ color_fg_code c ++ "m", bg c => "[" ++ color_bg_code c ++ "m", style s => "[" ++ style_code s ++ "m", reset => "" } def red (s : String) : String := escape (Modifier.fg Color.red) ++ s ++ escape Modifier.reset def green (s : String) : String := escape (Modifier.fg Color.green) ++ s ++ escape Modifier.reset def yellow (s : String) : String := escape (Modifier.fg Color.yellow) ++ s ++ escape Modifier.reset def bold (s : String) : String := escape (Modifier.style Style.bold) ++ s ++ escape Modifier.reset def dim (s : String) : String := escape (Modifier.style Style.dim) ++ s ++ escape Modifier.reset /// Bold red — for failure output. /// /// Dotted on purpose (`Ansi.fail`, the `Bench.now` convention): a BARE /// `def fail` here poisons every program that loads this module. /// `qualify_modules`'s `resolve_owner` rule 3 resolves a bare reference /// to "exactly one module [that] declares it" -- the only DECLARER it /// knows is defs, not `open`d constructors, so once a bare `def fail` /// entered the loaded set, `lang/parser.mo`'s own bare `fail` /// (`open ParseResult {fail, success}` -- a CONSTRUCTOR, invisible to /// rule 3) was rewritten to `std.ansi::fail` in the self-compiled /// binary and SIGSEGV'd in `String.concat` on the first parse (a /// `fail`-of-the-parser call became a String style-wrap). `pass` is /// renamed for the same reason: it is exactly as common a word, and the /// next `open X {pass}` would walk into the same trap. def Ansi.fail (s : String) : String := bold (red s) /// Bold green — for success output. See `Ansi.fail` for the dotted name. def Ansi.pass (s : String) : String := bold (green s) /// Yellow — for warning output. pub def warn (s : String) : String := escape (Modifier.fg Color.yellow) ++ s ++ escape Modifier.reset def env_flag_set (v : Option String) : Bool := match v { some _ => true, none => false } def term_is_dumb (v : Option String) : Bool := match v { some t => t == "dumb", none => false } /// Whether ANSI colors should be used, per NO_COLOR / FORCE_COLOR / TERM. /// NO_COLOR (https://no-color.org) always wins; FORCE_COLOR forces colors on /// unless NO_COLOR is set; otherwise colors are on unless TERM=dumb. def colors_enabled : IO Bool := do { let no_color <- get_env "NO_COLOR"; if env_flag_set no_color then return false else do { let force_color <- get_env "FORCE_COLOR"; if env_flag_set force_color then return true else do { let term <- get_env "TERM"; return (Bool.not (term_is_dumb term)) } } } /// Wraps `s` in `color` if colors are enabled, otherwise returns `s` unchanged. pub def colored (s : String) (color : Color) : IO String := do { let enabled <- colors_enabled; if enabled then return (escape (Modifier.fg color) ++ s ++ escape Modifier.reset) else return s } // ---------- Tests ---------- #[test] def test_color_beq_same_variant : Bool := Color.red == Color.red #[test] def test_color_beq_different_variant : Bool := Bool.not (Color.red == Color.green) #[test] def test_color_beq_color256_compares_code : Bool := Color.color256 42u8 == Color.color256 42u8 && Bool.not (Color.color256 1u8 == Color.color256 2u8) #[test] def test_color_beq_true_color_compares_all_fields : Bool := Color.true_color 1u8 2u8 3u8 == Color.true_color 1u8 2u8 3u8 && Bool.not (Color.true_color 1u8 2u8 3u8 == Color.true_color 1u8 2u8 9u8) #[test] def test_escape_reset : Bool := escape Modifier.reset == "" #[test] def test_escape_fg_red : Bool := escape (Modifier.fg Color.red) == "" #[test] def test_escape_bg_blue : Bool := escape (Modifier.bg Color.blue) == "" #[test] def test_escape_style_bold : Bool := escape (Modifier.style Style.bold) == "" #[test] def test_escape_color256 : Bool := escape (Modifier.fg (Color.color256 42u8)) == "" #[test] def test_escape_true_color : Bool := escape (Modifier.fg (Color.true_color 1u8 2u8 3u8)) == "" #[test] def test_red_wraps_and_resets : Bool := red "x" == "x" #[test] def test_green_wraps_and_resets : Bool := green "x" == "x" #[test] def test_bold_wraps_and_resets : Bool := bold "x" == "x" #[test] def test_fail_is_bold_red : Bool := Ansi.fail "x" == bold (red "x") #[test] def test_pass_is_bold_green : Bool := Ansi.pass "x" == bold (green "x") #[test] def test_env_flag_set_some : Bool := env_flag_set (Option.some "1") #[test] def test_env_flag_set_none : Bool := Bool.not (env_flag_set Option.none) #[test] def test_term_is_dumb_true : Bool := term_is_dumb (Option.some "dumb") #[test] def test_term_is_dumb_false_for_other_term : Bool := Bool.not (term_is_dumb (Option.some "xterm-256color")) #[test] def test_term_is_dumb_false_when_unset : Bool := Bool.not (term_is_dumb Option.none)