about summary refs log tree commit diff stats
path: root/src/local
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-09-08 15:18:45 +0200
committerbptato <nincsnevem662@gmail.com>2024-09-08 16:06:02 +0200
commit4124c041ed2e3b497ede72fdae229aa2c6aca249 (patch)
treee8488449de6f0be54b9c79547352829b998833d3 /src/local
parent5a64e3193924c7e503dddb10a99989148b26e922 (diff)
downloadchawan-4124c041ed2e3b497ede72fdae229aa2c6aca249.tar.gz
utils: add twtuni
std/unicode has the following issues:

* Rune is an int32, which implies overflow checking. Also, it is
  distinct, so you have to convert it manually to do arithmetic.
* QJS libunicode and Chagashi work with uint32, interfacing with these
  required pointless type conversions.
* fastRuneAt is a template, meaning it's pasted into every call
  site. Also, it decodes to UCS-4, so it generates two branches that
  aren't even used. Overall this lead to quite some code bloat.
* fastRuneAt and lastRune have frustratingly different
  interfaces. Writing code to handle both cases is error prone.
* On older Nim versions which we still support, std/unicode takes
  strings, not openArray[char]'s.

Replace it with "twtuni", which includes some improved versions of
the few procedures from std/unicode that we actually use.
Diffstat (limited to 'src/local')
-rw-r--r--src/local/container.nim205
-rw-r--r--src/local/lineedit.nim125
-rw-r--r--src/local/pager.nim21
-rw-r--r--src/local/term.nim1
4 files changed, 175 insertions, 177 deletions
diff --git a/src/local/container.nim b/src/local/container.nim
index 2c12c4ae..cb7738b8 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -3,7 +3,6 @@ import std/options
 import std/os
 import std/posix
 import std/tables
-import std/unicode
 
 import chagashi/charset
 import config/config
@@ -32,6 +31,7 @@ import utils/luwrap
 import utils/mimeguess
 import utils/strwidth
 import utils/twtstr
+import utils/twtuni
 import utils/wordbreak
 
 type
@@ -369,12 +369,12 @@ proc popCursorPos(select: Select; nojump = false) =
   if not nojump:
     select.queueDraw()
 
-const HorizontalBar = $Rune(0x2500)
-const VerticalBar = $Rune(0x2502)
-const CornerTopLeft = $Rune(0x250C)
-const CornerTopRight = $Rune(0x2510)
-const CornerBottomLeft = $Rune(0x2514)
-const CornerBottomRight = $Rune(0x2518)
+const HorizontalBar = "\u2500"
+const VerticalBar = "\u2502"
+const CornerTopLeft = "\u250C"
+const CornerTopRight = "\u2510"
+const CornerBottomLeft = "\u2514"
+const CornerBottomRight = "\u2518"
 
 proc drawBorders(display: var FixedGrid; sx, ex, sy, ey: int;
     upmore, downmore: bool) =
@@ -446,7 +446,6 @@ proc drawSelect*(select: Select; display: var FixedGrid) =
   # move inside border
   inc sy
   inc sx
-  var r: Rune
   var k = 0
   var format = Format()
   while k < select.selected.len and select.selected[k] < si:
@@ -462,13 +461,13 @@ proc drawSelect*(select: Select; display: var FixedGrid) =
     else:
       format.flags.excl(ffReverse)
     while j < select.options[i].len:
-      fastRuneAt(select.options[i], j, r)
-      let rw = r.twidth(x)
+      let pj = j
+      let u = select.options[i].nextUTF8(j)
       let ox = x
-      x += rw
+      x += u.twidth(x)
       if x > ex:
         break
-      display[dls + ox].str = $r
+      display[dls + ox].str = select.options[i].substr(pj, j - 1)
       display[dls + ox].format = format
     while x < ex:
       display[dls + x].str = " "
@@ -578,9 +577,8 @@ 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(s, i, r)
-    w += r.twidth(w)
+    let u = s.nextUTF8(i)
+    w += u.twidth(w)
   return i
 
 func cursorBytes(container: Container; y: int; cc = container.cursorx): int =
@@ -596,11 +594,10 @@ func cursorFirstX(container: Container): int =
   let line = container.currentLine
   var w = 0
   var i = 0
-  var r: Rune
   let cc = container.cursorx
   while i < line.len:
-    fastRuneAt(line, i, r)
-    let tw = r.twidth(w)
+    let u = line.nextUTF8(i)
+    let tw = u.twidth(w)
     if w + tw > cc:
       return w
     w += tw
@@ -613,11 +610,10 @@ func cursorLastX(container: Container): int =
   let line = container.currentLine
   var w = 0
   var i = 0
-  var r: Rune
   let cc = container.cursorx
   while i < line.len and w <= cc:
-    fastRuneAt(line, i, r)
-    w += r.twidth(w)
+    let u = line.nextUTF8(i)
+    w += u.twidth(w)
   return max(w - 1, 0)
 
 # Last cell for tab, first cell for everything else (e.g. double width.)
@@ -630,16 +626,15 @@ func cursorDispX(container: Container): int =
   var w = 0
   var pw = 0
   var i = 0
-  var r: Rune
+  var u = 0u32
   let cc = container.cursorx
   while i < line.len and w <= cc:
-    fastRuneAt(line, i, r)
+    u = line.nextUTF8(i)
     pw = w
-    w += r.twidth(w)
-  if r == Rune('\t'):
+    w += u.twidth(w)
+  if u == uint32('\t'):
     return max(w - 1, 0)
-  else:
-    return pw
+  return pw
 
 func acursorx*(container: Container): int =
   max(0, container.cursorDispX() - container.fromx)
@@ -911,10 +906,10 @@ proc setCursorXY*(container: Container; x, y: int; refresh = true) {.jsfunc.} =
 proc cursorLineTextStart(container: Container) {.jsfunc.} =
   if container.numLines == 0: return
   var x = 0
-  for r in container.currentLine.runes:
-    if not container.luctx.isWhiteSpaceLU(r):
+  for u in container.currentLine.points:
+    if not container.luctx.isWhiteSpaceLU(u):
       break
-    x += r.twidth(x)
+    x += u.twidth(x)
   if x == 0:
     dec x
   container.setCursorX(x)
@@ -1020,45 +1015,62 @@ proc cursorLineBegin(container: Container) {.jsfunc.} =
 proc cursorLineEnd(container: Container) {.jsfunc.} =
   container.setCursorX(container.currentLineWidth() - 1)
 
-type BreakFunc = proc(ctx: LUContext; r: Rune): BreakCategory {.nimcall.}
+type BreakFunc = proc(ctx: LUContext; r: uint32): BreakCategory {.nimcall.}
 
-proc skipSpace(container: Container; b, x: var int; breakFunc: BreakFunc) =
+# move to first char that is not in this category
+proc skipCat(container: Container; b, x: var int; breakFunc: BreakFunc;
+    cat: BreakCategory) =
   while b < container.currentLine.len:
-    var r: Rune
     let pb = b
-    fastRuneAt(container.currentLine, b, r)
-    if container.luctx.breakFunc(r) != bcSpace:
+    let u = container.currentLine.nextUTF8(b)
+    if container.luctx.breakFunc(u) != cat:
       b = pb
       break
-    x += r.twidth(x)
+    x += u.twidth(x)
 
-proc skipSpaceRev(container: Container; b, x: var int; breakFunc: BreakFunc) =
-  while b >= 0:
-    let (r, o) = lastRune(container.currentLine, b)
-    if container.luctx.breakFunc(r) != bcSpace:
+proc skipSpace(container: Container; b, x: var int; breakFunc: BreakFunc) =
+  container.skipCat(b, x, breakFunc, bcSpace)
+
+# move to last char in category, backwards
+proc lastCatRev(container: Container; b, x: var int; breakFunc: BreakFunc;
+    cat: BreakCategory) =
+  while b > 0:
+    let pb = b
+    let u = container.currentLine.prevUTF8(b)
+    if container.luctx.breakFunc(u) != cat:
+      b = pb
       break
-    b -= o
-    x -= r.twidth(x)
+    x -= u.width()
+
+# move to first char that is not in this category, backwards
+proc skipCatRev(container: Container; b, x: var int; breakFunc: BreakFunc;
+    cat: BreakCategory): BreakCategory =
+  while b > 0:
+    let u = container.currentLine.prevUTF8(b)
+    x -= u.width()
+    let it = container.luctx.breakFunc(u)
+    if it != cat:
+      return it
+  b = -1
+  return cat
+
+proc skipSpaceRev(container: Container; b, x: var int; breakFunc: BreakFunc):
+    BreakCategory =
+  return container.skipCatRev(b, x, breakFunc, bcSpace)
 
 proc cursorNextWord(container: Container; breakFunc: BreakFunc) =
   if container.numLines == 0: return
-  var r: Rune
   var b = container.currentCursorBytes()
   var x = container.cursorx
   # meow
   let currentCat = if b < container.currentLine.len:
-    container.luctx.breakFunc(container.currentLine.runeAt(b))
+    var tmp = b
+    container.luctx.breakFunc(container.currentLine.nextUTF8(tmp))
   else:
     bcSpace
   if currentCat != bcSpace:
     # not in space, skip chars that have the same category
-    while b < container.currentLine.len:
-      let pb = b
-      fastRuneAt(container.currentLine, b, r)
-      if container.luctx.breakFunc(r) != currentCat:
-        b = pb
-        break
-      x += r.twidth(x)
+    container.skipCat(b, x, breakFunc, currentCat)
   container.skipSpace(b, x, breakFunc)
   if b < container.currentLine.len:
     container.setCursorX(x)
@@ -1084,19 +1096,16 @@ proc cursorPrevWord(container: Container; breakFunc: BreakFunc) =
   var x = container.cursorx
   if container.currentLine.len > 0:
     b = min(b, container.currentLine.len - 1)
-    let currentCat = if b >= 0:
-      container.luctx.breakFunc(container.currentLine.runeAt(b))
+    var currentCat = if b >= 0:
+      var tmp = b
+      container.luctx.breakFunc(container.currentLine.nextUTF8(tmp))
     else:
       bcSpace
     if currentCat != bcSpace:
       # not in space, skip chars that have the same category
-      while b >= 0:
-        let (r, o) = lastRune(container.currentLine, b)
-        if container.luctx.breakFunc(r) != currentCat:
-          break
-        b -= o
-        x -= r.twidth(x)
-    container.skipSpaceRev(b, x, breakFunc)
+      currentCat = container.skipCatRev(b, x, breakFunc, currentCat)
+    if currentCat == bcSpace:
+      discard container.skipSpaceRev(b, x, breakFunc)
   else:
     b = -1
   if b >= 0:
@@ -1119,32 +1128,33 @@ proc cursorPrevBigWord(container: Container) {.jsfunc.} =
 
 proc cursorWordEnd(container: Container; breakFunc: BreakFunc) =
   if container.numLines == 0: return
-  var r: Rune
   var b = container.currentCursorBytes()
   var x = container.cursorx
   var px = x
   # if not in space, move to the right by one
   if b < container.currentLine.len:
     let pb = b
-    fastRuneAt(container.currentLine, b, r)
-    if container.luctx.breakFunc(r) == bcSpace:
+    let u = container.currentLine.nextUTF8(b)
+    if container.luctx.breakFunc(u) == bcSpace:
       b = pb
     else:
       px = x
-      x += r.twidth(x)
+      x += u.twidth(x)
   container.skipSpace(b, x, breakFunc)
   # move to the last char in the current category
   let ob = b
   if b < container.currentLine.len:
-    let currentCat = container.luctx.breakFunc(container.currentLine.runeAt(b))
+    var tmp = b
+    let u = container.currentLine.nextUTF8(tmp)
+    let currentCat = container.luctx.breakFunc(u)
     while b < container.currentLine.len:
       let pb = b
-      fastRuneAt(container.currentLine, b, r)
-      if container.luctx.breakFunc(r) != currentCat:
+      let u = container.currentLine.nextUTF8(b)
+      if container.luctx.breakFunc(u) != currentCat:
         b = pb
         break
       px = x
-      x += r.twidth(x)
+      x += u.twidth(x)
     x = px
   if b < container.currentLine.len or ob != b:
     container.setCursorX(x)
@@ -1168,35 +1178,27 @@ proc cursorWordBegin(container: Container; breakFunc: BreakFunc) =
   if container.numLines == 0: return
   var b = container.currentCursorBytes()
   var x = container.cursorx
-  var px = x
-  var ob = b
   if container.currentLine.len > 0:
     b = min(b, container.currentLine.len - 1)
     if b >= 0:
-      let (r, o) = lastRune(container.currentLine, b)
+      var tmp = b
+      var u = container.currentLine.nextUTF8(tmp)
+      var currentCat = container.luctx.breakFunc(u)
       # if not in space, move to the left by one
-      if container.luctx.breakFunc(r) != bcSpace:
-        b -= o
-        px = x
-        x -= r.twidth(x)
-    container.skipSpaceRev(b, x, breakFunc)
-    # move to the first char in the current category
-    ob = b
-    if b >= 0:
-      let (r, _) = lastRune(container.currentLine, b)
-      let currentCat = container.luctx.breakFunc(r)
-      while b >= 0:
-        let (r, o) = lastRune(container.currentLine, b)
-        if container.luctx.breakFunc(r) != currentCat:
-          break
-        b -= o
-        px = x
-        x -= r.twidth(x)
-    x = px
+      if currentCat != bcSpace:
+        if b > 0:
+          u = container.currentLine.prevUTF8(b)
+          x -= u.width()
+          currentCat = container.luctx.breakFunc(u)
+        else:
+          b = -1
+      if container.luctx.breakFunc(u) == bcSpace:
+        currentCat = container.skipSpaceRev(b, x, breakFunc)
+      # move to the first char in the current category
+      container.lastCatRev(b, x, breakFunc, currentCat)
   else:
     b = -1
-    ob = -1
-  if b >= 0 or ob != b:
+  if b >= 0:
     container.setCursorX(x)
   else:
     if container.cursory > 0:
@@ -1994,7 +1996,6 @@ proc drawLines*(container: Container; display: var FixedGrid; hlcolor: CellColor
       cell.format = cf.format
     if bgcolor != defaultColor and cell.format.bgcolor == defaultColor:
       cell.format.bgcolor = bgcolor
-  var r: Rune
   var by = 0
   let endy = min(container.fromy + display.height, container.numLines)
   for line in container.ilines(container.fromy ..< endy):
@@ -2002,8 +2003,8 @@ proc drawLines*(container: Container; display: var FixedGrid; hlcolor: CellColor
     var i = 0 # byte in line.str
     # Skip cells till fromx.
     while w < container.fromx and i < line.str.len:
-      fastRuneAt(line.str, i, r)
-      w += r.twidth(w)
+      let u = line.str.nextUTF8(i)
+      w += u.twidth(w)
     let dls = by * display.width # starting position of row in display
     # Fill in the gap in case we skipped more cells than fromx mandates (i.e.
     # we encountered a double-width character.)
@@ -2018,25 +2019,27 @@ proc drawLines*(container: Container; display: var FixedGrid; hlcolor: CellColor
     # Now fill in the visible part of the row.
     while i < line.str.len:
       let pw = w
-      fastRuneAt(line.str, i, r)
-      let rw = r.twidth(w)
-      w += rw
+      let pi = i
+      let u = line.str.nextUTF8(i)
+      let uw = u.twidth(w)
+      w += uw
       if w > container.fromx + display.width:
         break # die on exceeding the width limit
       if nf.pos != -1 and nf.pos <= pw:
         cf = nf
         nf = line.findNextFormat(pw)
-      if r == Rune('\t'):
+      if u == uint32('\t'):
         # Needs to be replaced with spaces, otherwise bgcolor isn't displayed.
-        let tk = k + rw
+        let tk = k + uw
         while k < tk:
           display[dls + k].str &= ' '
           set_fmt display[dls + k], cf
           inc k
       else:
-        display[dls + k].str &= r
+        for j in pi ..< i:
+          display[dls + k].str &= line.str[j]
         set_fmt display[dls + k], cf
-        k += rw
+        k += uw
     if bgcolor != defaultColor:
       # Fill the screen if bgcolor is not default.
       while k < display.width:
diff --git a/src/local/lineedit.nim b/src/local/lineedit.nim
index ecfc3db8..edb6ee09 100644
--- a/src/local/lineedit.nim
+++ b/src/local/lineedit.nim
@@ -1,5 +1,4 @@
 import std/strutils
-import std/unicode
 
 import chagashi/charset
 import chagashi/decoder
@@ -11,6 +10,7 @@ import types/winattrs
 import utils/luwrap
 import utils/strwidth
 import utils/twtstr
+import utils/twtuni
 import utils/wordbreak
 
 type
@@ -37,6 +37,7 @@ type
     hist: LineHistory
     histindex: int
     histtmp: string
+    luctx: LUContext
     redraw*: bool
 
 jsDestructor(LineEdit)
@@ -48,10 +49,8 @@ func newLineHistory*(): LineHistory =
 func getDisplayWidth(edit: LineEdit): int =
   var dispw = 0
   var i = edit.shifti
-  var r: Rune
   while i < edit.news.len and dispw < edit.maxwidth:
-    fastRuneAt(edit.news, i, r)
-    dispw += r.width()
+    dispw += edit.news.nextUTF8(i).width()
   return dispw
 
 proc shiftView(edit: LineEdit) =
@@ -69,17 +68,14 @@ proc shiftView(edit: LineEdit) =
         edit.shifti = 0
       else:
         while edit.shiftx > targetx:
-          let (r, len) = edit.news.lastRune(edit.shifti - 1)
-          edit.shiftx -= r.width()
-          edit.shifti -= len
+          let u = edit.news.prevUTF8(edit.shifti)
+          edit.shiftx -= u.width()
   edit.padding = 0
   # Shift view so it contains the cursor. (act 2)
   if edit.shiftx < edit.cursorx - edit.maxwidth:
     while edit.shiftx < edit.cursorx - edit.maxwidth and
         edit.shifti < edit.news.len:
-      var r: Rune
-      fastRuneAt(edit.news, edit.shifti, r)
-      edit.shiftx += r.width()
+      edit.shiftx += edit.news.nextUTF8(edit.shifti).width()
     if edit.shiftx > edit.cursorx - edit.maxwidth:
       # skipped over a cell because of a double-width char
       edit.padding = 1
@@ -89,9 +85,9 @@ proc generateOutput*(edit: LineEdit): FixedGrid =
   # Make the output grid +1 cell wide, so it covers the whole input area.
   result = newFixedGrid(edit.promptw + edit.maxwidth + 1)
   var x = 0
-  for r in edit.prompt.runes:
-    result[x].str &= $r
-    x += r.width()
+  for u in edit.prompt.points:
+    result[x].str.addUTF8(u)
+    x += u.width()
     if x >= result.width: break
   for i in 0 ..< edit.padding:
     if x < result.width:
@@ -99,18 +95,19 @@ proc generateOutput*(edit: LineEdit): FixedGrid =
       inc x
   var i = edit.shifti
   while i < edit.news.len:
-    var r: Rune
-    fastRuneAt(edit.news, i, r)
+    let pi = i
+    let u = edit.news.nextUTF8(i)
     if not edit.hide:
-      let w = r.width()
+      let w = u.width()
       if x + w > result.width: break
-      if r.isControlChar():
+      if u.isControlChar():
         result[x].str &= '^'
         inc x
-        result[x].str &= char(r).getControlLetter()
+        result[x].str &= char(u).getControlLetter()
         inc x
       else:
-        result[x].str &= $r
+        for j in pi ..< i:
+          result[x].str &= edit.news[j]
         x += w
     else:
       if x + 1 > result.width: break
@@ -143,10 +140,10 @@ proc submit(edit: LineEdit) {.jsfunc.} =
 
 proc backspace(edit: LineEdit) {.jsfunc.} =
   if edit.cursori > 0:
-    let (r, len) = edit.news.lastRune(edit.cursori - 1)
-    edit.news.delete(edit.cursori - len .. edit.cursori - 1)
-    edit.cursori -= len
-    edit.cursorx -= r.width()
+    let pi = edit.cursori
+    let u = edit.news.prevUTF8(edit.cursori)
+    edit.news.delete(edit.cursori ..< pi)
+    edit.cursorx -= u.width()
     edit.redraw = true
  
 proc write*(edit: LineEdit; s: string; cs: Charset): bool =
@@ -171,7 +168,7 @@ proc write(edit: LineEdit; s: string): bool {.jsfunc.} =
 
 proc delete(edit: LineEdit) {.jsfunc.} =
   if edit.cursori < edit.news.len:
-    let len = edit.news.runeLenAt(edit.cursori)
+    let len = edit.news.pointLenAt(edit.cursori)
     edit.news.delete(edit.cursori ..< edit.cursori + len)
     edit.redraw = true
 
@@ -192,55 +189,53 @@ proc kill(edit: LineEdit) {.jsfunc.} =
 
 proc backward(edit: LineEdit) {.jsfunc.} =
   if edit.cursori > 0:
-    let (r, len) = edit.news.lastRune(edit.cursori - 1)
-    edit.cursori -= len
-    edit.cursorx -= r.width()
+    let u = edit.news.prevUTF8(edit.cursori)
+    edit.cursorx -= u.width()
     if edit.cursorx < edit.shiftx:
       edit.redraw = true
 
 proc forward(edit: LineEdit) {.jsfunc.} =
   if edit.cursori < edit.news.len:
-    var r: Rune
-    fastRuneAt(edit.news, edit.cursori, r)
-    edit.cursorx += r.width()
+    let u = edit.news.nextUTF8(edit.cursori)
+    edit.cursorx += u.width()
     if edit.cursorx >= edit.shiftx + edit.maxwidth:
       edit.redraw = true
 
 proc prevWord(edit: LineEdit) {.jsfunc.} =
   if edit.cursori == 0:
     return
-  let ctx = LUContext()
-  let (r, len) = edit.news.lastRune(edit.cursori - 1)
-  if ctx.breaksWord(r):
-    edit.cursori -= len
-    edit.cursorx -= r.width()
+  let pi = edit.cursori
+  let u = edit.news.prevUTF8(edit.cursori)
+  if edit.luctx.breaksWord(u):
+    edit.cursorx -= u.width()
+  else:
+    edit.cursori = pi
   while edit.cursori > 0:
-    let (r, len) = edit.news.lastRune(edit.cursori - 1)
-    if ctx.breaksWord(r):
+    let pi = edit.cursori
+    let u = edit.news.prevUTF8(edit.cursori)
+    if edit.luctx.breaksWord(u):
+      edit.cursori = pi
       break
-    edit.cursori -= len
-    edit.cursorx -= r.width()
+    edit.cursorx -= u.width()
   if edit.cursorx < edit.shiftx:
     edit.redraw = true
 
 proc nextWord(edit: LineEdit) {.jsfunc.} =
   if edit.cursori >= edit.news.len:
     return
-  let ctx = LUContext()
-  let oc = edit.cursori
-  var r: Rune
-  fastRuneAt(edit.news, edit.cursori, r)
-  if ctx.breaksWord(r):
-    edit.cursorx += r.width()
+  let pi = edit.cursori
+  let u = edit.news.nextUTF8(edit.cursori)
+  if edit.luctx.breaksWord(u):
+    edit.cursorx += u.width()
   else:
-    edit.cursori = oc
+    edit.cursori = pi
   while edit.cursori < edit.news.len:
-    let pc = edit.cursori
-    fastRuneAt(edit.news, edit.cursori, r)
-    if ctx.breaksWord(r):
-      edit.cursori = pc
+    let pi = edit.cursori
+    let u = edit.news.nextUTF8(edit.cursori)
+    if edit.luctx.breaksWord(u):
+      edit.cursori = pi
       break
-    edit.cursorx += r.width()
+    edit.cursorx += u.width()
   if edit.cursorx >= edit.shiftx + edit.maxwidth:
     edit.redraw = true
 
@@ -254,18 +249,17 @@ proc clearWord(edit: LineEdit) {.jsfunc.} =
 proc killWord(edit: LineEdit) {.jsfunc.} =
   if edit.cursori >= edit.news.len:
     return
-  let oc = edit.cursori
-  let ox = edit.cursorx
-  edit.nextWord()
-  if edit.cursori != oc:
-    if edit.cursori < edit.news.len:
-      let len = edit.news.runeLenAt(edit.cursori)
-      edit.news.delete(oc ..< edit.cursori + len)
-    else:
-      edit.news.delete(oc ..< edit.cursori)
-    edit.cursori = oc
-    edit.cursorx = ox
-    edit.redraw = true
+  var i = edit.cursori
+  var u = edit.news.nextUTF8(i)
+  if not edit.luctx.breaksWord(u):
+    while i < edit.news.len:
+      let pi = i
+      let u = edit.news.nextUTF8(i)
+      if edit.luctx.breaksWord(u):
+        i = pi
+        break
+  edit.news.delete(edit.cursori ..< i)
+  edit.redraw = true
 
 proc begin(edit: LineEdit) {.jsfunc.} =
   edit.cursori = 0
@@ -310,7 +304,7 @@ proc windowChange*(edit: LineEdit; attrs: WindowAttributes) =
   edit.maxwidth = attrs.width - edit.promptw - 1
 
 proc readLine*(prompt, current: string; termwidth: int; disallowed: set[char];
-    hide: bool; hist: LineHistory): LineEdit =
+    hide: bool; hist: LineHistory; luctx: LUContext): LineEdit =
   let promptw = prompt.width()
   return LineEdit(
     prompt: prompt,
@@ -324,7 +318,8 @@ proc readLine*(prompt, current: string; termwidth: int; disallowed: set[char];
     # - 1, so that the cursor always has place
     maxwidth: termwidth - promptw - 1,
     hist: hist,
-    histindex: hist.lines.len
+    histindex: hist.lines.len,
+    luctx: luctx
   )
 
 proc addLineEditModule*(ctx: JSContext) =
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 035ec2d7..3822eb77 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -7,7 +7,6 @@ import std/posix
 import std/selectors
 import std/sets
 import std/tables
-import std/unicode
 
 import chagashi/charset
 import config/chapath
@@ -52,6 +51,7 @@ import utils/mimeguess
 import utils/regexutils
 import utils/strwidth
 import utils/twtstr
+import utils/twtuni
 
 type
   LineMode* = enum
@@ -284,7 +284,7 @@ proc setLineEdit(pager: Pager; mode: LineMode; current = ""; hide = false;
   if pager.term.isatty() and pager.config.input.use_mouse:
     pager.term.disableMouse()
   pager.lineedit = readLine($mode & extraPrompt, current, pager.attrs.width,
-    {}, hide, hist)
+    {}, hide, hist, pager.luctx)
   pager.linemode = mode
 
 proc clearLineEdit(pager: Pager) =
@@ -387,19 +387,19 @@ proc writeStatusMessage(pager: Pager; str: string; format = Format();
   if i >= e:
     return i
   pager.status.redraw = true
-  for r in str.runes:
-    let w = r.width()
+  for u in str.points:
+    let w = u.width()
     if i + w >= e:
       pager.status.grid[i].format = format
       pager.status.grid[i].str = $clip
       inc i # Note: we assume `clip' is 1 cell wide
       break
-    if r.isControlChar():
+    if u.isControlChar():
       pager.status.grid[i].str = "^"
-      pager.status.grid[i + 1].str = $getControlLetter(char(r))
+      pager.status.grid[i + 1].str = $getControlLetter(char(u))
       pager.status.grid[i + 1].format = format
     else:
-      pager.status.grid[i].str = $r
+      pager.status.grid[i].str = u.toUTF8()
     pager.status.grid[i].format = format
     i += w
   result = i
@@ -461,9 +461,8 @@ proc drawBuffer*(pager: Pager; container: Container; ofile: File) =
       for f in line.formats:
         let si = i
         while x < f.pos:
-          var r: Rune
-          fastRuneAt(line.str, i, r)
-          x += r.width()
+          let u = line.str.nextUTF8(i)
+          x += u.width()
         let outstr = line.str.substr(si, i - 1)
         s &= pager.term.processOutputString(outstr, w)
         s &= pager.term.processFormat(format, f.format)
@@ -576,6 +575,8 @@ proc initImages(pager: Pager; container: Container) =
       dispw = min(width + xpx, maxwpx) - xpx
       let ypx = (image.y - container.fromy) * pager.attrs.ppl
       erry = -min(ypx, 0) mod 6
+    if dispw <= offx:
+      continue
     let cached = container.findCachedImage(image, offx, erry, dispw)
     let imageId = image.bmp.imageId
     if cached == nil:
diff --git a/src/local/term.nim b/src/local/term.nim
index 263e6363..2f31104a 100644
--- a/src/local/term.nim
+++ b/src/local/term.nim
@@ -4,7 +4,6 @@ import std/posix
 import std/strutils
 import std/tables
 import std/termios
-import std/unicode
 
 import bindings/termcap
 import chagashi/charset