Fix terminal.nim to be GCSAFE. (#8296)

* Also fix deprecated callsite().
This commit is contained in:
Eugene Kabanov 2018-07-12 19:06:15 +03:00 • committed by Andreas Rumpf
commit 3fec2ba5e5

View file

@ -19,22 +19,30 @@
import macros import macros
import strformat import strformat
from strutils import toLowerAscii from strutils import toLowerAscii
import colors import colors, tables
const when defined(windows):
hasThreadSupport = compileOption("threads") import winlean
when not hasThreadSupport: type
import tables PTerminal = ref object
var
colorsFGCache = initTable[Color, string]()
colorsBGCache = initTable[Color, string]()
styleCache = initTable[int, string]()
var
trueColorIsSupported: bool trueColorIsSupported: bool
trueColorIsEnabled: bool trueColorIsEnabled: bool
fgSetColor: bool fgSetColor: bool
when defined(windows):
hStdout: Handle
hStderr: Handle
oldStdoutAttr: int16
oldStderrAttr: int16
var gTerm {.threadvar.}: PTerminal
proc newTerminal(): PTerminal
proc getTerminal(): PTerminal {.inline.} =
if isNil(gTerm):
gTerm = newTerminal()
result = gTerm
const const
fgPrefix = "\x1b[38;2;" fgPrefix = "\x1b[38;2;"
@ -156,23 +164,6 @@ when defined(windows):
proc setConsoleMode(hConsoleHandle: Handle, dwMode: DWORD): WINBOOL{. proc setConsoleMode(hConsoleHandle: Handle, dwMode: DWORD): WINBOOL{.
stdcall, dynlib: "kernel32", importc: "SetConsoleMode".} stdcall, dynlib: "kernel32", importc: "SetConsoleMode".}
var
hStdout: Handle # = createFile("CONOUT$", GENERIC_WRITE, 0, nil,
# OPEN_ALWAYS, 0, 0)
hStderr: Handle
block:
var hStdoutTemp = getStdHandle(STD_OUTPUT_HANDLE)
if duplicateHandle(getCurrentProcess(), hStdoutTemp, getCurrentProcess(),
addr(hStdout), 0, 1, DUPLICATE_SAME_ACCESS) == 0:
when defined(consoleapp):
raiseOSError(osLastError())
var hStderrTemp = getStdHandle(STD_ERROR_HANDLE)
if duplicateHandle(getCurrentProcess(), hStderrTemp, getCurrentProcess(),
addr(hStderr), 0, 1, DUPLICATE_SAME_ACCESS) == 0:
when defined(consoleapp):
raiseOSError(osLastError())
proc getCursorPos(h: Handle): tuple [x,y: int] = proc getCursorPos(h: Handle): tuple [x,y: int] =
var c: CONSOLESCREENBUFFERINFO var c: CONSOLESCREENBUFFERINFO
if getConsoleScreenBufferInfo(h, addr(c)) == 0: if getConsoleScreenBufferInfo(h, addr(c)) == 0:
@ -193,12 +184,23 @@ when defined(windows):
return c.wAttributes return c.wAttributes
return 0x70'i16 # ERROR: return white background, black text return 0x70'i16 # ERROR: return white background, black text
var proc initTerminal(term: PTerminal) =
oldStdoutAttr = getAttributes(hStdout) var hStdoutTemp = getStdHandle(STD_OUTPUT_HANDLE)
oldStderrAttr = getAttributes(hStderr) if duplicateHandle(getCurrentProcess(), hStdoutTemp, getCurrentProcess(),
addr(term.hStdout), 0, 1, DUPLICATE_SAME_ACCESS) == 0:
when defined(consoleapp):
raiseOSError(osLastError())
var hStderrTemp = getStdHandle(STD_ERROR_HANDLE)
if duplicateHandle(getCurrentProcess(), hStderrTemp, getCurrentProcess(),
addr(term.hStderr), 0, 1, DUPLICATE_SAME_ACCESS) == 0:
when defined(consoleapp):
raiseOSError(osLastError())
term.oldStdoutAttr = getAttributes(term.hStdout)
term.oldStderrAttr = getAttributes(term.hStderr)
template conHandle(f: File): Handle = template conHandle(f: File): Handle =
if f == stderr: hStderr else: hStdout let term = getTerminal()
if f == stderr: term.hStderr else: term.hStdout
else: else:
import termios, posix, os, parseutils import termios, posix, os, parseutils
@ -459,10 +461,11 @@ proc eraseScreen*(f: File) =
proc resetAttributes*(f: File) = proc resetAttributes*(f: File) =
## Resets all attributes. ## Resets all attributes.
when defined(windows): when defined(windows):
let term = getTerminal()
if f == stderr: if f == stderr:
discard setConsoleTextAttribute(hStderr, oldStderrAttr) discard setConsoleTextAttribute(term.hStderr, term.oldStderrAttr)
else: else:
discard setConsoleTextAttribute(hStdout, oldStdoutAttr) discard setConsoleTextAttribute(term.hStdout, term.oldStdoutAttr)
else: else:
f.write(ansiResetCode) f.write(ansiResetCode)
@ -487,14 +490,7 @@ when not defined(windows):
gBG {.threadvar.}: int gBG {.threadvar.}: int
proc ansiStyleCode*(style: int): string = proc ansiStyleCode*(style: int): string =
when hasThreadSupport:
result = fmt"{stylePrefix}{style}m" result = fmt"{stylePrefix}{style}m"
else:
if styleCache.hasKey(style):
result = styleCache[style]
else:
result = fmt"{stylePrefix}{style}m"
styleCache[style] = result
template ansiStyleCode*(style: Style): string = template ansiStyleCode*(style: Style): string =
ansiStyleCode(style.int) ansiStyleCode(style.int)
@ -521,10 +517,11 @@ proc setStyle*(f: File, style: set[Style]) =
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.
when defined(windows): when defined(windows):
var old = getAttributes(hStdout) let term = getTerminal()
var old = getAttributes(term.hStdout)
stdout.setStyle(style) stdout.setStyle(style)
stdout.write(txt) stdout.write(txt)
discard setConsoleTextAttribute(hStdout, old) discard setConsoleTextAttribute(term.hStdout, old)
else: else:
stdout.setStyle(style) stdout.setStyle(style)
stdout.write(txt) stdout.write(txt)
@ -633,32 +630,16 @@ template ansiForegroundColorCode*(fg: static[ForegroundColor],
ansiStyleCode(fg.int + bright.int * 60) ansiStyleCode(fg.int + bright.int * 60)
proc ansiForegroundColorCode*(color: Color): string = proc ansiForegroundColorCode*(color: Color): string =
when hasThreadSupport:
let rgb = extractRGB(color) let rgb = extractRGB(color)
result = fmt"{fgPrefix}{rgb.r};{rgb.g};{rgb.b}m" 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
template ansiForegroundColorCode*(color: static[Color]): string = template ansiForegroundColorCode*(color: static[Color]): string =
const rgb = extractRGB(color) const rgb = extractRGB(color)
(static(fmt"{fgPrefix}{rgb.r};{rgb.g};{rgb.b}m")) (static(fmt"{fgPrefix}{rgb.r};{rgb.g};{rgb.b}m"))
proc ansiBackgroundColorCode*(color: Color): string = proc ansiBackgroundColorCode*(color: Color): string =
when hasThreadSupport:
let rgb = extractRGB(color) let rgb = extractRGB(color)
result = fmt"{bgPrefix}{rgb.r};{rgb.g};{rgb.b}m" 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
template ansiBackgroundColorCode*(color: static[Color]): string = template ansiBackgroundColorCode*(color: static[Color]): string =
const rgb = extractRGB(color) const rgb = extractRGB(color)
@ -666,16 +647,17 @@ template ansiBackgroundColorCode*(color: static[Color]): string =
proc setForegroundColor*(f: File, color: Color) = proc setForegroundColor*(f: File, color: Color) =
## Sets the terminal's foreground true color. ## Sets the terminal's foreground true color.
if trueColorIsEnabled: if getTerminal().trueColorIsEnabled:
f.write(ansiForegroundColorCode(color)) f.write(ansiForegroundColorCode(color))
proc setBackgroundColor*(f: File, color: Color) = proc setBackgroundColor*(f: File, color: Color) =
## Sets the terminal's background true color. ## Sets the terminal's background true color.
if trueColorIsEnabled: if getTerminal().trueColorIsEnabled:
f.write(ansiBackgroundColorCode(color)) f.write(ansiBackgroundColorCode(color))
proc setTrueColor(f: File, color: Color) = proc setTrueColor(f: File, color: Color) =
if fgSetColor: let term = getTerminal()
if term.fgSetColor:
setForegroundColor(f, color) setForegroundColor(f, color)
else: else:
setBackgroundColor(f, color) setBackgroundColor(f, color)
@ -727,11 +709,10 @@ macro styledWrite*(f: File, m: varargs[typed]): untyped =
## stdout.styledWrite(fgRed, "red text ") ## stdout.styledWrite(fgRed, "red text ")
## stdout.styledWrite(fgGreen, "green text") ## stdout.styledWrite(fgGreen, "green text")
## ##
let m = callsite()
var reset = false var reset = false
result = newNimNode(nnkStmtList) result = newNimNode(nnkStmtList)
for i in countup(2, m.len - 1): for i in countup(0, m.len - 1):
let item = m[i] let item = m[i]
case item.kind case item.kind
of nnkStrLit..nnkTripleStrLit: of nnkStrLit..nnkTripleStrLit:
@ -872,54 +853,61 @@ proc resetAttributes*() {.noconv.} =
proc isTrueColorSupported*(): bool = proc isTrueColorSupported*(): bool =
## Returns true if a terminal supports true color. ## Returns true if a terminal supports true color.
return trueColorIsSupported return getTerminal().trueColorIsSupported
when defined(windows): when defined(windows):
import os import os
proc enableTrueColors*() = proc enableTrueColors*() =
## Enable true color. ## Enable true color.
var term = getTerminal()
when defined(windows): when defined(windows):
var var
ver: OSVERSIONINFO ver: OSVERSIONINFO
ver.dwOSVersionInfoSize = sizeof(ver).DWORD ver.dwOSVersionInfoSize = sizeof(ver).DWORD
let res = getVersionExW(addr ver) let res = getVersionExW(addr ver)
if res == 0: if res == 0:
trueColorIsSupported = false term.trueColorIsSupported = false
else: else:
trueColorIsSupported = ver.dwMajorVersion > 10 or term.trueColorIsSupported = ver.dwMajorVersion > 10 or
(ver.dwMajorVersion == 10 and (ver.dwMinorVersion > 0 or (ver.dwMajorVersion == 10 and (ver.dwMinorVersion > 0 or
(ver.dwMinorVersion == 0 and ver.dwBuildNumber >= 10586))) (ver.dwMinorVersion == 0 and ver.dwBuildNumber >= 10586)))
if not trueColorIsSupported: if not term.trueColorIsSupported:
trueColorIsSupported = getEnv("ANSICON_DEF").len > 0 term.trueColorIsSupported = getEnv("ANSICON_DEF").len > 0
if trueColorIsSupported: if term.trueColorIsSupported:
if getEnv("ANSICON_DEF").len == 0: if getEnv("ANSICON_DEF").len == 0:
var mode: DWORD = 0 var mode: DWORD = 0
if getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), addr(mode)) != 0: if getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), addr(mode)) != 0:
mode = mode or ENABLE_VIRTUAL_TERMINAL_PROCESSING mode = mode or ENABLE_VIRTUAL_TERMINAL_PROCESSING
if setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), mode) != 0: if setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), mode) != 0:
trueColorIsEnabled = true term.trueColorIsEnabled = true
else: else:
trueColorIsEnabled = false term.trueColorIsEnabled = false
else: else:
trueColorIsEnabled = true term.trueColorIsEnabled = true
else: else:
trueColorIsSupported = string(getEnv("COLORTERM")).toLowerAscii() in ["truecolor", "24bit"] term.trueColorIsSupported = string(getEnv("COLORTERM")).toLowerAscii() in ["truecolor", "24bit"]
trueColorIsEnabled = trueColorIsSupported term.trueColorIsEnabled = term.trueColorIsSupported
proc disableTrueColors*() = proc disableTrueColors*() =
## Disable true color. ## Disable true color.
var term = getTerminal()
when defined(windows): when defined(windows):
if trueColorIsSupported: if term.trueColorIsSupported:
if getEnv("ANSICON_DEF").len == 0: if getEnv("ANSICON_DEF").len == 0:
var mode: DWORD = 0 var mode: DWORD = 0
if getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), addr(mode)) != 0: if getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), addr(mode)) != 0:
mode = mode and not ENABLE_VIRTUAL_TERMINAL_PROCESSING mode = mode and not ENABLE_VIRTUAL_TERMINAL_PROCESSING
discard setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), mode) discard setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), mode)
trueColorIsEnabled = false term.trueColorIsEnabled = false
else: else:
trueColorIsEnabled = false term.trueColorIsEnabled = false
proc newTerminal(): PTerminal =
new result
when defined(windows):
initTerminal(result)
when not defined(testing) and isMainModule: when not defined(testing) and isMainModule:
assert ansiStyleCode(styleBright) == "\e[1m" assert ansiStyleCode(styleBright) == "\e[1m"