about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--bonus/w3m.toml77
-rw-r--r--doc/config.md22
-rw-r--r--res/config.toml6
-rw-r--r--src/bindings/quickjs.nim4
-rw-r--r--src/buffer/container.nim125
-rw-r--r--src/config/config.nim18
-rw-r--r--src/display/client.nim15
-rw-r--r--src/display/pager.nim110
-rw-r--r--src/js/javascript.nim36
9 files changed, 264 insertions, 149 deletions
diff --git a/bonus/w3m.toml b/bonus/w3m.toml
new file mode 100644
index 00000000..9acc887f
--- /dev/null
+++ b/bonus/w3m.toml
@@ -0,0 +1,77 @@
+# w3m-like keybindings for Chawan.
+# Copy-paste this into your config file, or just include it (place it in
+# ~/.config/chawan/w3m.toml, then at the beginning of
+# ~/.config/chawan/chawan.toml, include = "w3m.toml".)
+
+[page]
+# Page/cursor movement
+' ' = 'pager.pageDown()'
+C-v = 'pager.pageDown()'
+b = 'pager.pageUp()'
+M-v = 'pager.pageUp()'
+l = 'pager.cursorRight()'
+h = 'pager.cursorLeft()'
+j = 'pager.cursorDown()'
+k = 'pager.cursorUp()'
+C-f = 'pager.cursorRight()'
+C-b = 'pager.cursorLeft()'
+C-n = 'pager.cursorDown()'
+C-p = 'pager.cursorUp()'
+J = 'pager.scrollUp()'
+K = 'pager.scrollDown()'
+'^' = 'pager.cursorLineBegin()'
+C-a = 'pager.cursorLineBegin()'
+'$' = 'pager.cursorLineEnd()'
+C-e = 'pager.cursorLineEnd()'
+w = 'pager.cursorNextWord()'
+W = 'pager.cursorPrevWord()'
+'<' = 'pager.pageLeft()'
+'>' = 'pager.pageRight()'
+'.' = 'pager.scrollLeft()'
+',' = 'pager.scrollRight()'
+g = 'pager.cursorFirstLine()'
+'M-<' = 'pager.cursorFirstLine()'
+G = 'pager.cursorLastLine()'
+'M->' = 'pager.cursorLastLine()'
+M-g = 'pager.gotoLine()'
+Z = 'pager.centerColumn()'
+z = 'pager.centerLine()'
+C-i = 'pager.cursorNextLink()'
+C-u = 'pager.cursorPrevLink()'
+M-C-i = 'pager.cursorPrevLink()'
+'[' = 'pager.cursorFirstLine();pager.cursorLineBegin();pager.cursorNextLink()'
+']' = 'pager.cursorLastLine();pager.cursorLineEnd();pager.cursorPrevLink()'
+# Hyperlink selection
+C-j = 'pager.click()'
+C-m = 'pager.click()'
+c = 'pager.peek()'
+u = 'pager.peekCursor()'
+#TODO download, etc
+# File and URL-related actions
+U = 'pager.load()'
+V = 'pager.load()' #TODO file only
+#TODO exec shell
+# Buffer operations
+B = 'pager.discardBuffer()'
+v = 'pager.toggleSource()'
+#TODO edit
+C-l = 'pager.redraw()'
+R = 'pager.reload()'
+#TODO save, save source, view in editor
+#TODO buffer selection mode
+#TODO mark
+# Search
+'/' = 'pager.searchForward()'
+C-s = 'pager.searchForward()'
+'?' = 'pager.searchBackward()'
+C-r = 'pager.searchBackward()'
+n = 'pager.searchNext()'
+N = 'pager.searchPrev()'
+C-w = 'config.searchwrap = !config.searchwrap; pager.alert("Wrap search " + (config.searchwrap ? "on" : "off"))'
+# Misc
+#TODO shell out, help file, options, cookies
+C-c = 'pager.cancel()'
+q = 'pager.quit()' #TODO confirm
+Q = 'pager.quit()'
+
+# w3m line editing is equivalent to Chawan's defaults.
diff --git a/doc/config.md b/doc/config.md
index 65ea2fb0..5aec7a5d 100644
--- a/doc/config.md
+++ b/doc/config.md
@@ -559,7 +559,7 @@ open the current buffer's contents as HTML.</td>
 
 <tr>
 <td>`pager.cursorMiddle()`</td>
-<td>Move to the middle of the screen. (Equivalent to M in vi.)</td>
+<td>Move to the line in the middle of the screen. (Equivalent to M in vi.)</td>
 </tr>
 
 <tr>
@@ -573,6 +573,26 @@ open the current buffer's contents as HTML.</td>
 </tr>
 
 <tr>
+<td>`pager.cursorLeftEdge()`</td>
+<td>Move to the first column on the screen.</td>
+</tr>
+
+<tr>
+<td>`pager.cursorMiddleColumn()`</td>
+<td>Move to the column in the middle of the screen.</td>
+</tr>
+
+<tr>
+<td>`pager.cursorRightEdge()`</td>
+<td>Move to the last column on the screen.</td>
+</tr>
+
+<tr>
+<td>`pager.centerColumn()`</td>
+<td>Center screen around the current column.</td>
+</tr>
+
+<tr>
 <td>`pager.lineInfo()`</td>
 <td>Display information about the current line.</td>
 </tr>
diff --git a/res/config.toml b/res/config.toml
index dd15aebc..27589198 100644
--- a/res/config.toml
+++ b/res/config.toml
@@ -3,6 +3,9 @@ visual-home = "about:chawan"
 run-script = ""
 headless = false
 
+[search]
+wrap = true
+
 [external]
 tmpdir = "/tmp/cha"
 editor = "vi %s +%d"
@@ -46,7 +49,7 @@ H = 'pager.cursorTop()'
 M = 'pager.cursorMiddle()'
 L = 'pager.cursorBottom()'
 ';' = 'pager.cursorLeftEdge()'
-'+' = 'pager.cursorVertMiddle()'
+'+' = 'pager.cursorMiddleColumn()'
 '@' = 'pager.cursorRightEdge()'
 C-d = 'pager.halfPageDown()'
 C-u = 'pager.halfPageUp()'
@@ -93,6 +96,7 @@ n = 'pager.searchNext()'
 N = 'pager.searchPrev()'
 c = 'pager.peek()'
 u = 'pager.peekCursor()'
+C-w = 'config.searchwrap = !config.searchwrap; pager.alert("Wrap search " + (config.searchwrap ? "on" : "off"))'
 
 [line]
 C-m = 'line.submit()'
diff --git a/src/bindings/quickjs.nim b/src/bindings/quickjs.nim
index cfef9693..a606ede7 100644
--- a/src/bindings/quickjs.nim
+++ b/src/bindings/quickjs.nim
@@ -85,7 +85,8 @@ else:
 type
   JSRuntime* = ptr object
   JSContext* = ptr object
-  JSCFunction* = proc (ctx: JSContext, this_val: JSValue, argc: int, argv: ptr JSValue): JSValue {.cdecl.}
+  JSCFunction* = proc (ctx: JSContext, this_val: JSValue, argc: cint, argv: ptr JSValue): JSValue {.cdecl.}
+  JSCFunctionData* = proc (ctx: JSContext, this_val: JSValue, argc: cint, argv: ptr JSValue, magic: cint, func_data: ptr JSValue): JSValue {.cdecl.}
   JSGetterFunction* = proc(ctx: JSContext, this_val: JSValue): JSValue {.cdecl.}
   JSSetterFunction* = proc(ctx: JSContext, this_val: JSValue, val: JSValue): JSValue {.cdecl.}
   JSGetterMagicFunction* = proc(ctx: JSContext, this_val: JSValue, magic: cint): JSValue {.cdecl.}
@@ -339,6 +340,7 @@ proc JS_FreeAtom*(ctx: JSContext, atom: JSAtom)
 proc JS_FreeAtomRT*(rt: JSRuntime, atom: JSAtom)
 
 proc JS_NewCFunction2*(ctx: JSContext, cfunc: JSCFunction, name: cstring, length: cint, proto: JSCFunctionEnum, magic: cint): JSValue
+proc JS_NewCFunctionData*(ctx: JSContext, cfunc: JSCFunctionData, length: cint, magic: cint, data_len: cint, data: ptr JSValue): JSValue
 proc JS_NewCFunction*(ctx: JSContext, cfunc: JSCFunction, name: cstring, length: cint): JSValue
 
 proc JS_NewString*(ctx: JSContext, str: cstring): JSValue
diff --git a/src/buffer/container.nim b/src/buffer/container.nim
index f85ed39e..e6a65550 100644
--- a/src/buffer/container.nim
+++ b/src/buffer/container.nim
@@ -2,6 +2,7 @@ import deques
 import options
 import streams
 import strformat
+import strutils
 import unicode
 
 when defined(posix):
@@ -68,11 +69,11 @@ type
     config*: BufferConfig
     iface*: BufferInterface
     attrs: WindowAttributes
-    width*: int
-    height*: int
-    contenttype*: Option[string]
-    title*: string
-    hovertext*: array[HoverType, string]
+    width* {.jsget.}: int
+    height* {.jsget.}: int
+    contenttype* {.jsget.}: Option[string]
+    title: string
+    hovertext: array[HoverType, string]
     lastpeek: HoverType
     source*: BufferSource
     pos: CursorPosition
@@ -134,8 +135,8 @@ func cursorx*(container: Container): int {.inline.} = container.pos.cursorx
 func cursory*(container: Container): int {.inline.} = container.pos.cursory
 func fromx*(container: Container): int {.inline.} = container.pos.fromx
 func fromy*(container: Container): int {.inline.} = container.pos.fromy
-func xend*(container: Container): int {.inline.} = container.pos.xend
-func lastVisibleLine*(container: Container): int = min(container.fromy + container.height, container.numLines) - 1
+func xend(container: Container): int {.inline.} = container.pos.xend
+func lastVisibleLine(container: Container): int = min(container.fromy + container.height, container.numLines) - 1
 
 func acursorx*(container: Container): int =
   max(0, container.cursorx - container.fromx)
@@ -143,7 +144,7 @@ func acursorx*(container: Container): int =
 func acursory*(container: Container): int =
   container.cursory - container.fromy
 
-func currentLine*(container: Container): string =
+func currentLine(container: Container): string =
   return container.getLine(container.cursory).str
 
 func cursorBytes(container: Container, y: int, cc = container.cursorx): int =
@@ -159,7 +160,7 @@ func cursorBytes(container: Container, y: int, cc = container.cursorx): int =
 func currentCursorBytes(container: Container, cc = container.cursorx): int =
   return container.cursorBytes(container.cursory, cc)
 
-func prevWidth*(container: Container): int =
+func prevWidth(container: Container): int =
   if container.numLines == 0: return 0
   let line = container.currentLine
   if line.len == 0: return 0
@@ -175,7 +176,7 @@ func prevWidth*(container: Container): int =
     w += r.width()
   return pr.width()
 
-func currentWidth*(container: Container): int =
+func currentWidth(container: Container): int =
   if container.numLines == 0: return 0
   let line = container.currentLine
   if line.len == 0: return 0
@@ -193,7 +194,7 @@ func maxScreenWidth(container: Container): int =
   for line in container.ilines(container.fromy..container.lastVisibleLine):
     result = max(line.str.width(), result)
 
-func getTitle*(container: Container): string =
+func getTitle*(container: Container): string {.jsfunc.} =
   if container.title != "":
     return container.title
   return container.source.location.serialize(excludepassword = true)
@@ -210,9 +211,6 @@ func atPercentOf*(container: Container): int =
   if container.numLines == 0: return 100
   return (100 * (container.cursory + 1)) div container.numLines
 
-func lineInfo*(container: Container): string =
-  fmt"line {container.cursory + 1}/{container.numLines} ({container.atPercentOf}%) col {container.cursorx + 1}/{container.currentLineWidth} (byte {container.currentCursorBytes})"
-
 func lineWindow(container: Container): Slice[int] =
   if container.numLines == 0: # not loaded
     return 0..container.height * 5
@@ -293,7 +291,7 @@ proc requestLines*(container: Container, w = container.lineWindow): auto {.disca
     if w.a in cw or w.b in cw or cw.a in w or cw.b in w:
       container.triggerEvent(UPDATE))
 
-proc redraw*(container: Container) {.jsfunc.} =
+proc redraw(container: Container) {.jsfunc.} =
   container.triggerEvent(ContainerEvent(t: UPDATE, force: true))
 
 proc sendCursorPosition(container: Container) =
@@ -307,13 +305,13 @@ proc sendCursorPosition(container: Container) =
     if res.repaint:
       container.needslines = true)
 
-proc setFromY*(container: Container, y: int) {.jsfunc.} =
+proc setFromY(container: Container, y: int) {.jsfunc.} =
   if container.pos.fromy != y:
     container.pos.fromy = max(min(y, container.maxfromy), 0)
     container.needslines = true
     container.triggerEvent(UPDATE)
 
-proc setFromX*(container: Container, x: int, refresh = true) {.jsfunc.} =
+proc setFromX(container: Container, x: int, refresh = true) {.jsfunc.} =
   if container.pos.fromx != x:
     container.pos.fromx = max(min(x, container.maxfromx), 0)
     if container.pos.fromx > container.cursorx:
@@ -322,11 +320,11 @@ proc setFromX*(container: Container, x: int, refresh = true) {.jsfunc.} =
         container.sendCursorPosition()
     container.triggerEvent(UPDATE)
 
-proc setFromXY*(container: Container, x, y: int) {.jsfunc.} =
+proc setFromXY(container: Container, x, y: int) {.jsfunc.} =
   container.setFromY(y)
   container.setFromX(x)
 
-proc setCursorX*(container: Container, x: int, refresh = true, save = true) {.jsfunc.} =
+proc setCursorX(container: Container, x: int, refresh = true, save = true) {.jsfunc.} =
   if not container.lineLoaded(container.cursory):
     container.pos.setx = x
     return
@@ -354,7 +352,7 @@ proc setCursorX*(container: Container, x: int, refresh = true, save = true) {.js
 proc restoreCursorX(container: Container) {.jsfunc.} =
   container.setCursorX(max(min(container.currentLineWidth() - 1, container.xend), 0), false, false)
 
-proc setCursorY*(container: Container, y: int) {.jsfunc.} =
+proc setCursorY(container: Container, y: int) {.jsfunc.} =
   let y = max(min(y, container.numLines - 1), 0)
   if container.cursory == y: return
   if y - container.fromy >= 0 and y - container.height < container.fromy:
@@ -368,38 +366,41 @@ proc setCursorY*(container: Container, y: int) {.jsfunc.} =
   container.restoreCursorX()
   container.sendCursorPosition()
 
-proc centerLine*(container: Container) {.jsfunc.} =
+proc centerLine(container: Container) {.jsfunc.} =
   container.setFromY(container.cursory - container.height div 2)
 
-proc setCursorXY*(container: Container, x, y: int) {.jsfunc.} =
+proc centerColumn(container: Container) {.jsfunc.} =
+  container.setFromX(container.cursorx - container.width div 2)
+
+proc setCursorXY(container: Container, x, y: int) {.jsfunc.} =
   let fy = container.fromy
   container.setCursorY(y)
   container.setCursorX(x)
   if fy != container.fromy:
     container.centerLine()
 
-proc cursorDown*(container: Container) {.jsfunc.} =
+proc cursorDown(container: Container) {.jsfunc.} =
   container.setCursorY(container.cursory + 1)
 
-proc cursorUp*(container: Container) {.jsfunc.} =
+proc cursorUp(container: Container) {.jsfunc.} =
   container.setCursorY(container.cursory - 1)
 
-proc cursorLeft*(container: Container) {.jsfunc.} =
+proc cursorLeft(container: Container) {.jsfunc.} =
   var w = container.prevWidth()
   if w == 0:
     w = 1
   container.setCursorX(container.cursorx - w)
 
-proc cursorRight*(container: Container) {.jsfunc.} =
+proc cursorRight(container: Container) {.jsfunc.} =
   container.setCursorX(container.cursorx + container.currentWidth())
 
-proc cursorLineBegin*(container: Container) {.jsfunc.} =
+proc cursorLineBegin(container: Container) {.jsfunc.} =
   container.setCursorX(0)
 
-proc cursorLineEnd*(container: Container) {.jsfunc.} =
+proc cursorLineEnd(container: Container) {.jsfunc.} =
   container.setCursorX(container.currentLineWidth() - 1)
 
-proc cursorNextWord*(container: Container) {.jsfunc.} =
+proc cursorNextWord(container: Container) {.jsfunc.} =
   if container.numLines == 0: return
   var r: Rune
   var b = container.currentCursorBytes()
@@ -429,7 +430,7 @@ proc cursorNextWord*(container: Container) {.jsfunc.} =
     else:
       container.cursorLineEnd()
 
-proc cursorPrevWord*(container: Container) {.jsfunc.} =
+proc cursorPrevWord(container: Container) {.jsfunc.} =
   if container.numLines == 0: return
   var b = container.currentCursorBytes()
   var x = container.cursorx
@@ -460,57 +461,57 @@ proc cursorPrevWord*(container: Container) {.jsfunc.} =
     else:
       container.cursorLineBegin()
 
-proc pageDown*(container: Container) {.jsfunc.} =
+proc pageDown(container: Container) {.jsfunc.} =
   container.setFromY(container.fromy + container.height)
   container.setCursorY(container.cursory + container.height)
   container.restoreCursorX()
 
-proc pageUp*(container: Container) {.jsfunc.} =
+proc pageUp(container: Container) {.jsfunc.} =
   container.setFromY(container.fromy - container.height)
   container.setCursorY(container.cursory - container.height)
   container.restoreCursorX()
 
-proc pageLeft*(container: Container) {.jsfunc.} =
+proc pageLeft(container: Container) {.jsfunc.} =
   container.setFromX(container.fromx - container.width)
 
-proc pageRight*(container: Container) {.jsfunc.} =
+proc pageRight(container: Container) {.jsfunc.} =
   container.setFromX(container.fromx + container.width)
 
-proc halfPageUp*(container: Container) {.jsfunc.} =
+proc halfPageUp(container: Container) {.jsfunc.} =
   container.setFromY(container.fromy - container.height div 2 + 1)
   container.setCursorY(container.cursory - container.height div 2 + 1)
   container.restoreCursorX()
 
-proc halfPageDown*(container: Container) {.jsfunc.} =
+proc halfPageDown(container: Container) {.jsfunc.} =
   container.setFromY(container.fromy + container.height div 2 - 1)
   container.setCursorY(container.cursory + container.height div 2 - 1)
   container.restoreCursorX()
 
-proc cursorFirstLine*(container: Container) {.jsfunc.} =
+proc cursorFirstLine(container: Container) {.jsfunc.} =
   container.setCursorY(0)
 
 proc cursorLastLine*(container: Container) {.jsfunc.} =
   container.setCursorY(container.numLines - 1)
 
-proc cursorTop*(container: Container) {.jsfunc.} =
+proc cursorTop(container: Container) {.jsfunc.} =
   container.setCursorY(container.fromy)
 
-proc cursorMiddle*(container: Container) {.jsfunc.} =
+proc cursorMiddle(container: Container) {.jsfunc.} =
   container.setCursorY(container.fromy + (container.height - 2) div 2)
 
-proc cursorBottom*(container: Container) {.jsfunc.} =
+proc cursorBottom(container: Container) {.jsfunc.} =
   container.setCursorY(container.fromy + container.height - 1)
 
-proc cursorLeftEdge*(container: Container) {.jsfunc.} =
+proc cursorLeftEdge(container: Container) {.jsfunc.} =
   container.setCursorX(container.fromx)
 
-proc cursorVertMiddle*(container: Container) {.jsfunc.} =
+proc cursorMiddleColumn(container: Container) {.jsfunc.} =
   container.setCursorX(container.fromx + (container.width - 2) div 2)
 
-proc cursorRightEdge*(container: Container) {.jsfunc.} =
+proc cursorRightEdge(container: Container) {.jsfunc.} =
   container.setCursorX(container.fromx + container.width - 1)
 
-proc scrollDown*(container: Container) {.jsfunc.} =
+proc scrollDown(container: Container) {.jsfunc.} =
   if container.fromy + container.height < container.numLines:
     container.setFromY(container.fromy + 1)
     if container.fromy > container.cursory:
@@ -518,7 +519,7 @@ proc scrollDown*(container: Container) {.jsfunc.} =
   else:
     container.cursorDown()
 
-proc scrollUp*(container: Container) {.jsfunc.} =
+proc scrollUp(container: Container) {.jsfunc.} =
   if container.fromy > 0:
     container.setFromY(container.fromy - 1)
     if container.fromy + container.height <= container.cursory:
@@ -526,11 +527,11 @@ proc scrollUp*(container: Container) {.jsfunc.} =
   else:
     container.cursorUp()
 
-proc scrollRight*(container: Container) {.jsfunc.} =
+proc scrollRight(container: Container) {.jsfunc.} =
   if container.fromx + container.width < container.maxScreenWidth():
     container.setFromX(container.fromx + 1)
 
-proc scrollLeft*(container: Container) {.jsfunc.} =
+proc scrollLeft(container: Container) {.jsfunc.} =
   if container.fromx > 0:
     container.setFromX(container.fromx - 1)
     if container.cursorx < container.fromx:
@@ -539,6 +540,9 @@ proc scrollLeft*(container: Container) {.jsfunc.} =
 proc alert(container: Container, msg: string) =
   container.triggerEvent(ContainerEvent(t: ALERT, msg: msg))
 
+proc lineInfo(container: Container) {.jsfunc.} =
+  container.alert(fmt"line {container.cursory + 1}/{container.numLines} ({container.atPercentOf}%) col {container.cursorx + 1}/{container.currentLineWidth} (byte {container.currentCursorBytes})")
+
 proc updateCursor(container: Container) =
   if container.pos.setx > -1:
     container.setCursorX(container.pos.setx)
@@ -548,6 +552,23 @@ proc updateCursor(container: Container) =
     container.setCursorY(container.lastVisibleLine)
     container.alert("Last line is #" & $container.numLines)
 
+proc gotoLine*[T: string|int](container: Container, s: T) =
+  when s is string:
+    if s == "":
+      redraw(container)
+    elif s[0] == '^':
+      container.cursorFirstLine()
+    elif s[0] == '$':
+      container.cursorLastLine()
+    else:
+      try:
+        let i = parseInt(s)
+        container.setCursorY(i)
+      except ValueError:
+        container.alert("First line is #1") # :)
+  else:
+    container.setCursorY(s)
+
 proc pushCursorPos*(container: Container) =
   container.bpos.add(container.pos)
 
@@ -654,7 +675,7 @@ proc onload(container: Container, res: LoadResult) =
         if res.x != -1 and res.y != -1:
           container.setCursorXY(res.x, res.y))
 
-proc load*(container: Container) =
+proc load(container: Container) =
   container.setLoadInfo("Connecting to " & $container.source.location & "...")
   container.iface.connect().then(proc(res: ConnectResult): auto =
     let info = container.loadinfo
@@ -705,7 +726,7 @@ proc readSuccess*(container: Container, s: string) =
     if res.open.isSome:
       container.triggerEvent(ContainerEvent(t: OPEN, request: res.open.get)))
 
-proc reshape*(container: Container, noreq = false) {.jsfunc.} =
+proc reshape(container: Container, noreq = false) {.jsfunc.} =
   container.iface.render().then(proc(lines: int) =
     container.setNumLines(lines))
   if not noreq:
@@ -725,7 +746,7 @@ proc dupeBuffer*(dispatcher: Dispatcher, container: Container, config: Config, l
       container.pipeto = nil)
   return container.pipeto
 
-proc click*(container: Container) {.jsfunc.} =
+proc click(container: Container) {.jsfunc.} =
   container.iface.click(container.cursorx, container.cursory).then(proc(res: ClickResult) =
     if res.repaint:
       container.needslines = true
@@ -761,13 +782,13 @@ proc windowChange*(container: Container, attrs: WindowAttributes) =
         container.setNumLines(lines, true)
       container.needslines = true)
 
-proc peek*(container: Container) {.jsfunc.} =
+proc peek(container: Container) {.jsfunc.} =
   container.alert($container.source.location)
 
 proc clearHover*(container: Container) =
   container.lastpeek = low(HoverType)
 
-proc peekCursor*(container: Container) {.jsfunc.} =
+proc peekCursor(container: Container) {.jsfunc.} =
   var p = container.lastpeek
   while true:
     if container.hovertext[p] != "":
diff --git a/src/config/config.nim b/src/config/config.nim
index 7ed1441a..d3cad0b1 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -53,12 +53,13 @@ type
 
   Config* = ref ConfigObj
   ConfigObj* = object
+    searchwrap* {.jsget, jsset.}: bool
     maxredirect*: int
-    prependhttps*: bool
+    prependhttps* {.jsget, jsset.}: bool
     termreload*: bool
     nmap*: ActionMap
     lemap*: ActionMap
-    stylesheet*: string
+    stylesheet* {.jsget, jsset.}: string
     startup*: string
     ambiguous_double*: bool
     hlcolor*: RGBAColor
@@ -254,15 +255,20 @@ proc parseConfig(config: Config, dir: string, t: TomlValue) =
     of "include":
       if v.vt == VALUE_STRING:
         when nimvm:
-          config.loadConfig(v.s)
+          config.parseConfig(dir, staticRead(dir / v.s))
         else:
-          config.loadConfig(v.s)
+          config.parseConfig(dir, newFileStream(dir / v.s))
       elif t.vt == VALUE_ARRAY:
         for v in t.a:
           when nimvm:
-            config.parseConfig(parentDir(v.s), staticRead(v.s))
+            config.parseConfig(dir, staticRead(dir / v.s))
           else:
-            config.parseConfig(parentDir(v.s), newFileStream(v.s))
+            config.parseConfig(dir, newFileStream(dir / v.s))
+    of "search":
+      for k, v in v:
+        case k
+        of "wrap":
+          config.searchwrap = v.b
     of "start":
       for k, v in v:
         case k
diff --git a/src/display/client.nim b/src/display/client.nim
index 9feb5410..d1c33816 100644
--- a/src/display/client.nim
+++ b/src/display/client.nim
@@ -194,7 +194,7 @@ proc setTimeout[T: JSValue|string](client: Client, handler: T, timeout = 0): int
   else:
     let fun = JS_DupValue(client.jsctx, handler)
     client.timeouts[id] = ((proc() =
-      let ret = callFunction(client.jsctx, fun)
+      let ret = JS_Call(client.jsctx, fun, JS_UNDEFINED, 0, nil)
       if JS_IsException(ret):
         client.jsctx.writeException(client.console.err)
       JS_FreeValue(client.jsctx, ret)
@@ -214,7 +214,7 @@ proc setInterval[T: JSValue|string](client: Client, handler: T, interval = 0): i
   else:
     let fun = JS_DupValue(client.jsctx, handler)
     client.intervals[id] = ((proc() =
-      let ret = callFunction(client.jsctx, handler)
+      let ret = JS_Call(client.jsctx, handler, JS_UNDEFINED, 0, nil)
       if JS_IsException(ret):
         client.jsctx.writeException(client.console.err)
       JS_FreeValue(client.jsctx, ret)
@@ -405,9 +405,10 @@ proc launchClient*(client: Client, pages: seq[string], ctype: Option[string], du
   if not dump:
     if stdin.isatty():
       tty = stdin
-    elif stdout.isatty():
-      discard open(tty, "/dev/tty", fmRead)
-    if tty == nil:
+    if stdout.isatty():
+      if tty == nil:
+        dump = not open(tty, "/dev/tty", fmRead)
+    else:
       dump = true
   client.ssock = initServerSocket(false)
   client.selector = newSelector[Container]()
@@ -467,9 +468,7 @@ proc newClient*(config: Config, dispatcher: Dispatcher): Client =
   result.jsrt.setInterruptHandler(interruptHandler, cast[pointer](result))
   let ctx = result.jsrt.newJSContext()
   result.jsctx = ctx
-  result.pager = newPager(config, result.attrs, dispatcher,
-                          result.config.getSiteConfig(ctx),
-                          result.config.getOmniRules(ctx))
+  result.pager = newPager(config, result.attrs, dispatcher, ctx)
   var global = JS_GetGlobalObject(ctx)
   ctx.registerType(Client, asglobal = true)
   setOpaque(ctx, global, result)
diff --git a/src/display/pager.nim b/src/display/pager.nim
index 25bcee82..f90b057c 100644
--- a/src/display/pager.nim
+++ b/src/display/pager.nim
@@ -31,9 +31,10 @@ import utils/twtstr
 type
   LineMode* = enum
     NO_LINEMODE, LOCATION, USERNAME, PASSWORD, COMMAND, BUFFER, SEARCH_F,
-    SEARCH_B, ISEARCH_F, ISEARCH_B
+    SEARCH_B, ISEARCH_F, ISEARCH_B, GOTO_LINE
 
   Pager* = ref object
+    jsctx: JSContext
     numload*: int
     alerts: seq[string]
     commandMode*: bool
@@ -88,52 +89,43 @@ proc setContainer*(pager: Pager, c: Container) {.jsfunc.} =
   pager.container = c
   pager.redraw = true
 
-proc cursorDown(pager: Pager) {.jsfunc.} = pager.container.cursorDown()
-proc cursorUp(pager: Pager) {.jsfunc.} = pager.container.cursorUp()
-proc cursorLeft(pager: Pager) {.jsfunc.} = pager.container.cursorLeft()
-proc cursorRight(pager: Pager) {.jsfunc.} = pager.container.cursorRight()
-proc cursorLineBegin(pager: Pager) {.jsfunc.} = pager.container.cursorLineBegin()
-proc cursorLineEnd(pager: Pager) {.jsfunc.} = pager.container.cursorLineEnd()
-proc cursorNextWord(pager: Pager) {.jsfunc.} = pager.container.cursorNextWord()
-proc cursorPrevWord(pager: Pager) {.jsfunc.} = pager.container.cursorPrevWord()
-proc cursorNextLink(pager: Pager) {.jsfunc.} = pager.container.cursorNextLink()
-proc cursorPrevLink(pager: Pager) {.jsfunc.} = pager.container.cursorPrevLink()
-proc pageUp(pager: Pager) {.jsfunc.} = pager.container.pageUp()
-proc pageDown(pager: Pager) {.jsfunc.} = pager.container.pageDown()
-proc pageRight(pager: Pager) {.jsfunc.} = pager.container.pageRight()
-proc pageLeft(pager: Pager) {.jsfunc.} = pager.container.pageLeft()
-proc halfPageDown(pager: Pager) {.jsfunc.} = pager.container.halfPageDown()
-proc halfPageUp(pager: Pager) {.jsfunc.} = pager.container.halfPageUp()
-proc cursorFirstLine(pager: Pager) {.jsfunc.} = pager.container.cursorFirstLine()
-proc cursorLastLine(pager: Pager) {.jsfunc.} = pager.container.cursorLastLine()
-proc cursorTop(pager: Pager) {.jsfunc.} = pager.container.cursorTop()
-proc cursorMiddle(pager: Pager) {.jsfunc.} = pager.container.cursorMiddle()
-proc cursorBottom(pager: Pager) {.jsfunc.} = pager.container.cursorBottom()
-proc cursorLeftEdge(pager: Pager) {.jsfunc.} = pager.container.cursorLeftEdge()
-proc cursorVertMiddle(pager: Pager) {.jsfunc.} = pager.container.cursorVertMiddle()
-proc cursorRightEdge(pager: Pager) {.jsfunc.} = pager.container.cursorRightEdge()
-proc centerLine(pager: Pager) {.jsfunc.} = pager.container.centerLine()
-proc scrollDown(pager: Pager) {.jsfunc.} = pager.container.scrollDown()
-proc scrollUp(pager: Pager) {.jsfunc.} = pager.container.scrollUp()
-proc scrollLeft(pager: Pager) {.jsfunc.} = pager.container.scrollLeft()
-proc scrollRight(pager: Pager) {.jsfunc.} = pager.container.scrollRight()
-proc reshape(pager: Pager) {.jsfunc.} = pager.container.reshape()
-proc peek(pager: Pager) {.jsfunc.} = pager.container.peek()
-proc peekCursor(pager: Pager) {.jsfunc.} = pager.container.peekCursor()
+proc hasprop(pager: Pager, s: string): bool {.jshasprop.} =
+  if pager.container != nil:
+    let cval = toJS(pager.jsctx, pager.container)
+    let val = JS_GetPropertyStr(pager.jsctx, cval, s)
+    if val != JS_UNDEFINED:
+      result = true
+    JS_FreeValue(pager.jsctx, val)
+
+proc reflect(ctx: JSContext, this_val: JSValue, argc: cint, argv: ptr JSValue,
+             magic: cint, func_data: ptr JSValue): JSValue {.cdecl.} =
+  let fun = cast[ptr JSValue](cast[int](func_data) + sizeof(JSValue))[]
+  return JS_Call(ctx, fun, func_data[], argc, argv)
+
+proc getter(pager: Pager, s: string): Option[JSValue] {.jsgetprop.} =
+  if pager.container != nil:
+    let cval = toJS(pager.jsctx, pager.container)
+    let val = JS_GetPropertyStr(pager.jsctx, cval, s)
+    if val != JS_UNDEFINED:
+      if JS_IsFunction(pager.jsctx, val):
+        var func_data = @[cval, val]
+        let fun = JS_NewCFunctionData(pager.jsctx, reflect, 1, 0, 2, addr func_data[0])
+        return some(fun)
+      return some(val)
 
 proc searchNext(pager: Pager) {.jsfunc.} =
   if pager.regex.issome:
     if not pager.reverseSearch:
-      pager.container.cursorNextMatch(pager.regex.get, true)
+      pager.container.cursorNextMatch(pager.regex.get, pager.config.searchwrap)
     else:
-      pager.container.cursorPrevMatch(pager.regex.get, true)
+      pager.container.cursorPrevMatch(pager.regex.get, pager.config.searchwrap)
 
 proc searchPrev(pager: Pager) {.jsfunc.} =
   if pager.regex.issome:
     if not pager.reverseSearch:
-      pager.container.cursorPrevMatch(pager.regex.get, true)
+      pager.container.cursorPrevMatch(pager.regex.get, pager.config.searchwrap)
     else:
-      pager.container.cursorNextMatch(pager.regex.get, true)
+      pager.container.cursorNextMatch(pager.regex.get, pager.config.searchwrap)
 
 proc getLineHist(pager: Pager, mode: LineMode): LineHistory =
   if pager.linehist[mode] == nil:
@@ -161,22 +153,24 @@ proc isearchBackward(pager: Pager) {.jsfunc.} =
   pager.container.pushCursorPos()
   pager.setLineEdit("?", ISEARCH_B)
 
-proc newPager*(config: Config, attrs: WindowAttributes, dispatcher: Dispatcher, siteconf: seq[SiteConfig], omnirules: seq[OmniRule]): Pager =
+proc gotoLine[T: string|int](pager: Pager, s: T = "") {.jsfunc.} =
+  when s is string:
+    if s == "":
+      pager.setLineEdit("Goto line: ", GOTO_LINE)
+      return
+  pager.container.gotoLine(s)
+
+proc newPager*(config: Config, attrs: WindowAttributes, dispatcher: Dispatcher, ctx: JSContext): Pager =
   let pager = Pager(
     dispatcher: dispatcher,
     config: config,
     display: newFixedGrid(attrs.width, attrs.height - 1),
     statusgrid: newFixedGrid(attrs.width),
     term: newTerminal(stdout, config, attrs),
+    jsctx: ctx,
+    siteconf: config.getSiteConfig(ctx),
+    omnirules: config.getOmniRules(ctx)
   )
-  for sc in siteconf:
-    # not sure why but normal copies don't seem to work here... probably
-    # something weird going on with lambdas
-    pager.siteconf.add(sc)
-    pager.siteconf[^1].subst = sc.subst
-  for rule in omnirules:
-    pager.omnirules.add(rule)
-    pager.omnirules[^1].subst = rule.subst
   return pager
 
 proc launchPager*(pager: Pager, tty: File) =
@@ -411,9 +405,6 @@ proc nextSiblingBuffer(pager: Pager): bool {.jsfunc.} =
 proc alert*(pager: Pager, msg: string) {.jsfunc.} =
   pager.alerts.add(msg)
 
-proc lineInfo(pager: Pager) {.jsfunc.} =
-  pager.alert(pager.container.lineInfo())
-
 proc deleteContainer(pager: Pager, container: Container) =
   container.cancel()
   if container.sourcepair != nil:
@@ -466,7 +457,7 @@ proc discardTree(pager: Pager, container = none(Container)) {.jsfunc.} =
   else:
     pager.alert("Buffer has no children!")
 
-proc toggleSource*(pager: Pager) {.jsfunc.} =
+proc toggleSource(pager: Pager) {.jsfunc.} =
   if pager.container.sourcepair != nil:
     pager.setContainer(pager.container.sourcepair)
   else:
@@ -519,7 +510,7 @@ proc applySiteconf(pager: Pager, request: Request): BufferConfig =
   return pager.config.getBufferConfig(request.url, cookiejar, headers, refererfrom, scripting)
 
 # Load request in a new buffer.
-proc gotoURL*(pager: Pager, request: Request, prevurl = none(URL),
+proc gotoURL(pager: Pager, request: Request, prevurl = none(URL),
               ctype = none(string), replace: Container = nil,
               redirectdepth = 0, referrer: Container = nil) =
   if referrer != nil and referrer.config.refererfrom:
@@ -628,9 +619,9 @@ proc updateReadLineISearch(pager: Pager, linemode: LineMode) =
     pager.container.popCursorPos(true)
     if pager.iregex.isSome:
       if linemode == ISEARCH_F:
-        pager.container.cursorNextMatch(pager.iregex.get, true)
+        pager.container.cursorNextMatch(pager.iregex.get, pager.config.searchwrap)
       else:
-        pager.container.cursorPrevMatch(pager.iregex.get, true)
+        pager.container.cursorPrevMatch(pager.iregex.get, pager.config.searchwrap)
       pager.container.hlon = true
     pager.container.pushCursorPos()
   of FINISH:
@@ -674,7 +665,9 @@ proc updateReadLine*(pager: Pager) =
         let x = s
         if x != "": pager.regex = compileSearchRegex(x)
         pager.reverseSearch = true
-        pager.searchPrev()
+        pager.searchNext()
+      of GOTO_LINE:
+        pager.container.gotoLine(s)
       else: discard
     of CANCEL:
       case pager.linemode
@@ -703,14 +696,7 @@ proc load(pager: Pager, s = "") {.jsfunc.} =
 proc reload(pager: Pager) {.jsfunc.} =
   pager.gotoURL(newRequest(pager.container.source.location), none(URL), pager.container.contenttype, pager.container)
 
-# Cancel loading current page (if exists).
-proc cancel(pager: Pager) {.jsfunc.} =
-  pager.container.cancel()
-
-proc click(pager: Pager) {.jsfunc.} =
-  pager.container.click()
-
-proc authorize*(pager: Pager) =
+proc authorize(pager: Pager) =
   pager.setLineEdit("Username: ", USERNAME)
 
 proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bool =
diff --git a/src/js/javascript.nim b/src/js/javascript.nim
index 876f7127..d7ccc115 100644
--- a/src/js/javascript.nim
+++ b/src/js/javascript.nim
@@ -1,10 +1,10 @@
 # JavaScript binding generator. Horrifying, I know. But it works!
-# Note 1: Function overloading is currently not implemented. Though there is a
+# Warning: Function overloading is currently not implemented. Though there is a
 # block dielabel:
 #   ...
 # around each bound function call, so it shouldn't be too difficult to get it
 # working. (This would involve generating JS functions in registerType.)
-# Note 2: Now, for the pragmas:
+# Now for the pragmas:
 # {.jsctr.} for constructors. These need no `this' value, and are bound as
 #   regular constructors in JS. They must return a ref object, which will have
 #   a JS counterpart too. (Other functions can return ref objects too, which
@@ -19,6 +19,8 @@
 #   keyword, unfortunately that didn't work out.)
 #{.jsgetprop.} for property getters. Called when GetOwnProperty would return
 #   nothing. The key should probably be either a string or an integer.
+#{.jshasprop.} for overriding has_property. Must return a boolean.
+
 import macros
 import options
 import sets
@@ -48,7 +50,9 @@ export
 
 export JSRuntime, JSContext, JSValue
 
-export JS_GetGlobalObject, JS_FreeValue, JS_IsException
+export
+  JS_GetGlobalObject, JS_FreeValue, JS_IsException, JS_GetPropertyStr,
+  JS_IsFunction, JS_NewCFunctionData, JS_Call
 
 type
   JSContextOpaque* = ref object
@@ -246,7 +250,7 @@ proc setProperty*(ctx: JSContext, val: JSValue, name: string, prop: JSValue) =
 proc setProperty*(ctx: JSContext, val: JSValue, name: string, fun: JSCFunction, argc: int = 0) =
   ctx.setProperty(val, name, ctx.newJSCFunction(name, fun, argc))
 
-func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, ctor: JSCFunction, funcs: JSFunctionList, nimt: pointer, parent: JSClassID, asglobal: bool, nointerface: bool): JSClassID {.discardable.} =
+func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, tname: string, ctor: JSCFunction, funcs: JSFunctionList, nimt: pointer, parent: JSClassID, asglobal: bool, nointerface: bool): JSClassID {.discardable.} =
   let rt = JS_GetRuntime(ctx)
   discard JS_NewClassID(addr result)
   var ctxOpaque = ctx.getOpaque()
@@ -254,7 +258,7 @@ func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, ctor: JSCFunction, funcs
   if JS_NewClass(rt, result, cdef) != 0:
     raise newException(Defect, "Failed to allocate JS class: " & $cdef.class_name)
   ctxOpaque.typemap[nimt] = result
-  ctxOpaque.creg[$cdef.class_name] = result
+  ctxOpaque.creg[tname] = result
   var proto: JSValue
   if parent != 0:
     let parentProto = JS_GetClassProto(ctx, parent)
@@ -277,7 +281,7 @@ func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, ctor: JSCFunction, funcs
   if asglobal:
     let global = JS_GetGlobalObject(ctx)
     assert ctxOpaque.gclaz == ""
-    ctxOpaque.gclaz = $cdef.class_name
+    ctxOpaque.gclaz = tname
     if JS_SetPrototype(ctx, global, proto) != 1:
       raise newException(Defect, "Failed to set global prototype: " & $cdef.class_name)
     JS_FreeValue(ctx, global)
@@ -289,11 +293,6 @@ func newJSClass*(ctx: JSContext, cdef: JSClassDefConst, ctor: JSCFunction, funcs
     ctx.setProperty(global, $cdef.class_name, jctor)
     JS_FreeValue(ctx, global)
 
-proc callFunction*(ctx: JSContext, fun: JSValue): JSValue =
-  let global = JS_GetGlobalObject(ctx)
-  result = JS_Call(ctx, fun, JS_UNDEFINED, 0, nil)
-  JS_FreeValue(ctx, global)
-
 type FuncParam = tuple[name: string, t: NimNode, val: Option[NimNode], generic: Option[NimNode]]
 
 func getMinArgs(params: seq[FuncParam]): int =
@@ -790,7 +789,7 @@ type
     magic: uint16
 
   BoundFunctionType = enum
-    FUNCTION = "js_"
+    FUNCTION = "js_func"
     CONSTRUCTOR = "js_ctor"
     GETTER = "js_get"
     SETTER = "js_set"
@@ -877,7 +876,7 @@ template getJSParams(): untyped =
     (quote do: JSValue),
     newIdentDefs(ident("ctx"), quote do: JSContext),
     newIdentDefs(ident("this"), quote do: JSValue),
-    newIdentDefs(ident("argc"), quote do: int),
+    newIdentDefs(ident("argc"), quote do: cint),
     newIdentDefs(ident("argv"), quote do: ptr JSValue)
   ]
 
@@ -1439,7 +1438,7 @@ proc nim_finalize_for_js[T](obj: T) =
       # trigger the JS finalizer and free the JS value.
       JS_FreeValueRT(rt, val)
 
-proc js_illegal_ctor*(ctx: JSContext, this: JSValue, argc: int, argv: ptr JSValue): JSValue {.cdecl.} =
+proc js_illegal_ctor*(ctx: JSContext, this: JSValue, argc: cint, argv: ptr JSValue): JSValue {.cdecl.} =
   return JS_ThrowTypeError(ctx, "Illegal constructor")
 
 type JSObjectPragmas = object
@@ -1482,7 +1481,8 @@ proc findPragmas(t: NimNode): JSObjectPragmas =
 
 macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = false, nointerface = false, name: static string = ""): JSClassID =
   result = newStmtList()
-  let name = if name == "": t.strVal else: name
+  let tname = t.strVal # the nim type's name.
+  let name = if name == "": tname else: name # possibly a different name, e.g. Buffer for Container
   var sctr = ident("js_illegal_ctor")
   var sfin = ident("js_" & t.strVal & "ClassFin")
   # constructor
@@ -1500,7 +1500,7 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = fals
     let fn = $node
     result.add(quote do:
       proc `id`(ctx: JSContext, this: JSValue): JSValue {.cdecl.} =
-        if not (JS_IsUndefined(this) or ctx.isGlobal(`name`)) and not ctx.isInstanceOf(this, `name`):
+        if not (JS_IsUndefined(this) or ctx.isGlobal(`tname`)) and not ctx.isInstanceOf(this, `tname`):
           # undefined -> global.
           return JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `name`)
         let arg_0 = fromJS_or_return(`t`, ctx, this)
@@ -1512,7 +1512,7 @@ macro registerType*(ctx: typed, t: typed, parent: JSClassID = 0, asglobal = fals
     let fn = $node
     result.add(quote do:
       proc `id`(ctx: JSContext, this: JSValue, val: JSValue): JSValue {.cdecl.} =
-        if not (JS_IsUndefined(this) or ctx.isGlobal(`name`)) and not ctx.isInstanceOf(this, `name`):
+        if not (JS_IsUndefined(this) or ctx.isGlobal(`tname`)) and not ctx.isInstanceOf(this, `tname`):
           # undefined -> global.
           return JS_ThrowTypeError(ctx, "'%s' called on an object that is not an instance of %s", `fn`, `name`)
         let arg_0 = fromJS_or_return(`t`, ctx, this)
@@ -1610,7 +1610,7 @@ static JSClassDef """, `cdname`, """ = {
     # any associated JS object from all relevant runtimes.
     var x: `t`
     new(x, nim_finalize_for_js)
-    `ctx`.newJSClass(`classDef`, `sctr`, `tabList`, getTypePtr(x), `parent`, `asglobal`, `nointerface`)
+    `ctx`.newJSClass(`classDef`, `tname`, `sctr`, `tabList`, getTypePtr(x), `parent`, `asglobal`, `nointerface`)
   )
   result.add(newBlockStmt(endstmts))