123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955 |
- #
- #
- # 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.
- ## 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, tables
- 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.}: PTerminal
- proc newTerminal(): PTerminal
- proc getTerminal(): PTerminal {.inline.} =
- if isNil(gTerm):
- gTerm = newTerminal()
- result = gTerm
- const
- fgPrefix = "\x1b[38;2;"
- bgPrefix = "\x1b[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: CONSOLESCREENBUFFERINFO
- 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: CONSOLESCREENBUFFERINFO
- # 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(string(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(string(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: CONSOLESCREENBUFFERINFO
- 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: CONSOLESCREENBUFFERINFO
- 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: CONSOLESCREENBUFFERINFO
- 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: CONSOLESCREENBUFFERINFO
- 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")
- 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)
- 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
- {.deprecated: [TStyle: Style].}
- {.deprecated: [styleUnknown: styleItalic].}
- when not defined(windows):
- var
- gFG {.threadvar.}: int
- gBG {.threadvar.}: int
- 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
- {.deprecated: [TForegroundColor: ForegroundColor,
- TBackgroundColor: BackgroundColor].}
- 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)
- when cmd == fgColor:
- fgSetColor = true
- when cmd == bgColor:
- fgSetColor = false
- 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):
- from unicode import toUTF8, Rune, runeLenAt
- proc readPasswordFromStdin*(prompt: string, password: var TaintedString):
- 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.string.setLen(0)
- stdout.write(prompt)
- while true:
- let c = getch()
- case c.char
- of '\r', chr(0xA):
- break
- of '\b':
- # ensure we delete the whole UTF-8 character:
- var i = 0
- var x = 1
- while i < password.len:
- x = runeLenAt(password.string, i)
- inc i, x
- password.string.setLen(max(password.len - x, 0))
- of chr(0x0):
- # modifier key - ignore - for details see
- # https://github.com/nim-lang/Nim/issues/7764
- continue
- else:
- password.string.add(toUTF8(c.Rune))
- stdout.write "\n"
- else:
- import termios
- proc readPasswordFromStdin*(prompt: string, password: var TaintedString):
- bool {.tags: [ReadIOEffect, WriteIOEffect].} =
- password.string.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: "): TaintedString =
- ## Reads a password from stdin without printing it.
- result = TaintedString("")
- 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
- ## ``system.addQuitProc(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 = string(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(): PTerminal =
- new result
- when defined(windows):
- initTerminal(result)
- when not defined(testing) and isMainModule:
- assert ansiStyleCode(styleBright) == "\e[1m"
- assert ansiStyleCode(styleStrikethrough) == "\e[9m"
- #system.addQuitProc(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")
- 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"
|