1717# # ``showCursor`` before quitting.
1818
1919import 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
2153when 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 = 0x 0004
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
279319proc 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
294334when 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
331371proc 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
341381proc 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
351391when true :
352392 discard
@@ -448,9 +488,18 @@ type
448488
449489when 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
455504proc 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
470519proc 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
486535type
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
532581proc 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
554644proc 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
565655type
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
569661template styledEchoProcessArg (f: File , s: string ) = write f, s
570662template styledEchoProcessArg (f: File , style: Style ) = setStyle (f, {style})
@@ -573,9 +665,15 @@ template styledEchoProcessArg(f: File, color: ForegroundColor) =
573665 setForegroundColor f, color
574666template styledEchoProcessArg (f: File , color: BackgroundColor ) =
575667 setBackgroundColor f, color
668+ template styledEchoProcessArg (f: File , color: Color ) =
669+ setTrueColor f, color
576670template 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
580678macro 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)
665763template 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)
667769proc 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