jal3/terminalUtils/keycodes.nim

228 lines
6.7 KiB
Nim

# 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)