summary refs log tree commit diff stats
path: root/lib/pure/terminal.nim
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pure/terminal.nim')
-rw-r--r--lib/pure/terminal.nim272
1 files changed, 245 insertions, 27 deletions
diff --git a/lib/pure/terminal.nim b/lib/pure/terminal.nim
index f15cee66a..fcca4d5d7 100644
--- a/lib/pure/terminal.nim
+++ b/lib/pure/terminal.nim
@@ -17,6 +17,30 @@
 ## ``showCursor`` before quitting.
 
 import macros
+import strformat
+from strutils import toLowerAscii
+import colors
+
+const
+  hasThreadSupport = compileOption("threads")
+
+when not hasThreadSupport:
+  import tables
+  var
+    colorsFGCache = initTable[Color, string]()
+    colorsBGCache = initTable[Color, string]()
+    styleCache = initTable[int, string]()
+
+var
+  trueColorIsSupported: bool
+  trueColorIsEnabled: bool
+  fgSetColor: bool
+
+const
+  fgPrefix = "\x1b[38;2;"
+  bgPrefix = "\x1b[48;2;"
+  ansiResetCode* = "\e[0m"
+  stylePrefix = "\e["
 
 when defined(windows):
   import winlean, os
@@ -34,6 +58,8 @@ when defined(windows):
     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
@@ -124,6 +150,12 @@ when defined(windows):
                                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".}
+
   var
     hStdout: Handle # = createFile("CONOUT$", GENERIC_WRITE, 0, nil,
                     #              OPEN_ALWAYS, 0, 0)
@@ -274,7 +306,7 @@ proc setCursorPos*(f: File, x, y: int) =
     let h = conHandle(f)
     setCursorPos(h, x, y)
   else:
-    f.write("\e[" & $y & ';' & $x & 'f')
+    f.write(fmt"{stylePrefix}{y};{x}f")
 
 proc setCursorXPos*(f: File, x: int) =
   ## Sets the terminal's cursor to the x position.
@@ -289,7 +321,7 @@ proc setCursorXPos*(f: File, x: int) =
     if setConsoleCursorPosition(h, origin) == 0:
       raiseOSError(osLastError())
   else:
-    f.write("\e[" & $x & 'G')
+    f.write(fmt"{stylePrefix}{x}G")
 
 when defined(windows):
   proc setCursorYPos*(f: File, y: int) =
@@ -326,7 +358,7 @@ proc cursorDown*(f: File, count=1) =
     inc(p.y, count)
     setCursorPos(h, p.x, p.y)
   else:
-    f.write("\e[" & $count & 'B')
+    f.write(fmt"{stylePrefix}{count}B")
 
 proc cursorForward*(f: File, count=1) =
   ## Moves the cursor forward by `count` columns.
@@ -336,7 +368,7 @@ proc cursorForward*(f: File, count=1) =
     inc(p.x, count)
     setCursorPos(h, p.x, p.y)
   else:
-    f.write("\e[" & $count & 'C')
+    f.write(fmt"{stylePrefix}{count}C")
 
 proc cursorBackward*(f: File, count=1) =
   ## Moves the cursor backward by `count` columns.
@@ -346,7 +378,7 @@ proc cursorBackward*(f: File, count=1) =
     dec(p.x, count)
     setCursorPos(h, p.x, p.y)
   else:
-    f.write("\e[" & $count & 'D')
+    f.write(fmt"{stylePrefix}{count}D")
 
 when true:
   discard
@@ -391,12 +423,11 @@ proc eraseLine*(f: File) =
     origin.X = 0'i16
     if setConsoleCursorPosition(h, origin) == 0:
       raiseOSError(osLastError())
-    var ht: DWORD = scrbuf.dwSize.Y - origin.Y
     var wt: DWORD = scrbuf.dwSize.X - origin.X
-    if fillConsoleOutputCharacter(h, ' ', ht*wt,
+    if fillConsoleOutputCharacter(h, ' ', wt,
                                   origin, addr(numwrote)) == 0:
       raiseOSError(osLastError())
-    if fillConsoleOutputAttribute(h, scrbuf.wAttributes, ht * wt,
+    if fillConsoleOutputAttribute(h, scrbuf.wAttributes, wt,
                                   scrbuf.dwCursorPosition, addr(numwrote)) == 0:
       raiseOSError(osLastError())
   else:
@@ -433,7 +464,7 @@ proc resetAttributes*(f: File) =
     else:
       discard setConsoleTextAttribute(hStdout, oldStdoutAttr)
   else:
-    f.write("\e[0m")
+    f.write(ansiResetCode)
 
 type
   Style* = enum         ## different styles for text output
@@ -449,9 +480,25 @@ type
 
 when not defined(windows):
   var
-    # XXX: These better be thread-local
-    gFG = 0
-    gBG = 0
+    gFG {.threadvar.}: int
+    gBG {.threadvar.}: int
+
+proc ansiStyleCode*(style: int): string =
+  when hasThreadSupport:
+    result = fmt"{stylePrefix}{style}m"
+  else:
+    if styleCache.hasKey(style):
+      result = styleCache[style]
+    else:
+      result = fmt"{stylePrefix}{style}m"
+      styleCache[style] = result
+
+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.
@@ -466,7 +513,7 @@ proc setStyle*(f: File, style: set[Style]) =
     discard setConsoleTextAttribute(h, old or a)
   else:
     for s in items(style):
-      f.write("\e[" & $ord(s) & 'm')
+      f.write(ansiStyleCode(s))
 
 proc writeStyled*(txt: string, style: set[Style] = {styleBright}) =
   ## Writes the text `txt` in a given `style` to stdout.
@@ -480,9 +527,9 @@ proc writeStyled*(txt: string, style: set[Style] = {styleBright}) =
     stdout.write(txt)
     stdout.resetAttributes()
     if gFG != 0:
-      stdout.write("\e[" & $ord(gFG) & 'm')
+      stdout.write(ansiStyleCode(gFG))
     if gBG != 0:
-      stdout.write("\e[" & $ord(gBG) & 'm')
+      stdout.write(ansiStyleCode(gBG))
 
 type
   ForegroundColor* = enum  ## terminal's foreground colors
@@ -513,8 +560,8 @@ proc setForegroundColor*(f: File, fg: ForegroundColor, bright=false) =
   when defined(windows):
     let h = conHandle(f)
     var old = getAttributes(h) and not FOREGROUND_RGB
-    if bright:
-      old = old or FOREGROUND_INTENSITY
+    old = if bright: old or FOREGROUND_INTENSITY
+          else:      old and not(FOREGROUND_INTENSITY)
     const lookup: array[ForegroundColor, int] = [
       0,
       (FOREGROUND_RED),
@@ -528,15 +575,15 @@ proc setForegroundColor*(f: File, fg: ForegroundColor, bright=false) =
   else:
     gFG = ord(fg)
     if bright: inc(gFG, 60)
-    f.write("\e[" & $gFG & 'm')
+    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 bright:
-      old = old or BACKGROUND_INTENSITY
+    old = if bright: old or BACKGROUND_INTENSITY
+          else:      old and not(BACKGROUND_INTENSITY)
     const lookup: array[BackgroundColor, int] = [
       0,
       (BACKGROUND_RED),
@@ -550,7 +597,64 @@ proc setBackgroundColor*(f: File, bg: BackgroundColor, bright=false) =
   else:
     gBG = ord(bg)
     if bright: inc(gBG, 60)
-    f.write("\e[" & $gBG & 'm')
+    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 =
+  when hasThreadSupport:
+    let rgb = extractRGB(color)
+    result = fmt"{fgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
+  else:
+    if colorsFGCache.hasKey(color):
+      result = colorsFGCache[color]
+    else:
+      let rgb = extractRGB(color)
+      result = fmt"{fgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
+      colorsFGCache[color] = result
+
+template ansiForegroundColorCode*(color: static[Color]): string =
+  const rgb = extractRGB(color)
+  (static(fmt"{fgPrefix}{rgb.r};{rgb.g};{rgb.b}m"))
+
+proc ansiBackgroundColorCode*(color: Color): string =
+  when hasThreadSupport:
+    let rgb = extractRGB(color)
+    result = fmt"{bgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
+  else:
+    if colorsBGCache.hasKey(color):
+      result = colorsBGCache[color]
+    else:
+      let rgb = extractRGB(color)
+      result = fmt"{bgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
+      colorsFGCache[color] = result
+
+template ansiBackgroundColorCode*(color: static[Color]): string =
+  const rgb = extractRGB(color)
+  (static(fmt"{bgPrefix}{rgb.r};{rgb.g};{rgb.b}m"))
+
+proc setForegroundColor*(f: File, color: Color) =
+  ## Sets the terminal's foreground true color.
+  if trueColorIsEnabled:
+    f.write(ansiForegroundColorCode(color))
+
+proc setBackgroundColor*(f: File, color: Color) =
+  ## Sets the terminal's background true color.
+  if trueColorIsEnabled:
+    f.write(ansiBackgroundColorCode(color))
+
+proc setTrueColor(f: File, color: Color) =
+  if fgSetColor:
+    setForegroundColor(f, color)
+  else:
+    setBackgroundColor(f, color)
 
 proc isatty*(f: File): bool =
   ## Returns true if `f` is associated with a terminal device.
@@ -565,7 +669,9 @@ proc isatty*(f: File): bool =
 
 type
   TerminalCmd* = enum  ## commands that can be expressed as arguments
-    resetStyle         ## reset attributes
+    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})
@@ -574,9 +680,15 @@ 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 styledWriteLine*(f: File, m: varargs[typed]): untyped =
   ## Similar to ``writeLine``, but treating terminal style arguments specially.
@@ -634,10 +746,7 @@ proc getch*(): char =
       doAssert(readConsoleInput(fd, addr(keyEvent), 1, addr(numRead)) != 0)
       if numRead == 0 or keyEvent.eventType != 1 or keyEvent.bKeyDown == 0:
         continue
-      if keyEvent.uChar == 0:
-        return char(keyEvent.wVirtualKeyCode)
-      else:
-        return char(keyEvent.uChar)
+      return char(keyEvent.uChar)
   else:
     let fd = getFileHandle(stdin)
     var oldMode: Termios
@@ -646,13 +755,67 @@ proc getch*(): char =
     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 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)
@@ -665,6 +828,10 @@ 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
@@ -680,3 +847,54 @@ when not defined(testing) and isMainModule:
   stdout.setForeGroundColor(fgBlue)
   stdout.writeLine("ordinary text")
   stdout.resetAttributes()
+
+proc isTrueColorSupported*(): bool =
+  ## Returns true if a terminal supports true color.
+  return trueColorIsSupported
+
+when defined(windows):
+  import os
+
+proc enableTrueColors*() =
+  ## Enable true color.
+  when defined(windows):
+    var
+      ver: OSVERSIONINFO
+    ver.dwOSVersionInfoSize = sizeof(ver).DWORD
+    let res = getVersionExW(addr ver)
+    if res == 0:
+      trueColorIsSupported = false
+    else:
+      trueColorIsSupported = ver.dwMajorVersion > 10 or
+        (ver.dwMajorVersion == 10 and (ver.dwMinorVersion > 0 or
+        (ver.dwMinorVersion == 0 and ver.dwBuildNumber >= 10586)))
+    if not trueColorIsSupported:
+      trueColorIsSupported = getEnv("ANSICON_DEF").len > 0
+
+    if 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:
+            trueColorIsEnabled = true
+          else:
+            trueColorIsEnabled = false
+      else:
+        trueColorIsEnabled = true
+  else:
+    trueColorIsSupported = string(getEnv("COLORTERM")).toLowerAscii() in ["truecolor", "24bit"]
+    trueColorIsEnabled = trueColorIsSupported
+
+proc disableTrueColors*() =
+  ## Disable true color.
+  when defined(windows):
+    if 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)
+      trueColorIsEnabled = false
+  else:
+    trueColorIsEnabled = false