« get me outta code hell

Root.js « ui - 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/Root.js
blob: a6b3acfcec85de4b22723c92de98d864e13c4617 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
const ansi = require('../util/ansi')

const DisplayElement = require('./DisplayElement')

const FocusElement = require('./form/FocusElement')

module.exports = class Root extends DisplayElement {
  // An element to be used as the root of a UI. Handles lots of UI and
  // socket stuff.

  constructor(interfacer) {
    super()

    this.interfacer = interfacer

    this.selectedElement = null

    this.cursorBlinkOffset = Date.now()

    interfacer.on('inputData', buf => this.handleData(buf))
  }

  render() {
    this.renderTo(this.interfacer)
  }

  handleData(buffer) {
    if (this.selectedElement) {
      const els = [
        ...this.selectedElement.directAncestors, this.selectedElement]
      for (const el of els) {
        if (el instanceof FocusElement) {
          const shouldBreak = (el.keyPressed(buffer) === false)
          if (shouldBreak) {
            break
          }
          el.emit('keypressed', buffer)
        }
      }
    }
  }

  drawTo(writable) {
    writable.write(ansi.moveCursor(0, 0))
    writable.write(' '.repeat(this.w * this.h))
  }

  didRenderTo(writable) {
    // Render the cursor, based on the cursorX and cursorY of the currently
    // selected element.
    if (this.selectedElement && this.selectedElement.cursorVisible) {
      if ((Date.now() - this.cursorBlinkOffset) % 1000 < 500) {
        writable.write(ansi.moveCursor(
          this.selectedElement.absCursorY, this.selectedElement.absCursorX))
        writable.write(ansi.invert())
        writable.write('I')
        writable.write(ansi.resetAttributes())
      }

      writable.write(ansi.showCursor())
      writable.write(ansi.moveCursor(
        this.selectedElement.absCursorY, this.selectedElement.absCursorX))
    } else {
      writable.write(ansi.hideCursor())
    }
  }

  cursorMoved() {
    // Resets the blinking animation for the cursor. Call this whenever you
    // move the cursor.

    this.cursorBlinkOffset = Date.now()
  }

  select(el) {
    // Select an element. Calls the unfocus method on the already-selected
    // element, if there is one.

    const oldSelected = this.selectedElement
    const newSelected = el

    // Relevant elements that COULD have their "isSelected" state change.
    // We ignore elements where isSelected is undefined, because they aren't
    // build to handle being selected, and they break the compare-old-and-new-
    // state code below.
    const relevantElements = [
      ...(oldSelected ? [...oldSelected.directAncestors, oldSelected] : []),
      ...(newSelected ? newSelected.directAncestors : [])
    ].filter(el => typeof el.isSelected !== 'undefined')

    const oldStates = relevantElements.map(el => [el, el.isSelected])

    this.selectedElement = el

    // Same stuff as in the for loop below. We always call selected() on the
    // passed element, even if it was already selected before.
    if (el.selected) el.selected()
    if (typeof el.focused === 'function') el.focused()
    if (this.selectedElement !== newSelected) return

    // Compare the old "isSelected" state of every relevant element with their
    // current "isSelected" state, and call the respective selected/unselected
    // functions. (Also call focused and unfocused for some sense of trying to
    // not break old programs, but, like, old programs are going to be broken
    // anyways.)
    for (const [ el, wasSelected ] of oldStates) {
      const { isSelected } = el
      if (isSelected && !wasSelected) {
        if (el.selected) el.selected()
        if (typeof el.focused === 'function') el.focused()
      } else if (wasSelected && !isSelected) {
        if (el.unselected) el.unselected()
        if (typeof el.unfocused === 'function') el.unfocused()
      }

      // If the (un)selected() handler actually selected a different element
      // itself, then further processing of new selected states is irrelevant,
      // so stop here. (We return instead of breaking the for loop because
      // anything after this loop would have already been handled by the call
      // to Root.select() from the (un)selected() handler.)
      if (this.selectedElement !== newSelected) {
        return
      }
    }

    this.cursorMoved()
  }

  isChildOrSelfSelected(el) {
    if (!this.selectedElement) return false
    if (this.selectedElement === el) return true
    if (this.selectedElement.directAncestors.includes(el)) return true
    return false
  }
}