initial commit

This commit is contained in:
Productive2 2021-02-15 20:18:31 +01:00
commit 2358434bae
11 changed files with 511 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
main

52
defaults.nim Normal file
View File

@ -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()

115
editor.nim Normal file
View File

@ -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()

17
event.nim Normal file
View File

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

85
keycodes.nim Normal file
View File

@ -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

40
line.nim Normal file
View File

@ -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

32
main.nim Normal file
View File

@ -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 "==="

107
multiline.nim Normal file
View File

@ -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

21
renderer.nim Normal file
View File

@ -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

21
templates.nim Normal file
View File

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

20
todo.md Normal file
View File

@ -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: