Add truecolor and unicode support to terminal utilities.
This commit is contained in:
@@ -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 CSI = "\x1b["
|
||||||
const RESET_FORMATTING* = "\x1b[0m"
|
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])")
|
let FORMATTING_REGEX* = re("\x1b\\[([0-9;]*)([a-zA-Z])")
|
||||||
|
|
||||||
type
|
type
|
||||||
|
TrueColor* = tuple[r, g, b: int]
|
||||||
|
|
||||||
CursorType* = enum
|
CursorType* = enum
|
||||||
ctBlockBlink = 1, ctBlock, ctUnderlineBlink, ctUnderline, ctBarBlink, ctBar
|
ctBlockBlink = 1, ctBlock, ctUnderlineBlink, ctUnderline, ctBarBlink, ctBar
|
||||||
|
|
||||||
@@ -13,8 +21,15 @@ type
|
|||||||
emToEnd, emToStart, emAll
|
emToEnd, emToStart, emAll
|
||||||
|
|
||||||
TerminalColors* = enum
|
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 =
|
proc stripFormatting*(text: string): string =
|
||||||
text.replace(FORMATTING_REGEX, "")
|
text.replace(FORMATTING_REGEX, "")
|
||||||
@@ -22,47 +37,92 @@ proc stripFormatting*(text: string): string =
|
|||||||
proc stripAnsi*(text: string): string = stripFormatting(text)
|
proc stripAnsi*(text: string): string = stripFormatting(text)
|
||||||
|
|
||||||
func ansiAwareSubstring*(s: string, start, length: int): string =
|
func ansiAwareSubstring*(s: string, start, length: int): string =
|
||||||
result = ""
|
var outputRunes = newSeq[Rune]()
|
||||||
var curAnsiEscCode = ""
|
var curAnsiSequences = newSeq[Rune]()
|
||||||
|
var runes = s.toRunes
|
||||||
|
var visibleRunesSeen = 0
|
||||||
|
var visibleOutputLen = 0
|
||||||
var i = 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
|
if runes.len > i + runeLen(CSI) and
|
||||||
# We need to notice ANSI escape codes...
|
$runes[i..<i + len(CSI)] == CSI:
|
||||||
s[i..<i + len(CSI)] == CSI:
|
# Recognize ANSI escape codes...
|
||||||
var j = i + len(CSI)
|
|
||||||
while j < s.len and s[j] notin ANSI_ESCAPE_CODE_ENDINGS: j += 1
|
var j = i + runeLen(CSI)
|
||||||
|
while j < runes.len and runes[j] notin ANSI_ESCAPE_CODE_ENDINGS: j += 1
|
||||||
|
|
||||||
# and remember it if we're before the start of the substring
|
# and remember it if we're before the start of the substring
|
||||||
if i < start: curAnsiEscCode = s[i..j]
|
if visibleRunesSeen < start: curAnsiSequences.add(runes[i..j])
|
||||||
|
|
||||||
# or add it without increasing our substring length
|
# or add it to the output without increasing our substring length
|
||||||
else: result.add(s[i..j])
|
else: outputRunes.add(runes[i..j])
|
||||||
|
|
||||||
# either way we want to pick up after it
|
# either way we want to pick up after it
|
||||||
i = j
|
i = j
|
||||||
|
|
||||||
else:
|
else:
|
||||||
result.add(s[i])
|
if visibleRunesSeen >= start:
|
||||||
visibleLen += 1
|
outputRunes.add(runes[i])
|
||||||
|
inc visibleOutputLen
|
||||||
|
inc visibleRunesSeen
|
||||||
|
|
||||||
i += 1
|
inc i
|
||||||
|
|
||||||
result = curAnsiEscCode & result
|
result = $(curAnsiSequences & outputRunes)
|
||||||
|
|
||||||
func color*(text: string, fg = cWhite, bg = cBlack): string =
|
|
||||||
return CSI & $int(fg) & ";" & $(int(bg) + 40) & "m" & text & RESET_FORMATTING
|
|
||||||
|
|
||||||
|
|
||||||
func color*(text: string, fg: TerminalColors): string =
|
func ansiAwareSubstring*[T, U](s: string, r: HSlice[T,U]): string =
|
||||||
return CSI & $int(fg) & "m" & text & RESET_FORMATTING
|
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 =
|
func ansiEscSeq*(style: set[TerminalStyle]): string =
|
||||||
return CSI & $(int(bg) + 40) & "m" & text & RESET_FORMATTING
|
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 invert*(text: string): string = return CSI & "7m" & text & RESET_FORMATTING
|
||||||
func striketrough*(text: string): string = return CSI & "9m" & text & RESET_FORMATTING
|
func striketrough*(text: string): string = return CSI & "9m" & text & RESET_FORMATTING
|
||||||
|
|||||||
Reference in New Issue
Block a user