about summary refs log tree commit diff stats
path: root/src/display
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2023-09-14 01:41:47 +0200
committerbptato <nincsnevem662@gmail.com>2023-09-14 02:01:21 +0200
commitc1b8338045716b25d664c0b8dd91eac0cb76480e (patch)
treea9c0a6763f180c2b6dd380aa880253ffc7685d85 /src/display
parentdb0798acccbedcef4b16737f6be0cf7388cc0528 (diff)
downloadchawan-c1b8338045716b25d664c0b8dd91eac0cb76480e.tar.gz
move around more modules
* ips -> io/
* loader related stuff -> loader/
* tempfile -> extern/
* buffer, forkserver -> server/
* lineedit, window -> display/
* cell -> types/
* opt -> types/
Diffstat (limited to 'src/display')
-rw-r--r--src/display/lineedit.nim369
-rw-r--r--src/display/term.nim6
-rw-r--r--src/display/window.nim54
3 files changed, 426 insertions, 3 deletions
diff --git a/src/display/lineedit.nim b/src/display/lineedit.nim
new file mode 100644
index 00000000..bf6a3ab5
--- /dev/null
+++ b/src/display/lineedit.nim
@@ -0,0 +1,369 @@
+import sequtils
+import streams
+import strutils
+import unicode
+
+import bindings/quickjs
+import display/term
+import js/javascript
+import types/cell
+import types/color
+import types/opt
+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
+    isnew*: bool #TODO hack
+    news*: seq[Rune]
+    prompt*: string
+    promptw: int
+    current: string
+    state*: LineEditState
+    escNext*: bool
+    cursor: int
+    shift: int
+    minlen: int
+    maxwidth: int
+    displen: int
+    disallowed: set[char]
+    hide: bool
+    term: Terminal
+    hist: LineHistory
+    histindex: int
+    histtmp: string
+
+jsDestructor(LineEdit)
+
+func newLineHistory*(): LineHistory =
+  return LineHistory()
+
+proc printesc(edit: LineEdit, rs: seq[Rune]) =
+  var dummy = 0
+  edit.term.write(edit.term.processOutputString0(rs.items, true, dummy))
+
+proc print(edit: LineEdit, s: string) =
+  var dummy = 0
+  edit.term.write(edit.term.processOutputString(s, dummy))
+
+template kill0(edit: LineEdit, i: int) =
+  edit.space(i)
+  edit.backward0(i)
+
+template kill0(edit: LineEdit) =
+  let w = min(edit.news.width(edit.cursor), edit.displen)
+  edit.kill0(w)
+
+proc backward0(state: LineEdit, i: int) =
+  state.term.cursorBackward(i)
+
+proc forward0(state: LineEdit, i: int) =
+  state.term.cursorForward(i)
+
+proc begin0(edit: LineEdit) =
+  edit.term.cursorBegin()
+  edit.forward0(edit.minlen)
+
+proc space(edit: LineEdit, i: int) =
+  edit.term.write(' '.repeat(i))
+
+#TODO this is broken (e.g. it doesn't account for shift, but for other
+# reasons too)
+proc generateOutput*(edit: LineEdit): FixedGrid =
+  result = newFixedGrid(edit.promptw + edit.maxwidth)
+  var x = 0
+  for r in edit.prompt.runes():
+    result[x].str &= $r
+    x += r.width()
+  if edit.hide:
+    for r in edit.news:
+      let w = r.width()
+      result[x].str = '*'.repeat(w)
+      x += w
+      if x >= result.width: break
+  else:
+    for r in edit.news:
+      result[x].str &= $r
+      x += r.width()
+      if x >= result.width: break
+  var s = ""
+  for c in result:
+    s &= c.str
+
+proc getCursorX*(edit: LineEdit): int =
+  return edit.promptw + edit.news.width(edit.shift, edit.cursor)
+
+proc redraw(state: LineEdit) =
+  if state.shift + state.displen > state.news.len:
+    state.displen = state.news.len - state.shift
+  var dispw = state.news.width(state.shift, state.shift + state.displen)
+  while dispw > state.maxwidth - 1:
+    dispw -= state.news[state.shift + state.displen - 1].width()
+    dec state.displen
+  state.begin0()
+  let os = state.news.substr(state.shift, state.shift + state.displen)
+  if state.hide:
+    state.print('*'.repeat(os.width()))
+  else:
+    state.printesc(os)
+  state.space(max(state.maxwidth - state.minlen - os.width(), 0))
+  state.begin0()
+  state.forward0(state.news.width(state.shift, state.cursor))
+
+proc zeroShiftRedraw(state: LineEdit) =
+  state.shift = 0
+  state.displen = state.news.len
+  state.redraw()
+
+proc fullRedraw*(state: LineEdit) =
+  state.displen = state.news.len
+  if state.cursor > state.shift:
+    var shiftw = state.news.width(state.shift, state.cursor)
+    while shiftw > state.maxwidth - 1:
+      inc state.shift
+      shiftw -= state.news[state.shift].width()
+  else:
+    state.shift = max(state.cursor - 1, 0)
+  state.redraw()
+
+proc drawPrompt*(edit: LineEdit) =
+  edit.term.write(edit.prompt)
+
+proc insertCharseq(edit: LineEdit, cs: var seq[Rune]) =
+  let escNext = edit.escNext
+  var i = 0
+  for j in 0 ..< cs.len:
+    if cs[i].isAscii():
+      let c = cast[char](cs[i])
+      if not escNext and c in Controls or c in edit.disallowed:
+        continue
+    if i != j:
+      cs[i] = cs[j]
+    inc i
+
+  edit.escNext = false
+  if cs.len == 0:
+    return
+
+  if edit.cursor >= edit.news.len and edit.news.width(edit.shift, edit.cursor) + cs.width() < edit.maxwidth:
+    edit.news &= cs
+    edit.cursor += cs.len
+    if edit.hide:
+      edit.print('*'.repeat(cs.width()))
+    else:
+      edit.printesc(cs)
+  else:
+    edit.news.insert(cs, edit.cursor)
+    edit.cursor += cs.len
+    edit.fullRedraw()
+
+proc cancel(edit: LineEdit) {.jsfunc.} =
+  edit.state = CANCEL
+
+proc submit(edit: LineEdit) {.jsfunc.} =
+  let s = $edit.news
+  if edit.hist.lines.len == 0 or s != edit.hist.lines[^1]:
+    edit.hist.lines.add(s)
+  edit.state = FINISH
+
+proc backspace(edit: LineEdit) {.jsfunc.} =
+  if edit.cursor > 0:
+    let w = edit.news[edit.cursor - 1].width()
+    edit.news.delete(edit.cursor - 1..edit.cursor - 1)
+    dec edit.cursor
+    if edit.cursor == edit.news.len and edit.shift == 0:
+      edit.backward0(w)
+      edit.kill0(w)
+    else:
+      edit.fullRedraw()
+
+const buflen = 128
+var buf {.threadVar.}: array[buflen, uint32]
+proc write*(edit: LineEdit, s: string, cs: Charset): bool =
+  let ss = newStringStream(s)
+  let ds = newDecoderStream(ss, cs = cs, buflen = buflen,
+    errormode = DECODER_ERROR_MODE_FATAL)
+  var cseq: seq[Rune]
+  while not ds.atEnd:
+    let n = ds.readData(buf)
+    for i in 0 ..< n div 4:
+      let r = cast[Rune](buf[i])
+      cseq.add(r)
+  if ds.failed:
+    return false
+  edit.insertCharseq(cseq)
+  return true
+
+proc write(edit: LineEdit, s: string): bool {.jsfunc.} =
+  edit.write(s, CHARSET_UTF_8)
+
+proc delete(edit: LineEdit) {.jsfunc.} =
+  if edit.cursor >= 0 and edit.cursor < edit.news.len:
+    let w = edit.news[edit.cursor].width()
+    edit.news.delete(edit.cursor..edit.cursor)
+    if edit.cursor == edit.news.len and edit.shift == 0:
+      edit.kill0(w)
+    else:
+      edit.fullRedraw()
+
+proc escape(edit: LineEdit) {.jsfunc.} =
+  edit.escNext = true
+
+proc clear(edit: LineEdit) {.jsfunc.} =
+  if edit.cursor > 0:
+    edit.news.delete(0..edit.cursor - 1)
+    edit.cursor = 0
+    edit.zeroShiftRedraw()
+
+proc kill(edit: LineEdit) {.jsfunc.} =
+  if edit.cursor < edit.news.len:
+    edit.kill0()
+    edit.news.setLen(edit.cursor)
+
+proc backward(edit: LineEdit) {.jsfunc.} =
+  if edit.cursor > 0:
+    dec edit.cursor
+    if edit.cursor > edit.shift or edit.shift == 0:
+      edit.backward0(edit.news[edit.cursor].width())
+    else:
+      edit.fullRedraw()
+
+proc forward(edit: LineEdit) {.jsfunc.} =
+  if edit.cursor < edit.news.len:
+    inc edit.cursor
+    if edit.news.width(edit.shift, edit.cursor) < edit.maxwidth:
+      var n = 1
+      if edit.news.len > edit.cursor:
+        n = edit.news[edit.cursor].width()
+      edit.forward0(n)
+    else:
+      edit.fullRedraw()
+
+proc prevWord(edit: LineEdit, check = opt(BoundaryFunction)) {.jsfunc.} =
+  let oc = edit.cursor
+  while edit.cursor > 0:
+    dec edit.cursor
+    if edit.news[edit.cursor].breaksWord(check):
+      break
+  if edit.cursor != oc:
+    if edit.cursor > edit.shift or edit.shift == 0:
+      edit.backward0(edit.news.width(edit.cursor, oc))
+    else:
+      edit.fullRedraw()
+
+proc nextWord(edit: LineEdit, check = opt(BoundaryFunction)) {.jsfunc.} =
+  let oc = edit.cursor
+  let ow = edit.news.width(edit.shift, edit.cursor)
+  while edit.cursor < edit.news.len:
+    inc edit.cursor
+    if edit.cursor < edit.news.len:
+      if edit.news[edit.cursor].breaksWord(check):
+        break
+  if edit.cursor != oc:
+    let dw = edit.news.width(oc, edit.cursor)
+    if ow + dw < edit.maxwidth:
+      edit.forward0(dw)
+    else:
+      edit.fullRedraw()
+
+proc clearWord(edit: LineEdit, check = opt(BoundaryFunction)) {.jsfunc.} =
+  var i = edit.cursor
+  if i > 0:
+    # point to the previous character
+    dec i
+  while i > 0:
+    dec i
+    if edit.news[i].breaksWord(check):
+      inc i
+      break
+  if i != edit.cursor:
+    edit.news.delete(i..<edit.cursor)
+    edit.cursor = i
+    edit.fullRedraw()
+
+proc killWord(edit: LineEdit, check = opt(BoundaryFunction)) {.jsfunc.} =
+  var i = edit.cursor
+  if i < edit.news.len and edit.news[i].breaksWord(check):
+    inc i
+  while i < edit.news.len:
+    if edit.news[i].breaksWord(check):
+      break
+    inc i
+  if i != edit.cursor:
+    edit.news.delete(edit.cursor..<i)
+    edit.fullRedraw()
+
+proc begin(edit: LineEdit) {.jsfunc.} =
+  if edit.cursor > 0:
+    if edit.shift == 0:
+      edit.backward0(edit.news.width(0, edit.cursor))
+      edit.cursor = 0
+    else:
+      edit.cursor = 0
+      edit.fullRedraw()
+
+proc `end`(edit: LineEdit) {.jsfunc.} =
+  if edit.cursor < edit.news.len:
+    if edit.news.width(edit.shift, edit.news.len) < edit.maxwidth:
+      edit.forward0(edit.news.width(edit.cursor, edit.news.len))
+      edit.cursor = edit.news.len
+    else:
+      edit.cursor = edit.news.len
+      edit.fullRedraw()
+
+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].toRunes()
+    edit.begin()
+    edit.end()
+    edit.fullRedraw()
+
+proc nextHist(edit: LineEdit) {.jsfunc.} =
+  if edit.histindex + 1 < edit.hist.lines.len:
+    inc edit.histindex
+    edit.news = edit.hist.lines[edit.histindex].toRunes()
+    edit.begin()
+    edit.end()
+    edit.fullRedraw()
+  elif edit.histindex < edit.hist.lines.len:
+    inc edit.histindex
+    edit.news = edit.histtmp.toRunes()
+    edit.begin()
+    edit.end()
+    edit.fullRedraw()
+    edit.histtmp = ""
+
+proc readLine*(prompt: string, termwidth: int, current = "",
+               disallowed: set[char] = {}, hide = false,
+               term: Terminal, hist: LineHistory): LineEdit =
+  result = LineEdit(
+    prompt: prompt,
+    promptw: prompt.width(),
+    current: current,
+    news: current.toRunes(),
+    minlen: prompt.width(),
+    disallowed: disallowed,
+    hide: hide,
+    term: term,
+    isnew: true
+  )
+  result.cursor = result.news.width()
+  result.maxwidth = termwidth - result.promptw
+  result.displen = result.cursor
+  result.hist = hist
+  result.histindex = result.hist.lines.len
+
+proc addLineEditModule*(ctx: JSContext) =
+  ctx.registerType(LineEdit)
diff --git a/src/display/term.nim b/src/display/term.nim
index 233c9cd4..60b5ff7b 100644
--- a/src/display/term.nim
+++ b/src/display/term.nim
@@ -8,12 +8,12 @@ import termios
 import unicode
 
 import bindings/termcap
-import buffer/cell
 import config/config
+import display/window
 import io/runestream
-import io/window
+import types/cell
 import types/color
-import utils/opt
+import types/opt
 import utils/twtstr
 
 import chakasu/charset
diff --git a/src/display/window.nim b/src/display/window.nim
new file mode 100644
index 00000000..278fc3fb
--- /dev/null
+++ b/src/display/window.nim
@@ -0,0 +1,54 @@
+import terminal
+
+when defined(posix):
+  import termios
+
+
+type
+  WindowAttributes* = object
+    width*: int
+    height*: int
+    ppc*: int # cell width
+    ppl*: int # cell height
+    cell_ratio*: float64 # ppl / ppc
+    width_px*: int
+    height_px*: int
+
+proc getWindowAttributes*(tty: File): WindowAttributes =
+  when defined(posix):
+    if tty.isatty():
+      var win: IOctl_WinSize
+      if ioctl(cint(getOsFileHandle(tty)), TIOCGWINSZ, addr win) != -1:
+        var cols = win.ws_col
+        var rows = win.ws_row
+        if cols == 0:
+          cols = 80
+        if rows == 0:
+          rows = 24
+        # Filling the last row without raw mode breaks things. However,
+        # not supporting Windows means we can always have raw mode, so we can
+        # use all available columns.
+        result.width = int(cols)
+        result.height = int(rows)
+        result.ppc = int(win.ws_xpixel) div result.width
+        result.ppl = int(win.ws_ypixel) div result.height
+        # some terminal emulators (aka vte) don't set ws_xpixel or ws_ypixel.
+        # solution: use xterm.
+        if result.ppc == 0:
+          result.ppc = 9
+        if result.ppl == 0:
+          result.ppl = 18
+        result.width_px = result.width * result.ppc
+        result.height_px = result.height * result.ppl
+        result.cell_ratio = result.ppl / result.ppc
+        return
+  # for Windows. unused.
+  result.width = terminalWidth() - 1
+  result.height = terminalHeight()
+  if result.height == 0:
+    result.height = 24
+  result.ppc = 9
+  result.ppl = 18
+  result.cell_ratio = result.ppl / result.ppc
+  result.width_px = result.ppc * result.width
+  result.height_px = result.ppl * result.height