fx 20.0.2 → 24.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.
package/fx.js DELETED
@@ -1,717 +0,0 @@
1
- 'use strict'
2
- const fs = require('fs')
3
- const tty = require('tty')
4
- const blessed = require('@medv/blessed')
5
- const stringWidth = require('string-width')
6
- const reduce = require('./reduce')
7
- const print = require('./print')
8
- const find = require('./find')
9
- const config = require('./config')
10
-
11
- module.exports = function start(filename, source, prev = {}) {
12
- // Current rendered object on a screen.
13
- let json = prev.json || source
14
-
15
- // Contains map from row number to expand path.
16
- // Example: {0: '', 1: '.foo', 2: '.foo[0]'}
17
- let index = new Map()
18
-
19
- // Contains expanded paths. Example: ['', '.foo']
20
- // Empty string represents root path.
21
- const expanded = prev.expanded || new Set()
22
- expanded.add('')
23
-
24
- // Current filter code.
25
- let currentCode = null
26
-
27
- // Current search regexp and generator.
28
- let highlight = null
29
- let findGen = null
30
- let currentPath = null
31
-
32
- let ttyReadStream, ttyWriteStream
33
-
34
- // Reopen tty
35
- if (process.platform === 'win32') {
36
- const cfs = process.binding('fs')
37
- ttyReadStream = tty.ReadStream(cfs.open('conin$', fs.constants.O_RDWR | fs.constants.O_EXCL, 0o666))
38
- ttyWriteStream = tty.WriteStream(cfs.open('conout$', fs.constants.O_RDWR | fs.constants.O_EXCL, 0o666))
39
- } else {
40
- const ttyFd = fs.openSync('/dev/tty', 'r+')
41
- ttyReadStream = tty.ReadStream(ttyFd)
42
- ttyWriteStream = tty.WriteStream(ttyFd)
43
- }
44
-
45
- const program = blessed.program({
46
- input: ttyReadStream,
47
- output: ttyWriteStream,
48
- })
49
-
50
- const screen = blessed.screen({
51
- program: program,
52
- smartCSR: true,
53
- fullUnicode: true,
54
- })
55
-
56
- const box = blessed.box({
57
- parent: screen,
58
- tags: false,
59
- left: 0,
60
- top: 0,
61
- width: '100%',
62
- height: '100%',
63
- mouse: true,
64
- keys: true,
65
- vi: true,
66
- ignoreArrows: true,
67
- alwaysScroll: true,
68
- scrollable: true,
69
- })
70
-
71
- const input = blessed.textbox({
72
- parent: screen,
73
- bottom: 0,
74
- left: 0,
75
- height: 1,
76
- width: '100%',
77
- })
78
-
79
- const search = blessed.textbox({
80
- parent: screen,
81
- bottom: 0,
82
- left: 0,
83
- height: 1,
84
- width: '100%',
85
- })
86
-
87
- const statusBar = blessed.box({
88
- parent: screen,
89
- tags: false,
90
- bottom: 0,
91
- left: 0,
92
- height: 1,
93
- width: '100%',
94
- })
95
-
96
- const autocomplete = blessed.list({
97
- parent: screen,
98
- width: 6,
99
- height: 7,
100
- left: 1,
101
- bottom: 1,
102
- style: config.list,
103
- })
104
-
105
- screen.title = filename
106
- box.focus()
107
- input.hide()
108
- search.hide()
109
- statusBar.hide()
110
- autocomplete.hide()
111
-
112
- process.stdout.on('resize', () => {
113
- // Blessed has a bug with resizing the terminal. I tried my best to fix it but was not succeeded.
114
- // For now exit and print seem like a reasonable alternative, as it not usable after resize.
115
- // If anyone can fix this bug it will be cool.
116
- printJson({expanded})
117
- })
118
-
119
- screen.key(['escape', 'q', 'C-c'], function () {
120
- exit()
121
- })
122
-
123
- input.on('submit', function () {
124
- if (autocomplete.hidden) {
125
- const code = input.getValue()
126
- if (/^\//.test(code)) {
127
- // Forgive a mistake to the user. This looks like user wanted to search something.
128
- apply('')
129
- applyPattern(code)
130
- } else {
131
- apply(code)
132
- }
133
- } else {
134
- // Autocomplete selected
135
- let code = input.getValue()
136
- let replace = autocomplete.getSelected()
137
- if (/^[a-z]\w*$/i.test(replace)) {
138
- replace = '.' + replace
139
- } else {
140
- replace = `["${replace}"]`
141
- }
142
- code = code.replace(/\.\w*$/, replace)
143
-
144
- input.setValue(code)
145
- autocomplete.hide()
146
- update(code)
147
-
148
- // Keep editing code
149
- input.readInput()
150
- }
151
- })
152
-
153
- input.on('cancel', function () {
154
- if (autocomplete.hidden) {
155
- const code = input.getValue()
156
- apply(code)
157
- } else {
158
- // Autocomplete not selected
159
- autocomplete.hide()
160
- screen.render()
161
-
162
- // Keep editing code
163
- input.readInput()
164
- }
165
- })
166
-
167
- input.on('update', function (code) {
168
- if (currentCode === code) {
169
- return
170
- }
171
- currentCode = code
172
- if (index.size < 10000) { // Don't live update in we have a big JSON file.
173
- update(code)
174
- }
175
- complete(code)
176
- })
177
-
178
- input.key('up', function () {
179
- if (!autocomplete.hidden) {
180
- autocomplete.up()
181
- screen.render()
182
- }
183
- })
184
-
185
- input.key('down', function () {
186
- if (!autocomplete.hidden) {
187
- autocomplete.down()
188
- screen.render()
189
- }
190
- })
191
-
192
- input.key('C-u', function () {
193
- input.setValue('')
194
- update('')
195
- render()
196
- })
197
-
198
- input.key('C-w', function () {
199
- let code = input.getValue()
200
- code = code.replace(/[\.\[][^\.\[]*$/, '')
201
- input.setValue(code)
202
- update(code)
203
- render()
204
- })
205
-
206
- search.on('submit', function (pattern) {
207
- applyPattern(pattern)
208
- })
209
-
210
- search.on('cancel', function () {
211
- highlight = null
212
- currentPath = null
213
-
214
- search.hide()
215
- search.setValue('')
216
-
217
- box.height = '100%'
218
- box.focus()
219
-
220
- program.cursorPos(0, 0)
221
- render()
222
- })
223
-
224
- box.key('.', function () {
225
- hideStatusBar()
226
- box.height = '100%-1'
227
- input.show()
228
- if (input.getValue() === '') {
229
- input.setValue('.')
230
- complete('.')
231
- }
232
- input.readInput()
233
- screen.render()
234
- })
235
-
236
- box.key('/', function () {
237
- hideStatusBar()
238
- box.height = '100%-1'
239
- search.show()
240
- search.setValue('/')
241
- search.readInput()
242
- screen.render()
243
- })
244
-
245
- box.key('e', function () {
246
- hideStatusBar()
247
- expanded.clear()
248
- for (let path of dfs(json)) {
249
- if (expanded.size < 1000) {
250
- expanded.add(path)
251
- } else {
252
- break
253
- }
254
- }
255
- render()
256
- })
257
-
258
- box.key('S-e', function () {
259
- hideStatusBar()
260
- expanded.clear()
261
- expanded.add('')
262
- render()
263
-
264
- // Make sure cursor stay on JSON object.
265
- const [n] = getLine(program.y)
266
- if (typeof n === 'undefined' || !index.has(n)) {
267
- // No line under cursor
268
- let rest = [...index.keys()]
269
- if (rest.length > 0) {
270
- const next = Math.max(...rest)
271
- let y = box.getScreenNumber(next) - box.childBase
272
- if (y <= 0) {
273
- y = 0
274
- }
275
- const line = box.getScreenLine(y + box.childBase)
276
- program.cursorPos(y, line.search(/\S/))
277
- }
278
- }
279
- })
280
-
281
- box.key('n', function () {
282
- hideStatusBar()
283
- findNext()
284
- })
285
-
286
- box.key(['up', 'k'], function () {
287
- hideStatusBar()
288
- program.showCursor()
289
- const [n] = getLine(program.y)
290
-
291
- let next
292
- for (let [i,] of index) {
293
- if (i < n && (typeof next === 'undefined' || i > next)) {
294
- next = i
295
- }
296
- }
297
-
298
- if (typeof next !== 'undefined') {
299
- let y = box.getScreenNumber(next) - box.childBase
300
- if (y <= 0) {
301
- box.scroll(-1)
302
- screen.render()
303
- y = 0
304
- }
305
-
306
- const line = box.getScreenLine(y + box.childBase)
307
- program.cursorPos(y, line.search(/\S/))
308
- }
309
- })
310
-
311
- box.key(['down', 'j'], function () {
312
- hideStatusBar()
313
- program.showCursor()
314
- const [n] = getLine(program.y)
315
-
316
- let next
317
- for (let [i,] of index) {
318
- if (i > n && (typeof next === 'undefined' || i < next)) {
319
- next = i
320
- }
321
- }
322
-
323
- if (typeof next !== 'undefined') {
324
- let y = box.getScreenNumber(next) - box.childBase
325
- if (y >= box.height) {
326
- box.scroll(1)
327
- screen.render()
328
- y = box.height - 1
329
- }
330
-
331
- const line = box.getScreenLine(y + box.childBase)
332
- program.cursorPos(y, line.search(/\S/))
333
- }
334
- })
335
-
336
- box.key(['right', 'l'], function () {
337
- hideStatusBar()
338
- const [n, line] = getLine(program.y)
339
- program.showCursor()
340
- program.cursorPos(program.y, line.search(/\S/))
341
- const path = index.get(n)
342
- if (!expanded.has(path)) {
343
- expanded.add(path)
344
- render()
345
- }
346
- })
347
-
348
- // Expand everything under cursor.
349
- box.key(['S-right', 'S-l'], function () {
350
- hideStatusBar()
351
- const [n, line] = getLine(program.y)
352
- program.showCursor()
353
- program.cursorPos(program.y, line.search(/\S/))
354
- const path = index.get(n)
355
- const subJson = reduce(json, 'this' + path)
356
- for (let p of dfs(subJson, path)) {
357
- if (expanded.size < 1000) {
358
- expanded.add(p)
359
- } else {
360
- break
361
- }
362
- }
363
- render()
364
- })
365
-
366
- box.key(['left', 'h'], function () {
367
- hideStatusBar()
368
- const [n, line] = getLine(program.y)
369
- program.showCursor()
370
- program.cursorPos(program.y, line.search(/\S/))
371
-
372
- // Find path at current cursor position.
373
- const path = index.get(n)
374
-
375
- if (expanded.has(path)) {
376
- // Collapse current path.
377
- expanded.delete(path)
378
- render()
379
- } else {
380
- // If there is no expanded paths on current line,
381
- // collapse parent path of current location.
382
- if (typeof path === 'string') {
383
- // Trip last part (".foo", "[0]") to get parent path.
384
- const parentPath = path.replace(/(\.[^\[\].]+|\[\d+\])$/, '')
385
- if (expanded.has(parentPath)) {
386
- expanded.delete(parentPath)
387
- render()
388
-
389
- // Find line number of parent path, and if we able to find it,
390
- // move cursor to this position of just collapsed parent path.
391
- for (let y = program.y; y >= 0; --y) {
392
- const [n, line] = getLine(y)
393
- if (index.get(n) === parentPath) {
394
- program.cursorPos(y, line.search(/\S/))
395
- break
396
- }
397
- }
398
- }
399
- }
400
- }
401
- })
402
-
403
- box.on('click', function (mouse) {
404
- hideStatusBar()
405
- const [n, line] = getLine(mouse.y)
406
- if (mouse.x >= stringWidth(line)) {
407
- return
408
- }
409
-
410
- program.hideCursor()
411
- program.cursorPos(mouse.y, line.search(/\S/))
412
- autocomplete.hide()
413
-
414
- const path = index.get(n)
415
- if (expanded.has(path)) {
416
- expanded.delete(path)
417
- } else {
418
- expanded.add(path)
419
- }
420
- render()
421
- })
422
-
423
- box.on('scroll', function () {
424
- hideStatusBar()
425
- })
426
-
427
- box.key('p', function () {
428
- printJson({expanded})
429
- })
430
-
431
- box.key('S-p', function () {
432
- printJson()
433
- })
434
-
435
- function printJson(options = {}) {
436
- screen.destroy()
437
- program.disableMouse()
438
- program.destroy()
439
- setTimeout(() => {
440
- const [text] = print(json, options)
441
- console.log(text)
442
- process.exit(0)
443
- }, 10)
444
- }
445
-
446
- function getLine(y) {
447
- const dy = box.childBase + y
448
- const n = box.getNumber(dy)
449
- const line = box.getScreenLine(dy)
450
- if (typeof line === 'undefined') {
451
- return [n, '']
452
- }
453
- return [n, line]
454
- }
455
-
456
- function apply(code) {
457
- if (code && code.length !== 0) {
458
- try {
459
- json = reduce(source, code)
460
- } catch (e) {
461
- // pass
462
- }
463
- } else {
464
- box.height = '100%'
465
- input.hide()
466
- json = source
467
- }
468
- box.focus()
469
- program.cursorPos(0, 0)
470
- render()
471
- }
472
-
473
- function complete(inputCode) {
474
- const match = inputCode.match(/\.(\w*)$/)
475
- const code = /^\.\w*$/.test(inputCode) ? '.' : inputCode.replace(/\.\w*$/, '')
476
-
477
- let json
478
- try {
479
- json = reduce(source, code)
480
- } catch (e) {
481
- }
482
-
483
- if (match) {
484
- if (typeof json === 'object' && json.constructor === Object) {
485
- const keys = Object.keys(json)
486
- .filter(key => key.startsWith(match[1]))
487
- .slice(0, 1000) // With lots of items, list takes forever to render.
488
-
489
- // Hide if there is nothing to show or
490
- // don't show if there is complete match.
491
- if (keys.length === 0 || (keys.length === 1 && keys[0] === match[1])) {
492
- autocomplete.hide()
493
- return
494
- }
495
-
496
- autocomplete.width = Math.max(...keys.map(key => key.length)) + 1
497
- autocomplete.height = Math.min(7, keys.length)
498
- autocomplete.left = Math.min(
499
- screen.width - autocomplete.width,
500
- code.length === 1 ? 1 : code.length + 1
501
- )
502
-
503
- let selectFirst = autocomplete.items.length !== keys.length
504
- autocomplete.setItems(keys)
505
-
506
- if (selectFirst) {
507
- autocomplete.select(autocomplete.items.length - 1)
508
- }
509
- if (autocomplete.hidden) {
510
- autocomplete.show()
511
- }
512
- } else {
513
- autocomplete.clearItems()
514
- autocomplete.hide()
515
- }
516
- }
517
- }
518
-
519
- function update(code) {
520
- if (code && code.length !== 0) {
521
- try {
522
- const pretender = reduce(source, code)
523
- if (
524
- typeof pretender !== 'undefined'
525
- && typeof pretender !== 'function'
526
- && !(pretender instanceof RegExp)
527
- ) {
528
- json = pretender
529
- }
530
- } catch (e) {
531
- // pass
532
- }
533
- }
534
- if (code === '') {
535
- json = source
536
- }
537
-
538
- if (highlight) {
539
- findGen = find(json, highlight)
540
- }
541
- render()
542
- }
543
-
544
- function applyPattern(pattern) {
545
- let regex
546
- let m = pattern.match(/^\/(.*)\/([gimuy]*)$/)
547
- if (m) {
548
- try {
549
- regex = new RegExp(m[1], m[2])
550
- } catch (e) {
551
- // Wrong regexp.
552
- }
553
- } else {
554
- m = pattern.match(/^\/(.*)$/)
555
- if (m) {
556
- try {
557
- regex = new RegExp(m[1], 'gi')
558
- } catch (e) {
559
- // Wrong regexp.
560
- }
561
- }
562
- }
563
- highlight = regex
564
-
565
- search.hide()
566
-
567
- if (highlight) {
568
- findGen = find(json, highlight)
569
- findNext()
570
- } else {
571
- findGen = null
572
- currentPath = null
573
- }
574
- search.setValue('')
575
-
576
- box.height = '100%'
577
- box.focus()
578
-
579
- program.cursorPos(0, 0)
580
- render()
581
- }
582
-
583
- function findNext() {
584
- if (!findGen) {
585
- return
586
- }
587
-
588
- const {value: path, done} = findGen.next()
589
-
590
- if (done) {
591
- showStatusBar('Pattern not found')
592
- } else {
593
-
594
- currentPath = ''
595
- for (let p of path) {
596
- expanded.add(currentPath += p)
597
- }
598
- render()
599
-
600
- for (let [k, v] of index) {
601
- if (v === currentPath) {
602
- let y = box.getScreenNumber(k)
603
-
604
- // Scroll one line up for better view and make sure it's not negative.
605
- if (--y < 0) {
606
- y = 0
607
- }
608
-
609
- box.scrollTo(y)
610
- screen.render()
611
- }
612
- }
613
-
614
- // Set cursor to current path.
615
- // We need timeout here to give our terminal some time.
616
- // Without timeout first cursorPos call does not working,
617
- // it looks like an ugly hack and it is an ugly hack.
618
- setTimeout(() => {
619
- for (let [k, v] of index) {
620
- if (v === currentPath) {
621
- let y = box.getScreenNumber(k) - box.childBase
622
- if (y <= 0) {
623
- y = 0
624
- }
625
- const line = box.getScreenLine(y + box.childBase)
626
- program.cursorPos(y, line.search(/\S/))
627
- }
628
- }
629
- }, 100)
630
- }
631
- }
632
-
633
- function showStatusBar(status) {
634
- statusBar.show()
635
- statusBar.setContent(config.statusBar(` ${status} `))
636
- screen.render()
637
- }
638
-
639
- function hideStatusBar() {
640
- if (!statusBar.hidden) {
641
- statusBar.hide()
642
- statusBar.setContent('')
643
- screen.render()
644
- }
645
- }
646
-
647
- function render() {
648
- let content
649
- [content, index] = print(json, {expanded, highlight, currentPath})
650
-
651
- if (typeof content === 'undefined') {
652
- content = 'undefined'
653
- }
654
-
655
- box.setContent(content)
656
- screen.render()
657
- }
658
-
659
- function exit() {
660
- // If exit program immediately, stdin may still receive
661
- // mouse events which will be printed in stdout.
662
- program.disableMouse()
663
- setTimeout(() => process.exit(0), 10)
664
- }
665
-
666
- render()
667
- }
668
-
669
- function* bfs(json) {
670
- const queue = [[json, '']]
671
-
672
- while (queue.length > 0) {
673
- const [v, path] = queue.shift()
674
-
675
- if (!v) {
676
- continue
677
- }
678
-
679
- if (Array.isArray(v)) {
680
- yield path
681
- let i = 0
682
- for (let item of v) {
683
- const p = path + '[' + (i++) + ']'
684
- queue.push([item, p])
685
- }
686
- }
687
-
688
- if (typeof v === 'object' && v.constructor === Object) {
689
- yield path
690
- for (let [key, value] of Object.entries(v)) {
691
- const p = path + '.' + key
692
- queue.push([value, p])
693
- }
694
- }
695
- }
696
- }
697
-
698
- function* dfs(v, path = '') {
699
- if (!v) {
700
- return
701
- }
702
-
703
- if (Array.isArray(v)) {
704
- yield path
705
- let i = 0
706
- for (let item of v) {
707
- yield* dfs(item, path + '[' + (i++) + ']')
708
- }
709
- }
710
-
711
- if (typeof v === 'object' && v.constructor === Object) {
712
- yield path
713
- for (let [key, value] of Object.entries(v)) {
714
- yield* dfs(value, path + '.' + key)
715
- }
716
- }
717
- }