about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/local/client.nim2
-rw-r--r--src/local/container.nim402
-rw-r--r--src/local/pager.nim6
-rw-r--r--src/local/select.nim315
4 files changed, 374 insertions, 351 deletions
diff --git a/src/local/client.nim b/src/local/client.nim
index f32e758d..33bb44e3 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -34,6 +34,7 @@ import loader/response
 import local/container
 import local/lineedit
 import local/pager
+import local/select
 import local/term
 import monoucha/constcharp
 import monoucha/fromjs
@@ -820,6 +821,7 @@ proc addJSModules(client: Client; ctx: JSContext) =
   ctx.addConfigModule()
   ctx.addPagerModule()
   ctx.addContainerModule()
+  ctx.addSelectModule()
   ctx.addCookieModule()
 
 func getClient(client: Client): Client {.jsfget: "client".} =
diff --git a/src/local/container.nim b/src/local/container.nim
index 81f5ffb0..6bdfe64b 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -15,6 +15,7 @@ import loader/headers
 import loader/loaderiface
 import loader/request
 import loader/response
+import local/select
 import monoucha/javascript
 import monoucha/jsregex
 import monoucha/jstypes
@@ -122,7 +123,7 @@ type
     dispw*: int # same as CanvasImage.dispw
     erry*: int # same as CanvasImage.offy % 6
 
-  Container* = ref object
+  Container* = ref object of RootObj
     # note: this is not the same as source.request.url (but should be synced
     # with buffer.url)
     url* {.jsget.}: URL
@@ -173,7 +174,7 @@ type
     events*: Deque[ContainerEvent]
     startpos: Option[CursorPosition]
     redirectDepth*: int
-    select*: Select
+    select* {.jsget.}: Select
     currentSelection {.jsget.}: Highlight
     tmpJumpMark: PagePos
     jumpMark: PagePos
@@ -188,23 +189,6 @@ type
     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)
 
@@ -214,275 +198,6 @@ 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 = "\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) =
-  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 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:
-      let pj = j
-      let u = select.options[i].nextUTF8(j)
-      let nx = x + u.twidth(x)
-      if nx > ex:
-        break
-      display[dls + x].str = select.options[i].substr(pj, j - 1)
-      display[dls + x].format = format
-      inc x
-      while x < nx:
-        display[dls + x].str = ""
-        display[dls + x].format = format
-        inc x
-    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];
@@ -1000,28 +715,16 @@ proc setCursorXYCenter(container: Container; x, y: int; refresh = true)
     container.centerColumn()
 
 proc cursorDown(container: Container; n = 1) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cursorDown()
-  else:
-    container.setCursorY(container.cursory + n)
+  container.setCursorY(container.cursory + n)
 
 proc cursorUp(container: Container; n = 1) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cursorUp()
-  else:
-    container.setCursorY(container.cursory - n)
+  container.setCursorY(container.cursory - n)
 
 proc cursorLeft(container: Container; n = 1) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cursorLeft()
-  else:
-    container.setCursorX(container.cursorFirstX() - n)
+  container.setCursorX(container.cursorFirstX() - n)
 
 proc cursorRight(container: Container; n = 1) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cursorRight()
-  else:
-    container.setCursorX(container.cursorLastX() + n)
+  container.setCursorX(container.cursorLastX() + n)
 
 proc cursorLineBegin(container: Container) {.jsfunc.} =
   container.setCursorX(-1)
@@ -1275,20 +978,14 @@ proc markPos*(container: Container) =
     container.jumpMark = pos
 
 proc cursorFirstLine(container: Container) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cursorFirstLine()
-  else:
-    container.markPos0()
-    container.setCursorY(0)
-    container.markPos()
+  container.markPos0()
+  container.setCursorY(0)
+  container.markPos()
 
 proc cursorLastLine*(container: Container) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cursorLastLine()
-  else:
-    container.markPos0()
-    container.setCursorY(container.numLines - 1)
-    container.markPos()
+  container.markPos0()
+  container.setCursorY(container.numLines - 1)
+  container.markPos()
 
 proc cursorTop(container: Container; i = 1) {.jsfunc.} =
   container.markPos0()
@@ -1815,9 +1512,7 @@ proc remoteCancel*(container: Container) =
   container.alert("Canceled loading")
 
 proc cancel*(container: Container) {.jsfunc.} =
-  if container.select != nil:
-    container.select.cancel()
-  elif container.loadState == lsLoading:
+  if container.loadState == lsLoading:
     container.loadState = lsCanceled
     if container.iface != nil:
       container.remoteCancel()
@@ -1856,21 +1551,30 @@ proc reshape(container: Container): EmptyPromise {.jsfunc.} =
     return container.requestLines()
   )
 
+proc selectFinish(opaque: RootRef; select: Select; res: SubmitResult) =
+  let container = Container(opaque)
+  case res
+  of srCancel:
+    container.iface.select(select.oselected).then(proc(res: ClickResult) =
+      container.onclick(res, save = false)
+    )
+  of srSubmit:
+    container.iface.select(select.selected).then(proc(res: ClickResult) =
+      container.onclick(res, save = false)
+    )
+  container.select = nil
+
 proc displaySelect(container: Container; selectResult: SelectResult) =
-  container.select = Select(
-    container: container,
-    multiple: selectResult.multiple,
-    options: selectResult.options,
-    oselected: selectResult.selected,
-    selected: selectResult.selected,
-    x: container.acursorx,
-    y: container.acursory
+  container.select = newSelect(
+    selectResult.multiple,
+    selectResult.options,
+    selectResult.selected,
+    container.acursorx,
+    container.acursory,
+    container.height,
+    selectFinish,
+    container
   )
-  for opt in container.select.options.mitems:
-    opt.mnormalize()
-    container.select.maxw = max(container.select.maxw, opt.width())
-  container.select.windowChange(container.height)
-  container.select.queueDraw()
 
 proc onclick(container: Container; res: ClickResult; save: bool) =
   if res.repaint:
@@ -1887,13 +1591,10 @@ proc onclick(container: Container; res: ClickResult; save: bool) =
     container.onReadLine(res.readline.get)
 
 proc click*(container: Container) {.jsfunc.} =
-  if container.select != nil:
-    container.select.click()
-  else:
-    if container.iface == nil:
-      return
-    container.iface.click(container.cursorx, container.cursory)
-      .then(proc(res: ClickResult) = container.onclick(res, save = false))
+  if container.iface == nil:
+    return
+  container.iface.click(container.cursorx, container.cursory)
+    .then(proc(res: ClickResult) = container.onclick(res, save = false))
 
 proc saveLink*(container: Container) {.jsfunc.} =
   if container.iface == nil:
@@ -1912,17 +1613,18 @@ proc saveSource*(container: Container) {.jsfunc.} =
   ))
 
 proc windowChange*(container: Container; attrs: WindowAttributes) =
-  if attrs.width != container.width or attrs.height - 1 != container.height:
-    container.width = attrs.width
-    container.height = attrs.height - 1
-    if container.iface != nil:
-      var attrs = attrs
-      # subtract status line height
-      attrs.height -= 1
-      attrs.heightPx -= attrs.ppl
-      container.iface.windowChange(attrs).then(proc() =
-        container.needslines = true
-      )
+  container.width = attrs.width
+  container.height = attrs.height - 1
+  if container.iface != nil:
+    var attrs = attrs
+    # subtract status line height
+    attrs.height -= 1
+    attrs.heightPx -= attrs.ppl
+    container.iface.windowChange(attrs).then(proc() =
+      container.needslines = true
+    )
+  if container.select != nil:
+    container.select.windowChange(container.height)
 
 proc peek(container: Container) {.jsfunc.} =
   container.alert($container.url)
diff --git a/src/local/pager.nim b/src/local/pager.nim
index 3e0c21a0..ec242472 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -27,6 +27,7 @@ import loader/request
 import loader/response
 import local/container
 import local/lineedit
+import local/select
 import local/term
 import monoucha/fromjs
 import monoucha/javascript
@@ -231,7 +232,10 @@ proc reflect(ctx: JSContext; this_val: JSValue; argc: cint;
 
 proc getter(ctx: JSContext; pager: Pager; a: JSAtom): JSValue {.jsgetprop.} =
   if pager.container != nil:
-    let cval = ctx.toJS(pager.container)
+    let cval = if pager.container.select != nil:
+      ctx.toJS(pager.container.select)
+    else:
+      ctx.toJS(pager.container)
     let val = JS_GetProperty(ctx, cval, a)
     if JS_IsFunction(ctx, val):
       let func_data = @[cval, val]
diff --git a/src/local/select.nim b/src/local/select.nim
new file mode 100644
index 00000000..70726e7c
--- /dev/null
+++ b/src/local/select.nim
@@ -0,0 +1,315 @@
+import monoucha/javascript
+import monoucha/jsregex
+import types/cell
+import utils/luwrap
+import utils/strwidth
+import utils/twtuni
+
+type
+  SelectFinish* = proc(opaque: RootRef; select: Select; res: SubmitResult)
+    {.nimcall.}
+
+  Select* = ref object
+    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]
+    opaque: RootRef
+    finishImpl: SelectFinish
+
+  SubmitResult* = enum
+    srCancel, srSubmit
+
+jsDestructor(Select)
+
+proc queueDraw(select: Select) =
+  select.redraw = true
+
+# 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) {.jsfunc.} =
+  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) {.jsfunc.} =
+  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 cancel(select: Select) {.jsfunc.} =
+  select.finishImpl(select.opaque, select, srCancel)
+
+proc submit(select: Select) {.jsfunc.} =
+  select.finishImpl(select.opaque, select, srSubmit)
+
+proc click(select: Select) {.jsfunc.} =
+  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) {.jsfunc.} =
+  select.submit()
+
+proc cursorRight(select: Select) {.jsfunc.} =
+  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) {.jsfunc.} =
+  if select.cursor != 0 or select.si != 0:
+    select.cursor = 0
+    select.si = 0
+    select.queueDraw()
+
+proc cursorLastLine(select: Select) {.jsfunc.} =
+  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 = "\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) =
+  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 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:
+      let pj = j
+      let u = select.options[i].nextUTF8(j)
+      let nx = x + u.twidth(x)
+      if nx > ex:
+        break
+      display[dls + x].str = select.options[i].substr(pj, j - 1)
+      display[dls + x].format = format
+      inc x
+      while x < nx:
+        display[dls + x].str = ""
+        display[dls + x].format = format
+        inc x
+    while x < ex:
+      display[dls + x].str = " "
+      display[dls + x].format = format
+      inc x
+
+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()
+
+proc newSelect*(multiple: bool; options: seq[string]; selected: seq[int];
+    x, y, height: int; finishImpl: SelectFinish; opaque: RootRef): Select =
+  let select = Select(
+    multiple: multiple,
+    options: options,
+    oselected: selected,
+    selected: selected,
+    x: x,
+    y: y,
+    finishImpl: finishImpl,
+    opaque: opaque
+  )
+  select.windowChange(height)
+  for opt in select.options.mitems:
+    opt.mnormalize()
+    select.maxw = max(select.maxw, opt.width())
+  select.windowChange(height)
+  select.queueDraw()
+  return select
+
+proc addSelectModule*(ctx: JSContext) =
+  ctx.registerType(Select)