about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-06-22 17:16:44 +0200
committerbptato <nincsnevem662@gmail.com>2024-06-22 17:16:44 +0200
commitaa3e08c3ea432b8d9a9558c1609ebae1ab75cc1e (patch)
tree9172d7ec24c1cf7d817a40f9b23da18f2874bda3
parentca295c18cda7bbdffb42e65729f4dd969fe16d69 (diff)
downloadchawan-aa3e08c3ea432b8d9a9558c1609ebae1ab75cc1e.tar.gz
pager: refactor drawing code
* merge select into container
* avoid unnecessary redraws in draw() for parts of the screen that
  haven't been updated
* various image redraw fixes
-rw-r--r--src/local/client.nim26
-rw-r--r--src/local/container.nim394
-rw-r--r--src/local/lineedit.nim34
-rw-r--r--src/local/pager.nim204
-rw-r--r--src/local/select.nim307
-rw-r--r--src/local/term.nim74
-rw-r--r--todo1
7 files changed, 536 insertions, 504 deletions
diff --git a/src/local/client.nim b/src/local/client.nim
index c52ea71f..78213b6e 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -7,8 +7,9 @@ import std/posix
 import std/selectors
 import std/strutils
 import std/tables
-import std/unicode
 
+import chagashi/charset
+import chagashi/decoder
 import config/config
 import html/catom
 import html/chadombuilder
@@ -55,8 +56,6 @@ import types/opt
 import types/url
 import utils/twtstr
 
-import chagashi/charset
-
 type
   Client* = ref object of Window
     alive: bool
@@ -183,11 +182,6 @@ proc feedNext(client: Client) {.jsfunc.} =
 proc alert(client: Client; msg: string) {.jsfunc.} =
   client.pager.alert(msg)
 
-proc handlePagerEvents(client: Client) =
-  let container = client.pager.container
-  if container != nil:
-    client.pager.handleEvents(container)
-
 proc evalActionJS(client: Client; action: string): JSValue =
   client.config.cmd.map.withValue(action, p):
     return JS_DupValue(client.jsctx, p[])
@@ -244,7 +238,7 @@ proc handleCommandInput(client: Client; c: char): EmptyPromise =
     if not client.feednext:
       client.pager.precnum = 0
       client.pager.notnum = false
-      client.handlePagerEvents()
+      client.pager.handleEvents()
     return p
   if client.config.input.use_mouse:
     if client.pager.inputBuffer == "\e[<":
@@ -321,12 +315,12 @@ proc input(client: Client): EmptyPromise =
         client.pager.fulfillAsk(false)
     elif client.pager.askcharpromise != nil:
       buf &= c
-      if buf.validateUtf8() != -1:
+      if buf.validateUtf8Surr() != -1:
         continue
       client.pager.fulfillCharAsk(buf)
-    elif client.pager.lineedit.isSome:
+    elif client.pager.lineedit != nil:
       client.pager.inputBuffer &= c
-      let edit = client.pager.lineedit.get
+      let edit = client.pager.lineedit
       if edit.escNext:
         edit.escNext = false
         if edit.write(client.pager.inputBuffer, client.pager.term.cs):
@@ -469,7 +463,7 @@ proc acceptBuffers(client: Client) =
 proc handleRead(client: Client; fd: int) =
   if client.pager.term.istream != nil and fd == client.pager.term.istream.fd:
     client.input().then(proc() =
-      client.handlePagerEvents()
+      client.pager.handleEvents()
     )
   elif (let i = client.pager.findConnectingContainer(fd); i != -1):
     client.pager.handleConnectingContainer(i)
@@ -589,8 +583,8 @@ proc inputLoop(client: Client) =
     if client.pager.scommand != "":
       client.command(client.pager.scommand)
       client.pager.scommand = ""
-      client.handlePagerEvents()
-    if client.pager.container == nil and client.pager.lineedit.isNone:
+      client.pager.handleEvents()
+    if client.pager.container == nil and client.pager.lineedit == nil:
       # No buffer to display.
       if not client.pager.hasload:
         # Failed to load every single URL the user passed us. We quit, and that
@@ -800,7 +794,7 @@ proc btoa(ctx: JSContext; client: Client; data: JSValue): DOMResult[string]
   return btoa(ctx, data)
 
 func line(client: Client): LineEdit {.jsfget.} =
-  return client.pager.lineedit.get(nil)
+  return client.pager.lineedit
 
 proc addJSModules(client: Client; ctx: JSContext) =
   ctx.addWindowModule2()
diff --git a/src/local/container.nim b/src/local/container.nim
index 52498047..1fddb490 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -17,7 +17,6 @@ import layout/renderdocument
 import loader/headers
 import loader/loader
 import loader/request
-import local/select
 import monoucha/javascript
 import monoucha/jsregex
 import monoucha/jstypes
@@ -49,8 +48,8 @@ type
     setxsave: bool
 
   ContainerEventType* = enum
-    cetAnchor, cetNoAnchor, cetUpdate, cetReadLine, cetReadArea, cetReadFile,
-    cetOpen, cetSetLoadInfo, cetStatus, cetAlert, cetLoaded, cetTitle, cetCancel
+    cetAnchor, cetNoAnchor, cetReadLine, cetReadArea, cetReadFile, cetOpen,
+    cetSetLoadInfo, cetStatus, cetAlert, cetLoaded, cetTitle, cetCancel
 
   ContainerEvent* = object
     case t*: ContainerEventType
@@ -68,8 +67,6 @@ type
       anchor*: string
     of cetAlert:
       msg*: string
-    of cetUpdate:
-      force*: bool
     else: discard
 
   HighlightType = enum
@@ -163,10 +160,300 @@ type
     images*: seq[PosBitmap]
     cachedImages*: seq[CachedImage]
     luctx: LUContext
+    redraw*: bool
+
+  Select = ref object
+    container: Container
+    options: seq[string]
+    multiple: bool
+    oselected: seq[int] # old selection
+    selected: seq[int] # new selection
+    cursor: int # cursor distance from y
+    maxw: int # widest option
+    maxh: int # maximum height on screen (yes the naming is dumb)
+    si: int # first index to display
+    # location on screen
+    #TODO make this absolute
+    x: int
+    y: int
+    redraw*: bool
+    bpos: seq[int]
 
 jsDestructor(Highlight)
 jsDestructor(Container)
 
+# Forward declarations
+proc onclick(container: Container; res: ClickResult; save: bool)
+proc updateCursor(container: Container)
+proc cursorLastLine*(container: Container)
+proc triggerEvent(container: Container; t: ContainerEventType)
+
+proc queueDraw(select: Select) =
+  select.redraw = true
+
+proc windowChange(select: Select; height: int) =
+  select.maxh = height - 2
+  if select.y + select.options.len >= select.maxh:
+    select.y = height - select.options.len
+    if select.y < 0:
+      select.si = -select.y
+      select.y = 0
+  if select.selected.len > 0:
+    let i = select.selected[0]
+    if select.si > i:
+      select.si = i
+    elif select.si + select.maxh < i:
+      select.si = max(i - select.maxh, 0)
+  select.queueDraw()
+
+# index of option currently under cursor
+func hover(select: Select): int =
+  return select.cursor + select.si
+
+func dispheight(select: Select): int =
+  return select.maxh - select.y
+
+proc `hover=`(select: Select; i: int) =
+  let i = clamp(i, 0, select.options.high)
+  if i >= select.si + select.dispheight:
+    select.si = i - select.dispheight + 1
+    select.cursor = select.dispheight - 1
+  elif i < select.si:
+    select.si = i
+    select.cursor = 0
+  else:
+    select.cursor = i - select.si
+
+proc cursorDown(select: Select) =
+  if select.hover < select.options.high and
+      select.cursor + select.y < select.maxh - 1:
+    inc select.cursor
+    select.queueDraw()
+  elif select.si < select.options.len - select.maxh:
+    inc select.si
+    select.queueDraw()
+
+proc cursorUp(select: Select) =
+  if select.cursor > 0:
+    dec select.cursor
+    select.queueDraw()
+  elif select.si > 0:
+    dec select.si
+    select.queueDraw()
+  elif select.multiple and select.cursor > -1:
+    select.cursor = -1
+
+proc close(select: Select) =
+  let container = select.container
+  container.select = nil
+
+proc cancel(select: Select) =
+  let container = select.container
+  container.iface.select(select.oselected).then(proc(res: ClickResult) =
+    container.onclick(res, save = false))
+  select.close()
+
+proc submit(select: Select) =
+  let container = select.container
+  container.iface.select(select.selected).then(proc(res: ClickResult) =
+    container.onclick(res, save = false))
+  select.close()
+
+proc click(select: Select) =
+  if not select.multiple:
+    select.selected = @[select.hover]
+    select.submit()
+  elif select.cursor == -1:
+    select.submit()
+  else:
+    var k = select.selected.len
+    let i = select.hover
+    for j in 0 ..< select.selected.len:
+      if select.selected[j] >= i:
+        k = j
+        break
+    if k < select.selected.len and select.selected[k] == i:
+      select.selected.delete(k)
+    else:
+      select.selected.insert(i, k)
+    select.queueDraw()
+
+proc cursorLeft(select: Select) =
+  select.submit()
+
+proc cursorRight(select: Select) =
+  select.click()
+
+proc getCursorX*(select: Select): int =
+  if select.cursor == -1:
+    return select.x
+  return select.x + 1
+
+proc getCursorY*(select: Select): int =
+  return select.y + 1 + select.cursor
+
+proc cursorFirstLine(select: Select) =
+  if select.cursor != 0 or select.si != 0:
+    select.cursor = 0
+    select.si = 0
+    select.queueDraw()
+
+proc cursorLastLine(select: Select) =
+  if select.hover < select.options.len:
+    select.cursor = select.dispheight - 1
+    select.si = max(select.options.len - select.maxh, 0)
+    select.queueDraw()
+
+proc cursorNextMatch(select: Select; regex: Regex; wrap: bool) =
+  var j = -1
+  for i in select.hover + 1 ..< select.options.len:
+    if regex.exec(select.options[i]).success:
+      j = i
+      break
+  if j != -1:
+    select.hover = j
+    select.queueDraw()
+  elif wrap:
+    for i in 0 ..< select.hover:
+      if regex.exec(select.options[i]).success:
+        j = i
+        break
+    if j != -1:
+      select.hover = j
+      select.queueDraw()
+
+proc cursorPrevMatch(select: Select; regex: Regex; wrap: bool) =
+  var j = -1
+  for i in countdown(select.hover - 1, 0):
+    if regex.exec(select.options[i]).success:
+      j = i
+      break
+  if j != -1:
+    select.hover = j
+    select.queueDraw()
+  elif wrap:
+    for i in countdown(select.options.high, select.hover):
+      if regex.exec(select.options[i]).success:
+        j = i
+        break
+    if j != -1:
+      select.hover = j
+      select.queueDraw()
+
+proc pushCursorPos(select: Select) =
+  select.bpos.add(select.hover)
+
+proc popCursorPos(select: Select; nojump = false) =
+  select.hover = select.bpos.pop()
+  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)
+
+proc drawBorders(display: var FixedGrid; sx, ex, sy, ey: int;
+    upmore, downmore: bool) =
+  for y in sy .. ey:
+    var x = 0
+    while x < sx:
+      if display[y * display.width + x].str == "":
+        display[y * display.width + x].str = " "
+        inc x
+      else:
+        #x = display[y * display.width + x].str.twidth(x)
+        inc x
+  # Draw corners.
+  let tl = if upmore: VerticalBar else: CornerTopLeft
+  let tr = if upmore: VerticalBar else: CornerTopRight
+  let bl = if downmore: VerticalBar else: CornerBottomLeft
+  let br = if downmore: VerticalBar else: CornerBottomRight
+  const fmt = Format()
+  display[sy * display.width + sx].str = tl
+  display[sy * display.width + ex].str = tr
+  display[ey * display.width + sx].str = bl
+  display[ey * display.width + ex].str = br
+  display[sy * display.width + sx].format = fmt
+  display[sy * display.width + ex].format = fmt
+  display[ey * display.width + sx].format = fmt
+  display[ey * display.width + ex].format = fmt
+  # Draw top, bottom borders.
+  let ups = if upmore: " " else: HorizontalBar
+  let downs = if downmore: " " else: HorizontalBar
+  for x in sx + 1 .. ex - 1:
+    display[sy * display.width + x].str = ups
+    display[ey * display.width + x].str = downs
+    display[sy * display.width + x].format = fmt
+    display[ey * display.width + x].format = fmt
+  if upmore:
+    display[sy * display.width + sx + (ex - sx) div 2].str = ":"
+  if downmore:
+    display[ey * display.width + sx + (ex - sx) div 2].str = ":"
+  # Draw left, right borders.
+  for y in sy + 1 .. ey - 1:
+    display[y * display.width + sx].str = VerticalBar
+    display[y * display.width + ex].str = VerticalBar
+    display[y * display.width + sx].format = fmt
+    display[y * display.width + ex].format = fmt
+
+proc drawSelect*(select: Select; display: var FixedGrid) =
+  if display.width < 2 or display.height < 2:
+    return # border does not fit...
+  # Max width, height with one row/column on the sides.
+  let mw = display.width - 2
+  let mh = display.height - 2
+  var sy = select.y
+  let si = select.si
+  var ey = min(sy + select.options.len, mh) + 1
+  var sx = select.x
+  if sx + select.maxw >= mw:
+    sx = display.width - select.maxw
+    if sx < 0:
+      # This means the widest option is wider than the available screen.
+      # w3m simply cuts off the part that doesn't fit, and we do that too,
+      # but I feel like this may not be the best solution.
+      sx = 0
+  var ex = min(sx + select.maxw, mw) + 1
+  let upmore = select.si > 0
+  let downmore = select.si + mh < select.options.len
+  drawBorders(display, sx, ex, sy, ey, upmore, downmore)
+  if select.multiple and not upmore:
+    display[sy * display.width + sx].str = "X"
+  # 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:
+    inc k
+  for y in sy ..< ey:
+    let i = y - sy + si
+    var j = 0
+    var x = sx
+    let dls = y * display.width
+    if k < select.selected.len and select.selected[k] == i:
+      format.flags.incl(ffReverse)
+      inc k
+    else:
+      format.flags.excl(ffReverse)
+    while j < select.options[i].len:
+      fastRuneAt(select.options[i], j, r)
+      let rw = r.twidth(x)
+      let ox = x
+      x += rw
+      if x > ex:
+        break
+      display[dls + ox].str = $r
+      display[dls + ox].format = format
+    while x < ex:
+      display[dls + x].str = " "
+      display[dls + x].format = format
+      inc x
+
 proc newContainer*(config: BufferConfig; loaderConfig: LoaderClientConfig;
     url: URL; request: Request; luctx: LUContext; attrs: WindowAttributes;
     title: string; redirectDepth: int; flags: set[ContainerFlag];
@@ -190,7 +477,8 @@ proc newContainer*(config: BufferConfig; loaderConfig: LoaderClientConfig;
     process: -1,
     mainConfig: mainConfig,
     flags: flags,
-    luctx: luctx
+    luctx: luctx,
+    redraw: true
   )
 
 func location(container: Container): URL {.jsfget.} =
@@ -380,7 +668,10 @@ func startx(hl: Highlight): int =
     hl.x2
   else:
     min(hl.x1, hl.x2)
-func starty(hl: Highlight): int = min(hl.y1, hl.y2)
+
+func starty(hl: Highlight): int =
+  return min(hl.y1, hl.y2)
+
 func endx(hl: Highlight): int =
   if hl.y1 > hl.y2:
     hl.x1
@@ -388,7 +679,9 @@ func endx(hl: Highlight): int =
     hl.x2
   else:
     max(hl.x1, hl.x2)
-func endy(hl: Highlight): int = max(hl.y1, hl.y2)
+
+func endy(hl: Highlight): int =
+  return max(hl.y1, hl.y2)
 
 func colorNormal(container: Container; hl: Highlight; y: int;
     limitx: Slice[int]): Slice[int] =
@@ -448,8 +741,6 @@ proc triggerEvent(container: Container; event: ContainerEvent) =
 proc triggerEvent(container: Container; t: ContainerEventType) =
   container.triggerEvent(ContainerEvent(t: t))
 
-proc updateCursor(container: Container)
-
 proc setNumLines(container: Container; lines: int; finish = false) =
   if container.numLines != lines:
     container.numLines = lines
@@ -458,7 +749,8 @@ proc setNumLines(container: Container; lines: int; finish = false) =
       container.startpos = none(CursorPosition)
     container.updateCursor()
 
-proc cursorLastLine*(container: Container)
+proc queueDraw*(container: Container) =
+  container.redraw = true
 
 proc requestLines(container: Container): EmptyPromise {.discardable.} =
   if container.iface == nil:
@@ -483,13 +775,10 @@ proc requestLines(container: Container): EmptyPromise {.discardable.} =
         container.cursorLastLine()
     let cw = container.fromy ..< container.fromy + container.height
     if w.a in cw or w.b in cw or cw.a in w or cw.b in w or isBgNew:
-      container.triggerEvent(cetUpdate)
+      container.queueDraw()
     container.images = res.images
   )
 
-proc redraw(container: Container) {.jsfunc.} =
-  container.triggerEvent(ContainerEvent(t: cetUpdate, force: true))
-
 proc sendCursorPosition*(container: Container) =
   if container.iface == nil:
     return
@@ -508,7 +797,7 @@ 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(cetUpdate)
+    container.queueDraw()
 
 proc setFromX(container: Container; x: int; refresh = true) {.jsfunc.} =
   if container.pos.fromx != x:
@@ -518,7 +807,7 @@ proc setFromX(container: Container; x: int; refresh = true) {.jsfunc.} =
         container.currentLineWidth())
       if refresh:
         container.sendCursorPosition()
-    container.triggerEvent(cetUpdate)
+    container.queueDraw()
 
 proc setFromXY(container: Container; x, y: int) {.jsfunc.} =
   container.setFromY(y)
@@ -564,7 +853,7 @@ proc setCursorX(container: Container; x: int; refresh = true; save = true)
   if container.cursorx == x and container.currentSelection != nil and
       container.currentSelection.x2 != x:
     container.currentSelection.x2 = x
-    container.triggerEvent(cetUpdate)
+    container.queueDraw()
   if refresh:
     container.sendCursorPosition()
   if save:
@@ -586,7 +875,7 @@ proc setCursorY(container: Container; y: int; refresh = true) {.jsfunc.} =
       container.setFromY(y)
     container.pos.cursory = y
   if container.currentSelection != nil and container.currentSelection.y2 != y:
-    container.triggerEvent(cetUpdate)
+    container.queueDraw()
     container.currentSelection.y2 = y
   container.restoreCursorX()
   if refresh:
@@ -679,25 +968,25 @@ proc setCursorXYCenter(container: Container; x, y: int; refresh = true)
     container.centerColumn()
 
 proc cursorDown(container: Container; n = 1) {.jsfunc.} =
-  if container.select.open:
+  if container.select != nil:
     container.select.cursorDown()
   else:
     container.setCursorY(container.cursory + n)
 
 proc cursorUp(container: Container; n = 1) {.jsfunc.} =
-  if container.select.open:
+  if container.select != nil:
     container.select.cursorUp()
   else:
     container.setCursorY(container.cursory - n)
 
 proc cursorLeft(container: Container; n = 1) {.jsfunc.} =
-  if container.select.open:
+  if container.select != nil:
     container.select.cursorLeft()
   else:
     container.setCursorX(container.cursorFirstX() - n)
 
 proc cursorRight(container: Container; n = 1) {.jsfunc.} =
-  if container.select.open:
+  if container.select != nil:
     container.select.cursorRight()
   else:
     container.setCursorX(container.cursorLastX() + n)
@@ -947,7 +1236,7 @@ proc markPos*(container: Container) =
     container.jumpMark = pos
 
 proc cursorFirstLine(container: Container) {.jsfunc.} =
-  if container.select.open:
+  if container.select != nil:
     container.select.cursorFirstLine()
   else:
     container.markPos0()
@@ -955,7 +1244,7 @@ proc cursorFirstLine(container: Container) {.jsfunc.} =
     container.markPos()
 
 proc cursorLastLine*(container: Container) {.jsfunc.} =
-  if container.select.open:
+  if container.select != nil:
     container.select.cursorLastLine()
   else:
     container.markPos0()
@@ -1041,7 +1330,7 @@ proc updateCursor(container: Container) =
 proc gotoLine*[T: string|int](container: Container; s: T) =
   when s is string:
     if s == "":
-      redraw(container)
+      container.redraw = true
     elif s[0] == '^':
       container.cursorFirstLine()
     elif s[0] == '$':
@@ -1060,13 +1349,13 @@ proc gotoLine*[T: string|int](container: Container; s: T) =
     container.markPos()
 
 proc pushCursorPos*(container: Container) =
-  if container.select.open:
+  if container.select != nil:
     container.select.pushCursorPos()
   else:
     container.bpos.add(container.pos)
 
 proc popCursorPos*(container: Container; nojump = false) =
-  if container.select.open:
+  if container.select != nil:
     container.select.popCursorPos(nojump)
   else:
     container.pos = container.bpos.pop()
@@ -1131,17 +1420,17 @@ proc setMark*(container: Container; id: string; x = none(int);
   let y = y.get(container.cursory)
   container.marks.withValue(id, p):
     p[] = (x, y)
-    container.triggerEvent(cetUpdate)
+    container.queueDraw()
     return false
   do:
     container.marks[id] = (x, y)
-    container.triggerEvent(cetUpdate)
+    container.queueDraw()
     return true
 
 proc clearMark*(container: Container; id: string): bool {.jsfunc.} =
   result = id in container.marks
   container.marks.del(id)
-  container.triggerEvent(cetUpdate)
+  container.queueDraw()
 
 proc getMarkPos(container: Container; id: string): Opt[PagePos] {.jsfunc.} =
   if id == "`" or id == "'":
@@ -1237,18 +1526,18 @@ proc onMatch(container: Container; res: BufferMatch; refresh: bool) =
         y2: res.y
       )
       container.highlights.add(hl)
-      container.triggerEvent(cetUpdate)
+      container.queueDraw()
       container.hlon = false
       container.needslines = true
   elif container.hlon:
     container.clearSearchHighlights()
-    container.triggerEvent(cetUpdate)
+    container.queueDraw()
     container.needslines = true
     container.hlon = false
 
 proc cursorNextMatch*(container: Container; regex: Regex; wrap, refresh: bool;
     n: int): EmptyPromise {.discardable.} =
-  if container.select.open:
+  if container.select != nil:
     #TODO
     for _ in 0 ..< n:
       container.select.cursorNextMatch(regex, wrap)
@@ -1263,7 +1552,7 @@ proc cursorNextMatch*(container: Container; regex: Regex; wrap, refresh: bool;
 
 proc cursorPrevMatch*(container: Container; regex: Regex; wrap, refresh: bool;
     n: int): EmptyPromise {.discardable.} =
-  if container.select.open:
+  if container.select != nil:
     #TODO
     for _ in 0 ..< n:
       container.select.cursorPrevMatch(regex, wrap)
@@ -1304,7 +1593,7 @@ proc cursorToggleSelection(container: Container; n = 1;
     )
     container.highlights.add(hl)
     container.currentSelection = hl
-  container.triggerEvent(cetUpdate)
+  container.queueDraw()
   return container.currentSelection
 
 #TODO I don't like this API
@@ -1481,7 +1770,7 @@ proc remoteCancel*(container: Container) =
   container.alert("Canceled loading")
 
 proc cancel*(container: Container) {.jsfunc.} =
-  if container.select.open:
+  if container.select != nil:
     container.select.cancel()
   elif container.loadState == lsLoading:
     container.loadState = lsCanceled
@@ -1522,15 +1811,21 @@ proc reshape(container: Container): EmptyPromise {.jsfunc.} =
     return container.requestLines()
   )
 
-proc onclick(container: Container; res: ClickResult; save: bool)
-
 proc displaySelect(container: Container; selectResult: SelectResult) =
-  let submitSelect = proc(selected: seq[int]) =
-    container.iface.select(selected).then(proc(res: ClickResult) =
-      container.onclick(res, save = false))
-  container.select.initSelect(selectResult, container.acursorx,
-    container.acursory, container.height, submitSelect)
-  container.triggerEvent(cetUpdate)
+  container.select = Select(
+    container: container,
+    multiple: selectResult.multiple,
+    options: selectResult.options,
+    oselected: selectResult.selected,
+    selected: selectResult.selected,
+    x: container.acursorx,
+    y: container.acursory
+  )
+  for opt in container.select.options.mitems:
+    opt.mnormalize()
+    container.select.maxw = max(container.select.maxw, opt.width())
+  container.select.windowChange(container.height)
+  container.queueDraw()
 
 proc onclick(container: Container; res: ClickResult; save: bool) =
   if res.repaint:
@@ -1547,7 +1842,7 @@ proc onclick(container: Container; res: ClickResult; save: bool) =
     container.onReadLine(res.readline.get)
 
 proc click*(container: Container) {.jsfunc.} =
-  if container.select.open:
+  if container.select != nil:
     container.select.click()
   else:
     if container.iface == nil:
@@ -1663,8 +1958,7 @@ proc readLines*(container: Container; handle: proc(line: SimpleFlexibleLine)) =
       # fulfill all promises
       container.handleCommand()
 
-proc drawLines*(container: Container; display: var FixedGrid;
-    hlcolor: CellColor) =
+proc drawLines*(container: Container; display: var FixedGrid; hlcolor: CellColor) =
   let bgcolor = container.bgcolor
   template set_fmt(cell, cf: typed) =
     if cf.pos != -1:
@@ -1722,12 +2016,12 @@ proc drawLines*(container: Container; display: var FixedGrid;
         inc k
     # Finally, override cell formatting for highlighted cells.
     let hls = container.findHighlights(container.fromy + by)
-    let aw = container.width - (startw - container.fromx) # actual width
+    let aw = display.width - (startw - container.fromx) # actual width
     for hl in hls:
       let area = container.colorArea(hl, container.fromy + by,
         startw .. startw + aw)
       for i in area:
-        if i - startw >= container.width:
+        if i - startw >= display.width:
           break
         var hlformat = display[dls + i - startw].format
         hlformat.bgcolor = hlcolor
diff --git a/src/local/lineedit.nim b/src/local/lineedit.nim
index ba02e2ae..15cbdc54 100644
--- a/src/local/lineedit.nim
+++ b/src/local/lineedit.nim
@@ -38,7 +38,7 @@ type
     hist: LineHistory
     histindex: int
     histtmp: string
-    invalid*: bool
+    redraw*: bool
 
 jsDestructor(LineEdit)
 
@@ -135,7 +135,7 @@ proc insertCharseq(edit: LineEdit; s: string) =
   edit.news &= rem
   edit.cursori += s.len
   edit.cursorx += s.notwidth()
-  edit.invalid = true
+  edit.redraw = true
 
 proc cancel(edit: LineEdit) {.jsfunc.} =
   edit.state = lesCancel
@@ -151,7 +151,7 @@ proc backspace(edit: LineEdit) {.jsfunc.} =
     edit.news.delete(edit.cursori - len .. edit.cursori - 1)
     edit.cursori -= len
     edit.cursorx -= r.width()
-    edit.invalid = true
+    edit.redraw = true
  
 proc write*(edit: LineEdit; s: string; cs: Charset): bool =
   if cs == CHARSET_UTF_8:
@@ -177,7 +177,7 @@ proc delete(edit: LineEdit) {.jsfunc.} =
   if edit.cursori < edit.news.len:
     let len = edit.news.runeLenAt(edit.cursori)
     edit.news.delete(edit.cursori ..< edit.cursori + len)
-    edit.invalid = true
+    edit.redraw = true
 
 proc escape(edit: LineEdit) {.jsfunc.} =
   edit.escNext = true
@@ -187,12 +187,12 @@ proc clear(edit: LineEdit) {.jsfunc.} =
     edit.news.delete(0..edit.cursori - 1)
     edit.cursori = 0
     edit.cursorx = 0
-    edit.invalid = true
+    edit.redraw = true
 
 proc kill(edit: LineEdit) {.jsfunc.} =
   if edit.cursori < edit.news.len:
     edit.news.setLen(edit.cursori)
-    edit.invalid = true
+    edit.redraw = true
 
 proc backward(edit: LineEdit) {.jsfunc.} =
   if edit.cursori > 0:
@@ -200,7 +200,7 @@ proc backward(edit: LineEdit) {.jsfunc.} =
     edit.cursori -= len
     edit.cursorx -= r.width()
     if edit.cursorx < edit.shiftx:
-      edit.invalid = true
+      edit.redraw = true
 
 proc forward(edit: LineEdit) {.jsfunc.} =
   if edit.cursori < edit.news.len:
@@ -208,7 +208,7 @@ proc forward(edit: LineEdit) {.jsfunc.} =
     fastRuneAt(edit.news, edit.cursori, r)
     edit.cursorx += r.width()
     if edit.cursorx >= edit.shiftx + edit.maxwidth:
-      edit.invalid = true
+      edit.redraw = true
 
 proc prevWord(edit: LineEdit) {.jsfunc.} =
   if edit.cursori == 0:
@@ -225,7 +225,7 @@ proc prevWord(edit: LineEdit) {.jsfunc.} =
     edit.cursori -= len
     edit.cursorx -= r.width()
   if edit.cursorx < edit.shiftx:
-    edit.invalid = true
+    edit.redraw = true
 
 proc nextWord(edit: LineEdit) {.jsfunc.} =
   if edit.cursori >= edit.news.len:
@@ -246,14 +246,14 @@ proc nextWord(edit: LineEdit) {.jsfunc.} =
       break
     edit.cursorx += r.width()
   if edit.cursorx >= edit.shiftx + edit.maxwidth:
-    edit.invalid = true
+    edit.redraw = true
 
 proc clearWord(edit: LineEdit) {.jsfunc.} =
   let oc = edit.cursori
   edit.prevWord()
   if oc != edit.cursori:
     edit.news.delete(edit.cursori .. oc - 1)
-    edit.invalid = true
+    edit.redraw = true
 
 proc killWord(edit: LineEdit) {.jsfunc.} =
   if edit.cursori >= edit.news.len:
@@ -269,20 +269,20 @@ proc killWord(edit: LineEdit) {.jsfunc.} =
       edit.news.delete(oc ..< edit.cursori)
     edit.cursori = oc
     edit.cursorx = ox
-    edit.invalid = true
+    edit.redraw = true
 
 proc begin(edit: LineEdit) {.jsfunc.} =
   edit.cursori = 0
   edit.cursorx = 0
   if edit.shiftx > 0:
-    edit.invalid = true
+    edit.redraw = true
 
 proc `end`(edit: LineEdit) {.jsfunc.} =
   if edit.cursori < edit.news.len:
     edit.cursori = edit.news.len
     edit.cursorx = edit.news.notwidth()
     if edit.cursorx >= edit.shiftx + edit.maxwidth:
-      edit.invalid = true
+      edit.redraw = true
 
 proc prevHist(edit: LineEdit) {.jsfunc.} =
   if edit.histindex > 0:
@@ -294,7 +294,7 @@ proc prevHist(edit: LineEdit) {.jsfunc.} =
     # the string.
     edit.begin()
     edit.end()
-    edit.invalid = true
+    edit.redraw = true
 
 proc nextHist(edit: LineEdit) {.jsfunc.} =
   if edit.histindex + 1 < edit.hist.lines.len:
@@ -302,7 +302,7 @@ proc nextHist(edit: LineEdit) {.jsfunc.} =
     edit.news = edit.hist.lines[edit.histindex]
     edit.begin()
     edit.end()
-    edit.invalid = true
+    edit.redraw = true
   elif edit.histindex < edit.hist.lines.len:
     inc edit.histindex
     edit.news = edit.histtmp
@@ -322,7 +322,7 @@ proc readLine*(prompt, current: string; termwidth: int; disallowed: set[char];
     news: current,
     disallowed: disallowed,
     hide: hide,
-    invalid: true,
+    redraw: true,
     cursori: current.len,
     cursorx: current.notwidth(),
     # - 1, so that the cursor always has place
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 073b941c..411ef1b1 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -26,7 +26,6 @@ import loader/loader
 import loader/request
 import local/container
 import local/lineedit
-import local/select
 import local/term
 import monoucha/fromjs
 import monoucha/javascript
@@ -109,6 +108,10 @@ type
     ndFirstChild
     ndAny = "any"
 
+  Surface = object
+    redraw: bool
+    grid: FixedGrid
+
   Pager* = ref object
     alertState: PagerAlertState
     alerts*: seq[string]
@@ -122,7 +125,7 @@ type
     container*: Container
     cookiejars: Table[string, CookieJar]
     devRandom: PosixStream
-    display: FixedGrid
+    display: Surface
     forkserver*: ForkServer
     formRequestMap*: Table[string, FormRequestType]
     hasload*: bool # has a page been successfully loaded since startup?
@@ -132,7 +135,7 @@ type
     isearchpromise: EmptyPromise
     jsctx: JSContext
     lineData: LineData
-    lineedit*: Option[LineEdit]
+    lineedit*: LineEdit
     linehist: array[LineMode, LineHistory]
     linemode: LineMode
     loader*: FileLoader
@@ -142,12 +145,11 @@ type
     numload*: int # number of pages currently being loaded
     precnum*: int32 # current number prefix (when vi-numeric-prefix is true)
     procmap*: seq[ProcMapItem]
-    redraw: bool
     regex: Opt[Regex]
     reverseSearch: bool
     scommand*: string
     selector*: Selector[int]
-    statusgrid*: FixedGrid
+    status: Surface
     term*: Terminal
     unreg*: seq[Container]
 
@@ -164,9 +166,16 @@ func loaderPid(pager: Pager): int64 {.jsfget.} =
 
 func getRoot(container: Container): Container =
   var c = container
-  while c.parent != nil: c = c.parent
+  while c.parent != nil:
+    c = c.parent
   return c
 
+func bufWidth(pager: Pager): int =
+  return pager.attrs.width
+
+func bufHeight(pager: Pager): int =
+  return pager.attrs.height - 1
+
 # depth-first descendant iterator
 iterator descendants(parent: Container): Container {.inline.} =
   var stack = newSeqOfCap[Container](parent.children.len)
@@ -186,10 +195,22 @@ iterator containers*(pager: Pager): Container {.inline.} =
     for c in root.descendants:
       yield c
 
+proc clearDisplay(pager: Pager) =
+  pager.display = Surface(
+    grid: newFixedGrid(pager.bufWidth, pager.bufHeight),
+    redraw: true
+  )
+
+proc clearStatus(pager: Pager) =
+  pager.status = Surface(
+    grid: newFixedGrid(pager.attrs.width),
+    redraw: true
+  )
+
 proc setContainer*(pager: Pager; c: Container) {.jsfunc.} =
   pager.container = c
-  pager.redraw = true
   if c != nil:
+    c.queueDraw()
     pager.term.setTitle(c.getTitle())
 
 proc hasprop(ctx: JSContext; pager: Pager; s: string): bool {.jshasprop.} =
@@ -259,13 +280,12 @@ proc setLineEdit(pager: Pager; mode: LineMode; current = ""; hide = false;
   let hist = pager.getLineHist(mode)
   if pager.term.isatty() and pager.config.input.use_mouse:
     pager.term.disableMouse()
-  let edit = readLine($mode & extraPrompt, current, pager.attrs.width, {}, hide,
-    hist)
-  pager.lineedit = some(edit)
+  pager.lineedit = readLine($mode & extraPrompt, current, pager.attrs.width,
+    {}, hide, hist)
   pager.linemode = mode
 
 proc clearLineEdit(pager: Pager) =
-  pager.lineedit = none(LineEdit)
+  pager.lineedit = nil
   if pager.term.isatty() and pager.config.input.use_mouse:
     pager.term.enableMouse()
 
@@ -342,52 +362,43 @@ proc launchPager*(pager: Pager; istream: PosixStream; selector: Selector[int]) =
   of tsrSuccess: discard
   of tsrDA1Fail:
     pager.alert("Failed to query DA1, please set display.query-da1 = false")
-  pager.display = newFixedGrid(pager.attrs.width, pager.attrs.height - 1)
-  pager.statusgrid = newFixedGrid(pager.attrs.width)
-
-proc clearDisplay(pager: Pager) =
-  pager.display = newFixedGrid(pager.display.width, pager.display.height)
-
-proc buffer(pager: Pager): Container {.jsfget, inline.} = pager.container
-
-proc refreshDisplay(pager: Pager; container = pager.container) =
   pager.clearDisplay()
-  let hlcolor = cellColor(pager.config.display.highlight_color)
-  container.drawLines(pager.display, hlcolor)
-  if pager.config.display.highlight_marks:
-    container.highlightMarks(pager.display, hlcolor)
+  pager.clearStatus()
+
+proc buffer(pager: Pager): Container {.jsfget, inline.} =
+  return pager.container
 
 # Note: this function does not work correctly if start < i of last written char
 proc writeStatusMessage(pager: Pager; str: string; format = Format();
     start = 0; maxwidth = -1; clip = '$'): int {.discardable.} =
   var maxwidth = maxwidth
   if maxwidth == -1:
-    maxwidth = pager.statusgrid.len
+    maxwidth = pager.status.grid.len
   var i = start
-  let e = min(start + maxwidth, pager.statusgrid.width)
+  let e = min(start + maxwidth, pager.status.grid.width)
   if i >= e:
     return i
-  pager.redraw = true
+  pager.status.redraw = true
   for r in str.runes:
     let w = r.width()
     if i + w >= e:
-      pager.statusgrid[i].format = format
-      pager.statusgrid[i].str = $clip
+      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():
-      pager.statusgrid[i].str = "^"
-      pager.statusgrid[i + 1].str = $getControlLetter(char(r))
-      pager.statusgrid[i + 1].format = format
+      pager.status.grid[i].str = "^"
+      pager.status.grid[i + 1].str = $getControlLetter(char(r))
+      pager.status.grid[i + 1].format = format
     else:
-      pager.statusgrid[i].str = $r
-    pager.statusgrid[i].format = format
+      pager.status.grid[i].str = $r
+    pager.status.grid[i].format = format
     i += w
   result = i
   var def = Format()
   while i < e:
-    pager.statusgrid[i].str = ""
-    pager.statusgrid[i].format = def
+    pager.status.grid[i].str = ""
+    pager.status.grid[i].format = def
     inc i
 
 # Note: should only be called directly after user interaction.
@@ -416,7 +427,7 @@ proc refreshStatusMsg*(pager: Pager) =
       pager.writeStatusMessage(title, format, mw)
     else:
       let hover2 = " " & hover
-      let maxwidth = pager.statusgrid.width - hover2.width() - mw
+      let maxwidth = pager.status.grid.width - hover2.width() - mw
       let tw = pager.writeStatusMessage(title, format, mw, maxwidth, '>')
       pager.writeStatusMessage(hover2, format, tw)
 
@@ -455,8 +466,13 @@ proc drawBuffer*(pager: Pager; container: Container; ofile: File) =
   ofile.flushFile()
 
 proc redraw(pager: Pager) {.jsfunc.} =
-  pager.redraw = true
   pager.term.clearCanvas()
+  pager.display.redraw = true
+  pager.status.redraw = true
+  if pager.container != nil:
+    pager.container.redraw = true
+    if pager.container.select != nil:
+      pager.container.select.redraw = true
 
 proc loadCachedImage(pager: Pager; container: Container; bmp: NetworkBitmap) =
   #TODO this is kinda dumb, because we cannot unload cached images.
@@ -478,7 +494,7 @@ proc loadCachedImage(pager: Pager; container: Container; bmp: NetworkBitmap) =
       return nil
     return res.get.saveToBitmap(bmp)
   ).then(proc() =
-    pager.redraw = true
+    container.redraw = true
     cachedImage.loaded = true
     pager.loader.removeCachedItem(cacheId)
   )
@@ -504,65 +520,72 @@ proc initImages(pager: Pager; container: Container) =
       imageId = pager.imageId
       inc pager.imageId
     let canvasImage = pager.term.loadImage(image.bmp, container.process, imageId,
-      image.x - container.fromx, image.y - container.fromy, pager.attrs.width,
-      pager.attrs.height - 1)
+      image.x - container.fromx, image.y - container.fromy, pager.bufWidth,
+      pager.bufHeight)
     if canvasImage != nil:
       newImages.add(canvasImage)
-      canvasImage.marked = true
-  if pager.term.imageMode == imKitty:
-    for image in pager.term.canvasImages:
-      if not image.marked:
-        pager.term.imagesToClear.add(image)
-      image.marked = false
+  pager.term.clearImages(pager.bufHeight)
   pager.term.canvasImages = newImages
 
 proc draw*(pager: Pager) =
+  var redraw = false
   let container = pager.container
   if container != nil:
-    if pager.redraw:
-      pager.refreshDisplay()
-      pager.term.writeGrid(pager.display)
-    if container.select.open and container.select.redraw:
-      container.select.drawSelect(pager.display)
-      pager.term.writeGrid(pager.display)
+    if container.redraw:
+      pager.clearDisplay()
+      let hlcolor = cellColor(pager.config.display.highlight_color)
+      container.drawLines(pager.display.grid, hlcolor)
+      if pager.config.display.highlight_marks:
+        container.highlightMarks(pager.display.grid, hlcolor)
+      container.redraw = false
+      pager.display.redraw = true
+    if (let select = container.select; select != nil and select.redraw):
+      select.drawSelect(pager.display.grid)
+      select.redraw = false
+      pager.display.redraw = true
+  if pager.display.redraw:
+    pager.term.writeGrid(pager.display.grid)
+    pager.display.redraw = false
+    redraw = true
   if pager.askpromise != nil or pager.askcharpromise != nil:
-    discard
-  elif pager.lineedit.isSome:
-    if pager.lineedit.get.invalid:
-      let x = pager.lineedit.get.generateOutput()
+    pager.term.writeGrid(pager.status.grid, 0, pager.attrs.height - 1)
+    pager.status.redraw = false
+    redraw = true
+  elif pager.lineedit != nil:
+    if pager.lineedit.redraw:
+      let x = pager.lineedit.generateOutput()
       pager.term.writeGrid(x, 0, pager.attrs.height - 1)
-      pager.lineedit.get.invalid = false
-      pager.redraw = true
-  else:
-    pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1)
-  if pager.redraw:
-    if container != nil and pager.term.imageMode != imNone:
-      pager.initImages(container)
+      pager.lineedit.redraw = false
+      redraw = true
+  elif pager.status.redraw:
+    pager.term.writeGrid(pager.status.grid, 0, pager.attrs.height - 1)
+    pager.status.redraw = false
+    redraw = true
+  if container != nil and pager.term.imageMode != imNone:
+    # init images only after term canvas has been finalized
+    pager.initImages(container)
+  if redraw:
     pager.term.hideCursor()
     pager.term.outputGrid()
     if pager.term.imageMode != imNone:
       pager.term.outputImages()
   if pager.askpromise != nil:
     pager.term.setCursor(pager.askcursor, pager.attrs.height - 1)
-  elif pager.lineedit.isSome:
-    pager.term.setCursor(pager.lineedit.get.getCursorX(),
-      pager.attrs.height - 1)
+  elif pager.lineedit != nil:
+    pager.term.setCursor(pager.lineedit.getCursorX(), pager.attrs.height - 1)
   elif container != nil:
-    if container.select.open:
-      pager.term.setCursor(container.select.getCursorX(),
-        container.select.getCursorY())
+    if (let select = container.select; select != nil):
+      pager.term.setCursor(select.getCursorX(), select.getCursorY())
     else:
-      pager.term.setCursor(pager.container.acursorx, pager.container.acursory)
-  if pager.redraw:
+      pager.term.setCursor(container.acursorx, container.acursory)
+  if redraw:
     pager.term.showCursor()
   pager.term.flush()
-  pager.redraw = false
 
 proc writeAskPrompt(pager: Pager; s = "") =
-  let maxwidth = pager.statusgrid.width - s.len
+  let maxwidth = pager.status.grid.width - s.len
   let i = pager.writeStatusMessage(pager.askprompt, maxwidth = maxwidth)
   pager.askcursor = pager.writeStatusMessage(s, start = i)
-  pager.term.writeGrid(pager.statusgrid, 0, pager.attrs.height - 1)
 
 proc ask(pager: Pager; prompt: string): Promise[bool] {.jsfunc.} =
   pager.askprompt = prompt
@@ -1010,10 +1033,10 @@ proc windowChange*(pager: Pager) =
   if pager.attrs == oldAttrs:
     #TODO maybe it's more efficient to let false positives through?
     return
-  if pager.lineedit.isSome:
-    pager.lineedit.get.windowChange(pager.attrs)
-  pager.display = newFixedGrid(pager.attrs.width, pager.attrs.height - 1)
-  pager.statusgrid = newFixedGrid(pager.attrs.width)
+  if pager.lineedit != nil:
+    pager.lineedit.windowChange(pager.attrs)
+  pager.clearDisplay()
+  pager.clearStatus()
   for container in pager.containers:
     container.windowChange(pager.attrs)
   if pager.askprompt != "":
@@ -1241,14 +1264,14 @@ proc compileSearchRegex(pager: Pager; s: string): Result[Regex, string] =
   return compileSearchRegex(s, flags)
 
 proc updateReadLineISearch(pager: Pager; linemode: LineMode) =
-  let lineedit = pager.lineedit.get
+  let lineedit = pager.lineedit
   pager.isearchpromise = pager.isearchpromise.then(proc(): EmptyPromise =
     case lineedit.state
     of lesCancel:
       pager.iregex.err()
       pager.container.popCursorPos()
       pager.container.clearSearchHighlights()
-      pager.redraw = true
+      pager.container.redraw = true
       pager.isearchpromise = nil
     of lesEdit:
       if lineedit.news != "":
@@ -1271,7 +1294,7 @@ proc updateReadLineISearch(pager: Pager; linemode: LineMode) =
       pager.container.markPos()
       pager.container.clearSearchHighlights()
       pager.container.sendCursorPosition()
-      pager.redraw = true
+      pager.container.redraw = true
       pager.isearchpromise = nil
   )
 
@@ -1292,7 +1315,7 @@ proc saveTo(pager: Pager; data: LineDataDownload; path: string) =
     )
 
 proc updateReadLine*(pager: Pager) =
-  let lineedit = pager.lineedit.get
+  let lineedit = pager.lineedit
   if pager.linemode in {lmISearchF, lmISearchB}:
     pager.updateReadLineISearch(pager.linemode)
   else:
@@ -1359,8 +1382,7 @@ proc updateReadLine*(pager: Pager) =
         data.stream.sclose()
       else: discard
       pager.lineData = nil
-  if lineedit.state in {lesCancel, lesFinish} and
-      pager.lineedit.get == lineedit:
+  if lineedit.state in {lesCancel, lesFinish} and pager.lineedit == lineedit:
     pager.clearLineEdit()
 
 # Same as load(s + '\n')
@@ -1773,7 +1795,6 @@ proc askDownloadPath(pager: Pager; container: Container; response: Response) =
     stream: response.body
   )
   pager.deleteContainer(container, container.find(ndAny))
-  pager.redraw = true
   pager.refreshStatusMsg()
   dec pager.numload
 
@@ -1840,7 +1861,6 @@ proc connected(pager: Pager; container: Container; response: Response) =
   else:
     dec pager.numload
     pager.deleteContainer(container, container.find(ndAny))
-    pager.redraw = true
     pager.refreshStatusMsg()
 
 # true if done, false if keep
@@ -1915,11 +1935,6 @@ proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent):
     pager.dupeBuffer(container, url2)
   of cetNoAnchor:
     pager.alert("Couldn't find anchor " & event.anchor)
-  of cetUpdate:
-    if container == pager.container:
-      pager.redraw = true
-      if event.force:
-        pager.term.clearCanvas()
   of cetReadLine:
     if container == pager.container:
       pager.setLineEdit(lmBuffer, event.value, hide = event.password,
@@ -1931,7 +1946,6 @@ proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent):
         pager.container.readSuccess(s)
       else:
         pager.container.readCanceled()
-      pager.redraw = true
   of cetReadFile:
     if container == pager.container:
       pager.setLineEdit(lmBufferFile, "")
@@ -1990,6 +2004,10 @@ proc handleEvents*(pager: Pager; container: Container) =
     if not pager.handleEvent0(container, event):
       break
 
+proc handleEvents*(pager: Pager) =
+  if pager.container != nil:
+    pager.handleEvents(pager.container)
+
 proc handleEvent*(pager: Pager; container: Container) =
   try:
     container.handleEvent()
diff --git a/src/local/select.nim b/src/local/select.nim
deleted file mode 100644
index 9e2f2c22..00000000
--- a/src/local/select.nim
+++ /dev/null
@@ -1,307 +0,0 @@
-import std/unicode
-
-import monoucha/jsregex
-import server/buffer
-import types/cell
-import utils/luwrap
-import utils/strwidth
-
-type
-  SubmitSelect* = proc(selected: seq[int])
-  CloseSelect* = proc()
-
-  Select* = object
-    open*: bool
-    options: seq[string]
-    multiple: bool
-    # old selection
-    oselected*: seq[int]
-    # new selection
-    selected*: seq[int]
-    # cursor distance from y
-    cursor: int
-    # widest option
-    maxw: int
-    # maximum height on screen (yes the naming is dumb)
-    maxh: int
-    # first index to display
-    si: int
-    # location on screen
-    x: int
-    y: int
-    redraw*: bool
-    submitFun: SubmitSelect
-    bpos: seq[int]
-
-proc windowChange*(select: var Select; height: int) =
-  select.maxh = height - 2
-  if select.y + select.options.len >= select.maxh:
-    select.y = height - select.options.len
-    if select.y < 0:
-      select.si = -select.y
-      select.y = 0
-  if select.selected.len > 0:
-    let i = select.selected[0]
-    if select.si > i:
-      select.si = i
-    elif select.si + select.maxh < i:
-      select.si = max(i - select.maxh, 0)
-  select.redraw = true
-
-proc initSelect*(select: var Select; selectResult: SelectResult;
-    x, y, height: int; submitFun: SubmitSelect) =
-  select.open = true
-  select.multiple = selectResult.multiple
-  select.options = selectResult.options
-  select.oselected = selectResult.selected
-  select.selected = selectResult.selected
-  select.submitFun = submitFun
-  for opt in select.options.mitems:
-    opt.mnormalize()
-    select.maxw = max(select.maxw, opt.width())
-  select.x = x
-  select.y = y
-  select.windowChange(height)
-
-# index of option currently under cursor
-func hover(select: Select): int =
-  return select.cursor + select.si
-
-func dispheight(select: Select): int =
-  return select.maxh - select.y
-
-proc `hover=`(select: var Select; i: int) =
-  let i = clamp(i, 0, select.options.high)
-  if i >= select.si + select.dispheight:
-    select.si = i - select.dispheight + 1
-    select.cursor = select.dispheight - 1
-  elif i < select.si:
-    select.si = i
-    select.cursor = 0
-  else:
-    select.cursor = i - select.si
-
-proc cursorDown*(select: var Select) =
-  if select.hover < select.options.high and
-      select.cursor + select.y < select.maxh - 1:
-    inc select.cursor
-    select.redraw = true
-  elif select.si < select.options.len - select.maxh:
-    inc select.si
-    select.redraw = true
-
-proc cursorUp*(select: var Select) =
-  if select.cursor > 0:
-    dec select.cursor
-    select.redraw = true
-  elif select.si > 0:
-    dec select.si
-    select.redraw = true
-  elif select.multiple and select.cursor > -1:
-    select.cursor = -1
-
-proc close(select: var Select) =
-  select = Select()
-
-proc cancel*(select: var Select) =
-  select.submitFun(select.oselected)
-  select.close()
-
-proc submit(select: var Select) =
-  select.submitFun(select.selected)
-  select.close()
-
-proc click*(select: var Select) =
-  if not select.multiple:
-    select.selected = @[select.hover]
-    select.submit()
-  elif select.cursor == -1:
-    select.submit()
-  else:
-    var k = select.selected.len
-    let i = select.hover
-    for j in 0 ..< select.selected.len:
-      if select.selected[j] >= i:
-        k = j
-        break
-    if k < select.selected.len and select.selected[k] == i:
-      select.selected.delete(k)
-    else:
-      select.selected.insert(i, k)
-    select.redraw = true
-
-proc cursorLeft*(select: var Select) =
-  select.submit()
-
-proc cursorRight*(select: var Select) =
-  select.click()
-
-proc getCursorX*(select: var Select): int =
-  if select.cursor == -1:
-    return select.x
-  return select.x + 1
-
-proc getCursorY*(select: var Select): int =
-  return select.y + 1 + select.cursor
-
-proc cursorFirstLine*(select: var Select) =
-  if select.cursor != 0 or select.si != 0:
-    select.cursor = 0
-    select.si = 0
-    select.redraw = true
-
-proc cursorLastLine*(select: var Select) =
-  if select.hover < select.options.len:
-    select.cursor = select.dispheight - 1
-    select.si = max(select.options.len - select.maxh, 0)
-    select.redraw = true
-
-proc cursorNextMatch*(select: var Select; regex: Regex; wrap: bool) =
-  var j = -1
-  for i in select.hover + 1 ..< select.options.len:
-    if regex.exec(select.options[i]).success:
-      j = i
-      break
-  if j != -1:
-    select.hover = j
-    select.redraw = true
-  elif wrap:
-    for i in 0 ..< select.hover:
-      if regex.exec(select.options[i]).success:
-        j = i
-        break
-    if j != -1:
-      select.hover = j
-      select.redraw = true
-
-proc cursorPrevMatch*(select: var Select; regex: Regex; wrap: bool) =
-  var j = -1
-  for i in countdown(select.hover - 1, 0):
-    if regex.exec(select.options[i]).success:
-      j = i
-      break
-  if j != -1:
-    select.hover = j
-    select.redraw = true
-  elif wrap:
-    for i in countdown(select.options.high, select.hover):
-      if regex.exec(select.options[i]).success:
-        j = i
-        break
-    if j != -1:
-      select.hover = j
-      select.redraw = true
-
-proc pushCursorPos*(select: var Select) =
-  select.bpos.add(select.hover)
-
-proc popCursorPos*(select: var Select; nojump = false) =
-  select.hover = select.bpos.pop()
-  if not nojump:
-    select.redraw = true
-
-const HorizontalBar = $Rune(0x2500)
-const VerticalBar = $Rune(0x2502)
-const CornerTopLeft = $Rune(0x250C)
-const CornerTopRight = $Rune(0x2510)
-const CornerBottomLeft = $Rune(0x2514)
-const CornerBottomRight = $Rune(0x2518)
-
-proc drawBorders(display: var FixedGrid; sx, ex, sy, ey: int;
-    upmore, downmore: bool) =
-  for y in sy .. ey:
-    var x = 0
-    while x < sx:
-      if display[y * display.width + x].str == "":
-        display[y * display.width + x].str = " "
-        inc x
-      else:
-        #x = display[y * display.width + x].str.twidth(x)
-        inc x
-  # Draw corners.
-  let tl = if upmore: VerticalBar else: CornerTopLeft
-  let tr = if upmore: VerticalBar else: CornerTopRight
-  let bl = if downmore: VerticalBar else: CornerBottomLeft
-  let br = if downmore: VerticalBar else: CornerBottomRight
-  const fmt = Format()
-  display[sy * display.width + sx].str = tl
-  display[sy * display.width + ex].str = tr
-  display[ey * display.width + sx].str = bl
-  display[ey * display.width + ex].str = br
-  display[sy * display.width + sx].format = fmt
-  display[sy * display.width + ex].format = fmt
-  display[ey * display.width + sx].format = fmt
-  display[ey * display.width + ex].format = fmt
-  # Draw top, bottom borders.
-  let ups = if upmore: " " else: HorizontalBar
-  let downs = if downmore: " " else: HorizontalBar
-  for x in sx + 1 .. ex - 1:
-    display[sy * display.width + x].str = ups
-    display[ey * display.width + x].str = downs
-    display[sy * display.width + x].format = fmt
-    display[ey * display.width + x].format = fmt
-  if upmore:
-    display[sy * display.width + sx + (ex - sx) div 2].str = ":"
-  if downmore:
-    display[ey * display.width + sx + (ex - sx) div 2].str = ":"
-  # Draw left, right borders.
-  for y in sy + 1 .. ey - 1:
-    display[y * display.width + sx].str = VerticalBar
-    display[y * display.width + ex].str = VerticalBar
-    display[y * display.width + sx].format = fmt
-    display[y * display.width + ex].format = fmt
-
-proc drawSelect*(select: Select; display: var FixedGrid) =
-  if display.width < 2 or display.height < 2:
-    return # border does not fit...
-  # Max width, height with one row/column on the sides.
-  let mw = display.width - 2
-  let mh = display.height - 2
-  var sy = select.y
-  let si = select.si
-  var ey = min(sy + select.options.len, mh) + 1
-  var sx = select.x
-  if sx + select.maxw >= mw:
-    sx = display.width - select.maxw
-    if sx < 0:
-      # This means the widest option is wider than the available screen.
-      # w3m simply cuts off the part that doesn't fit, and we do that too,
-      # but I feel like this may not be the best solution.
-      sx = 0
-  var ex = min(sx + select.maxw, mw) + 1
-  let upmore = select.si > 0
-  let downmore = select.si + mh < select.options.len
-  drawBorders(display, sx, ex, sy, ey, upmore, downmore)
-  if select.multiple and not upmore:
-    display[sy * display.width + sx].str = "X"
-  # 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:
-    inc k
-  for y in sy ..< ey:
-    let i = y - sy + si
-    var j = 0
-    var x = sx
-    let dls = y * display.width
-    if k < select.selected.len and select.selected[k] == i:
-      format.flags.incl(ffReverse)
-      inc k
-    else:
-      format.flags.excl(ffReverse)
-    while j < select.options[i].len:
-      fastRuneAt(select.options[i], j, r)
-      let rw = r.twidth(x)
-      let ox = x
-      x += rw
-      if x > ex:
-        break
-      display[dls + ox].str = $r
-      display[dls + ox].format = format
-    while x < ex:
-      display[dls + x].str = " "
-      display[dls + x].format = format
-      inc x
diff --git a/src/local/term.nim b/src/local/term.nim
index e4fc7429..91fe4a88 100644
--- a/src/local/term.nim
+++ b/src/local/term.nim
@@ -68,8 +68,7 @@ type
     kittyId: int
     bmp: Bitmap
 
-  Terminal* = ref TerminalObj
-  TerminalObj = object
+  Terminal* = ref object
     cs*: Charset
     config: Config
     istream*: PosixStream
@@ -95,6 +94,8 @@ type
     ibuf*: string # buffer for chars when we can't process them
     sixelRegisterNum: int
     kittyId: int # counter for kitty image (*not* placement) ids.
+    cursorx: int
+    cursory: int
 
 # control sequence introducer
 template CSI(s: varargs[string, `$`]): string =
@@ -283,7 +284,11 @@ proc endFormat(term: Terminal; flag: FormatFlags): string =
   return SGR(FormatCodes[flag].e)
 
 proc setCursor*(term: Terminal; x, y: int) =
-  term.write(term.cursorGoto(x, y))
+  assert x >= 0 and y >= 0
+  if x != term.cursorx or y != term.cursory:
+    term.write(term.cursorGoto(x, y))
+    term.cursorx = x
+    term.cursory = y
 
 proc enableAltScreen(term: Terminal): string =
   when termcap_found:
@@ -608,6 +613,8 @@ proc outputGrid*(term: Terminal) =
     term.cleared = true
   else:
     term.outfile.write(term.generateSwapOutput())
+  term.cursorx = -1
+  term.cursory = -1
 
 func findImage(term: Terminal; pid, imageId: int): CanvasImage =
   for it in term.canvasImages:
@@ -633,25 +640,50 @@ proc positionImage(term: Terminal; image: CanvasImage; x, y, maxw, maxh: int):
   image.dispw = min(int(image.bmp.width) + xpx, maxwpx) - xpx
   image.disph = min(int(image.bmp.height) + ypx, maxhpx) - ypx
   image.damaged = true
-  return image.dispw > 0 and image.disph > 0
+  return image.dispw > image.offx and image.disph > image.offy
+
+proc clearImage*(term: Terminal; image: CanvasImage; maxh: int) =
+  case term.imageMode
+  of imNone: discard
+  of imSixel:
+    # we must clear sixels the same way as we clear text.
+    var y = max(image.y, 0)
+    let ey = min(image.y + int(image.bmp.height), maxh)
+    let x = image.x
+    while y < ey:
+      term.lineDamage[y] = min(x, term.lineDamage[y])
+      inc y
+  of imKitty:
+    term.imagesToClear.add(image)
+
+proc clearImages*(term: Terminal; maxh: int) =
+  for image in term.canvasImages:
+    if not image.marked:
+      term.clearImage(image, maxh)
+    image.marked = false
 
 proc loadImage*(term: Terminal; bmp: Bitmap; pid, imageId, x, y, maxw,
     maxh: int): CanvasImage =
   if (let image = term.findImage(pid, imageId); image != nil):
     # reuse image on screen
     if image.x != x or image.y != y:
-      # with sixel, we must clear the image currently on the screen the same way
-      # as we clear text.
+      # only clear sixels; with kitty we just move the existing image
       if term.imageMode == imSixel:
-        var y = max(image.y, 0)
-        let ey = min(image.y + int(image.bmp.height), maxh)
-        let x = image.x
-        while y < ey:
-          term.lineDamage[y] = min(x, term.lineDamage[y])
-          inc y
+        term.clearImage(image, maxh)
       if not term.positionImage(image, x, y, maxw, maxh):
         # no longer on screen
         return nil
+    elif term.imageMode == imSixel:
+      # check if any line our image is on is damaged
+      let ey = min(image.y + int(image.bmp.height), maxh)
+      let mx = (image.offx + image.dispw) div term.attrs.ppc
+      for y in max(image.y, 0) ..< ey:
+        if term.lineDamage[y] < mx:
+          image.damaged = true
+          break
+    # only mark old images; new images will not be checked until the next
+    # initImages call.
+    image.marked = true
     return image
   # new image
   let image = CanvasImage(bmp: bmp, pid: pid, imageId: imageId)
@@ -807,14 +839,14 @@ proc clearCanvas*(term: Terminal) =
   term.cleared = false
   let maxw = term.attrs.width
   let maxh = term.attrs.height - 1
-  var toRemove: seq[int] = @[]
-  for i, image in term.canvasImages:
-    if not term.positionImage(image, image.x, image.y, maxw, maxh):
-      toRemove.add(i)
-      if term.imageMode == imKitty:
-        term.imagesToClear.add(image)
-  for i in countdown(toRemove.high, 0):
-    term.canvasImages.delete(toRemove[i])
+  var newImages: seq[CanvasImage] = @[]
+  for image in term.canvasImages:
+    if term.positionImage(image, image.x, image.y, maxw, maxh):
+      image.damaged = true
+      image.marked = true
+      newImages.add(image)
+  term.clearImages(maxh)
+  term.canvasImages = newImages
 
 # see https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html
 proc disableRawMode(term: Terminal) =
@@ -1216,6 +1248,8 @@ proc initScreen(term: Terminal) =
     term.write(term.enableAltScreen())
   if term.config.input.use_mouse:
     term.enableMouse()
+  term.cursorx = -1
+  term.cursory = -1
 
 proc start*(term: Terminal; istream: PosixStream): TermStartResult =
   term.istream = istream
diff --git a/todo b/todo
index fab985f5..84be5079 100644
--- a/todo
+++ b/todo
@@ -74,7 +74,6 @@ layout engine:
 - writing-mode, grid, ruby, ... (i.e. cool new stuff)
 images:
 - more efficient sixel display (store encoded images)
-- more efficient display in general (why are we repainting twice per keypress?)
 - document it (when performance is acceptable)
 - proper sixel color register allocation, dithering
 - fix race condition where images decoded after buffer load won't display until