harn-stdlib 0.8.23

Embedded Harn standard library source catalog
Documentation
// std/ansi - terminal styling helpers with deterministic no-color fallbacks.
let __ANSI_ESC = hex_decode("1b")

let __ANSI_BEL = hex_decode("07")

let __ANSI_RESET = __ANSI_ESC + "[0m"

type AnsiOptions = {stream?: string, mode?: string, enabled?: bool}

type AnsiStyle = {
  fg?: string,
  color?: string,
  bg?: string,
  background?: string,
  bold?: bool,
  dim?: bool,
  italic?: bool,
  underline?: bool,
  inverse?: bool,
  strikethrough?: bool,
}

fn __ansi_options(options) {
  if type_of(options) == "dict" {
    return options ?? {}
  }
  if type_of(options) == "string" {
    return {mode: options}
  }
  return {}
}

fn __ansi_color_index(name) {
  let n = lowercase(replace(name ?? "", "-", "_"))
  if n == "black" {
    return 0
  }
  if n == "red" {
    return 1
  }
  if n == "green" {
    return 2
  }
  if n == "yellow" {
    return 3
  }
  if n == "blue" {
    return 4
  }
  if n == "magenta" || n == "purple" {
    return 5
  }
  if n == "cyan" {
    return 6
  }
  if n == "white" {
    return 7
  }
  if n == "bright_black" || n == "gray" || n == "grey" {
    return 8
  }
  if n == "bright_red" {
    return 9
  }
  if n == "bright_green" {
    return 10
  }
  if n == "bright_yellow" {
    return 11
  }
  if n == "bright_blue" {
    return 12
  }
  if n == "bright_magenta" || n == "bright_purple" {
    return 13
  }
  if n == "bright_cyan" {
    return 14
  }
  if n == "bright_white" {
    return 15
  }
  return nil
}

fn __ansi_color_code(name, background) {
  let index = __ansi_color_index(name)
  if index == nil {
    return nil
  }
  if index >= 8 {
    let bright_base = if background {
      100
    } else {
      90
    }
    return to_string(bright_base + index - 8)
  }
  let base = if background {
    40
  } else {
    30
  }
  return to_string(base + index)
}

fn __ansi_codes(style) {
  let s = style ?? {}
  var codes = []
  if s?.bold ?? false {
    codes = codes.push("1")
  }
  if s?.dim ?? false {
    codes = codes.push("2")
  }
  if s?.italic ?? false {
    codes = codes.push("3")
  }
  if s?.underline ?? false {
    codes = codes.push("4")
  }
  if s?.inverse ?? false {
    codes = codes.push("7")
  }
  if s?.strikethrough ?? false {
    codes = codes.push("9")
  }
  let fg = s?.fg ?? s?.color
  let fg_code = __ansi_color_code(fg, false)
  if fg_code != nil {
    codes = codes.push(fg_code)
  }
  let bg = s?.bg ?? s?.background
  let bg_code = __ansi_color_code(bg, true)
  if bg_code != nil {
    codes = codes.push(bg_code)
  }
  return codes
}

/** Return whether ANSI output should be emitted for the requested stream. */
pub fn ansi_enabled(options: AnsiOptions = {}) -> bool {
  let opts = __ansi_options(options)
  if opts?.enabled != nil {
    return opts.enabled ? true : false
  }
  let mode = lowercase(opts?.mode ?? "auto")
  if mode == "always" {
    return true
  }
  if mode == "never" {
    return false
  }
  return __ansi_enabled(opts?.stream ?? "stdout")
}

/** Build an ANSI Select Graphic Rendition escape sequence such as `ansi_escape("1;31")`. */
pub fn ansi_escape(code: string) -> string {
  return __ANSI_ESC + "[" + code + "m"
}

/** Return the terminal reset escape sequence. */
pub fn ansi_reset() -> string {
  return __ANSI_RESET
}

/** Remove ANSI CSI/OSC escape sequences from text. */
pub fn ansi_strip(text: string) -> string {
  let value = text ?? ""
  let csi = __ANSI_ESC + "\\[[0-9;?]*[ -/]*[@-~]"
  let without_csi = regex_replace_all(csi, "", value)
  let osc = __ANSI_ESC + "\\][^" + __ANSI_BEL + "]*(" + __ANSI_BEL + "|" + __ANSI_ESC + "\\\\)"
  return regex_replace_all(osc, "", without_csi)
}

/** Return the visible character count after stripping ANSI escapes. */
pub fn ansi_visible_len(text: string) -> int {
  return len(ansi_strip(text ?? ""))
}

/** Apply a style dict to text when ANSI is enabled. */
pub fn ansi_style(text: string, style: AnsiStyle = {}, options: AnsiOptions = {}) -> string {
  let value = text ?? ""
  if !ansi_enabled(options) {
    return value
  }
  let codes = __ansi_codes(style)
  if len(codes) == 0 {
    return value
  }
  return ansi_escape(join(codes, ";")) + value + __ANSI_RESET
}

/** Apply a foreground color. */
pub fn ansi_color(text: string, name: string, options: AnsiOptions = {}) -> string {
  return ansi_style(text, {fg: name}, options)
}

/** Apply a background color. */
pub fn ansi_bg(text: string, name: string, options: AnsiOptions = {}) -> string {
  return ansi_style(text, {bg: name}, options)
}

/** Apply bold styling. */
pub fn ansi_bold(text: string, options: AnsiOptions = {}) -> string {
  return ansi_style(text, {bold: true}, options)
}

/** Apply dim styling. */
pub fn ansi_dim(text: string, options: AnsiOptions = {}) -> string {
  return ansi_style(text, {dim: true}, options)
}

/** Apply underline styling. */
pub fn ansi_underline(text: string, options: AnsiOptions = {}) -> string {
  return ansi_style(text, {underline: true}, options)
}

/** Standard success styling. */
pub fn ansi_success(text: string, options: AnsiOptions = {}) -> string {
  return ansi_style(text, {fg: "green", bold: true}, options)
}

/** Standard warning styling. */
pub fn ansi_warn(text: string, options: AnsiOptions = {}) -> string {
  return ansi_style(text, {fg: "yellow", bold: true}, options)
}

/** Standard error styling. */
pub fn ansi_error(text: string, options: AnsiOptions = {}) -> string {
  return ansi_style(text, {fg: "red", bold: true}, options)
}

/** Standard informational styling. */
pub fn ansi_info(text: string, options: AnsiOptions = {}) -> string {
  return ansi_style(text, {fg: "cyan"}, options)
}

/** Low-emphasis styling for secondary text. */
pub fn ansi_muted(text: string, options: AnsiOptions = {}) -> string {
  return ansi_style(text, {dim: true}, options)
}

/** Render an OSC-8 terminal hyperlink when ANSI is enabled; otherwise return the label. */
pub fn ansi_link(label: string, url: string, options: AnsiOptions = {}) -> string {
  let text = label ?? ""
  if !ansi_enabled(options) || trim(url ?? "") == "" {
    return text
  }
  return __ANSI_ESC + "]8;;" + url + __ANSI_BEL + text + __ANSI_ESC + "]8;;" + __ANSI_BEL
}

/** Truncate visible text after stripping ANSI escapes. */
pub fn ansi_truncate(text: string, max_chars: int, marker: string = "...") -> string {
  let plain = ansi_strip(text ?? "")
  let limit = max_chars ?? 0
  if limit <= 0 || len(plain) <= limit {
    return plain
  }
  let suffix = marker ?? "..."
  if limit <= len(suffix) {
    return substring(suffix, 0, limit)
  }
  return substring(plain, 0, limit - len(suffix)) + suffix
}