Nim/lib/pure/terminal.nim
Adam Strzelecki 1057770ce5 colors: Introduce resetStyle enum & use templates
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.
2015-06-15 11:26:53 +02:00

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})