summary refs log tree commit diff stats
path: root/lib/impure/rdstdin.nim
blob: f722a6b39a3d6e9bbb6072eab49eaf358585d04a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#
#
#            Nim's Runtime Library
#        (c) Copyright 2015 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

## This module contains code for reading from `stdin`:idx:. On UNIX the
## linenoise library is wrapped and set up to provide default key bindings
## (e.g. you can navigate with the arrow keys). On Windows ``system.readLine``
## is used. This suffices because Windows' console already provides the
## wanted functionality.

{.deadCodeElim: on.}

when defined(Windows):
  proc readLineFromStdin*(prompt: string): TaintedString {.
                          tags: [ReadIOEffect, WriteIOEffect].} =
    ## Reads a line from stdin.
    stdout.write(prompt)
    result = readLine(stdin)

  proc readLineFromStdin*(prompt: string, line: var TaintedString): bool {.
                          tags: [ReadIOEffect, WriteIOEffect].} =
    ## Reads a `line` from stdin. `line` must not be
    ## ``nil``! May throw an IO exception.
    ## A line of text may be delimited by ``CR``, ``LF`` or
    ## ``CRLF``. The newline character(s) are not part of the returned string.
    ## Returns ``false`` if the end of the file has been reached, ``true``
    ## otherwise. If ``false`` is returned `line` contains no new data.
    stdout.write(prompt)
    result = readLine(stdin, line)

  import winlean

  const
    VK_SHIFT* = 16
    VK_CONTROL* = 17
    VK_MENU* = 18
    KEY_EVENT* = 1

  type
    KEY_EVENT_RECORD = object
      bKeyDown: WinBool
      wRepeatCount: uint16
      wVirtualKeyCode: uint16
      wVirtualScanCode: uint16
      unicodeChar: uint16
      dwControlKeyState: uint32
    INPUT_RECORD = object
      eventType*: int16
      reserved*: int16
      event*: KEY_EVENT_RECORD
      safetyBuffer: array[0..5, DWORD]

  proc readConsoleInputW*(hConsoleInput: HANDLE, lpBuffer: var INPUTRECORD,
                          nLength: uint32,
                          lpNumberOfEventsRead: var uint32): WINBOOL{.
      stdcall, dynlib: "kernel32", importc: "ReadConsoleInputW".}

  proc getch(): uint16 =
    let hStdin = getStdHandle(STD_INPUT_HANDLE)
    var
      irInputRecord: INPUT_RECORD
      dwEventsRead: uint32

    while readConsoleInputW(hStdin, irInputRecord, 1, dwEventsRead) != 0:
      if irInputRecord.eventType == KEY_EVENT and
          irInputRecord.event.wVirtualKeyCode notin {VK_SHIFT, VK_MENU, VK_CONTROL}:
         result = irInputRecord.event.unicodeChar
         discard readConsoleInputW(hStdin, irInputRecord, 1, dwEventsRead)
         return result

  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.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, i)
          inc i, x
        password.setLen(max(password.len - x, 0))
      else:
        password.add(toUTF8(c.Rune))
    stdout.write "\n"

else:
  import linenoise, termios

  proc readLineFromStdin*(prompt: string): TaintedString {.
                          tags: [ReadIOEffect, WriteIOEffect].} =
    var buffer = linenoise.readLine(prompt)
    if isNil(buffer):
      raise newException(IOError, "Linenoise returned nil")
    result = TaintedString($buffer)
    if result.string.len > 0:
      historyAdd(buffer)
    linenoise.free(buffer)

  proc readLineFromStdin*(prompt: string, line: var TaintedString): bool {.
                          tags: [ReadIOEffect, WriteIOEffect].} =
    var buffer = linenoise.readLine(prompt)
    if isNil(buffer):
      raise newException(IOError, "Linenoise returned nil")
    line = TaintedString($buffer)
    if line.string.len > 0:
      historyAdd(buffer)
    linenoise.free(buffer)
    result = true

  proc readPasswordFromStdin*(prompt: string, password: var TaintedString):
                              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: string): TaintedString =
  ## Reads a password from stdin without printing it.
  result = TaintedString("")
  discard readPasswordFromStdin(prompt, result)