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