summary refs log blame commit diff stats
path: root/lib/pure/terminal.nim
blob: b6d99e4298e2baf44865fda7f98797dc4957ff4a (plain) (tree)
1
2
3

 
                                  















                                                                               



























































                                                                                 

     
                     


                                                                     

                                                                       
                                                                          
                                 

                                         
                                  
                                                           
                                 
                                                                 

                               
                                  
                                                
                                                           





                                                                





                                                       
                                                                         
                     


                                                                               

                               

                                         



                                                                   
                

                  
                                                                               






                                                                    
                                       
                           
                                                              
                                 
                                        
                       
                                                        
                                 







                                                                      
                                         
                             
                                                                
                                   
                                          
                         
                                                          
                                   
         
             
 
                         







                                         
                           







                                           
                              







                                                 
                               








                                                  
         
     
                      

                                                                              
             


                          
                        

                                                                                
             


                           
                   

                                                                                
             


                          
                 

                                                                           
             


                           
                 

                                    
                                       

                           
                                                              
                                 
                                        
                    
                                                        
                                 

                                       
                                                     
                                                               
                                 
                                                                       
                                                                                
                                 



                         
                   

                                                                               
                                       
                       
                                               
                           
 
                                                              
                                 

                                                                
                                                         
                                                               
                                 
                                                                        
                                                               
                                 



                         
                                  


                                                                           
                                                       



                         
                                                           







                                              

                               





                                       
                                   






                                                                            
                                                 



                                         
                                                                   




                                              
                                                   









                                           
                                                          








                                     
                                                          








                                     



                                                             




                                            
                                                  







                                                              
                                                                        




                                    
                                                             




                                            
                                                  







                                                              
                                                                        



                                    
 
                             

                                                              
                                            

                                               
                                            
                                            
 
                                            
 











                                                                                
 
                                           
                      
                    
                   


                                  



                                  

                                                                      







                                                                      

                                                                           
                                                         
 






                                                                               



                                                                               






                                                 
                                           






                                                                         
                                    
 

                                                                        
#
#
#            Nim's Runtime Library
#        (c) Copyright 2012 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

## This module contains a few procedures to control the *terminal*
## (also called *console*). On UNIX, the implementation simply uses ANSI escape
## sequences and does not depend on any other module, on Windows it uses the
## Windows API.
## Changing the style is permanent even after program termination! Use the
## code ``system.addQuitProc(resetAttributes)`` to restore the defaults.

import macros

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

  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

  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 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".}

  var
    conHandle: Handle
  # = createFile("CONOUT$", GENERIC_WRITE, 0, nil, OPEN_ALWAYS, 0, 0)

  block:
    var hTemp = getStdHandle(STD_OUTPUT_HANDLE)
    if duplicateHandle(getCurrentProcess(), hTemp, getCurrentProcess(),
                       addr(conHandle), 0, 1, DUPLICATE_SAME_ACCESS) == 0:
      raiseOSError(osLastError())

  proc getCursorPos(): tuple [x,y: int] =
    var c: CONSOLESCREENBUFFERINFO
    if getConsoleScreenBufferInfo(conHandle, addr(c)) == 0:
      raiseOSError(osLastError())
    return (int(c.dwCursorPosition.X), int(c.dwCursorPosition.Y))

  proc getAttributes(): int16 =
    var c: CONSOLESCREENBUFFERINFO
    # workaround Windows bugs: try several times
    if getConsoleScreenBufferInfo(conHandle, addr(c)) != 0:
      return c.wAttributes
    return 0x70'i16 # ERROR: return white background, black text

  var
    oldAttr = getAttributes()

else:
  import termios, unsigned

  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 setCursorPos*(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):
    var c: COORD
    c.X = int16(x)
    c.Y = int16(y)
    if setConsoleCursorPosition(conHandle, c) == 0: raiseOSError(osLastError())
  else:
    stdout.write("\e[" & $y & ';' & $x & 'f')

proc setCursorXPos*(x: int) =
  ## sets the terminal's cursor to the x position. The y position is
  ## not changed.
  when defined(windows):
    var scrbuf: CONSOLESCREENBUFFERINFO
    var hStdout = conHandle
    if getConsoleScreenBufferInfo(hStdout, addr(scrbuf)) == 0:
      raiseOSError(osLastError())
    var origin = scrbuf.dwCursorPosition
    origin.X = int16(x)
    if setConsoleCursorPosition(conHandle, origin) == 0:
      raiseOSError(osLastError())
  else:
    stdout.write("\e[" & $x & 'G')

when defined(windows):
  proc setCursorYPos*(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):
      var scrbuf: CONSOLESCREENBUFFERINFO
      var hStdout = conHandle
      if getConsoleScreenBufferInfo(hStdout, addr(scrbuf)) == 0:
        raiseOSError(osLastError())
      var origin = scrbuf.dwCursorPosition
      origin.Y = int16(y)
      if setConsoleCursorPosition(conHandle, origin) == 0:
        raiseOSError(osLastError())
    else:
      discard

proc cursorUp*(count=1) =
  ## Moves the cursor up by `count` rows.
  when defined(windows):
    var p = getCursorPos()
    dec(p.y, count)
    setCursorPos(p.x, p.y)
  else:
    stdout.write("\e[" & $count & 'A')

proc cursorDown*(count=1) =
  ## Moves the cursor down by `count` rows.
  when defined(windows):
    var p = getCursorPos()
    inc(p.y, count)
    setCursorPos(p.x, p.y)
  else:
    stdout.write("\e[" & $count & 'B')

proc cursorForward*(count=1) =
  ## Moves the cursor forward by `count` columns.
  when defined(windows):
    var p = getCursorPos()
    inc(p.x, count)
    setCursorPos(p.x, p.y)
  else:
    stdout.write("\e[" & $count & 'C')

proc cursorBackward*(count=1) =
  ## Moves the cursor backward by `count` columns.
  when defined(windows):
    var p = getCursorPos()
    dec(p.x, count)
    setCursorPos(p.x, p.y)
  else:
    stdout.write("\e[" & $count & 'D')

when true:
  discard
else:
  proc eraseLineEnd* =
    ## Erases from the current cursor position to the end of the current line.
    when defined(windows):
      discard
    else:
      stdout.write("\e[K")

  proc eraseLineStart* =
    ## Erases from the current cursor position to the start of the current line.
    when defined(windows):
      discard
    else:
      stdout.write("\e[1K")

  proc eraseDown* =
    ## Erases the screen from the current line down to the bottom of the screen.
    when defined(windows):
      discard
    else:
      stdout.write("\e[J")

  proc eraseUp* =
    ## Erases the screen from the current line up to the top of the screen.
    when defined(windows):
      discard
    else:
      stdout.write("\e[1J")

proc eraseLine* =
  ## Erases the entire current line.
  when defined(windows):
    var scrbuf: CONSOLESCREENBUFFERINFO
    var numwrote: DWORD
    var hStdout = conHandle
    if getConsoleScreenBufferInfo(hStdout, addr(scrbuf)) == 0:
      raiseOSError(osLastError())
    var origin = scrbuf.dwCursorPosition
    origin.X = 0'i16
    if setConsoleCursorPosition(conHandle, origin) == 0:
      raiseOSError(osLastError())
    var ht = scrbuf.dwSize.Y - origin.Y
    var wt = scrbuf.dwSize.X - origin.X
    if fillConsoleOutputCharacter(hStdout,' ', ht*wt,
                                  origin, addr(numwrote)) == 0:
      raiseOSError(osLastError())
    if fillConsoleOutputAttribute(hStdout, scrbuf.wAttributes, ht * wt,
                                  scrbuf.dwCursorPosition, addr(numwrote)) == 0:
      raiseOSError(osLastError())
  else:
    stdout.write("\e[2K")
    setCursorXPos(0)

proc eraseScreen* =
  ## Erases the screen with the background colour and moves the cursor to home.
  when defined(windows):
    var scrbuf: CONSOLESCREENBUFFERINFO
    var numwrote: DWORD
    var origin: COORD # is inititalized to 0, 0
    var hStdout = conHandle

    if getConsoleScreenBufferInfo(hStdout, addr(scrbuf)) == 0:
      raiseOSError(osLastError())
    let numChars = int32(scrbuf.dwSize.X)*int32(scrbuf.dwSize.Y)

    if fillConsoleOutputCharacter(hStdout, ' ', numChars,
                                  origin, addr(numwrote)) == 0:
      raiseOSError(osLastError())
    if fillConsoleOutputAttribute(hStdout, scrbuf.wAttributes, numChars,
                                  origin, addr(numwrote)) == 0:
      raiseOSError(osLastError())
    setCursorXPos(0)
  else:
    stdout.write("\e[2J")

proc resetAttributes* {.noconv.} =
  ## resets all attributes; it is advisable to register this as a quit proc
  ## with ``system.addQuitProc(resetAttributes)``.
  when defined(windows):
    discard setConsoleTextAttribute(conHandle, oldAttr)
  else:
    stdout.write("\e[0m")

type
  Style* = enum         ## different styles for text output
    styleBright = 1,     ## bright text
    styleDim,            ## dim text
    styleUnknown,        ## unknown
    styleUnderscore = 4, ## underscored text
    styleBlink,          ## blinking/bold text
    styleReverse = 7,    ## unknown
    styleHidden          ## hidden text

{.deprecated: [TStyle: Style].}

when not defined(windows):
  var
    # XXX: These better be thread-local
    gFG = 0
    gBG = 0

proc setStyle*(style: set[Style]) =
  ## sets the terminal style
  when defined(windows):
    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(conHandle, a)
  else:
    for s in items(style):
      stdout.write("\e[" & $ord(s) & 'm')

proc writeStyled*(txt: string, style: set[Style] = {styleBright}) =
  ## writes the text `txt` in a given `style`.
  when defined(windows):
    var old = getAttributes()
    setStyle(style)
    stdout.write(txt)
    discard setConsoleTextAttribute(conHandle, old)
  else:
    setStyle(style)
    stdout.write(txt)
    resetAttributes()
    if gFG != 0:
      stdout.write("\e[" & $ord(gFG) & 'm')
    if gBG != 0:
      stdout.write("\e[" & $ord(gBG) & 'm')

type
  ForegroundColor* = enum  ## terminal's foreground colors
    fgBlack = 30,          ## black
    fgRed,                 ## red
    fgGreen,               ## green
    fgYellow,              ## yellow
    fgBlue,                ## blue
    fgMagenta,             ## magenta
    fgCyan,                ## cyan
    fgWhite                ## white

  BackgroundColor* = enum  ## terminal's background colors
    bgBlack = 40,          ## black
    bgRed,                 ## red
    bgGreen,               ## green
    bgYellow,              ## yellow
    bgBlue,                ## blue
    bgMagenta,             ## magenta
    bgCyan,                ## cyan
    bgWhite                ## white

{.deprecated: [TForegroundColor: ForegroundColor,
               TBackgroundColor: BackgroundColor].}

proc setForegroundColor*(fg: ForegroundColor, bright=false) =
  ## sets the terminal's foreground color
  when defined(windows):
    var old = getAttributes() and not 0x0007
    if bright:
      old = old or FOREGROUND_INTENSITY
    const lookup: array [ForegroundColor, int] = [
      0,
      (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)]
    discard setConsoleTextAttribute(conHandle, toU16(old or lookup[fg]))
  else:
    gFG = ord(fg)
    if bright: inc(gFG, 60)
    stdout.write("\e[" & $gFG & 'm')

proc setBackgroundColor*(bg: BackgroundColor, bright=false) =
  ## sets the terminal's background color
  when defined(windows):
    var old = getAttributes() and not 0x0070
    if bright:
      old = old or BACKGROUND_INTENSITY
    const lookup: array [BackgroundColor, int] = [
      0,
      (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)]
    discard setConsoleTextAttribute(conHandle, toU16(old or lookup[bg]))
  else:
    gBG = ord(bg)
    if bright: inc(gBG, 60)
    stdout.write("\e[" & $gBG & 'm')

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

template styledEchoProcessArg(s: string) = write stdout, s
template styledEchoProcessArg(style: Style) = setStyle({style})
template styledEchoProcessArg(style: set[Style]) = setStyle style
template styledEchoProcessArg(color: ForegroundColor) = setForegroundColor color
template styledEchoProcessArg(color: BackgroundColor) = setBackgroundColor color
template styledEchoProcessArg(cmd: TerminalCmd) =
  when cmd == resetStyle:
    resetAttributes()

macro styledEcho*(m: varargs[expr]): stmt =
  ## to be documented.
  let m = callsite()
  var reset = false
  result = newNimNode(nnkStmtList)

  for i in countup(1, 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 writeLine
        result.add(newCall(bindSym"writeLine", bindSym"stdout", item))
        if reset: result.add(newCall(bindSym"resetAttributes"))
        return
      else:
        # if it is string literal just call write, do not enable reset
        result.add(newCall(bindSym"write", bindSym"stdout", item))
    else:
      result.add(newCall(bindSym"styledEchoProcessArg", item))
      reset = true

  result.add(newCall(bindSym"write", bindSym"stdout", newStrLitNode("\n")))
  if reset: result.add(newCall(bindSym"resetAttributes"))

when defined(nimdoc):
  proc getch*(): char =
    ## Read a single character from the terminal, blocking until it is entered.
    ## The character is not printed to the terminal. This is not available for
    ## Windows.
    discard
elif not defined(windows):
  proc getch*(): char =
    ## Read a single character from the terminal, blocking until it is entered.
    ## The character is not printed to the terminal. This is not available for
    ## Windows.
    let fd = getFileHandle(stdin)
    var oldMode: Termios
    discard fd.tcgetattr(addr oldMode)
    fd.setRaw()
    result = stdin.readChar()
    discard fd.tcsetattr(TCSADRAIN, addr oldMode)

when not defined(testing) and isMainModule:
  system.addQuitProc(resetAttributes)
  write(stdout, "never mind")
  eraseLine()
  #setCursorPos(2, 2)
  writeStyled("styled text ", {styleBright, styleBlink, styleUnderscore})
  setBackGroundColor(bgCyan, true)
  setForeGroundColor(fgBlue)
  writeLine(stdout, "ordinary text")

  styledEcho("styled text ", {styleBright, styleBlink, styleUnderscore})