diff --git a/config.nims b/config.nims index 34d2b0b..88f3b55 100644 --- a/config.nims +++ b/config.nims @@ -4,7 +4,6 @@ task test, "run tests": selfExec "r tests/tbbansi.nim" task develop, "install cligen for development": - exec "nimble install -l 'illwill@0.4.1'" exec "nimble install -l 'cligen@1.7.5'" proc docFixup(deployDir:string, pkgName: string) = @@ -31,4 +30,3 @@ task docs, "Deploy doc html + search index to public/ directory": when withDir(thisDir(), system.dirExists("nimbledeps")): --path:"./nimbledeps/pkgs2/cligen-1.7.5-f3ffe7329c8db755677d3ca377d02ff176cec8b1" - --path:"./nimbledeps/pkgs2/illwill-0.4.1-9c58351502f89a16caf031cbd1992ad3fdfd3c67" diff --git a/src/hwylterm/chooser.nim b/src/hwylterm/chooser.nim index bd4c801..cce53fd 100644 --- a/src/hwylterm/chooser.nim +++ b/src/hwylterm/chooser.nim @@ -11,12 +11,7 @@ import std/[enumerate, os, strutils, sequtils, sets, terminal] import ./bbansi - -template tryImport*(x, body) = - when not (compiles do: import x): body else: import x -tryImport illwill: - {.fatal: "hwylterm/choose requires illwill >= 0.4.1".} - +import ./vendor/illwill proc exitProc() {.noconv.} = illwillDeInit() diff --git a/src/hwylterm/vendor/illwill.nim b/src/hwylterm/vendor/illwill.nim new file mode 100644 index 0000000..0d1cecf --- /dev/null +++ b/src/hwylterm/vendor/illwill.nim @@ -0,0 +1,1659 @@ +## :Authors: John Novak +## +## This is a *curses* inspired simple terminal library that aims to make +## writing cross-platform text mode applications easier. The main features are: +## +## * Non-blocking keyboard input +## * Support for key combinations and special keys available in the standard +## Windows Console (`cmd.exe`) and most common POSIX terminals +## * Virtual terminal buffers with double-buffering support (only +## display changes from the previous frame and minimise the number of +## attribute changes to reduce CPU usage) +## * Simple graphics using UTF-8 box drawing symbols +## * Full-screen support with restoring the contents of the terminal after +## exit (restoring works only on POSIX) +## * Basic suspend/continue (`SIGTSTP`, `SIGCONT`) support on POSIX +## * Basic mouse support +## +## The module depends only on the standard `terminal +## `_ module. However, you +## should not use any terminal functions directly, neither should you use +## `echo`, `write` or other similar functions for output. You should **only** +## use the interface provided by the module to interact with the terminal. +## +## The following symbols are exported from the terminal_ module (these are +## safe to use): +## +## * `terminalWidth() `_ +## * `terminalHeight() `_ +## * `terminalSize() `_ +## * `hideCursor() `_ +## * `showCursor() `_ +## * `Style `_ +## +{.push hints:off .} +import macros, os, terminal, unicode, bitops + +export terminal.terminalWidth +export terminal.terminalHeight +export terminal.terminalSize +export terminal.hideCursor +export terminal.showCursor +export terminal.Style + +type + ForegroundColor* = enum ## Foreground colors + fgNone = 0, ## default + fgBlack = 30, ## black + fgRed, ## red + fgGreen, ## green + fgYellow, ## yellow + fgBlue, ## blue + fgMagenta, ## magenta + fgCyan, ## cyan + fgWhite ## white + + BackgroundColor* = enum ## Background colors + bgNone = 0, ## default (transparent) + bgBlack = 40, ## black + bgRed, ## red + bgGreen, ## green + bgYellow, ## yellow + bgBlue, ## blue + bgMagenta, ## magenta + bgCyan, ## cyan + bgWhite ## white + + Key* {.pure.} = enum ## Supported single key presses and key combinations + None = (-1, "None"), + + # Special ASCII characters + CtrlA = (1, "CtrlA"), + CtrlB = (2, "CtrlB"), + CtrlC = (3, "CtrlC"), + CtrlD = (4, "CtrlD"), + CtrlE = (5, "CtrlE"), + CtrlF = (6, "CtrlF"), + CtrlG = (7, "CtrlG"), + CtrlH = (8, "CtrlH"), + Tab = (9, "Tab"), # Ctrl-I + CtrlJ = (10, "CtrlJ"), + CtrlK = (11, "CtrlK"), + CtrlL = (12, "CtrlL"), + Enter = (13, "Enter"), # Ctrl-M + CtrlN = (14, "CtrlN"), + CtrlO = (15, "CtrlO"), + CtrlP = (16, "CtrlP"), + CtrlQ = (17, "CtrlQ"), + CtrlR = (18, "CtrlR"), + CtrlS = (19, "CtrlS"), + CtrlT = (20, "CtrlT"), + CtrlU = (21, "CtrlU"), + CtrlV = (22, "CtrlV"), + CtrlW = (23, "CtrlW"), + CtrlX = (24, "CtrlX"), + CtrlY = (25, "CtrlY"), + CtrlZ = (26, "CtrlZ"), + Escape = (27, "Escape"), + + CtrlBackslash = (28, "CtrlBackslash"), + CtrlRightBracket = (29, "CtrlRightBracket"), + + # Printable ASCII characters + Space = (32, "Space"), + ExclamationMark = (33, "ExclamationMark"), + DoubleQuote = (34, "DoubleQuote"), + Hash = (35, "Hash"), + Dollar = (36, "Dollar"), + Percent = (37, "Percent"), + Ampersand = (38, "Ampersand"), + SingleQuote = (39, "SingleQuote"), + LeftParen = (40, "LeftParen"), + RightParen = (41, "RightParen"), + Asterisk = (42, "Asterisk"), + Plus = (43, "Plus"), + Comma = (44, "Comma"), + Minus = (45, "Minus"), + Dot = (46, "Dot"), + Slash = (47, "Slash"), + + Zero = (48, "Zero"), + One = (49, "One"), + Two = (50, "Two"), + Three = (51, "Three"), + Four = (52, "Four"), + Five = (53, "Five"), + Six = (54, "Six"), + Seven = (55, "Seven"), + Eight = (56, "Eight"), + Nine = (57, "Nine"), + + Colon = (58, "Colon"), + Semicolon = (59, "Semicolon"), + LessThan = (60, "LessThan"), + Equals = (61, "Equals"), + GreaterThan = (62, "GreaterThan"), + QuestionMark = (63, "QuestionMark"), + At = (64, "At"), + + ShiftA = (65, "ShiftA"), + ShiftB = (66, "ShiftB"), + ShiftC = (67, "ShiftC"), + ShiftD = (68, "ShiftD"), + ShiftE = (69, "ShiftE"), + ShiftF = (70, "ShiftF"), + ShiftG = (71, "ShiftG"), + ShiftH = (72, "ShiftH"), + ShiftI = (73, "ShiftI"), + ShiftJ = (74, "ShiftJ"), + ShiftK = (75, "ShiftK"), + ShiftL = (76, "ShiftL"), + ShiftM = (77, "ShiftM"), + ShiftN = (78, "ShiftN"), + ShiftO = (79, "ShiftO"), + ShiftP = (80, "ShiftP"), + ShiftQ = (81, "ShiftQ"), + ShiftR = (82, "ShiftR"), + ShiftS = (83, "ShiftS"), + ShiftT = (84, "ShiftT"), + ShiftU = (85, "ShiftU"), + ShiftV = (86, "ShiftV"), + ShiftW = (87, "ShiftW"), + ShiftX = (88, "ShiftX"), + ShiftY = (89, "ShiftY"), + ShiftZ = (90, "ShiftZ"), + + LeftBracket = (91, "LeftBracket"), + Backslash = (92, "Backslash"), + RightBracket = (93, "RightBracket"), + Caret = (94, "Caret"), + Underscore = (95, "Underscore"), + GraveAccent = (96, "GraveAccent"), + + A = (97, "A"), + B = (98, "B"), + C = (99, "C"), + D = (100, "D"), + E = (101, "E"), + F = (102, "F"), + G = (103, "G"), + H = (104, "H"), + I = (105, "I"), + J = (106, "J"), + K = (107, "K"), + L = (108, "L"), + M = (109, "M"), + N = (110, "N"), + O = (111, "O"), + P = (112, "P"), + Q = (113, "Q"), + R = (114, "R"), + S = (115, "S"), + T = (116, "T"), + U = (117, "U"), + V = (118, "V"), + W = (119, "W"), + X = (120, "X"), + Y = (121, "Y"), + Z = (122, "Z"), + + LeftBrace = (123, "LeftBrace"), + Pipe = (124, "Pipe"), + RightBrace = (125, "RightBrace"), + Tilde = (126, "Tilde"), + Backspace = (127, "Backspace"), + + # Special characters with virtual keycodes + Up = (1001, "Up"), + Down = (1002, "Down"), + Right = (1003, "Right"), + Left = (1004, "Left"), + Home = (1005, "Home"), + Insert = (1006, "Insert"), + Delete = (1007, "Delete"), + End = (1008, "End"), + PageUp = (1009, "PageUp"), + PageDown = (1010, "PageDown"), + + F1 = (1011, "F1"), + F2 = (1012, "F2"), + F3 = (1013, "F3"), + F4 = (1014, "F4"), + F5 = (1015, "F5"), + F6 = (1016, "F6"), + F7 = (1017, "F7"), + F8 = (1018, "F8"), + F9 = (1019, "F9"), + F10 = (1020, "F10"), + F11 = (1021, "F11"), + F12 = (1022, "F12"), + + Mouse = (5000, "Mouse") + + IllwillError* = object of CatchableError + +type + MouseButtonAction* {.pure.} = enum + mbaNone, mbaPressed, mbaReleased + + MouseInfo* = object + x*: int ## X mouse position + y*: int ## Y mouse position + button*: MouseButton ## which button was pressed + action*: MouseButtonAction ## if button was released or pressed + ctrl*: bool ## was Ctrl down + shift*: bool ## was Shift down + scroll*: bool ## if this is a mouse scroll event + scrollDir*: ScrollDirection ## scroll direction + move*: bool ## if this is a mouse move event + + MouseButton* {.pure.} = enum + mbNone, mbLeft, mbMiddle, mbRight + + ScrollDirection* {.pure.} = enum + sdNone, sdUp, sdDown + +var + gMouseInfo = MouseInfo() + gMouse: bool = false + +proc getMouse*(): MouseInfo = + ## When the library is initialised with `illwillInit(mouse=true)`, mouse + ## events are captured and can be retrieved by calling this function. + ## + ## See `MouseInfo` for further details. + ## + ## Example: + ## + ## .. code-block:: + ## + ## import illwill, os + ## + ## proc exitProc() {.noconv.} = + ## illwillDeinit() + ## showCursor() + ## quit(0) + ## + ## setControlCHook(exitProc) + ## illwillInit(mouse=true) + ## + ## var tb = newTerminalBuffer(terminalWidth(), terminalHeight()) + ## + ## while true: + ## var key = getKey() + ## if key == Key.Mouse: + ## echo getMouse() + ## tb.display() + ## sleep(10) + + return gMouseInfo + + +{.push warning[HoleEnumConv]:off.} + +func toKey(c: int): Key = + try: + result = Key(c) + except RangeDefect: # ignore unknown keycodes + result = Key.None + +{.pop} + + +var gIllwillInitialised = false +var gFullScreen = false +var gFullRedrawNextFrame = false + +when defined(windows): + import encodings, winlean + + 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".} + + # Mouse + const + INPUT_BUFFER_LEN = 512 + + const + ENABLE_MOUSE_INPUT = 0x10 + ENABLE_WINDOW_INPUT = 0x8 + ENABLE_QUICK_EDIT_MODE = 0x40 + ENABLE_EXTENDED_FLAGS = 0x80 + MOUSE_EVENT = 0x0002 + + const + FROM_LEFT_1ST_BUTTON_PRESSED = 0x0001 + FROM_LEFT_2ND_BUTTON_PRESSED = 0x0004 + RIGHTMOST_BUTTON_PRESSED = 0x0002 + + const + LEFT_CTRL_PRESSED = 0x0008 + RIGHT_CTRL_PRESSED = 0x0004 + SHIFT_PRESSED = 0x0010 + + const + MOUSE_WHEELED = 0x0004 + + type + WCHAR = WinChar + CHAR = char + BOOL = WINBOOL + WORD = uint16 + UINT = cint + SHORT = int16 + + # Windows console input structuress + type + KEY_EVENT_RECORD_UNION* {.bycopy, union.} = object + UnicodeChar*: WCHAR + AsciiChar*: CHAR + + INPUT_RECORD_UNION* {.bycopy, union.} = object + KeyEvent*: KEY_EVENT_RECORD + MouseEvent*: MOUSE_EVENT_RECORD + WindowBufferSizeEvent*: WINDOW_BUFFER_SIZE_RECORD + MenuEvent*: MENU_EVENT_RECORD + FocusEvent*: FOCUS_EVENT_RECORD + + COORD* {.bycopy.} = object + X*: SHORT + Y*: SHORT + + PCOORD* = ptr COORD + FOCUS_EVENT_RECORD* {.bycopy.} = object + bSetFocus*: BOOL + + MENU_EVENT_RECORD* {.bycopy.} = object + dwCommandId*: UINT + + PMENU_EVENT_RECORD* = ptr MENU_EVENT_RECORD + MOUSE_EVENT_RECORD* {.bycopy.} = object + dwMousePosition*: COORD + dwButtonState*: DWORD + dwControlKeyState*: DWORD + dwEventFlags*: DWORD + + WINDOW_BUFFER_SIZE_RECORD* {.bycopy.} = object + dwSize*: COORD + + INPUT_RECORD* {.bycopy.} = object + EventType*: WORD + Event*: INPUT_RECORD_UNION + + type + PINPUT_RECORD = ptr array[INPUT_BUFFER_LEN, INPUT_RECORD] + LPDWORD = PDWORD + + proc peekConsoleInputA(hConsoleInput: HANDLE, lpBuffer: PINPUT_RECORD, + nLength: DWORD, lpNumberOfEventsRead: LPDWORD): WINBOOL + {.stdcall, dynlib: "kernel32", importc: "PeekConsoleInputA".} + + const + ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002 + + var gOldConsoleModeInput: DWORD + var gOldConsoleMode: DWORD + + proc consoleInit() = + discard getConsoleMode(getStdHandle(STD_INPUT_HANDLE), gOldConsoleModeInput.addr) + if gFullScreen: + if getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), gOldConsoleMode.addr) != 0: + var mode = gOldConsoleMode and (not ENABLE_WRAP_AT_EOL_OUTPUT) + discard setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), mode) + else: + discard getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), gOldConsoleMode.addr) + + proc consoleDeinit() = + if gOldConsoleMode != 0: + discard setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), gOldConsoleMode) + + proc getchTimeout(ms: int32): KEY_EVENT_RECORD = + let fd = getStdHandle(STD_INPUT_HANDLE) + var keyEvent = KEY_EVENT_RECORD() + var numRead: cint + while true: + case waitForSingleObject(fd, ms) + of WAIT_TIMEOUT: + keyEvent.eventType = -1 + return + of 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 keyEvent + else: + doAssert(false) + + proc getKeyAsync(ms: int): Key = + let event = getchTimeout(int32(ms)) + + if event.eventType == -1: + return Key.None + + if event.uChar != 0: + return toKey((event.uChar)) + else: + case event.wVirtualScanCode + of 8: return Key.Backspace + of 9: return Key.Tab + of 13: return Key.Enter + of 32: return Key.Space + of 59: return Key.F1 + of 60: return Key.F2 + of 61: return Key.F3 + of 62: return Key.F4 + of 63: return Key.F5 + of 64: return Key.F6 + of 65: return Key.F7 + of 66: return Key.F8 + of 67: return Key.F9 + of 68: return Key.F10 + of 71: return Key.Home + of 72: return Key.Up + of 73: return Key.PageUp + of 75: return Key.Left + of 77: return Key.Right + of 79: return Key.End + of 80: return Key.Down + of 81: return Key.PageDown + of 82: return Key.Insert + of 83: return Key.Delete + of 87: return Key.F11 + of 88: return Key.F12 + else: return Key.None + + proc writeConsole(hConsoleOutput: HANDLE, lpBuffer: pointer, + nNumberOfCharsToWrite: DWORD, + lpNumberOfCharsWritten: ptr DWORD, + lpReserved: pointer): WINBOOL {. + stdcall, dynlib: "kernel32", importc: "WriteConsoleW".} + + var hStdout = getStdHandle(STD_OUTPUT_HANDLE) + var utf16LEConverter = open(destEncoding = "utf-16", srcEncoding = "UTF-8") + + proc put(s: string) = + var us = utf16LEConverter.convert(s) + var numWritten: DWORD + discard writeConsole(hStdout, pointer(us[0].addr), DWORD(s.runeLen), + numWritten.addr, nil) + +else: # OS X & Linux + import posix, tables, termios + import strutils, strformat + + proc consoleInit() + proc consoleDeinit() + + # References: + # https://de.wikipedia.org/wiki/ANSI-Escapesequenz + # https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates + const + CSI = 0x1B.chr & 0x5B.chr + SET_BTN_EVENT_MOUSE = "1002" + SET_ANY_EVENT_MOUSE = "1003" + SET_SGR_EXT_MODE_MOUSE = "1006" + # SET_URXVT_EXT_MODE_MOUSE = "1015" + ENABLE = "h" + DISABLE = "l" + MouseTrackAny = fmt"{CSI}?{SET_BTN_EVENT_MOUSE}{ENABLE}{CSI}?{SET_ANY_EVENT_MOUSE}{ENABLE}{CSI}?{SET_SGR_EXT_MODE_MOUSE}{ENABLE}" + DisableMouseTrackAny = fmt"{CSI}?{SET_BTN_EVENT_MOUSE}{DISABLE}{CSI}?{SET_ANY_EVENT_MOUSE}{DISABLE}{CSI}?{SET_SGR_EXT_MODE_MOUSE}{DISABLE}" + KEYS_D = [Key.Up, Key.Down, Key.Right, Key.Left, Key.None, Key.End, Key.None, Key.Home] + KEYS_E = [Key.Delete, Key.End, Key.PageUp, Key.PageDown, Key.Home, Key.End] + KEYS_F = [Key.F1, Key.F2, Key.F3, Key.F4, Key.F5, Key.None, Key.F6, Key.F7, Key.F8] + KEYS_G = [Key.F9, Key.F10, Key.None, Key.F11, Key.F12] + + # Adapted from: + # https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_chapter/libc_24.html#SEC499 + proc SIGTSTP_handler(sig: cint) {.noconv.} = + signal(SIGTSTP, SIG_DFL) + # XXX why don't the below 3 lines seem to have any effect? + resetAttributes() + showCursor() + consoleDeinit() + discard posix.raise(SIGTSTP) + + proc SIGCONT_handler(sig: cint) {.noconv.} = + signal(SIGCONT, SIGCONT_handler) + signal(SIGTSTP, SIGTSTP_handler) + + gFullRedrawNextFrame = true + consoleInit() + hideCursor() + + proc installSignalHandlers() = + signal(SIGCONT, SIGCONT_handler) + signal(SIGTSTP, SIGTSTP_handler) + + proc nonblock(enabled: bool) = + var ttyState: Termios + + # get the terminal state + discard tcGetAttr(STDIN_FILENO, ttyState.addr) + + if enabled: + # turn off canonical mode & echo + ttyState.c_lflag = ttyState.c_lflag and not Cflag(ICANON or ECHO) + + # minimum of number input read + ttyState.c_cc[VMIN] = 0.char + + else: + # turn on canonical mode & echo + ttyState.c_lflag = ttyState.c_lflag or ICANON or ECHO + + # set the terminal attributes. + discard tcSetAttr(STDIN_FILENO, TCSANOW, ttyState.addr) + + proc kbhit(ms: int): cint = + var tv: Timeval + tv.tv_sec = Time(ms div 1000) + tv.tv_usec = 1000 * (int32(ms) mod 1000) # int32 because of macos + + var fds: TFdSet + FD_ZERO(fds) + FD_SET(STDIN_FILENO, fds) + discard select(STDIN_FILENO+1, fds.addr, nil, nil, tv.addr) + return FD_ISSET(STDIN_FILENO, fds) + + proc consoleInit() = + nonblock(true) + installSignalHandlers() + + proc consoleDeinit() = + nonblock(false) + + # surely a 100 char buffer is more than enough; the longest + # keycode sequence I've seen was 6 chars + const KeySequenceMaxLen = 100 + + # global keycode buffer + var keyBuf {.threadvar.}: array[KeySequenceMaxLen, int] + + proc splitInputs(inp: openarray[int], max: Natural): seq[seq[int]] = + ## splits the input buffer to extract mouse coordinates + var parts: seq[seq[int]] = @[] + var cur: seq[int] = @[] + for ch in inp[CSI.len+1 .. max-1]: + if ch == ord('M'): + # Button press + parts.add(cur) + gMouseInfo.action = mbaPressed + break + elif ch == ord('m'): + # Button release + parts.add(cur) + gMouseInfo.action = mbaReleased + break + elif ch != ord(';'): + cur.add(ch) + else: + parts.add(cur) + cur = @[] + return parts + + proc getPos(inp: seq[int]): int = + var str = "" + for ch in inp: + str &= $(ch.chr) + result = parseInt(str) + + proc fillGlobalMouseInfo(keyBuf: array[KeySequenceMaxLen, int]) = + let parts = splitInputs(keyBuf, keyBuf.len) + gMouseInfo.x = parts[1].getPos() - 1 + gMouseInfo.y = parts[2].getPos() - 1 + + let bitset = parts[0].getPos() + gMouseInfo.ctrl = bitset.testBit(4) + gMouseInfo.shift = bitset.testBit(2) + gMouseInfo.move = bitset.testBit(5) + + case ((bitset.uint8 shl 6) shr 6).int + of 0: gMouseInfo.button = MouseButton.mbLeft + of 1: gMouseInfo.button = MouseButton.mbMiddle + of 2: gMouseInfo.button = MouseButton.mbRight + else: + gMouseInfo.action = MouseButtonAction.mbaNone + # Move sends 3, but we ignore + gMouseInfo.button = MouseButton.mbNone + + gMouseInfo.scroll = bitset.testBit(6) + + if gMouseInfo.scroll: + # On scroll button=3 is reported, but we want no button pressed + gMouseInfo.button = MouseButton.mbNone + if bitset.testBit(0): gMouseInfo.scrollDir = ScrollDirection.sdDown + else: gMouseInfo.scrollDir = ScrollDirection.sdUp + else: + gMouseInfo.scrollDir = ScrollDirection.sdNone + + proc parseStdin[T](input: T): Key = + var ch1, ch2, ch3, ch4, ch5: char + result = Key.None + if read(input, ch1.addr, 1) > 0: + case ch1 + of '\e': + if read(input, ch2.addr, 1) > 0: + if ch2 == 'O' and read(input, ch3.addr, 1) > 0: + if ch3 in "ABCDFH": + result = KEYS_D[int(ch3) - int('A')] + elif ch3 in "PQRS": + result = KEYS_F[int(ch3) - int('P')] + elif ch2 == '[' and read(input, ch3.addr, 1) > 0: + if ch3 in "ABCDFH": + result = KEYS_D[int(ch3) - int('A')] + elif ch3 in "PQRS": + result = KEYS_F[int(ch3) - int('P')] + elif ch3 == '1' and read(input, ch4.addr, 1) > 0: + if ch4 == '~': + result = Key.Home + elif ch4 in "12345789" and read(input, ch5.addr, 1) > 0 and ch5 == '~': + result = KEYS_F[int(ch4) - int('1')] + elif ch3 == '2' and read(input, ch4.addr, 1) > 0: + if ch4 == '~': + result = Key.Insert + elif ch4 in "0134" and read(input, ch5.addr, 1) > 0 and ch5 == '~': + result = KEYS_G[int(ch4) - int('0')] + elif ch3 in "345678" and read(input, ch4.addr, 1) > 0 and ch4 == '~': + result = KEYS_E[int(ch3) - int('3')] + else: + discard # if cannot parse full seq it is discarded + else: + discard # if cannot parse full seq it is discarded + else: + result = Key.Escape + of '\n': + result = Key.Enter + of '\b': + result = Key.Backspace + else: + result = toKey(int(ch1)) + + proc getKeyAsync(ms: int): Key = + result = Key.None + if kbhit(ms) > 0: + result = parseStdin(cint(STDIN_FILENO)) + + template put(s: string) = stdout.write s + +when defined(posix): + const + XtermColor = "xterm-color" + Xterm256Color = "xterm-256color" + +proc enterFullScreen() = + ## Enters full-screen mode (clears the terminal). + when defined(posix): + case getEnv("TERM"): + of XtermColor: + stdout.write "\e7\e[?47h" + of Xterm256Color: + stdout.write "\e[?1049h" + else: + eraseScreen() + else: + eraseScreen() + +proc exitFullScreen() = + ## Exits full-screen mode (restores the previous contents of the terminal). + when defined(posix): + case getEnv("TERM"): + of XtermColor: + stdout.write "\e[2J\e[?47l\e8" + of Xterm256Color: + stdout.write "\e[?1049l" + else: + eraseScreen() + else: + eraseScreen() + setCursorPos(0, 0) + +when defined(posix): + proc enableMouse() = + stdout.write(MouseTrackAny) + stdout.flushFile() + + proc disableMouse() = + stdout.write(DisableMouseTrackAny) + stdout.flushFile() +else: + proc enableMouse(hConsoleInput: Handle) = + var currentMode: DWORD + discard getConsoleMode(hConsoleInput, currentMode.addr) + discard setConsoleMode(hConsoleInput, + ENABLE_WINDOW_INPUT or ENABLE_MOUSE_INPUT or ENABLE_EXTENDED_FLAGS or + (currentMode and ENABLE_QUICK_EDIT_MODE.bitnot()) + ) + + proc disableMouse(hConsoleInput: Handle, oldConsoleMode: DWORD) = + # TODO remove mouse option only? + discard setConsoleMode(hConsoleInput, oldConsoleMode) + +proc illwillInit*(fullScreen: bool=true, mouse: bool=false) = + ## Initializes the terminal and enables non-blocking keyboard input. Needs + ## to be called before doing anything with the library. + ## + ## If `mouse` is set to `true`, mouse events are captured and can be + ## retrieved with `getMouse()`. + ## + ## If the module is already intialised, `IllwillError` is raised. + if gIllwillInitialised: + raise newException(IllwillError, "Illwill already initialised") + gFullScreen = fullScreen + if gFullScreen: enterFullScreen() + + consoleInit() + gMouse = mouse + if gMouse: + when defined(posix): + enableMouse() + else: + enableMouse(getStdHandle(STD_INPUT_HANDLE)) + gIllwillInitialised = true + resetAttributes() + +proc checkInit() = + if not gIllwillInitialised: + raise newException(IllwillError, "Illwill not initialised") + +proc illwillDeinit*() = + ## Resets the terminal to its previous state. Needs to be called before + ## exiting the application. + ## + ## If the module is not intialised, `IllwillError` is raised. + checkInit() + if gFullScreen: exitFullScreen() + if gMouse: + when defined(posix): + disableMouse() + else: + disableMouse(getStdHandle(STD_INPUT_HANDLE), gOldConsoleModeInput) + consoleDeinit() + gIllwillInitialised = false + resetAttributes() + showCursor() + +when defined(windows): + + template alias(newName: untyped, call: untyped) = + template newName(): untyped = call + + var gLastMouseInfo = MouseInfo() + + proc fillGlobalMouseInfo(inputRecord: INPUT_RECORD) = + alias(me, inputRecord.Event.MouseEvent) + + gMouseInfo.x = me.dwMousePosition.X + gMouseInfo.y = me.dwMousePosition.Y + + case me.dwButtonState + of FROM_LEFT_1ST_BUTTON_PRESSED: gMouseInfo.button = mbLeft + of FROM_LEFT_2ND_BUTTON_PRESSED: gMouseInfo.button = mbMiddle + of RIGHTMOST_BUTTON_PRESSED: gMouseInfo.button = mbRight + else: gMouseInfo.button = mbNone + + if gMouseInfo.button != mbNone: + gMouseInfo.action = MouseButtonAction.mbaPressed + elif gMouseInfo.button == mbNone and gLastMouseInfo.button != mbNone: + gMouseInfo.action = MouseButtonAction.mbaReleased + else: + gMouseInfo.action = MouseButtonAction.mbaNone + + if gLastMouseInfo.x != gMouseInfo.x or gLastMouseInfo.y != gMouseInfo.y: + gMouseInfo.move = true + else: + gMouseInfo.move = false + + if bitand(me.dwEventFlags, MOUSE_WHEELED) == MOUSE_WHEELED: + gMouseInfo.scroll = true + if me.dwButtonState.testBit(31): + gMouseInfo.scrollDir = ScrollDirection.sdDown + else: + gMouseInfo.scrollDir = ScrollDirection.sdUp + else: + gMouseInfo.scroll = false + gMouseInfo.scrollDir = ScrollDirection.sdNone + + gMouseInfo.ctrl = ( + bitand(me.dwControlKeyState, LEFT_CTRL_PRESSED) == LEFT_CTRL_PRESSED or + bitand(me.dwControlKeyState, RIGHT_CTRL_PRESSED) == RIGHT_CTRL_PRESSED + ) + + gMouseInfo.shift = bitand(me.dwControlKeyState, SHIFT_PRESSED) == SHIFT_PRESSED + + gLastMouseInfo = gMouseInfo + + + proc hasMouseInput(): bool = + var buffer: array[INPUT_BUFFER_LEN, INPUT_RECORD] + var numberOfEventsRead: DWORD + var toRead: int = 0 + + discard peekConsoleInputA(getStdHandle(STD_INPUT_HANDLE), buffer.addr, + buffer.len.DWORD, numberOfEventsRead.addr) + + if numberOfEventsRead == 0: return false + + for inputRecord in buffer[0.. 0: + put buf + buf = "" + + for y in 0.. 0: + if currYPos != bufYPos: + currXPos = bufXPos + currYPos = bufYPos + setPos(currXPos, currYPos) + elif currXPos != bufXPos: + currXPos = bufXPos + setXPos(currXPos) + put buf + inc(currXPos, buf.runeLen) + buf = "" + + for y in 0..= bb.height: return + var xStart = x1 + var xEnd = x2 + if xStart > xEnd: swap(xStart, xEnd) + if xStart >= bb.width: return + + xEnd = min(xEnd, bb.width-1) + if connect: + for x in xStart..xEnd: + var c = bb[x,y] + var h: int + if x == xStart: + h = if (c and LEFT) > 0: HORIZ else: RIGHT + elif x == xEnd: + h = if (c and RIGHT) > 0: HORIZ else: LEFT + else: + h = HORIZ + if doubleStyle: h = h or H_DBL + bb[x,y] = c or h + else: + for x in xStart..xEnd: + var h = HORIZ + if doubleStyle: h = h or H_DBL + bb[x,y] = h + + +proc drawVertLine*(bb: var BoxBuffer, x, y1, y2: Natural, + doubleStyle: bool = false, connect: bool = true) = + ## Draws a vertical line into the box buffer. Set `doubleStyle` to `true` to + ## draw double lines. Set `connect` to `true` to connect overlapping lines. + if x >= bb.width: return + var yStart = y1 + var yEnd = y2 + if yStart > yEnd: swap(yStart, yEnd) + if yStart >= bb.height: return + + yEnd = min(yEnd, bb.height-1) + if connect: + for y in yStart..yEnd: + var c = bb[x,y] + var v: int + if y == yStart: + v = if (c and UP) > 0: VERT else: DOWN + elif y == yEnd: + v = if (c and DOWN) > 0: VERT else: UP + else: + v = VERT + if doubleStyle: v = v or V_DBL + bb[x,y] = c or v + else: + for y in yStart..yEnd: + var v = VERT + if doubleStyle: v = v or V_DBL + bb[x,y] = v + + +proc drawRect*(bb: var BoxBuffer, x1, y1, x2, y2: Natural, + doubleStyle: bool = false, connect: bool = true) = + ## Draws a rectangle into the box buffer. Set `doubleStyle` to `true` to + ## draw double lines. Set `connect` to `true` to connect overlapping lines. + if abs(x1-x2) < 1 or abs(y1-y2) < 1: return + + if connect: + bb.drawHorizLine(x1, x2, y1, doubleStyle) + bb.drawHorizLine(x1, x2, y2, doubleStyle) + bb.drawVertLine(x1, y1, y2, doubleStyle) + bb.drawVertLine(x2, y1, y2, doubleStyle) + else: + bb.drawHorizLine(x1+1, x2-1, y1, doubleStyle, connect = false) + bb.drawHorizLine(x1+1, x2-1, y2, doubleStyle, connect = false) + bb.drawVertLine(x1, y1+1, y2-1, doubleStyle, connect = false) + bb.drawVertLine(x2, y1+1, y2-1, doubleStyle, connect = false) + + var c = RIGHT or DOWN + if doubleStyle: c = c or V_DBL or H_DBL + bb[x1,y1] = c + + c = LEFT or DOWN + if doubleStyle: c = c or V_DBL or H_DBL + bb[x2,y1] = c + + c = RIGHT or UP + if doubleStyle: c = c or V_DBL or H_DBL + bb[x1,y2] = c + + c = LEFT or UP + if doubleStyle: c = c or V_DBL or H_DBL + bb[x2,y2] = c + + +proc write*(tb: var TerminalBuffer, bb: var BoxBuffer) = + ## Writes the contents of the box buffer into this terminal buffer with + ## the current text attributes. + let width = min(tb.width, bb.width) + let height = min(tb.height, bb.height) + var horizBoxCharCount: int + var forceWrite: bool + + for y in 0.. 0: + if ((boxChar and LEFT) or (boxChar and RIGHT)) > 0: + if horizBoxCharCount == 1: + var prev = tb[x-1,y] + prev.forceWrite = true + tb[x-1,y] = prev + if horizBoxCharCount >= 1: + forceWrite = true + inc(horizBoxCharCount) + else: + horizBoxCharCount = 0 + forceWrite = false + + var c = TerminalChar(ch: toUTF8String(boxChar).runeAt(0), + fg: tb.currFg, bg: tb.currBg, + style: tb.currStyle, forceWrite: forceWrite) + tb[x,y] = c + + +type + TerminalCmd* = enum ## commands that can be expressed as arguments + resetStyle ## reset attributes + +template writeProcessArg(tb: var TerminalBuffer, s: string) = + tb.write(s) + +template writeProcessArg(tb: var TerminalBuffer, style: Style) = + tb.setStyle({style}) + +template writeProcessArg(tb: var TerminalBuffer, style: set[Style]) = + tb.setStyle(style) + +template writeProcessArg(tb: var TerminalBuffer, color: ForegroundColor) = + tb.setForegroundColor(color) + +template writeProcessArg(tb: var TerminalBuffer, color: BackgroundColor) = + tb.setBackgroundColor(color) + +template writeProcessArg(tb: var TerminalBuffer, cmd: TerminalCmd) = + when cmd == resetStyle: + tb.resetAttributes() + +macro write*(tb: var TerminalBuffer, args: varargs[typed]): untyped = + ## Special version of `write` that allows to intersperse text literals with + ## set attribute commands. + ## + ## Example: + ## + ## .. code-block:: + ## import illwill + ## + ## illwillInit(fullscreen=false) + ## + ## var tb = newTerminalBuffer(terminalWidth(), terminalHeight()) + ## + ## tb.setForegroundColor(fgGreen) + ## tb.setBackgroundColor(bgBlue) + ## tb.write(0, 10, "before") + ## + ## tb.write(0, 11, "unchanged", resetStyle, fgYellow, "yellow", bgRed, "red bg", + ## styleBlink, "blink", resetStyle, "reset") + ## + ## tb.write(0, 12, "after") + ## + ## tb.display() + ## + ## illwillDeinit() + ## + ## This will output the following: + ## + ## * 1st row: + ## - `before` with blue background, green foreground and default style + ## * 2nd row: + ## - `unchanged` with blue background, green foreground and default style + ## - `yellow` with default background, yellow foreground and default style + ## - `red bg` with red background, yellow foreground and default style + ## - `blink` with red background, yellow foreground and blink style (if + ## supported by the terminal) + ## - `reset` with the default background and foreground and default style + ## * 3rd row: + ## - `after` with the default background and foreground and default style + ## + ## + result = newNimNode(nnkStmtList) + + if args.len >= 3 and + args[0].typeKind() == ntyInt and args[1].typeKind() == ntyInt: + + let x = args[0] + let y = args[1] + result.add(newCall(bindSym"setCursorPos", tb, x, y)) + for i in 2..