initial commit
This commit is contained in:
commit
2358434bae
|
@ -0,0 +1 @@
|
|||
main
|
|
@ -0,0 +1,52 @@
|
|||
import editor
|
||||
import keycodes
|
||||
import multiline
|
||||
import event
|
||||
import tables
|
||||
import templates
|
||||
|
||||
# default populate
|
||||
proc defInsert(editor: LineEditor) =
|
||||
if editor.lastKeystroke > 31 and editor.lastKeystroke < 127:
|
||||
let ch = char(editor.lastKeystroke)
|
||||
editor.content.insert($ch)
|
||||
|
||||
proc defControl(editor: LineEditor) =
|
||||
block control:
|
||||
template check(key: string, blk: untyped) =
|
||||
if editor.lastKeystroke == keysByName[key]:
|
||||
blk
|
||||
break control
|
||||
check("left"):
|
||||
editor.content.left()
|
||||
check("right"):
|
||||
editor.content.right()
|
||||
check("up"):
|
||||
editor.content.up()
|
||||
check("down"):
|
||||
editor.content.down()
|
||||
check("backspace"):
|
||||
editor.content.backspace()
|
||||
check("delete"):
|
||||
editor.content.delete()
|
||||
check("enter"):
|
||||
if editor.content.Y() == editor.content.high() and editor.content.getLine(editor.content.high()) == "":
|
||||
editor.finished = true
|
||||
else:
|
||||
editor.content.insertline()
|
||||
check("ctrl+c"):
|
||||
editor.finished = true
|
||||
editor.events.call(jeQuit)
|
||||
check("ctrl+d"):
|
||||
if editor.content.getContent() == "":
|
||||
editor.finished = true
|
||||
editor.events.call(jeQuit)
|
||||
|
||||
proc defLog(editor: LineEditor) =
|
||||
echo editor.lastKeystroke
|
||||
|
||||
proc populateDefaults*(editor: LineEditor) =
|
||||
editor.bindEvent(jeKeypress):
|
||||
editor.defInsert()
|
||||
editor.defControl()
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import strformat
|
||||
import strutils
|
||||
import tables
|
||||
import terminal
|
||||
|
||||
import line
|
||||
import multiline
|
||||
import keycodes
|
||||
import event
|
||||
import renderer
|
||||
|
||||
type
|
||||
JaleEvent* = enum
|
||||
jeKeypress, jeQuit
|
||||
|
||||
LineEditor* = ref object
|
||||
content*: Multiline
|
||||
historyIndex*: int
|
||||
history: seq[Multiline]
|
||||
keystrokes*: Event[int]
|
||||
events*: Event[JaleEvent]
|
||||
prompt*: string
|
||||
lastKeystroke*: int
|
||||
finished*: bool
|
||||
rendered: int # how many lines were printed last full refresh
|
||||
|
||||
proc newLineEditor*: LineEditor =
|
||||
new(result)
|
||||
result.content = newMultiline()
|
||||
result.history = @[]
|
||||
result.history.add(result.content)
|
||||
result.historyIndex = 0
|
||||
result.keystrokes.new()
|
||||
result.events.new()
|
||||
result.prompt = ""
|
||||
result.rendered = 0
|
||||
|
||||
proc render(editor: LineEditor, line: int = -1, hscroll: bool = true) =
|
||||
var y = line
|
||||
if y == -1:
|
||||
y = editor.content.Y
|
||||
|
||||
renderLine(
|
||||
(
|
||||
if y == 0:
|
||||
editor.prompt
|
||||
else:
|
||||
" ".repeat(editor.prompt.len())
|
||||
),
|
||||
editor.content.getLine(y),
|
||||
0
|
||||
)
|
||||
|
||||
|
||||
proc fullRender(editor: LineEditor) =
|
||||
# from the current (proper) cursor pos, it draws the entire multiline prompt, then
|
||||
# moves cursor to current x,y
|
||||
for i in countup(0, editor.content.high()):
|
||||
editor.render(i, false)
|
||||
# if i <= editor.rendered:
|
||||
cursorDown(1)
|
||||
# else:
|
||||
# write stdout, "\n"
|
||||
# inc editor.rendered
|
||||
|
||||
proc restore(editor: LineEditor) =
|
||||
# from the line that's represented as y=0 it moves the cursor to editor.y
|
||||
# if it's at the bottom, it also scrolls enough
|
||||
# it's achieved by the right number of newlines
|
||||
# does not restore editor.x
|
||||
|
||||
write stdout, "\n".repeat(editor.content.len())
|
||||
cursorUp(editor.content.len())
|
||||
editor.rendered = editor.content.len()
|
||||
|
||||
if editor.content.Y == 0:
|
||||
return
|
||||
cursorDown(editor.content.Y)
|
||||
|
||||
proc moveCursorToEnd(editor: LineEditor) =
|
||||
# only called when read finished
|
||||
cursorDown(editor.content.high() - editor.content.Y)
|
||||
write stdout, "\n"
|
||||
|
||||
proc moveCursorToStart(editor: LineEditor, delta: int = 0) =
|
||||
if delta > 0:
|
||||
cursorUp(delta)
|
||||
|
||||
proc restoreCursor(editor: LineEditor) =
|
||||
if editor.content.Y > 0:
|
||||
cursorDown(editor.content.Y)
|
||||
|
||||
proc read*(editor: LineEditor): string =
|
||||
# write stdout, "\n"
|
||||
editor.restore()
|
||||
editor.fullRender()
|
||||
editor.restoreCursor()
|
||||
while not editor.finished:
|
||||
editor.render()
|
||||
setCursorXPos(editor.content.X + editor.prompt.len())
|
||||
let key = getKey()
|
||||
let preY = editor.content.Y
|
||||
editor.lastKeystroke = key
|
||||
editor.keystrokes.call(key)
|
||||
editor.events.call(jeKeypress)
|
||||
if preY != editor.content.Y:
|
||||
# redraw everything because y changed
|
||||
editor.moveCursorToStart(preY)
|
||||
editor.fullRender()
|
||||
editor.restoreCursor()
|
||||
|
||||
editor.finished = false
|
||||
editor.moveCursorToEnd()
|
||||
|
||||
return editor.content.getContent()
|
|
@ -0,0 +1,17 @@
|
|||
# event.nim
|
||||
|
||||
import tables
|
||||
|
||||
type
|
||||
Event*[T] = TableRef[T, seq[proc ()]]
|
||||
|
||||
proc call*[T](evt: Event[T], key: T) =
|
||||
if evt.hasKey(key):
|
||||
for callback in evt[key]:
|
||||
callback()
|
||||
|
||||
proc add*[T](event: Event[T], key: T, callback: proc) =
|
||||
if not event.hasKey(key):
|
||||
event[key] = @[]
|
||||
event[key].add(callback)
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
# keycodes.nim
|
||||
|
||||
import tables
|
||||
import strutils
|
||||
import terminal
|
||||
|
||||
type
|
||||
JaleKeycode* = enum
|
||||
jkStart = 255 # jale keycodes exported start one above jkStart
|
||||
jkLeft = 256, jkRight, jkUp, jkDown, jkHome, jkEnd, 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"
|
||||
|
||||
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
|
||||
escapeSeqs[result] = id
|
||||
|
||||
block:
|
||||
when defined(windows):
|
||||
defEscSeq(@[224], jkContinue)
|
||||
defEscSeq(@[224, 72], jkUp)
|
||||
defEscSeq(@[224, 80], jkDown)
|
||||
defEscSeq(@[224, 77], jkRight)
|
||||
defEscSeq(@[224, 75], jkLeft)
|
||||
defEscSeq(@[224, 71], jkHome)
|
||||
defEscSeq(@[224, 79], jkEnd)
|
||||
defEscSeq(@[224, 82], jkInsert)
|
||||
defEscSeq(@[224, 83], jkDelete)
|
||||
# TODO: finish defining escape sequences
|
||||
else:
|
||||
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)
|
||||
defEscSeq(@[27, 91, 72], jkHome)
|
||||
defEscSeq(@[27, 91, 70], jkEnd)
|
||||
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)
|
||||
|
||||
proc getKey*: int =
|
||||
var key: int = 0
|
||||
while true:
|
||||
key *= 256
|
||||
key += int(getch())
|
||||
if escapeSeqs.hasKey(key):
|
||||
if escapeSeqs[key] != jkContinue:
|
||||
key = int(escapeSeqs[key])
|
||||
break
|
||||
else:
|
||||
break
|
||||
return key
|
|
@ -0,0 +1,40 @@
|
|||
# line.nim
|
||||
|
||||
import strformat
|
||||
|
||||
type
|
||||
Line* = ref object
|
||||
content: string
|
||||
|
||||
# getters/setters
|
||||
|
||||
proc content*(l: Line): string =
|
||||
l.content
|
||||
|
||||
proc `content=`*(l: Line, str: string) =
|
||||
l.content = str
|
||||
|
||||
# constructor
|
||||
|
||||
proc newLine*: Line =
|
||||
Line(content: "")
|
||||
|
||||
# methods
|
||||
|
||||
proc insert*(line: Line, str: string, pos: int) =
|
||||
if pos > line.content.high():
|
||||
line.content &= str
|
||||
elif pos == 0:
|
||||
line.content = str & line.content
|
||||
else:
|
||||
line.content = line.content[0..pos-1] & str & line.content[pos..line.content.high()]
|
||||
|
||||
proc delete*(line: Line, start: int, finish: int) =
|
||||
if start > finish or start < 0 or finish > line.content.high():
|
||||
raise newException(CatchableError, &"Invalid arguments for Line.delete: start {start}, finish {finish} for line of length {line.content.len()}")
|
||||
var result = ""
|
||||
if start > 0:
|
||||
result &= line.content[0..start-1]
|
||||
if finish < line.content.high():
|
||||
result &= line.content[finish+1..line.content.high()]
|
||||
line.content = result
|
|
@ -0,0 +1,32 @@
|
|||
import defaults
|
||||
import tables
|
||||
import editor
|
||||
import strutils
|
||||
import templates
|
||||
import multiline
|
||||
import event
|
||||
|
||||
var keep = true
|
||||
|
||||
let e = newLineEditor()
|
||||
|
||||
e.bindEvent(jeQuit):
|
||||
keep = false
|
||||
|
||||
e.bindKey('a'):
|
||||
echo "a has been pressed"
|
||||
|
||||
e.bindKey("ctrl+b"):
|
||||
echo "ctrl+b has been pressed"
|
||||
|
||||
e.prompt = "> "
|
||||
e.populateDefaults()
|
||||
while keep:
|
||||
let input = e.read()
|
||||
if input.contains("quit"):
|
||||
break
|
||||
else:
|
||||
echo "==="
|
||||
echo input
|
||||
echo "==="
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
# multiline.nim
|
||||
|
||||
import line
|
||||
|
||||
type
|
||||
Multiline* = ref object
|
||||
lines: seq[Line]
|
||||
x,y: int
|
||||
|
||||
# getters/setters
|
||||
|
||||
proc X*(ml: Multiline): int =
|
||||
ml.x
|
||||
|
||||
proc Y*(ml: Multiline): int =
|
||||
ml.y
|
||||
|
||||
# constructor
|
||||
|
||||
proc newMultiline*: Multiline =
|
||||
new(result)
|
||||
result.lines = @[]
|
||||
result.lines.add(newLine())
|
||||
result.x = 0
|
||||
result.y = 0
|
||||
|
||||
# methods
|
||||
|
||||
proc lineLen*(ml: Multiline): int =
|
||||
ml.lines[ml.y].content.len()
|
||||
|
||||
proc lineHigh*(ml: Multiline): int =
|
||||
ml.lineLen() - 1
|
||||
|
||||
proc len*(ml: Multiline): int =
|
||||
ml.lines.len()
|
||||
|
||||
proc high*(ml: Multiline): int =
|
||||
ml.lines.high()
|
||||
|
||||
proc left*(ml: Multiline) =
|
||||
if ml.x > 0:
|
||||
dec ml.x
|
||||
|
||||
proc right*(ml: Multiline) =
|
||||
if ml.x < ml.lineLen():
|
||||
inc ml.x
|
||||
|
||||
proc up*(ml: Multiline) =
|
||||
if ml.y > 0:
|
||||
dec ml.y
|
||||
if ml.x > ml.lineLen():
|
||||
ml.x = ml.lineLen()
|
||||
|
||||
proc down*(ml: Multiline) =
|
||||
if ml.y < ml.lines.high():
|
||||
inc ml.y
|
||||
if ml.x > ml.lineLen():
|
||||
ml.x = ml.lineLen()
|
||||
|
||||
proc insert*(ml: Multiline, str: string) =
|
||||
ml.lines[ml.y].insert(str, ml.x)
|
||||
ml.x += str.len()
|
||||
|
||||
proc delete*(ml: Multiline) =
|
||||
if ml.x < ml.lineLen():
|
||||
ml.lines[ml.y].delete(ml.x, ml.x)
|
||||
|
||||
proc backspace*(ml: Multiline) =
|
||||
if ml.x > 0:
|
||||
ml.lines[ml.y].delete(ml.x - 1, ml.x - 1)
|
||||
dec ml.x
|
||||
|
||||
proc insertline*(ml: Multiline) =
|
||||
# TODO split line support
|
||||
if ml.y == ml.lines.high():
|
||||
ml.lines.add(newLine())
|
||||
else:
|
||||
ml.lines.insert(newLine(), ml.y + 1)
|
||||
inc ml.y
|
||||
ml.x = 0
|
||||
|
||||
|
||||
proc clearline*(ml: Multiline) =
|
||||
ml.lines[ml.y].content = ""
|
||||
|
||||
proc removeline*(ml: Multiline) =
|
||||
ml.lines.delete(ml.y)
|
||||
if ml.lines.len() == 0:
|
||||
ml.lines.add(newLine())
|
||||
if ml.y > ml.lines.high():
|
||||
dec ml.y
|
||||
|
||||
proc getLine*(ml: Multiline, line: int = -1): string =
|
||||
if line == -1:
|
||||
ml.lines[ml.y].content
|
||||
else:
|
||||
if line >= 0 and line <= ml.lines.high():
|
||||
ml.lines[line].content
|
||||
else:
|
||||
""
|
||||
|
||||
proc getContent*(ml: Multiline): string =
|
||||
for line in ml.lines:
|
||||
result &= line.content & "\n"
|
||||
result[0..result.high()-1] # cut finishing newline
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# renderer.nim
|
||||
#
|
||||
# a terminal renderer for readline-like libraries
|
||||
|
||||
import terminal
|
||||
import strutils
|
||||
|
||||
proc renderLine*(prompt: string, content: string, hscroll: int = 0) =
|
||||
var content = prompt & content
|
||||
if content.len() < terminalWidth():
|
||||
content &= " ".repeat(terminalWidth() - content.len())
|
||||
if content.len() > terminalWidth():
|
||||
var lower = hscroll
|
||||
var upper = hscroll + terminalWidth() - 1
|
||||
if upper > content.high():
|
||||
upper = content.high()
|
||||
content = content[lower..upper]
|
||||
write stdout, "\r" & content
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import editor
|
||||
import event
|
||||
import keycodes
|
||||
|
||||
template bindKey*(editor: LineEditor, key: int, body: untyped) =
|
||||
proc action {.gensym.} =
|
||||
body
|
||||
editor.keystrokes.add(key, action)
|
||||
|
||||
template bindKey*(editor: LineEditor, key: char, body: untyped) =
|
||||
editor.bindKey(int(key)):
|
||||
body
|
||||
|
||||
template bindKey*(editor: LineEditor, key: string, body: untyped) =
|
||||
editor.bindKey(keysByName[key]):
|
||||
body
|
||||
|
||||
template bindEvent*(editor: LineEditor, event: JaleEvent, body: untyped) =
|
||||
proc action {.gensym.} =
|
||||
body
|
||||
editor.events.add(event, action)
|
|
@ -0,0 +1,20 @@
|
|||
Soon:
|
||||
|
||||
Horizontal scrolling for the current line in render
|
||||
Add new keycodes (enter, ctrl+h, ctrl+j?)
|
||||
Add a way to hook for the quit event (ctrl+c or ctrl+d auto triggers in default)
|
||||
Move arrow keys to per-key events/create template that hooks by name
|
||||
create template that hooks any key
|
||||
|
||||
Multiline editing:
|
||||
|
||||
- when moving up/down render should re-render the line to reset horizontal scrolling
|
||||
|
||||
Other stuff:
|
||||
|
||||
- move codebase into multiple modules to ensure modularity (move away defaults and anything that can be submodules (keycodes) e.g.)
|
||||
- tab completion into the defaults
|
||||
- new events such as pre-terminate, pre-return, pre-render (filter)
|
||||
|
||||
|
||||
Done:
|
Loading…
Reference in New Issue