CommandLineInterface.js 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import EventEmitter from 'node:events'
  2. import * as ansi from '../ansi.js'
  3. import waitForData from '../waitForData.js'
  4. export default class CommandLineInterface extends EventEmitter {
  5. constructor(inStream = process.stdin, outStream = process.stdout, proc = process) {
  6. super()
  7. this.inStream = inStream
  8. this.outStream = outStream
  9. this.process = proc
  10. inStream.on('data', buffer => {
  11. this.emit('inputData', buffer)
  12. })
  13. inStream.setRawMode(true)
  14. proc.on('SIGWINCH', async buffer => {
  15. this.emit('resize', await this.getScreenSize())
  16. })
  17. }
  18. async getScreenSize() {
  19. const waitUntil = cond => waitForData(this.inStream, cond)
  20. // Get old cursor position..
  21. this.outStream.write(ansi.requestCursorPosition())
  22. const { options: oldCoords } = this.parseANSICommand(
  23. await waitUntil(buf => ansi.isANSICommand(buf, 82))
  24. )
  25. // Move far to the bottom right of the screen, then get cursor position..
  26. // (We could use moveCursor here, but the 0-index offset isn't really
  27. // relevant.)
  28. this.outStream.write(ansi.moveCursorRaw(9999, 9999))
  29. this.outStream.write(ansi.requestCursorPosition())
  30. const { options: sizeCoords } = this.parseANSICommand(
  31. await waitUntil(buf => ansi.isANSICommand(buf, 82))
  32. )
  33. // Restore to old cursor position.. (Using moveCursorRaw is actaully
  34. // necessary here, since we'll be passing the coordinates returned from
  35. // another ANSI command.)
  36. this.outStream.write(ansi.moveCursorRaw(oldCoords[0], oldCoords[1]))
  37. // And return dimensions.
  38. const [ sizeLine, sizeCol ] = sizeCoords
  39. return {
  40. lines: sizeLine, cols: sizeCol,
  41. width: sizeCol, height: sizeLine
  42. }
  43. }
  44. parseANSICommand(buffer) {
  45. // Typically ANSI commands are written ESC[1;2;3;4C
  46. // ..where ESC is the ANSI escape code, equal to hexadecimal 1B and
  47. // decimal 33
  48. // ..where [ and ; are the literal strings "[" and ";"
  49. // ..where 1, 2, 3, and 4 are decimal integer arguments written in ASCII
  50. // that may last more than one byte (e.g. "15")
  51. // ..where C is some number representing the code of the command
  52. if (buffer[0] !== 0x1b || buffer[1] !== 0x5b) {
  53. throw new Error('Not an ANSI command')
  54. }
  55. const options = []
  56. let curOption = ''
  57. let commandCode = null
  58. for (const val of buffer.slice(2)) {
  59. if (48 <= val && val <= 57) { // 0124356789
  60. curOption = curOption.concat(val - 48)
  61. } else {
  62. options.push(parseInt(curOption))
  63. curOption = ''
  64. if (val !== 59) { // ;
  65. commandCode = val
  66. break
  67. }
  68. }
  69. }
  70. return {code: commandCode, options: options}
  71. }
  72. write(data) {
  73. this.outStream.write(data)
  74. }
  75. }