diff --git a/cliutils/ansiterm.nim b/cliutils/ansiterm.nim index 3e49df7..fe5d3d8 100644 --- a/cliutils/ansiterm.nim +++ b/cliutils/ansiterm.nim @@ -1,11 +1,19 @@ -import std/[nre, sequtils] +import std/[nre, sequtils, strutils, unicode] + +# See +# - https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b +# - https://en.wikipedia.org/wiki/ANSI_escape_code const CSI = "\x1b[" const RESET_FORMATTING* = "\x1b[0m" -const ANSI_ESCAPE_CODE_ENDINGS*: seq[char] = toSeq('A'..'Z') & toSeq('a'..'z') +const ANSI_ESCAPE_CODE_ENDINGS*: seq[Rune] = + toRunes(toSeq('A'..'Z') & toSeq('a'..'z')) + let FORMATTING_REGEX* = re("\x1b\\[([0-9;]*)([a-zA-Z])") type + TrueColor* = tuple[r, g, b: int] + CursorType* = enum ctBlockBlink = 1, ctBlock, ctUnderlineBlink, ctUnderline, ctBarBlink, ctBar @@ -13,8 +21,15 @@ type emToEnd, emToStart, emAll TerminalColors* = enum - cBlack, cRed, cGreen, cYellow, cBlue, cMagenta, cCyan, cWhite + cBlack = 0, cRed, cGreen, cYellow, cBlue, cMagenta, cCyan, cWhite, + cDefault = 9 + cBrightBlack = 60, cBrightRed, cBrightGreen, cBrightYellow, cBrightBlue, + cBrightMagenta, cBrightCyan, cBrightWhite + + TerminalStyle* = enum + tsBold = 1, tsDim, tsItalic, tsUnderline, tsBlink, tsInverse, tsHidden, + tsStrikethrough proc stripFormatting*(text: string): string = text.replace(FORMATTING_REGEX, "") @@ -22,47 +37,92 @@ proc stripFormatting*(text: string): string = proc stripAnsi*(text: string): string = stripFormatting(text) func ansiAwareSubstring*(s: string, start, length: int): string = - result = "" - var curAnsiEscCode = "" + var outputRunes = newSeq[Rune]() + var curAnsiSequences = newSeq[Rune]() + var runes = s.toRunes + var visibleRunesSeen = 0 + var visibleOutputLen = 0 var i = 0 - var visibleLen = 0 - while i < len(s) and visibleLen < length: + while i < runes.len and visibleOutputLen < length: - if len(s) > i + len(CSI) and - # We need to notice ANSI escape codes... - s[i.. i + runeLen(CSI) and + $runes[i..= start: + outputRunes.add(runes[i]) + inc visibleOutputLen + inc visibleRunesSeen - i += 1 + inc i - result = curAnsiEscCode & result - -func color*(text: string, fg = cWhite, bg = cBlack): string = - return CSI & $int(fg) & ";" & $(int(bg) + 40) & "m" & text & RESET_FORMATTING + result = $(curAnsiSequences & outputRunes) -func color*(text: string, fg: TerminalColors): string = - return CSI & $int(fg) & "m" & text & RESET_FORMATTING +func ansiAwareSubstring*[T, U](s: string, r: HSlice[T,U]): string = + let idxStart = + when r.a is BackwardsIndex: s.runeLen - int(r.a) + else: int(r.a) + + let idxEnd = + when r.b is BackwardsIndex: s.runeLen - int(r.b) + else: int(r.b) + + ansiAwareSubstring(s, idxStart, idxEnd - idxStart + 1) -func color*(text: string, bg: TerminalColors): string = - return CSI & $(int(bg) + 40) & "m" & text & RESET_FORMATTING +func ansiEscSeq*(style: set[TerminalStyle]): string = + CSI & toSeq(style).mapIt($int(it)).join(";") & "m" +func ansiEscSeq*(fg: int): string = CSI & "38;5;" & $fg & "m" +func ansiEscSeq*(bg: int): string = CSI & "48;5;" & $bg & "m" + + +func ansiEscSeq*(fg: TrueColor): string = + "$#38;2;$#;$#;$#m" % [CSI, $fg.r, $fg.g, $fg.b] + +func ansiEscSeq*(bg: TrueColor): string = + "$#48;2;$#;$#;$#m" % [CSI, $bg.r, $bg.g, $bg.b] + + +func ansiEscSeq*(fg: TerminalColors): string = CSI & $(int(fg) + 30) & "m" +func ansiEscSeq*(bg: TerminalColors): string = CSI & $(int(bg) + 40) & "m" + + +func ansiEscSeq*(fg: TerminalColors, bg: TerminalColors, style: set[TerminalStyle]): string = + result = CSI + if style != {}: result &= toSeq(style).mapIt($int(it)).join(";") & ";" + result &= $(int(fg) + 30) & ";" & $(int(bg) + 40) & "m" + + +func termFmt*(text: string, fg = cWhite, bg = cBlack, style: set[TerminalStyle] = {}): string = + return ansiEscSeq(fg, bg, style) & text & RESET_FORMATTING + + +func termFmt*[C: int | TrueColor]( + text: string, + fg, bg: C, + style: set[TerminalStyle] = {}): string = + return ansiEscSeq(style) & ansiEscSeq(fg = fg) & ansiEscSeq(bg = bg) & + text & RESET_FORMATTING + + +func color*(text: string, fg = cDefault, bg = cDefault): string = + return termFmt(text, fg, bg, {}) func invert*(text: string): string = return CSI & "7m" & text & RESET_FORMATTING func striketrough*(text: string): string = return CSI & "9m" & text & RESET_FORMATTING