diff options
Diffstat (limited to 'src/local/lineedit.nim')
-rw-r--r-- | src/local/lineedit.nim | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/src/local/lineedit.nim b/src/local/lineedit.nim new file mode 100644 index 00000000..0f048586 --- /dev/null +++ b/src/local/lineedit.nim @@ -0,0 +1,328 @@ +import std/strutils +import std/unicode + +import bindings/quickjs +import js/javascript +import types/cell +import types/opt +import types/winattrs +import utils/strwidth +import utils/twtstr + +import chagashi/charset +import chagashi/validator +import chagashi/decoder + +type + LineEditState* = enum + EDIT, FINISH, CANCEL + + 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 + invalid*: 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 + var r: Rune + while i < edit.news.len and dispw < edit.maxwidth: + fastRuneAt(edit.news, i, r) + dispw += r.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 (r, len) = edit.news.lastRune(edit.shifti - 1) + edit.shiftx -= r.width() + edit.shifti -= len + 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: + var r: Rune + fastRuneAt(edit.news, edit.shifti, r) + edit.shiftx += r.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 r in edit.prompt.runes: + result[x].str &= $r + x += r.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: + var r: Rune + fastRuneAt(edit.news, i, r) + if not edit.hide: + let w = r.width() + if x + w > result.width: break + if r.isControlChar(): + result[x].str &= '^' + inc x + result[x].str &= char(r).getControlLetter() + inc x + else: + result[x].str &= $r + 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 + let rem = edit.news.substr(edit.cursori) + edit.news.setLen(edit.cursori) + edit.news &= s + edit.news &= rem + edit.cursori += s.len + edit.cursorx += s.notwidth() + edit.invalid = true + +proc cancel(edit: LineEdit) {.jsfunc.} = + edit.state = CANCEL + +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 = FINISH + +proc backspace(edit: LineEdit) {.jsfunc.} = + if edit.cursori > 0: + let (r, len) = edit.news.lastRune(edit.cursori - 1) + edit.news.delete(edit.cursori - len .. edit.cursori - 1) + edit.cursori -= len + edit.cursorx -= r.width() + edit.invalid = 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.} = + edit.write(s, CHARSET_UTF_8) + +proc delete(edit: LineEdit) {.jsfunc.} = + if edit.cursori < edit.news.len: + let len = edit.news.runeLenAt(edit.cursori) + edit.news.delete(edit.cursori ..< edit.cursori + len) + edit.invalid = 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.invalid = true + +proc kill(edit: LineEdit) {.jsfunc.} = + if edit.cursori < edit.news.len: + edit.news.setLen(edit.cursori) + edit.invalid = true + +proc backward(edit: LineEdit) {.jsfunc.} = + if edit.cursori > 0: + let (r, len) = edit.news.lastRune(edit.cursori - 1) + edit.cursori -= len + edit.cursorx -= r.width() + if edit.cursorx < edit.shiftx: + edit.invalid = true + +proc forward(edit: LineEdit) {.jsfunc.} = + if edit.cursori < edit.news.len: + var r: Rune + fastRuneAt(edit.news, edit.cursori, r) + edit.cursorx += r.width() + if edit.cursorx >= edit.shiftx + edit.maxwidth: + edit.invalid = true + +proc prevWord(edit: LineEdit) {.jsfunc.} = + if edit.cursori == 0: + return + let (r, len) = edit.news.lastRune(edit.cursori - 1) + if r.breaksWord(): + edit.cursori -= len + edit.cursorx -= r.width() + while edit.cursori > 0: + let (r, len) = edit.news.lastRune(edit.cursori - 1) + if r.breaksWord(): + break + edit.cursori -= len + edit.cursorx -= r.width() + if edit.cursorx < edit.shiftx: + edit.invalid = true + +proc nextWord(edit: LineEdit) {.jsfunc.} = + if edit.cursori >= edit.news.len: + return + let oc = edit.cursori + var r: Rune + fastRuneAt(edit.news, edit.cursori, r) + if r.breaksWord(): + edit.cursorx += r.width() + else: + edit.cursori = oc + while edit.cursori < edit.news.len: + let pc = edit.cursori + fastRuneAt(edit.news, edit.cursori, r) + if r.breaksWord(): + edit.cursori = pc + break + edit.cursorx += r.width() + if edit.cursorx >= edit.shiftx + edit.maxwidth: + edit.invalid = true + +proc clearWord(edit: LineEdit) {.jsfunc.} = + let oc = edit.cursori + edit.prevWord() + if oc != edit.cursori: + edit.news.delete(edit.cursori .. oc - 1) + edit.invalid = true + +proc killWord(edit: LineEdit) {.jsfunc.} = + if edit.cursori >= edit.news.len: + return + let oc = edit.cursori + let ox = edit.cursorx + edit.nextWord() + if edit.cursori != oc: + if edit.cursori < edit.news.len: + let len = edit.news.runeLenAt(edit.cursori) + edit.news.delete(oc ..< edit.cursori + len) + else: + edit.news.delete(oc ..< edit.cursori) + edit.cursori = oc + edit.cursorx = ox + edit.invalid = true + +proc begin(edit: LineEdit) {.jsfunc.} = + edit.cursori = 0 + edit.cursorx = 0 + if edit.shiftx > 0: + edit.invalid = 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.invalid = 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.invalid = 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.invalid = 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: string, termwidth: int, current = "", + disallowed: set[char] = {}, hide = false, hist: LineHistory): LineEdit = + result = LineEdit( + prompt: prompt, + promptw: prompt.width(), + news: current, + disallowed: disallowed, + hide: hide, + invalid: true + ) + result.cursori = result.news.len + result.cursorx = result.news.notwidth() + # - 1, so that the cursor always has place + result.maxwidth = termwidth - result.promptw - 1 + result.hist = hist + result.histindex = result.hist.lines.len + +proc addLineEditModule*(ctx: JSContext) = + ctx.registerType(LineEdit) |