commit
d9ed102638
28 changed files with 1601 additions and 0 deletions
@ -0,0 +1,40 @@ |
|||
--- |
|||
name: Bug report |
|||
about: Create a report to help us improve |
|||
title: '' |
|||
labels: bug |
|||
assignees: '' |
|||
|
|||
--- |
|||
|
|||
**Describe the bug** |
|||
|
|||
A clear and concise description of what the bug is. |
|||
|
|||
**To Reproduce** |
|||
|
|||
Library bug? |
|||
|
|||
Please attach code to reproduce. |
|||
|
|||
Bug is an example? |
|||
|
|||
1. In example '...' |
|||
2. Do '...' |
|||
3. Expect '...' to happen but |
|||
4. '...' happens |
|||
|
|||
**Expected behavior** |
|||
A clear and concise description of what you expected to happen. |
|||
|
|||
**Screenshots** |
|||
If applicable, add screenshots to help explain your problem. |
|||
|
|||
**Desktop (please complete the following information):** |
|||
- OS: [e.g. iOS] |
|||
- Jale version (github commit hash) |
|||
- Terminal emulator: [e.g. xterm or cmd.exe] |
|||
- Possible interfering conditions [e.g. it's over ssh or inside tmux] |
|||
|
|||
**Additional context** |
|||
Add any other context about the problem here. |
@ -0,0 +1,20 @@ |
|||
--- |
|||
name: Feature request |
|||
about: Suggest an idea for this project |
|||
title: '' |
|||
labels: enhancement |
|||
assignees: '' |
|||
|
|||
--- |
|||
|
|||
**Is your feature request related to a problem? Please describe.** |
|||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] |
|||
|
|||
**Describe the solution you'd like** |
|||
A clear and concise description of what you want to happen. |
|||
|
|||
**Describe alternatives you've considered** |
|||
A clear and concise description of any alternative solutions or features you've considered. |
|||
|
|||
**Additional context** |
|||
Add any other context or screenshots about the feature request here. |
@ -0,0 +1,9 @@ |
|||
examples/editor |
|||
examples/interactive_basic |
|||
examples/interactive_history |
|||
examples/getkey |
|||
|
|||
# Ignore all |
|||
tests/* |
|||
# Unignore all with extensions |
|||
!tests/*.* |
@ -0,0 +1,21 @@ |
|||
MIT License |
|||
|
|||
Copyright (c) 2021 Productive2 |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
@ -0,0 +1,25 @@ |
|||
# Sources of inspiration |
|||
|
|||
https://github.com/h3rald/nimline |
|||
|
|||
MIT License |
|||
|
|||
Copyright (c) 2017 Fabio Cevasco |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
@ -0,0 +1,61 @@ |
|||
# Just Another Line Editor |
|||
|
|||
or jale.nim |
|||
|
|||
# Note |
|||
|
|||
This is a new (and very immature) alternative to other |
|||
line editors such as linenoise (see rdstdin in the nim |
|||
standard library) or nimline (https://github.com/h3rald/nimline). Currently you probably want to use either of |
|||
those because this is a wip. |
|||
|
|||
# Installation |
|||
|
|||
``` |
|||
git clone https://github.com/japl-lang/jale |
|||
cd jale |
|||
nimble install |
|||
``` |
|||
|
|||
# Checking the examples out |
|||
|
|||
Building the examples |
|||
|
|||
``` |
|||
nimble examples |
|||
``` |
|||
|
|||
Checking the sample editor out. Quit with ctrl+c, save with ctrl+s. |
|||
|
|||
``` |
|||
examples/editor <filename> |
|||
# or windows: |
|||
.\examples\editor.exe <filename> |
|||
``` |
|||
|
|||
Checking the interactive prompt out. Move between lines using ctrl+up/down. Create new lines with ctrl+down on the last line. ctrl+page up/down also works. |
|||
|
|||
``` |
|||
examples/interactive_history |
|||
# or windows: |
|||
.\examples\interactive_history.exe |
|||
``` |
|||
|
|||
# Features |
|||
|
|||
- multiline support |
|||
- easily add new keybindings (using templates) |
|||
- very customizable (even inserting characters is a keybinding that's optional) |
|||
- plugin system based |
|||
- history |
|||
- horizontal scrolling |
|||
|
|||
# Missing features |
|||
|
|||
Note: they won't be missing forever hopefully. |
|||
|
|||
- No utf-8 |
|||
- No tab autocompletion support |
|||
- No syntax highlighting support |
|||
- Windows output still really unstable/untested in depth |
|||
|
@ -0,0 +1,33 @@ |
|||
import jale/editor |
|||
import jale/types/multiline |
|||
import jale/defaults |
|||
import jale/types/event |
|||
import jale/keycodes |
|||
import tables |
|||
|
|||
import terminal |
|||
import strutils |
|||
import os |
|||
|
|||
eraseScreen() |
|||
setCursorPos(stdout, 0,0) |
|||
|
|||
let e = newMultilineEditor() |
|||
|
|||
if paramCount() > 0: |
|||
let arg = paramStr(1) |
|||
if fileExists(arg): |
|||
e.content = readFile(arg).fromString() |
|||
|
|||
var save = false |
|||
e.evtTable.subscribe(jeKeypress): |
|||
if args[0].intVal == keysByName["ctrl+s"]: |
|||
e.finish() |
|||
save = true |
|||
|
|||
|
|||
e.horizontalScrollMode = hsbAllScroll |
|||
let result = e.read() |
|||
if save and paramCount() > 0: |
|||
writeFile(paramStr(1), result) |
|||
|
@ -0,0 +1,28 @@ |
|||
# handy tool that prints out char codes when pressed |
|||
|
|||
import jale/uniterm |
|||
import jale/keycodes |
|||
import strutils |
|||
import terminal |
|||
import os |
|||
import tables |
|||
|
|||
echo "Press 'ctrl+c' to quit" |
|||
echo "Press 'ctrl+a' to print a horizontal bar" |
|||
|
|||
var escape = false |
|||
if paramCount() > 0 and paramStr(1) == "esc": |
|||
escape = true |
|||
|
|||
while true: |
|||
|
|||
let key = if escape: getKey() else: int(uniGetchr()) |
|||
if key == 3: |
|||
break |
|||
if key == 1: |
|||
echo "=".repeat(terminalWidth()) |
|||
else: |
|||
if keysById.hasKey(key): |
|||
echo keysById[key] |
|||
else: |
|||
echo key |
@ -0,0 +1,17 @@ |
|||
import jale/defaults |
|||
import jale/editor |
|||
import jale/renderer |
|||
import jale/types/event |
|||
|
|||
import strutils |
|||
|
|||
var keep = true |
|||
let e = newSimpleLineEditor() |
|||
|
|||
e.evtTable.subscribe(jeQuit): |
|||
keep = false |
|||
|
|||
e.prompt = "> " |
|||
while keep: |
|||
let input = e.read() |
|||
echo "output:<" & input.replace("\n", "\\n") & ">" |
@ -0,0 +1,19 @@ |
|||
import jale/editor |
|||
import jale/types/event |
|||
import jale/defaults |
|||
|
|||
import strutils |
|||
|
|||
var keep = true |
|||
|
|||
let e = newHistoryEditor() |
|||
|
|||
e.evtTable.subscribe(jeQuit): |
|||
keep = false |
|||
|
|||
e.prompt = "> " |
|||
|
|||
while keep: |
|||
let input = e.read() |
|||
echo "output:<" & input.replace("\n", "\\n") & ">" |
|||
|
@ -0,0 +1,22 @@ |
|||
# Package |
|||
|
|||
version = "0.2" |
|||
author = "Productive2" |
|||
description = "Just Another Line Editor" |
|||
license = "MIT" |
|||
srcDir = "src" |
|||
|
|||
# Dependencies |
|||
|
|||
requires "nim >= 1.0.0" |
|||
|
|||
import os |
|||
task examples, "Builds examples": |
|||
for kind, path in walkDir("examples/"): |
|||
if path.splitFile().ext == ".nim": |
|||
let (oup, exitcode) = gorgeEx "nim c " & path |
|||
if exitcode != 0: |
|||
echo "Failed building example " & path |
|||
echo oup |
|||
else: |
|||
echo "Successfully built example " & path |
@ -0,0 +1,85 @@ |
|||
|
|||
# Platforms |
|||
|
|||
Reading vt100 docs/relying on nim's terminal is not enough, it's best to |
|||
test everything out there. Different terminal emulators, a few different OS'es |
|||
or distros and also windows specific emulators. |
|||
|
|||
Unless otherwise specified, everything falls back to Artix linux (basically the same versions of software as arch), x86_64, nim 1.4.2. |
|||
|
|||
| Terminal | Last tested and worked | |
|||
| :-------- | :------------ | |
|||
| xfce terminal | passed on 26.2.2021 | |
|||
| konsole | passed on 26.2.2021 | |
|||
| alacritty | passed on 26.2.2021 | |
|||
| xterm | passed on 26.2.2021 | |
|||
| termite | passed on 26.2.2021 | |
|||
| urxvt | passed on 26.2.2021 | |
|||
| win 10 + cmd.exe | 26.2.2021: see notes | |
|||
| win 10 + powershell | 26.2.2021: see notes | |
|||
| xfce terminal + ssh | passed on 26.2.2021 | |
|||
| xfce terminal + tmux | 26.2.2021: see notes | |
|||
| tty | 26.2.2021: see notes | |
|||
| freebsd xterm | passed on 19.2.2021 | |
|||
| freebsd xfce term | passed on 19.2.2021 | |
|||
| debian xterm | passed on 19.2.2021 | |
|||
| debian qterminal | passed on 19.2.2021 | |
|||
| debian kitty | passed on 19.2.2021 | |
|||
| nim 1.0.0 | passed on 26.2.2021 | |
|||
|
|||
Info about testing dates: |
|||
|
|||
| Testing date | commit | |
|||
| :----------- | :-------- | |
|||
| 19.2.2021 | 12c7c28714508e7a1c16bcd7b3fa1372c4a19ae2 | |
|||
| 26.2.2021 | d4d2f52ec13a3c5cfea2cdce2d09777317de3545 | |
|||
|
|||
## Notes on 26.2.2021 |
|||
|
|||
### Found issues |
|||
|
|||
- (minor, doesn't affect repls) examples/editor does not scroll to the |
|||
end right after opening when opening a file too large to fit in the screen. |
|||
|
|||
- (minor) examples/interactive_history when a history element is taller than the screen, |
|||
it can cause issues with the rendering of the next history element when scrolling |
|||
through history |
|||
|
|||
### tmux notes |
|||
|
|||
- ctrl+pageup and ctrl+page down do not create any input (not even for getch) |
|||
- otherwise pass |
|||
|
|||
### tty notes |
|||
|
|||
- ctrl+(arrow keys) does not create a distinct key |
|||
|
|||
### powershell notes |
|||
|
|||
- editor issues: |
|||
- on horizontal scroll conditions the line can overflow causing rendering bugs |
|||
- on vertical scroll + page up the first line could disappear (maybe only when first line is a horizontal scroll candidate) |
|||
- very slow experience, a lot of cursor jumping |
|||
|
|||
### cmd.exe notes |
|||
|
|||
- same issues as powershell |
|||
|
|||
# Testing procedure |
|||
|
|||
Platform |
|||
- [ ] Jale compiles? |
|||
examples/interactive_basic |
|||
- [ ] Entering single line input, backspace, delete |
|||
- [ ] entering new lines, deleting lines with backspace |
|||
- [ ] home/end/page up/page down |
|||
- [ ] Submitting output |
|||
examples/interactive_history |
|||
- [ ] Multiple multiline history events |
|||
examples/editor |
|||
- [ ] Clears the screen well |
|||
- [ ] Writing small files |
|||
- [ ] Reading small files |
|||
- [ ] horizontal scroll |
|||
- [ ] vertical scroll |
|||
|
@ -0,0 +1 @@ |
|||
const jaleVersion = "0.2.0" |
@ -0,0 +1,84 @@ |
|||
# defaults.nim |
|||
|
|||
# creates a LineEditor and binds a terminal renderer and many default keys |
|||
|
|||
import editor |
|||
import keycodes |
|||
import types/multiline |
|||
import types/event |
|||
import tables |
|||
import renderer |
|||
import editor_history |
|||
|
|||
proc defaultBindings*(le: LineEditor, enterSubmits = true, ctrlForVerticalMove = true) = |
|||
|
|||
le.evtTable.subscribe(jeKeypress): |
|||
let key = args[0].intVal |
|||
|
|||
template bindKey(k: int, body: untyped) = |
|||
if key == k: |
|||
body |
|||
|
|||
template bindKey(k: string, body: untyped) = |
|||
if key == keysByName[k]: |
|||
body |
|||
|
|||
if key > 31 and key < 127: |
|||
let ch = char(key) |
|||
le.content.insert($ch) |
|||
|
|||
bindKey("ctrl+c"): |
|||
le.quit() |
|||
|
|||
bindKey("ctrl+d"): |
|||
if le.content.getContent() == "": |
|||
le.quit() |
|||
|
|||
bindKey("left"): |
|||
le.content.left() |
|||
bindKey("right"): |
|||
le.content.right() |
|||
if ctrlForVerticalMove: |
|||
bindKey("ctrlup"): |
|||
le.content.up() |
|||
bindKey("ctrldown"): |
|||
if le.content.Y() == le.content.high(): |
|||
le.content.insertline() |
|||
le.content.down() |
|||
else: |
|||
bindKey("up"): |
|||
le.content.up() |
|||
bindKey("down"): |
|||
le.content.down() |
|||
bindKey("pageup"): |
|||
le.content.vhome() |
|||
bindKey("pagedown"): |
|||
le.content.vend() |
|||
bindKey("home"): |
|||
le.content.home() |
|||
bindKey("end"): |
|||
le.content.`end`() |
|||
bindKey("backspace"): |
|||
le.content.backspace() |
|||
bindKey("delete"): |
|||
le.content.delete() |
|||
if enterSubmits: |
|||
bindKey("enter"): |
|||
le.finish() |
|||
else: |
|||
bindKey("enter"): |
|||
le.content.enter() |
|||
|
|||
proc newSimpleLineEditor*: LineEditor = |
|||
result = newLineEditor() |
|||
result.defaultBindings() |
|||
result.bindRenderer() |
|||
|
|||
proc newMultilineEditor*: LineEditor = |
|||
result = newLineEditor() |
|||
result.defaultBindings(false, false) |
|||
result.bindRenderer() |
|||
|
|||
proc newHistoryEditor*: LineEditor = |
|||
result = newSimpleLineEditor() |
|||
result.bindHistory() |
@ -0,0 +1,118 @@ |
|||
# editor.nim |
|||
|
|||
import types/event |
|||
import types/multiline |
|||
import terminal |
|||
import keycodes |
|||
import sequtils |
|||
|
|||
when defined(posix): |
|||
import posix |
|||
|
|||
type |
|||
JaleState = enum |
|||
jsInactive, jsActive, jsFinishing, jsQuitting |
|||
|
|||
|
|||
HorizontalScrollBehavior* = enum |
|||
hsbSingleScroll, hsbAllScroll, hsbWrap |
|||
|
|||
LineEditorObj* = object |
|||
# permanent across reads |
|||
evtTable*: EventTable |
|||
prompt*: string |
|||
horizontalScrollMode*: HorizontalScrollBehavior |
|||
|
|||
# change across every read() call |
|||
content*: Multiline |
|||
lastKeystroke*: int # TODO potentially deprecate |
|||
state: JaleState |
|||
hscroll*: int |
|||
vscroll*: int |
|||
vmax: int # maximum amount of vertical screen space |
|||
|
|||
# TODO remove the need for these through events |
|||
rendered*: int # how many lines were printed last full refresh |
|||
forceRedraw*: bool |
|||
|
|||
LineEditor* = ref LineEditorObj |
|||
|
|||
# weak list of references |
|||
var editors: seq[ptr LineEditorObj] |
|||
|
|||
# constructors and related code |
|||
|
|||
proc reset*(le: LineEditor) = |
|||
# called when read() is over |
|||
le.state = jsInactive |
|||
le.rendered = 0 # TODO deprecate |
|||
le.content = newMultiline() |
|||
le.lastKeystroke = -1 # TODO deprecate |
|||
le.forceRedraw = false # TODO deprecate |
|||
|
|||
le.hscroll = 0 |
|||
le.vscroll = 0 |
|||
|
|||
proc finalizer (inst: LineEditor) = |
|||
editors = editors.filterIt(cast[ptr LineEditorObj](inst) != it) |
|||
|
|||
proc newLineEditor*: LineEditor = |
|||
new(result, finalizer) |
|||
result.reset() |
|||
result.evtTable = newEventTable() |
|||
result.prompt = "" |
|||
result.horizontalScrollMode = hsbSingleScroll |
|||
result.vmax = 0 |
|||
editors.add(cast[ptr LineEditorObj](result)) |
|||
|
|||
# resize event firing |
|||
when defined(posix): |
|||
onSignal(28): |
|||
for ed in editors: |
|||
discard ed.evtTable.callRaw(jeResize, @[]) |
|||
|
|||
# getters/setters |
|||
|
|||
proc getVmax*(le: LineEditor): int = |
|||
if le.vmax <= 0: |
|||
terminalHeight() + le.vmax |
|||
else: |
|||
le.vmax |
|||
|
|||
proc setVmax*(le: LineEditor, val: int) = |
|||
le.vmax = val |
|||
|
|||
|
|||
# methods |
|||
|
|||
proc unfinish*(le: LineEditor) = |
|||
le.state = jsActive |
|||
|
|||
proc finish*(le: LineEditor) = |
|||
le.state = jsFinishing |
|||
discard le.evtTable.callRaw(jeFinish, @[]) |
|||
|
|||
proc quit*(le: LineEditor) = |
|||
le.state = jsQuitting |
|||
discard le.evtTable.callRaw(jeQuit, @[]) |
|||
|
|||
# TODO: deprecate |
|||
proc redraw*(le: LineEditor) = |
|||
le.forceRedraw = true |
|||
|
|||
proc read*(le: LineEditor): string = |
|||
le.state = jsActive |
|||
discard le.evtTable.callRaw(jePreRead, @[]) |
|||
while le.state == jsActive: |
|||
|
|||
discard le.evtTable.callRaw(jePreKeypress, @[]) |
|||
let key = getKey() |
|||
le.lastKeystroke = key # TODO deprecate |
|||
discard le.evtTable.callRaw(jeKeypress, @[JaleObject(kind: jkInt, intVal: key)]) |
|||
discard le.evtTable.callRaw(jePostKeypress, @[]) |
|||
|
|||
discard le.evtTable.callRaw(jePostRead, @[]) |
|||
|
|||
if le.state == jsFinishing: |
|||
result = le.content.getContent() |
|||
le.reset() |
@ -0,0 +1,59 @@ |
|||
import types/history |
|||
import editor |
|||
import types/event |
|||
import keycodes |
|||
import tables |
|||
|
|||
import options |
|||
|
|||
proc bindHistory*(le: LineEditor, useControl: bool = false) = |
|||
# adds hooks to events |
|||
# after reading finished, it adds to history |
|||
# before reading, it adds the temporary input to the history |
|||
let hist = newHistory() |
|||
|
|||
le.evtTable.subscribe(jeFinish): |
|||
hist.clean() |
|||
hist.newEntry(le.content) |
|||
|
|||
le.evtTable.subscribe(jePreRead): |
|||
hist.newEntry(le.content, temp = true) |
|||
discard hist.toEnd() |
|||
|
|||
# Adds history keybindings to editor (up, down, pg up/down) |
|||
# Works with the history provided |
|||
# if useShift is true, then the up/down keys and page up/down |
|||
# will remain free, and shift+up/down and ctrl+pg up/down |
|||
# will be used |
|||
|
|||
# for sanity, NEVER bind both history and multiline to control |
|||
|
|||
let upkey = keysByName[if useControl: "controlup" else: "up"] |
|||
let downkey = keysByName[if useControl: "controldown" else: "down"] |
|||
let homekey = keysByName[if useControl: "ctrlpageup" else: "pageup"] |
|||
let endkey = keysByName[if useControl: "ctrlpagedown" else: "pagedown"] |
|||
|
|||
le.evtTable.subscribe(jeKeypress): |
|||
let key = args[0].intVal |
|||
if key == upkey: |
|||
let res = hist.delta(-1) |
|||
if res.isSome(): |
|||
le.content = res.get() |
|||
le.redraw() |
|||
elif key == downkey: |
|||
let res = hist.delta(1) |
|||
if res.isSome(): |
|||
le.content = res.get() |
|||
le.redraw() |
|||
elif key == homekey: |
|||
let res = hist.toStart() |
|||
if res.isSome(): |
|||
le.content = res.get() |
|||
le.redraw() |
|||
elif key == endkey: |
|||
let res = hist.toStart() |
|||
if res.isSome(): |
|||
le.content = res.get() |
|||
le.redraw() |
|||
else: |
|||
discard |
@ -0,0 +1,172 @@ |
|||
# keycodes.nim |
|||
|
|||
import tables |
|||
import strutils |
|||
import uniterm |
|||
|
|||
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[uint64, JaleKeycode] |
|||
|
|||
proc defEscSeq(keys: seq[int], id: JaleKeycode) = |
|||
var result: uint64 = 0'u64 |
|||
for key in keys: |
|||
result *= 256 |
|||
result += key.uint64 |
|||
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 getKey*: int = |
|||
var key: uint64 = 0 |
|||
while true: |
|||
key *= 256 |
|||
key += uniGetChr().uint64 |
|||
if escapeSeqs.hasKey(key): |
|||
if escapeSeqs[key] != jkContinue: |
|||
key = escapeSeqs[key].uint64 |
|||
break |
|||
else: |
|||
break |
|||
return key.int |
@ -0,0 +1,27 @@ |
|||
import ../editor |
|||
import ../templates |
|||
|
|||
import terminal |
|||
|
|||
type ViewBehavior* = enum |
|||
vbLines, vbLinesLimited, vbFullscreen |
|||
|
|||
proc setViewBehavior*(ed: LineEditor, behavior: ViewBehavior) = |
|||
case behavior: |
|||
of vbLines: |
|||
ed.bindEvent(jeResize): |
|||
ed.vmax = terminalHeight() - 1 |
|||
of vbLinesLimited: |
|||
discard |
|||
of vbFullscreen: |
|||
var resized = false |
|||
ed.bindEvent(jeResize): |
|||
ed.vmax = terminalHeight() - 1 |
|||
ed.redraw() |
|||
resized = true |
|||
ed.bindEvent(jePreFullRender): |
|||
if resized: |
|||
eraseScreen() |
|||
setCursorPos(0,0) |
|||
resized = false |
|||
|
@ -0,0 +1,144 @@ |
|||
# renderer.nim |
|||
# |
|||
# listens to LineEditor events and updates the screen |
|||
|
|||
import editor |
|||
import types/event |
|||
import types/multiline |
|||
import strutils |
|||
import terminal |
|||
|
|||
|
|||
proc renderLine*(prompt: string, text: string, hscroll: int = 0) = |
|||
eraseLine() |
|||
setCursorXPos(0) |
|||
var lower = hscroll |
|||
var upper = hscroll + terminalWidth() - prompt.len() - 1 |
|||
if upper > text.high(): |
|||
upper = text.high() |
|||
if lower < -1: |
|||
raise newException(Defect, "negative hscroll submitted to renderLine") |
|||
if lower > text.high(): |
|||
write stdout, prompt |
|||
else: |
|||
let content = prompt & text[lower..upper] |
|||
write stdout, content |
|||
|
|||
|
|||
proc render(editor: LineEditor, line: int = -1) = |
|||
## Assumes that the cursor is already on the right line then |
|||
## proceeds to render the line-th line of the editor (if -1, will check |
|||
## the y). |
|||
var y = line |
|||
if y == -1: |
|||
y = editor.content.Y |
|||
|
|||
# the prompt's length is assumed to be always padded |
|||
let prompt = if y == 0: editor.prompt else: " ".repeat(editor.prompt.len()) |
|||
let content = editor.content.getLine(y) |
|||
|
|||
if editor.horizontalScrollMode == hsbAllScroll or |
|||
(editor.horizontalScrollMode == hsbSingleScroll and y == editor.content.Y): |
|||
renderLine(prompt, content, editor.hscroll) |
|||
else: |
|||
renderLine(prompt, content, 0) |
|||
|
|||
proc fullRender(editor: LineEditor) = |
|||
# from the top cursor pos, it draws the entire multiline prompt, then |
|||
# moves cursor to current y |
|||
|
|||
#editor.events.call(jePreFullRender) |
|||
|
|||
let lastY = min(editor.content.high(), editor.vscroll + editor.getVmax() - 1) |
|||
for i in countup(editor.vscroll, lastY): |
|||
editor.render(i) |
|||
if i - editor.vscroll < editor.rendered: |
|||
cursorDown(1) |
|||
else: |
|||
write stdout, "\n" |
|||
inc editor.rendered |
|||
|
|||
let rendered = lastY - editor.vscroll + 1 |
|||
var extraup = 0 |
|||
while rendered < editor.rendered: |
|||
eraseLine() |
|||
cursorDown(1) |
|||
dec editor.rendered |
|||
inc extraup |
|||
|
|||
# return to the selected y pos |
|||
cursorUp(lastY + 1 - editor.content.Y + extraup) |
|||
|
|||
proc moveCursorToEnd(editor: LineEditor) = |
|||
# only called when read finished |
|||
if editor.content.high() > editor.content.Y: |
|||
cursorDown(editor.content.high() - editor.content.Y) |
|||
write stdout, "\n" |
|||
|
|||
|
|||
|
|||
proc bindRenderer*(le: LineEditor) = |
|||
|
|||
var |
|||
preY: int |
|||
preVScroll: int |
|||
|
|||
le.evtTable.subscribe(jePreRead): |
|||
le.fullRender() |
|||
|
|||
le.evtTable.subscribe(jePreKeypress): |
|||
setCursorXPos(le.content.X - le.hscroll + le.prompt.len()) |
|||
preY = le.content.Y |
|||
preVScroll = le.vscroll |
|||
|
|||
le.evtTable.subscribe(jeKeypress): |
|||
discard |
|||
|
|||
le.evtTable.subscribe(jePostKeypress): |
|||
# scrolling |
|||
# last X that can be rendered on the screen |
|||
let lastX = terminalWidth() - le.prompt.len() + le.hscroll - 1 |
|||
# first X to be rendered |
|||
let firstX = le.hscroll |
|||
# X index put in bounds |
|||
let boundX = min(max(firstX, le.content.X), lastX) |
|||
# if outside of bounds |
|||
if le.content.X != boundX: |
|||
# scroll to move it inside bounds |
|||
le.hscroll += le.content.X - boundX |
|||
# if all lines scroll horizontally, full redraw is neccessary |
|||
if le.horizontalScrollMode == hsbAllScroll: |
|||
le.redraw() |
|||
|
|||
# first Y to be rendered |
|||
let firstY = le.vscroll |
|||
# last Y to be (potentially) rendered |
|||
let lastY = le.vscroll + le.getVmax() - 1 |
|||
# Y index put into bounds |
|||
let boundY = min(max(firstY, le.content.Y), lastY) |
|||
# Y outside of bounds: |
|||
if le.content.Y != boundY: |
|||
# scroll vertically to move it inside bounds |
|||
le.vscroll += le.content.Y - boundY |
|||
# vertical scrolling always means full redraw |
|||
le.redraw() |
|||
|
|||
# actual redraw handling |
|||
if le.forceRedraw or preY != le.content.Y or preVScroll != le.vscroll: |
|||
# move to the top |
|||
if preY - preVScroll > 0: |
|||
cursorUp(preY - preVScroll) |
|||
# redraw everything |
|||
le.fullRender() |
|||
if le.forceRedraw: |
|||
le.forceRedraw = false |
|||
else: |
|||
# redraw a single line |
|||
le.render() |
|||
|
|||
le.evtTable.subscribe(jePostRead): |
|||
le.moveCursorToEnd() |
|||
|
|||
le.evtTable.subscribe(jeResize): |
|||
le.redraw() |
|||
|
@ -0,0 +1,54 @@ |
|||
# event.nim |
|||
|
|||
# Using async is not needed, since the only events that happen are key presses |
|||
# and terminal resizes, while all other operations should complete reasonably fast. |
|||
# If you need to include jale in a larger async program, it is advised to read those |
|||
# yourself and feed them to jale (sort of reimplementing the editor main loop part). |
|||
|
|||
import tables |
|||
|
|||
type |
|||
JaleEvent* = enum |
|||
jeQuit, jeFinish, |
|||
jePreKeypress, jeKeypress, jePostKeypress, |
|||
jePreRead, jePostRead, |
|||
jeResize |
|||
|
|||
JaleKind* = enum |
|||
jkInt, jkChar, jkString, jkVoid |
|||
|
|||
JaleObject* = object |
|||
case kind*: JaleKind: |
|||
of jkInt: |
|||
intVal*: int |
|||
of jkChar: |
|||
charVal*: char |
|||
of jkString: |
|||
stringVal*: string |
|||
of jkVoid: |
|||
discard |
|||
|
|||
EventTable* = TableRef[JaleEvent, seq[proc (args: seq[JaleObject]): JaleObject]] |
|||
|
|||
proc newEventTable*: EventTable = |
|||
new(result) |
|||
|
|||
proc subscribeRaw*(evtTable: EventTable, event: JaleEvent, action: proc(args: seq[JaleObject]): JaleObject) = |
|||
|
|||
if not evtTable.hasKey(event): |
|||
evtTable[event] = @[] |
|||
evtTable[event].add(action) |
|||
|
|||
template subscribe*(evtTable: EventTable, event: JaleEvent, body: untyped) = |
|||
proc action (args {.inject.}: seq[JaleObject]): JaleObject {.gensym.} = |
|||
result = JaleObject(kind: jkVoid) |
|||
body |
|||
|
|||
evtTable.subscribeRaw(event, action) |
|||
|
|||
|
|||
proc callRaw*(evtTable: EventTable, event: JaleEvent, args: seq[JaleObject]): seq[JaleObject] = |
|||
result = @[] |
|||
if evtTable.hasKey(event): |
|||
for callback in evtTable[event]: |
|||
result.add(callback(args)) |
@ -0,0 +1,101 @@ |
|||
# history.nim |
|||
|
|||
import ../types/multiline |
|||
|
|||
import os |
|||
import options |
|||
|
|||
type HistoryElement* = ref object |
|||
original*: Multiline |
|||
current*: Multiline |
|||
temp: bool |
|||
|
|||
type History* = ref object |
|||
elements: seq[HistoryElement] |
|||
index: int |
|||
lowestTouchedIndex: int |
|||
|
|||
proc newHistory*: History = |
|||
new(result) |
|||
result.index = 0 |
|||
result.lowestTouchedIndex = 0 |
|||
result.elements = @[] |
|||
|
|||
template newIndex(h: History): Option[Multiline] = |
|||
if h.lowestTouchedIndex > h.index: |
|||
h.lowestTouchedIndex = h.index |
|||
|
|||
some(h.elements[h.index].current) |
|||
|
|||
proc delta*(h: History, amt: int): Option[Multiline] = |
|||
# move up/down in history and return reference to current |
|||
# also update lowest touched index |
|||
if h.elements.len() == 0: |
|||
return none[Multiline]() |
|||
|
|||
if h.index + amt <= 0: |
|||
h.index = 0 |
|||
elif h.index + amt >= h.elements.high(): |
|||
h.index = h.elements.high() |
|||
else: |
|||
h.index += amt |
|||
h.newIndex() |
|||
|
|||
|
|||
proc toEnd*(h: History): Option[Multiline] = |
|||
if h.elements.len() == 0: |
|||
return none[Multiline]() |
|||
h.index = h.elements.high() |
|||
h.newIndex() |
|||
|
|||
proc toStart*(h: History): Option[Multiline] = |
|||
if h.elements.len() == 0: |
|||
return none[Multiline]() |
|||
h.index = 0 |
|||
h.newIndex() |
|||
|
|||
proc clean*(h: History) = |
|||
# restore originals to current |
|||
# from lowest touched index to the top |
|||
|
|||
if h.lowestTouchedIndex <= h.elements.high(): |
|||
for i in countup(h.lowestTouchedIndex, h.elements.high()): |
|||
if h.elements[i].temp: |
|||
h.elements.delete(i) |
|||
else: |
|||
h.elements[i].current = h.elements[i].original.copy() |
|||
|
|||
h.lowestTouchedIndex = h.elements.len() |
|||
|
|||
proc newEntry*(h: History, ml: Multiline, temp: bool = false) = |
|||
if not temp: |
|||
h.elements.add(HistoryElement(original: ml, current: ml.copy(), temp: temp)) |
|||
else: |
|||
h.elements.add(HistoryElement(original: ml, current: ml, temp: temp)) |
|||
|
|||
proc save*(h: History, path: string) = |
|||
# discards currents and temps, only saves originals |
|||
if dirExists(path): |
|||
raise newException(CatchableError, "Attempt to save history to " & path & ", but a directory at that path exists") |
|||
|
|||
let file = open(path, fmWrite) |
|||
|
|||
for el in h.elements: |
|||
if el.temp: |
|||
continue |
|||
file.writeLine(el.original.serialize()) |
|||
|
|||
file.close() |
|||
|
|||
proc loadHistory*(path: string): History = |
|||
if not fileExists(path): |
|||
raise newException(CatchableError, "Attempt to read history from a non-existant file") |
|||
let file = open(path, fmRead) |
|||
|
|||
let h = newHistory() |
|||
var line: string |
|||
while readLine(file, line): |
|||
h.newEntry(line.deserialize()) |
|||
|
|||
file.close() |
|||
return h |
@ -0,0 +1,109 @@ |
|||
# line.nim |
|||
# |
|||
# supports multiple encodings |
|||
# all public methods shall work the same regardless of encoding |
|||
# |
|||
# mx/x are volatile, they are not expected to store information long term |
|||
|
|||
import strformat |
|||
import ../utf8 |
|||
|
|||
type |
|||
Line* = object |
|||
content: string |
|||
encoding: Encoding |
|||
length: int |
|||
x: int # real index |
|||
mx: int # mouse index (rune index) |
|||
|
|||
# getter |
|||
|
|||
proc content*(line: Line): string = |
|||
line.content |
|||
|
|||
proc X*(line: Line): int = |
|||
# line.X == line.len when at complete end |
|||
# line.X == line.high when at over the last char |
|||
# line.X == 0 when over the first char |
|||
line.mx |
|||
|
|||
# constructor |
|||
|
|||
proc newLine*: Line = |
|||
Line(content: "", encoding: ecUtf8, length: 0, x: 0, mx: 0) |
|||
|
|||
proc copy*(l: Line): Line = |
|||
Line(content: l.content, encoding: l.encoding, length: l.length, x: l.x, mx: l.mx) |
|||
|
|||
# methods |
|||
|
|||
proc insert*(line: var Line, str: string) = |
|||
# position: x |
|||
case line.encoding: |
|||
of ecUtf8: |
|||
line.length += str.runeLen |
|||
of ecSingle: |
|||
line.length += str.len |
|||
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()] |
|||
|
|||
# TODO: x += str.len() |
|||
|
|||
proc delete(line: var 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 |
|||
|
|||
|
|||
proc backspace*(line: var Line) = |
|||
# TODO: position: x, move x |
|||
# from multiline: |
|||
if ml.x > 0: |
|||
ml.lines[ml.y].backspace() |
|||
elif ml.x == 0 and ml.y > 0: |
|||
let cut = ml.lines[ml.y].content |
|||
ml.lines.delete(ml.y) |
|||
dec ml.y |
|||
ml.x = ml.lineLen() |
|||
ml.lines[ml.y].insert(cut, ml.x) |
|||
# end "from multiline" |
|||
if position == 0: |
|||
return |
|||
|
|||
proc delete*(line: var Line) = |
|||
# TODO: position: x |
|||
|
|||
proc navigateToMx*(line: var Line, target: mx) = |
|||
|
|||
proc left*(line: var Line) = |
|||
|
|||
proc right*(line: var Line) = |
|||
|
|||
proc home*(line: var Line) = |
|||
|
|||
proc `end`*(line: var Line) = |
|||
|
|||
|
|||
#proc range*(line: var Line, start: int, finish: int): string = |
|||
# if start > finish or start < 0 or finish > line.content.high(): |
|||
# raise newException(CatchableError, &"Invalid arguments for Line.range: start {start}, finish {finish} for line of length {line.content.len()}") |
|||
# result = line.content[start..finish] |
|||
|
|||
proc len*(line: Line): int = |
|||
line.length |
|||
|
|||
proc high*(line: Line): int = |
|||
line.length - 1 |
|||
|
|||
proc clearLine*(line: var Line) = |
|||
line.content = "" |
|||
line.length = 0 |
@ -0,0 +1,174 @@ |
|||
# multiline.nim |
|||
|
|||
import line |
|||
|
|||
import strutils |
|||
|
|||
type |
|||
Multiline* = ref object |
|||
lines: seq[Line] |
|||
y: int |
|||
|
|||
# getters/setters |
|||
|
|||
proc X*(ml: Multiline): int = |
|||
ml.lines[ml.y].X |
|||
|
|||
proc MX*(ml: Multiline): int = |
|||
ml.lines[ml.y].MX |
|||
|
|||
proc Y*(ml: Multiline): int = |
|||
ml.y |
|||
|
|||
# constructor |
|||
|
|||
proc newMultiline*(initEmpty: bool = true): Multiline = |
|||
new(result) |
|||
result.lines = @[] |
|||
if initEmpty: |
|||
result.lines.add(newLine()) |
|||
result.y = 0 |
|||
|
|||
proc copy*(ml: Multiline): Multiline = |
|||
new(result) |
|||
for l in ml.lines: |
|||
result.lines.add(l.copy()) |
|||
result.y = ml.y |
|||
|
|||
# methods |
|||
|
|||
proc lineLen*(ml: Multiline): int = |
|||
ml.lines[ml.y].len() |
|||
|
|||
proc lineHigh*(ml: Multiline): int = |
|||
ml.lines[ml.y].high() |
|||
|
|||
proc len*(ml: Multiline): int = |
|||
ml.lines.len() |
|||
|
|||
proc high*(ml: Multiline): int = |
|||
ml.lines.high() |
|||
|
|||
# internal setter |
|||
proc sety(ml: Multiline, target: int) = |
|||
ml.lines[target].navigateToMx(ml.lines[ml.y].MX) |
|||
ml.y = target |
|||
|
|||
# warning check before calling them if y lands in illegal territory |
|||
# these are unsafe! |
|||
proc decy(ml: Multiline) = |
|||
ml.sety(ml.y-1) |
|||
|
|||
proc incy(ml: Multiline) = |
|||
ml.sety(ml.y+1) |
|||
|
|||
# publically callable movement methods |
|||
proc left*(ml: Multiline) = |
|||
ml.lines[ml.y].left() |
|||
|
|||
proc right*(ml: Multiline) = |
|||
ml.lines[ml.y].right() |
|||
|
|||
proc up*(ml: Multiline) = |
|||
if ml.y > 0: |
|||
ml.decy |
|||
|
|||
proc down*(ml: Multiline) = |
|||
if ml.y < ml.lines.high(): |
|||
ml.incy |
|||
|
|||
proc home*(ml: Multiline) = |
|||
ml.lines[ml.y].home() |
|||
|
|||
proc `end`*(ml: Multiline) = |
|||
ml.lines[ml.y].`end`() |
|||
|
|||
proc vhome*(ml: Multiline) = |
|||
ml.sety(0) |
|||
|
|||
proc vend*(ml: Multiline) = |
|||
ml.sety(ml.high()) |
|||
|
|||
proc insert*(ml: Multiline, str: string) = |
|||
ml.lines[ml.y].insert(str) |
|||
|
|||
proc delete*(ml: Multiline) = |
|||
if ml.lines[ml.y].X < ml.lineLen(): |
|||
ml.lines[ml.y].delete() |
|||
else: |
|||
discard # TODO merge two lines |
|||
|
|||
proc backspace*(ml: Multiline) = |
|||
if ml.lines[ml.y].X > 0: |
|||
ml.lines[ml.y].backspace() |
|||
else: |
|||
discard # TODO merge two lines |
|||
|
|||
proc insertline*(ml: Multiline) = |
|||
# the default behaviour of command mode o |
|||
if ml.y == ml.lines.high(): |
|||
ml.lines.add(newLine()) |
|||
else: |
|||
ml.lines.insert(newLine(), ml.y + 1) |
|||
inc ml.y # this automatically moves x and mx to 0, since the newLine creates one with these coords |
|||
|
|||
proc enter*(ml: Multiline) = # TODO REDO THIS |
|||
# the default behaviour of enter in normie editors |
|||
if ml.lines[ml.y].X > ml.lineHigh(): |
|||
ml.insertline() # when end of line, it's just an insertline |
|||
else: |
|||
let cut = ml.lines[ml.y].range(ml.x, ml.lineHigh()) |
|||
ml.lines[ml.y].delete(ml.x, ml.lineHigh()) |
|||
ml.insertline() |
|||
ml.lines[ml.y].insert(cut, 0) |
|||
|
|||
proc clearline*(ml: Multiline) = |
|||
ml.lines[ml.y].clearLine() |
|||
|
|||
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 #TODO: preserve x |
|||
|
|||
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: |
|||
"" |
|||
|
|||
# without the extra args these work together to convert a multiline to a single |
|||
# line string. With extra args customizable |
|||
proc serialize*(ml: Multiline, sep: string = r"\n", replaceBS: bool = true): string = |
|||
# replaceBS = replace backslash |
|||
for line in ml.lines: |
|||
if replaceBS: |
|||
result &= line.content.replace(r"\", r"\\") & sep |
|||
else: |
|||
result &= line.content & sep |
|||
result[0..result.high() - sep.len()] |
|||
|
|||
proc deserialize*(str: string, sep: string = r"\n", replaceBS: bool = true): Multiline = |
|||
result = newMultiline(initEmpty = false) |
|||
for line in str.split(sep): |
|||
if replaceBS: |
|||
result.lines.add(newLine(line.replace(r"\\", r"\"))) |
|||
else: |
|||
result.lines.add(newLine(line)) |
|||
|
|||
result.y = result.high() |
|||
result.x = result.lineLen() |
|||
|
|||
proc fromString*(str: string): Multiline = |
|||
# simple load of string to multiline |
|||
deserialize(str, sep = "\n", replaceBS = false) |
|||
|
|||
proc getContent*(ml: Multiline): string = |
|||
# simple convert of multiline to string |
|||
ml.serialize(sep = "\n", replaceBS = false) |
|||
|
|||
|
@ -0,0 +1,26 @@ |
|||
## Universal (cross platform) terminal abstractions |
|||
|
|||
# source: https://github.com/h3rald/nimline/blob/master/nimline.nim |
|||
# lines 42-56 (modified) |
|||
|
|||
import terminal |
|||
|
|||
when defined(windows): |
|||
proc putchr(c: cint): cint {.discardable, header: "<conio.h>", importc: "_putch".} |
|||
proc getchr(): cint {.header: "<conio.h>", importc: "_getch".} |
|||
|
|||
proc uniPutChr*(c: char) = |
|||
## Prints an ASCII character to stdout. |
|||
putchr(c.cint) |
|||
proc uniGetChr*: int = |
|||
## Retrieves an ASCII character from stdin. |
|||
getchr().int |
|||
|
|||
else: |
|||
proc uniPutChr*(c: char) = |
|||
## Prints an ASCII character to stdout. |
|||
stdout.write(c) |
|||
|
|||
proc uniGetChr*: int = |
|||
## Retrieves an ASCII character from stdin. |
|||
return getch().int |
@ -0,0 +1,34 @@ |
|||
type |
|||
Encoding* = enum |
|||
ecSingle, ecUtf8 |
|||
ByteType* = enum |
|||
btSingle, btContinuation, |
|||
btDouble = 2, btTriple = 3, btQuadruple = 4, |
|||
# btInvalid |
|||
|
|||
proc byteType*(c: char): ByteType = |
|||
let n = c.uint8 |
|||
if n < 0x80: |
|||
return btSingle |
|||
elif n < 0xc0: |
|||
return btContinuation |
|||
elif n >= 0xc2 and n < 0xe0: |
|||
# c0 and c1 are invalid utf8 bytes |
|||
return btDouble |
|||
elif n >= 0xe0 and n < 0xf0: |
|||
return btTriple |
|||
elif n >= 0xf0 and n < 0xf5: |
|||
return btQuadruple |
|||
else: |
|||
raise newException(Exception, "Invalid utf8 sequence") |
|||
#return btInvalid |
|||
|
|||
|
|||
proc runeLen*(s: string): int = |
|||
for c in s: |
|||
case c.byteType: |
|||
of btSingle, btDouble, btTriple, btQuadruple: |
|||
result.inc |
|||
of btContinuation: |
|||
discard |
|||
|
@ -0,0 +1 @@ |
|||
switch("path", "$projectDir/../src") |
@ -0,0 +1,11 @@ |
|||
# This is just an example to get you started. You may wish to put all of your |
|||
# tests into a single file, or separate them into multiple `test1`, `test2` |
|||
# etc. files (better names are recommended, just make sure the name starts with |
|||
# the letter 't'). |
|||
# |
|||
# To run these tests, simply execute `nimble test`. |
|||
|
|||
import unittest |
|||
|
|||
test "can test": |
|||
check true |
@ -0,0 +1,106 @@ |
|||
import unittest |
|||
|
|||
import jale/event |
|||
import tables |
|||
|
|||
test "can add procs to tests": |
|||
let e = newEvent[int]() |
|||
proc sayHi = |
|||
echo "hi" |
|||
|
|||
proc sayHello = |
|||
echo "hello" |
|||
|
|||
e.add(1, sayHi) |
|||
e.add(2, sayHello) |
|||
|
|||
check e[1].len() == 1 |
|||
check e[2].len() == 1 |
|||
|
|||
test "can do closures with events": |
|||
let e = newEvent[int]() |
|||
var val = 4 |
|||
proc setVal = |
|||
val = 2 |
|||
|
|||
e.add(5, setVal) |
|||
|
|||
check val == 4 |
|||
e.call(1) |
|||
check val == 4 |
|||
e.call(5) |
|||
check val == 2 |
|||
|
|||
test "can remove from events": |
|||
let e = newEvent[int]() |
|||
var val = 10 |
|||
proc decVal = |
|||
dec val |
|||
|
|||
e.add(1, decVal) |
|||
e.add(2, decVal) |
|||
e.add(3, decVal) |
|||
|
|||
e.call(5) |
|||
check val == 10 |
|||
e.call(4) |
|||
check val == 10 |
|||
e.call(3) |
|||
check val == 9 |
|||
e.call(2) |
|||
check val == 8 |
|||
e.call(1) |
|||
check val == 7 |
|||
|
|||
check e.remove(1, decVal) == true |
|||
e.call(1) |
|||
check val == 7 |
|||
e.call(2) |
|||
check val == 6 |
|||
|
|||
test "can remove multiple at once": |
|||
let e = newEvent[char]() |
|||
var val = 8 |
|||
proc incVal = |
|||
inc val |
|||
|
|||
e.add('a', incVal) |
|||
e.add('a', incVal) |
|||
|
|||
e.call('a') |
|||
check val == 10 |
|||
check e.remove('a', incVal) == true |
|||
e.call('a') |
|||
check val == 10 |
|||
|
|||
test "can remove non existent keys without crashing": |
|||
let e = newEvent[uint8]() |
|||
proc nothing = |
|||
discard |
|||
check e.remove(5'u8, nothing) == |