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