#
#
# 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 writeLine
result.add(newCall(bindSym"writeLine", 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)
writeLine(stdout, "ordinary text")
styledEcho("styled text ", {styleBright, styleBlink, styleUnderscore})