« get me outta code hell

CommandLineInterface.js « interfaces « util - tui-lib - Pure Node.js library for making visual command-line programs (ala vim, ncdu)
about summary refs log tree commit diff
path: root/util/interfaces/CommandLineInterface.js
blob: 66c8c43a342c2ccac870af4ce1f1ab812edaedc7 (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
import EventEmitter from 'node:events'

import * as ansi from '../ansi.js'
import waitForData from '../waitForData.js'

export default class CommandLineInterface extends EventEmitter {
  constructor(inStream = process.stdin, outStream = process.stdout, proc = process) {
    super()

    this.inStream = inStream
    this.outStream = outStream
    this.process = proc

    inStream.on('data', buffer => {
      this.emit('inputData', buffer)
    })

    inStream.setRawMode(true)

    proc.on('SIGWINCH', async buffer => {
      this.emit('resize', await this.getScreenSize())
    })
  }

  async getScreenSize() {
    const waitUntil = cond => waitForData(this.inStream, cond)

    // Get old cursor position..
    this.outStream.write(ansi.requestCursorPosition())
    const { options: oldCoords } = this.parseANSICommand(
      await waitUntil(buf => ansi.isANSICommand(buf, 82))
    )

    // Move far to the bottom right of the screen, then get cursor position..
    // (We could use moveCursor here, but the 0-index offset isn't really
    // relevant.)
    this.outStream.write(ansi.moveCursorRaw(9999, 9999))
    this.outStream.write(ansi.requestCursorPosition())
    const { options: sizeCoords } = this.parseANSICommand(
      await waitUntil(buf => ansi.isANSICommand(buf, 82))
    )

    // Restore to old cursor position.. (Using moveCursorRaw is actaully
    // necessary here, since we'll be passing the coordinates returned from
    // another ANSI command.)
    this.outStream.write(ansi.moveCursorRaw(oldCoords[0], oldCoords[1]))

    // And return dimensions.
    const [ sizeLine, sizeCol ] = sizeCoords
    return {
      lines: sizeLine, cols: sizeCol,
      width: sizeCol, height: sizeLine
    }
  }

  parseANSICommand(buffer) {
    // Typically ANSI commands are written ESC[1;2;3;4C
    // ..where ESC is the ANSI escape code, equal to hexadecimal 1B and
    //   decimal 33
    // ..where [ and ; are the literal strings "[" and ";"
    // ..where 1, 2, 3, and 4 are decimal integer arguments written in ASCII
    //   that may last more than one byte (e.g. "15")
    // ..where C is some number representing the code of the command

    if (buffer[0] !== 0x1b || buffer[1] !== 0x5b) {
      throw new Error('Not an ANSI command')
    }

    const options = []
    let curOption = ''
    let commandCode = null
    for (const val of buffer.slice(2)) {
      if (48 <= val && val <= 57) { // 0124356789
        curOption = curOption.concat(val - 48)
      } else {
        options.push(parseInt(curOption))
        curOption = ''

        if (val !== 59) { // ;
          commandCode = val
          break
        }
      }
    }

    return {code: commandCode, options: options}
  }

  write(data) {
    this.outStream.write(data)
  }
}