diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96236f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +example \ No newline at end of file diff --git a/editor.nim b/editor.nim new file mode 100644 index 0000000..3540f14 --- /dev/null +++ b/editor.nim @@ -0,0 +1,102 @@ +import unicode +import strutils +import terminal +import strformat +import posix + +import keycodes + +type + EditorState* = ref object + # config + prompt: string # prompt to display + maxRowsGoal: int # if terminal resizes, how to compute max rows + # positive: it will set max rows to maxRowsGoal, or terminal height, whichever is lower + # negative: it will set it to terminal height - maxRowsGoal or terminal height, whichever is lower + # 0: terminal height + + # state reached during execution + x: int # current cursor x position on screen + bx: int # current x position in buffer (not same as x due to UTF-8) + y: int # which row are we editing + size: int # size of the row being edited in characters + buffer: seq[string] # current text + history: seq[seq[string]] # past texts + historyIndex: int # where are we in history + cols: int # number of columns in the terminal + maxRows: int # max number of rows allowed + +# for editors +var editors: seq[EditorState] + +# resize support +onSignal(28): + discard + +proc newEditor*(prompt: string = "> ", multiline: bool = true): EditorState = + new(result) + result.prompt = prompt + result.maxRowsGoal = if multiline: 0 else: 1 + result.buffer.add("") + editors.add(result) + +proc destroyEditor*(oldEditor: EditorState) = + for i in 0..editors.high(): + let ed = editors[i] + if ed == oldEditor: + editors.del(i) + return + +proc render(ed: EditorState) = + eraseLine() + setCursorXPos(0) + write stdout, ed.prompt + write stdout, ed.buffer[ed.y] + +template cline(ed: EditorState): var string = + ed.buffer[ed.y] + +proc read*(ed: EditorState): string = + ed.render() + while true: + let key = getKey() + case key: + of jkEnter.int: + break + of jkBackspace.int: + if ed.x == 0: + discard # merge two lines TODO + elif ed.x > ed.cline().high(): + ed.cline() = ed.cline[0..ed.cline().high()-1] + dec ed.x + dec ed.bx + else: + # TODO UTF8 + ed.cline() = ed.cline[0..ed.x-1] & ed.cline[ed.x+1..ed.cline().high()] + dec ed.x + dec ed.bx + else: + if key > 31 and key < 127: + # ascii char + let ch = key.char() + if ed.x == 0: + ed.cline() = $ch & ed.cline + elif ed.x > ed.cline.high(): + ed.cline() = ed.cline & $ch + else: + ed.cline() = ed.cline[0..ed.x-1] & $ch & ed.cline[ed.x..ed.cline().high()] + inc ed.x + inc ed.bx + + ed.render() + + # return val + result = ed.buffer.join("\n") + + # cleanup + write(stdout, "\n") + ed.history.add(ed.buffer) + ed.buffer = @[""] + ed.y = 0 + ed.x = 0 + \ No newline at end of file diff --git a/example.nim b/example.nim new file mode 100644 index 0000000..ddfc2f4 --- /dev/null +++ b/example.nim @@ -0,0 +1,9 @@ +import editor + +let e = newEditor("> ", false) + +while true: + let text = e.read() + if text == "quit": + break + echo text \ No newline at end of file diff --git a/keycodes.nim b/keycodes.nim new file mode 100644 index 0000000..f1477e6 --- /dev/null +++ b/keycodes.nim @@ -0,0 +1,176 @@ +# keycodes.nim + +import tables +import strutils +import terminal + +type + JaleKeycode* = enum + jkStart = 255 # jale keycodes exported start one above jkStart + # arrow keys + jkLeft = 256, jkRight, jkUp, jkDown, + jkCtrlLeft, jkCtrlRight, jkCtrlUp, jkCtrlDown, + # other 4 move keys + jkHome, jkEnd, jkPageUp, jkPageDown, + jkCtrlHome, jkCtrlEnd, jkCtrlPageUp, jkCtrlPageDown, + # special keys + jkDelete, jkBackspace, jkInsert, jkEnter, + + jkFinish, # jale keycodes exported end one below jkFinish + # non-exported jale keycodes come here: + jkContinue # represents an unfinished escape sequence + +var keysById*: Table[int, string] +var keysByName*: Table[string, int] + +block: + # 1 - 26 + for i in countup(1, 26): # iterate through lowercase letters + keysById[i] = "ctrl+" & $char(i + 96) + + keysById[9] = "tab" + # keysById[8] will never get triggered because it's an escape seq + + keysById[28] = r"ctrl+\" + keysById[29] = "ctrl+]" + keysByName[r"ctrl+\"] = 28 + keysByName["ctrl+]"] = 29 + + for i in countup(1, 26): + keysByName[keysById[i]] = i + + # jale keycodes: + for i in countup(int(jkStart) + 1, int(jkFinish) - 1): + var name: string = ($JaleKeycode(i)) + name = name[2..name.high()].toLower() + keysByName[name] = i + keysById[i] = name + +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: + 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(@[8], jkBackspace) + defEscSeq(@[13], jkEnter) + 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) + defEscSeq(@[13], jkEnter) + defEscSeq(@[127], jkBackspace) + defEscSeq(@[8], jkBackspace) + + +proc getChar: int = + getch().int + +proc getKey*: int = + var key = 0 + while true: + key *= 256 + key += getChar() + if escapeSeqs.hasKey(key): + if escapeSeqs[key] != jkContinue: + key = escapeSeqs[key].int() + break + else: + break + return key