1'use strict'; 2 3const { 4 ArrayPrototypeJoin, 5 ArrayPrototypePush, 6 Promise, 7} = primordials; 8 9const { CSI } = require('internal/readline/utils'); 10const { validateBoolean, validateInteger } = require('internal/validators'); 11const { isWritable } = require('internal/streams/utils'); 12const { codes: { ERR_INVALID_ARG_TYPE } } = require('internal/errors'); 13 14const { 15 kClearToLineBeginning, 16 kClearToLineEnd, 17 kClearLine, 18 kClearScreenDown, 19} = CSI; 20 21class Readline { 22 #autoCommit = false; 23 #stream; 24 #todo = []; 25 26 constructor(stream, options = undefined) { 27 if (!isWritable(stream)) 28 throw new ERR_INVALID_ARG_TYPE('stream', 'Writable', stream); 29 this.#stream = stream; 30 if (options?.autoCommit != null) { 31 validateBoolean(options.autoCommit, 'options.autoCommit'); 32 this.#autoCommit = options.autoCommit; 33 } 34 } 35 36 /** 37 * Moves the cursor to the x and y coordinate on the given stream. 38 * @param {integer} x 39 * @param {integer} [y] 40 * @returns {Readline} this 41 */ 42 cursorTo(x, y = undefined) { 43 validateInteger(x, 'x'); 44 if (y != null) validateInteger(y, 'y'); 45 46 const data = y == null ? CSI`${x + 1}G` : CSI`${y + 1};${x + 1}H`; 47 if (this.#autoCommit) process.nextTick(() => this.#stream.write(data)); 48 else ArrayPrototypePush(this.#todo, data); 49 50 return this; 51 } 52 53 /** 54 * Moves the cursor relative to its current location. 55 * @param {integer} dx 56 * @param {integer} dy 57 * @returns {Readline} this 58 */ 59 moveCursor(dx, dy) { 60 if (dx || dy) { 61 validateInteger(dx, 'dx'); 62 validateInteger(dy, 'dy'); 63 64 let data = ''; 65 66 if (dx < 0) { 67 data += CSI`${-dx}D`; 68 } else if (dx > 0) { 69 data += CSI`${dx}C`; 70 } 71 72 if (dy < 0) { 73 data += CSI`${-dy}A`; 74 } else if (dy > 0) { 75 data += CSI`${dy}B`; 76 } 77 if (this.#autoCommit) process.nextTick(() => this.#stream.write(data)); 78 else ArrayPrototypePush(this.#todo, data); 79 } 80 return this; 81 } 82 83 /** 84 * Clears the current line the cursor is on. 85 * @param {-1|0|1} dir Direction to clear: 86 * -1 for left of the cursor 87 * +1 for right of the cursor 88 * 0 for the entire line 89 * @returns {Readline} this 90 */ 91 clearLine(dir) { 92 validateInteger(dir, 'dir', -1, 1); 93 94 const data = 95 dir < 0 ? kClearToLineBeginning : 96 dir > 0 ? kClearToLineEnd : 97 kClearLine; 98 if (this.#autoCommit) process.nextTick(() => this.#stream.write(data)); 99 else ArrayPrototypePush(this.#todo, data); 100 return this; 101 } 102 103 /** 104 * Clears the screen from the current position of the cursor down. 105 * @returns {Readline} this 106 */ 107 clearScreenDown() { 108 if (this.#autoCommit) { 109 process.nextTick(() => this.#stream.write(kClearScreenDown)); 110 } else { 111 ArrayPrototypePush(this.#todo, kClearScreenDown); 112 } 113 return this; 114 } 115 116 /** 117 * Sends all the pending actions to the associated `stream` and clears the 118 * internal list of pending actions. 119 * @returns {Promise<void>} Resolves when all pending actions have been 120 * flushed to the associated `stream`. 121 */ 122 commit() { 123 return new Promise((resolve) => { 124 this.#stream.write(ArrayPrototypeJoin(this.#todo, ''), resolve); 125 this.#todo = []; 126 }); 127 } 128 129 /** 130 * Clears the internal list of pending actions without sending it to the 131 * associated `stream`. 132 * @returns {Readline} this 133 */ 134 rollback() { 135 this.#todo = []; 136 return this; 137 } 138} 139 140module.exports = { 141 Readline, 142}; 143