#
#
#            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 `exitprocs.addExitProc(resetAttributes)` to restore the defaults.
## Similarly, if you hide the cursor, make sure to unhide it with
## `showCursor` before quitting.
##
## Progress bar
## ============
##
## Basic progress bar example:
runnableExamples("-r:off"):
  import std/[os, strutils]

  for i in 0..100:
    stdout.styledWriteLine(fgRed, "0% ", fgWhite, '#'.repeat i, if i > 50: fgGreen else: fgYellow, "\t", $i , "%")
    sleep 42
    cursorUp 1
    eraseLine()

  stdout.resetAttributes()

##[
## Playing with colorful and styled text
]##

## Procs like `styledWriteLine`, `styledEcho` etc. have a temporary effect on
## text parameters. Style parameters only affect the text parameter right after them.
## After being called, these procs will reset the default style of the terminal.
## While `setBackGroundColor`, `setForeGroundColor` etc. have a lasting
## influence on the terminal, you can use `resetAttributes` to
## reset the default style of the terminal.
runnableExamples("-r:off"):
  stdout.styledWriteLine({styleBright, styleBlink, styleUnderscore}, "styled text ")
  stdout.styledWriteLine(fgRed, "red text ")
  stdout.styledWriteLine(fgWhite, bgRed, "white text in red background")
  stdout.styledWriteLine(" ordinary text without style ")

  stdout.setBackGroundColor(bgCyan, true)
  stdout.setForeGroundColor(fgBlue)
  stdout.write("blue text in cyan background")
  stdout.resetAttributes()

  # You can specify multiple text parameters. Style parameters
  # only affect the text parameter right after them.
  styledEcho styleBright, fgGreen, "[PASS]", resetStyle, fgGreen, " Yay!"

  stdout.styledWriteLine(fgRed, "red text ", styleBright, "bold red", fgDefault, " bold text")

import macros
import strformat
from strutils import toLowerAscii, `%`
import colors

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.}: owned(PTerminal)

when defined(windows) and defined(consoleapp):
  proc newTerminal(): owned(PTerminal) {.gcsafe, raises: [OSError].}
else:
  proc newTerminal(): owned(PTerminal) {.gcsafe, raises: [].}

proc getTerminal(): PTerminal {.inline.} =
  if isNil(gTerm):
    gTerm = newTerminal()
  result = gTerm

const
  fgPrefix = "\e[38;2;"
  bgPrefix = "\e[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: CONSOLE_SCREEN_BUFFER_INFO
    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: CONSOLE_SCREEN_BUFFER_INFO
    # 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.

    # POSIX environment variable takes precendence.
    # _COLUMNS_: This variable shall represent a decimal integer >0 used
    # to indicate the user's preferred width in column positions for
    # the terminal screen or window. If this variable is unset or null,
    # the implementation determines the number of columns, appropriate
    # for the terminal or window, in an unspecified manner.
    # When COLUMNS is set, any terminal-width information implied by TERM
    # is overridden. Users and conforming applications should not set COLUMNS
    # unless they wish to override the system selection and produce output
    # unrelated to the terminal characteristics.
    # See POSIX Base Definitions Section 8.1 Environment Variable Definition

    var w: int
    var s = getEnv("COLUMNS") # Try standard env var
    if len(s) > 0 and parseInt(s, w) > 0 and w > 0:
      return w
    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
    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.

    # POSIX environment variable takes precendence.
    # _LINES_: This variable shall represent a decimal integer >0 used
    # to indicate the user's preferred number of lines on a page or
    # the vertical screen or window size in lines. A line in this case
    # is a vertical measure large enough to hold the tallest character
    # in the character set being displayed. If this variable is unset or null,
    # the implementation determines the number of lines, appropriate
    # for the terminal or window (size, terminal baud rate, and so on),
    # in an unspecified manner.
    # When LINES is set, any terminal-height information implied by TERM
    # is overridden. Users and conforming applications should not set LINES
    # unless they wish to override the system selection and produce output
    # unrelated to the terminal characteristics.
    # See POSIX Base Definitions Section 8.1 Environment Variable Definition

    var h: int
    var s = getEnv("LINES") # Try standard env var
    if len(s) > 0 and parseInt(s, h) > 0 and h > 0:
      return h
    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
    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: CONSOLE_SCREEN_BUFFER_INFO
    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: CONSOLE_SCREEN_BUFFER_INFO
      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.
  runnableExamples("-r:off"):
    write(stdout, "never mind")
    stdout.eraseLine() # nothing will be printed on the screen
  when defined(windows):
    let h = conHandle(f)
    var scrbuf: CONSOLE_SCREEN_BUFFER_INFO
    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: CONSOLE_SCREEN_BUFFER_INFO
    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")

when not defined(windows):
  var
    gFG {.threadvar.}: int
    gBG {.threadvar.}: int

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)
    gFG = 0
    gBG = 0

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

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

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)
  elif cmd in {fgColor, bgColor}:
    let term = getTerminal()
    term.fgSetColor = cmd == fgColor

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.
  runnableExamples("-r:off"):
    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.
  runnableExamples:
    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 =
  ## Reads 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):
  proc readPasswordFromStdin*(prompt: string, password: var string):
                              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.setLen(0)
    stdout.write(prompt)
    let hi = createFileA("CONIN$",
      GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0)
    var mode = DWORD 0
    discard getConsoleMode(hi, addr mode)
    let origMode = mode
    const
      ENABLE_PROCESSED_INPUT = 1
      ENABLE_ECHO_INPUT = 4
    mode = (mode or ENABLE_PROCESSED_INPUT) and not ENABLE_ECHO_INPUT

    discard setConsoleMode(hi, mode)
    result = readLine(stdin, password)
    discard setConsoleMode(hi, origMode)
    discard closeHandle(hi)
    stdout.write "\n"

else:
  import termios

  proc readPasswordFromStdin*(prompt: string, password: var string):
                            bool {.tags: [ReadIOEffect, WriteIOEffect].} =
    password.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: "): string =
  ## Reads a password from stdin without printing it.
  result = ""
  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
  ## `exitprocs.addExitProc(resetAttributes)`.
  resetAttributes(stdout)

proc isTrueColorSupported*(): bool =
  ## Returns true if a terminal supports true color.
  return getTerminal().trueColorIsSupported

when defined(windows):
  import os

proc enableTrueColors*() =
  ## Enables 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 = getEnv("COLORTERM").toLowerAscii() in [
        "truecolor", "24bit"]
    term.trueColorIsEnabled = term.trueColorIsSupported

proc disableTrueColors*() =
  ## Disables 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(): owned(PTerminal) =
  new result
  when defined(windows):
    initTerminal(result)
