about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--res/chawan.html5
-rw-r--r--res/config.toml19
-rw-r--r--src/bindings/quickjs.nim2
-rw-r--r--src/config/config.nim2
-rw-r--r--src/display/term.nim15
-rw-r--r--src/extern/runproc.nim14
-rw-r--r--src/js/fromjs.nim25
-rw-r--r--src/js/opaque.nim1
-rw-r--r--src/local/client.nim34
-rw-r--r--src/local/container.nim203
-rw-r--r--src/local/pager.nim33
11 files changed, 280 insertions, 73 deletions
diff --git a/res/chawan.html b/res/chawan.html
index fcd054ff..271e3e6b 100644
--- a/res/chawan.html
+++ b/res/chawan.html
@@ -50,6 +50,9 @@ on your environment, the <kbd>Meta</kbd> key may be called <kbd>Alt</kbd> or
 up/down by one row
 <li><kbd>C-l</kbd>: location bar (to enter a URL, etc.)
 <li><kbd>C-k</kbd>: web search
+<li><kbd>v</kbd>, <kbd>V</kbd>, <kbd>C-v</kbd>: select (normal), select
+(line), select (block)
+<li><kbd>y</kbd>: yank (copy) current selection to system clipboard (needs xsel)
 <li><kbd>U</kbd>: reload page
 <li><kbd>,</kbd> (comma), <kbd>.</kbd> (period): previous/next buffer
 <li><kbd>D</kbd>: discard current buffer
@@ -75,7 +78,7 @@ beginning)
 <li><kbd>zb</kbd>, <kbd>zC-m</kbd>: center on current line (and move to
 beginning)
 <li><kbd>w</kbd>, <kbd>b</kbd>: move cursor to next/previous word
-<li><kbd>v</kbd>: toggle page source view
+<li><kbd>\</kbd>: toggle page source view
 <li><kbd>0</kbd>: cursor to first cell on line
 <li><kbd>^</kbd>: cursor to first non-whitespace on line
 <li><kbd>$</kbd>: cursor to last character on line
diff --git a/res/config.toml b/res/config.toml
index f821d694..a263fcb7 100644
--- a/res/config.toml
+++ b/res/config.toml
@@ -132,7 +132,24 @@ zz = 'n => pager.centerLine(n)'
 'z+' = 'n => pager.nextPageBegin(n)'
 'z^' = 'n => pager.previousPageBegin(n)'
 C-g = 'pager.lineInfo()'
-v = 'pager.toggleSource()'
+v = 'n => pager.cursorToggleSelection(n)'
+V = 'n => pager.cursorToggleSelection(n, {selectionType: "line"})'
+C-v = 'n => pager.cursorToggleSelection(n, {selectionType: "block"})'
+y = '''
+async () => {
+	if (!pager.currentSelection) {
+		pager.alert("No selection to yank.");
+		return;
+	}
+	const text = await pager.getSelectionText(pager.currentSelection);
+	if (pager.externInto('xsel -bi', text))
+		pager.alert("Copied selection to clipboard.");
+	else
+		pager.alert("Failed to copy selection to clipboard. (Is xsel installed?)");
+	pager.cursorToggleSelection();
+}
+'''
+'\' = 'pager.toggleSource()'
 D = 'pager.discardBuffer()'
 M-d = 'pager.discardTree()'
 ',' = 'pager.prevBuffer()'
diff --git a/src/bindings/quickjs.nim b/src/bindings/quickjs.nim
index d573a1dd..c7c9554f 100644
--- a/src/bindings/quickjs.nim
+++ b/src/bindings/quickjs.nim
@@ -423,6 +423,8 @@ proc JS_Call*(ctx: JSContext, func_obj, this_obj: JSValue, argc: cint,
   argv: ptr JSValue): JSValue
 proc JS_NewObjectFromCtor*(ctx: JSContext, ctor: JSValue,
   class_id: JSClassID): JSValue
+proc JS_Invoke*(ctx: JSContext, this_obj: JSValue, atom: JSAtom, argc: cint,
+  argv: ptr JSValue): JSValue
 proc JS_CallConstructor*(ctx: JSContext, func_obj: JSValue, argc: cint,
   argv: ptr JSValue): JSValue
 
diff --git a/src/config/config.nim b/src/config/config.nim
index b0e33685..3cb1d10e 100644
--- a/src/config/config.nim
+++ b/src/config/config.nim
@@ -322,6 +322,8 @@ func getRealKey(key: string): string =
     realk &= 'C'
   if meta == 1:
     realk &= 'M'
+  if skip:
+    realk &= '\\'
   return realk
 
 proc openFileExpand(dir, file: string): FileStream =
diff --git a/src/display/term.nim b/src/display/term.nim
index b4d4a61b..3a3a7773 100644
--- a/src/display/term.nim
+++ b/src/display/term.nim
@@ -410,11 +410,17 @@ proc generateSwapOutput(term: Terminal, grid, prev: FixedGrid): string =
     var change = false
     # scan for changes, and set cx to x of the first change
     var cx = 0
+    # if there is a change, we have to start from the last x with
+    # a string (otherwise we might overwrite a double-width char)
+    var lastx = 0
     for x in 0 ..< grid.width:
-      if grid[y * grid.width + x] != prev[y * grid.width + x]:
+      let i = y * grid.width + x
+      if grid[i].str != "":
+        lastx = x
+      if grid[i] != prev[i]:
         change = true
-        cx = x
-        w = x
+        cx = lastx
+        w = lastx
         break
     if change:
       if cx == 0 and vy != -1:
@@ -427,6 +433,9 @@ proc generateSwapOutput(term: Terminal, grid, prev: FixedGrid): string =
       result &= term.resetFormat()
       var format = newFormat()
       for x in cx ..< grid.width:
+        while w < x: # if previous cell had no width, catch up with x
+          result &= ' '
+          inc w
         let cell = grid[y * grid.width + x]
         result &= term.processFormat(format, cell.format)
         result &= term.processOutputString(cell.str, w)
diff --git a/src/extern/runproc.nim b/src/extern/runproc.nim
index fa1ed9bc..6cad73a0 100644
--- a/src/extern/runproc.nim
+++ b/src/extern/runproc.nim
@@ -30,7 +30,7 @@ proc runProcess*(term: Terminal, cmd: string, wait = false): bool =
   term.restart()
 
 # Run process, and capture its output.
-proc runProcessCapture*(term: Terminal, cmd: string, outs: var string): bool =
+proc runProcessCapture*(cmd: string, outs: var string): bool =
   let file = popen(cmd, "r")
   if file == nil:
     return false
@@ -40,3 +40,15 @@ proc runProcessCapture*(term: Terminal, cmd: string, outs: var string): bool =
   if rv == -1:
     return false
   return rv == 0
+
+# Run process, and write an arbitrary string into its standard input.
+proc runProcessInto*(cmd, ins: string): bool =
+  let file = popen(cmd, "w")
+  if file == nil:
+    return false
+  let fs = newFileStream(file)
+  fs.write(ins)
+  let rv = pclose(file)
+  if rv == -1:
+    return false
+  return rv == 0
diff --git a/src/js/fromjs.nim b/src/js/fromjs.nim
index 426b04b4..bddaa2e1 100644
--- a/src/js/fromjs.nim
+++ b/src/js/fromjs.nim
@@ -5,6 +5,7 @@ import tables
 import unicode
 
 import bindings/quickjs
+import io/promise
 import js/arraybuffer
 import js/dict
 import js/error
@@ -449,6 +450,28 @@ proc fromJSArrayBufferView(ctx: JSContext, val: JSValue):
   )
   return ok(view)
 
+proc promiseThenCallback(ctx: JSContext, this_val: JSValue, argc: cint,
+    argv: ptr JSValue, magic: cint, func_data: ptr JSValue): JSValue {.cdecl.} =
+  let op = JS_GetOpaque(func_data[], JS_GetClassID(func_data[]))
+  let p = cast[EmptyPromise](op)
+  p.resolve()
+  GC_unref(p)
+  return JS_UNDEFINED
+
+proc fromJSEmptyPromise(ctx: JSContext, val: JSValue): JSResult[EmptyPromise] =
+  if not JS_IsObject(val):
+    return err(newTypeError("Value is not an object"))
+  #TODO I have a feeling this leaks memory in some cases :(
+  var p = EmptyPromise()
+  GC_ref(p)
+  var tmp = JS_NewObject(ctx)
+  JS_SetOpaque(tmp, cast[pointer](p))
+  var fun = JS_NewCFunctionData(ctx, promiseThenCallback, 0, 0, 1, addr tmp)
+  let res = JS_Invoke(ctx, val, ctx.getOpaque().str_refs[THEN], 1, addr fun)
+  if JS_IsException(res):
+    return err()
+  return ok(p)
+
 type FromJSAllowedT = (object and not (Result|Option|Table|JSValue|JSDict|
   JSArrayBuffer|JSArrayBufferView|JSUint8Array))
 
@@ -482,6 +505,8 @@ proc fromJS*[T](ctx: JSContext, val: JSValue): JSResult[T] =
     return fromJSEnum[T](ctx, val)
   elif T is JSValue:
     return ok(val)
+  elif T is EmptyPromise:
+    return fromJSEmptyPromise(ctx, val)
   elif T is ref object:
     return fromJSObject[T](ctx, val)
   elif T is void:
diff --git a/src/js/opaque.nim b/src/js/opaque.nim
index ae79481b..96b8fa55 100644
--- a/src/js/opaque.nim
+++ b/src/js/opaque.nim
@@ -15,6 +15,7 @@ type
     VALUE = "value"
     NEXT = "next"
     PROTOTYPE = "prototype"
+    THEN = "then"
 
   JSContextOpaque* = ref object
     creg*: Table[string, JSClassID]
diff --git a/src/local/client.nim b/src/local/client.nim
index 4dc29f24..51055472 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -182,9 +182,11 @@ proc handlePagerEvents(client: Client) =
   if container != nil:
     client.pager.handleEvents(container)
 
-proc evalAction(client: Client, action: string, arg0: int32) =
+proc evalAction(client: Client, action: string, arg0: int32): EmptyPromise =
   var ret = client.evalJS(action, "<command>")
   let ctx = client.jsctx
+  var p = EmptyPromise()
+  p.resolve()
   if JS_IsFunction(ctx, ret):
     if arg0 != 0:
       var arg0 = toJS(ctx, arg0)
@@ -199,7 +201,12 @@ proc evalAction(client: Client, action: string, arg0: int32) =
       ret = ret2
   if JS_IsException(ret):
     client.jsctx.writeException(client.console.err)
+  if JS_IsObject(ret):
+    let maybep = fromJS[EmptyPromise](ctx, ret)
+    if maybep.isOk:
+      p = maybep.get
   JS_FreeValue(ctx, ret)
+  return p
 
 # The maximum number we are willing to accept.
 # This should be fine for 32-bit signed ints (which precnum currently is).
@@ -207,7 +214,7 @@ proc evalAction(client: Client, action: string, arg0: int32) =
 # it proves to be too low.
 const MaxPrecNum = 100000000
 
-proc handleCommandInput(client: Client, c: char) =
+proc handleCommandInput(client: Client, c: char): EmptyPromise =
   if client.config.input.vi_numeric_prefix and not client.pager.notnum:
     if client.pager.precnum != 0 and c == '0' or c in '1' .. '9':
       if client.pager.precnum < MaxPrecNum: # better ignore than eval...
@@ -218,23 +225,23 @@ proc handleCommandInput(client: Client, c: char) =
       client.pager.notnum = true
   client.pager.inputBuffer &= c
   let action = getNormalAction(client.config, client.pager.inputBuffer)
-  client.evalAction(action, client.pager.precnum)
+  let p = client.evalAction(action, client.pager.precnum)
   if not client.feedNext:
     client.pager.precnum = 0
     client.pager.notnum = false
     client.handlePagerEvents()
+  return p
 
-proc input(client: Client) =
+proc input(client: Client): EmptyPromise =
+  var p: EmptyPromise = nil
   client.pager.term.restoreStdin()
   while true:
     let c = client.readChar()
     if client.pager.askpromise != nil:
       if c == 'y':
         client.pager.fulfillAsk(true)
-        client.runJSJobs()
       elif c == 'n':
         client.pager.fulfillAsk(false)
-        client.runJSJobs()
     elif client.pager.lineedit.isSome:
       client.pager.inputBuffer &= c
       let edit = client.pager.lineedit.get
@@ -250,11 +257,11 @@ proc input(client: Client) =
           else:
             client.feedNext = true
         elif not client.feednext:
-          client.evalAction(action, 0)
+          discard client.evalAction(action, 0)
         if not client.feedNext:
           client.pager.updateReadLine()
     else:
-      client.handleCommandInput(c)
+      p = client.handleCommandInput(c)
       if not client.feednext:
         client.pager.inputBuffer = ""
         client.pager.refreshStatusMsg()
@@ -267,6 +274,10 @@ proc input(client: Client) =
     else:
       client.feednext = false
   client.pager.inputBuffer = ""
+  if p == nil:
+    p = EmptyPromise()
+    p.resolve()
+  return p
 
 proc setTimeout[T: JSValue|string](client: Client, handler: T,
     timeout = 0i32): int32 {.jsfunc.} =
@@ -322,8 +333,9 @@ proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {.
 
 proc handleRead(client: Client, fd: int) =
   if client.pager.infile != nil and fd == client.pager.infile.getFileHandle():
-    client.input()
-    client.handlePagerEvents()
+    client.input().then(proc() =
+      client.handlePagerEvents()
+    )
   elif fd == client.forkserver.estream.fd:
     var nl = false
     const prefix = "STDERR: "
@@ -405,9 +417,9 @@ proc inputLoop(client: Client) =
       if selectors.Event.Timer in event.events:
         let r = client.timeouts.runTimeoutFd(event.fd)
         assert r
-        client.runJSJobs()
         client.pager.container.requestLines().then(proc() =
           client.pager.container.cursorLastLine())
+    client.runJSJobs()
     client.loader.unregistered.setLen(0)
     client.acceptBuffers()
     if client.pager.scommand != "":
diff --git a/src/local/container.nim b/src/local/container.nim
index fc113ad3..2a0051e6 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -11,6 +11,7 @@ import display/winattrs
 import extern/stdio
 import io/promise
 import io/serialize
+import js/dict
 import js/javascript
 import js/regex
 import loader/request
@@ -60,11 +61,21 @@ type
       force*: bool
     else: discard
 
-  Highlight = object
-    x, y: int
-    endy, endx: int
-    rect: bool
-    clear: bool
+  HighlightType = enum
+    HL_SEARCH, HL_SELECT
+
+  SelectionType = enum
+    SEL_NORMAL = "normal"
+    SEL_BLOCK = "block"
+    SEL_LINE = "line"
+
+  Highlight = ref object
+    case t: HighlightType
+    of HL_SEARCH: discard
+    of HL_SELECT:
+      selectionType {.jsget.}: SelectionType
+    x1, y1: int
+    x2, y2: int
 
   Container* = ref object
     parent* {.jsget.}: Container
@@ -90,7 +101,6 @@ type
     retry*: seq[URL]
     hlon*: bool # highlight on?
     sourcepair*: Container # pointer to buffer with a source view (may be nil)
-    redraw*: bool
     needslines*: bool
     canceled: bool
     events*: Deque[ContainerEvent]
@@ -100,7 +110,9 @@ type
     select*: Select
     canreinterpret*: bool
     cloned: bool
+    currentSelection {.jsget.}: Highlight
 
+jsDestructor(Highlight)
 jsDestructor(Container)
 
 proc newBuffer*(forkserver: ForkServer, mainproc: Pid, config: BufferConfig,
@@ -162,7 +174,6 @@ proc clone*(container: Container, newurl: URL): Promise[Container] =
       code: container.code,
       retry: container.retry,
       hlon: container.hlon,
-      redraw: container.redraw,
       #needslines: container.needslines,
       canceled: container.canceled,
       events: container.events,
@@ -205,16 +216,18 @@ func lastVisibleLine(container: Container): int = min(container.fromy + containe
 func currentLine(container: Container): string =
   return container.getLine(container.cursory).str
 
-func cursorBytes(container: Container, y: int, cc = container.cursorx): int =
-  let line = container.getLine(y).str
-  var w = 0
-  var i = 0
-  while i < line.len and w < cc:
+func findColBytes(s: string, endx: int, startx = 0, starti = 0): int =
+  var w = startx
+  var i = starti
+  while i < s.len and w < endx:
     var r: Rune
-    fastRuneAt(line, i, r)
+    fastRuneAt(s, i, r)
     w += r.twidth(w)
   return i
 
+func cursorBytes(container: Container, y: int, cc = container.cursorx): int =
+  return container.getLine(y).str.findColBytes(cc, 0, 0)
+
 func currentCursorBytes(container: Container, cc = container.cursorx): int =
   return container.cursorBytes(container.cursory, cc)
 
@@ -310,35 +323,63 @@ func lineWindow(container: Container): Slice[int] =
     x = 0
   return x .. y
 
-func contains*(hl: Highlight, x, y: int): bool =
-  if hl.rect:
-    let rx = hl.x .. hl.endx
-    let ry = hl.y .. hl.endy
-    return x in rx and y in ry
+func startx(hl: Highlight): int =
+  if hl.y1 < hl.y2:
+    hl.x1
+  elif hl.y2 < hl.y1:
+    hl.x2
   else:
-    return (y > hl.y or y == hl.y and x >= hl.x) and
-      (y < hl.endy or y == hl.endy and x <= hl.endx)
-
-func contains*(hl: Highlight, y: int): bool =
-  return y in hl.y .. hl.endy
-
-func colorArea*(hl: Highlight, y: int, limitx: Slice[int]): Slice[int] =
-  if hl.rect:
-    if y in hl.y .. hl.endy:
-      return max(hl.x, limitx.a) .. min(hl.endx, limitx.b)
+    min(hl.x1, hl.x2)
+func starty(hl: Highlight): int = min(hl.y1, hl.y2)
+func endx(hl: Highlight): int =
+  if hl.y1 > hl.y2:
+    hl.x1
+  elif hl.y2 > hl.y1:
+    hl.x2
+  else:
+    max(hl.x1, hl.x2)
+func endy(hl: Highlight): int = max(hl.y1, hl.y2)
+
+func colorNormal(container: Container, hl: Highlight, y: int,
+    limitx: Slice[int]): Slice[int] =
+  let starty = hl.starty
+  let endy = hl.endy
+  if y in starty + 1 .. endy - 1:
+    let w = container.getLine(y).str.width()
+    return min(limitx.a, w) .. min(limitx.b, w)
+  if y == starty and y == endy:
+    return max(hl.startx, limitx.a) .. min(hl.endx, limitx.b)
+  if y == starty:
+    let w = container.getLine(y).str.width()
+    return max(hl.startx, limitx.a) .. min(limitx.b, w)
+  if y == endy:
+    let w = container.getLine(y).str.width()
+    return min(limitx.a, w) .. min(hl.endx, limitx.b)
+
+func colorArea(container: Container, hl: Highlight, y: int,
+    limitx: Slice[int]): Slice[int] =
+  case hl.t
+  of HL_SELECT:
+    case hl.selectionType
+    of SEL_NORMAL:
+      return container.colorNormal(hl, y, limitx)
+    of SEL_BLOCK:
+      if y in hl.starty .. hl.endy:
+        let (x, endx) = if hl.x1 < hl.x2:
+          (hl.x1, hl.x2)
+        else:
+          (hl.x2, hl.x1)
+        return max(x, limitx.a) .. min(endx, limitx.b)
+    of SEL_LINE:
+      if y in hl.starty .. hl.endy:
+        let w = container.getLine(y).str.width()
+        return min(limitx.a, w) .. min(limitx.b, w)
   else:
-    if y in hl.y + 1 .. hl.endy - 1:
-      return limitx
-    if y == hl.y and y == hl.endy:
-      return max(hl.x, limitx.a) .. min(hl.endx, limitx.b)
-    if y == hl.y:
-      return max(hl.x, limitx.a) .. limitx.b
-    if y == hl.endy:
-      return limitx.a .. min(hl.endx, limitx.b)
+    return container.colorNormal(hl, y, limitx)
 
 func findHighlights*(container: Container, y: int): seq[Highlight] =
   for hl in container.highlights:
-    if y in hl:
+    if y in hl.starty .. hl.endy:
       result.add(hl)
 
 func getHoverText*(container: Container): string =
@@ -449,6 +490,10 @@ proc setCursorX(container: Container, x: int, refresh = true, save = true)
   elif x < container.cursorx:
     container.setFromX(x, false)
     container.pos.cursorx = x
+  if container.cursorx == x and container.currentSelection != nil and
+      container.currentSelection.x2 != x:
+    container.currentSelection.x2 = x
+    container.triggerEvent(UPDATE)
   if refresh:
     container.sendCursorPosition()
   if save:
@@ -469,6 +514,9 @@ proc setCursorY(container: Container, y: int, refresh = true) {.jsfunc.} =
     else:
       container.setFromY(y)
     container.pos.cursory = y
+  if container.currentSelection != nil and container.currentSelection.y2 != y:
+    container.triggerEvent(UPDATE)
+    container.currentSelection.y2 = y
   container.restoreCursorX()
   if refresh:
     container.sendCursorPosition()
@@ -818,7 +866,7 @@ proc cursorRevNthLink*(container: Container, n = 1) {.jsfunc.} =
 
 proc clearSearchHighlights*(container: Container) =
   for i in countdown(container.highlights.high, 0):
-    if container.highlights[i].clear:
+    if container.highlights[i].t == HL_SEARCH:
       container.highlights.del(i)
 
 proc onMatch(container: Container, res: BufferMatch, refresh: bool) =
@@ -827,7 +875,13 @@ proc onMatch(container: Container, res: BufferMatch, refresh: bool) =
     if container.hlon:
       container.clearSearchHighlights()
       let ex = res.x + res.str.twidth(res.x) - 1
-      let hl = Highlight(x: res.x, y: res.y, endx: ex, endy: res.y, clear: true)
+      let hl = Highlight(
+        t: HL_SEARCH,
+        x1: res.x,
+        y1: res.y,
+        x2: ex,
+        y2: res.y
+      )
       container.highlights.add(hl)
       container.triggerEvent(UPDATE)
       container.hlon = false
@@ -863,6 +917,73 @@ proc cursorPrevMatch*(container: Container, regex: Regex, wrap, refresh: bool,
       .then(proc(res: BufferMatch) =
         container.onMatch(res, refresh))
 
+type
+  SelectionOptions = object of JSDict
+    selectionType: SelectionType
+
+proc cursorToggleSelection(container: Container, n = 1,
+    opts = SelectionOptions()): Highlight {.jsfunc.} =
+  if container.currentSelection != nil:
+    let i = container.highlights.find(container.currentSelection)
+    if i != -1:
+      container.highlights.delete(i)
+    container.currentSelection = nil
+  else:
+    let n = n - 1
+    let hl = Highlight(
+      t: HL_SELECT,
+      selectionType: opts.selectionType,
+      x1: container.cursorx,
+      y1: container.cursory,
+      x2: container.cursorx + n,
+      y2: container.cursory
+    )
+    container.highlights.add(hl)
+    container.currentSelection = hl
+    container.cursorRight(n)
+  container.triggerEvent(UPDATE)
+  return container.currentSelection
+
+#TODO I don't like this API
+# maybe make selection a subclass of highlight?
+proc getSelectionText(container: Container, hl: Highlight = nil):
+    Promise[string] {.jsfunc.} =
+  let hl = if hl == nil: container.currentSelection else: hl
+  if hl.t != HL_SELECT:
+    let p = newPromise[string]()
+    p.resolve("")
+    return p
+  let startx = hl.startx
+  let starty = hl.starty
+  let endx = hl.endx
+  let endy = hl.endy
+  let nw = starty .. endy
+  return container.iface.getLines(nw).then(proc(res: GetLinesResult): string =
+    var s = ""
+    case hl.selectionType
+    of SEL_NORMAL:
+      if starty == endy:
+        let si = res.lines[0].str.findColBytes(startx)
+        let ei = res.lines[0].str.findColBytes(endx, startx, si)
+        s = res.lines[0].str.substr(si, ei)
+      else:
+        let si = res.lines[0].str.findColBytes(startx)
+        s &= res.lines[0].str.substr(si) & '\n'
+        for i in 1 .. res.lines.high - 1:
+          s &= res.lines[i].str & '\n'
+        let ei = res.lines[^1].str.findColBytes(endx)
+        s &= res.lines[^1].str.substr(0, ei)
+    of SEL_BLOCK:
+      for line in res.lines:
+        let si = line.str.findColBytes(startx)
+        let ei = line.str.findColBytes(endx, startx, si)
+        s &= line.str.substr(si, ei) & '\n'
+    of SEL_LINE:
+      for line in res.lines:
+        s &= line.str & '\n'
+    return s
+  )
+
 proc setLoadInfo(container: Container, msg: string) =
   container.loadinfo = msg
   container.triggerEvent(STATUS)
@@ -1169,7 +1290,8 @@ proc drawLines*(container: Container, display: var FixedGrid,
     let hls = container.findHighlights(container.fromy + by)
     let aw = container.width - (startw - container.fromx) # actual width
     for hl in hls:
-      let area = hl.colorArea(container.fromy + by, startw .. startw + aw)
+      let area = container.colorArea(hl, container.fromy + by,
+        startw .. startw + aw)
       for i in area:
         if i - startw >= container.width:
           break
@@ -1185,4 +1307,5 @@ proc handleEvent*(container: Container) =
     container.needslines = false
 
 proc addContainerModule*(ctx: JSContext) =
+  ctx.registerType(Highlight)
   ctx.registerType(Container, name = "Buffer")
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 005fefa5..11364ad5 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -243,10 +243,9 @@ proc refreshDisplay(pager: Pager, container = pager.container) =
   container.drawLines(pager.display,
     cellColor(pager.config.display.highlight_color))
 
-# Note: this function doesn't work if start < i of last written char
-proc writeStatusMessage(pager: Pager, str: string,
-                        format: Format = newFormat(), start = 0,
-                        maxwidth = -1, clip = '$'): int {.discardable.} =
+# Note: this function does not work correctly if start < i of last written char
+proc writeStatusMessage(pager: Pager, str: string, format = newFormat(),
+    start = 0, maxwidth = -1, clip = '$'): int {.discardable.} =
   var maxwidth = maxwidth
   if maxwidth == -1:
     maxwidth = pager.statusgrid.len
@@ -255,22 +254,20 @@ proc writeStatusMessage(pager: Pager, str: string,
   if i >= e:
     return i
   for r in str.runes:
-    let pi = i
-    i += r.width()
-    if i >= e:
-      if i >= pager.statusgrid.width:
-        i = pi
+    let w = r.width()
+    if i + w >= e:
       pager.statusgrid[i].format = format
       pager.statusgrid[i].str = $clip
-      inc i
+      inc i # Note: we assume `clip' is 1 cell wide
       break
     if r.isControlChar():
-      pager.statusgrid[pi].str = "^"
-      pager.statusgrid[pi + 1].str = $getControlLetter(char(r))
-      pager.statusgrid[pi + 1].format = format
+      pager.statusgrid[i].str = "^"
+      pager.statusgrid[i + 1].str = $getControlLetter(char(r))
+      pager.statusgrid[i + 1].format = format
     else:
-      pager.statusgrid[pi].str = $r
-    pager.statusgrid[pi].format = format
+      pager.statusgrid[i].str = $r
+    pager.statusgrid[i].format = format
+    i += w
   result = i
   var def = newFormat()
   while i < e:
@@ -883,10 +880,14 @@ proc extern(pager: Pager, cmd: string, t = ExternDict()): bool {.jsfunc.} =
 proc externCapture(pager: Pager, cmd: string): Opt[string] {.jsfunc.} =
   pager.setEnvVars()
   var s: string
-  if not runProcessCapture(pager.term, cmd, s):
+  if not runProcessCapture(cmd, s):
     return err()
   return ok(s)
 
+proc externInto(pager: Pager, cmd, ins: string): bool {.jsfunc.} =
+  pager.setEnvVars()
+  return runProcessInto(cmd, ins)
+
 proc authorize(pager: Pager) =
   pager.setLineEdit("Username: ", USERNAME)