import std/streams import std/strutils import std/unicode import bindings/quickjs import display/winattrs import js/javascript import types/cell import types/opt import utils/strwidth import utils/twtstr import chakasu/charset import chakasu/decoderstream import chakasu/encoderstream 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 -