about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2022-11-29 12:28:04 +0100
committerbptato <nincsnevem662@gmail.com>2022-11-29 12:28:04 +0100
commitd7a05e7a58d0c2de3078fc9854534974c7e347d4 (patch)
tree7e5bc4327631ac4106d885b5d5e74bfb1cfa3d64
parentf65667797376e65d8e19e1aa514d160210013540 (diff)
downloadchawan-d7a05e7a58d0c2de3078fc9854534974c7e347d4.tar.gz
Add line editing history, other lineedit fixes
It's still kind of broken...
-rw-r--r--res/config.toml2
-rw-r--r--src/display/client.nim4
-rw-r--r--src/display/pager.nim32
-rw-r--r--src/io/lineedit.nim195
4 files changed, 137 insertions, 96 deletions
diff --git a/res/config.toml b/res/config.toml
index f4cbba2a..c2c612fa 100644
--- a/res/config.toml
+++ b/res/config.toml
@@ -91,3 +91,5 @@ M-d = 'line.killWord()'
 C-a = 'line.begin()'
 C-e = 'line.end()'
 C-v = 'line.escape()'
+C-p = 'line.prevHist()'
+C-n = 'line.nextHist()'
diff --git a/src/display/client.nim b/src/display/client.nim
index 34477c1a..2fd8bb89 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -264,8 +264,12 @@ proc log(console: Console, ss: varargs[string]) {.jsfunc.} =
   console.err.write('\n')
   console.err.flush()
 
+proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {.
+  importc: "setvbuf", header: "<stdio.h>", tags: [].}
+
 proc inputLoop(client: Client) =
   let selector = client.selector
+  discard c_setvbuf(client.console.tty, nil, IONBF, 0)
   selector.registerHandle(int(client.console.tty.getFileHandle()), {Read}, nil)
   let sigwinch = selector.registerSignal(int(SIGWINCH), nil)
   let redrawtimer = client.selector.registerTimer(1000, false, nil)
diff --git a/src/display/pager.nim b/src/display/pager.nim
index 849dbcca..d60196cd 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -52,6 +52,7 @@ type
     display: FixedGrid
     redraw*: bool
     term*: Terminal
+    linehist: array[LineMode, LineHistory]
 
 iterator containers*(pager: Pager): Container =
   if pager.container != nil:
@@ -114,28 +115,33 @@ proc searchPrev(pager: Pager) {.jsfunc.} =
     else:
       pager.container.cursorNextMatch(pager.regex.get, true)
 
-proc setLineEdit(pager: Pager, edit: LineEdit, mode: LineMode) =
-  pager.lineedit = some(edit)
+func attrs(pager: Pager): WindowAttributes = pager.term.attrs
+
+proc getLineHist(pager: Pager, mode: LineMode): LineHistory =
+  if pager.linehist[mode] == nil:
+    pager.linehist[mode] = newLineHistory()
+  return pager.linehist[mode]
+
+proc setLineEdit(pager: Pager, prompt: string, mode: LineMode, current = "", hide = false) =
+  pager.lineedit = some(readLine(prompt, pager.attrs.width, current = current, term = pager.term, hide = hide, hist = pager.getLineHist(mode)))
   pager.linemode = mode
 
 proc clearLineEdit(pager: Pager) =
   pager.lineedit = none(LineEdit)
 
-func attrs(pager: Pager): WindowAttributes = pager.term.attrs
-
 proc searchForward(pager: Pager) {.jsfunc.} =
-  pager.setLineEdit(readLine("/", pager.attrs.width, term = pager.term), SEARCH_F)
+  pager.setLineEdit("/", SEARCH_F)
 
 proc searchBackward(pager: Pager) {.jsfunc.} =
-  pager.setLineEdit(readLine("?", pager.attrs.width, term = pager.term), SEARCH_B)
+  pager.setLineEdit("?", SEARCH_B)
 
 proc isearchForward(pager: Pager) {.jsfunc.} =
   pager.container.pushCursorPos()
-  pager.setLineEdit(readLine("/", pager.attrs.width, term = pager.term), ISEARCH_F)
+  pager.setLineEdit("/", ISEARCH_F)
 
 proc isearchBackward(pager: Pager) {.jsfunc.} =
   pager.container.pushCursorPos()
-  pager.setLineEdit(readLine("?", pager.attrs.width, term = pager.term), ISEARCH_B)
+  pager.setLineEdit("?", ISEARCH_B)
 
 proc newPager*(config: Config, attrs: WindowAttributes, dispatcher: Dispatcher): Pager =
   let pager = Pager(
@@ -479,7 +485,7 @@ proc readPipe*(pager: Pager, ctype: Option[string], fd: FileHandle) =
   pager.addContainer(container)
 
 proc command(pager: Pager) {.jsfunc.} =
-  pager.setLineEdit(readLine("COMMAND: ", pager.attrs.width, term = pager.term), COMMAND)
+  pager.setLineEdit("COMMAND: ", COMMAND)
 
 proc commandMode(pager: Pager) {.jsfunc.} =
   pager.commandmode = true
@@ -527,7 +533,7 @@ proc updateReadLine*(pager: Pager) =
       of LOCATION: pager.loadURL(s)
       of USERNAME:
         pager.username = s
-        pager.setLineEdit(readLine("Password: ", pager.attrs.width, hide = true, term = pager.term), PASSWORD)
+        pager.setLineEdit("Password: ", PASSWORD, hide = true)
       of PASSWORD:
         let url = newURL(pager.container.source.location)
         url.username = pager.username
@@ -571,7 +577,7 @@ proc load(pager: Pager, s = "") {.jsfunc.} =
     var url = s
     if url == "":
       url = pager.container.source.location.serialize()
-    pager.setLineEdit(readLine("URL: ", pager.attrs.width, current = url, term = pager.term), LOCATION)
+    pager.setLineEdit("URL: ", LOCATION, url)
 
 # Reload the page in a new buffer, then kill the previous buffer.
 proc reload(pager: Pager) {.jsfunc.} =
@@ -585,7 +591,7 @@ proc click(pager: Pager) {.jsfunc.} =
   pager.container.click()
 
 proc authorize*(pager: Pager) =
-  pager.setLineEdit(readLine("Username: ", pager.attrs.width, term = pager.term), USERNAME)
+  pager.setLineEdit("Username: ", USERNAME)
 
 proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bool =
   case event.t
@@ -629,7 +635,7 @@ proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bo
       pager.redraw = true
   of READ_LINE:
     if container == pager.container:
-      pager.setLineEdit(readLine(event.prompt, pager.attrs.width, current = event.value, hide = event.password, term = pager.term), BUFFER)
+      pager.setLineEdit("(BUFFER) " & event.prompt, BUFFER, event.value, hide = event.password)
   of READ_AREA:
     if container == pager.container:
       var s = event.tvalue
diff --git a/src/io/lineedit.nim b/src/io/lineedit.nim
index 2cd76fff..d19fe71e 100644
--- a/src/io/lineedit.nim
+++ b/src/io/lineedit.nim
@@ -14,6 +14,9 @@ type
   LineEditState* = enum
     EDIT, FINISH, CANCEL
 
+  LineHistory* = ref object
+    lines: seq[string]
+
   LineEdit* = ref object
     news*: seq[Rune]
     prompt*: string
@@ -24,39 +27,17 @@ type
     cursor: int
     shift: int
     minlen: int
-    maxlen: int
+    maxwidth: int
     displen: int
     disallowed: set[char]
     hide: bool
     term: Terminal
+    hist: LineHistory
+    histindex: int
+    histtmp: string
 
-proc printesc(edit: LineEdit, rs: seq[Rune]) =
-  var s = ""
-  var format = newFormat()
-  var cformat = newFormat()
-  cformat.fgcolor = ColorsANSIFg[4] # blue
-  var dformat = newFormat() # reset
-  for r in rs:
-    if r.isControlChar():
-      s &= edit.term.processFormat(format, cformat)
-    else:
-      s &= edit.term.processFormat(format, dformat)
-    s &= r
-  edit.term.write(s)
-
-proc printesc(edit: LineEdit, s: string) =
-  var s = ""
-  var format = newFormat()
-  var cformat = newFormat()
-  cformat.fgcolor = ColorsANSIFg[4] # blue
-  var dformat = newFormat() # reset
-  for r in s.runes:
-    if r.isControlChar():
-      s &= edit.term.processFormat(format, cformat)
-    else:
-      s &= edit.term.processFormat(format, dformat)
-    s &= r
-  edit.term.write(s)
+func newLineHistory*(): LineHistory =
+  return LineHistory()
 
 func lwidth(r: Rune): int =
   if r.isControlChar():
@@ -84,6 +65,33 @@ func lwidth(s: seq[Rune], min: int): int =
     result += lwidth(s[i])
     inc i
 
+const colorFormat = (func(): Format =
+  result = newFormat()
+  result.fgcolor = ColorsANSIFg[4] # blue
+)()
+const defaultFormat = newFormat()
+proc printesc(edit: LineEdit, rs: seq[Rune]) =
+  var s = ""
+  var format = newFormat()
+  for r in rs:
+    if r.isControlChar():
+      s &= edit.term.processFormat(format, colorFormat)
+    else:
+      s &= edit.term.processFormat(format, defaultFormat)
+    s &= r
+  edit.term.write(s)
+
+proc printesc(edit: LineEdit, s: string) =
+  var s = ""
+  var format = newFormat()
+  for r in s.runes:
+    if r.isControlChar():
+      s &= edit.term.processFormat(format, colorFormat)
+    else:
+      s &= edit.term.processFormat(format, defaultFormat)
+    s &= r
+  edit.term.write(s)
+
 template kill0(edit: LineEdit, i: int) =
   edit.space(i)
   edit.backward0(i)
@@ -106,7 +114,7 @@ proc space(edit: LineEdit, i: int) =
   edit.term.write(' '.repeat(i))
 
 proc generateOutput*(edit: LineEdit): FixedGrid =
-  result = newFixedGrid(edit.promptw + edit.maxlen)
+  result = newFixedGrid(edit.promptw + edit.maxwidth)
   let os = edit.news.substr(edit.shift, edit.shift + edit.displen)
   var x = 0
   for r in edit.prompt.runes():
@@ -114,8 +122,9 @@ proc generateOutput*(edit: LineEdit): FixedGrid =
     x += r.lwidth()
   if edit.hide:
     for r in os:
-      result[x].str = "*"
-      x += r.lwidth()
+      let w = r.lwidth()
+      result[x].str = '*'.repeat(w)
+      x += w
       if x >= result.width: break
   else:
     for r in os:
@@ -127,10 +136,10 @@ proc getCursorX*(edit: LineEdit): int =
   return edit.promptw + edit.news.lwidth(edit.shift, edit.cursor)
 
 proc redraw(state: LineEdit) =
-  var dispw = state.news.lwidth(state.shift, state.shift + state.displen)
   if state.shift + state.displen > state.news.len:
     state.displen = state.news.len - state.shift
-  while dispw > state.maxlen - 1:
+  var dispw = state.news.lwidth(state.shift, state.shift + state.displen)
+  while dispw > state.maxwidth - 1:
     dispw -= state.news[state.shift + state.displen - 1].lwidth()
     dec state.displen
   state.begin0()
@@ -139,52 +148,55 @@ proc redraw(state: LineEdit) =
     state.printesc('*'.repeat(os.lwidth()))
   else:
     state.printesc(os)
-  state.space(max(state.maxlen - state.minlen - os.lwidth(), 0))
+  state.space(max(state.maxwidth - state.minlen - os.lwidth(), 0))
   state.begin0()
   state.forward0(state.news.lwidth(state.shift, state.cursor))
 
 proc zeroShiftRedraw(state: LineEdit) =
   state.shift = 0
-  state.displen = state.maxlen - 1
+  state.displen = state.news.len
   state.redraw()
 
-proc fullRedraw*(state: LineEdit) =
-  state.displen = state.maxlen - 1
+proc fullRedraw(state: LineEdit) =
+  state.displen = state.news.len
   if state.cursor > state.shift:
     var shiftw = state.news.lwidth(state.shift, state.cursor)
-    while shiftw > state.maxlen - 1:
+    while shiftw > state.maxwidth - 1:
       inc state.shift
       shiftw -= state.news[state.shift].lwidth()
   else:
     state.shift = max(state.cursor - 1, 0)
   state.redraw()
 
-proc insertCharseq(state: LineEdit, cs: var seq[Rune], disallowed: set[char]) =
-  let escNext = state.escNext
-  cs.keepIf((r) => (escNext or not r.isControlChar) and not (r.isAscii and char(r) in disallowed))
-  state.escNext = false
+proc insertCharseq(edit: LineEdit, cs: var seq[Rune]) =
+  let escNext = edit.escNext
+  cs.keepIf((r) => (escNext or not r.isControlChar) and not (r.isAscii and char(r) in edit.disallowed))
+  edit.escNext = false
   if cs.len == 0:
     return
 
-  if state.cursor >= state.news.len and state.news.lwidth(state.shift, state.cursor) + cs.lwidth() < state.displen:
-    state.news &= cs
-    state.cursor += cs.len
-    if state.hide:
-      state.printesc('*'.repeat(cs.lwidth()))
+  if edit.cursor >= edit.news.len and edit.news.lwidth(edit.shift, edit.cursor) + cs.lwidth() < edit.maxwidth:
+    edit.news &= cs
+    edit.cursor += cs.len
+    if edit.hide:
+      edit.printesc('*'.repeat(cs.lwidth()))
     else:
-      state.printesc(cs)
+      edit.printesc(cs)
   else:
-    state.news.insert(cs, state.cursor)
-    state.cursor += cs.len
-    state.fullRedraw()
+    edit.news.insert(cs, edit.cursor)
+    edit.cursor += cs.len
+    edit.fullRedraw()
 
-proc cancel*(edit: LineEdit) {.jsfunc.} =
+proc cancel(edit: LineEdit) {.jsfunc.} =
   edit.state = CANCEL
 
-proc submit*(edit: LineEdit) {.jsfunc.} =
+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.} =
+proc backspace(edit: LineEdit) {.jsfunc.} =
   if edit.cursor > 0:
     let w = edit.news[edit.cursor - 1].lwidth()
     edit.news.delete(edit.cursor - 1..edit.cursor - 1)
@@ -198,10 +210,10 @@ proc backspace*(edit: LineEdit) {.jsfunc.} =
 proc write*(edit: LineEdit, s: string): bool {.jsfunc.} =
   if validateUtf8(s) == -1:
     var cs = s.toRunes()
-    edit.insertCharseq(cs, edit.disallowed)
+    edit.insertCharseq(cs)
     return true
 
-proc delete*(edit: LineEdit) {.jsfunc.} =
+proc delete(edit: LineEdit) {.jsfunc.} =
   if edit.cursor >= 0 and edit.cursor < edit.news.len:
     let w = edit.news[edit.cursor].lwidth()
     edit.news.delete(edit.cursor..edit.cursor)
@@ -210,21 +222,21 @@ proc delete*(edit: LineEdit) {.jsfunc.} =
     else:
       edit.fullRedraw()
 
-proc escape*(edit: LineEdit) {.jsfunc.} =
+proc escape(edit: LineEdit) {.jsfunc.} =
   edit.escNext = true
 
-proc clear*(edit: LineEdit) {.jsfunc.} =
+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.} =
+proc kill(edit: LineEdit) {.jsfunc.} =
   if edit.cursor < edit.news.len:
     edit.kill0()
     edit.news.setLen(edit.cursor)
 
-proc backward*(edit: LineEdit) {.jsfunc.} =
+proc backward(edit: LineEdit) {.jsfunc.} =
   if edit.cursor > 0:
     dec edit.cursor
     if edit.cursor > edit.shift or edit.shift == 0:
@@ -232,10 +244,10 @@ proc backward*(edit: LineEdit) {.jsfunc.} =
     else:
       edit.fullRedraw()
 
-proc forward*(edit: LineEdit) {.jsfunc.} =
+proc forward(edit: LineEdit) {.jsfunc.} =
   if edit.cursor < edit.news.len:
     inc edit.cursor
-    if edit.news.lwidth(edit.shift, edit.cursor) < edit.displen:
+    if edit.news.lwidth(edit.shift, edit.cursor) < edit.maxwidth:
       var n = 1
       if edit.news.len > edit.cursor:
         n = edit.news[edit.cursor].lwidth()
@@ -243,7 +255,7 @@ proc forward*(edit: LineEdit) {.jsfunc.} =
     else:
       edit.fullRedraw()
 
-proc prevWord*(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} =
+proc prevWord(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} =
   let oc = edit.cursor
   while edit.cursor > 0:
     dec edit.cursor
@@ -255,8 +267,9 @@ proc prevWord*(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} =
     else:
       edit.fullRedraw()
 
-proc nextWord*(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} =
+proc nextWord(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} =
   let oc = edit.cursor
+  let ow = edit.news.lwidth(edit.shift, edit.cursor)
   while edit.cursor < edit.news.len:
     inc edit.cursor
     if edit.cursor < edit.news.len:
@@ -264,12 +277,12 @@ proc nextWord*(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} =
         break
   if edit.cursor != oc:
     let dw = edit.news.lwidth(oc, edit.cursor)
-    if oc + dw - edit.shift < edit.displen:
+    if ow + dw < edit.maxwidth:
       edit.forward0(dw)
     else:
       edit.fullRedraw()
 
-proc clearWord*(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} =
+proc clearWord(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} =
   var i = edit.cursor
   if i > 0:
     # point to the previous character
@@ -284,7 +297,7 @@ proc clearWord*(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} =
     edit.cursor = i
     edit.fullRedraw()
 
-proc killWord*(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} =
+proc killWord(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} =
   var i = edit.cursor
   if i < edit.news.len and edit.news[i].breaksWord(check):
     inc i
@@ -296,35 +309,49 @@ proc killWord*(edit: LineEdit, check = none(BoundaryFunction)) {.jsfunc.} =
     edit.news.delete(edit.cursor..<i)
     edit.fullRedraw()
 
-proc begin*(edit: LineEdit) {.jsfunc.} =
+proc begin(edit: LineEdit) {.jsfunc.} =
   if edit.cursor > 0:
     if edit.shift == 0:
       edit.backward0(edit.news.lwidth(0, edit.cursor))
+      edit.cursor = 0
     else:
+      edit.cursor = 0
       edit.fullRedraw()
-    edit.cursor = 0
 
-proc `end`*(edit: LineEdit) {.jsfunc.} =
+proc `end`(edit: LineEdit) {.jsfunc.} =
   if edit.cursor < edit.news.len:
-    if edit.news.lwidth(edit.shift, edit.news.len) < edit.maxlen:
+    if edit.news.lwidth(edit.shift, edit.news.len) < edit.maxwidth:
       edit.forward0(edit.news.lwidth(edit.cursor, edit.news.len))
+      edit.cursor = edit.news.len
     else:
+      edit.cursor = edit.news.len
       edit.fullRedraw()
-    edit.cursor = edit.news.len
 
-proc writePrompt*(edit: LineEdit) =
-  edit.printesc(edit.prompt)
+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.end()
+    edit.fullRedraw()
 
-proc writeStart*(edit: LineEdit) =
-  edit.writePrompt()
-  if edit.hide:
-    edit.printesc('*'.repeat(edit.current.lwidth()))
-  else:
-    edit.printesc(edit.current)
+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.end()
+    edit.fullRedraw()
+  elif edit.histindex < edit.hist.lines.len:
+    inc edit.histindex
+    edit.news = edit.histtmp.toRunes()
+    edit.end()
+    edit.fullRedraw()
+    edit.histtmp = ""
 
 proc readLine*(prompt: string, termwidth: int, current = "",
                disallowed: set[char] = {}, hide = false,
-               term: Terminal): LineEdit =
+               term: Terminal, hist: LineHistory): LineEdit =
   result = LineEdit(
     prompt: prompt,
     promptw: prompt.lwidth(),
@@ -336,8 +363,10 @@ proc readLine*(prompt: string, termwidth: int, current = "",
     term: term
   )
   result.cursor = result.news.lwidth()
-  result.maxlen = termwidth - result.promptw
-  result.displen = result.maxlen - 1
+  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)