table-viewer 0.0.2 → 0.0.3-beta.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.
- package/index.js +188 -23
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -38,6 +38,8 @@ let consoleWidth = process.stdout.columns
|
|
|
38
38
|
let consoleHeight = process.stdout.rows
|
|
39
39
|
let xOffset = 0
|
|
40
40
|
let yOffset = 0
|
|
41
|
+
let mouseSequenceBuffer = ''
|
|
42
|
+
let didCleanupTerminal = false
|
|
41
43
|
|
|
42
44
|
if (!consoleTableOutputSplit.length) process.exit(0)
|
|
43
45
|
|
|
@@ -46,10 +48,26 @@ const consoleTableOutputHeight = consoleTableOutputSplit.length
|
|
|
46
48
|
|
|
47
49
|
// Enter alternate screen buffer and hide cursor
|
|
48
50
|
process.stdout.write('\x1B[?1049h\x1B[?25l')
|
|
51
|
+
enableMouseReporting()
|
|
49
52
|
|
|
50
53
|
// Restore cursor and leave alternate screen buffer on exit
|
|
51
54
|
process.on('exit', () => {
|
|
52
|
-
|
|
55
|
+
cleanupTerminal()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
process.on('SIGINT', () => {
|
|
59
|
+
process.exit(0)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
process.on('unhandledRejection', (error) => {
|
|
63
|
+
cleanupTerminal()
|
|
64
|
+
throw error
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
process.on('uncaughtException', (error) => {
|
|
68
|
+
cleanupTerminal()
|
|
69
|
+
console.error(error)
|
|
70
|
+
process.exit(1)
|
|
53
71
|
})
|
|
54
72
|
|
|
55
73
|
process.stdout.on('resize', () => {
|
|
@@ -64,31 +82,24 @@ process.stdout.on('resize', () => {
|
|
|
64
82
|
let pendingG = false
|
|
65
83
|
|
|
66
84
|
ttyIn.on('keypress', (_str, key) => {
|
|
85
|
+
// Ignore mouse sequences that readline may emit as unrecognized keypresses
|
|
86
|
+
if (key.sequence?.startsWith('\x1b[<')) return
|
|
87
|
+
|
|
67
88
|
// Ctrl+C or q to exit
|
|
68
89
|
if ((key.ctrl && key.name === 'c') || key.name === 'q') {
|
|
69
90
|
process.exit(0)
|
|
70
91
|
}
|
|
71
92
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
const maxXOffset =
|
|
75
|
-
0,
|
|
76
|
-
consoleTableOutputWidth - consoleWidth + (needsScrollbarY ? 1 : 0),
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
const maxYOffset = Math.max(
|
|
80
|
-
0,
|
|
81
|
-
consoleTableOutputHeight - consoleHeight + (needsScrollbarX ? 1 : 0),
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
const visibleHeight = consoleHeight - (needsScrollbarX ? 1 : 0)
|
|
93
|
+
const prevX = xOffset
|
|
94
|
+
const prevY = yOffset
|
|
95
|
+
const { maxXOffset, maxYOffset, visibleHeight } = getViewportMetrics()
|
|
85
96
|
|
|
86
97
|
// Handle gg (go to top)
|
|
87
98
|
if (key.name === 'g' && !key.ctrl && !key.shift) {
|
|
88
99
|
if (pendingG) {
|
|
89
100
|
pendingG = false
|
|
90
101
|
yOffset = 0
|
|
91
|
-
render()
|
|
102
|
+
if (yOffset !== prevY) render()
|
|
92
103
|
return
|
|
93
104
|
}
|
|
94
105
|
|
|
@@ -101,7 +112,7 @@ ttyIn.on('keypress', (_str, key) => {
|
|
|
101
112
|
pendingG = false
|
|
102
113
|
yOffset = maxYOffset
|
|
103
114
|
|
|
104
|
-
render()
|
|
115
|
+
if (yOffset !== prevY) render()
|
|
105
116
|
return
|
|
106
117
|
}
|
|
107
118
|
|
|
@@ -112,21 +123,21 @@ ttyIn.on('keypress', (_str, key) => {
|
|
|
112
123
|
// -- vertical movement --
|
|
113
124
|
case 'k':
|
|
114
125
|
case 'up':
|
|
115
|
-
|
|
126
|
+
moveUp(key.shift ? 10 : 5)
|
|
116
127
|
break
|
|
117
128
|
case 'j':
|
|
118
129
|
case 'down':
|
|
119
|
-
|
|
130
|
+
moveDown(key.shift ? 10 : 5)
|
|
120
131
|
break
|
|
121
132
|
|
|
122
133
|
// -- horizontal movement --
|
|
123
134
|
case 'h':
|
|
124
135
|
case 'left':
|
|
125
|
-
|
|
136
|
+
moveLeft(key.shift ? 10 : 5)
|
|
126
137
|
break
|
|
127
138
|
case 'l':
|
|
128
139
|
case 'right':
|
|
129
|
-
|
|
140
|
+
moveRight(key.shift ? 10 : 5)
|
|
130
141
|
break
|
|
131
142
|
|
|
132
143
|
// Ctrl+d — half page down
|
|
@@ -165,8 +176,8 @@ ttyIn.on('keypress', (_str, key) => {
|
|
|
165
176
|
xOffset = 0
|
|
166
177
|
break
|
|
167
178
|
// $ — scroll to rightmost
|
|
168
|
-
case
|
|
169
|
-
if (key.
|
|
179
|
+
case undefined:
|
|
180
|
+
if (key.sequence === '$') {
|
|
170
181
|
xOffset = maxXOffset
|
|
171
182
|
} else {
|
|
172
183
|
return
|
|
@@ -177,11 +188,165 @@ ttyIn.on('keypress', (_str, key) => {
|
|
|
177
188
|
return
|
|
178
189
|
}
|
|
179
190
|
|
|
180
|
-
render()
|
|
191
|
+
if (xOffset !== prevX || yOffset !== prevY) render()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
ttyIn.on('data', (chunk) => {
|
|
195
|
+
mouseSequenceBuffer += chunk.toString('utf8')
|
|
196
|
+
processMouseBuffer()
|
|
181
197
|
})
|
|
182
198
|
|
|
183
199
|
render()
|
|
184
200
|
|
|
201
|
+
function processMouseBuffer() {
|
|
202
|
+
while (mouseSequenceBuffer.length) {
|
|
203
|
+
const mouseStart = mouseSequenceBuffer.indexOf('\x1b[<')
|
|
204
|
+
|
|
205
|
+
if (mouseStart === -1) {
|
|
206
|
+
mouseSequenceBuffer = mouseSequenceBuffer.endsWith('\x1b') ? '\x1b' : ''
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (mouseStart > 0) {
|
|
211
|
+
mouseSequenceBuffer = mouseSequenceBuffer.slice(mouseStart)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const match = mouseSequenceBuffer.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])/)
|
|
215
|
+
|
|
216
|
+
if (!match) {
|
|
217
|
+
if (/^\x1b\[<[\d;]*$/.test(mouseSequenceBuffer)) {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
mouseSequenceBuffer = mouseSequenceBuffer.slice(1)
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
handleMouseButton(
|
|
226
|
+
Number.parseInt(match[1], 10),
|
|
227
|
+
/** @type {'M' | 'm'} */ (match[4]),
|
|
228
|
+
)
|
|
229
|
+
mouseSequenceBuffer = mouseSequenceBuffer.slice(match[0].length)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @param {number} buttonCode
|
|
235
|
+
* @param {'M' | 'm'} action
|
|
236
|
+
*/
|
|
237
|
+
function handleMouseButton(buttonCode, action) {
|
|
238
|
+
// Wheel events should be press-only. Ignore release-style reports to avoid jitter.
|
|
239
|
+
if (action !== 'M') return
|
|
240
|
+
|
|
241
|
+
if ((buttonCode & 64) === 0) return
|
|
242
|
+
|
|
243
|
+
const wheelButton = buttonCode & 3
|
|
244
|
+
const isShiftPressed = (buttonCode & 4) !== 0
|
|
245
|
+
|
|
246
|
+
let moved = false
|
|
247
|
+
|
|
248
|
+
switch (wheelButton) {
|
|
249
|
+
case 0: // wheel up
|
|
250
|
+
moved = isShiftPressed ? moveRight(5) : moveUp(5)
|
|
251
|
+
break
|
|
252
|
+
case 1: // wheel down
|
|
253
|
+
moved = isShiftPressed ? moveLeft(5) : moveDown(5)
|
|
254
|
+
break
|
|
255
|
+
case 2: // wheel right (native horizontal)
|
|
256
|
+
moved = moveRight(5)
|
|
257
|
+
break
|
|
258
|
+
case 3: // wheel left (native horizontal)
|
|
259
|
+
moved = moveLeft(5)
|
|
260
|
+
break
|
|
261
|
+
default:
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (moved) render()
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getViewportMetrics() {
|
|
269
|
+
const { needsScrollbarX, needsScrollbarY } = computeScrollbarVisibility()
|
|
270
|
+
|
|
271
|
+
const maxXOffset = Math.max(
|
|
272
|
+
0,
|
|
273
|
+
consoleTableOutputWidth - consoleWidth + (needsScrollbarY ? 1 : 0),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
const maxYOffset = Math.max(
|
|
277
|
+
0,
|
|
278
|
+
consoleTableOutputHeight - consoleHeight + (needsScrollbarX ? 1 : 0),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
const visibleHeight = consoleHeight - (needsScrollbarX ? 1 : 0)
|
|
282
|
+
|
|
283
|
+
return { maxXOffset, maxYOffset, visibleHeight }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @param {number} amount
|
|
288
|
+
* @returns {boolean} whether the offset changed
|
|
289
|
+
*/
|
|
290
|
+
function moveUp(amount) {
|
|
291
|
+
const prev = yOffset
|
|
292
|
+
yOffset = Math.max(0, yOffset - amount)
|
|
293
|
+
return yOffset !== prev
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @param {number} amount
|
|
298
|
+
* @returns {boolean} whether the offset changed
|
|
299
|
+
*/
|
|
300
|
+
function moveDown(amount) {
|
|
301
|
+
const prev = yOffset
|
|
302
|
+
const { maxYOffset } = getViewportMetrics()
|
|
303
|
+
yOffset = Math.min(maxYOffset, yOffset + amount)
|
|
304
|
+
return yOffset !== prev
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* @param {number} amount
|
|
309
|
+
* @returns {boolean} whether the offset changed
|
|
310
|
+
*/
|
|
311
|
+
function moveLeft(amount) {
|
|
312
|
+
const prev = xOffset
|
|
313
|
+
xOffset = Math.max(0, xOffset - amount)
|
|
314
|
+
return xOffset !== prev
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @param {number} amount
|
|
319
|
+
* @returns {boolean} whether the offset changed
|
|
320
|
+
*/
|
|
321
|
+
function moveRight(amount) {
|
|
322
|
+
const prev = xOffset
|
|
323
|
+
const { maxXOffset } = getViewportMetrics()
|
|
324
|
+
xOffset = Math.min(maxXOffset, xOffset + amount)
|
|
325
|
+
return xOffset !== prev
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function enableMouseReporting() {
|
|
329
|
+
process.stdout.write('\x1B[?1000h\x1B[?1006h')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function disableMouseReporting() {
|
|
333
|
+
process.stdout.write('\x1B[?1000l\x1B[?1006l')
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function cleanupTerminal() {
|
|
337
|
+
if (didCleanupTerminal) return
|
|
338
|
+
|
|
339
|
+
didCleanupTerminal = true
|
|
340
|
+
|
|
341
|
+
disableMouseReporting()
|
|
342
|
+
|
|
343
|
+
if (ttyIn.isRaw) {
|
|
344
|
+
ttyIn.setRawMode(false)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
process.stdout.write('\x1B[?25h\x1B[?1049l')
|
|
348
|
+
}
|
|
349
|
+
|
|
185
350
|
function render() {
|
|
186
351
|
console.clear()
|
|
187
352
|
|