table-viewer 0.0.1 → 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 +205 -32
- package/package.json +10 -2
package/index.js
CHANGED
|
@@ -23,6 +23,13 @@ const consoleTableStartChars = {
|
|
|
23
23
|
2: '├',
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
const scrollbarEndChars = {
|
|
27
|
+
up: '·',
|
|
28
|
+
down: '·',
|
|
29
|
+
left: '·',
|
|
30
|
+
right: '·',
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
const consoleTableOutput = captureConsoleTableOutput(tableData)
|
|
27
34
|
|
|
28
35
|
const consoleTableOutputSplit = consoleTableOutput.split('\n')
|
|
@@ -31,18 +38,36 @@ let consoleWidth = process.stdout.columns
|
|
|
31
38
|
let consoleHeight = process.stdout.rows
|
|
32
39
|
let xOffset = 0
|
|
33
40
|
let yOffset = 0
|
|
41
|
+
let mouseSequenceBuffer = ''
|
|
42
|
+
let didCleanupTerminal = false
|
|
34
43
|
|
|
35
44
|
if (!consoleTableOutputSplit.length) process.exit(0)
|
|
36
45
|
|
|
37
46
|
const consoleTableOutputWidth = consoleTableOutputSplit[0].length
|
|
38
47
|
const consoleTableOutputHeight = consoleTableOutputSplit.length
|
|
39
48
|
|
|
40
|
-
//
|
|
41
|
-
process.stdout.write('\x1B[?25l')
|
|
49
|
+
// Enter alternate screen buffer and hide cursor
|
|
50
|
+
process.stdout.write('\x1B[?1049h\x1B[?25l')
|
|
51
|
+
enableMouseReporting()
|
|
42
52
|
|
|
43
|
-
// Restore cursor on exit
|
|
53
|
+
// Restore cursor and leave alternate screen buffer on exit
|
|
44
54
|
process.on('exit', () => {
|
|
45
|
-
|
|
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)
|
|
46
71
|
})
|
|
47
72
|
|
|
48
73
|
process.stdout.on('resize', () => {
|
|
@@ -57,31 +82,24 @@ process.stdout.on('resize', () => {
|
|
|
57
82
|
let pendingG = false
|
|
58
83
|
|
|
59
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
|
+
|
|
60
88
|
// Ctrl+C or q to exit
|
|
61
89
|
if ((key.ctrl && key.name === 'c') || key.name === 'q') {
|
|
62
90
|
process.exit(0)
|
|
63
91
|
}
|
|
64
92
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
const maxXOffset =
|
|
68
|
-
0,
|
|
69
|
-
consoleTableOutputWidth - consoleWidth + (needsScrollbarY ? 1 : 0),
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
const maxYOffset = Math.max(
|
|
73
|
-
0,
|
|
74
|
-
consoleTableOutputHeight - consoleHeight + (needsScrollbarX ? 1 : 0),
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
const visibleHeight = consoleHeight - (needsScrollbarX ? 1 : 0)
|
|
93
|
+
const prevX = xOffset
|
|
94
|
+
const prevY = yOffset
|
|
95
|
+
const { maxXOffset, maxYOffset, visibleHeight } = getViewportMetrics()
|
|
78
96
|
|
|
79
97
|
// Handle gg (go to top)
|
|
80
98
|
if (key.name === 'g' && !key.ctrl && !key.shift) {
|
|
81
99
|
if (pendingG) {
|
|
82
100
|
pendingG = false
|
|
83
101
|
yOffset = 0
|
|
84
|
-
render()
|
|
102
|
+
if (yOffset !== prevY) render()
|
|
85
103
|
return
|
|
86
104
|
}
|
|
87
105
|
|
|
@@ -94,7 +112,7 @@ ttyIn.on('keypress', (_str, key) => {
|
|
|
94
112
|
pendingG = false
|
|
95
113
|
yOffset = maxYOffset
|
|
96
114
|
|
|
97
|
-
render()
|
|
115
|
+
if (yOffset !== prevY) render()
|
|
98
116
|
return
|
|
99
117
|
}
|
|
100
118
|
|
|
@@ -105,21 +123,21 @@ ttyIn.on('keypress', (_str, key) => {
|
|
|
105
123
|
// -- vertical movement --
|
|
106
124
|
case 'k':
|
|
107
125
|
case 'up':
|
|
108
|
-
|
|
126
|
+
moveUp(key.shift ? 10 : 5)
|
|
109
127
|
break
|
|
110
128
|
case 'j':
|
|
111
129
|
case 'down':
|
|
112
|
-
|
|
130
|
+
moveDown(key.shift ? 10 : 5)
|
|
113
131
|
break
|
|
114
132
|
|
|
115
133
|
// -- horizontal movement --
|
|
116
134
|
case 'h':
|
|
117
135
|
case 'left':
|
|
118
|
-
|
|
136
|
+
moveLeft(key.shift ? 10 : 5)
|
|
119
137
|
break
|
|
120
138
|
case 'l':
|
|
121
139
|
case 'right':
|
|
122
|
-
|
|
140
|
+
moveRight(key.shift ? 10 : 5)
|
|
123
141
|
break
|
|
124
142
|
|
|
125
143
|
// Ctrl+d — half page down
|
|
@@ -158,8 +176,8 @@ ttyIn.on('keypress', (_str, key) => {
|
|
|
158
176
|
xOffset = 0
|
|
159
177
|
break
|
|
160
178
|
// $ — scroll to rightmost
|
|
161
|
-
case
|
|
162
|
-
if (key.
|
|
179
|
+
case undefined:
|
|
180
|
+
if (key.sequence === '$') {
|
|
163
181
|
xOffset = maxXOffset
|
|
164
182
|
} else {
|
|
165
183
|
return
|
|
@@ -170,11 +188,165 @@ ttyIn.on('keypress', (_str, key) => {
|
|
|
170
188
|
return
|
|
171
189
|
}
|
|
172
190
|
|
|
173
|
-
render()
|
|
191
|
+
if (xOffset !== prevX || yOffset !== prevY) render()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
ttyIn.on('data', (chunk) => {
|
|
195
|
+
mouseSequenceBuffer += chunk.toString('utf8')
|
|
196
|
+
processMouseBuffer()
|
|
174
197
|
})
|
|
175
198
|
|
|
176
199
|
render()
|
|
177
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
|
+
|
|
178
350
|
function render() {
|
|
179
351
|
console.clear()
|
|
180
352
|
|
|
@@ -236,7 +408,7 @@ function renderScrollbarX(needsScrollbarX, needsScrollbarY) {
|
|
|
236
408
|
if (!needsScrollbarX) return ''
|
|
237
409
|
|
|
238
410
|
const visibleWidth = consoleWidth - (needsScrollbarY ? 1 : 0)
|
|
239
|
-
const trackLength = visibleWidth -
|
|
411
|
+
const trackLength = visibleWidth - 2
|
|
240
412
|
|
|
241
413
|
const thumbSize = Math.max(
|
|
242
414
|
1,
|
|
@@ -251,11 +423,12 @@ function renderScrollbarX(needsScrollbarX, needsScrollbarY) {
|
|
|
251
423
|
) || 0
|
|
252
424
|
|
|
253
425
|
return (
|
|
254
|
-
|
|
426
|
+
scrollbarEndChars.left +
|
|
255
427
|
'-'.repeat(thumbPos) +
|
|
256
428
|
'═'.repeat(thumbSize) +
|
|
257
429
|
'-'.repeat(trackLength - thumbPos - thumbSize) +
|
|
258
|
-
|
|
430
|
+
scrollbarEndChars.right +
|
|
431
|
+
(needsScrollbarY ? ' ' : '')
|
|
259
432
|
)
|
|
260
433
|
}
|
|
261
434
|
|
|
@@ -268,7 +441,7 @@ function renderScrollbarY(needsScrollbarX, needsScrollbarY) {
|
|
|
268
441
|
if (!needsScrollbarY) return ''
|
|
269
442
|
|
|
270
443
|
const visibleHeight = consoleHeight - (needsScrollbarX ? 1 : 0)
|
|
271
|
-
const trackLength = visibleHeight -
|
|
444
|
+
const trackLength = visibleHeight - 2
|
|
272
445
|
|
|
273
446
|
const thumbSize = Math.max(
|
|
274
447
|
1,
|
|
@@ -283,11 +456,11 @@ function renderScrollbarY(needsScrollbarX, needsScrollbarY) {
|
|
|
283
456
|
) || 0
|
|
284
457
|
|
|
285
458
|
return (
|
|
286
|
-
|
|
459
|
+
scrollbarEndChars.up +
|
|
287
460
|
'╎'.repeat(thumbPos) +
|
|
288
461
|
'║'.repeat(thumbSize) +
|
|
289
462
|
'╎'.repeat(trackLength - thumbPos - thumbSize) +
|
|
290
|
-
|
|
463
|
+
scrollbarEndChars.down
|
|
291
464
|
)
|
|
292
465
|
}
|
|
293
466
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "table-viewer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3-beta.0",
|
|
4
4
|
"description": "A keyboard-navigable terminal table viewer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
7
|
-
"bin": "./index.js"
|
|
7
|
+
"bin": "./index.js",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/aaronccasanova/table-viewer.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/aaronccasanova/table-viewer/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/aaronccasanova/table-viewer"
|
|
8
16
|
}
|