version 0.2
This commit is contained in:
commit
d9ed102638
|
@ -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) == false
|
||||||
|
|
||||||
|
test "removal is selective for procs":
|
||||||
|
let e = newEvent[string]()
|
||||||
|
proc first =
|
||||||
|
echo "1"
|
||||||
|
proc second =
|
||||||
|
echo "2"
|
||||||
|
e.add("key", first)
|
||||||
|
e.add("key", second)
|
||||||
|
check e.remove("key", first) == true
|
||||||
|
check e["key"].len() == 1
|
||||||
|
|
||||||
|
test "purging":
|
||||||
|
let e = newEvent[int]()
|
||||||
|
proc nothing =
|
||||||
|
discard
|
||||||
|
|
||||||
|
e.add(1, nothing)
|
||||||
|
e.add(1, nothing)
|
||||||
|
e.add(2, nothing)
|
||||||
|
e.add(1, nothing)
|
||||||
|
|
||||||
|
check e[1].len() == 3
|
||||||
|
e.purge(1)
|
||||||
|
check e[1].len() == 0
|
||||||
|
check e[2].len() == 1
|
Loading…
Reference in New Issue