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.
Files changed (2) hide show
  1. package/index.js +188 -23
  2. 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
- process.stdout.write('\x1B[?25h\x1B[?1049l')
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 { needsScrollbarX, needsScrollbarY } = computeScrollbarVisibility()
73
-
74
- const maxXOffset = Math.max(
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
- yOffset = Math.max(0, yOffset - (key.shift ? 10 : 5))
126
+ moveUp(key.shift ? 10 : 5)
116
127
  break
117
128
  case 'j':
118
129
  case 'down':
119
- yOffset = Math.min(maxYOffset, yOffset + (key.shift ? 10 : 5))
130
+ moveDown(key.shift ? 10 : 5)
120
131
  break
121
132
 
122
133
  // -- horizontal movement --
123
134
  case 'h':
124
135
  case 'left':
125
- xOffset = Math.max(0, xOffset - (key.shift ? 10 : 5))
136
+ moveLeft(key.shift ? 10 : 5)
126
137
  break
127
138
  case 'l':
128
139
  case 'right':
129
- xOffset = Math.min(maxXOffset, xOffset + (key.shift ? 10 : 5))
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 '4': // shift+4 = $
169
- if (key.shift) {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "table-viewer",
3
- "version": "0.0.2",
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",