# keycodes.nim import tables import strutils import terminal import unicode type JaleKeycode* = enum jkNull = 0, jkCtrlA = 1, jkCtrlB, jkCtrlC, jkCtrlD, jkCtrlE, jkCtrlF, jkCtrlG, jkBackspace = 8, # ctrl+h = backspace jkTab = 9, # ctrl+i = tab jkCtrlJ = 10, jkCtrlK, jkCtrlL, jkEnter = 13, # ctrl+m = enter jkCtrlN = 14, jkCtrlO, jkCtrlP, jkCtrlQ, jkCtrlR, jkCtrlS, jkCtrlT, jkCtrlU, jkCtrlV, jkCtrlW, jkCtrlX, jkCtrlY, jkCtrlZ, jkEscape = 27, jkCtrlBackslash = 28, jkCtrlRightBracket = 29, # the rest are handled by escapeSeqs jkAltBackspace, jkLeft, jkRight, jkUp, jkDown, jkCtrlLeft, jkCtrlRight, jkCtrlUp, jkCtrlDown, # other 4 move keys jkHome, jkEnd, jkPageUp, jkPageDown, jkCtrlHome, jkCtrlEnd, jkCtrlPageUp, jkCtrlPageDown, # special keys jkDelete, jkInsert, jkContinue # represents an unfinished escape sequence var escapeSeqs*: Table[int, JaleKeycode] proc defEscSeq(keys: seq[int], id: JaleKeycode) = var result = 0 for key in keys: result *= 256 result += key if escapeSeqs.hasKey(result): raise newException(Defect, "Duplicate escape sequence definition") escapeSeqs[result] = id block: defEscSeq(@[127], jkAltBackspace) when defined(windows): defEscSeq(@[224], jkContinue) # arrow keys defEscSeq(@[224, 72], jkUp) defEscSeq(@[224, 80], jkDown) defEscSeq(@[224, 77], jkRight) defEscSeq(@[224, 75], jkLeft) # ctrl+arrow keys defEscSeq(@[224, 141], jkCtrlUp) defEscSeq(@[224, 145], jkCtrlDown) defEscSeq(@[224, 116], jkCtrlRight) defEscSeq(@[224, 115], jkCtrlLeft) # moves defEscSeq(@[224, 71], jkHome) defEscSeq(@[224, 79], jkEnd) defEscSeq(@[224, 73], jkPageUp) defEscSeq(@[224, 81], jkPageDown) # ctrl+moves defEscSeq(@[224, 134], jkCtrlPageUp) defEscSeq(@[224, 118], jkCtrlPageDown) defEscSeq(@[224, 119], jkCtrlHome) defEscSeq(@[224, 117], jkCtrlEnd) # special keys defEscSeq(@[224, 82], jkInsert) defEscSeq(@[224, 83], jkDelete) else: # arrow keys defEscSeq(@[27], jkContinue) defEscSeq(@[27, 91], jkContinue) defEscSeq(@[27, 91, 65], jkUp) defEscSeq(@[27, 91, 66], jkDown) defEscSeq(@[27, 91, 67], jkRight) defEscSeq(@[27, 91, 68], jkLeft) # ctrl+arrow keys defEscSeq(@[27, 91, 49], jkContinue) defEscSeq(@[27, 91, 49, 59], jkContinue) defEscSeq(@[27, 91, 49, 59, 53], jkContinue) # ctrl defEscSeq(@[27, 91, 49, 59, 50], jkContinue) # shift defEscSeq(@[27, 91, 49, 59, 53, 65], jkCtrlUp) # ctrl defEscSeq(@[27, 91, 49, 59, 53, 66], jkCtrlDown) # ctrl defEscSeq(@[27, 91, 49, 59, 53, 67], jkCtrlRight) # ctrl defEscSeq(@[27, 91, 49, 59, 53, 68], jkCtrlLeft) # ctrl # urxvt defEscSeq(@[27, 79], jkContinue) defEscSeq(@[27, 79, 97], jkCtrlUp) defEscSeq(@[27, 79, 98], jkCtrlDown) defEscSeq(@[27, 79, 99], jkCtrlRight) defEscSeq(@[27, 79, 100], jkCtrlLeft) # other 4 move keys defEscSeq(@[27, 91, 72], jkHome) defEscSeq(@[27, 91, 70], jkEnd) defEscSeq(@[27, 91, 54], jkContinue) defEscSeq(@[27, 91, 53], jkContinue) defEscSeq(@[27, 91, 53, 126], jkPageUp) defEscSeq(@[27, 91, 54, 126], jkPageDown) # alternative home/end for tty defEscSeq(@[27, 91, 49, 126], jkHome) defEscSeq(@[27, 91, 52], jkContinue) defEscSeq(@[27, 91, 52, 126], jkEnd) # urxvt defEscSeq(@[27, 91, 55], jkContinue) defEscSeq(@[27, 91, 56], jkContinue) defEscSeq(@[27, 91, 55, 126], jkHome) defEscSeq(@[27, 91, 56, 126], jkEnd) # ctrl + fancy keys like pgup, pgdown, home, end defEscSeq(@[27, 91, 53, 59], jkContinue) defEscSeq(@[27, 91, 53, 59, 53], jkContinue) defEscSeq(@[27, 91, 53, 59, 53, 126], jkCtrlPageUp) defEscSeq(@[27, 91, 54, 59], jkContinue) defEscSeq(@[27, 91, 54, 59, 53], jkContinue) defEscSeq(@[27, 91, 54, 59, 53, 126], jkCtrlPageDown) defEscSeq(@[27, 91, 49, 59, 53, 72], jkCtrlHome) defEscSeq(@[27, 91, 49, 59, 53, 70], jkCtrlEnd) # urxvt defEscSeq(@[27, 91, 53, 94], jkCtrlPageUp) defEscSeq(@[27, 91, 54, 94], jkCtrlPageDown) defEscSeq(@[27, 91, 55, 94], jkCtrlHome) defEscSeq(@[27, 91, 56, 94], jkCtrlEnd) # other keys defEscSeq(@[27, 91, 51], jkContinue) defEscSeq(@[27, 91, 50], jkContinue) defEscSeq(@[27, 91, 51, 126], jkDelete) defEscSeq(@[27, 91, 50, 126], jkInsert) type IsKeyUTF8Results = enum ikValid, ikUnfinished, ikOverlong, ikInvalid2, ikInvalid3, ikInvalid4, ikInvalidUnknown func isContinuation(singleByte: uint8): bool = singleByte shr 6 == 0b10 func isValidUTF8(key: int): IsKeyUTF8Results = const L = sizeof(int) var bytes: array[L, uint8] var key = key var i = L - 1 # to not make any assumptions about how int is # stored in memory while key > 0: bytes[i] = (key mod 256).uint8 key = key div 256 dec i # https://github.com/nim-lang/Nim/blob/version-1-6/lib/pure/unicode.nim#L166 i = 0 while i < L: if bytes[i] <= 127: inc i elif bytes[i] shr 5 == 0b110: # 2 long if bytes[i] < 0xc2: return ikOverlong # overlong elif i + 1 >= L: return ikUnfinished elif bytes[i+1].isContinuation: i += 2 else: return ikInvalid2 elif bytes[i] shr 4 == 0b1110: # 3 long if i + 2 >= L: return ikUnfinished elif bytes[i+1].isContinuation() and bytes[i+2].isContinuation(): i += 3 else: return ikInvalid3 elif bytes[i] shr 3 == 0b11110: # 4 long if i+3 >= L: return ikUnfinished elif bytes[i+1].isContinuation() and bytes[i+2].isContinuation() and bytes[i+3].isContinuation(): i += 4 else: return ikInvalid4 else: return ikInvalidUnknown return ikValid type GetKeyResult* = enum gkControl, gkChar proc getKey*: (GetKeyResult, int) = # TODO move away from int approach completely var key = 0 var bytes: string while true: key *= 256 let newChar = getch() key += newChar.int() bytes &= newChar if escapeSeqs.hasKey(key): if escapeSeqs[key] != jkContinue: key = escapeSeqs[key].int() return (gkControl, key) elif key < 30 and key > 0: # JaleKeycode has values from 1 to 29 # which are NOT in escapeSeqs to save typing return (gkControl, key) else: let validity = isValidUTF8(key) case validity: of ikValid: # TODO: find a better alternative to runeAt return (gkChar, bytes.runeAt(0).int) of ikUnfinished: discard # continue looping else: # For now, in development builds, crash # later it might be wise to just ignore # bad bytes, might check what other line # editors do raise newException(ValueError, "Invalid UTF8 input, " & $validity)