about summary refs log tree commit diff stats
path: root/src/display/pager.nim
diff options
context:
space:
mode:
Diffstat (limited to 'src/display/pager.nim')
-rw-r--r--src/display/pager.nim380
1 files changed, 371 insertions, 9 deletions
diff --git a/src/display/pager.nim b/src/display/pager.nim
index 9fc8957d..22574998 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -1,16 +1,39 @@
+import options
+import os
+import terminal
+import unicode
+
 import config/config
 import io/buffer
+import io/cell
+import io/lineedit
+import io/loader
+import io/request
+import io/term
 import js/javascript
+import js/regex
+import types/url
+import utils/twtstr
 
 type
   Container = ref object
-    buffer: Buffer
+    buffer*: Buffer
     children: seq[Container]
+    pos: CursorPosition
+    parent: Container
+    sourcepair: Container
+    needsauth*: bool #TODO move to buffer?
+    redirecturl: Option[URL]
 
   Pager* = ref object
-    rootContainer: Container
-    container: Container
+    attrs: TermAttributes
+    commandMode*: bool
+    container*: Container
     config: Config
+    loader: FileLoader
+    regex: Option[Regex]
+    reverseSearch: bool
+    status*: seq[string]
 
 proc cursorLeft(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorLeft()
 proc cursorDown(pager: Pager) {.jsfunc.} = pager.container.buffer.cursorDown()
@@ -45,18 +68,357 @@ proc lineInfo(pager: Pager) {.jsfunc.} = pager.container.buffer.lineInfo()
 proc reshape(pager: Pager) {.jsfunc.} = pager.container.buffer.reshape = true
 proc redraw(pager: Pager) {.jsfunc.} = pager.container.buffer.redraw = true
 
-proc newContainer(): Container =
+proc searchNext(pager: Pager) {.jsfunc.} =
+  if pager.regex.issome:
+    if not pager.reverseSearch:
+      discard pager.container.buffer.cursorNextMatch(pager.regex.get)
+    else:
+      discard pager.container.buffer.cursorPrevMatch(pager.regex.get)
+
+proc searchPrev(pager: Pager) {.jsfunc.} =
+  if pager.regex.issome:
+    if not pager.reverseSearch:
+      discard pager.container.buffer.cursorPrevMatch(pager.regex.get)
+    else:
+      discard pager.container.buffer.cursorNextMatch(pager.regex.get)
+
+proc statusMode(pager: Pager) =
+  print(HVP(pager.attrs.height + 1, 1))
+  print(EL())
+
+proc search(pager: Pager) {.jsfunc.} =
+  pager.statusMode()
+  var iput: string
+  let status = readLine("/", iput, pager.attrs.width, config = pager.config)
+  if status:
+    if iput.len != 0:
+      pager.regex = compileSearchRegex(iput)
+    pager.reverseSearch = false
+    pager.searchNext()
+
+proc searchBack(pager: Pager) {.jsfunc.} =
+  pager.statusMode()
+  var iput: string
+  let status = readLine("?", iput, pager.attrs.width, config = pager.config)
+  if status:
+    if iput.len != 0:
+      pager.regex = compileSearchRegex(iput)
+    pager.reverseSearch = true
+    pager.searchNext()
+
+proc displayPage*(pager: Pager) =
+  let buffer = pager.container.buffer
+  if buffer.refreshBuffer():
+    stdout.hideCursor()
+    print(buffer.generateFullOutput())
+    stdout.showCursor()
+
+proc isearch(pager: Pager) {.jsfunc.} =
+  pager.statusMode()
+  var iput: string
+  let cpos = pager.container.buffer.cpos
+  var mark: Mark
+  template del_mark() =
+    if mark != nil:
+      pager.container.buffer.removeMark(mark)
+
+  let status = readLine("/", iput, pager.attrs.width, {}, false, pager.config, (proc(state: var LineState): bool =
+    del_mark
+    let regex = compileSearchRegex($state.news)
+    pager.container.buffer.cpos = cpos
+    if regex.issome:
+      let match = pager.container.buffer.cursorNextMatch(regex.get)
+      if match.success:
+        mark = pager.container.buffer.addMark(match.x, match.y, match.str.width())
+        pager.container.buffer.redraw = true
+        pager.container.buffer.refreshBuffer(true)
+        pager.displayPage()
+        print(HVP(pager.attrs.height + 1, 2))
+        print(SGR())
+      else:
+        del_mark
+        pager.container.buffer.redraw = true
+        pager.container.buffer.refreshBuffer(true)
+        pager.displayPage()
+        print(HVP(pager.attrs.height + 1, 2))
+        print(SGR())
+      return true
+    false
+  ))
+
+  del_mark
+  pager.container.buffer.redraw = true
+  pager.container.buffer.refreshBuffer(true)
+  if status:
+    pager.regex = compileSearchRegex(iput)
+  else:
+    pager.container.buffer.cpos = cpos
+
+proc isearchBack(pager: Pager) {.jsfunc.} =
+  pager.statusMode()
+  var iput: string
+  let cpos = pager.container.buffer.cpos
+  var mark: Mark
+  template del_mark() =
+    if mark != nil:
+      pager.container.buffer.removeMark(mark)
+  let status = readLine("?", iput, pager.container.buffer.width, {}, false, pager.config, (proc(state: var LineState): bool =
+    del_mark
+    let regex = compileSearchRegex($state.news)
+    pager.container.buffer.cpos = cpos
+    if regex.issome:
+      let match = pager.container.buffer.cursorPrevMatch(regex.get)
+      if match.success:
+        mark = pager.container.buffer.addMark(match.x, match.y, match.str.width())
+        pager.container.buffer.redraw = true
+        pager.container.buffer.refreshBuffer(true)
+        pager.displayPage()
+        print(HVP(pager.attrs.height + 1, 2))
+        print(SGR())
+      else:
+        del_mark
+        pager.container.buffer.redraw = true
+        pager.container.buffer.refreshBuffer(true)
+        pager.displayPage()
+        print(HVP(pager.attrs.height + 1, 2))
+        print(SGR())
+      return true
+    false
+  ))
+  del_mark
+  pager.container.buffer.redraw = true
+  if status:
+    pager.regex = compileSearchRegex(iput)
+  else:
+    pager.container.buffer.cpos = cpos
+
+proc newContainer(buffer: Buffer, parent: Container): Container =
   new(result)
+  result.buffer = buffer
+  result.parent = parent
 
-proc newPager*(config: Config, buffer: Buffer): Pager =
+proc newPager*(config: Config, attrs: TermAttributes, loader: FileLoader): Pager =
+  new(result)
   result.config = config
-  result.rootContainer = newContainer()
+  result.attrs = attrs
+  result.loader = loader
 
 proc addBuffer*(pager: Pager, buffer: Buffer) =
-  var ncontainer = newContainer()
-  ncontainer.buffer = buffer
-  pager.container.children.add(ncontainer)
+  var ncontainer = newContainer(buffer, pager.container)
+  if pager.container != nil:
+    pager.container.children.add(ncontainer)
   pager.container = ncontainer
 
+proc dupeBuffer*(pager: Pager, location = none(URL)) {.jsfunc.} =
+  var clone: Buffer
+  clone = pager.container.buffer.dupeBuffer(location)
+  pager.addBuffer(clone)
+
+# The prevBuffer and nextBuffer procedures emulate w3m's PREV and NEXT
+# commands by traversing the container tree in a depth-first order.
+proc prevBuffer*(pager: Pager): bool {.jsfunc.} =
+  if pager.container == nil:
+    return false
+  if pager.container.parent == nil:
+    return false
+  for i in 0..pager.container.parent.children.high:
+    let child = pager.container.parent.children[i]
+    if child == pager.container:
+      if i > 0:
+        pager.container = pager.container.parent.children[i - 1]
+      else:
+        pager.container = pager.container.parent
+      return true
+  assert false, "Container not a child of its parent"
+
+proc nextBuffer*(pager: Pager): bool {.jsfunc.} =
+  if pager.container == nil:
+    return false
+  if pager.container.children.len > 0:
+    pager.container = pager.container.children[0]
+    return true
+  if pager.container.parent == nil:
+    return false
+  for i in countdown(pager.container.parent.children.high, 0):
+    let child = pager.container.parent.children[i]
+    if child == pager.container:
+      if i < pager.container.parent.children.high:
+        pager.container = pager.container.parent.children[i + 1]
+        return true
+      return false
+  assert false, "Container not a child of its parent"
+
+#TODO we should have a separate status message stack for all buffers AND the
+# pager.
+proc setStatusMessage(pager: Pager, msg: string) =
+  if pager.container != nil:
+    pager.container.buffer.setStatusMessage(msg)
+  else:
+    pager.status.add(msg)
+
+proc discardBuffer*(pager: Pager) {.jsfunc.} =
+  if pager.container.parent == nil and pager.container.children.len == 0:
+    pager.setStatusMessage("Cannot discard last buffer!")
+  else:
+    if pager.container.parent != nil:
+      let parent = pager.container.parent
+      let n = parent.children.find(pager.container)
+      assert n != -1, "Container not a child of its parent"
+      for i in countdown(pager.container.children.high, 0):
+        let child = pager.container.children[i]
+        child.parent = pager.container.parent
+        parent.children.insert(child, n + 1)
+      parent.children.delete(n)
+      pager.container = parent
+    else:
+      pager.container = pager.container.children[0]
+      pager.container.parent = nil
+
+proc drawBuffer*(pager: Pager) {.jsfunc.} =
+  pager.container.buffer.drawBuffer() #TODO move this to pager
+
+proc toggleSource*(pager: Pager) {.jsfunc.} =
+  if pager.container.sourcepair != nil:
+    pager.container = pager.container.sourcepair
+  else:
+    let buffer = newBuffer(pager.config, pager.loader)
+    buffer.source = pager.container.buffer.source
+    buffer.streamclosed = true
+    buffer.location = pager.container.buffer.location
+    buffer.ispipe = pager.container.buffer.ispipe
+    if pager.container.buffer.contenttype == "text/plain":
+      buffer.contenttype = "text/html"
+    else:
+      buffer.contenttype = "text/plain"
+    buffer.setupBuffer()
+    let container = newContainer(buffer, pager.container)
+    container.sourcepair = pager.container
+    pager.container.sourcepair = container
+    pager.container.children.add(container)
+
+# Load request in a new buffer.
+proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL), force = false, ctype = "", replace = false): bool {.discardable.} =
+  if force or prevurl.isnone or not prevurl.get.equals(request.url, true) or
+      request.url.hash == "" or request.httpmethod != HTTP_GET:
+    # Basically, we want to reload the page *only* when
+    # a) force == true
+    # b) or the new URL isn't just the old URL + an anchor
+    # I think this makes navigation pretty natural, or at least very close to
+    # what other browsers do. Still, it would be nice if we got some visual
+    # feedback on what is actually going to happen when typing a URL; TODO.
+    let response = pager.loader.doRequest(request)
+    if response.body != nil:
+      let buffer = newBuffer(pager.config, pager.loader)
+      buffer.contenttype = if ctype != "": ctype else: response.contenttype
+      buffer.istream = response.body
+      buffer.location = request.url
+      buffer.setupBuffer()
+      if replace:
+        pager.discardBuffer()
+      pager.addBuffer(buffer)
+      pager.container.needsauth = response.status == 401 # Unauthorized
+      pager.container.redirecturl = response.redirect
+    else:
+      pager.setStatusMessage("Couldn't load " & $request.url & " (" & $response.res & ")")
+      return false
+  else:
+    if pager.container.buffer.hasAnchor(request.url.anchor):
+      pager.dupeBuffer(request.url.some)
+    else:
+      pager.setStatusMessage("Couldn't find anchor " & request.url.anchor)
+      return false
+  return true
+
+# When the user has passed a partial URL as an argument, they might've meant
+# either:
+# * file://$PWD/<file>
+# * https://<url>
+# So we attempt to load both, and see what works.
+# (TODO: make this optional)
+proc loadURL*(pager: Pager, url: string, force = false, ctype = "") =
+  let firstparse = parseURL(url)
+  if firstparse.issome:
+    let prev = if pager.container != nil:
+      some(pager.container.buffer.location)
+    else:
+      none(URL)
+    pager.gotoURL(newRequest(firstparse.get), prev, force, ctype)
+    return
+  let cdir = parseURL("file://" & getCurrentDir() & DirSep)
+  let newurl = parseURL(url, cdir)
+  if newurl.isSome:
+    # attempt to load local file
+    if pager.gotoURL(newRequest(newurl.get), force = force, ctype = ctype):
+      return
+  block:
+    let purl = percentEncode(url, LocalPathPercentEncodeSet)
+    if purl != url:
+      let newurl = parseURL(purl, cdir)
+      if newurl.isSome:
+        if pager.gotoURL(newRequest(newurl.get), force = force, ctype = ctype):
+          pager.status.setLen(0)
+          return
+  block:
+    let newurl = parseURL("https://" & url)
+    if newurl.isSome:
+      # attempt to load remote page
+      if pager.gotoURL(newRequest(newurl.get), force = force, ctype = ctype):
+        pager.status.setLen(0)
+        return
+  pager.setStatusMessage("Invalid URL " & url)
+
+# Open a URL prompt and visit the specified URL.
+proc changeLocation(pager: Pager) {.jsfunc.} =
+  var url = pager.container.buffer.location.serialize()
+  pager.statusMode()
+  let status = readLine("URL: ", url, pager.attrs.width, config = pager.config)
+  if status:
+    pager.loadURL(url)
+
+# Reload the page in a new buffer, then kill the previous buffer.
+proc reloadPage(pager: Pager) {.jsfunc.} =
+  pager.gotoURL(newRequest(pager.container.buffer.location), none(URL), true, pager.container.buffer.contenttype, true)
+
+proc click(pager: Pager) {.jsfunc.} =
+  #TODO this conflicts with the planned event loop
+  let req = pager.container.buffer.click()
+  if req.issome:
+    pager.gotoURL(req.get, pager.container.buffer.location.some)
+
+proc followRedirect*(pager: Pager)
+
+proc checkAuth*(pager: Pager) =
+  if pager.container != nil and pager.container.needsauth:
+    pager.container.buffer.refreshBuffer()
+    pager.statusMode()
+    var username = ""
+    let ustatus = readLine("Username: ", username, pager.attrs.width, config = pager.config)
+    if not ustatus:
+      pager.container.needsauth = false
+      return
+    pager.statusMode()
+    var password = ""
+    let pstatus = readLine("Password: ", password, pager.attrs.width, hide = true, config = pager.config)
+    if not pstatus:
+      pager.container.needsauth = false
+      return
+    var url = pager.container.buffer.location
+    url.username = username
+    url.password = password
+    pager.gotoURL(newRequest(url), prevurl = some(pager.container.buffer.location), replace = true)
+    pager.followRedirect()
+
+proc followRedirect*(pager: Pager) =
+  while pager.container != nil and pager.container.redirecturl.issome:
+    pager.statusMode()
+    print("Redirecting to ", $pager.container.redirecturl.get)
+    stdout.flushFile()
+    pager.container.buffer.refreshBuffer(true)
+    let redirecturl = pager.container.redirecturl.get
+    pager.container.redirecturl = none(URL)
+    pager.gotoURL(newRequest(redirecturl), prevurl = some(pager.container.buffer.location), replace = true)
+    if pager.container.needsauth:
+      pager.checkAuth()
+
 proc addPagerModule*(ctx: JSContext) =
   ctx.registerType(Pager)