mirror of
https://github.com/antonmedv/fx
synced 2024-11-07 09:20:28 +00:00
New features
- Arrow navigation - Interactive digger - Themes support
This commit is contained in:
parent
851427d8c8
commit
f110142160
15
config.js
Normal file
15
config.js
Normal file
@ -0,0 +1,15 @@
|
||||
'use strict'
|
||||
const chalk = require('chalk')
|
||||
const noop = x => x
|
||||
|
||||
module.exports = {
|
||||
space: global.FX_STYLE_SPACE || 2,
|
||||
null: global.FX_STYLE_NULL || chalk.grey.bold,
|
||||
number: global.FX_STYLE_NUMBER || chalk.cyan.bold,
|
||||
boolean: global.FX_STYLE_BOOLEAN || chalk.yellow.bold,
|
||||
string: global.FX_STYLE_STRING || chalk.green.bold,
|
||||
key: global.FX_STYLE_KEY || chalk.blue.bold,
|
||||
bracket: global.FX_STYLE_BRACKET || noop,
|
||||
comma: global.FX_STYLE_COMMA || noop,
|
||||
colon: global.FX_STYLE_COLON || noop,
|
||||
}
|
297
fx.js
297
fx.js
@ -1,11 +1,17 @@
|
||||
'use strict'
|
||||
const fs = require('fs')
|
||||
const tty = require('tty')
|
||||
const blessed = require('neo-blessed')
|
||||
const blessed = require('@medv/blessed')
|
||||
const stringWidth = require('string-width')
|
||||
const indent = require('indent-string')
|
||||
const chalk = require('chalk')
|
||||
const reduce = require('./reduce')
|
||||
const print = require('./print')
|
||||
|
||||
module.exports = function start(filename, source) {
|
||||
let json = source
|
||||
let index = new Map()
|
||||
const expanded = new Set()
|
||||
expanded.add('') // Root of JSON
|
||||
|
||||
module.exports = function start(input) {
|
||||
const ttyFd = fs.openSync('/dev/tty', 'r+')
|
||||
|
||||
const program = blessed.program({
|
||||
@ -18,11 +24,6 @@ module.exports = function start(input) {
|
||||
smartCSR: true,
|
||||
fullUnicode: true,
|
||||
})
|
||||
screen.title = 'fx'
|
||||
screen.key(['escape', 'q', 'C-c'], function (ch, key) {
|
||||
return process.exit(0)
|
||||
})
|
||||
screen.on('resize', render)
|
||||
|
||||
const box = blessed.box({
|
||||
parent: screen,
|
||||
@ -31,8 +32,10 @@ module.exports = function start(input) {
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
mouse: true,
|
||||
keys: true,
|
||||
vi: true,
|
||||
ignoreArrows: true,
|
||||
alwaysScroll: true,
|
||||
scrollable: true,
|
||||
})
|
||||
@ -44,170 +47,143 @@ module.exports = function start(input) {
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
const scrollSpeed = (() => {
|
||||
let prev = new Date()
|
||||
return () => {
|
||||
const now = new Date()
|
||||
const lines = now - prev < 20 ? 3 : 1 // TODO: Speed based on terminal.
|
||||
prev = now
|
||||
return lines
|
||||
const input = blessed.textbox({
|
||||
parent: screen,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
height: 1,
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
screen.title = filename
|
||||
input.hide()
|
||||
|
||||
screen.key(['escape', 'q', 'C-c'], function (ch, key) {
|
||||
// If exit program immediately, stdin may still receive mouse events which will be printed in stdout.
|
||||
program.disableMouse()
|
||||
setTimeout(() => process.exit(0), 10)
|
||||
})
|
||||
|
||||
screen.on('resize', render)
|
||||
|
||||
input.on('action', function (code) {
|
||||
if (code && code.length !== 0) {
|
||||
try {
|
||||
json = reduce(source, code)
|
||||
} catch (e) {
|
||||
// pass
|
||||
}
|
||||
} else {
|
||||
box.height = '100%'
|
||||
input.hide()
|
||||
json = source
|
||||
}
|
||||
})()
|
||||
|
||||
box.on('wheeldown', function () {
|
||||
box.scroll(scrollSpeed())
|
||||
screen.render()
|
||||
})
|
||||
box.on('wheelup', function () {
|
||||
box.scroll(-scrollSpeed())
|
||||
screen.render()
|
||||
})
|
||||
|
||||
// TODO: fx input
|
||||
// const inputBar = blessed.textbox({
|
||||
// parent: screen,
|
||||
// bottom: 0,
|
||||
// left: 0,
|
||||
// height: 1,
|
||||
// width: '100%',
|
||||
// keys: true,
|
||||
// mouse: true,
|
||||
// inputOnFocus: true,
|
||||
// })
|
||||
|
||||
const expanded = new Set()
|
||||
expanded.add('') // Root of JSON
|
||||
|
||||
box.key('e', function () {
|
||||
walk(input, path => expanded.size < 1000 && expanded.add(path))
|
||||
box.focus()
|
||||
program.cursorPos(0, 0)
|
||||
render()
|
||||
})
|
||||
|
||||
input.on('update', function (code) {
|
||||
if (code && code.length !== 0) {
|
||||
try {
|
||||
const pretender = reduce(source, code)
|
||||
if (typeof pretender !== 'undefined') {
|
||||
json = pretender
|
||||
}
|
||||
} catch (e) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
render()
|
||||
})
|
||||
|
||||
|
||||
box.key(':', function () {
|
||||
box.height = '100%-1'
|
||||
input.show()
|
||||
input.readInput()
|
||||
render()
|
||||
})
|
||||
|
||||
box.key('e', function () {
|
||||
walk(json, path => expanded.size < 1000 && expanded.add(path))
|
||||
render()
|
||||
})
|
||||
|
||||
box.key('S-e', function () {
|
||||
expanded.clear()
|
||||
expanded.add('')
|
||||
render()
|
||||
})
|
||||
|
||||
function walk(v, cb, path = '') {
|
||||
if (!v) {
|
||||
return
|
||||
}
|
||||
box.key('up', function () {
|
||||
program.showCursor()
|
||||
|
||||
if (Array.isArray(v)) {
|
||||
cb(path)
|
||||
let i = 0
|
||||
for (let item of v) {
|
||||
walk(item, cb, path + '[' + (i++) + ']')
|
||||
const pos = box.childBase + program.y
|
||||
const rest = [...index.keys()].filter(i => i < pos)
|
||||
if (rest.length > 0) {
|
||||
const next = Math.max(...rest)
|
||||
const y = next - box.childBase
|
||||
if (y <= 0) {
|
||||
box.scroll(-1)
|
||||
screen.render()
|
||||
}
|
||||
const line = box.getScreenLines()[next]
|
||||
const x = line.search(/\S/)
|
||||
program.cursorPos(y, x)
|
||||
}
|
||||
})
|
||||
|
||||
if (typeof v === 'object' && v.constructor === Object) {
|
||||
cb(path)
|
||||
let i = 0
|
||||
for (let [key, value] of Object.entries(v)) {
|
||||
walk(value, cb, path + '.' + key)
|
||||
box.key('down', function () {
|
||||
program.showCursor()
|
||||
|
||||
const pos = box.childBase + program.y
|
||||
const rest = [...index.keys()].filter(i => i > pos)
|
||||
if (rest.length > 0) {
|
||||
const next = Math.min(...rest)
|
||||
const y = next - box.childBase
|
||||
if (y >= box.height) {
|
||||
box.scroll(1)
|
||||
screen.render()
|
||||
}
|
||||
const line = box.getScreenLines()[next]
|
||||
const x = line.search(/\S/)
|
||||
program.cursorPos(y, x)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const space = 2
|
||||
|
||||
let index = new Map()
|
||||
let row = 0
|
||||
|
||||
function print(input) {
|
||||
row = 0
|
||||
index = new Map()
|
||||
return doPrint(input)
|
||||
}
|
||||
|
||||
function doPrint(v, path = '') {
|
||||
const eol = () => {
|
||||
row++
|
||||
return '\n'
|
||||
box.key('right', function () {
|
||||
program.showCursor()
|
||||
const pos = box.childBase + program.y
|
||||
const path = index.get(pos)
|
||||
if (!expanded.has(path)) {
|
||||
expanded.add(path)
|
||||
render()
|
||||
}
|
||||
})
|
||||
|
||||
if (typeof v === 'undefined') {
|
||||
return void 0
|
||||
box.key('left', function () {
|
||||
program.showCursor()
|
||||
const pos = box.childBase + program.y
|
||||
const path = index.get(pos)
|
||||
if (expanded.has(path)) {
|
||||
expanded.delete(path)
|
||||
render()
|
||||
}
|
||||
|
||||
if (v === null) {
|
||||
return chalk.grey.bold(v)
|
||||
}
|
||||
|
||||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||||
return chalk.cyan.bold(v)
|
||||
|
||||
}
|
||||
|
||||
if (typeof v === 'boolean') {
|
||||
return chalk.yellow.bold(v)
|
||||
|
||||
}
|
||||
|
||||
if (typeof v === 'string') {
|
||||
return chalk.green.bold(JSON.stringify(v))
|
||||
}
|
||||
|
||||
if (Array.isArray(v)) {
|
||||
index.set(row, path)
|
||||
|
||||
if (!expanded.has(path)) {
|
||||
return '[\u2026]'
|
||||
}
|
||||
|
||||
let output = '[' + eol()
|
||||
|
||||
const len = v.length
|
||||
let i = 0
|
||||
|
||||
for (let item of v) {
|
||||
const value = typeof item === 'undefined' ? null : item // JSON.stringify compatibility
|
||||
output += indent(doPrint(value, path + '[' + i + ']'), space)
|
||||
output += i++ < len - 1 ? ',' : ''
|
||||
output += eol()
|
||||
}
|
||||
|
||||
return output + ']'
|
||||
}
|
||||
|
||||
if (typeof v === 'object' && v.constructor === Object) {
|
||||
index.set(row, path)
|
||||
|
||||
if (!expanded.has(path)) {
|
||||
return '{\u2026}'
|
||||
}
|
||||
|
||||
let output = '{' + eol()
|
||||
|
||||
const entries = Object.entries(v).filter(noUndefined) // JSON.stringify compatibility
|
||||
const len = entries.length
|
||||
|
||||
let i = 0
|
||||
for (let [key, value] of entries) {
|
||||
const part = chalk.blue.bold(JSON.stringify(key)) + ': ' + doPrint(value, path + '.' + key)
|
||||
output += indent(part, space)
|
||||
output += i++ < len - 1 ? ',' : ''
|
||||
output += eol()
|
||||
}
|
||||
|
||||
return output + '}'
|
||||
}
|
||||
|
||||
return JSON.stringify(v)
|
||||
}
|
||||
|
||||
function noUndefined([key, value]) {
|
||||
return typeof value !== 'undefined'
|
||||
}
|
||||
})
|
||||
|
||||
box.on('click', function (mouse) {
|
||||
program.hideCursor()
|
||||
|
||||
const pos = box.childBase + mouse.y
|
||||
const line = box.getScreenLines()[pos]
|
||||
if (mouse.x >= stringWidth(line)) {
|
||||
return
|
||||
}
|
||||
|
||||
const x = line.search(/\S/)
|
||||
program.cursorPos(mouse.y, x)
|
||||
|
||||
const path = index.get(pos)
|
||||
if (expanded.has(path)) {
|
||||
expanded.delete(path)
|
||||
@ -218,7 +194,12 @@ module.exports = function start(input) {
|
||||
})
|
||||
|
||||
function render() {
|
||||
const content = print(input)
|
||||
let content
|
||||
[content, index] = print(json, expanded)
|
||||
|
||||
if (typeof content === 'undefined') {
|
||||
content = 'undefined'
|
||||
}
|
||||
|
||||
// TODO: Move to own fork of blessed.
|
||||
let row = 0
|
||||
@ -252,3 +233,25 @@ module.exports = function start(input) {
|
||||
box.focus()
|
||||
render()
|
||||
}
|
||||
|
||||
function walk(v, cb, path = '') {
|
||||
if (!v) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(v)) {
|
||||
cb(path)
|
||||
let i = 0
|
||||
for (let item of v) {
|
||||
walk(item, cb, path + '[' + (i++) + ']')
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof v === 'object' && v.constructor === Object) {
|
||||
cb(path)
|
||||
let i = 0
|
||||
for (let [key, value] of Object.entries(v)) {
|
||||
walk(value, cb, path + '.' + key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
76
index.js
76
index.js
@ -1,10 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict'
|
||||
const os = require('os')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const pretty = require('@medv/prettyjson')
|
||||
const {stdin, stdout, stderr} = process
|
||||
|
||||
try {
|
||||
require(path.join(os.homedir(), '.fxrc'))
|
||||
} catch (err) {
|
||||
@ -12,14 +11,21 @@ try {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
const print = require('./print')
|
||||
const reduce = require('./reduce')
|
||||
|
||||
const usage = `
|
||||
Usage
|
||||
$ fx [code ...]
|
||||
|
||||
Examples
|
||||
$ fx package.json
|
||||
|
||||
$ echo '{"key": "value"}' | fx 'x => x.key'
|
||||
value
|
||||
|
||||
$ echo '{"key": "value"}' | fx .key
|
||||
value
|
||||
|
||||
$ echo '[1,2,3]' | fx 'this.map(x => x * 2)'
|
||||
[2, 4, 6]
|
||||
@ -32,75 +38,49 @@ const usage = `
|
||||
|
||||
$ echo '{"foo": 1, "bar": 2}' | fx ?
|
||||
["foo", "bar"]
|
||||
|
||||
$ echo '{"key": "value"}' | fx .key
|
||||
value
|
||||
|
||||
|
||||
`
|
||||
|
||||
function main(input) {
|
||||
let args = process.argv.slice(2)
|
||||
let filename = 'fx'
|
||||
|
||||
if (input === '') {
|
||||
stderr.write(usage)
|
||||
process.exit(2)
|
||||
if (args.length === 0) {
|
||||
stderr.write(usage)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
input = fs.readFileSync(args[0])
|
||||
filename = path.basename(args[0])
|
||||
args = args.slice(1)
|
||||
}
|
||||
|
||||
const json = JSON.parse(input)
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
if (args.length === 0 && stdout.isTTY) {
|
||||
require('./fx')(json)
|
||||
require('./fx')(filename, json)
|
||||
return
|
||||
}
|
||||
|
||||
const result = args.reduce(reduce, json)
|
||||
const output = args.reduce(reduce, json)
|
||||
|
||||
if (typeof result === 'undefined') {
|
||||
if (typeof output === 'undefined') {
|
||||
stderr.write('undefined\n')
|
||||
} else if (typeof result === 'string') {
|
||||
console.log(result)
|
||||
} else if (stdout.isTTY) {
|
||||
console.log(pretty(result))
|
||||
} else if (typeof output === 'string') {
|
||||
console.log(output)
|
||||
} else {
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
const [text] = print(output)
|
||||
console.log(text)
|
||||
}
|
||||
}
|
||||
|
||||
function reduce(json, code) {
|
||||
if (/^\w+\s*=>/.test(code)) {
|
||||
const fx = eval(code)
|
||||
return fx(json)
|
||||
}
|
||||
|
||||
if (/yield/.test(code)) {
|
||||
const fx = eval(`
|
||||
function fn() {
|
||||
const gen = (function*(){
|
||||
${code.replace(/\\\n/g, '')}
|
||||
}).call(this)
|
||||
return [...gen]
|
||||
}; fn
|
||||
`)
|
||||
return fx.call(json)
|
||||
}
|
||||
|
||||
if (/^\?$/.test(code)) {
|
||||
return Object.keys(json)
|
||||
}
|
||||
|
||||
if (/^\./.test(code)) {
|
||||
const fx = eval(`function fn() { return ${code === '.' ? 'this' : 'this' + code} }; fn`)
|
||||
return fx.call(json)
|
||||
}
|
||||
|
||||
const fx = eval(`function fn() { return ${code} }; fn`)
|
||||
return fx.call(json)
|
||||
}
|
||||
|
||||
function run() {
|
||||
stdin.setEncoding('utf8')
|
||||
|
||||
if (stdin.isTTY) {
|
||||
main('')
|
||||
return
|
||||
}
|
||||
|
||||
let buff = ''
|
||||
|
@ -34,15 +34,15 @@
|
||||
"node": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medv/prettyjson": "^1.0.1",
|
||||
"@medv/blessed": "^1.0.0",
|
||||
"chalk": "^2.4.1",
|
||||
"editor-widget": "^1.1.1",
|
||||
"indent-string": "^3.2.0",
|
||||
"neo-blessed": "^0.2.0",
|
||||
"string-width": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^0.25.0",
|
||||
"pkg": "^4.3.4",
|
||||
"release-it": "^7.6.2"
|
||||
"release-it": "^8.2.0"
|
||||
}
|
||||
}
|
||||
|
86
print.js
Normal file
86
print.js
Normal file
@ -0,0 +1,86 @@
|
||||
'use strict'
|
||||
const indent = require('indent-string')
|
||||
const config = require('./config')
|
||||
|
||||
function print(input, expanded = null) {
|
||||
const index = new Map()
|
||||
let row = 0
|
||||
|
||||
function doPrint(v, path = '') {
|
||||
index.set(row, path)
|
||||
|
||||
const eol = () => {
|
||||
row++
|
||||
return '\n'
|
||||
}
|
||||
|
||||
if (typeof v === 'undefined') {
|
||||
return void 0
|
||||
}
|
||||
|
||||
if (v === null) {
|
||||
return config.null(v)
|
||||
}
|
||||
|
||||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||||
return config.number(v)
|
||||
}
|
||||
|
||||
if (typeof v === 'boolean') {
|
||||
return config.boolean(v)
|
||||
|
||||
}
|
||||
|
||||
if (typeof v === 'string') {
|
||||
return config.string(JSON.stringify(v))
|
||||
}
|
||||
|
||||
if (Array.isArray(v)) {
|
||||
if (expanded && !expanded.has(path)) {
|
||||
return config.bracket('[') + '\u2026' + config.bracket(']')
|
||||
}
|
||||
|
||||
let output = config.bracket('[') + eol()
|
||||
|
||||
const len = v.length
|
||||
let i = 0
|
||||
|
||||
for (let item of v) {
|
||||
const value = typeof item === 'undefined' ? null : item // JSON.stringify compatibility
|
||||
output += indent(doPrint(value, path + '[' + i + ']'))
|
||||
output += i++ < len - 1 ? config.comma(',') : ''
|
||||
output += eol()
|
||||
}
|
||||
|
||||
return output + config.bracket(']')
|
||||
}
|
||||
|
||||
if (typeof v === 'object' && v.constructor === Object) {
|
||||
if (expanded && !expanded.has(path)) {
|
||||
return config.bracket('{') + '\u2026' + config.bracket('}')
|
||||
}
|
||||
|
||||
let output = config.bracket('{') + eol()
|
||||
|
||||
const entries = Object.entries(v)
|
||||
.filter(([key, value]) => typeof value !== 'undefined') // JSON.stringify compatibility
|
||||
const len = entries.length
|
||||
|
||||
let i = 0
|
||||
for (let [key, value] of entries) {
|
||||
const part = config.key(JSON.stringify(key)) + config.colon(':') + ' ' + doPrint(value, path + '.' + key)
|
||||
output += indent(part, config.space)
|
||||
output += i++ < len - 1 ? config.comma(',') : ''
|
||||
output += eol()
|
||||
}
|
||||
|
||||
return output + config.bracket('}')
|
||||
}
|
||||
|
||||
return v.toString()
|
||||
}
|
||||
|
||||
return [doPrint(input), index]
|
||||
}
|
||||
|
||||
module.exports = print
|
34
reduce.js
Normal file
34
reduce.js
Normal file
@ -0,0 +1,34 @@
|
||||
'use strict'
|
||||
|
||||
function reduce(json, code) {
|
||||
if (/^\w+\s*=>/.test(code)) {
|
||||
const fx = eval(code)
|
||||
return fx(json)
|
||||
}
|
||||
|
||||
if (/yield/.test(code)) {
|
||||
const fx = eval(`
|
||||
function fn() {
|
||||
const gen = (function*(){
|
||||
${code.replace(/\\\n/g, '')}
|
||||
}).call(this)
|
||||
return [...gen]
|
||||
}; fn
|
||||
`)
|
||||
return fx.call(json)
|
||||
}
|
||||
|
||||
if ('?' === code) {
|
||||
return Object.keys(json)
|
||||
}
|
||||
|
||||
if (/^\./.test(code)) {
|
||||
const fx = eval(`function fn() { return ${code === '.' ? 'this' : 'this' + code} }; fn`)
|
||||
return fx.call(json)
|
||||
}
|
||||
|
||||
const fx = eval(`function fn() { return ${code} }; fn`)
|
||||
return fx.call(json)
|
||||
}
|
||||
|
||||
module.exports = reduce
|
Loading…
Reference in New Issue
Block a user