mirror of
https://github.com/nim-lang/Nim.git
synced 2025-12-29 17:34:43 +00:00
953 lines
32 KiB
Nim
953 lines
32 KiB
Nim
#
|
|
#
|
|
# Nim's Runtime Library
|
|
# (c) Copyright 2012 Andreas Rumpf
|
|
#
|
|
# See the file "copying.txt", included in this
|
|
# distribution, for details about the copyright.
|
|
#
|
|
|
|
## This module contains a few procedures to control the *terminal*
|
|
## (also called *console*). On UNIX, the implementation simply uses ANSI escape
|
|
## sequences and does not depend on any other module, on Windows it uses the
|
|
## Windows API.
|
|
## Changing the style is permanent even after program termination! Use the
|
|
## code `exitprocs.addExitProc(resetAttributes)` to restore the defaults.
|
|
## Similarly, if you hide the cursor, make sure to unhide it with
|
|
## `showCursor` before quitting.
|
|
|
|
import macros
|
|
import strformat
|
|
from strutils import toLowerAscii, `%`
|
|
import colors
|
|
|
|
when defined(windows):
|
|
import winlean
|
|
|
|
type
|
|
PTerminal = ref object
|
|
trueColorIsSupported: bool
|
|
trueColorIsEnabled: bool
|
|
fgSetColor: bool
|
|
when defined(windows):
|
|
hStdout: Handle
|
|
hStderr: Handle
|
|
oldStdoutAttr: int16
|
|
oldStderrAttr: int16
|
|
|
|
var gTerm {.threadvar.}: owned(PTerminal)
|
|
|
|
when defined(windows) and defined(consoleapp):
|
|
proc newTerminal(): owned(PTerminal) {.gcsafe, raises: [OSError].}
|
|
else:
|
|
proc newTerminal(): owned(PTerminal) {.gcsafe, raises: [].}
|
|
|
|
proc getTerminal(): PTerminal {.inline.} =
|
|
if isNil(gTerm):
|
|
gTerm = newTerminal()
|
|
result = gTerm
|
|
|
|
const
|
|
fgPrefix = "\e[38;2;"
|
|
bgPrefix = "\e[48;2;"
|
|
ansiResetCode* = "\e[0m"
|
|
stylePrefix = "\e["
|
|
|
|
when defined(windows):
|
|
import winlean, os
|
|
|
|
const
|
|
DUPLICATE_SAME_ACCESS = 2
|
|
FOREGROUND_BLUE = 1
|
|
FOREGROUND_GREEN = 2
|
|
FOREGROUND_RED = 4
|
|
FOREGROUND_INTENSITY = 8
|
|
BACKGROUND_BLUE = 16
|
|
BACKGROUND_GREEN = 32
|
|
BACKGROUND_RED = 64
|
|
BACKGROUND_INTENSITY = 128
|
|
FOREGROUND_RGB = FOREGROUND_RED or FOREGROUND_GREEN or FOREGROUND_BLUE
|
|
BACKGROUND_RGB = BACKGROUND_RED or BACKGROUND_GREEN or BACKGROUND_BLUE
|
|
|
|
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
|
|
type
|
|
SHORT = int16
|
|
COORD = object
|
|
x: SHORT
|
|
y: SHORT
|
|
|
|
SMALL_RECT = object
|
|
left: SHORT
|
|
top: SHORT
|
|
right: SHORT
|
|
bottom: SHORT
|
|
|
|
CONSOLE_SCREEN_BUFFER_INFO = object
|
|
dwSize: COORD
|
|
dwCursorPosition: COORD
|
|
wAttributes: int16
|
|
srWindow: SMALL_RECT
|
|
dwMaximumWindowSize: COORD
|
|
|
|
CONSOLE_CURSOR_INFO = object
|
|
dwSize: DWORD
|
|
bVisible: WINBOOL
|
|
|
|
proc duplicateHandle(hSourceProcessHandle: Handle, hSourceHandle: Handle,
|
|
hTargetProcessHandle: Handle, lpTargetHandle: ptr Handle,
|
|
dwDesiredAccess: DWORD, bInheritHandle: WINBOOL,
|
|
dwOptions: DWORD): WINBOOL{.stdcall, dynlib: "kernel32",
|
|
importc: "DuplicateHandle".}
|
|
proc getCurrentProcess(): Handle{.stdcall, dynlib: "kernel32",
|
|
importc: "GetCurrentProcess".}
|
|
proc getConsoleScreenBufferInfo(hConsoleOutput: Handle,
|
|
lpConsoleScreenBufferInfo: ptr CONSOLE_SCREEN_BUFFER_INFO): WINBOOL{.stdcall,
|
|
dynlib: "kernel32", importc: "GetConsoleScreenBufferInfo".}
|
|
|
|
proc getConsoleCursorInfo(hConsoleOutput: Handle,
|
|
lpConsoleCursorInfo: ptr CONSOLE_CURSOR_INFO): WINBOOL{.
|
|
stdcall, dynlib: "kernel32", importc: "GetConsoleCursorInfo".}
|
|
|
|
proc setConsoleCursorInfo(hConsoleOutput: Handle,
|
|
lpConsoleCursorInfo: ptr CONSOLE_CURSOR_INFO): WINBOOL{.
|
|
stdcall, dynlib: "kernel32", importc: "SetConsoleCursorInfo".}
|
|
|
|
proc terminalWidthIoctl*(handles: openArray[Handle]): int =
|
|
var csbi: CONSOLE_SCREEN_BUFFER_INFO
|
|
for h in handles:
|
|
if getConsoleScreenBufferInfo(h, addr csbi) != 0:
|
|
return int(csbi.srWindow.right - csbi.srWindow.left + 1)
|
|
return 0
|
|
|
|
proc terminalHeightIoctl*(handles: openArray[Handle]): int =
|
|
var csbi: CONSOLE_SCREEN_BUFFER_INFO
|
|
for h in handles:
|
|
if getConsoleScreenBufferInfo(h, addr csbi) != 0:
|
|
return int(csbi.srWindow.bottom - csbi.srWindow.top + 1)
|
|
return 0
|
|
|
|
proc terminalWidth*(): int =
|
|
var w: int = 0
|
|
w = terminalWidthIoctl([getStdHandle(STD_INPUT_HANDLE),
|
|
getStdHandle(STD_OUTPUT_HANDLE),
|
|
getStdHandle(STD_ERROR_HANDLE)])
|
|
if w > 0: return w
|
|
return 80
|
|
|
|
proc terminalHeight*(): int =
|
|
var h: int = 0
|
|
h = terminalHeightIoctl([getStdHandle(STD_INPUT_HANDLE),
|
|
getStdHandle(STD_OUTPUT_HANDLE),
|
|
getStdHandle(STD_ERROR_HANDLE)])
|
|
if h > 0: return h
|
|
return 0
|
|
|
|
proc setConsoleCursorPosition(hConsoleOutput: Handle,
|
|
dwCursorPosition: COORD): WINBOOL{.
|
|
stdcall, dynlib: "kernel32", importc: "SetConsoleCursorPosition".}
|
|
|
|
proc fillConsoleOutputCharacter(hConsoleOutput: Handle, cCharacter: char,
|
|
nLength: DWORD, dwWriteCoord: COORD,
|
|
lpNumberOfCharsWritten: ptr DWORD): WINBOOL{.
|
|
stdcall, dynlib: "kernel32", importc: "FillConsoleOutputCharacterA".}
|
|
|
|
proc fillConsoleOutputAttribute(hConsoleOutput: Handle, wAttribute: int16,
|
|
nLength: DWORD, dwWriteCoord: COORD,
|
|
lpNumberOfAttrsWritten: ptr DWORD): WINBOOL{.
|
|
stdcall, dynlib: "kernel32", importc: "FillConsoleOutputAttribute".}
|
|
|
|
proc setConsoleTextAttribute(hConsoleOutput: Handle,
|
|
wAttributes: int16): WINBOOL{.
|
|
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".}
|
|
|
|
proc getCursorPos(h: Handle): tuple [x, y: int] =
|
|
var c: CONSOLE_SCREEN_BUFFER_INFO
|
|
if getConsoleScreenBufferInfo(h, addr(c)) == 0:
|
|
raiseOSError(osLastError())
|
|
return (int(c.dwCursorPosition.x), int(c.dwCursorPosition.y))
|
|
|
|
proc setCursorPos(h: Handle, x, y: int) =
|
|
var c: COORD
|
|
c.x = int16(x)
|
|
c.y = int16(y)
|
|
if setConsoleCursorPosition(h, c) == 0:
|
|
raiseOSError(osLastError())
|
|
|
|
proc getAttributes(h: Handle): int16 =
|
|
var c: CONSOLE_SCREEN_BUFFER_INFO
|
|
# workaround Windows bugs: try several times
|
|
if getConsoleScreenBufferInfo(h, addr(c)) != 0:
|
|
return c.wAttributes
|
|
return 0x70'i16 # ERROR: return white background, black text
|
|
|
|
proc initTerminal(term: PTerminal) =
|
|
var hStdoutTemp = getStdHandle(STD_OUTPUT_HANDLE)
|
|
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 =
|
|
let term = getTerminal()
|
|
if f == stderr: term.hStderr else: term.hStdout
|
|
|
|
else:
|
|
import termios, posix, os, parseutils
|
|
|
|
proc setRaw(fd: FileHandle, time: cint = TCSAFLUSH) =
|
|
var mode: Termios
|
|
discard fd.tcGetAttr(addr mode)
|
|
mode.c_iflag = mode.c_iflag and not Cflag(BRKINT or ICRNL or INPCK or
|
|
ISTRIP or IXON)
|
|
mode.c_oflag = mode.c_oflag and not Cflag(OPOST)
|
|
mode.c_cflag = (mode.c_cflag and not Cflag(CSIZE or PARENB)) or CS8
|
|
mode.c_lflag = mode.c_lflag and not Cflag(ECHO or ICANON or IEXTEN or ISIG)
|
|
mode.c_cc[VMIN] = 1.cuchar
|
|
mode.c_cc[VTIME] = 0.cuchar
|
|
discard fd.tcSetAttr(time, addr mode)
|
|
|
|
proc terminalWidthIoctl*(fds: openArray[int]): int =
|
|
## Returns terminal width from first fd that supports the ioctl.
|
|
|
|
var win: IOctl_WinSize
|
|
for fd in fds:
|
|
if ioctl(cint(fd), TIOCGWINSZ, addr win) != -1:
|
|
return int(win.ws_col)
|
|
return 0
|
|
|
|
proc terminalHeightIoctl*(fds: openArray[int]): int =
|
|
## Returns terminal height from first fd that supports the ioctl.
|
|
|
|
var win: IOctl_WinSize
|
|
for fd in fds:
|
|
if ioctl(cint(fd), TIOCGWINSZ, addr win) != -1:
|
|
return int(win.ws_row)
|
|
return 0
|
|
|
|
var L_ctermid{.importc, header: "<stdio.h>".}: cint
|
|
|
|
proc terminalWidth*(): int =
|
|
## Returns some reasonable terminal width from either standard file
|
|
## descriptors, controlling terminal, environment variables or tradition.
|
|
|
|
var w = terminalWidthIoctl([0, 1, 2]) #Try standard file descriptors
|
|
if w > 0: return w
|
|
var cterm = newString(L_ctermid) #Try controlling tty
|
|
var fd = open(ctermid(cstring(cterm)), O_RDONLY)
|
|
if fd != -1:
|
|
w = terminalWidthIoctl([int(fd)])
|
|
discard close(fd)
|
|
if w > 0: return w
|
|
var s = getEnv("COLUMNS") #Try standard env var
|
|
if len(s) > 0 and parseInt(s, w) > 0 and w > 0:
|
|
return w
|
|
return 80 #Finally default to venerable value
|
|
|
|
proc terminalHeight*(): int =
|
|
## Returns some reasonable terminal height from either standard file
|
|
## descriptors, controlling terminal, environment variables or tradition.
|
|
## Zero is returned if the height could not be determined.
|
|
|
|
var h = terminalHeightIoctl([0, 1, 2]) # Try standard file descriptors
|
|
if h > 0: return h
|
|
var cterm = newString(L_ctermid) # Try controlling tty
|
|
var fd = open(ctermid(cstring(cterm)), O_RDONLY)
|
|
if fd != -1:
|
|
h = terminalHeightIoctl([int(fd)])
|
|
discard close(fd)
|
|
if h > 0: return h
|
|
var s = getEnv("LINES") # Try standard env var
|
|
if len(s) > 0 and parseInt(s, h) > 0 and h > 0:
|
|
return h
|
|
return 0 # Could not determine height
|
|
|
|
proc terminalSize*(): tuple[w, h: int] =
|
|
## Returns the terminal width and height as a tuple. Internally calls
|
|
## `terminalWidth` and `terminalHeight`, so the same assumptions apply.
|
|
result = (terminalWidth(), terminalHeight())
|
|
|
|
when defined(windows):
|
|
proc setCursorVisibility(f: File, visible: bool) =
|
|
var ccsi: CONSOLE_CURSOR_INFO
|
|
let h = conHandle(f)
|
|
if getConsoleCursorInfo(h, addr(ccsi)) == 0:
|
|
raiseOSError(osLastError())
|
|
ccsi.bVisible = if visible: 1 else: 0
|
|
if setConsoleCursorInfo(h, addr(ccsi)) == 0:
|
|
raiseOSError(osLastError())
|
|
|
|
proc hideCursor*(f: File) =
|
|
## Hides the cursor.
|
|
when defined(windows):
|
|
setCursorVisibility(f, false)
|
|
else:
|
|
f.write("\e[?25l")
|
|
|
|
proc showCursor*(f: File) =
|
|
## Shows the cursor.
|
|
when defined(windows):
|
|
setCursorVisibility(f, true)
|
|
else:
|
|
f.write("\e[?25h")
|
|
|
|
proc setCursorPos*(f: File, x, y: int) =
|
|
## Sets the terminal's cursor to the (x,y) position.
|
|
## (0,0) is the upper left of the screen.
|
|
when defined(windows):
|
|
let h = conHandle(f)
|
|
setCursorPos(h, x, y)
|
|
else:
|
|
f.write(fmt"{stylePrefix}{y+1};{x+1}f")
|
|
|
|
proc setCursorXPos*(f: File, x: int) =
|
|
## Sets the terminal's cursor to the x position.
|
|
## The y position is not changed.
|
|
when defined(windows):
|
|
let h = conHandle(f)
|
|
var scrbuf: CONSOLE_SCREEN_BUFFER_INFO
|
|
if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0:
|
|
raiseOSError(osLastError())
|
|
var origin = scrbuf.dwCursorPosition
|
|
origin.x = int16(x)
|
|
if setConsoleCursorPosition(h, origin) == 0:
|
|
raiseOSError(osLastError())
|
|
else:
|
|
f.write(fmt"{stylePrefix}{x+1}G")
|
|
|
|
when defined(windows):
|
|
proc setCursorYPos*(f: File, y: int) =
|
|
## Sets the terminal's cursor to the y position.
|
|
## The x position is not changed.
|
|
## .. warning:: This is not supported on UNIX!
|
|
when defined(windows):
|
|
let h = conHandle(f)
|
|
var scrbuf: CONSOLE_SCREEN_BUFFER_INFO
|
|
if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0:
|
|
raiseOSError(osLastError())
|
|
var origin = scrbuf.dwCursorPosition
|
|
origin.y = int16(y)
|
|
if setConsoleCursorPosition(h, origin) == 0:
|
|
raiseOSError(osLastError())
|
|
else:
|
|
discard
|
|
|
|
proc cursorUp*(f: File, count = 1) =
|
|
## Moves the cursor up by `count` rows.
|
|
when defined(windows):
|
|
let h = conHandle(f)
|
|
var p = getCursorPos(h)
|
|
dec(p.y, count)
|
|
setCursorPos(h, p.x, p.y)
|
|
else:
|
|
f.write("\e[" & $count & 'A')
|
|
|
|
proc cursorDown*(f: File, count = 1) =
|
|
## Moves the cursor down by `count` rows.
|
|
when defined(windows):
|
|
let h = conHandle(f)
|
|
var p = getCursorPos(h)
|
|
inc(p.y, count)
|
|
setCursorPos(h, p.x, p.y)
|
|
else:
|
|
f.write(fmt"{stylePrefix}{count}B")
|
|
|
|
proc cursorForward*(f: File, count = 1) =
|
|
## Moves the cursor forward by `count` columns.
|
|
when defined(windows):
|
|
let h = conHandle(f)
|
|
var p = getCursorPos(h)
|
|
inc(p.x, count)
|
|
setCursorPos(h, p.x, p.y)
|
|
else:
|
|
f.write(fmt"{stylePrefix}{count}C")
|
|
|
|
proc cursorBackward*(f: File, count = 1) =
|
|
## Moves the cursor backward by `count` columns.
|
|
when defined(windows):
|
|
let h = conHandle(f)
|
|
var p = getCursorPos(h)
|
|
dec(p.x, count)
|
|
setCursorPos(h, p.x, p.y)
|
|
else:
|
|
f.write(fmt"{stylePrefix}{count}D")
|
|
|
|
when true:
|
|
discard
|
|
else:
|
|
proc eraseLineEnd*(f: File) =
|
|
## Erases from the current cursor position to the end of the current line.
|
|
when defined(windows):
|
|
discard
|
|
else:
|
|
f.write("\e[K")
|
|
|
|
proc eraseLineStart*(f: File) =
|
|
## Erases from the current cursor position to the start of the current line.
|
|
when defined(windows):
|
|
discard
|
|
else:
|
|
f.write("\e[1K")
|
|
|
|
proc eraseDown*(f: File) =
|
|
## Erases the screen from the current line down to the bottom of the screen.
|
|
when defined(windows):
|
|
discard
|
|
else:
|
|
f.write("\e[J")
|
|
|
|
proc eraseUp*(f: File) =
|
|
## Erases the screen from the current line up to the top of the screen.
|
|
when defined(windows):
|
|
discard
|
|
else:
|
|
f.write("\e[1J")
|
|
|
|
proc eraseLine*(f: File) =
|
|
## Erases the entire current line.
|
|
when defined(windows):
|
|
let h = conHandle(f)
|
|
var scrbuf: CONSOLE_SCREEN_BUFFER_INFO
|
|
var numwrote: DWORD
|
|
if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0:
|
|
raiseOSError(osLastError())
|
|
var origin = scrbuf.dwCursorPosition
|
|
origin.x = 0'i16
|
|
if setConsoleCursorPosition(h, origin) == 0:
|
|
raiseOSError(osLastError())
|
|
var wt: DWORD = scrbuf.dwSize.x - origin.x
|
|
if fillConsoleOutputCharacter(h, ' ', wt,
|
|
origin, addr(numwrote)) == 0:
|
|
raiseOSError(osLastError())
|
|
if fillConsoleOutputAttribute(h, scrbuf.wAttributes, wt,
|
|
scrbuf.dwCursorPosition, addr(numwrote)) == 0:
|
|
raiseOSError(osLastError())
|
|
else:
|
|
f.write("\e[2K")
|
|
setCursorXPos(f, 0)
|
|
|
|
proc eraseScreen*(f: File) =
|
|
## Erases the screen with the background colour and moves the cursor to home.
|
|
when defined(windows):
|
|
let h = conHandle(f)
|
|
var scrbuf: CONSOLE_SCREEN_BUFFER_INFO
|
|
var numwrote: DWORD
|
|
var origin: COORD # is inititalized to 0, 0
|
|
|
|
if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0:
|
|
raiseOSError(osLastError())
|
|
let numChars = int32(scrbuf.dwSize.x)*int32(scrbuf.dwSize.y)
|
|
|
|
if fillConsoleOutputCharacter(h, ' ', numChars,
|
|
origin, addr(numwrote)) == 0:
|
|
raiseOSError(osLastError())
|
|
if fillConsoleOutputAttribute(h, scrbuf.wAttributes, numChars,
|
|
origin, addr(numwrote)) == 0:
|
|
raiseOSError(osLastError())
|
|
setCursorXPos(f, 0)
|
|
else:
|
|
f.write("\e[2J")
|
|
|
|
when not defined(windows):
|
|
var
|
|
gFG {.threadvar.}: int
|
|
gBG {.threadvar.}: int
|
|
|
|
proc resetAttributes*(f: File) =
|
|
## Resets all attributes.
|
|
when defined(windows):
|
|
let term = getTerminal()
|
|
if f == stderr:
|
|
discard setConsoleTextAttribute(term.hStderr, term.oldStderrAttr)
|
|
else:
|
|
discard setConsoleTextAttribute(term.hStdout, term.oldStdoutAttr)
|
|
else:
|
|
f.write(ansiResetCode)
|
|
gFG = 0
|
|
gBG = 0
|
|
|
|
type
|
|
Style* = enum ## different styles for text output
|
|
styleBright = 1, ## bright text
|
|
styleDim, ## dim text
|
|
styleItalic, ## italic (or reverse on terminals not supporting)
|
|
styleUnderscore, ## underscored text
|
|
styleBlink, ## blinking/bold text
|
|
styleBlinkRapid, ## rapid blinking/bold text (not widely supported)
|
|
styleReverse, ## reverse
|
|
styleHidden, ## hidden text
|
|
styleStrikethrough ## strikethrough
|
|
|
|
proc ansiStyleCode*(style: int): string =
|
|
result = fmt"{stylePrefix}{style}m"
|
|
|
|
template ansiStyleCode*(style: Style): string =
|
|
ansiStyleCode(style.int)
|
|
|
|
# The styleCache can be skipped when `style` is known at compile-time
|
|
template ansiStyleCode*(style: static[Style]): string =
|
|
(static(stylePrefix & $style.int & "m"))
|
|
|
|
proc setStyle*(f: File, style: set[Style]) =
|
|
## Sets the terminal style.
|
|
when defined(windows):
|
|
let h = conHandle(f)
|
|
var old = getAttributes(h) and (FOREGROUND_RGB or BACKGROUND_RGB)
|
|
var a = 0'i16
|
|
if styleBright in style: a = a or int16(FOREGROUND_INTENSITY)
|
|
if styleBlink in style: a = a or int16(BACKGROUND_INTENSITY)
|
|
if styleReverse in style: a = a or 0x4000'i16 # COMMON_LVB_REVERSE_VIDEO
|
|
if styleUnderscore in style: a = a or 0x8000'i16 # COMMON_LVB_UNDERSCORE
|
|
discard setConsoleTextAttribute(h, old or a)
|
|
else:
|
|
for s in items(style):
|
|
f.write(ansiStyleCode(s))
|
|
|
|
proc writeStyled*(txt: string, style: set[Style] = {styleBright}) =
|
|
## Writes the text `txt` in a given `style` to stdout.
|
|
when defined(windows):
|
|
let term = getTerminal()
|
|
var old = getAttributes(term.hStdout)
|
|
stdout.setStyle(style)
|
|
stdout.write(txt)
|
|
discard setConsoleTextAttribute(term.hStdout, old)
|
|
else:
|
|
stdout.setStyle(style)
|
|
stdout.write(txt)
|
|
stdout.resetAttributes()
|
|
if gFG != 0:
|
|
stdout.write(ansiStyleCode(gFG))
|
|
if gBG != 0:
|
|
stdout.write(ansiStyleCode(gBG))
|
|
|
|
type
|
|
ForegroundColor* = enum ## terminal's foreground colors
|
|
fgBlack = 30, ## black
|
|
fgRed, ## red
|
|
fgGreen, ## green
|
|
fgYellow, ## yellow
|
|
fgBlue, ## blue
|
|
fgMagenta, ## magenta
|
|
fgCyan, ## cyan
|
|
fgWhite, ## white
|
|
fg8Bit, ## 256-color (not supported, see `enableTrueColors` instead.)
|
|
fgDefault ## default terminal foreground color
|
|
|
|
BackgroundColor* = enum ## terminal's background colors
|
|
bgBlack = 40, ## black
|
|
bgRed, ## red
|
|
bgGreen, ## green
|
|
bgYellow, ## yellow
|
|
bgBlue, ## blue
|
|
bgMagenta, ## magenta
|
|
bgCyan, ## cyan
|
|
bgWhite, ## white
|
|
bg8Bit, ## 256-color (not supported, see `enableTrueColors` instead.)
|
|
bgDefault ## default terminal background color
|
|
|
|
when defined(windows):
|
|
var defaultForegroundColor, defaultBackgroundColor: int16 = 0xFFFF'i16 # Default to an invalid value 0xFFFF
|
|
|
|
proc setForegroundColor*(f: File, fg: ForegroundColor, bright = false) =
|
|
## Sets the terminal's foreground color.
|
|
when defined(windows):
|
|
let h = conHandle(f)
|
|
var old = getAttributes(h) and not FOREGROUND_RGB
|
|
if defaultForegroundColor == 0xFFFF'i16:
|
|
defaultForegroundColor = old
|
|
old = if bright: old or FOREGROUND_INTENSITY
|
|
else: old and not(FOREGROUND_INTENSITY)
|
|
const lookup: array[ForegroundColor, int] = [
|
|
0, # ForegroundColor enum with ordinal 30
|
|
(FOREGROUND_RED),
|
|
(FOREGROUND_GREEN),
|
|
(FOREGROUND_RED or FOREGROUND_GREEN),
|
|
(FOREGROUND_BLUE),
|
|
(FOREGROUND_RED or FOREGROUND_BLUE),
|
|
(FOREGROUND_BLUE or FOREGROUND_GREEN),
|
|
(FOREGROUND_BLUE or FOREGROUND_GREEN or FOREGROUND_RED),
|
|
0, # fg8Bit not supported, see `enableTrueColors` instead.
|
|
0] # unused
|
|
if fg == fgDefault:
|
|
discard setConsoleTextAttribute(h, toU16(old or defaultForegroundColor))
|
|
else:
|
|
discard setConsoleTextAttribute(h, toU16(old or lookup[fg]))
|
|
else:
|
|
gFG = ord(fg)
|
|
if bright: inc(gFG, 60)
|
|
f.write(ansiStyleCode(gFG))
|
|
|
|
proc setBackgroundColor*(f: File, bg: BackgroundColor, bright = false) =
|
|
## Sets the terminal's background color.
|
|
when defined(windows):
|
|
let h = conHandle(f)
|
|
var old = getAttributes(h) and not BACKGROUND_RGB
|
|
if defaultBackgroundColor == 0xFFFF'i16:
|
|
defaultBackgroundColor = old
|
|
old = if bright: old or BACKGROUND_INTENSITY
|
|
else: old and not(BACKGROUND_INTENSITY)
|
|
const lookup: array[BackgroundColor, int] = [
|
|
0, # BackgroundColor enum with ordinal 40
|
|
(BACKGROUND_RED),
|
|
(BACKGROUND_GREEN),
|
|
(BACKGROUND_RED or BACKGROUND_GREEN),
|
|
(BACKGROUND_BLUE),
|
|
(BACKGROUND_RED or BACKGROUND_BLUE),
|
|
(BACKGROUND_BLUE or BACKGROUND_GREEN),
|
|
(BACKGROUND_BLUE or BACKGROUND_GREEN or BACKGROUND_RED),
|
|
0, # bg8Bit not supported, see `enableTrueColors` instead.
|
|
0] # unused
|
|
if bg == bgDefault:
|
|
discard setConsoleTextAttribute(h, toU16(old or defaultBackgroundColor))
|
|
else:
|
|
discard setConsoleTextAttribute(h, toU16(old or lookup[bg]))
|
|
else:
|
|
gBG = ord(bg)
|
|
if bright: inc(gBG, 60)
|
|
f.write(ansiStyleCode(gBG))
|
|
|
|
proc ansiForegroundColorCode*(fg: ForegroundColor, bright = false): string =
|
|
var style = ord(fg)
|
|
if bright: inc(style, 60)
|
|
return ansiStyleCode(style)
|
|
|
|
template ansiForegroundColorCode*(fg: static[ForegroundColor],
|
|
bright: static[bool] = false): string =
|
|
ansiStyleCode(fg.int + bright.int * 60)
|
|
|
|
proc ansiForegroundColorCode*(color: Color): string =
|
|
let rgb = extractRGB(color)
|
|
result = fmt"{fgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
|
|
|
|
template ansiForegroundColorCode*(color: static[Color]): string =
|
|
const rgb = extractRGB(color)
|
|
# no usage of `fmt`, see issue #7632
|
|
(static("$1$2;$3;$4m" % [$fgPrefix, $(rgb.r), $(rgb.g), $(rgb.b)]))
|
|
|
|
proc ansiBackgroundColorCode*(color: Color): string =
|
|
let rgb = extractRGB(color)
|
|
result = fmt"{bgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
|
|
|
|
template ansiBackgroundColorCode*(color: static[Color]): string =
|
|
const rgb = extractRGB(color)
|
|
# no usage of `fmt`, see issue #7632
|
|
(static("$1$2;$3;$4m" % [$bgPrefix, $(rgb.r), $(rgb.g), $(rgb.b)]))
|
|
|
|
proc setForegroundColor*(f: File, color: Color) =
|
|
## Sets the terminal's foreground true color.
|
|
if getTerminal().trueColorIsEnabled:
|
|
f.write(ansiForegroundColorCode(color))
|
|
|
|
proc setBackgroundColor*(f: File, color: Color) =
|
|
## Sets the terminal's background true color.
|
|
if getTerminal().trueColorIsEnabled:
|
|
f.write(ansiBackgroundColorCode(color))
|
|
|
|
proc setTrueColor(f: File, color: Color) =
|
|
let term = getTerminal()
|
|
if term.fgSetColor:
|
|
setForegroundColor(f, color)
|
|
else:
|
|
setBackgroundColor(f, color)
|
|
|
|
proc isatty*(f: File): bool =
|
|
## Returns true if `f` is associated with a terminal device.
|
|
when defined(posix):
|
|
proc isatty(fildes: FileHandle): cint {.
|
|
importc: "isatty", header: "<unistd.h>".}
|
|
else:
|
|
proc isatty(fildes: FileHandle): cint {.
|
|
importc: "_isatty", header: "<io.h>".}
|
|
|
|
result = isatty(getFileHandle(f)) != 0'i32
|
|
|
|
type
|
|
TerminalCmd* = enum ## commands that can be expressed as arguments
|
|
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, style: Style) = setStyle(f, {style})
|
|
template styledEchoProcessArg(f: File, style: set[Style]) = setStyle f, style
|
|
template styledEchoProcessArg(f: File, color: ForegroundColor) =
|
|
setForegroundColor f, color
|
|
template styledEchoProcessArg(f: File, color: BackgroundColor) =
|
|
setBackgroundColor f, color
|
|
template styledEchoProcessArg(f: File, color: Color) =
|
|
setTrueColor f, color
|
|
template styledEchoProcessArg(f: File, cmd: TerminalCmd) =
|
|
when cmd == resetStyle:
|
|
resetAttributes(f)
|
|
elif cmd in {fgColor, bgColor}:
|
|
let term = getTerminal()
|
|
term.fgSetColor = cmd == fgColor
|
|
|
|
macro styledWrite*(f: File, m: varargs[typed]): untyped =
|
|
## Similar to `write`, but treating terminal style arguments specially.
|
|
## When some argument is `Style`, `set[Style]`, `ForegroundColor`,
|
|
## `BackgroundColor` or `TerminalCmd` then it is not sent directly to
|
|
## `f`, but instead corresponding terminal style proc is called.
|
|
##
|
|
## Example:
|
|
##
|
|
## .. code-block:: nim
|
|
##
|
|
## stdout.styledWrite(fgRed, "red text ")
|
|
## stdout.styledWrite(fgGreen, "green text")
|
|
##
|
|
var reset = false
|
|
result = newNimNode(nnkStmtList)
|
|
|
|
for i in countup(0, m.len - 1):
|
|
let item = m[i]
|
|
case item.kind
|
|
of nnkStrLit..nnkTripleStrLit:
|
|
if i == m.len - 1:
|
|
# optimize if string literal is last, just call write
|
|
result.add(newCall(bindSym"write", f, item))
|
|
if reset: result.add(newCall(bindSym"resetAttributes", f))
|
|
return
|
|
else:
|
|
# if it is string literal just call write, do not enable reset
|
|
result.add(newCall(bindSym"write", f, item))
|
|
else:
|
|
result.add(newCall(bindSym"styledEchoProcessArg", f, item))
|
|
reset = true
|
|
if reset: result.add(newCall(bindSym"resetAttributes", f))
|
|
|
|
template styledWriteLine*(f: File, args: varargs[untyped]) =
|
|
## Calls `styledWrite` and appends a newline at the end.
|
|
##
|
|
## Example:
|
|
##
|
|
## .. code-block:: nim
|
|
##
|
|
## proc error(msg: string) =
|
|
## styledWriteLine(stderr, fgRed, "Error: ", resetStyle, msg)
|
|
##
|
|
styledWrite(f, args)
|
|
write(f, "\n")
|
|
|
|
template styledEcho*(args: varargs[untyped]) =
|
|
## Echoes styles arguments to stdout using `styledWriteLine`.
|
|
stdout.styledWriteLine(args)
|
|
|
|
proc getch*(): char =
|
|
## Read a single character from the terminal, blocking until it is entered.
|
|
## The character is not printed to the terminal.
|
|
when defined(windows):
|
|
let fd = getStdHandle(STD_INPUT_HANDLE)
|
|
var keyEvent = KEY_EVENT_RECORD()
|
|
var numRead: cint
|
|
while true:
|
|
# Block until character is entered
|
|
doAssert(waitForSingleObject(fd, INFINITE) == WAIT_OBJECT_0)
|
|
doAssert(readConsoleInput(fd, addr(keyEvent), 1, addr(numRead)) != 0)
|
|
if numRead == 0 or keyEvent.eventType != 1 or keyEvent.bKeyDown == 0:
|
|
continue
|
|
return char(keyEvent.uChar)
|
|
else:
|
|
let fd = getFileHandle(stdin)
|
|
var oldMode: Termios
|
|
discard fd.tcGetAttr(addr oldMode)
|
|
fd.setRaw()
|
|
result = stdin.readChar()
|
|
discard fd.tcSetAttr(TCSADRAIN, addr oldMode)
|
|
|
|
when defined(windows):
|
|
proc readPasswordFromStdin*(prompt: string, password: var string):
|
|
bool {.tags: [ReadIOEffect, WriteIOEffect].} =
|
|
## Reads a `password` from stdin without printing it. `password` must not
|
|
## be `nil`! Returns `false` if the end of the file has been reached,
|
|
## `true` otherwise.
|
|
password.setLen(0)
|
|
stdout.write(prompt)
|
|
let hi = createFileA("CONIN$",
|
|
GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0)
|
|
var mode = DWORD 0
|
|
discard getConsoleMode(hi, addr mode)
|
|
let origMode = mode
|
|
const
|
|
ENABLE_PROCESSED_INPUT = 1
|
|
ENABLE_ECHO_INPUT = 4
|
|
mode = (mode or ENABLE_PROCESSED_INPUT) and not ENABLE_ECHO_INPUT
|
|
|
|
discard setConsoleMode(hi, mode)
|
|
result = readLine(stdin, password)
|
|
discard setConsoleMode(hi, origMode)
|
|
discard closeHandle(hi)
|
|
stdout.write "\n"
|
|
|
|
else:
|
|
import termios
|
|
|
|
proc readPasswordFromStdin*(prompt: string, password: var string):
|
|
bool {.tags: [ReadIOEffect, WriteIOEffect].} =
|
|
password.setLen(0)
|
|
let fd = stdin.getFileHandle()
|
|
var cur, old: Termios
|
|
discard fd.tcGetAttr(cur.addr)
|
|
old = cur
|
|
cur.c_lflag = cur.c_lflag and not Cflag(ECHO)
|
|
discard fd.tcSetAttr(TCSADRAIN, cur.addr)
|
|
stdout.write prompt
|
|
result = stdin.readLine(password)
|
|
stdout.write "\n"
|
|
discard fd.tcSetAttr(TCSADRAIN, old.addr)
|
|
|
|
proc readPasswordFromStdin*(prompt = "password: "): string =
|
|
## Reads a password from stdin without printing it.
|
|
result = ""
|
|
discard readPasswordFromStdin(prompt, result)
|
|
|
|
|
|
# Wrappers assuming output to stdout:
|
|
template hideCursor*() = hideCursor(stdout)
|
|
template showCursor*() = showCursor(stdout)
|
|
template setCursorPos*(x, y: int) = setCursorPos(stdout, x, y)
|
|
template setCursorXPos*(x: int) = setCursorXPos(stdout, x)
|
|
when defined(windows):
|
|
template setCursorYPos*(x: int) = setCursorYPos(stdout, x)
|
|
template cursorUp*(count = 1) = cursorUp(stdout, count)
|
|
template cursorDown*(count = 1) = cursorDown(stdout, count)
|
|
template cursorForward*(count = 1) = cursorForward(stdout, count)
|
|
template cursorBackward*(count = 1) = cursorBackward(stdout, count)
|
|
template eraseLine*() = eraseLine(stdout)
|
|
template eraseScreen*() = eraseScreen(stdout)
|
|
template setStyle*(style: set[Style]) =
|
|
setStyle(stdout, style)
|
|
template setForegroundColor*(fg: ForegroundColor, bright = false) =
|
|
setForegroundColor(stdout, fg, bright)
|
|
template setBackgroundColor*(bg: BackgroundColor, bright = false) =
|
|
setBackgroundColor(stdout, bg, bright)
|
|
template setForegroundColor*(color: Color) =
|
|
setForegroundColor(stdout, color)
|
|
template setBackgroundColor*(color: Color) =
|
|
setBackgroundColor(stdout, color)
|
|
proc resetAttributes*() {.noconv.} =
|
|
## Resets all attributes on stdout.
|
|
## It is advisable to register this as a quit proc with
|
|
## `exitprocs.addExitProc(resetAttributes)`.
|
|
resetAttributes(stdout)
|
|
|
|
proc isTrueColorSupported*(): bool =
|
|
## Returns true if a terminal supports true color.
|
|
return getTerminal().trueColorIsSupported
|
|
|
|
when defined(windows):
|
|
import os
|
|
|
|
proc enableTrueColors*() =
|
|
## Enable true color.
|
|
var term = getTerminal()
|
|
when defined(windows):
|
|
var
|
|
ver: OSVERSIONINFO
|
|
ver.dwOSVersionInfoSize = sizeof(ver).DWORD
|
|
let res = getVersionExW(addr ver)
|
|
if res == 0:
|
|
term.trueColorIsSupported = false
|
|
else:
|
|
term.trueColorIsSupported = ver.dwMajorVersion > 10 or
|
|
(ver.dwMajorVersion == 10 and (ver.dwMinorVersion > 0 or
|
|
(ver.dwMinorVersion == 0 and ver.dwBuildNumber >= 10586)))
|
|
if not term.trueColorIsSupported:
|
|
term.trueColorIsSupported = getEnv("ANSICON_DEF").len > 0
|
|
|
|
if term.trueColorIsSupported:
|
|
if getEnv("ANSICON_DEF").len == 0:
|
|
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:
|
|
term.trueColorIsEnabled = true
|
|
else:
|
|
term.trueColorIsEnabled = false
|
|
else:
|
|
term.trueColorIsEnabled = true
|
|
else:
|
|
term.trueColorIsSupported = getEnv("COLORTERM").toLowerAscii() in [
|
|
"truecolor", "24bit"]
|
|
term.trueColorIsEnabled = term.trueColorIsSupported
|
|
|
|
proc disableTrueColors*() =
|
|
## Disable true color.
|
|
var term = getTerminal()
|
|
when defined(windows):
|
|
if term.trueColorIsSupported:
|
|
if getEnv("ANSICON_DEF").len == 0:
|
|
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)
|
|
term.trueColorIsEnabled = false
|
|
else:
|
|
term.trueColorIsEnabled = false
|
|
|
|
proc newTerminal(): owned(PTerminal) =
|
|
new result
|
|
when defined(windows):
|
|
initTerminal(result)
|
|
|
|
when not defined(testing) and isMainModule:
|
|
assert ansiStyleCode(styleBright) == "\e[1m"
|
|
assert ansiStyleCode(styleStrikethrough) == "\e[9m"
|
|
# exitprocs.addExitProc(resetAttributes)
|
|
write(stdout, "never mind")
|
|
stdout.eraseLine()
|
|
stdout.styledWriteLine({styleBright, styleBlink, styleUnderscore}, "styled text ")
|
|
stdout.styledWriteLine("italic text ", {styleItalic})
|
|
stdout.setBackGroundColor(bgCyan, true)
|
|
stdout.setForeGroundColor(fgBlue)
|
|
stdout.write("blue text in cyan background")
|
|
stdout.resetAttributes()
|
|
echo ""
|
|
stdout.writeLine("ordinary text")
|
|
echo "more ordinary text"
|
|
styledEcho styleBright, fgGreen, "[PASS]", resetStyle, fgGreen, " Yay!"
|
|
echo "ordinary text again"
|
|
styledEcho styleBright, fgRed, "[FAIL]", resetStyle, fgRed, " Nay :("
|
|
echo "ordinary text again"
|
|
setForeGroundColor(fgGreen)
|
|
echo "green text"
|
|
echo "more green text"
|
|
setForeGroundColor(fgBlue)
|
|
echo "blue text"
|
|
resetAttributes()
|
|
echo "ordinary text"
|
|
|
|
stdout.styledWriteLine(fgRed, "red text ")
|
|
stdout.styledWriteLine(fgWhite, bgRed, "white text in red background")
|
|
stdout.styledWriteLine(" ordinary text ")
|
|
stdout.styledWriteLine(fgGreen, "green text")
|
|
|
|
writeStyled("underscored text", {styleUnderscore})
|
|
stdout.styledWrite(fgRed, " red text ")
|
|
writeStyled("bright text ", {styleBright})
|
|
echo "ordinary text"
|
|
|
|
stdout.styledWrite(fgRed, "red text ")
|
|
stdout.styledWrite(fgWhite, bgRed, "white text in red background")
|
|
stdout.styledWrite(" ordinary text ")
|
|
stdout.styledWrite(fgGreen, "green text")
|
|
echo ""
|
|
echo "ordinary text"
|
|
stdout.styledWriteLine(fgRed, "red text ", styleBright, "bold red", fgDefault, " bold text")
|
|
stdout.styledWriteLine(bgYellow, "text in yellow bg", styleBright,
|
|
" bold text in yellow bg", bgDefault, " bold text")
|
|
echo "ordinary text"
|