import std/strutils
import chagashi/charset
import chagashi/decoder
import monoucha/javascript
import monoucha/quickjs
import types/cell
import types/opt
import types/winattrs
import utils/luwrap
import utils/strwidth
import utils/twtstr
import utils/twtuni
import utils/wordbreak
type
LineEditState* = enum
lesEdit, lesFinish, lesCancel
LineHistory* = ref object
lines: seq[string]
LineEdit* = ref object
news*: string
prompt: string
promptw: int
state*: LineEditState
escNext*: bool
cursorx: int # 0 ..< news.notwidth
cursori: int # 0 ..< news.len
shiftx: int # 0 ..< news.notwidth
shifti: int # 0 ..< news.len
padding: int # 0 or 1
maxwidth: int
disallowed: set[char]
hide: bool
hist: LineHistory
histindex: int
histtmp: string
luctx: LUContext
redraw*: bool
jsDestructor(LineEdit)
func newLineHistory*(): LineHistory =
return LineHistory()
# Note: capped at edit.maxwidth.
func getDisplayWidth(edit: LineEdit): int =
var dispw = 0
var i = edit.shifti
while i < edit.news.len and dispw < edit.maxwidth:
dispw += edit.news.nextUTF8(i).width()
return dispw
proc shiftView(edit: LineEdit) =
# Shift view so it contains the cursor.
if edit.cursorx < edit.shiftx:
edit.shiftx = edit.cursorx
edit.shifti = edit.cursori
# Shift view so it is completely filled.
if edit.shiftx > 0:
let dispw = edit.getDisplayWidth()
if dispw < edit.maxwidth:
let targetx = edit.shiftx - edit.maxwidth + dispw
if targetx <= 0:
edit.shiftx = 0
edit.shifti = 0
else:
while edit.shiftx > targetx:
let u = edit.news.prevUTF8(edit.shifti)
edit.shiftx -= u.width()
edit.padding = 0
# Shift view so it contains the cursor. (act 2)
if edit.shiftx < edit.cursorx - edit.maxwidth:
while edit.shiftx < edit.cursorx - edit.maxwidth and
edit.shifti < edit.news.len:
edit.shiftx += edit.news.nextUTF8(edit.shifti).width()
if edit.shiftx > edit.cursorx - edit.maxwidth:
# skipped over a cell because of a double-width char
edit.padding = 1
proc generateOutput*(edit: LineEdit): FixedGrid =
edit.shiftView()
# Make the output grid +1 cell wide, so it covers the whole input area.
result = newFixedGrid(edit.promptw + edit.maxwidth + 1)
var x = 0
for u in edit.prompt.points:
result[x].str.addUTF8(u)
x += u.width()
if x >= result.width: break
for i in 0 ..< edit.padding:
if x < result.width:
result[x].str = " "
inc x
var i = edit.shifti
while i < edit.news.len:
let pi = i
let u = edit.news.nextUTF8(i)
if not edit.hide:
let w = u.width()
if x + w > result.width: break
if u.isControlChar():
result[x].str &= '^'
inc x
result[x].str &= char(u).getControlLetter()
inc x
else:
for j in pi ..< i:
result[x].str &= edit.news[j]
x += w
else:
if x + 1 > result.width: break
result[x].str &= '*'
inc x
proc getCursorX*(edit: LineEdit): int =
return edit.promptw + edit.cursorx + edit.padding - edit.shiftx
proc insertCharseq(edit: LineEdit; s: string) =
let s = if edit.escNext:
s
else:
deleteChars(s, edit.disallowed)
edit.escNext = false
if s.len == 0:
return
edit.news.insert(s, edit.cursori)
edit.cursori += s.len
edit.cursorx += s.notwidth()
edit.redraw = true
proc cancel(edit: LineEdit) {.jsfunc.} =
edit.state = lesCancel
proc submit(edit: LineEdit) {.jsfunc.} =
if edit.hist.lines.len == 0 or edit.news != edit.hist.lines[^1]:
edit.hist.lines.add(edit.news)
edit.state = lesFinish
proc backspace(edit: LineEdit) {.jsfunc.} =
if edit.cursori > 0:
let pi = edit.cursori
let u = edit.news.prevUTF8(edit.cursori)
edit.news.delete(edit.cursori ..< pi)
edit.cursorx -= u.width()
edit.redraw = true
proc write*(edit: LineEdit; s: string; cs: Charset): bool =
if cs == CHARSET_UTF_8:
if s.validateUTF8Surr() != -1:
return false
edit.insertCharseq(s)
else:
let td = newTextDecoder(cs)
var success = false
let s = td.decodeAll(s, success)
if not success:
return false
edit.insertCharseq(s)
return true
proc write(edit: LineEdit; s: string): bool {.jsfunc.} =
if s.validateUTF8Surr() != -1:
return false
edit.insertCharseq(s)
return true
proc delete(edit: LineEdit) {.jsfunc.} =
if edit.cursori < edit.news.len:
let len = edit.news.pointLenAt(edit.cursori)
edit.news.delete(edit.cursori ..< edit.cursori + len)
edit.redraw = true
proc escape(edit: LineEdit) {.jsfunc.} =
edit.escNext = true
proc clear(edit: LineEdit) {.jsfunc.} =
if edit.cursori > 0:
edit.news.delete(0..edit.cursori - 1)
edit.cursori = 0
edit.cursorx = 0
edit.redraw = true
proc kill(edit: LineEdit) {.jsfunc.} =
if edit.cursori < edit.news.len:
edit.news.setLen(edit.cursori)
edit.redraw = true
proc backward(edit: LineEdit) {.jsfunc.} =
if edit.cursori > 0:
let u = edit.news.prevUTF8(edit.cursori)
edit.cursorx -= u.width()
if edit.cursorx < edit.shiftx:
edit.redraw = true
proc forward(edit: LineEdit) {.jsfunc.} =
if edit.cursori < edit.news.len:
let u = edit.news.nextUTF8(edit.cursori)
edit.cursorx += u.width()
if edit.cursorx >= edit.shiftx + edit.maxwidth:
edit.redraw = true
proc prevWord(edit: LineEdit) {.jsfunc.} =
if edit.cursori == 0:
return
let pi = edit.cursori
let u = edit.news.prevUTF8(edit.cursori)
if edit.luctx.breaksWord(u):
edit.cursorx -= u.width()
else:
edit.cursori = pi
while edit.cursori > 0:
let pi = edit.cursori
let u = edit.news.prevUTF8(edit.cursori)
if edit.luctx.breaksWord(u):
edit.cursori = pi
break
edit.cursorx -= u.width()
if edit.cursorx < edit.shiftx:
edit.redraw = true
proc nextWord(edit: LineEdit) {.jsfunc.} =
if edit.cursori >= edit.news.len:
return
let pi = edit.cursori
let u = edit.news.nextUTF8(edit.cursori)
if edit.luctx.breaksWord(u):
edit.cursorx += u.width()
else:
edit.cursori = pi
while edit.cursori < edit.news.len:
let pi = edit.cursori
let u = edit.news.nextUTF8(edit.cursori)
if edit.luctx.breaksWord(u):
edit.cursori = pi
break
edit.cursorx += u.width()
if edit.cursorx >= edit.shiftx + edit.maxwidth:
edit.redraw = true
proc clearWord(edit: LineEdit) {.jsfunc.} =
let oc = edit.cursori
edit.prevWord()
if oc != edit.cursori:
edit.news.delete(edit.cursori .. oc - 1)
edit.redraw = true
proc killWord(edit: LineEdit) {.jsfunc.} =
if edit.cursori >= edit.news.len:
return
var i = edit.cursori
var u = edit.news.nextUTF8(i)
if not edit.luctx.breaksWord(u):
while i < edit.news.len:
let pi = i
let u = edit.news.nextUTF8(i)
if edit.luctx.breaksWord(u):
i = pi
break
edit.news.delete(edit.cursori ..< i)
edit.redraw = true
proc begin(edit: LineEdit) {.jsfunc.} =
edit.cursori = 0
edit.cursorx = 0
if edit.shiftx > 0:
edit.redraw = true
proc `end`(edit: LineEdit) {.jsfunc.} =
if edit.cursori < edit.news.len:
edit.cursori = edit.news.len
edit.cursorx = edit.news.notwidth()
if edit.cursorx >= edit.shiftx + edit.maxwidth:
edit.redraw = true
proc prevHist(edit: LineEdit) {.jsfunc.} =
if edit.histindex > 0:
if edit.news.len > 0:
edit.histtmp = $edit.news
dec edit.histindex
edit.news = edit.hist.lines[edit.histindex]
# The begin call is needed so the cursor doesn't get lost outside
# the string.
edit.begin()
edit.end()
edit.redraw = true
proc nextHist(edit: LineEdit) {.jsfunc.} =
if edit.histindex + 1 < edit.hist.lines.len:
inc edit.histindex
edit.news = edit.hist.lines[edit.histindex]
edit.begin()
edit.end()
edit.redraw = true
elif edit.histindex < edit.hist.lines.len:
inc edit.histindex
edit.news = edit.histtmp
edit.begin()
edit.end()
edit.histtmp = ""
proc windowChange*(edit: LineEdit; attrs: WindowAttributes) =
edit.maxwidth = attrs.width - edit.promptw - 1
proc readLine*(prompt, current: string; termwidth: int; disallowed: set[char];
hide: bool; hist: LineHistory; luctx: LUContext): LineEdit =
let promptw = prompt.width()
return LineEdit(
prompt: prompt,
promptw: promptw,
news: current,
disallowed: disallowed,
hide: hide,
redraw: true,
cursori: current.len,
cursorx: current.notwidth(),
# - 1, so that the cursor always has place
maxwidth: termwidth - promptw - 1,
hist: hist,
histindex: hist.lines.len,
luctx: luctx
)
proc addLineEditModule*(ctx: JSContext) =
ctx.registerType(LineEdit)