Support truecolor for the terminal stdlib module (#6936)
This commit is contained in:
parent
1b3f640188
commit
7ce38122e8
2 changed files with 202 additions and 14 deletions
25
changelog.md
25
changelog.md
|
|
@ -190,6 +190,31 @@ let
|
||||||
|
|
||||||
- Added support for casting between integers of same bitsize in VM (compile time and nimscript).
|
- Added support for casting between integers of same bitsize in VM (compile time and nimscript).
|
||||||
This allow to among other things to reinterpret signed integers as unsigned.
|
This allow to among other things to reinterpret signed integers as unsigned.
|
||||||
|
|
||||||
- Pragmas now support call syntax, for example: ``{.exportc"myname".}`` and ``{.exportc("myname").}``
|
- Pragmas now support call syntax, for example: ``{.exportc"myname".}`` and ``{.exportc("myname").}``
|
||||||
- Custom pragmas are now supported using pragma ``pragma``, please see language manual for details
|
- Custom pragmas are now supported using pragma ``pragma``, please see language manual for details
|
||||||
|
|
||||||
|
- Added True Color support for some terminals
|
||||||
|
Example:
|
||||||
|
```nim
|
||||||
|
import colors, terminal
|
||||||
|
|
||||||
|
const Nim = "Efficient and expressive programming."
|
||||||
|
|
||||||
|
var
|
||||||
|
fg = colYellow
|
||||||
|
bg = colBlue
|
||||||
|
int = 1.0
|
||||||
|
|
||||||
|
enableTrueColors()
|
||||||
|
|
||||||
|
for i in 1..15:
|
||||||
|
styledEcho bgColor, bg, fgColor, fg, Nim, resetStyle
|
||||||
|
int -= 0.01
|
||||||
|
fg = intensity(fg, int)
|
||||||
|
|
||||||
|
setForegroundColor colRed
|
||||||
|
setBackgroundColor colGreen
|
||||||
|
styledEcho "Red on Green.", resetStyle
|
||||||
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,38 @@
|
||||||
## ``showCursor`` before quitting.
|
## ``showCursor`` before quitting.
|
||||||
|
|
||||||
import macros
|
import macros
|
||||||
|
import strformat
|
||||||
|
from strutils import toLowerAscii
|
||||||
|
import colors
|
||||||
|
|
||||||
|
const
|
||||||
|
hasThreadSupport = compileOption("threads")
|
||||||
|
|
||||||
|
when not hasThreadSupport:
|
||||||
|
import tables
|
||||||
|
var
|
||||||
|
colorsFGCache: Table[Color, string]
|
||||||
|
colorsBGCache: Table[Color, string]
|
||||||
|
when not defined(windows):
|
||||||
|
var
|
||||||
|
styleCache: Table[int, string]
|
||||||
|
|
||||||
|
var
|
||||||
|
trueColorIsSupported {.threadvar.}: bool
|
||||||
|
trueColorIsEnabled {.threadvar.}: bool
|
||||||
|
fgSetColor {.threadvar.}: bool
|
||||||
|
|
||||||
|
when defined(windows):
|
||||||
|
var
|
||||||
|
terminalIsNonStandard {.threadvar.}: bool
|
||||||
|
|
||||||
|
const
|
||||||
|
fgPrefix = "\x1b[38;2;"
|
||||||
|
bgPrefix = "\x1b[48;2;"
|
||||||
|
|
||||||
|
when not defined(windows):
|
||||||
|
const
|
||||||
|
stylePrefix = "\e["
|
||||||
|
|
||||||
when defined(windows):
|
when defined(windows):
|
||||||
import winlean, os
|
import winlean, os
|
||||||
|
|
@ -34,6 +66,8 @@ when defined(windows):
|
||||||
FOREGROUND_RGB = FOREGROUND_RED or FOREGROUND_GREEN or FOREGROUND_BLUE
|
FOREGROUND_RGB = FOREGROUND_RED or FOREGROUND_GREEN or FOREGROUND_BLUE
|
||||||
BACKGROUND_RGB = BACKGROUND_RED or BACKGROUND_GREEN or BACKGROUND_BLUE
|
BACKGROUND_RGB = BACKGROUND_RED or BACKGROUND_GREEN or BACKGROUND_BLUE
|
||||||
|
|
||||||
|
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
||||||
|
|
||||||
type
|
type
|
||||||
SHORT = int16
|
SHORT = int16
|
||||||
COORD = object
|
COORD = object
|
||||||
|
|
@ -124,6 +158,12 @@ when defined(windows):
|
||||||
wAttributes: int16): WINBOOL{.
|
wAttributes: int16): WINBOOL{.
|
||||||
stdcall, dynlib: "kernel32", importc: "SetConsoleTextAttribute".}
|
stdcall, dynlib: "kernel32", importc: "SetConsoleTextAttribute".}
|
||||||
|
|
||||||
|
proc getConsoleMode(hConsoleHandle: Handle, dwMode: ptr DWORD): WINBOOL{.
|
||||||
|
stdcall, dynlib: "kernel32", importc: "GetConsoleMode".}
|
||||||
|
|
||||||
|
proc setConsoleMode(hConsoleHandle: Handle, dwMode: DWORD): WINBOOL{.
|
||||||
|
stdcall, dynlib: "kernel32", importc: "SetConsoleMode".}
|
||||||
|
|
||||||
var
|
var
|
||||||
hStdout: Handle # = createFile("CONOUT$", GENERIC_WRITE, 0, nil,
|
hStdout: Handle # = createFile("CONOUT$", GENERIC_WRITE, 0, nil,
|
||||||
# OPEN_ALWAYS, 0, 0)
|
# OPEN_ALWAYS, 0, 0)
|
||||||
|
|
@ -274,7 +314,7 @@ proc setCursorPos*(f: File, x, y: int) =
|
||||||
let h = conHandle(f)
|
let h = conHandle(f)
|
||||||
setCursorPos(h, x, y)
|
setCursorPos(h, x, y)
|
||||||
else:
|
else:
|
||||||
f.write("\e[" & $y & ';' & $x & 'f')
|
f.write(fmt"{stylePrefix}{y};{x}f")
|
||||||
|
|
||||||
proc setCursorXPos*(f: File, x: int) =
|
proc setCursorXPos*(f: File, x: int) =
|
||||||
## Sets the terminal's cursor to the x position.
|
## Sets the terminal's cursor to the x position.
|
||||||
|
|
@ -289,7 +329,7 @@ proc setCursorXPos*(f: File, x: int) =
|
||||||
if setConsoleCursorPosition(h, origin) == 0:
|
if setConsoleCursorPosition(h, origin) == 0:
|
||||||
raiseOSError(osLastError())
|
raiseOSError(osLastError())
|
||||||
else:
|
else:
|
||||||
f.write("\e[" & $x & 'G')
|
f.write(fmt"{stylePrefix}{x}G")
|
||||||
|
|
||||||
when defined(windows):
|
when defined(windows):
|
||||||
proc setCursorYPos*(f: File, y: int) =
|
proc setCursorYPos*(f: File, y: int) =
|
||||||
|
|
@ -326,7 +366,7 @@ proc cursorDown*(f: File, count=1) =
|
||||||
inc(p.y, count)
|
inc(p.y, count)
|
||||||
setCursorPos(h, p.x, p.y)
|
setCursorPos(h, p.x, p.y)
|
||||||
else:
|
else:
|
||||||
f.write("\e[" & $count & 'B')
|
f.write(fmt"{stylePrefix}{count}B")
|
||||||
|
|
||||||
proc cursorForward*(f: File, count=1) =
|
proc cursorForward*(f: File, count=1) =
|
||||||
## Moves the cursor forward by `count` columns.
|
## Moves the cursor forward by `count` columns.
|
||||||
|
|
@ -336,7 +376,7 @@ proc cursorForward*(f: File, count=1) =
|
||||||
inc(p.x, count)
|
inc(p.x, count)
|
||||||
setCursorPos(h, p.x, p.y)
|
setCursorPos(h, p.x, p.y)
|
||||||
else:
|
else:
|
||||||
f.write("\e[" & $count & 'C')
|
f.write("{stylePrefix}{count}C")
|
||||||
|
|
||||||
proc cursorBackward*(f: File, count=1) =
|
proc cursorBackward*(f: File, count=1) =
|
||||||
## Moves the cursor backward by `count` columns.
|
## Moves the cursor backward by `count` columns.
|
||||||
|
|
@ -346,7 +386,7 @@ proc cursorBackward*(f: File, count=1) =
|
||||||
dec(p.x, count)
|
dec(p.x, count)
|
||||||
setCursorPos(h, p.x, p.y)
|
setCursorPos(h, p.x, p.y)
|
||||||
else:
|
else:
|
||||||
f.write("\e[" & $count & 'D')
|
f.write("{stylePrefix}{count}D")
|
||||||
|
|
||||||
when true:
|
when true:
|
||||||
discard
|
discard
|
||||||
|
|
@ -448,9 +488,18 @@ type
|
||||||
|
|
||||||
when not defined(windows):
|
when not defined(windows):
|
||||||
var
|
var
|
||||||
# XXX: These better be thread-local
|
gFG {.threadvar.}: int
|
||||||
gFG = 0
|
gBG {.threadvar.}: int
|
||||||
gBG = 0
|
|
||||||
|
proc getStyleStr(style: int): string =
|
||||||
|
when hasThreadSupport:
|
||||||
|
result = fmt"{stylePrefix}{style}m"
|
||||||
|
else:
|
||||||
|
if styleCache.hasKey(style):
|
||||||
|
result = styleCache[style]
|
||||||
|
else:
|
||||||
|
result = fmt"{stylePrefix}{style}m"
|
||||||
|
styleCache[style] = result
|
||||||
|
|
||||||
proc setStyle*(f: File, style: set[Style]) =
|
proc setStyle*(f: File, style: set[Style]) =
|
||||||
## Sets the terminal style.
|
## Sets the terminal style.
|
||||||
|
|
@ -465,7 +514,7 @@ proc setStyle*(f: File, style: set[Style]) =
|
||||||
discard setConsoleTextAttribute(h, old or a)
|
discard setConsoleTextAttribute(h, old or a)
|
||||||
else:
|
else:
|
||||||
for s in items(style):
|
for s in items(style):
|
||||||
f.write("\e[" & $ord(s) & 'm')
|
f.write(getStyleStr(ord(s)))
|
||||||
|
|
||||||
proc writeStyled*(txt: string, style: set[Style] = {styleBright}) =
|
proc writeStyled*(txt: string, style: set[Style] = {styleBright}) =
|
||||||
## Writes the text `txt` in a given `style` to stdout.
|
## Writes the text `txt` in a given `style` to stdout.
|
||||||
|
|
@ -479,9 +528,9 @@ proc writeStyled*(txt: string, style: set[Style] = {styleBright}) =
|
||||||
stdout.write(txt)
|
stdout.write(txt)
|
||||||
stdout.resetAttributes()
|
stdout.resetAttributes()
|
||||||
if gFG != 0:
|
if gFG != 0:
|
||||||
stdout.write("\e[" & $ord(gFG) & 'm')
|
stdout.write(getStyleStr(gFG))
|
||||||
if gBG != 0:
|
if gBG != 0:
|
||||||
stdout.write("\e[" & $ord(gBG) & 'm')
|
stdout.write(getStyleStr(gBG))
|
||||||
|
|
||||||
type
|
type
|
||||||
ForegroundColor* = enum ## terminal's foreground colors
|
ForegroundColor* = enum ## terminal's foreground colors
|
||||||
|
|
@ -527,7 +576,7 @@ proc setForegroundColor*(f: File, fg: ForegroundColor, bright=false) =
|
||||||
else:
|
else:
|
||||||
gFG = ord(fg)
|
gFG = ord(fg)
|
||||||
if bright: inc(gFG, 60)
|
if bright: inc(gFG, 60)
|
||||||
f.write("\e[" & $gFG & 'm')
|
f.write(getStyleStr(gFG))
|
||||||
|
|
||||||
proc setBackgroundColor*(f: File, bg: BackgroundColor, bright=false) =
|
proc setBackgroundColor*(f: File, bg: BackgroundColor, bright=false) =
|
||||||
## Sets the terminal's background color.
|
## Sets the terminal's background color.
|
||||||
|
|
@ -549,7 +598,48 @@ proc setBackgroundColor*(f: File, bg: BackgroundColor, bright=false) =
|
||||||
else:
|
else:
|
||||||
gBG = ord(bg)
|
gBG = ord(bg)
|
||||||
if bright: inc(gBG, 60)
|
if bright: inc(gBG, 60)
|
||||||
f.write("\e[" & $gBG & 'm')
|
f.write(getStyleStr(gBG))
|
||||||
|
|
||||||
|
|
||||||
|
proc getFGColorStr(color: Color): string =
|
||||||
|
when hasThreadSupport:
|
||||||
|
let rgb = extractRGB(color)
|
||||||
|
result = fmt"{fgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
|
||||||
|
else:
|
||||||
|
if colorsFGCache.hasKey(color):
|
||||||
|
result = colorsFGCache[color]
|
||||||
|
else:
|
||||||
|
let rgb = extractRGB(color)
|
||||||
|
result = fmt"{fgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
|
||||||
|
colorsFGCache[color] = result
|
||||||
|
|
||||||
|
proc getBGColorStr(color: Color): string =
|
||||||
|
when hasThreadSupport:
|
||||||
|
let rgb = extractRGB(color)
|
||||||
|
result = fmt"{bgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
|
||||||
|
else:
|
||||||
|
if colorsBGCache.hasKey(color):
|
||||||
|
result = colorsBGCache[color]
|
||||||
|
else:
|
||||||
|
let rgb = extractRGB(color)
|
||||||
|
result = fmt"{bgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
|
||||||
|
colorsFGCache[color] = result
|
||||||
|
|
||||||
|
proc setForegroundColor*(f: File, color: Color) =
|
||||||
|
## Sets the terminal's foreground true color.
|
||||||
|
if trueColorIsEnabled:
|
||||||
|
f.write(getFGColorStr(color))
|
||||||
|
|
||||||
|
proc setBackgroundColor*(f: File, color: Color) =
|
||||||
|
## Sets the terminal's background true color.
|
||||||
|
if trueColorIsEnabled:
|
||||||
|
f.write(getBGColorStr(color))
|
||||||
|
|
||||||
|
proc setTrueColor(f: File, color: Color) =
|
||||||
|
if fgSetColor:
|
||||||
|
setForegroundColor(f, color)
|
||||||
|
else:
|
||||||
|
setBackgroundColor(f, color)
|
||||||
|
|
||||||
proc isatty*(f: File): bool =
|
proc isatty*(f: File): bool =
|
||||||
## Returns true if `f` is associated with a terminal device.
|
## Returns true if `f` is associated with a terminal device.
|
||||||
|
|
@ -564,7 +654,9 @@ proc isatty*(f: File): bool =
|
||||||
|
|
||||||
type
|
type
|
||||||
TerminalCmd* = enum ## commands that can be expressed as arguments
|
TerminalCmd* = enum ## commands that can be expressed as arguments
|
||||||
resetStyle ## reset attributes
|
resetStyle, ## reset attributes
|
||||||
|
fgColor, ## set foreground's true color
|
||||||
|
bgColor ## set background's true color
|
||||||
|
|
||||||
template styledEchoProcessArg(f: File, s: string) = write f, s
|
template styledEchoProcessArg(f: File, s: string) = write f, s
|
||||||
template styledEchoProcessArg(f: File, style: Style) = setStyle(f, {style})
|
template styledEchoProcessArg(f: File, style: Style) = setStyle(f, {style})
|
||||||
|
|
@ -573,9 +665,15 @@ template styledEchoProcessArg(f: File, color: ForegroundColor) =
|
||||||
setForegroundColor f, color
|
setForegroundColor f, color
|
||||||
template styledEchoProcessArg(f: File, color: BackgroundColor) =
|
template styledEchoProcessArg(f: File, color: BackgroundColor) =
|
||||||
setBackgroundColor f, color
|
setBackgroundColor f, color
|
||||||
|
template styledEchoProcessArg(f: File, color: Color) =
|
||||||
|
setTrueColor f, color
|
||||||
template styledEchoProcessArg(f: File, cmd: TerminalCmd) =
|
template styledEchoProcessArg(f: File, cmd: TerminalCmd) =
|
||||||
when cmd == resetStyle:
|
when cmd == resetStyle:
|
||||||
resetAttributes(f)
|
resetAttributes(f)
|
||||||
|
when cmd == fgColor:
|
||||||
|
fgSetColor = true
|
||||||
|
when cmd == bgColor:
|
||||||
|
fgSetColor = false
|
||||||
|
|
||||||
macro styledWriteLine*(f: File, m: varargs[typed]): untyped =
|
macro styledWriteLine*(f: File, m: varargs[typed]): untyped =
|
||||||
## Similar to ``writeLine``, but treating terminal style arguments specially.
|
## Similar to ``writeLine``, but treating terminal style arguments specially.
|
||||||
|
|
@ -664,6 +762,10 @@ template setForegroundColor*(fg: ForegroundColor, bright=false) =
|
||||||
setForegroundColor(stdout, fg, bright)
|
setForegroundColor(stdout, fg, bright)
|
||||||
template setBackgroundColor*(bg: BackgroundColor, bright=false) =
|
template setBackgroundColor*(bg: BackgroundColor, bright=false) =
|
||||||
setBackgroundColor(stdout, bg, bright)
|
setBackgroundColor(stdout, bg, bright)
|
||||||
|
template setForegroundColor*(color: Color) =
|
||||||
|
setForegroundColor(stdout, color)
|
||||||
|
template setBackgroundColor*(color: Color) =
|
||||||
|
setBackgroundColor(stdout, color)
|
||||||
proc resetAttributes*() {.noconv.} =
|
proc resetAttributes*() {.noconv.} =
|
||||||
## Resets all attributes on stdout.
|
## Resets all attributes on stdout.
|
||||||
## It is advisable to register this as a quit proc with
|
## It is advisable to register this as a quit proc with
|
||||||
|
|
@ -679,3 +781,64 @@ when not defined(testing) and isMainModule:
|
||||||
stdout.setForeGroundColor(fgBlue)
|
stdout.setForeGroundColor(fgBlue)
|
||||||
stdout.writeLine("ordinary text")
|
stdout.writeLine("ordinary text")
|
||||||
stdout.resetAttributes()
|
stdout.resetAttributes()
|
||||||
|
|
||||||
|
proc isTrueColorSupported*(): bool =
|
||||||
|
## Returns true if a terminal supports true color.
|
||||||
|
return trueColorIsSupported
|
||||||
|
|
||||||
|
proc enableTrueColors*() =
|
||||||
|
## Enable true color.
|
||||||
|
when defined(windows):
|
||||||
|
if trueColorIsSupported:
|
||||||
|
if not terminalIsNonStandard:
|
||||||
|
var mode: DWORD = 0
|
||||||
|
if getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), addr(mode)) != 0:
|
||||||
|
mode = mode or ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||||
|
if setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), mode) != 0:
|
||||||
|
trueColorIsEnabled = true
|
||||||
|
else:
|
||||||
|
trueColorIsEnabled = false
|
||||||
|
else:
|
||||||
|
trueColorIsEnabled = true
|
||||||
|
else:
|
||||||
|
trueColorIsEnabled = true
|
||||||
|
|
||||||
|
proc disableTrueColors*() =
|
||||||
|
## Disable true color.
|
||||||
|
when defined(windows):
|
||||||
|
if trueColorIsSupported:
|
||||||
|
if not terminalIsNonStandard:
|
||||||
|
var mode: DWORD = 0
|
||||||
|
if getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), addr(mode)) != 0:
|
||||||
|
mode = mode and not ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||||
|
discard setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), mode)
|
||||||
|
trueColorIsEnabled = false
|
||||||
|
else:
|
||||||
|
trueColorIsEnabled = false
|
||||||
|
|
||||||
|
when defined(windows):
|
||||||
|
import os
|
||||||
|
var
|
||||||
|
ver: OSVERSIONINFO
|
||||||
|
ver.dwOSVersionInfoSize = sizeof(ver).DWORD
|
||||||
|
let res = when useWinUnicode: getVersionExW(ver.addr) else: getVersionExA(ver.addr)
|
||||||
|
if res == 0:
|
||||||
|
trueColorIsSupported = false
|
||||||
|
else:
|
||||||
|
trueColorIsSupported = ver.dwMajorVersion > 10 or
|
||||||
|
(ver.dwMajorVersion == 10 and (ver.dwMinorVersion > 0 or
|
||||||
|
(ver.dwMinorVersion == 0 and ver.dwBuildNumber >= 10586)))
|
||||||
|
if not trueColorIsSupported:
|
||||||
|
trueColorIsSupported = (getEnv("ANSICON_DEF").len > 0)
|
||||||
|
terminalIsNonStandard = trueColorIsSupported
|
||||||
|
else:
|
||||||
|
when compileOption("taintmode"):
|
||||||
|
trueColorIsSupported = string(getEnv("COLORTERM")).toLowerAscii() in ["truecolor", "24bit"]
|
||||||
|
when not compileOption("taintmode"):
|
||||||
|
trueColorIsSupported = getEnv("COLORTERM").toLowerAscii() in ["truecolor", "24bit"]
|
||||||
|
|
||||||
|
when not hasThreadSupport:
|
||||||
|
colorsFGCache = initTable[Color, string]()
|
||||||
|
colorsBGCache = initTable[Color, string]()
|
||||||
|
when not defined(windows):
|
||||||
|
styleCache = initTable[int, string]()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue