/* * Copyright 2025 Magnus Madsen * * Use of this source code is governed by the Apache 2.0 license * that can be found in the LICENSE.md file. */pubmod RichString {use Sys.Env////// A rich string composed of multiple styled spans.///pubenumRichString {case RichString(Chain[RichString.Span]) }instanceAdd[RichString] {pubdefadd(x: RichString, y: RichString): RichString = RichString.combine(x, y) }instanceToString[RichString] {pubdeftoString(x: RichString): String = RichString.toString(x) }instanceSemiGroup[RichString] {pubdefcombine(x: RichString, y: RichString): RichString = RichString.combine(x, y) }instanceMonoid[RichString] {pubdefempty(): RichString = RichString.empty() }instanceFormattable[RichString] {pubdefformat(x: RichString): RichString = x }instanceEq[RichString] {pubdefeq(x: RichString, y: RichString): Bool = RichString.equals(x, y) }////// Represents a color with RGB components or the terminal's default color.////// For RGB, each component is a value from 0 to 255 representing red, green, and blue intensity./// Default represents no color override, using the terminal's current color settings.///pubenumColorwithEq {case Rgb(Int32, Int32, Int32) // Red, Green, Blue componentscase Default // Use terminal's default color }////// Represents text styling options.////// Defines the visual appearance of text spans beyond color.///pubenumStylewithEq, Order {case Boldcase Underline }////// A text span with content, foreground color, background color, and styles.////// A span represents a contiguous piece of text with uniform styling.///pubenumSpan {case Span({text = String, fgColor = Color, bgColor = Color, styles = Set[Style]}) }////// A record type representing a palette of colors.///typealiasColorPalette = { black = Color, gray = Color, red = Color, green = Color, yellow = Color, blue = Color, magenta = Color, cyan = Color, white = Color }////// Returns an empty `RichString`.///pubdefempty(): RichString = RichString(Chain.empty())////// Returns a `RichString` with the given text `x` using default colors and normal style.///pubdeftext(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =Formattable.format(x)////// Returns a `RichString` with the function `f` applied to every span.///pubdefmap(f: Span -> Span, rs: RichString): RichString = matchrs {case RichString(spans) => RichString(Chain.map(f, spans)) }////// Returns a `Bool` according to the equality of `rs1` and `rs2`.////// Checks whether text and styling is equivalent for each individual character.///pubdefequals(rs1: RichString, rs2: RichString): Bool =let (RichString(chain1), RichString(chain2)) = (rs1, rs2);letsplitSpans = (match Span.Span({text = t, fgColor = fg, bgColor = bg, styles}) -> {String.toList(t) |> List.toChain |> Chain.map(c -> (Span.Span({text = Char.toString(c), fgColor = fg, bgColor = bg, styles = styles}))) } );letl1 = Chain.flatMap(splitSpans, chain1);letl2 = Chain.flatMap(splitSpans, chain2);if (Chain.length(l1) !=Chain.length(l2)) {false } else {Chain.zip(l1, l2) |> Chain.forAll((match (e1, e2) -> spanEquals(e1, e2))) }defspanEquals(s1: Span, s2: Span): Bool =let Span.Span({text = t1, fgColor = fg1, bgColor = bg1, styles = style1}) = s1;let Span.Span({text = t2, fgColor = fg2, bgColor = bg2, styles = style2}) = s2;t1==t2andfg1==fg2andbg1==bg2andstyle1==style2////// Returns a `RichString` from given text `x` with all spans updated to use the given foreground color `c`.///pubdefwithColor(c: Color, x: a): RichString \ Formattable.Aef[a] withFormattable[a] =letrs = Formattable.format(x);map(match Span.Span(r) -> Span.Span({text = r#text, fgColor = c, bgColor = r#bgColor, styles = r#styles}),rs )////// Returns a `RichString` from given text `x` with all spans updated to use the given background color `c`.///pubdefwithBgColor(c: Color, x: a): RichString \ Formattable.Aef[a] withFormattable[a] =letrs = Formattable.format(x);map(match Span.Span(r) -> Span.Span({text = r#text, fgColor = r#fgColor, bgColor = c, styles = r#styles}),rs )////// Returns a `RichString` from given text `x` with the given `style` in addition to any other styles it already has.///pubdefwithStyle(style: Style, x: a): RichString \ Formattable.Aef[a] withFormattable[a] =letrs = Formattable.format(x);map(match Span.Span(r) -> Span.Span({text = r#text, fgColor = r#fgColor, bgColor = r#bgColor, styles = Set.insert(style, r#styles)}),rs )////// Returns a `RichString` containing the given span `s`.///pubdeffromSpan(s: Span): RichString = RichString(Chain.singleton(s))////// Returns a `RichString` from the given string `s` using default colors and no styles.///pubdeffromString(s: String): RichString = RichString(Chain.singleton(Span.Span({text = s, fgColor = Color.Default, bgColor = Color.Default, styles = Set.empty()})))////// Returns a `RichString` with the given text `x` in black color.///pubdefblack(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withColor(colors()#fg#black, x)////// Returns a `RichString` with the given text `x` in red color.///pubdefred(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withColor(colors()#fg#red, x)////// Returns a `RichString` with the given text `x` in gray color.///pubdefgray(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withColor(colors()#fg#gray, x)////// Returns a `RichString` with the given text `x` in green color.///pubdefgreen(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withColor(colors()#fg#green, x)////// Returns a `RichString` with the given text `x` in yellow color.///pubdefyellow(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withColor(colors()#fg#yellow, x)////// Returns a `RichString` with the given text `x` in blue color.///pubdefblue(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withColor(colors()#fg#blue, x)////// Returns a `RichString` with the given text `x` in magenta color.///pubdefmagenta(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withColor(colors()#fg#magenta, x)////// Returns a `RichString` with the given text `x` in cyan color.///pubdefcyan(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withColor(colors()#fg#cyan, x)////// Returns a `RichString` with the given text `x` in white color.///pubdefwhite(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withColor(colors()#fg#white, x)////// Returns a `RichString` with the given text `x` on black background.///pubdefbgBlack(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withBgColor(colors()#bg#black, x)////// Returns a `RichString` with the given text `x` on red background.///pubdefbgRed(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withBgColor(colors()#bg#red, x)////// Returns a `RichString` with the given text `x` on green background.///pubdefbgGreen(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withBgColor(colors()#bg#green, x)////// Returns a `RichString` with the given text `x` on yellow background.///pubdefbgYellow(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withBgColor(colors()#bg#yellow, x)////// Returns a `RichString` with the given text `x` on blue background.///pubdefbgBlue(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withBgColor(colors()#bg#blue, x)////// Returns a `RichString` with the given text `x` on magenta background.///pubdefbgMagenta(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withBgColor(colors()#bg#magenta, x)////// Returns a `RichString` with the given text `x` on cyan background.///pubdefbgCyan(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withBgColor(colors()#bg#cyan, x)////// Returns a `RichString` with the given text `x` on white background.///pubdefbgWhite(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withBgColor(colors()#bg#white, x)////// Returns a `RichString` with the given text `x` in bold style.///pubdefbold(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withStyle(Style.Bold, x)////// Returns a `RichString` with the given text `x` in underline style.///pubdefunderline(x: a): RichString \ Formattable.Aef[a] withFormattable[a] =withStyle(Style.Underline, x)////// Returns the concatenation of the elements in `xs` formatted with RichString `sep` inserted between each.////// Uses the Formattable trait to format each element.///pubdefjoin(sep: RichString, xs: t[a]): RichString \ (Formattable.Aef[a] + Foldable.Aef[t]) withFormattable[a], Foldable[t] =joinWith(Formattable.format, sep, xs)////// Returns the concatenation of elements in `xs` according to `f` with RichString `sep` inserted between each.///pubdefjoinWith(f: a -> RichString \ ef, sep: RichString, xs: t[a]): RichString \ (ef + Foldable.Aef[t]) withFoldable[t] =letl = Foldable.toList(xs);matchl {case Nil => empty()casex :: Nil => f(x)casex :: rest =>List.foldLeft((acc, elem) -> acc+sep+f(elem), f(x), rest) }////// Returns the concatenation of `rs1` and `rs2`.////// The spans from `rs1` appear first, followed by the spans from `rs2`.///pubdefcombine(rs1: RichString, rs2: RichString): RichString = match (rs1, rs2) {case (RichString(spans1), RichString(spans2)) => RichString(Chain.append(spans1, spans2)) }////// Returns a string representation of the given `rs`.////// If the terminal supports ANSI colors, returns the string with ANSI escape codes./// Otherwise, returns the plain text representation without styling.///pubdeftoString(rs: RichString): String =if (isColorTerm()) toAnsiString(rs) elsetoPlainString(rs)////// Returns the plain text representation of the `RichString` `rs`.////// All styling and color information is discarded.///pubdeftoPlainString(rs: RichString): String = matchrs {case RichString(spans) =>Chain.joinWith(match Span.Span(r) -> r#text, "", spans) }////// Returns the string representation of the `RichString` `rs` with ANSI escape codes.////// The returned string includes ANSI color and style codes for terminal display.///deftoAnsiString(rs: RichString): String = matchrs {case RichString(spans) =>Chain.joinWith(match Span.Span(r) -> {// Collect all codes in a listletstyleCodes = r#styles |> Set.toList |> List.map(s -> matchs {case Style.Bold => "1"case Style.Underline => "4" } );letfgCodes = matchr#fgColor {case Color.Rgb(red, green, blue) => "38;2;${red};${green};${blue}" :: Nilcase Color.Default => Nil };letbgCodes = matchr#bgColor {case Color.Rgb(red, green, blue) => "48;2;${red};${green};${blue}" :: Nilcase Color.Default => Nil };letallCodes = styleCodes:::fgCodes:::bgCodes;if (List.isEmpty(allCodes))r#textelse"\u001B[${List.join(";", allCodes)}m${r#text}\u001B[0m" },"",spans ) }////// Returns a record containing all color definitions for foreground and background colors.////// The returned record has two fields:/// - `fg`: A `ColorPalette` record containing foreground colors/// - `bg`: A `ColorPalette` record containing background colors///defcolors(): {fg = ColorPalette, bg = ColorPalette} = { fg = { black = Color.Rgb(0, 0, 0), gray = Color.Rgb(128, 128, 128), red = Color.Rgb(220, 105, 105), green = Color.Rgb(13, 188, 121), yellow = Color.Rgb(229, 192, 123), blue = Color.Rgb(100, 130, 160), magenta = Color.Rgb(210, 125, 210), cyan = Color.Rgb(17, 168, 205), white = Color.Rgb(255, 255, 255) }, bg = { black = Color.Rgb(40, 40, 40), gray = Color.Rgb(128, 128, 128), red = Color.Rgb(205, 49, 49), green = Color.Rgb(13, 188, 121), yellow = Color.Rgb(229, 192, 123), blue = Color.Rgb(36, 114, 200), magenta = Color.Rgb(188, 63, 188), cyan = Color.Rgb(17, 168, 205), white = Color.Rgb(255, 255, 255) } }////// Returns `true` unless color output is explicitly disabled.////// Returns `false` when:/// - NO_COLOR environment variable is set (following the no-color.org standard)/// - TERM=dumb (indicates a terminal with no capabilities)///defisColorTerm(): Bool = unsafeIO {run {letnoColor = Env.getVar("NO_COLOR") |> Option.nonEmpty;letisDumbTerm = Env.getVar("TERM") |> Option.exists(t -> t=="dumb");not (noColororisDumbTerm) } withEnv.runWithIO }}