New features

- Arrow navigation
- Interactive digger
- Themes support
js-version
Anton Medvedev 6 years ago
parent 851427d8c8
commit f110142160

@ -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,
}

287
fx.js

@ -1,11 +1,17 @@
'use strict'
const fs = require('fs') const fs = require('fs')
const tty = require('tty') const tty = require('tty')
const blessed = require('neo-blessed') const blessed = require('@medv/blessed')
const stringWidth = require('string-width') const stringWidth = require('string-width')
const indent = require('indent-string') const reduce = require('./reduce')
const chalk = require('chalk') 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 ttyFd = fs.openSync('/dev/tty', 'r+')
const program = blessed.program({ const program = blessed.program({
@ -18,11 +24,6 @@ module.exports = function start(input) {
smartCSR: true, smartCSR: true,
fullUnicode: 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({ const box = blessed.box({
parent: screen, parent: screen,
@ -31,8 +32,10 @@ module.exports = function start(input) {
top: 0, top: 0,
width: '100%', width: '100%',
height: '100%', height: '100%',
mouse: true,
keys: true, keys: true,
vi: true, vi: true,
ignoreArrows: true,
alwaysScroll: true, alwaysScroll: true,
scrollable: true, scrollable: true,
}) })
@ -44,170 +47,143 @@ module.exports = function start(input) {
width: '100%', width: '100%',
}) })
const scrollSpeed = (() => { const input = blessed.textbox({
let prev = new Date() parent: screen,
return () => { bottom: 0,
const now = new Date() left: 0,
const lines = now - prev < 20 ? 3 : 1 // TODO: Speed based on terminal. height: 1,
prev = now width: '100%',
return lines })
}
})()
box.on('wheeldown', function () { screen.title = filename
box.scroll(scrollSpeed()) input.hide()
screen.render()
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)
}) })
box.on('wheelup', function () {
box.scroll(-scrollSpeed()) screen.on('resize', render)
screen.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.focus()
program.cursorPos(0, 0)
render()
}) })
// TODO: fx input input.on('update', function (code) {
// const inputBar = blessed.textbox({ if (code && code.length !== 0) {
// parent: screen, try {
// bottom: 0, const pretender = reduce(source, code)
// left: 0, if (typeof pretender !== 'undefined') {
// height: 1, json = pretender
// width: '100%', }
// keys: true, } catch (e) {
// mouse: true, // pass
// inputOnFocus: true, }
// }) }
render()
})
const expanded = new Set()
expanded.add('') // Root of JSON box.key(':', function () {
box.height = '100%-1'
input.show()
input.readInput()
render()
})
box.key('e', function () { box.key('e', function () {
walk(input, path => expanded.size < 1000 && expanded.add(path)) walk(json, path => expanded.size < 1000 && expanded.add(path))
render() render()
}) })
box.key('S-e', function () { box.key('S-e', function () {
expanded.clear() expanded.clear()
expanded.add('') expanded.add('')
render() render()
}) })
function walk(v, cb, path = '') { box.key('up', function () {
if (!v) { program.showCursor()
return
} const pos = box.childBase + program.y
const rest = [...index.keys()].filter(i => i < pos)
if (Array.isArray(v)) { if (rest.length > 0) {
cb(path) const next = Math.max(...rest)
let i = 0 const y = next - box.childBase
for (let item of v) { if (y <= 0) {
walk(item, cb, path + '[' + (i++) + ']') 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) { box.key('down', function () {
cb(path) program.showCursor()
let i = 0
for (let [key, value] of Object.entries(v)) { const pos = box.childBase + program.y
walk(value, cb, path + '.' + key) 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'
}
if (typeof v === 'undefined') {
return void 0
}
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 + ']' 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 === 'object' && v.constructor === Object) { box.key('left', function () {
index.set(row, path) program.showCursor()
const pos = box.childBase + program.y
if (!expanded.has(path)) { const path = index.get(pos)
return '{\u2026}' if (expanded.has(path)) {
} expanded.delete(path)
render()
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) { box.on('click', function (mouse) {
program.hideCursor()
const pos = box.childBase + mouse.y const pos = box.childBase + mouse.y
const line = box.getScreenLines()[pos] const line = box.getScreenLines()[pos]
if (mouse.x >= stringWidth(line)) { if (mouse.x >= stringWidth(line)) {
return return
} }
const x = line.search(/\S/)
program.cursorPos(mouse.y, x)
const path = index.get(pos) const path = index.get(pos)
if (expanded.has(path)) { if (expanded.has(path)) {
expanded.delete(path) expanded.delete(path)
@ -218,7 +194,12 @@ module.exports = function start(input) {
}) })
function render() { 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. // TODO: Move to own fork of blessed.
let row = 0 let row = 0
@ -252,3 +233,25 @@ module.exports = function start(input) {
box.focus() box.focus()
render() 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)
}
}
}

@ -1,10 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict' 'use strict'
const os = require('os') const os = require('os')
const fs = require('fs')
const path = require('path') const path = require('path')
const pretty = require('@medv/prettyjson')
const {stdin, stdout, stderr} = process const {stdin, stdout, stderr} = process
try { try {
require(path.join(os.homedir(), '.fxrc')) require(path.join(os.homedir(), '.fxrc'))
} catch (err) { } catch (err) {
@ -12,14 +11,21 @@ try {
throw err throw err
} }
} }
const print = require('./print')
const reduce = require('./reduce')
const usage = ` const usage = `
Usage Usage
$ fx [code ...] $ fx [code ...]
Examples Examples
$ fx package.json
$ echo '{"key": "value"}' | fx 'x => x.key' $ echo '{"key": "value"}' | fx 'x => x.key'
value value
$ echo '{"key": "value"}' | fx .key
value
$ echo '[1,2,3]' | fx 'this.map(x => x * 2)' $ echo '[1,2,3]' | fx 'this.map(x => x * 2)'
[2, 4, 6] [2, 4, 6]
@ -32,75 +38,49 @@ const usage = `
$ echo '{"foo": 1, "bar": 2}' | fx ? $ echo '{"foo": 1, "bar": 2}' | fx ?
["foo", "bar"] ["foo", "bar"]
$ echo '{"key": "value"}' | fx .key
value
` `
function main(input) { function main(input) {
let args = process.argv.slice(2)
let filename = 'fx'
if (input === '') { if (input === '') {
stderr.write(usage) if (args.length === 0) {
process.exit(2) 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 json = JSON.parse(input)
const args = process.argv.slice(2)
if (args.length === 0 && stdout.isTTY) { if (args.length === 0 && stdout.isTTY) {
require('./fx')(json) require('./fx')(filename, json)
return 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') stderr.write('undefined\n')
} else if (typeof result === 'string') { } else if (typeof output === 'string') {
console.log(result) console.log(output)
} else if (stdout.isTTY) {
console.log(pretty(result))
} else { } 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() { function run() {
stdin.setEncoding('utf8') stdin.setEncoding('utf8')
if (stdin.isTTY) { if (stdin.isTTY) {
main('') main('')
return
} }
let buff = '' let buff = ''

@ -34,15 +34,15 @@
"node": ">=8" "node": ">=8"
}, },
"dependencies": { "dependencies": {
"@medv/prettyjson": "^1.0.1", "@medv/blessed": "^1.0.0",
"chalk": "^2.4.1", "chalk": "^2.4.1",
"editor-widget": "^1.1.1",
"indent-string": "^3.2.0", "indent-string": "^3.2.0",
"neo-blessed": "^0.2.0",
"string-width": "^2.1.1" "string-width": "^2.1.1"
}, },
"devDependencies": { "devDependencies": {
"ava": "^0.25.0", "ava": "^0.25.0",
"pkg": "^4.3.4", "pkg": "^4.3.4",
"release-it": "^7.6.2" "release-it": "^8.2.0"
} }
} }

@ -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

@ -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…
Cancel
Save