« get me outta code hell

tui-lib - Pure Node.js library for making visual command-line programs (ala vim, ncdu)
about summary refs log tree commit diff
path: root/ui/form
diff options
context:
space:
mode:
Diffstat (limited to 'ui/form')
-rw-r--r--ui/form/Button.js51
-rw-r--r--ui/form/CancelDialog.js63
-rw-r--r--ui/form/ConfirmDialog.js79
-rw-r--r--ui/form/FocusBox.js32
-rw-r--r--ui/form/FocusElement.js45
-rw-r--r--ui/form/Form.js154
-rw-r--r--ui/form/ListScrollForm.js303
-rw-r--r--ui/form/ScrollBar.js121
-rw-r--r--ui/form/TextInput.js145
9 files changed, 0 insertions, 993 deletions
diff --git a/ui/form/Button.js b/ui/form/Button.js
deleted file mode 100644
index 46329a6..0000000
--- a/ui/form/Button.js
+++ /dev/null
@@ -1,51 +0,0 @@
-const ansi = require('../../util/ansi')
-const telc = require('../../util/telchars')
-
-const FocusElement = require('./FocusElement')
-
-module.exports = class Button extends FocusElement {
-  // A button.
-
-  constructor(text) {
-    super()
-
-    this.text = text
-
-    this.cursorX = null
-    this.cursorY = null
-  }
-
-  fixLayout() {
-    this.w = ansi.measureColumns(this.text)
-    this.h = 1
-  }
-
-  drawTo(writable) {
-    if (this.isSelected) {
-      writable.write(ansi.invert())
-    }
-
-    writable.write(ansi.moveCursor(this.absTop, this.absLeft))
-    writable.write(this.text)
-
-    writable.write(ansi.resetAttributes())
-
-    super.drawTo(writable)
-  }
-
-  keyPressed(keyBuf) {
-    if (telc.isSelect(keyBuf)) {
-      this.emit('pressed')
-    }
-  }
-
-  clicked(button) {
-    if (button === 'left') {
-      if (this.isSelected) {
-        this.emit('pressed')
-      } else {
-        this.root.select(this)
-      }
-    }
-  }
-}
diff --git a/ui/form/CancelDialog.js b/ui/form/CancelDialog.js
deleted file mode 100644
index 21ff6df..0000000
--- a/ui/form/CancelDialog.js
+++ /dev/null
@@ -1,63 +0,0 @@
-const telc = require('../../util/telchars')
-
-const FocusElement = require('./FocusElement')
-
-const Button = require('./Button')
-const Form = require('./Form')
-const Label = require('../Label')
-const Pane = require('../Pane')
-
-module.exports = class ConfirmDialog extends FocusElement {
-  // A basic cancel dialog. Has one buttons, cancel, and a label.
-  // The escape (esc) key can be used to exit the dialog (which sends a
-  // 'cancelled' event, as the cancel button also does).
-
-  constructor(text) {
-    super()
-
-    this.pane = new Pane()
-    this.addChild(this.pane)
-
-    this.cancelBtn = new Button('Cancel')
-    this.pane.addChild(this.cancelBtn)
-
-    this.label = new Label(text)
-    this.pane.addChild(this.label)
-
-    this.initEventListeners()
-  }
-
-  initEventListeners() {
-    this.cancelBtn.on('pressed', () => this.cancelPressed())
-  }
-
-  fixLayout() {
-    this.w = this.parent.contentW
-    this.h = this.parent.contentH
-
-    this.pane.w = Math.max(40, 4 + this.label.w)
-    this.pane.h = 7
-    this.pane.centerInParent()
-
-    this.label.x = Math.floor((this.pane.contentW - this.label.w) / 2)
-    this.label.y = 1
-
-    this.cancelBtn.x = Math.floor(
-      (this.pane.contentW - this.cancelBtn.w) / 2)
-    this.cancelBtn.y = this.pane.contentH - 2
-  }
-
-  selected() {
-    this.root.select(this.cancelBtn)
-  }
-
-  keyPressed(keyBuf) {
-    if (telc.isCancel(keyBuf)) {
-      this.emit('cancelled')
-    }
-  }
-
-  cancelPressed() {
-    this.emit('cancelled')
-  }
-}
diff --git a/ui/form/ConfirmDialog.js b/ui/form/ConfirmDialog.js
deleted file mode 100644
index 230230d..0000000
--- a/ui/form/ConfirmDialog.js
+++ /dev/null
@@ -1,79 +0,0 @@
-const telc = require('../../util/telchars')
-
-const FocusElement = require('./FocusElement')
-
-const Button = require('./Button')
-const Form = require('./Form')
-const Label = require('../Label')
-const Pane = require('../Pane')
-
-module.exports = class ConfirmDialog extends FocusElement {
-  // A basic yes/no dialog. Has two buttons, confirm/cancel, and a label.
-  // The escape (esc) key can be used to exit the dialog (which sends a
-  // 'cancelled' event, as the cancel button also does).
-
-  constructor(text) {
-    super()
-
-    this.pane = new Pane()
-    this.addChild(this.pane)
-
-    this.form = new Form()
-    this.pane.addChild(this.form)
-
-    this.confirmBtn = new Button('Confirm')
-    this.form.addInput(this.confirmBtn)
-
-    this.cancelBtn = new Button('Cancel')
-    this.form.addInput(this.cancelBtn)
-
-    this.label = new Label(text)
-    this.form.addChild(this.label)
-
-    this.initEventListeners()
-  }
-
-  initEventListeners() {
-    this.confirmBtn.on('pressed', () => this.confirmPressed())
-    this.cancelBtn.on('pressed', () => this.cancelPressed())
-  }
-
-  fixLayout() {
-    this.w = this.parent.contentW
-    this.h = this.parent.contentH
-
-    this.pane.w = Math.max(40, 2 + this.label.w)
-    this.pane.h = 7
-    this.pane.centerInParent()
-
-    this.form.w = this.pane.contentW
-    this.form.h = this.pane.contentH
-
-    this.label.x = Math.floor((this.form.contentW - this.label.w) / 2)
-    this.label.y = 1
-
-    this.confirmBtn.x = 1
-    this.confirmBtn.y = this.form.contentH - 2
-
-    this.cancelBtn.x = this.form.right - this.cancelBtn.w - 1
-    this.cancelBtn.y = this.form.contentH - 2
-  }
-
-  selected() {
-    this.root.select(this.form)
-  }
-
-  keyPressed(keyBuf) {
-    if (telc.isCancel(keyBuf)) {
-      this.emit('cancelled')
-    }
-  }
-
-  confirmPressed() {
-    this.emit('confirmed')
-  }
-
-  cancelPressed() {
-    this.emit('cancelled')
-  }
-}
diff --git a/ui/form/FocusBox.js b/ui/form/FocusBox.js
deleted file mode 100644
index 69b5bf5..0000000
--- a/ui/form/FocusBox.js
+++ /dev/null
@@ -1,32 +0,0 @@
-const ansi = require('../../util/ansi')
-
-const FocusElement = require('./FocusElement')
-
-module.exports = class FocusBox extends FocusElement {
-  // A box (not to be confused with Pane!) that can be selected. When it's
-  // selected, it applies an invert effect to its children. (This won't work
-  // well if you have elements inside of it that have their own attributes,
-  // since they're likely to reset all effects after drawing - including the
-  // invert from the FocusBox! Bad ANSI limitations; it's relatively likely
-  // I'll implement maaaaaagic to help deal with this - maybe something
-  // similar to 'pushMatrix' from Processing - at some point... [TODO])
-
-  constructor() {
-    super()
-
-    this.cursorX = null
-    this.cursorY = null
-  }
-
-  drawTo(writable) {
-    if (this.isSelected) {
-      writable.write(ansi.invert())
-    }
-  }
-
-  didRenderTo(writable) {
-    if (this.isSelected) {
-      writable.write(ansi.resetAttributes())
-    }
-  }
-}
diff --git a/ui/form/FocusElement.js b/ui/form/FocusElement.js
deleted file mode 100644
index 23c2e02..0000000
--- a/ui/form/FocusElement.js
+++ /dev/null
@@ -1,45 +0,0 @@
-const DisplayElement = require('../DisplayElement')
-
-module.exports = class FocusElement extends DisplayElement {
-  // A basic element that can receive cursor focus.
-
-  constructor() {
-    super()
-
-    this.cursorVisible = false
-    this.cursorX = 0
-    this.cursorY = 0
-  }
-
-  selected() {
-    // Should be overridden in subclasses.
-  }
-
-  unselected() {
-    // Should be overridden in subclasses.
-  }
-
-  get selectable() {
-    // Should be overridden if you want to make the element unselectable
-    // (according to particular conditions).
-
-    return true
-  }
-
-  keyPressed(keyBuf) {
-    // Do something with a buffer containing the key pressed (that is,
-    // telnet data sent). Should be overridden in subclasses.
-    //
-    // Arrow keys are sent as a buffer in the form of
-    // ESC[# where # is A, B, C or D. See more here:
-    // http://stackoverflow.com/a/11432632/4633828
-  }
-
-  get isSelected() {
-    const selected = this.root.selectedElement
-    return !!(selected && [selected, ...selected.directAncestors].includes(this))
-  }
-
-  get absCursorX() { return this.absX + this.cursorX }
-  get absCursorY() { return this.absY + this.cursorY }
-}
diff --git a/ui/form/Form.js b/ui/form/Form.js
deleted file mode 100644
index f61c7b6..0000000
--- a/ui/form/Form.js
+++ /dev/null
@@ -1,154 +0,0 @@
-const telc = require('../../util/telchars')
-
-const FocusElement = require('./FocusElement')
-
-module.exports = class Form extends FocusElement {
-  constructor() {
-    super()
-
-    this.inputs = []
-    this.curIndex = 0
-    this.captureTab = true
-  }
-
-  addInput(input, asChild = true, opts = {}) {
-    // Adds the given input as a child element and pushes it to the input
-    // list. If the optional argument asChild is false, it won't add the
-    // input element as a child of the form.
-
-    this.inputs.push(input)
-
-    if (asChild) {
-      this.addChild(input, this.children.length, opts)
-    }
-  }
-
-  removeInput(input, asChild = true, opts = {}) {
-    // Removes the given input from the form's input list. If the optional
-    // argument asChild is false, it won't try to removeChild the input.
-
-    if (this.inputs.includes(input)) {
-      this.inputs.splice(this.inputs.indexOf(input), 1)
-
-      if (asChild) {
-        this.removeChild(input, opts)
-      }
-    }
-  }
-
-  selectInput(input) {
-    if (this.inputs.includes(input)) {
-      this.curIndex = this.inputs.indexOf(input)
-      this.updateSelectedElement()
-    }
-  }
-
-  keyPressed(keyBuf) {
-    // Don't do anything if captureTab is set to false. This is handy for
-    // nested forms.
-    if (!this.captureTab) {
-      return
-    }
-
-    if (telc.isTab(keyBuf) || telc.isBackTab(keyBuf)) {
-      // No inputs to tab through, so do nothing.
-      if (this.inputs.length < 2) {
-        return
-      }
-
-      if (telc.isTab(keyBuf)) {
-        this.nextInput()
-      } else {
-        this.previousInput()
-      }
-
-      return false
-    }
-  }
-
-  get selectable() {
-    return this.inputs.some(inp => inp.selectable)
-  }
-
-  updateSelectedElement() {
-    if (this.root.select && this.inputs.length) {
-      if (this.curIndex > this.inputs.length - 1) {
-        this.curIndex = this.inputs.length - 1
-      }
-
-      this.root.select(this.inputs[this.curIndex], {fromForm: true})
-    }
-  }
-
-  previousInput() {
-    if (this.inputs.length === 0) {
-      return
-    }
-
-    do {
-      this.curIndex = (this.curIndex - 1)
-      if (this.curIndex < 0) {
-        this.curIndex = (this.inputs.length - 1)
-      }
-    } while (!this.inputs[this.curIndex].selectable)
-
-    this.updateSelectedElement()
-  }
-
-  nextInput() {
-    if (this.inputs.length === 0) {
-      return
-    }
-
-    do {
-      this.curIndex = (this.curIndex + 1) % this.inputs.length
-    } while (!this.inputs[this.curIndex].selectable)
-
-    this.updateSelectedElement()
-  }
-
-  firstInput(selectForm = true) {
-    if (this.inputs.length === 0) {
-      return
-    }
-
-    this.curIndex = 0
-
-    if (!this.inputs[this.curIndex].selectable) {
-      this.nextInput()
-    }
-
-    if (selectForm || (
-      this.root.isChildOrSelfSelected && this.root.isChildOrSelfSelected(this)
-    )) {
-      this.updateSelectedElement()
-    }
-  }
-
-  lastInput(selectForm = true) {
-    if (this.inputs.length === 0) {
-      return
-    }
-
-    this.curIndex = this.inputs.length - 1
-
-    if (!this.inputs[this.curIndex].selectable) {
-      this.previousInput()
-    }
-
-    if (selectForm || (
-      this.root.isChildOrSelfSelected && this.root.isChildOrSelfSelected(this)
-    )) {
-      this.updateSelectedElement()
-    }
-  }
-
-  selected() {
-    if (this.root.selectedElement === this) {
-      this.updateSelectedElement()
-    }
-  }
-
-  get curIndex() { return this.getDep('curIndex') }
-  set curIndex(v) { return this.setDep('curIndex', v) }
-}
diff --git a/ui/form/ListScrollForm.js b/ui/form/ListScrollForm.js
deleted file mode 100644
index e4f4249..0000000
--- a/ui/form/ListScrollForm.js
+++ /dev/null
@@ -1,303 +0,0 @@
-const ansi = require('../../util/ansi')
-const telc = require('../../util/telchars')
-
-const Form = require('./Form')
-const ScrollBar = require('./ScrollBar')
-
-module.exports = class ListScrollForm extends Form {
-  // A form that lets the user scroll through a list of items. It
-  // automatically adjusts to always allow the selected item to be visible.
-  // Unless disabled in the constructor, a scrollbar is automatically displayed
-  // if there are more items than can be shown in the height of the form at a
-  // single time.
-
-  constructor(layoutType = 'vertical', enableScrollBar = true) {
-    super()
-
-    this.layoutType = layoutType
-    this.wheelMode = 'scroll' // scroll, selection
-
-    this.scrollItems = 0
-
-    this.scrollBarEnabled = enableScrollBar
-
-    this.scrollBar = new ScrollBar({
-      getLayoutType: () => this.layoutType,
-      getCurrentScroll: () => this.scrollItems,
-      getMaximumScroll: () => this.getScrollItemsLength(),
-      getTotalItems: () => this.inputs.length
-    })
-    this.scrollBarShown = false
-  }
-
-  fixLayout() {
-    this.keepScrollInBounds()
-
-    const scrollItems = this.scrollItems
-
-    // The scrollItems property represents the item to the very left of where
-    // we've scrolled, so we know right away that none of those will be
-    // visible and we won't bother iterating over them. We do need to hide
-    // them, though.
-    for (let i = 0; i < Math.min(scrollItems, this.inputs.length); i++) {
-      this.inputs[i].visible = false
-    }
-
-    // This variable stores how far along the respective axis (implied by
-    // layoutType) the next element should be.
-    let nextPos = 0
-
-    let formEdge
-    if (this.layoutType === 'horizontal') {
-      formEdge = this.contentW
-    } else {
-      formEdge = this.contentH
-    }
-
-    for (let i = scrollItems; i < this.inputs.length; i++) {
-      const item = this.inputs[i]
-      item.fixLayout()
-
-      const curPos = nextPos
-      let curSize
-      if (this.layoutType === 'horizontal') {
-        item.x = curPos
-        curSize = item.w
-      } else {
-        item.y = curPos
-        curSize = item.h
-      }
-      nextPos += curSize
-
-      // By default, the item should be visible..
-      item.visible = true
-
-      // ..but the item's far edge is past the form's far edge, it isn't
-      // fully visible and should be hidden.
-      if (curPos + curSize > formEdge) {
-        item.visible = false
-      }
-
-      // Same deal goes for the close edge. We can check it against 0 since
-      // the close edge of the form's content is going to be 0, of course!
-      if (curPos < 0) {
-        item.visible = false
-      }
-    }
-
-    delete this._scrollItemsLength
-
-    if (this.scrollBarEnabled) {
-      this.showScrollbarIfNecessary()
-    }
-  }
-
-  keyPressed(keyBuf) {
-    let ret
-
-    handleKeyPress: {
-      if (this.layoutType === 'horizontal') {
-        if (telc.isLeft(keyBuf)) {
-          this.previousInput()
-          ret = false; break handleKeyPress
-        } else if (telc.isRight(keyBuf)) {
-          this.nextInput()
-          ret = false; break handleKeyPress
-        }
-      } else if (this.layoutType === 'vertical') {
-        if (telc.isUp(keyBuf)) {
-          this.previousInput()
-          ret = false; break handleKeyPress
-        } else if (telc.isDown(keyBuf)) {
-          this.nextInput()
-          ret = false; break handleKeyPress
-        }
-      }
-
-      ret = super.keyPressed(keyBuf)
-    }
-
-    this.scrollSelectedElementIntoView()
-
-    return ret
-  }
-
-  clicked(button) {
-    if (this.wheelMode === 'selection') {
-      // Change the actual selected item.
-      if (button === 'scroll-up') {
-        this.previousInput()
-        this.scrollSelectedElementIntoView()
-      } else if (button === 'scroll-down') {
-        this.nextInput()
-        this.scrollSelectedElementIntoView()
-      }
-    } else if (this.wheelMode === 'scroll') {
-      // Scrolling is typically pretty slow with a mouse wheel when it's by
-      // a single line, so scroll at 3x that speed.
-      for (let i = 0; i < 3; i++) {
-        if (button === 'scroll-up') {
-          this.scrollItems--
-        } else if (button === 'scroll-down') {
-          this.scrollItems++
-        } else {
-          return
-        }
-      }
-    }
-
-    this.fixLayout()
-  }
-
-  scrollSelectedElementIntoView() {
-    const sel = this.inputs[this.curIndex]
-
-    if (!sel) {
-      return
-    }
-
-    let formEdge
-    if (this.layoutType === 'horizontal') {
-      formEdge = this.contentW
-    } else {
-      formEdge = this.contentH
-    }
-
-    // If the item is ahead of our view (either to the right of or below),
-    // we should move the view so that the item is the farthest right (of all
-    // the visible items).
-    if (this.getItemPos(sel) > formEdge + this.scrollSize) {
-      this.scrollElementIntoEndOfView(sel)
-    }
-
-    // Adjusting the number of scroll items is much simpler to deal with if
-    // the item is behind our view. Since the item's behind, we need to move
-    // the scroll to be immediately behind it, which is simple since we
-    // already have its index.
-    if (this.getItemPos(sel) <= this.scrollSize) {
-      this.scrollItems = this.curIndex
-    }
-
-    this.fixLayout()
-  }
-
-  firstInput(...args) {
-    this.scrollItems = 0
-
-    super.firstInput(...args)
-
-    this.fixLayout()
-  }
-
-  getScrollPositionOfElementAtEndOfView(element) {
-    // We can decide how many items to scroll past by moving forward until
-    // the item's far edge is visible.
-    const pos = this.getItemPos(element)
-
-    let edge
-    if (this.layoutType === 'horizontal') {
-      edge = this.contentW
-    } else {
-      edge = this.contentH
-    }
-
-    for (let i = 0; i < this.inputs.length; i++) {
-      if (pos <= edge) {
-        return i
-      }
-
-      if (this.layoutType === 'horizontal') {
-        edge += this.inputs[i].w
-      } else {
-        edge += this.inputs[i].h
-      }
-    }
-    // No result? Well, it's at the end.
-    return this.inputs.length
-  }
-
-  scrollElementIntoEndOfView(element) {
-    this.scrollItems = this.getScrollPositionOfElementAtEndOfView(element)
-  }
-
-  scrollToBeginning() {
-    this.scrollItems = 0
-    this.fixLayout()
-  }
-
-  scrollToEnd() {
-    this.scrollElementIntoEndOfView(this.inputs[this.inputs.length - 1])
-    this.fixLayout()
-  }
-
-  keepScrollInBounds() {
-    this.scrollItems = Math.max(this.scrollItems, 0)
-    this.scrollItems = Math.min(this.scrollItems, this.getScrollItemsLength())
-  }
-
-  getScrollItemsLength() {
-    if (typeof this._scrollItemsLength === 'undefined') {
-      const lastInput = this.inputs[this.inputs.length - 1]
-      this._scrollItemsLength = this.getScrollPositionOfElementAtEndOfView(lastInput)
-    }
-
-    return this._scrollItemsLength
-  }
-
-  getItemPos(item) {
-    // Gets the position of the item in an unscrolled view.
-
-    const index = this.inputs.indexOf(item)
-    let pos = 0
-    for (let i = 0; i <= index; i++) {
-      if (this.layoutType === 'horizontal') {
-        pos += this.inputs[i].w
-      } else {
-        pos += this.inputs[i].h
-      }
-    }
-    return pos
-  }
-
-  showScrollbarIfNecessary() {
-    this.scrollBarShown = this.scrollBar.canScrollAtAll()
-
-    const isChild = this.children.includes(this.scrollBar)
-    if (this.scrollBarShown) {
-      if (!isChild) this.addChild(this.scrollBar)
-    } else {
-      if (isChild) this.removeChild(this.scrollBar)
-    }
-  }
-
-  get scrollSize() {
-    // Gets the actual length made up by all of the items currently scrolled
-    // past.
-
-    let size = 0
-    for (let i = 0; i < Math.min(this.scrollItems, this.inputs.length); i++) {
-      if (this.layoutType === 'horizontal') {
-        size += this.inputs[i].w
-      } else {
-        size += this.inputs[i].h
-      }
-    }
-    return size
-  }
-
-  get contentW() {
-    if (this.scrollBarShown && this.layoutType === 'vertical') {
-      return this.w - 1
-    } else {
-      return this.w
-    }
-  }
-
-  get contentH() {
-    if (this.scrollBarShown && this.layoutType === 'horizontal') {
-      return this.h - 1
-    } else {
-      return this.h
-    }
-  }
-}
diff --git a/ui/form/ScrollBar.js b/ui/form/ScrollBar.js
deleted file mode 100644
index 13ba7fe..0000000
--- a/ui/form/ScrollBar.js
+++ /dev/null
@@ -1,121 +0,0 @@
-const DisplayElement = require('../DisplayElement')
-
-const ansi = require('../../util/ansi')
-const unic = require('../../util/unichars')
-
-module.exports = class ScrollBar extends DisplayElement {
-  constructor({
-    getLayoutType,
-    getCurrentScroll,
-    getMaximumScroll,
-    getTotalItems
-  }) {
-    super()
-
-    this.getLayoutType = getLayoutType
-    this.getCurrentScroll = getCurrentScroll
-    this.getMaximumScroll = getMaximumScroll
-    this.getTotalItems = getTotalItems
-  }
-
-  fixLayout() {
-    // Normally we'd subtract one from contentW/contentH when setting the x/y
-    // position, but the scroll-bar is actually displayed OUTSIDE of (adjacent
-    // to) the parent's content area.
-    if (this.getLayoutType() === 'vertical') {
-      this.h = this.parent.contentH
-      this.w = 1
-      this.x = this.parent.contentW
-      this.y = 0
-    } else {
-      this.h = 1
-      this.w = this.parent.contentW
-      this.x = 0
-      this.y = this.parent.contentH
-    }
-  }
-
-  drawTo(writable) {
-    // Uuuurgh
-    this.fixLayout()
-
-    // TODO: Horizontal layout! Not functionally a lot different, but I'm too
-    // lazy to write a test UI for it right now.
-
-    const {
-      backwards: canScrollBackwards,
-      forwards: canScrollForwards
-    } = this.getScrollableDirections()
-
-    // - 2 for extra UI elements (arrows)
-    const totalLength = this.h - 2
-
-    // ..[-----]..
-    //   ^start|
-    //         ^end
-    //
-    // Start and end should correspond to how much of the scroll area
-    // is currently visible. So, if you can see 60% of the full scroll length
-    // at a time, and you are scrolled 10% down, the start position of the
-    // handle should be 10% down, and it should extend 60% of the scrollbar
-    // length, to the 70% mark.
-
-    // NB: I think this math mixes the units for "items" and "lines".
-    // edgeLength is measured in lines, while totalItems is a number of items.
-    // This isn't a problem when the length of an item is equal to one line,
-    // but it's still worth investigating at some point.
-    const currentScroll = this.getCurrentScroll()
-    const totalItems = this.getTotalItems()
-    const edgeLength = this.parent.contentH
-    const visibleAtOnce = Math.min(totalItems, edgeLength)
-    const handleLength = visibleAtOnce / totalItems * totalLength
-    let handlePosition = Math.floor(totalLength / totalItems * currentScroll)
-
-    // Silly peeve of mine: The handle should only be visibly touching the top
-    // or bottom of the scrollbar area if you're actually scrolled all the way
-    // to the start or end. Otherwise, it shouldn't be touching! There should
-    // visible space indicating that you can scroll in that direction
-    // (in addition to the arrows we show at the ends).
-
-    if (canScrollBackwards && handlePosition === 0) {
-      handlePosition = 1
-    }
-
-    if (canScrollForwards && (handlePosition + handleLength) === edgeLength) {
-      handlePosition--
-    }
-
-    if (this.getLayoutType() === 'vertical') {
-      const start = this.absTop + handlePosition + 1
-      for (let i = 0; i < handleLength; i++) {
-        writable.write(ansi.moveCursor(start + i, this.absLeft))
-        writable.write(unic.BOX_V_DOUBLE)
-      }
-
-      if (canScrollBackwards) {
-        writable.write(ansi.moveCursor(this.absTop, this.absLeft))
-        writable.write(unic.ARROW_UP_DOUBLE)
-      }
-
-      if (canScrollForwards) {
-        writable.write(ansi.moveCursor(this.absBottom, this.absLeft))
-        writable.write(unic.ARROW_DOWN_DOUBLE)
-      }
-    }
-  }
-
-  getScrollableDirections() {
-    const currentScroll = this.getCurrentScroll()
-    const maximumScroll = this.getMaximumScroll()
-
-    return {
-      backwards: (currentScroll > 0),
-      forwards: (currentScroll < maximumScroll)
-    }
-  }
-
-  canScrollAtAll() {
-    const {backwards, forwards} = this.getScrollableDirections()
-    return backwards || forwards
-  }
-}
diff --git a/ui/form/TextInput.js b/ui/form/TextInput.js
deleted file mode 100644
index 78d3b6d..0000000
--- a/ui/form/TextInput.js
+++ /dev/null
@@ -1,145 +0,0 @@
-const ansi = require('../../util/ansi')
-const unic = require('../../util/unichars')
-const telc = require('../../util/telchars')
-
-const FocusElement = require('./FocusElement')
-
-module.exports = class TextInput extends FocusElement {
-  // An element that the user can type in.
-
-  constructor() {
-    super()
-
-    this.value = ''
-    this.cursorVisible = true
-    this.cursorIndex = 0
-    this.scrollChars = 0
-  }
-
-  drawTo(writable) {
-    // There should be room for the cursor so move the "right edge" left a
-    // single character.
-
-    const startRange = this.scrollChars
-    const endRange = this.scrollChars + this.w - 3
-
-    let str = this.value.slice(startRange, endRange)
-
-    writable.write(ansi.moveCursor(this.absTop, this.absLeft + 1))
-    writable.write(str)
-
-    // Ellipsis on left side, if there's more characters behind the visible
-    // area.
-    if (startRange > 0) {
-      writable.write(ansi.moveCursor(this.absTop, this.absLeft))
-      writable.write(unic.ELLIPSIS)
-    }
-
-    // Ellipsis on the right side, if there's more characters ahead of the
-    // visible area.
-    if (endRange < this.value.length) {
-      writable.write(ansi.moveCursor(this.absTop, this.absRight - 1))
-      writable.write(unic.ELLIPSIS.repeat(2))
-    }
-
-    this.cursorX = this.cursorIndex - this.scrollChars + 1
-
-    super.drawTo(writable)
-  }
-
-  keyPressed(keyBuf) {
-    try {
-      if (keyBuf[0] === 127) {
-        this.value = (
-          this.value.slice(0, this.cursorIndex - 1) +
-          this.value.slice(this.cursorIndex)
-        )
-        this.cursorIndex--
-        this.root.cursorMoved()
-        return false
-      } else if (keyBuf[0] === 13) {
-        // These are aliases for each other.
-        this.emit('value', this.value)
-        this.emit('confirm', this.value)
-      } else if (keyBuf[0] === 0x1b && keyBuf[1] === 0x5b) {
-        // Keyboard navigation
-        if (keyBuf[2] === 0x44) {
-          this.cursorIndex--
-          this.root.cursorMoved()
-        } else if (keyBuf[2] === 0x43) {
-          this.cursorIndex++
-          this.root.cursorMoved()
-        }
-        return false
-      } else if (telc.isEscape(keyBuf)) {
-        // ESC is bad and we don't want that in the text input!
-        // Also emit a "cancel" event, which doesn't necessarily do anything,
-        // but can be listened to.
-        this.emit('cancel')
-      } else {
-        const isTextInput = keyBuf.toString().split('').every(chr => {
-          const n = chr.charCodeAt(0)
-          return n > 31 && n < 127
-        })
-
-        if (isTextInput) {
-          this.value = (
-            this.value.slice(0, this.cursorIndex) + keyBuf.toString() +
-            this.value.slice(this.cursorIndex)
-          )
-          this.cursorIndex += keyBuf.toString().length
-          this.root.cursorMoved()
-          this.emit('change', this.value)
-
-          return false
-        }
-      }
-    } finally {
-      this.keepCursorInRange()
-    }
-  }
-
-  setValue(value) {
-    this.value = value
-    this.moveToEnd()
-  }
-
-  moveToEnd() {
-    this.cursorIndex = this.value.length
-    this.keepCursorInRange()
-  }
-
-  keepCursorInRange() {
-    // Keep the cursor inside or at the end of the input value.
-
-    if (this.cursorIndex < 0) {
-      this.cursorIndex = 0
-    }
-
-    if (this.cursorIndex > this.value.length) {
-      this.cursorIndex = this.value.length
-    }
-
-    // Scroll right, if the cursor is past the right edge of where text is
-    // displayed.
-    while (this.cursorIndex - this.scrollChars > this.w - 3) {
-      this.scrollChars++
-    }
-
-    // Scroll left, if the cursor is behind the left edge of where text is
-    // displayed.
-    while (this.cursorIndex - this.scrollChars < 0) {
-      this.scrollChars--
-    }
-
-    // Scroll left, if we can see past the end of the text.
-    while (this.scrollChars > 0 && (
-      this.scrollChars + this.w - 3 > this.value.length)
-    ) {
-      this.scrollChars--
-    }
-  }
-
-  get value() { return this.getDep('value') }
-  set value(v) { return this.setDep('value', v) }
-}