1. Introduce TerminalCmd enum and resetStyle that can be issued to issue resetAttributes within styledEcho arguments. 2. Use templates to resolve styledEcho arguments for performance reasons. 3. Try to avoid calling trailing write "\n" and reset attributes where possible.
476 lines
16 KiB
Nim
476 lines
16 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 ``system.addQuitProc(resetAttributes)`` to restore the defaults.
|
|
|
|
import macros
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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 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".}
|
|
|
|
var
|
|
conHandle: Handle
|
|
# = createFile("CONOUT$", GENERIC_WRITE, 0, nil, OPEN_ALWAYS, 0, 0)
|
|
|
|
block:
|
|
var hTemp = getStdHandle(STD_OUTPUT_HANDLE)
|
|
if duplicateHandle(getCurrentProcess(), hTemp, getCurrentProcess(),
|
|
addr(conHandle), 0, 1, DUPLICATE_SAME_ACCESS) == 0:
|
|
raiseOSError(osLastError())
|
|
|
|
proc getCursorPos(): tuple [x,y: int] =
|
|
var c: CONSOLESCREENBUFFERINFO
|
|
if getConsoleScreenBufferInfo(conHandle, addr(c)) == 0:
|
|
raiseOSError(osLastError())
|
|
return (int(c.dwCursorPosition.X), int(c.dwCursorPosition.Y))
|
|
|
|
proc getAttributes(): int16 =
|
|
var c: CONSOLESCREENBUFFERINFO
|
|
# workaround Windows bugs: try several times
|
|
if getConsoleScreenBufferInfo(conHandle, addr(c)) != 0:
|
|
return c.wAttributes
|
|
return 0x70'i16 # ERROR: return white background, black text
|
|
|
|
var
|
|
oldAttr = getAttributes()
|
|
|
|
else:
|
|
import termios, unsigned
|
|
|
|
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 setCursorPos*(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):
|
|
var c: COORD
|
|
c.X = int16(x)
|
|
c.Y = int16(y)
|
|
if setConsoleCursorPosition(conHandle, c) == 0: raiseOSError(osLastError())
|
|
else:
|
|
stdout.write("\e[" & $y & ';' & $x & 'f')
|
|
|
|
proc setCursorXPos*(x: int) =
|
|
## sets the terminal's cursor to the x position. The y position is
|
|
## not changed.
|
|
when defined(windows):
|
|
var scrbuf: CONSOLESCREENBUFFERINFO
|
|
var hStdout = conHandle
|
|
if getConsoleScreenBufferInfo(hStdout, addr(scrbuf)) == 0:
|
|
raiseOSError(osLastError())
|
|
var origin = scrbuf.dwCursorPosition
|
|
origin.X = int16(x)
|
|
if setConsoleCursorPosition(conHandle, origin) == 0:
|
|
raiseOSError(osLastError())
|
|
else:
|
|
stdout.write("\e[" & $x & 'G')
|
|
|
|
when defined(windows):
|
|
proc setCursorYPos*(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):
|
|
var scrbuf: CONSOLESCREENBUFFERINFO
|
|
var hStdout = conHandle
|
|
if getConsoleScreenBufferInfo(hStdout, addr(scrbuf)) == 0:
|
|
raiseOSError(osLastError())
|
|
var origin = scrbuf.dwCursorPosition
|
|
origin.Y = int16(y)
|
|
if setConsoleCursorPosition(conHandle, origin) == 0:
|
|
raiseOSError(osLastError())
|
|
else:
|
|
discard
|
|
|
|
proc cursorUp*(count=1) =
|
|
## Moves the cursor up by `count` rows.
|
|
when defined(windows):
|
|
var p = getCursorPos()
|
|
dec(p.y, count)
|
|
setCursorPos(p.x, p.y)
|
|
else:
|
|
stdout.write("\e[" & $count & 'A')
|
|
|
|
proc cursorDown*(count=1) =
|
|
## Moves the cursor down by `count` rows.
|
|
when defined(windows):
|
|
var p = getCursorPos()
|
|
inc(p.y, count)
|
|
setCursorPos(p.x, p.y)
|
|
else:
|
|
stdout.write("\e[" & $count & 'B')
|
|
|
|
proc cursorForward*(count=1) =
|
|
## Moves the cursor forward by `count` columns.
|
|
when defined(windows):
|
|
var p = getCursorPos()
|
|
inc(p.x, count)
|
|
setCursorPos(p.x, p.y)
|
|
else:
|
|
stdout.write("\e[" & $count & 'C')
|
|
|
|
proc cursorBackward*(count=1) =
|
|
## Moves the cursor backward by `count` columns.
|
|
when defined(windows):
|
|
var p = getCursorPos()
|
|
dec(p.x, count)
|
|
setCursorPos(p.x, p.y)
|
|
else:
|
|
stdout.write("\e[" & $count & 'D')
|
|
|
|
when true:
|
|
discard
|
|
else:
|
|
proc eraseLineEnd* =
|
|
## Erases from the current cursor position to the end of the current line.
|
|
when defined(windows):
|
|
discard
|
|
else:
|
|
stdout.write("\e[K")
|
|
|
|
proc eraseLineStart* =
|
|
## Erases from the current cursor position to the start of the current line.
|
|
when defined(windows):
|
|
discard
|
|
else:
|
|
stdout.write("\e[1K")
|
|
|
|
proc eraseDown* =
|
|
## Erases the screen from the current line down to the bottom of the screen.
|
|
when defined(windows):
|
|
discard
|
|
else:
|
|
stdout.write("\e[J")
|
|
|
|
proc eraseUp* =
|
|
## Erases the screen from the current line up to the top of the screen.
|
|
when defined(windows):
|
|
discard
|
|
else:
|
|
stdout.write("\e[1J")
|
|
|
|
proc eraseLine* =
|
|
## Erases the entire current line.
|
|
when defined(windows):
|
|
var scrbuf: CONSOLESCREENBUFFERINFO
|
|
var numwrote: DWORD
|
|
var hStdout = conHandle
|
|
if getConsoleScreenBufferInfo(hStdout, addr(scrbuf)) == 0:
|
|
raiseOSError(osLastError())
|
|
var origin = scrbuf.dwCursorPosition
|
|
origin.X = 0'i16
|
|
if setConsoleCursorPosition(conHandle, origin) == 0:
|
|
raiseOSError(osLastError())
|
|
var ht = scrbuf.dwSize.Y - origin.Y
|
|
var wt = scrbuf.dwSize.X - origin.X
|
|
if fillConsoleOutputCharacter(hStdout,' ', ht*wt,
|
|
origin, addr(numwrote)) == 0:
|
|
raiseOSError(osLastError())
|
|
if fillConsoleOutputAttribute(hStdout, scrbuf.wAttributes, ht * wt,
|
|
scrbuf.dwCursorPosition, addr(numwrote)) == 0:
|
|
raiseOSError(osLastError())
|
|
else:
|
|
stdout.write("\e[2K")
|
|
setCursorXPos(0)
|
|
|
|
proc eraseScreen* =
|
|
## Erases the screen with the background colour and moves the cursor to home.
|
|
when defined(windows):
|
|
var scrbuf: CONSOLESCREENBUFFERINFO
|
|
var numwrote: DWORD
|
|
var origin: COORD # is inititalized to 0, 0
|
|
var hStdout = conHandle
|
|
|
|
if getConsoleScreenBufferInfo(hStdout, addr(scrbuf)) == 0:
|
|
raiseOSError(osLastError())
|
|
let numChars = int32(scrbuf.dwSize.X)*int32(scrbuf.dwSize.Y)
|
|
|
|
if fillConsoleOutputCharacter(hStdout, ' ', numChars,
|
|
origin, addr(numwrote)) == 0:
|
|
raiseOSError(osLastError())
|
|
if fillConsoleOutputAttribute(hStdout, scrbuf.wAttributes, numChars,
|
|
origin, addr(numwrote)) == 0:
|
|
raiseOSError(osLastError())
|
|
setCursorXPos(0)
|
|
else:
|
|
stdout.write("\e[2J")
|
|
|
|
proc resetAttributes* {.noconv.} =
|
|
## resets all attributes; it is advisable to register this as a quit proc
|
|
## with ``system.addQuitProc(resetAttributes)``.
|
|
when defined(windows):
|
|
discard setConsoleTextAttribute(conHandle, oldAttr)
|
|
else:
|
|
stdout.write("\e[0m")
|
|
|
|
type
|
|
Style* = enum ## different styles for text output
|
|
styleBright = 1, ## bright text
|
|
styleDim, ## dim text
|
|
styleUnknown, ## unknown
|
|
styleUnderscore = 4, ## underscored text
|
|
styleBlink, ## blinking/bold text
|
|
styleReverse = 7, ## unknown
|
|
styleHidden ## hidden text
|
|
|
|
{.deprecated: [TStyle: Style].}
|
|
|
|
when not defined(windows):
|
|
var
|
|
# XXX: These better be thread-local
|
|
gFG = 0
|
|
gBG = 0
|
|
|
|
proc setStyle*(style: set[Style]) =
|
|
## sets the terminal style
|
|
when defined(windows):
|
|
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(conHandle, a)
|
|
else:
|
|
for s in items(style):
|
|
stdout.write("\e[" & $ord(s) & 'm')
|
|
|
|
proc writeStyled*(txt: string, style: set[Style] = {styleBright}) =
|
|
## writes the text `txt` in a given `style`.
|
|
when defined(windows):
|
|
var old = getAttributes()
|
|
setStyle(style)
|
|
stdout.write(txt)
|
|
discard setConsoleTextAttribute(conHandle, old)
|
|
else:
|
|
setStyle(style)
|
|
stdout.write(txt)
|
|
resetAttributes()
|
|
if gFG != 0:
|
|
stdout.write("\e[" & $ord(gFG) & 'm')
|
|
if gBG != 0:
|
|
stdout.write("\e[" & $ord(gBG) & 'm')
|
|
|
|
type
|
|
ForegroundColor* = enum ## terminal's foreground colors
|
|
fgBlack = 30, ## black
|
|
fgRed, ## red
|
|
fgGreen, ## green
|
|
fgYellow, ## yellow
|
|
fgBlue, ## blue
|
|
fgMagenta, ## magenta
|
|
fgCyan, ## cyan
|
|
fgWhite ## white
|
|
|
|
BackgroundColor* = enum ## terminal's background colors
|
|
bgBlack = 40, ## black
|
|
bgRed, ## red
|
|
bgGreen, ## green
|
|
bgYellow, ## yellow
|
|
bgBlue, ## blue
|
|
bgMagenta, ## magenta
|
|
bgCyan, ## cyan
|
|
bgWhite ## white
|
|
|
|
{.deprecated: [TForegroundColor: ForegroundColor,
|
|
TBackgroundColor: BackgroundColor].}
|
|
|
|
proc setForegroundColor*(fg: ForegroundColor, bright=false) =
|
|
## sets the terminal's foreground color
|
|
when defined(windows):
|
|
var old = getAttributes() and not 0x0007
|
|
if bright:
|
|
old = old or FOREGROUND_INTENSITY
|
|
const lookup: array [ForegroundColor, int] = [
|
|
0,
|
|
(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)]
|
|
discard setConsoleTextAttribute(conHandle, toU16(old or lookup[fg]))
|
|
else:
|
|
gFG = ord(fg)
|
|
if bright: inc(gFG, 60)
|
|
stdout.write("\e[" & $gFG & 'm')
|
|
|
|
proc setBackgroundColor*(bg: BackgroundColor, bright=false) =
|
|
## sets the terminal's background color
|
|
when defined(windows):
|
|
var old = getAttributes() and not 0x0070
|
|
if bright:
|
|
old = old or BACKGROUND_INTENSITY
|
|
const lookup: array [BackgroundColor, int] = [
|
|
0,
|
|
(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)]
|
|
discard setConsoleTextAttribute(conHandle, toU16(old or lookup[bg]))
|
|
else:
|
|
gBG = ord(bg)
|
|
if bright: inc(gBG, 60)
|
|
stdout.write("\e[" & $gBG & 'm')
|
|
|
|
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
|
|
|
|
template styledEchoProcessArg(s: string) = write stdout, s
|
|
template styledEchoProcessArg(style: Style) = setStyle({style})
|
|
template styledEchoProcessArg(style: set[Style]) = setStyle style
|
|
template styledEchoProcessArg(color: ForegroundColor) = setForegroundColor color
|
|
template styledEchoProcessArg(color: BackgroundColor) = setBackgroundColor color
|
|
template styledEchoProcessArg(cmd: TerminalCmd) =
|
|
when cmd == resetStyle:
|
|
resetAttributes()
|
|
|
|
macro styledEcho*(m: varargs[expr]): stmt =
|
|
## to be documented.
|
|
let m = callsite()
|
|
var reset = false
|
|
result = newNimNode(nnkStmtList)
|
|
|
|
for i in countup(1, 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 writeln
|
|
result.add(newCall(bindSym"writeln", bindSym"stdout", item))
|
|
if reset: result.add(newCall(bindSym"resetAttributes"))
|
|
return
|
|
else:
|
|
# if it is string literal just call write, do not enable reset
|
|
result.add(newCall(bindSym"write", bindSym"stdout", item))
|
|
else:
|
|
result.add(newCall(bindSym"styledEchoProcessArg", item))
|
|
reset = true
|
|
|
|
result.add(newCall(bindSym"write", bindSym"stdout", newStrLitNode("\n")))
|
|
if reset: result.add(newCall(bindSym"resetAttributes"))
|
|
|
|
when defined(nimdoc):
|
|
proc getch*(): char =
|
|
## Read a single character from the terminal, blocking until it is entered.
|
|
## The character is not printed to the terminal. This is not available for
|
|
## Windows.
|
|
discard
|
|
elif not defined(windows):
|
|
proc getch*(): char =
|
|
## Read a single character from the terminal, blocking until it is entered.
|
|
## The character is not printed to the terminal. This is not available for
|
|
## Windows.
|
|
let fd = getFileHandle(stdin)
|
|
var oldMode: Termios
|
|
discard fd.tcgetattr(addr oldMode)
|
|
fd.setRaw()
|
|
result = stdin.readChar()
|
|
discard fd.tcsetattr(TCSADRAIN, addr oldMode)
|
|
|
|
when not defined(testing) and isMainModule:
|
|
system.addQuitProc(resetAttributes)
|
|
write(stdout, "never mind")
|
|
eraseLine()
|
|
#setCursorPos(2, 2)
|
|
writeStyled("styled text ", {styleBright, styleBlink, styleUnderscore})
|
|
setBackGroundColor(bgCyan, true)
|
|
setForeGroundColor(fgBlue)
|
|
writeln(stdout, "ordinary text")
|
|
|
|
styledEcho("styled text ", {styleBright, styleBlink, styleUnderscore})
|
|
|