jale/src/jale/editor.nim

209 lines
5.4 KiB
Nim
Raw Normal View History

2021-02-15 20:18:31 +01:00
import strformat
import strutils
import tables
import line
import multiline
import keycodes
import event
import renderer
2021-02-20 14:46:37 +01:00
import terminal
2021-02-23 17:35:27 +01:00
import os
2021-02-15 20:18:31 +01:00
type
JaleEvent* = enum
2021-02-24 00:31:58 +01:00
jeKeypress, jeQuit, jeFinish, jePreRead, jePostRead, jePreFullRender,
jePreKey, jePostKey, jeResize
2021-02-15 20:18:31 +01:00
EditorState = enum
esOutside, esTyping, esFinishing, esQuitting
ScrollBehavior* = enum
sbSingleScroll, sbAllScroll, sbWrap
2021-02-15 20:18:31 +01:00
LineEditor* = ref object
# permanents
2021-02-15 20:18:31 +01:00
keystrokes*: Event[int]
events*: Event[JaleEvent]
prompt*: string
scrollMode*: ScrollBehavior
# permanent internals: none
# per-read contents
content*: Multiline
2021-02-15 20:18:31 +01:00
lastKeystroke*: int
# per-read internals
state: EditorState
2021-02-15 20:18:31 +01:00
rendered: int # how many lines were printed last full refresh
forceRedraw: bool
hscroll: int
2021-02-24 00:31:58 +01:00
vmax*: int
2021-02-23 17:35:27 +01:00
vscroll: int
2021-02-15 20:18:31 +01:00
2021-02-15 20:59:40 +01:00
# getter/setter sorts
proc unfinish*(le: LineEditor) =
le.state = esTyping
2021-02-15 20:59:40 +01:00
proc finish*(le: LineEditor) =
le.state = esFinishing
2021-02-15 20:59:40 +01:00
# can be overwritten to false, inside the event
le.events.call(jeFinish)
2021-02-18 21:51:44 +01:00
proc quit*(le: LineEditor) =
le.state = esQuitting
2021-02-18 21:51:44 +01:00
le.events.call(jeQuit)
2021-02-19 14:35:41 +01:00
proc redraw*(le: LineEditor) =
le.forceRedraw = true
2021-02-15 20:59:40 +01:00
# constructor
2021-02-15 20:18:31 +01:00
proc newLineEditor*: LineEditor =
new(result)
result.content = newMultiline()
result.keystrokes.new()
result.events.new()
result.prompt = ""
result.rendered = 0
result.lastKeystroke = -1
result.forceRedraw = false
result.state = esOutside
result.scrollMode = sbSingleScroll
result.hscroll = 0
2021-02-23 17:35:27 +01:00
result.vscroll = 0
result.vmax = terminalHeight() - 1
2021-02-15 20:18:31 +01:00
2021-02-15 20:59:40 +01:00
# priv/pub methods
proc reset(editor: LineEditor) =
## Resets state to outside, resets internal rendering details
## resets last keystroke, creates new contents
editor.state = esOutside
2021-02-15 20:59:40 +01:00
editor.rendered = 0
editor.content = newMultiline()
editor.lastKeystroke = -1
editor.forceRedraw = false
editor.hscroll = 0
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).
2021-02-15 20:18:31 +01:00
var y = line
if y == -1:
y = editor.content.Y
# the prompt's length is assumed to be always padded
2021-02-19 17:57:10 +01:00
let prompt = if y == 0: editor.prompt else: " ".repeat(editor.prompt.len())
let content = editor.content.getLine(y)
if editor.scrollMode == sbAllScroll or
(editor.scrollMode == sbSingleScroll and y == editor.content.Y):
renderLine(prompt, content, editor.hscroll)
else:
renderLine(prompt, content, 0)
2021-02-15 20:18:31 +01:00
2021-02-20 14:46:37 +01:00
proc fullRender(editor: LineEditor) =
2021-02-15 20:59:40 +01:00
# from the top cursor pos, it draws the entire multiline prompt, then
# moves cursor to current y
2021-02-23 17:35:27 +01:00
2021-02-24 00:31:58 +01:00
editor.events.call(jePreFullRender)
2021-02-23 17:35:27 +01:00
let lastY = min(editor.content.high(), editor.vscroll + editor.vmax - 1)
for i in countup(editor.vscroll, lastY):
editor.render(i)
2021-02-23 17:35:27 +01:00
if i - editor.vscroll < editor.rendered:
2021-02-20 14:46:37 +01:00
cursorDown(1)
2021-02-15 20:59:40 +01:00
else:
2021-02-20 14:46:37 +01:00
write stdout, "\n"
2021-02-15 20:59:40 +01:00
inc editor.rendered
2021-02-23 17:35:27 +01:00
let rendered = lastY - editor.vscroll + 1
var extraup = 0
2021-02-23 17:35:27 +01:00
while rendered < editor.rendered:
2021-02-20 14:46:37 +01:00
eraseLine()
cursorDown(1)
dec editor.rendered
inc extraup
2021-02-23 17:35:27 +01:00
# return to the selected y pos
2021-02-23 17:40:21 +01:00
cursorUp(lastY + 1 - editor.content.Y + extraup)
2021-02-15 20:18:31 +01:00
2021-02-20 14:46:37 +01:00
proc moveCursorToEnd(editor: LineEditor) =
2021-02-15 20:18:31 +01:00
# only called when read finished
2021-02-15 20:59:40 +01:00
if editor.content.high() > editor.content.Y:
2021-02-20 14:46:37 +01:00
cursorDown(editor.content.high() - editor.content.Y)
write stdout, "\n"
2021-02-15 20:18:31 +01:00
proc read*(editor: LineEditor): string =
editor.state = esTyping
editor.events.call(jePreRead)
2021-02-15 20:59:40 +01:00
# starts at the top, full render moves it into the right y
2021-02-20 14:46:37 +01:00
editor.fullRender()
2021-02-15 20:59:40 +01:00
while editor.state == esTyping:
2021-02-15 20:59:40 +01:00
# refresh current line every time
setCursorXPos(editor.content.X - editor.hscroll + editor.prompt.len())
2021-02-15 20:59:40 +01:00
# get key (with escapes)
2021-02-23 17:35:27 +01:00
2021-02-15 20:18:31 +01:00
let key = getKey()
2021-02-15 20:59:40 +01:00
# record y pos
2021-02-15 20:18:31 +01:00
let preY = editor.content.Y
2021-02-23 17:35:27 +01:00
let preVScroll = editor.vscroll
2021-02-15 20:59:40 +01:00
# call the events
2021-02-15 20:18:31 +01:00
editor.lastKeystroke = key
editor.keystrokes.call(key)
2021-02-24 00:31:58 +01:00
editor.events.call(jePreKey)
2021-02-15 20:18:31 +01:00
editor.events.call(jeKeypress)
2021-02-24 00:31:58 +01:00
editor.events.call(jePostKey)
# autoscroll horizontally based on current scroll and x pos
# last x rendered
let lastX = terminalWidth() - editor.prompt.len() + editor.hscroll - 1
# first x rendered
let firstX = editor.hscroll
# x squished into boundaries
let boundX = min(max(firstX, editor.content.X), lastX)
if editor.content.X != boundX:
editor.hscroll += editor.content.X - boundX
if editor.scrollMode == sbAllScroll:
editor.forceRedraw = true
2021-02-23 17:35:27 +01:00
# first y possibly rendered
let firstY = editor.vscroll
let lastY = editor.vscroll + editor.vmax - 1
# y squished into boundaries
let boundY = min(max(firstY, editor.content.Y), lastY)
if editor.content.Y != boundY:
editor.vscroll += editor.content.Y - boundY
editor.forceRedraw = true
2021-02-15 20:59:40 +01:00
# redraw everything if y changed
2021-02-23 17:35:27 +01:00
if editor.forceRedraw or preY != editor.content.Y or preVScroll != editor.vscroll:
2021-02-15 20:59:40 +01:00
# move to the top
2021-02-23 17:35:27 +01:00
if preY - preVScroll > 0:
cursorUp(preY - preVScroll)
2021-02-15 20:59:40 +01:00
# move to the right y
2021-02-20 14:46:37 +01:00
editor.fullRender()
if editor.forceRedraw:
editor.forceRedraw = false
2021-02-19 14:35:41 +01:00
else:
2021-02-20 14:46:37 +01:00
editor.render()
editor.events.call(jePostRead)
2021-02-15 20:18:31 +01:00
2021-02-15 20:59:40 +01:00
# move cursor to end
2021-02-20 14:46:37 +01:00
editor.moveCursorToEnd()
2021-02-19 11:27:27 +01:00
if editor.state == esFinishing:
result = editor.content.getContent()
2021-02-15 20:59:40 +01:00
editor.reset()