mrmd-monitor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Terminal Buffer for mrmd-monitor
3
+ *
4
+ * Processes streaming terminal output with proper cursor movement and ANSI handling.
5
+ * Enables progress bars (tqdm, rich) to display correctly during execution.
6
+ *
7
+ * This is a simplified version of mrmd-editor's terminal.js for Node.js usage.
8
+ * Output is processed to plain text for document storage.
9
+ *
10
+ * @module mrmd-monitor/terminal
11
+ */
12
+
13
+ /**
14
+ * Terminal buffer that processes cursor movement and ANSI codes
15
+ */
16
+ export class TerminalBuffer {
17
+ constructor() {
18
+ /** @type {string[][]} Lines of characters */
19
+ this._lines = [[]];
20
+ /** @type {number} Current row */
21
+ this._row = 0;
22
+ /** @type {number} Current column */
23
+ this._col = 0;
24
+ /** @type {{row: number, col: number}|null} Saved cursor position */
25
+ this._savedCursor = null;
26
+ }
27
+
28
+ /**
29
+ * Process terminal output and write to buffer
30
+ * @param {string} text - Raw terminal output with escape sequences
31
+ */
32
+ write(text) {
33
+ let i = 0;
34
+
35
+ while (i < text.length) {
36
+ // Check for escape sequence
37
+ if (text[i] === '\x1b' && text[i + 1] === '[') {
38
+ i = this._parseEscapeSequence(text, i);
39
+ continue;
40
+ }
41
+
42
+ // Handle special characters
43
+ const char = text[i];
44
+
45
+ if (char === '\r') {
46
+ // Carriage return - back to start of line
47
+ this._col = 0;
48
+ } else if (char === '\n') {
49
+ // Newline - next line, column 0
50
+ this._row++;
51
+ this._col = 0;
52
+ this._ensureRow(this._row);
53
+ } else if (char === '\b') {
54
+ // Backspace
55
+ this._col = Math.max(0, this._col - 1);
56
+ } else if (char === '\t') {
57
+ // Tab - move to next 8-column boundary
58
+ this._col = Math.floor(this._col / 8) * 8 + 8;
59
+ } else if (char.charCodeAt(0) >= 32) {
60
+ // Printable character
61
+ this._writeChar(char);
62
+ }
63
+ // Ignore other control characters
64
+
65
+ i++;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Parse an escape sequence starting at position i
71
+ * @param {string} text
72
+ * @param {number} i
73
+ * @returns {number} Next index
74
+ */
75
+ _parseEscapeSequence(text, i) {
76
+ // Skip \x1b[
77
+ let j = i + 2;
78
+
79
+ // Check for DEC private mode prefix '?'
80
+ const isPrivateMode = text[j] === '?';
81
+ if (isPrivateMode) j++;
82
+
83
+ // Collect parameter bytes (digits and semicolons)
84
+ let params = '';
85
+ while (j < text.length && /[0-9;]/.test(text[j])) {
86
+ params += text[j];
87
+ j++;
88
+ }
89
+
90
+ // Get command byte
91
+ const cmd = text[j] || '';
92
+ j++;
93
+
94
+ // Ignore DEC private modes and SGR (colors/styles) - we output plain text
95
+ if (isPrivateMode || cmd === 'm') {
96
+ return j;
97
+ }
98
+
99
+ // Parse parameter numbers
100
+ const nums = params ? params.split(';').map(n => parseInt(n) || 0) : [];
101
+ const n = nums[0] || 1;
102
+
103
+ switch (cmd) {
104
+ case 'A': // Cursor Up
105
+ this._row = Math.max(0, this._row - n);
106
+ break;
107
+
108
+ case 'B': // Cursor Down
109
+ this._row += n;
110
+ this._ensureRow(this._row);
111
+ break;
112
+
113
+ case 'C': // Cursor Forward (Right)
114
+ this._col += n;
115
+ break;
116
+
117
+ case 'D': // Cursor Back (Left)
118
+ this._col = Math.max(0, this._col - n);
119
+ break;
120
+
121
+ case 'E': // Cursor Next Line
122
+ this._row += n;
123
+ this._col = 0;
124
+ this._ensureRow(this._row);
125
+ break;
126
+
127
+ case 'F': // Cursor Previous Line
128
+ this._row = Math.max(0, this._row - n);
129
+ this._col = 0;
130
+ break;
131
+
132
+ case 'G': // Cursor Horizontal Absolute
133
+ this._col = Math.max(0, n - 1);
134
+ break;
135
+
136
+ case 'H': // Cursor Position (row;col)
137
+ case 'f':
138
+ this._row = Math.max(0, (nums[0] || 1) - 1);
139
+ this._col = Math.max(0, (nums[1] || 1) - 1);
140
+ this._ensureRow(this._row);
141
+ break;
142
+
143
+ case 'J': // Erase in Display
144
+ if (n === 0 || params === '') {
145
+ this._clearToEndOfScreen();
146
+ } else if (n === 1) {
147
+ this._clearFromStartOfScreen();
148
+ } else if (n === 2 || n === 3) {
149
+ this._clearScreen();
150
+ }
151
+ break;
152
+
153
+ case 'K': // Erase in Line
154
+ if (n === 0 || params === '') {
155
+ this._clearToEndOfLine();
156
+ } else if (n === 1) {
157
+ this._clearFromStartOfLine();
158
+ } else if (n === 2) {
159
+ this._clearLine();
160
+ }
161
+ break;
162
+
163
+ case 's': // Save Cursor Position
164
+ this._savedCursor = { row: this._row, col: this._col };
165
+ break;
166
+
167
+ case 'u': // Restore Cursor Position
168
+ if (this._savedCursor) {
169
+ this._row = this._savedCursor.row;
170
+ this._col = this._savedCursor.col;
171
+ }
172
+ break;
173
+ }
174
+
175
+ return j;
176
+ }
177
+
178
+ /**
179
+ * Write a character at current cursor position
180
+ * @param {string} char
181
+ */
182
+ _writeChar(char) {
183
+ this._ensureRow(this._row);
184
+ const line = this._lines[this._row];
185
+
186
+ // Extend line if needed
187
+ while (line.length <= this._col) {
188
+ line.push(' ');
189
+ }
190
+
191
+ // Write character
192
+ line[this._col] = char;
193
+ this._col++;
194
+ }
195
+
196
+ /** @param {number} row */
197
+ _ensureRow(row) {
198
+ while (this._lines.length <= row) {
199
+ this._lines.push([]);
200
+ }
201
+ }
202
+
203
+ _clearToEndOfLine() {
204
+ if (this._lines[this._row]) {
205
+ this._lines[this._row] = this._lines[this._row].slice(0, this._col);
206
+ }
207
+ }
208
+
209
+ _clearFromStartOfLine() {
210
+ if (this._lines[this._row]) {
211
+ const line = this._lines[this._row];
212
+ for (let i = 0; i <= this._col && i < line.length; i++) {
213
+ line[i] = ' ';
214
+ }
215
+ }
216
+ }
217
+
218
+ _clearLine() {
219
+ this._lines[this._row] = [];
220
+ }
221
+
222
+ _clearToEndOfScreen() {
223
+ this._clearToEndOfLine();
224
+ for (let r = this._row + 1; r < this._lines.length; r++) {
225
+ this._lines[r] = [];
226
+ }
227
+ }
228
+
229
+ _clearFromStartOfScreen() {
230
+ for (let r = 0; r < this._row; r++) {
231
+ this._lines[r] = [];
232
+ }
233
+ this._clearFromStartOfLine();
234
+ }
235
+
236
+ _clearScreen() {
237
+ this._lines = [[]];
238
+ this._row = 0;
239
+ this._col = 0;
240
+ }
241
+
242
+ /**
243
+ * Convert buffer to plain text (for document storage)
244
+ * @returns {string}
245
+ */
246
+ toString() {
247
+ const output = [];
248
+
249
+ for (const line of this._lines) {
250
+ let lineText = line.join('');
251
+ // Trim trailing spaces
252
+ output.push(lineText.trimEnd());
253
+ }
254
+
255
+ // Trim trailing empty lines
256
+ while (output.length > 0 && output[output.length - 1] === '') {
257
+ output.pop();
258
+ }
259
+
260
+ return output.join('\n');
261
+ }
262
+
263
+ /**
264
+ * Clear the buffer and reset cursor
265
+ */
266
+ clear() {
267
+ this._lines = [[]];
268
+ this._row = 0;
269
+ this._col = 0;
270
+ this._savedCursor = null;
271
+ }
272
+
273
+ /**
274
+ * Get current line count
275
+ * @returns {number}
276
+ */
277
+ get lineCount() {
278
+ return this._lines.length;
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Process terminal output through a buffer
284
+ *
285
+ * Convenience function for one-shot processing.
286
+ *
287
+ * @param {string} text - Raw terminal output
288
+ * @returns {string} - Processed plain text
289
+ */
290
+ export function processTerminalOutput(text) {
291
+ const buffer = new TerminalBuffer();
292
+ buffer.write(text);
293
+ return buffer.toString();
294
+ }