1# 2# 3# Nim's Runtime Library 4# (c) Copyright 2012 Andreas Rumpf 5# 6# See the file "copying.txt", included in this 7# distribution, for details about the copyright. 8# 9 10## This module contains a few procedures to control the *terminal* 11## (also called *console*). On UNIX, the implementation simply uses ANSI escape 12## sequences and does not depend on any other module, on Windows it uses the 13## Windows API. 14## Changing the style is permanent even after program termination! Use the 15## code `exitprocs.addExitProc(resetAttributes)` to restore the defaults. 16## Similarly, if you hide the cursor, make sure to unhide it with 17## `showCursor` before quitting. 18## 19## Progress bar 20## ============ 21## 22## Basic progress bar example: 23runnableExamples("-r:off"): 24 import std/[os, strutils] 25 26 for i in 0..100: 27 stdout.styledWriteLine(fgRed, "0% ", fgWhite, '#'.repeat i, if i > 50: fgGreen else: fgYellow, "\t", $i , "%") 28 sleep 42 29 cursorUp 1 30 eraseLine() 31 32 stdout.resetAttributes() 33 34##[ 35## Playing with colorful and styled text 36]## 37 38## Procs like `styledWriteLine`, `styledEcho` etc. have a temporary effect on 39## text parameters. Style parameters only affect the text parameter right after them. 40## After being called, these procs will reset the default style of the terminal. 41## While `setBackGroundColor`, `setForeGroundColor` etc. have a lasting 42## influence on the terminal, you can use `resetAttributes` to 43## reset the default style of the terminal. 44runnableExamples("-r:off"): 45 stdout.styledWriteLine({styleBright, styleBlink, styleUnderscore}, "styled text ") 46 stdout.styledWriteLine(fgRed, "red text ") 47 stdout.styledWriteLine(fgWhite, bgRed, "white text in red background") 48 stdout.styledWriteLine(" ordinary text without style ") 49 50 stdout.setBackGroundColor(bgCyan, true) 51 stdout.setForeGroundColor(fgBlue) 52 stdout.write("blue text in cyan background") 53 stdout.resetAttributes() 54 55 # You can specify multiple text parameters. Style parameters 56 # only affect the text parameter right after them. 57 styledEcho styleBright, fgGreen, "[PASS]", resetStyle, fgGreen, " Yay!" 58 59 stdout.styledWriteLine(fgRed, "red text ", styleBright, "bold red", fgDefault, " bold text") 60 61import macros 62import strformat 63from strutils import toLowerAscii, `%` 64import colors 65 66when defined(windows): 67 import winlean 68 69type 70 PTerminal = ref object 71 trueColorIsSupported: bool 72 trueColorIsEnabled: bool 73 fgSetColor: bool 74 when defined(windows): 75 hStdout: Handle 76 hStderr: Handle 77 oldStdoutAttr: int16 78 oldStderrAttr: int16 79 80var gTerm {.threadvar.}: owned(PTerminal) 81 82when defined(windows) and defined(consoleapp): 83 proc newTerminal(): owned(PTerminal) {.gcsafe, raises: [OSError].} 84else: 85 proc newTerminal(): owned(PTerminal) {.gcsafe, raises: [].} 86 87proc getTerminal(): PTerminal {.inline.} = 88 if isNil(gTerm): 89 gTerm = newTerminal() 90 result = gTerm 91 92const 93 fgPrefix = "\e[38;2;" 94 bgPrefix = "\e[48;2;" 95 ansiResetCode* = "\e[0m" 96 stylePrefix = "\e[" 97 98when defined(windows): 99 import winlean, os 100 101 const 102 DUPLICATE_SAME_ACCESS = 2 103 FOREGROUND_BLUE = 1 104 FOREGROUND_GREEN = 2 105 FOREGROUND_RED = 4 106 FOREGROUND_INTENSITY = 8 107 BACKGROUND_BLUE = 16 108 BACKGROUND_GREEN = 32 109 BACKGROUND_RED = 64 110 BACKGROUND_INTENSITY = 128 111 FOREGROUND_RGB = FOREGROUND_RED or FOREGROUND_GREEN or FOREGROUND_BLUE 112 BACKGROUND_RGB = BACKGROUND_RED or BACKGROUND_GREEN or BACKGROUND_BLUE 113 114 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 115 116 type 117 SHORT = int16 118 COORD = object 119 x: SHORT 120 y: SHORT 121 122 SMALL_RECT = object 123 left: SHORT 124 top: SHORT 125 right: SHORT 126 bottom: SHORT 127 128 CONSOLE_SCREEN_BUFFER_INFO = object 129 dwSize: COORD 130 dwCursorPosition: COORD 131 wAttributes: int16 132 srWindow: SMALL_RECT 133 dwMaximumWindowSize: COORD 134 135 CONSOLE_CURSOR_INFO = object 136 dwSize: DWORD 137 bVisible: WINBOOL 138 139 proc duplicateHandle(hSourceProcessHandle: Handle, hSourceHandle: Handle, 140 hTargetProcessHandle: Handle, lpTargetHandle: ptr Handle, 141 dwDesiredAccess: DWORD, bInheritHandle: WINBOOL, 142 dwOptions: DWORD): WINBOOL{.stdcall, dynlib: "kernel32", 143 importc: "DuplicateHandle".} 144 proc getCurrentProcess(): Handle{.stdcall, dynlib: "kernel32", 145 importc: "GetCurrentProcess".} 146 proc getConsoleScreenBufferInfo(hConsoleOutput: Handle, 147 lpConsoleScreenBufferInfo: ptr CONSOLE_SCREEN_BUFFER_INFO): WINBOOL{.stdcall, 148 dynlib: "kernel32", importc: "GetConsoleScreenBufferInfo".} 149 150 proc getConsoleCursorInfo(hConsoleOutput: Handle, 151 lpConsoleCursorInfo: ptr CONSOLE_CURSOR_INFO): WINBOOL{. 152 stdcall, dynlib: "kernel32", importc: "GetConsoleCursorInfo".} 153 154 proc setConsoleCursorInfo(hConsoleOutput: Handle, 155 lpConsoleCursorInfo: ptr CONSOLE_CURSOR_INFO): WINBOOL{. 156 stdcall, dynlib: "kernel32", importc: "SetConsoleCursorInfo".} 157 158 proc terminalWidthIoctl*(handles: openArray[Handle]): int = 159 var csbi: CONSOLE_SCREEN_BUFFER_INFO 160 for h in handles: 161 if getConsoleScreenBufferInfo(h, addr csbi) != 0: 162 return int(csbi.srWindow.right - csbi.srWindow.left + 1) 163 return 0 164 165 proc terminalHeightIoctl*(handles: openArray[Handle]): int = 166 var csbi: CONSOLE_SCREEN_BUFFER_INFO 167 for h in handles: 168 if getConsoleScreenBufferInfo(h, addr csbi) != 0: 169 return int(csbi.srWindow.bottom - csbi.srWindow.top + 1) 170 return 0 171 172 proc terminalWidth*(): int = 173 var w: int = 0 174 w = terminalWidthIoctl([getStdHandle(STD_INPUT_HANDLE), 175 getStdHandle(STD_OUTPUT_HANDLE), 176 getStdHandle(STD_ERROR_HANDLE)]) 177 if w > 0: return w 178 return 80 179 180 proc terminalHeight*(): int = 181 var h: int = 0 182 h = terminalHeightIoctl([getStdHandle(STD_INPUT_HANDLE), 183 getStdHandle(STD_OUTPUT_HANDLE), 184 getStdHandle(STD_ERROR_HANDLE)]) 185 if h > 0: return h 186 return 0 187 188 proc setConsoleCursorPosition(hConsoleOutput: Handle, 189 dwCursorPosition: COORD): WINBOOL{. 190 stdcall, dynlib: "kernel32", importc: "SetConsoleCursorPosition".} 191 192 proc fillConsoleOutputCharacter(hConsoleOutput: Handle, cCharacter: char, 193 nLength: DWORD, dwWriteCoord: COORD, 194 lpNumberOfCharsWritten: ptr DWORD): WINBOOL{. 195 stdcall, dynlib: "kernel32", importc: "FillConsoleOutputCharacterA".} 196 197 proc fillConsoleOutputAttribute(hConsoleOutput: Handle, wAttribute: int16, 198 nLength: DWORD, dwWriteCoord: COORD, 199 lpNumberOfAttrsWritten: ptr DWORD): WINBOOL{. 200 stdcall, dynlib: "kernel32", importc: "FillConsoleOutputAttribute".} 201 202 proc setConsoleTextAttribute(hConsoleOutput: Handle, 203 wAttributes: int16): WINBOOL{. 204 stdcall, dynlib: "kernel32", importc: "SetConsoleTextAttribute".} 205 206 proc getConsoleMode(hConsoleHandle: Handle, dwMode: ptr DWORD): WINBOOL{. 207 stdcall, dynlib: "kernel32", importc: "GetConsoleMode".} 208 209 proc setConsoleMode(hConsoleHandle: Handle, dwMode: DWORD): WINBOOL{. 210 stdcall, dynlib: "kernel32", importc: "SetConsoleMode".} 211 212 proc getCursorPos(h: Handle): tuple [x, y: int] = 213 var c: CONSOLE_SCREEN_BUFFER_INFO 214 if getConsoleScreenBufferInfo(h, addr(c)) == 0: 215 raiseOSError(osLastError()) 216 return (int(c.dwCursorPosition.x), int(c.dwCursorPosition.y)) 217 218 proc setCursorPos(h: Handle, x, y: int) = 219 var c: COORD 220 c.x = int16(x) 221 c.y = int16(y) 222 if setConsoleCursorPosition(h, c) == 0: 223 raiseOSError(osLastError()) 224 225 proc getAttributes(h: Handle): int16 = 226 var c: CONSOLE_SCREEN_BUFFER_INFO 227 # workaround Windows bugs: try several times 228 if getConsoleScreenBufferInfo(h, addr(c)) != 0: 229 return c.wAttributes 230 return 0x70'i16 # ERROR: return white background, black text 231 232 proc initTerminal(term: PTerminal) = 233 var hStdoutTemp = getStdHandle(STD_OUTPUT_HANDLE) 234 if duplicateHandle(getCurrentProcess(), hStdoutTemp, getCurrentProcess(), 235 addr(term.hStdout), 0, 1, DUPLICATE_SAME_ACCESS) == 0: 236 when defined(consoleapp): 237 raiseOSError(osLastError()) 238 var hStderrTemp = getStdHandle(STD_ERROR_HANDLE) 239 if duplicateHandle(getCurrentProcess(), hStderrTemp, getCurrentProcess(), 240 addr(term.hStderr), 0, 1, DUPLICATE_SAME_ACCESS) == 0: 241 when defined(consoleapp): 242 raiseOSError(osLastError()) 243 term.oldStdoutAttr = getAttributes(term.hStdout) 244 term.oldStderrAttr = getAttributes(term.hStderr) 245 246 template conHandle(f: File): Handle = 247 let term = getTerminal() 248 if f == stderr: term.hStderr else: term.hStdout 249 250else: 251 import termios, posix, os, parseutils 252 253 proc setRaw(fd: FileHandle, time: cint = TCSAFLUSH) = 254 var mode: Termios 255 discard fd.tcGetAttr(addr mode) 256 mode.c_iflag = mode.c_iflag and not Cflag(BRKINT or ICRNL or INPCK or 257 ISTRIP or IXON) 258 mode.c_oflag = mode.c_oflag and not Cflag(OPOST) 259 mode.c_cflag = (mode.c_cflag and not Cflag(CSIZE or PARENB)) or CS8 260 mode.c_lflag = mode.c_lflag and not Cflag(ECHO or ICANON or IEXTEN or ISIG) 261 mode.c_cc[VMIN] = 1.cuchar 262 mode.c_cc[VTIME] = 0.cuchar 263 discard fd.tcSetAttr(time, addr mode) 264 265 proc terminalWidthIoctl*(fds: openArray[int]): int = 266 ## Returns terminal width from first fd that supports the ioctl. 267 268 var win: IOctl_WinSize 269 for fd in fds: 270 if ioctl(cint(fd), TIOCGWINSZ, addr win) != -1: 271 return int(win.ws_col) 272 return 0 273 274 proc terminalHeightIoctl*(fds: openArray[int]): int = 275 ## Returns terminal height from first fd that supports the ioctl. 276 277 var win: IOctl_WinSize 278 for fd in fds: 279 if ioctl(cint(fd), TIOCGWINSZ, addr win) != -1: 280 return int(win.ws_row) 281 return 0 282 283 var L_ctermid{.importc, header: "<stdio.h>".}: cint 284 285 proc terminalWidth*(): int = 286 ## Returns some reasonable terminal width from either standard file 287 ## descriptors, controlling terminal, environment variables or tradition. 288 289 var w = terminalWidthIoctl([0, 1, 2]) #Try standard file descriptors 290 if w > 0: return w 291 var cterm = newString(L_ctermid) #Try controlling tty 292 var fd = open(ctermid(cstring(cterm)), O_RDONLY) 293 if fd != -1: 294 w = terminalWidthIoctl([int(fd)]) 295 discard close(fd) 296 if w > 0: return w 297 var s = getEnv("COLUMNS") #Try standard env var 298 if len(s) > 0 and parseInt(s, w) > 0 and w > 0: 299 return w 300 return 80 #Finally default to venerable value 301 302 proc terminalHeight*(): int = 303 ## Returns some reasonable terminal height from either standard file 304 ## descriptors, controlling terminal, environment variables or tradition. 305 ## Zero is returned if the height could not be determined. 306 307 var h = terminalHeightIoctl([0, 1, 2]) # Try standard file descriptors 308 if h > 0: return h 309 var cterm = newString(L_ctermid) # Try controlling tty 310 var fd = open(ctermid(cstring(cterm)), O_RDONLY) 311 if fd != -1: 312 h = terminalHeightIoctl([int(fd)]) 313 discard close(fd) 314 if h > 0: return h 315 var s = getEnv("LINES") # Try standard env var 316 if len(s) > 0 and parseInt(s, h) > 0 and h > 0: 317 return h 318 return 0 # Could not determine height 319 320proc terminalSize*(): tuple[w, h: int] = 321 ## Returns the terminal width and height as a tuple. Internally calls 322 ## `terminalWidth` and `terminalHeight`, so the same assumptions apply. 323 result = (terminalWidth(), terminalHeight()) 324 325when defined(windows): 326 proc setCursorVisibility(f: File, visible: bool) = 327 var ccsi: CONSOLE_CURSOR_INFO 328 let h = conHandle(f) 329 if getConsoleCursorInfo(h, addr(ccsi)) == 0: 330 raiseOSError(osLastError()) 331 ccsi.bVisible = if visible: 1 else: 0 332 if setConsoleCursorInfo(h, addr(ccsi)) == 0: 333 raiseOSError(osLastError()) 334 335proc hideCursor*(f: File) = 336 ## Hides the cursor. 337 when defined(windows): 338 setCursorVisibility(f, false) 339 else: 340 f.write("\e[?25l") 341 342proc showCursor*(f: File) = 343 ## Shows the cursor. 344 when defined(windows): 345 setCursorVisibility(f, true) 346 else: 347 f.write("\e[?25h") 348 349proc setCursorPos*(f: File, x, y: int) = 350 ## Sets the terminal's cursor to the (x,y) position. 351 ## (0,0) is the upper left of the screen. 352 when defined(windows): 353 let h = conHandle(f) 354 setCursorPos(h, x, y) 355 else: 356 f.write(fmt"{stylePrefix}{y+1};{x+1}f") 357 358proc setCursorXPos*(f: File, x: int) = 359 ## Sets the terminal's cursor to the x position. 360 ## The y position is not changed. 361 when defined(windows): 362 let h = conHandle(f) 363 var scrbuf: CONSOLE_SCREEN_BUFFER_INFO 364 if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0: 365 raiseOSError(osLastError()) 366 var origin = scrbuf.dwCursorPosition 367 origin.x = int16(x) 368 if setConsoleCursorPosition(h, origin) == 0: 369 raiseOSError(osLastError()) 370 else: 371 f.write(fmt"{stylePrefix}{x+1}G") 372 373when defined(windows): 374 proc setCursorYPos*(f: File, y: int) = 375 ## Sets the terminal's cursor to the y position. 376 ## The x position is not changed. 377 ## .. warning:: This is not supported on UNIX! 378 when defined(windows): 379 let h = conHandle(f) 380 var scrbuf: CONSOLE_SCREEN_BUFFER_INFO 381 if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0: 382 raiseOSError(osLastError()) 383 var origin = scrbuf.dwCursorPosition 384 origin.y = int16(y) 385 if setConsoleCursorPosition(h, origin) == 0: 386 raiseOSError(osLastError()) 387 else: 388 discard 389 390proc cursorUp*(f: File, count = 1) = 391 ## Moves the cursor up by `count` rows. 392 when defined(windows): 393 let h = conHandle(f) 394 var p = getCursorPos(h) 395 dec(p.y, count) 396 setCursorPos(h, p.x, p.y) 397 else: 398 f.write("\e[" & $count & 'A') 399 400proc cursorDown*(f: File, count = 1) = 401 ## Moves the cursor down by `count` rows. 402 when defined(windows): 403 let h = conHandle(f) 404 var p = getCursorPos(h) 405 inc(p.y, count) 406 setCursorPos(h, p.x, p.y) 407 else: 408 f.write(fmt"{stylePrefix}{count}B") 409 410proc cursorForward*(f: File, count = 1) = 411 ## Moves the cursor forward by `count` columns. 412 when defined(windows): 413 let h = conHandle(f) 414 var p = getCursorPos(h) 415 inc(p.x, count) 416 setCursorPos(h, p.x, p.y) 417 else: 418 f.write(fmt"{stylePrefix}{count}C") 419 420proc cursorBackward*(f: File, count = 1) = 421 ## Moves the cursor backward by `count` columns. 422 when defined(windows): 423 let h = conHandle(f) 424 var p = getCursorPos(h) 425 dec(p.x, count) 426 setCursorPos(h, p.x, p.y) 427 else: 428 f.write(fmt"{stylePrefix}{count}D") 429 430when true: 431 discard 432else: 433 proc eraseLineEnd*(f: File) = 434 ## Erases from the current cursor position to the end of the current line. 435 when defined(windows): 436 discard 437 else: 438 f.write("\e[K") 439 440 proc eraseLineStart*(f: File) = 441 ## Erases from the current cursor position to the start of the current line. 442 when defined(windows): 443 discard 444 else: 445 f.write("\e[1K") 446 447 proc eraseDown*(f: File) = 448 ## Erases the screen from the current line down to the bottom of the screen. 449 when defined(windows): 450 discard 451 else: 452 f.write("\e[J") 453 454 proc eraseUp*(f: File) = 455 ## Erases the screen from the current line up to the top of the screen. 456 when defined(windows): 457 discard 458 else: 459 f.write("\e[1J") 460 461proc eraseLine*(f: File) = 462 ## Erases the entire current line. 463 runnableExamples("-r:off"): 464 write(stdout, "never mind") 465 stdout.eraseLine() # nothing will be printed on the screen 466 when defined(windows): 467 let h = conHandle(f) 468 var scrbuf: CONSOLE_SCREEN_BUFFER_INFO 469 var numwrote: DWORD 470 if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0: 471 raiseOSError(osLastError()) 472 var origin = scrbuf.dwCursorPosition 473 origin.x = 0'i16 474 if setConsoleCursorPosition(h, origin) == 0: 475 raiseOSError(osLastError()) 476 var wt: DWORD = scrbuf.dwSize.x - origin.x 477 if fillConsoleOutputCharacter(h, ' ', wt, 478 origin, addr(numwrote)) == 0: 479 raiseOSError(osLastError()) 480 if fillConsoleOutputAttribute(h, scrbuf.wAttributes, wt, 481 scrbuf.dwCursorPosition, addr(numwrote)) == 0: 482 raiseOSError(osLastError()) 483 else: 484 f.write("\e[2K") 485 setCursorXPos(f, 0) 486 487proc eraseScreen*(f: File) = 488 ## Erases the screen with the background colour and moves the cursor to home. 489 when defined(windows): 490 let h = conHandle(f) 491 var scrbuf: CONSOLE_SCREEN_BUFFER_INFO 492 var numwrote: DWORD 493 var origin: COORD # is inititalized to 0, 0 494 495 if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0: 496 raiseOSError(osLastError()) 497 let numChars = int32(scrbuf.dwSize.x)*int32(scrbuf.dwSize.y) 498 499 if fillConsoleOutputCharacter(h, ' ', numChars, 500 origin, addr(numwrote)) == 0: 501 raiseOSError(osLastError()) 502 if fillConsoleOutputAttribute(h, scrbuf.wAttributes, numChars, 503 origin, addr(numwrote)) == 0: 504 raiseOSError(osLastError()) 505 setCursorXPos(f, 0) 506 else: 507 f.write("\e[2J") 508 509when not defined(windows): 510 var 511 gFG {.threadvar.}: int 512 gBG {.threadvar.}: int 513 514proc resetAttributes*(f: File) = 515 ## Resets all attributes. 516 when defined(windows): 517 let term = getTerminal() 518 if f == stderr: 519 discard setConsoleTextAttribute(term.hStderr, term.oldStderrAttr) 520 else: 521 discard setConsoleTextAttribute(term.hStdout, term.oldStdoutAttr) 522 else: 523 f.write(ansiResetCode) 524 gFG = 0 525 gBG = 0 526 527type 528 Style* = enum ## Different styles for text output. 529 styleBright = 1, ## bright text 530 styleDim, ## dim text 531 styleItalic, ## italic (or reverse on terminals not supporting) 532 styleUnderscore, ## underscored text 533 styleBlink, ## blinking/bold text 534 styleBlinkRapid, ## rapid blinking/bold text (not widely supported) 535 styleReverse, ## reverse 536 styleHidden, ## hidden text 537 styleStrikethrough ## strikethrough 538 539proc ansiStyleCode*(style: int): string = 540 result = fmt"{stylePrefix}{style}m" 541 542template ansiStyleCode*(style: Style): string = 543 ansiStyleCode(style.int) 544 545# The styleCache can be skipped when `style` is known at compile-time 546template ansiStyleCode*(style: static[Style]): string = 547 (static(stylePrefix & $style.int & "m")) 548 549proc setStyle*(f: File, style: set[Style]) = 550 ## Sets the terminal style. 551 when defined(windows): 552 let h = conHandle(f) 553 var old = getAttributes(h) and (FOREGROUND_RGB or BACKGROUND_RGB) 554 var a = 0'i16 555 if styleBright in style: a = a or int16(FOREGROUND_INTENSITY) 556 if styleBlink in style: a = a or int16(BACKGROUND_INTENSITY) 557 if styleReverse in style: a = a or 0x4000'i16 # COMMON_LVB_REVERSE_VIDEO 558 if styleUnderscore in style: a = a or 0x8000'i16 # COMMON_LVB_UNDERSCORE 559 discard setConsoleTextAttribute(h, old or a) 560 else: 561 for s in items(style): 562 f.write(ansiStyleCode(s)) 563 564proc writeStyled*(txt: string, style: set[Style] = {styleBright}) = 565 ## Writes the text `txt` in a given `style` to stdout. 566 when defined(windows): 567 let term = getTerminal() 568 var old = getAttributes(term.hStdout) 569 stdout.setStyle(style) 570 stdout.write(txt) 571 discard setConsoleTextAttribute(term.hStdout, old) 572 else: 573 stdout.setStyle(style) 574 stdout.write(txt) 575 stdout.resetAttributes() 576 if gFG != 0: 577 stdout.write(ansiStyleCode(gFG)) 578 if gBG != 0: 579 stdout.write(ansiStyleCode(gBG)) 580 581type 582 ForegroundColor* = enum ## Terminal's foreground colors. 583 fgBlack = 30, ## black 584 fgRed, ## red 585 fgGreen, ## green 586 fgYellow, ## yellow 587 fgBlue, ## blue 588 fgMagenta, ## magenta 589 fgCyan, ## cyan 590 fgWhite, ## white 591 fg8Bit, ## 256-color (not supported, see `enableTrueColors` instead.) 592 fgDefault ## default terminal foreground color 593 594 BackgroundColor* = enum ## Terminal's background colors. 595 bgBlack = 40, ## black 596 bgRed, ## red 597 bgGreen, ## green 598 bgYellow, ## yellow 599 bgBlue, ## blue 600 bgMagenta, ## magenta 601 bgCyan, ## cyan 602 bgWhite, ## white 603 bg8Bit, ## 256-color (not supported, see `enableTrueColors` instead.) 604 bgDefault ## default terminal background color 605 606when defined(windows): 607 var defaultForegroundColor, defaultBackgroundColor: int16 = 0xFFFF'i16 # Default to an invalid value 0xFFFF 608 609proc setForegroundColor*(f: File, fg: ForegroundColor, bright = false) = 610 ## Sets the terminal's foreground color. 611 when defined(windows): 612 let h = conHandle(f) 613 var old = getAttributes(h) and not FOREGROUND_RGB 614 if defaultForegroundColor == 0xFFFF'i16: 615 defaultForegroundColor = old 616 old = if bright: old or FOREGROUND_INTENSITY 617 else: old and not(FOREGROUND_INTENSITY) 618 const lookup: array[ForegroundColor, int] = [ 619 0, # ForegroundColor enum with ordinal 30 620 (FOREGROUND_RED), 621 (FOREGROUND_GREEN), 622 (FOREGROUND_RED or FOREGROUND_GREEN), 623 (FOREGROUND_BLUE), 624 (FOREGROUND_RED or FOREGROUND_BLUE), 625 (FOREGROUND_BLUE or FOREGROUND_GREEN), 626 (FOREGROUND_BLUE or FOREGROUND_GREEN or FOREGROUND_RED), 627 0, # fg8Bit not supported, see `enableTrueColors` instead. 628 0] # unused 629 if fg == fgDefault: 630 discard setConsoleTextAttribute(h, toU16(old or defaultForegroundColor)) 631 else: 632 discard setConsoleTextAttribute(h, toU16(old or lookup[fg])) 633 else: 634 gFG = ord(fg) 635 if bright: inc(gFG, 60) 636 f.write(ansiStyleCode(gFG)) 637 638proc setBackgroundColor*(f: File, bg: BackgroundColor, bright = false) = 639 ## Sets the terminal's background color. 640 when defined(windows): 641 let h = conHandle(f) 642 var old = getAttributes(h) and not BACKGROUND_RGB 643 if defaultBackgroundColor == 0xFFFF'i16: 644 defaultBackgroundColor = old 645 old = if bright: old or BACKGROUND_INTENSITY 646 else: old and not(BACKGROUND_INTENSITY) 647 const lookup: array[BackgroundColor, int] = [ 648 0, # BackgroundColor enum with ordinal 40 649 (BACKGROUND_RED), 650 (BACKGROUND_GREEN), 651 (BACKGROUND_RED or BACKGROUND_GREEN), 652 (BACKGROUND_BLUE), 653 (BACKGROUND_RED or BACKGROUND_BLUE), 654 (BACKGROUND_BLUE or BACKGROUND_GREEN), 655 (BACKGROUND_BLUE or BACKGROUND_GREEN or BACKGROUND_RED), 656 0, # bg8Bit not supported, see `enableTrueColors` instead. 657 0] # unused 658 if bg == bgDefault: 659 discard setConsoleTextAttribute(h, toU16(old or defaultBackgroundColor)) 660 else: 661 discard setConsoleTextAttribute(h, toU16(old or lookup[bg])) 662 else: 663 gBG = ord(bg) 664 if bright: inc(gBG, 60) 665 f.write(ansiStyleCode(gBG)) 666 667proc ansiForegroundColorCode*(fg: ForegroundColor, bright = false): string = 668 var style = ord(fg) 669 if bright: inc(style, 60) 670 return ansiStyleCode(style) 671 672template ansiForegroundColorCode*(fg: static[ForegroundColor], 673 bright: static[bool] = false): string = 674 ansiStyleCode(fg.int + bright.int * 60) 675 676proc ansiForegroundColorCode*(color: Color): string = 677 let rgb = extractRGB(color) 678 result = fmt"{fgPrefix}{rgb.r};{rgb.g};{rgb.b}m" 679 680template ansiForegroundColorCode*(color: static[Color]): string = 681 const rgb = extractRGB(color) 682 # no usage of `fmt`, see issue #7632 683 (static("$1$2;$3;$4m" % [$fgPrefix, $(rgb.r), $(rgb.g), $(rgb.b)])) 684 685proc ansiBackgroundColorCode*(color: Color): string = 686 let rgb = extractRGB(color) 687 result = fmt"{bgPrefix}{rgb.r};{rgb.g};{rgb.b}m" 688 689template ansiBackgroundColorCode*(color: static[Color]): string = 690 const rgb = extractRGB(color) 691 # no usage of `fmt`, see issue #7632 692 (static("$1$2;$3;$4m" % [$bgPrefix, $(rgb.r), $(rgb.g), $(rgb.b)])) 693 694proc setForegroundColor*(f: File, color: Color) = 695 ## Sets the terminal's foreground true color. 696 if getTerminal().trueColorIsEnabled: 697 f.write(ansiForegroundColorCode(color)) 698 699proc setBackgroundColor*(f: File, color: Color) = 700 ## Sets the terminal's background true color. 701 if getTerminal().trueColorIsEnabled: 702 f.write(ansiBackgroundColorCode(color)) 703 704proc setTrueColor(f: File, color: Color) = 705 let term = getTerminal() 706 if term.fgSetColor: 707 setForegroundColor(f, color) 708 else: 709 setBackgroundColor(f, color) 710 711proc isatty*(f: File): bool = 712 ## Returns true if `f` is associated with a terminal device. 713 when defined(posix): 714 proc isatty(fildes: FileHandle): cint {. 715 importc: "isatty", header: "<unistd.h>".} 716 else: 717 proc isatty(fildes: FileHandle): cint {. 718 importc: "_isatty", header: "<io.h>".} 719 720 result = isatty(getFileHandle(f)) != 0'i32 721 722type 723 TerminalCmd* = enum ## commands that can be expressed as arguments 724 resetStyle, ## reset attributes 725 fgColor, ## set foreground's true color 726 bgColor ## set background's true color 727 728template styledEchoProcessArg(f: File, s: string) = write f, s 729template styledEchoProcessArg(f: File, style: Style) = setStyle(f, {style}) 730template styledEchoProcessArg(f: File, style: set[Style]) = setStyle f, style 731template styledEchoProcessArg(f: File, color: ForegroundColor) = 732 setForegroundColor f, color 733template styledEchoProcessArg(f: File, color: BackgroundColor) = 734 setBackgroundColor f, color 735template styledEchoProcessArg(f: File, color: Color) = 736 setTrueColor f, color 737template styledEchoProcessArg(f: File, cmd: TerminalCmd) = 738 when cmd == resetStyle: 739 resetAttributes(f) 740 elif cmd in {fgColor, bgColor}: 741 let term = getTerminal() 742 term.fgSetColor = cmd == fgColor 743 744macro styledWrite*(f: File, m: varargs[typed]): untyped = 745 ## Similar to `write`, but treating terminal style arguments specially. 746 ## When some argument is `Style`, `set[Style]`, `ForegroundColor`, 747 ## `BackgroundColor` or `TerminalCmd` then it is not sent directly to 748 ## `f`, but instead corresponding terminal style proc is called. 749 runnableExamples("-r:off"): 750 stdout.styledWrite(fgRed, "red text ") 751 stdout.styledWrite(fgGreen, "green text") 752 753 var reset = false 754 result = newNimNode(nnkStmtList) 755 756 for i in countup(0, m.len - 1): 757 let item = m[i] 758 case item.kind 759 of nnkStrLit..nnkTripleStrLit: 760 if i == m.len - 1: 761 # optimize if string literal is last, just call write 762 result.add(newCall(bindSym"write", f, item)) 763 if reset: result.add(newCall(bindSym"resetAttributes", f)) 764 return 765 else: 766 # if it is string literal just call write, do not enable reset 767 result.add(newCall(bindSym"write", f, item)) 768 else: 769 result.add(newCall(bindSym"styledEchoProcessArg", f, item)) 770 reset = true 771 if reset: result.add(newCall(bindSym"resetAttributes", f)) 772 773template styledWriteLine*(f: File, args: varargs[untyped]) = 774 ## Calls `styledWrite` and appends a newline at the end. 775 runnableExamples: 776 proc error(msg: string) = 777 styledWriteLine(stderr, fgRed, "Error: ", resetStyle, msg) 778 779 styledWrite(f, args) 780 write(f, "\n") 781 782template styledEcho*(args: varargs[untyped]) = 783 ## Echoes styles arguments to stdout using `styledWriteLine`. 784 stdout.styledWriteLine(args) 785 786proc getch*(): char = 787 ## Reads a single character from the terminal, blocking until it is entered. 788 ## The character is not printed to the terminal. 789 when defined(windows): 790 let fd = getStdHandle(STD_INPUT_HANDLE) 791 var keyEvent = KEY_EVENT_RECORD() 792 var numRead: cint 793 while true: 794 # Block until character is entered 795 doAssert(waitForSingleObject(fd, INFINITE) == WAIT_OBJECT_0) 796 doAssert(readConsoleInput(fd, addr(keyEvent), 1, addr(numRead)) != 0) 797 if numRead == 0 or keyEvent.eventType != 1 or keyEvent.bKeyDown == 0: 798 continue 799 return char(keyEvent.uChar) 800 else: 801 let fd = getFileHandle(stdin) 802 var oldMode: Termios 803 discard fd.tcGetAttr(addr oldMode) 804 fd.setRaw() 805 result = stdin.readChar() 806 discard fd.tcSetAttr(TCSADRAIN, addr oldMode) 807 808when defined(windows): 809 proc readPasswordFromStdin*(prompt: string, password: var string): 810 bool {.tags: [ReadIOEffect, WriteIOEffect].} = 811 ## Reads a `password` from stdin without printing it. `password` must not 812 ## be `nil`! Returns `false` if the end of the file has been reached, 813 ## `true` otherwise. 814 password.setLen(0) 815 stdout.write(prompt) 816 let hi = createFileA("CONIN$", 817 GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0) 818 var mode = DWORD 0 819 discard getConsoleMode(hi, addr mode) 820 let origMode = mode 821 const 822 ENABLE_PROCESSED_INPUT = 1 823 ENABLE_ECHO_INPUT = 4 824 mode = (mode or ENABLE_PROCESSED_INPUT) and not ENABLE_ECHO_INPUT 825 826 discard setConsoleMode(hi, mode) 827 result = readLine(stdin, password) 828 discard setConsoleMode(hi, origMode) 829 discard closeHandle(hi) 830 stdout.write "\n" 831 832else: 833 import termios 834 835 proc readPasswordFromStdin*(prompt: string, password: var string): 836 bool {.tags: [ReadIOEffect, WriteIOEffect].} = 837 password.setLen(0) 838 let fd = stdin.getFileHandle() 839 var cur, old: Termios 840 discard fd.tcGetAttr(cur.addr) 841 old = cur 842 cur.c_lflag = cur.c_lflag and not Cflag(ECHO) 843 discard fd.tcSetAttr(TCSADRAIN, cur.addr) 844 stdout.write prompt 845 result = stdin.readLine(password) 846 stdout.write "\n" 847 discard fd.tcSetAttr(TCSADRAIN, old.addr) 848 849proc readPasswordFromStdin*(prompt = "password: "): string = 850 ## Reads a password from stdin without printing it. 851 result = "" 852 discard readPasswordFromStdin(prompt, result) 853 854 855# Wrappers assuming output to stdout: 856template hideCursor*() = hideCursor(stdout) 857template showCursor*() = showCursor(stdout) 858template setCursorPos*(x, y: int) = setCursorPos(stdout, x, y) 859template setCursorXPos*(x: int) = setCursorXPos(stdout, x) 860when defined(windows): 861 template setCursorYPos*(x: int) = setCursorYPos(stdout, x) 862template cursorUp*(count = 1) = cursorUp(stdout, count) 863template cursorDown*(count = 1) = cursorDown(stdout, count) 864template cursorForward*(count = 1) = cursorForward(stdout, count) 865template cursorBackward*(count = 1) = cursorBackward(stdout, count) 866template eraseLine*() = eraseLine(stdout) 867template eraseScreen*() = eraseScreen(stdout) 868template setStyle*(style: set[Style]) = 869 setStyle(stdout, style) 870template setForegroundColor*(fg: ForegroundColor, bright = false) = 871 setForegroundColor(stdout, fg, bright) 872template setBackgroundColor*(bg: BackgroundColor, bright = false) = 873 setBackgroundColor(stdout, bg, bright) 874template setForegroundColor*(color: Color) = 875 setForegroundColor(stdout, color) 876template setBackgroundColor*(color: Color) = 877 setBackgroundColor(stdout, color) 878proc resetAttributes*() {.noconv.} = 879 ## Resets all attributes on stdout. 880 ## It is advisable to register this as a quit proc with 881 ## `exitprocs.addExitProc(resetAttributes)`. 882 resetAttributes(stdout) 883 884proc isTrueColorSupported*(): bool = 885 ## Returns true if a terminal supports true color. 886 return getTerminal().trueColorIsSupported 887 888when defined(windows): 889 import os 890 891proc enableTrueColors*() = 892 ## Enables true color. 893 var term = getTerminal() 894 when defined(windows): 895 var 896 ver: OSVERSIONINFO 897 ver.dwOSVersionInfoSize = sizeof(ver).DWORD 898 let res = getVersionExW(addr ver) 899 if res == 0: 900 term.trueColorIsSupported = false 901 else: 902 term.trueColorIsSupported = ver.dwMajorVersion > 10 or 903 (ver.dwMajorVersion == 10 and (ver.dwMinorVersion > 0 or 904 (ver.dwMinorVersion == 0 and ver.dwBuildNumber >= 10586))) 905 if not term.trueColorIsSupported: 906 term.trueColorIsSupported = getEnv("ANSICON_DEF").len > 0 907 908 if term.trueColorIsSupported: 909 if getEnv("ANSICON_DEF").len == 0: 910 var mode: DWORD = 0 911 if getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), addr(mode)) != 0: 912 mode = mode or ENABLE_VIRTUAL_TERMINAL_PROCESSING 913 if setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), mode) != 0: 914 term.trueColorIsEnabled = true 915 else: 916 term.trueColorIsEnabled = false 917 else: 918 term.trueColorIsEnabled = true 919 else: 920 term.trueColorIsSupported = getEnv("COLORTERM").toLowerAscii() in [ 921 "truecolor", "24bit"] 922 term.trueColorIsEnabled = term.trueColorIsSupported 923 924proc disableTrueColors*() = 925 ## Disables true color. 926 var term = getTerminal() 927 when defined(windows): 928 if term.trueColorIsSupported: 929 if getEnv("ANSICON_DEF").len == 0: 930 var mode: DWORD = 0 931 if getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), addr(mode)) != 0: 932 mode = mode and not ENABLE_VIRTUAL_TERMINAL_PROCESSING 933 discard setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), mode) 934 term.trueColorIsEnabled = false 935 else: 936 term.trueColorIsEnabled = false 937 938proc newTerminal(): owned(PTerminal) = 939 new result 940 when defined(windows): 941 initTerminal(result) 942