Skip to content

Commit 7ce3812

Browse files
data-manAraq
authored andcommitted
Support truecolor for the terminal stdlib module (#6936)
1 parent 1b3f640 commit 7ce3812

File tree

2 files changed

+202
-14
lines changed

2 files changed

+202
-14
lines changed

changelog.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,31 @@ let
190190

191191
- Added support for casting between integers of same bitsize in VM (compile time and nimscript).
192192
This allow to among other things to reinterpret signed integers as unsigned.
193+
193194
- Pragmas now support call syntax, for example: ``{.exportc"myname".}`` and ``{.exportc("myname").}``
194195
- Custom pragmas are now supported using pragma ``pragma``, please see language manual for details
195196

197+
- Added True Color support for some terminals
198+
Example:
199+
```nim
200+
import colors, terminal
201+
202+
const Nim = "Efficient and expressive programming."
203+
204+
var
205+
fg = colYellow
206+
bg = colBlue
207+
int = 1.0
208+
209+
enableTrueColors()
210+
211+
for i in 1..15:
212+
styledEcho bgColor, bg, fgColor, fg, Nim, resetStyle
213+
int -= 0.01
214+
fg = intensity(fg, int)
215+
216+
setForegroundColor colRed
217+
setBackgroundColor colGreen
218+
styledEcho "Red on Green.", resetStyle
219+
```
220+

lib/pure/terminal.nim

Lines changed: 177 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,38 @@
1717
## ``showCursor`` before quitting.
1818

1919
import macros
20+
import strformat
21+
from strutils import toLowerAscii
22+
import colors
23+
24+
const
25+
hasThreadSupport = compileOption("threads")
26+
27+
when not hasThreadSupport:
28+
import tables
29+
var
30+
colorsFGCache: Table[Color, string]
31+
colorsBGCache: Table[Color, string]
32+
when not defined(windows):
33+
var
34+
styleCache: Table[int, string]
35+
36+
var
37+
trueColorIsSupported {.threadvar.}: bool
38+
trueColorIsEnabled {.threadvar.}: bool
39+
fgSetColor {.threadvar.}: bool
40+
41+
when defined(windows):
42+
var
43+
terminalIsNonStandard {.threadvar.}: bool
44+
45+
const
46+
fgPrefix = "\x1b[38;2;"
47+
bgPrefix = "\x1b[48;2;"
48+
49+
when not defined(windows):
50+
const
51+
stylePrefix = "\e["
2052

2153
when defined(windows):
2254
import winlean, os
@@ -34,6 +66,8 @@ when defined(windows):
3466
FOREGROUND_RGB = FOREGROUND_RED or FOREGROUND_GREEN or FOREGROUND_BLUE
3567
BACKGROUND_RGB = BACKGROUND_RED or BACKGROUND_GREEN or BACKGROUND_BLUE
3668

69+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
70+
3771
type
3872
SHORT = int16
3973
COORD = object
@@ -124,6 +158,12 @@ when defined(windows):
124158
wAttributes: int16): WINBOOL{.
125159
stdcall, dynlib: "kernel32", importc: "SetConsoleTextAttribute".}
126160

161+
proc getConsoleMode(hConsoleHandle: Handle, dwMode: ptr DWORD): WINBOOL{.
162+
stdcall, dynlib: "kernel32", importc: "GetConsoleMode".}
163+
164+
proc setConsoleMode(hConsoleHandle: Handle, dwMode: DWORD): WINBOOL{.
165+
stdcall, dynlib: "kernel32", importc: "SetConsoleMode".}
166+
127167
var
128168
hStdout: Handle # = createFile("CONOUT$", GENERIC_WRITE, 0, nil,
129169
# OPEN_ALWAYS, 0, 0)
@@ -274,7 +314,7 @@ proc setCursorPos*(f: File, x, y: int) =
274314
let h = conHandle(f)
275315
setCursorPos(h, x, y)
276316
else:
277-
f.write("\e[" & $y & ';' & $x & 'f')
317+
f.write(fmt"{stylePrefix}{y};{x}f")
278318

279319
proc setCursorXPos*(f: File, x: int) =
280320
## Sets the terminal's cursor to the x position.
@@ -289,7 +329,7 @@ proc setCursorXPos*(f: File, x: int) =
289329
if setConsoleCursorPosition(h, origin) == 0:
290330
raiseOSError(osLastError())
291331
else:
292-
f.write("\e[" & $x & 'G')
332+
f.write(fmt"{stylePrefix}{x}G")
293333

294334
when defined(windows):
295335
proc setCursorYPos*(f: File, y: int) =
@@ -326,7 +366,7 @@ proc cursorDown*(f: File, count=1) =
326366
inc(p.y, count)
327367
setCursorPos(h, p.x, p.y)
328368
else:
329-
f.write("\e[" & $count & 'B')
369+
f.write(fmt"{stylePrefix}{count}B")
330370

331371
proc cursorForward*(f: File, count=1) =
332372
## Moves the cursor forward by `count` columns.
@@ -336,7 +376,7 @@ proc cursorForward*(f: File, count=1) =
336376
inc(p.x, count)
337377
setCursorPos(h, p.x, p.y)
338378
else:
339-
f.write("\e[" & $count & 'C')
379+
f.write("{stylePrefix}{count}C")
340380

341381
proc cursorBackward*(f: File, count=1) =
342382
## Moves the cursor backward by `count` columns.
@@ -346,7 +386,7 @@ proc cursorBackward*(f: File, count=1) =
346386
dec(p.x, count)
347387
setCursorPos(h, p.x, p.y)
348388
else:
349-
f.write("\e[" & $count & 'D')
389+
f.write("{stylePrefix}{count}D")
350390

351391
when true:
352392
discard
@@ -448,9 +488,18 @@ type
448488

449489
when not defined(windows):
450490
var
451-
# XXX: These better be thread-local
452-
gFG = 0
453-
gBG = 0
491+
gFG {.threadvar.}: int
492+
gBG {.threadvar.}: int
493+
494+
proc getStyleStr(style: int): string =
495+
when hasThreadSupport:
496+
result = fmt"{stylePrefix}{style}m"
497+
else:
498+
if styleCache.hasKey(style):
499+
result = styleCache[style]
500+
else:
501+
result = fmt"{stylePrefix}{style}m"
502+
styleCache[style] = result
454503

455504
proc setStyle*(f: File, style: set[Style]) =
456505
## Sets the terminal style.
@@ -465,7 +514,7 @@ proc setStyle*(f: File, style: set[Style]) =
465514
discard setConsoleTextAttribute(h, old or a)
466515
else:
467516
for s in items(style):
468-
f.write("\e[" & $ord(s) & 'm')
517+
f.write(getStyleStr(ord(s)))
469518

470519
proc writeStyled*(txt: string, style: set[Style] = {styleBright}) =
471520
## Writes the text `txt` in a given `style` to stdout.
@@ -479,9 +528,9 @@ proc writeStyled*(txt: string, style: set[Style] = {styleBright}) =
479528
stdout.write(txt)
480529
stdout.resetAttributes()
481530
if gFG != 0:
482-
stdout.write("\e[" & $ord(gFG) & 'm')
531+
stdout.write(getStyleStr(gFG))
483532
if gBG != 0:
484-
stdout.write("\e[" & $ord(gBG) & 'm')
533+
stdout.write(getStyleStr(gBG))
485534

486535
type
487536
ForegroundColor* = enum ## terminal's foreground colors
@@ -527,7 +576,7 @@ proc setForegroundColor*(f: File, fg: ForegroundColor, bright=false) =
527576
else:
528577
gFG = ord(fg)
529578
if bright: inc(gFG, 60)
530-
f.write("\e[" & $gFG & 'm')
579+
f.write(getStyleStr(gFG))
531580

532581
proc setBackgroundColor*(f: File, bg: BackgroundColor, bright=false) =
533582
## Sets the terminal's background color.
@@ -549,7 +598,48 @@ proc setBackgroundColor*(f: File, bg: BackgroundColor, bright=false) =
549598
else:
550599
gBG = ord(bg)
551600
if bright: inc(gBG, 60)
552-
f.write("\e[" & $gBG & 'm')
601+
f.write(getStyleStr(gBG))
602+
603+
604+
proc getFGColorStr(color: Color): string =
605+
when hasThreadSupport:
606+
let rgb = extractRGB(color)
607+
result = fmt"{fgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
608+
else:
609+
if colorsFGCache.hasKey(color):
610+
result = colorsFGCache[color]
611+
else:
612+
let rgb = extractRGB(color)
613+
result = fmt"{fgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
614+
colorsFGCache[color] = result
615+
616+
proc getBGColorStr(color: Color): string =
617+
when hasThreadSupport:
618+
let rgb = extractRGB(color)
619+
result = fmt"{bgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
620+
else:
621+
if colorsBGCache.hasKey(color):
622+
result = colorsBGCache[color]
623+
else:
624+
let rgb = extractRGB(color)
625+
result = fmt"{bgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
626+
colorsFGCache[color] = result
627+
628+
proc setForegroundColor*(f: File, color: Color) =
629+
## Sets the terminal's foreground true color.
630+
if trueColorIsEnabled:
631+
f.write(getFGColorStr(color))
632+
633+
proc setBackgroundColor*(f: File, color: Color) =
634+
## Sets the terminal's background true color.
635+
if trueColorIsEnabled:
636+
f.write(getBGColorStr(color))
637+
638+
proc setTrueColor(f: File, color: Color) =
639+
if fgSetColor:
640+
setForegroundColor(f, color)
641+
else:
642+
setBackgroundColor(f, color)
553643

554644
proc isatty*(f: File): bool =
555645
## Returns true if `f` is associated with a terminal device.
@@ -564,7 +654,9 @@ proc isatty*(f: File): bool =
564654

565655
type
566656
TerminalCmd* = enum ## commands that can be expressed as arguments
567-
resetStyle ## reset attributes
657+
resetStyle, ## reset attributes
658+
fgColor, ## set foreground's true color
659+
bgColor ## set background's true color
568660

569661
template styledEchoProcessArg(f: File, s: string) = write f, s
570662
template styledEchoProcessArg(f: File, style: Style) = setStyle(f, {style})
@@ -573,9 +665,15 @@ template styledEchoProcessArg(f: File, color: ForegroundColor) =
573665
setForegroundColor f, color
574666
template styledEchoProcessArg(f: File, color: BackgroundColor) =
575667
setBackgroundColor f, color
668+
template styledEchoProcessArg(f: File, color: Color) =
669+
setTrueColor f, color
576670
template styledEchoProcessArg(f: File, cmd: TerminalCmd) =
577671
when cmd == resetStyle:
578672
resetAttributes(f)
673+
when cmd == fgColor:
674+
fgSetColor = true
675+
when cmd == bgColor:
676+
fgSetColor = false
579677

580678
macro styledWriteLine*(f: File, m: varargs[typed]): untyped =
581679
## Similar to ``writeLine``, but treating terminal style arguments specially.
@@ -664,6 +762,10 @@ template setForegroundColor*(fg: ForegroundColor, bright=false) =
664762
setForegroundColor(stdout, fg, bright)
665763
template setBackgroundColor*(bg: BackgroundColor, bright=false) =
666764
setBackgroundColor(stdout, bg, bright)
765+
template setForegroundColor*(color: Color) =
766+
setForegroundColor(stdout, color)
767+
template setBackgroundColor*(color: Color) =
768+
setBackgroundColor(stdout, color)
667769
proc resetAttributes*() {.noconv.} =
668770
## Resets all attributes on stdout.
669771
## It is advisable to register this as a quit proc with
@@ -679,3 +781,64 @@ when not defined(testing) and isMainModule:
679781
stdout.setForeGroundColor(fgBlue)
680782
stdout.writeLine("ordinary text")
681783
stdout.resetAttributes()
784+
785+
proc isTrueColorSupported*(): bool =
786+
## Returns true if a terminal supports true color.
787+
return trueColorIsSupported
788+
789+
proc enableTrueColors*() =
790+
## Enable true color.
791+
when defined(windows):
792+
if trueColorIsSupported:
793+
if not terminalIsNonStandard:
794+
var mode: DWORD = 0
795+
if getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), addr(mode)) != 0:
796+
mode = mode or ENABLE_VIRTUAL_TERMINAL_PROCESSING
797+
if setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), mode) != 0:
798+
trueColorIsEnabled = true
799+
else:
800+
trueColorIsEnabled = false
801+
else:
802+
trueColorIsEnabled = true
803+
else:
804+
trueColorIsEnabled = true
805+
806+
proc disableTrueColors*() =
807+
## Disable true color.
808+
when defined(windows):
809+
if trueColorIsSupported:
810+
if not terminalIsNonStandard:
811+
var mode: DWORD = 0
812+
if getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), addr(mode)) != 0:
813+
mode = mode and not ENABLE_VIRTUAL_TERMINAL_PROCESSING
814+
discard setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), mode)
815+
trueColorIsEnabled = false
816+
else:
817+
trueColorIsEnabled = false
818+
819+
when defined(windows):
820+
import os
821+
var
822+
ver: OSVERSIONINFO
823+
ver.dwOSVersionInfoSize = sizeof(ver).DWORD
824+
let res = when useWinUnicode: getVersionExW(ver.addr) else: getVersionExA(ver.addr)
825+
if res == 0:
826+
trueColorIsSupported = false
827+
else:
828+
trueColorIsSupported = ver.dwMajorVersion > 10 or
829+
(ver.dwMajorVersion == 10 and (ver.dwMinorVersion > 0 or
830+
(ver.dwMinorVersion == 0 and ver.dwBuildNumber >= 10586)))
831+
if not trueColorIsSupported:
832+
trueColorIsSupported = (getEnv("ANSICON_DEF").len > 0)
833+
terminalIsNonStandard = trueColorIsSupported
834+
else:
835+
when compileOption("taintmode"):
836+
trueColorIsSupported = string(getEnv("COLORTERM")).toLowerAscii() in ["truecolor", "24bit"]
837+
when not compileOption("taintmode"):
838+
trueColorIsSupported = getEnv("COLORTERM").toLowerAscii() in ["truecolor", "24bit"]
839+
840+
when not hasThreadSupport:
841+
colorsFGCache = initTable[Color, string]()
842+
colorsBGCache = initTable[Color, string]()
843+
when not defined(windows):
844+
styleCache = initTable[int, string]()

0 commit comments

Comments
 (0)