forked from Archives/fx
Add interactive mode
This commit is contained in:
parent
840766996e
commit
961ee629f4
50
README.md
50
README.md
@ -1,4 +1,7 @@
|
||||
<img src="https://user-images.githubusercontent.com/141232/35405308-4b41f446-0238-11e8-86c1-21f407cc8460.png" height="100" alt="fx">
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/141232/35405308-4b41f446-0238-11e8-86c1-21f407cc8460.png" height="100" alt="fx"></p>
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/141232/47933350-f0f22900-df06-11e8-9cf2-88492c1be774.gif" width="530" alt="fx example"></p>
|
||||
|
||||
_* Function eXecution_
|
||||
|
||||
# [![Build Status](https://travis-ci.org/antonmedv/fx.svg?branch=master)](https://travis-ci.org/antonmedv/fx)
|
||||
|
||||
@ -10,6 +13,7 @@ Command-line JSON processing tool
|
||||
* Plain JavaScript
|
||||
* Formatting and highlighting
|
||||
* Standalone binary
|
||||
* Interactive mode 🎉
|
||||
|
||||
## Install
|
||||
|
||||
@ -22,17 +26,13 @@ Or download standalone binary from [releases](https://github.com/antonmedv/fx/re
|
||||
## Usage
|
||||
|
||||
Pipe into `fx` any JSON and anonymous function for reducing it.
|
||||
|
||||
```
|
||||
$ fx [code ...]
|
||||
$ echo '{...}' | fx [code ...]
|
||||
```
|
||||
|
||||
Pretty print JSON without passing any arguments:
|
||||
Start interactive mode without passing any arguments:
|
||||
```
|
||||
$ echo '{"key":"value"}' | fx
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
$ curl ... | fx
|
||||
```
|
||||
|
||||
### Anonymous function
|
||||
@ -43,7 +43,7 @@ $ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo[0].bar'
|
||||
value
|
||||
```
|
||||
|
||||
### This Binding
|
||||
### Binding
|
||||
|
||||
If you don't pass anonymous function `param => ...`, code will be automatically transformed into anonymous function.
|
||||
And you can get access to JSON by `this` keyword:
|
||||
@ -55,12 +55,19 @@ value
|
||||
### Dot
|
||||
|
||||
It is possible to omit `this` keyword:
|
||||
|
||||
```
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
|
||||
value
|
||||
```
|
||||
|
||||
If single dot is passed, JSON will be processed without modification:
|
||||
```
|
||||
$ echo '{"foo": "bar"}' | fx .
|
||||
{
|
||||
"foo": "bar"
|
||||
}
|
||||
```
|
||||
|
||||
### Chain
|
||||
|
||||
You can pass any number of anonymous functions for reducing JSON:
|
||||
@ -106,7 +113,7 @@ $ echo '{"count": 0}' | fx '{...this, count: 1}'
|
||||
}
|
||||
```
|
||||
|
||||
### Use npm package
|
||||
### Using packages
|
||||
|
||||
Use any npm package by installing it globally:
|
||||
```
|
||||
@ -144,13 +151,32 @@ By the way, fx has shortcut for `Object.keys(this)`. Previous example can be rew
|
||||
$ cat package.json | fx this.dependencies ?
|
||||
```
|
||||
|
||||
### Interactive mode
|
||||
|
||||
Start interactive mode without passing any arguments:
|
||||
```
|
||||
$ curl ... | fx
|
||||
```
|
||||
Click on fields to expand or collapse JSON tree, use mouse wheel to scroll view. To select text, press `alt` or `option` key.
|
||||
|
||||
Next commands available in interactive mode:
|
||||
|
||||
| Key | Command |
|
||||
|-----|---------|
|
||||
| `q` or `Esc` or `Ctrl`+`c` | Exit |
|
||||
| `e`/`E` | Expand/Collapse all |
|
||||
| `up`/`down` or `k`/`j` | Scroll up/down one line |
|
||||
| `g`/`G` | Goto top/bottom |
|
||||
|
||||
|
||||
## Related
|
||||
|
||||
* [jq](https://github.com/stedolan/jq) – cli JSON processor on C
|
||||
* [jl](https://github.com/chrisdone/jl) – functional sed for JSON on Haskell
|
||||
* [xx](https://github.com/antonmedv/xx) – `fx`-like JSON tool on Go
|
||||
* [xx](https://github.com/antonmedv/xx) – `fx`-like JSON tool (*go*)
|
||||
* [ymlx](https://github.com/matthewadams/ymlx) - `fx`-like YAML cli processor
|
||||
* [jv](https://github.com/maxzender/jv) – interactive JSON viewer (*go*)
|
||||
* [jid](https://github.com/simeji/jid) – interactive cli tool based on jq (*go*)
|
||||
|
||||
## License
|
||||
|
||||
|
250
fx.js
Normal file
250
fx.js
Normal file
@ -0,0 +1,250 @@
|
||||
const fs = require('fs')
|
||||
const tty = require('tty')
|
||||
const blessed = require('neo-blessed')
|
||||
const stringWidth = require('string-width')
|
||||
const indent = require('indent-string')
|
||||
const chalk = require('chalk')
|
||||
|
||||
module.exports = function start(input) {
|
||||
const ttyFd = fs.openSync('/dev/tty', 'r+')
|
||||
|
||||
const program = blessed.program({
|
||||
input: tty.ReadStream(ttyFd),
|
||||
output: tty.WriteStream(ttyFd),
|
||||
})
|
||||
|
||||
const screen = blessed.screen({
|
||||
program: program,
|
||||
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,
|
||||
tags: true,
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
keys: true,
|
||||
vi: true,
|
||||
alwaysScroll: true,
|
||||
scrollable: true,
|
||||
})
|
||||
|
||||
const test = blessed.box({
|
||||
parent: screen,
|
||||
hidden: true,
|
||||
tags: true,
|
||||
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
|
||||
}
|
||||
})()
|
||||
|
||||
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.add(path))
|
||||
render()
|
||||
})
|
||||
box.key('S-e', function () {
|
||||
expanded.clear()
|
||||
expanded.add('')
|
||||
render()
|
||||
})
|
||||
|
||||
function walk(v, cb, path = '') {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 + ']'
|
||||
}
|
||||
|
||||
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) {
|
||||
const pos = box.childBase + mouse.y
|
||||
const line = box.getScreenLines()[pos]
|
||||
if (mouse.x >= stringWidth(line)) {
|
||||
return
|
||||
}
|
||||
|
||||
const path = index.get(pos)
|
||||
if (expanded.has(path)) {
|
||||
expanded.delete(path)
|
||||
} else {
|
||||
expanded.add(path)
|
||||
}
|
||||
render()
|
||||
})
|
||||
|
||||
function render() {
|
||||
const content = print(input)
|
||||
|
||||
// TODO: Move to own fork of blessed.
|
||||
let row = 0
|
||||
for (let line of content.split('\n')) {
|
||||
if (stringWidth(line) > box.width) {
|
||||
test.setContent(line)
|
||||
const pad = test.getScreenLines().length - 1
|
||||
|
||||
const update = new Map()
|
||||
for (let [i, path] of index.entries()) {
|
||||
if (i > row) {
|
||||
index.delete(i)
|
||||
update.set(i + pad, path)
|
||||
}
|
||||
}
|
||||
|
||||
row += pad
|
||||
|
||||
for (let [i, path] of update.entries()) {
|
||||
index.set(i, path)
|
||||
}
|
||||
|
||||
}
|
||||
row++
|
||||
}
|
||||
|
||||
box.setContent(content)
|
||||
screen.render()
|
||||
}
|
||||
|
||||
box.focus()
|
||||
render()
|
||||
}
|
56
index.js
56
index.js
@ -24,22 +24,32 @@ const usage = `
|
||||
|
||||
$ echo '{"key": "value"}' | fx .key
|
||||
value
|
||||
|
||||
`
|
||||
|
||||
function main(input) {
|
||||
const {stdout, stderr} = process
|
||||
|
||||
if (input === '') {
|
||||
console.log(usage)
|
||||
stderr.write(usage)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const json = JSON.parse(input)
|
||||
const result = process.argv.slice(2).reduce(reduce, json)
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
if (args.length === 0 && stdout.isTTY) {
|
||||
require('./fx')(json)
|
||||
return
|
||||
}
|
||||
|
||||
const result = args.reduce(reduce, json)
|
||||
|
||||
if (typeof result === 'undefined') {
|
||||
process.stderr.write('undefined\n')
|
||||
stderr.write('undefined\n')
|
||||
} else if (typeof result === 'string') {
|
||||
console.log(result)
|
||||
} else if (process.stdout.isTTY) {
|
||||
} else if (stdout.isTTY) {
|
||||
console.log(pretty(result))
|
||||
} else {
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
@ -77,23 +87,27 @@ function reduce(json, code) {
|
||||
return fx.call(json)
|
||||
}
|
||||
|
||||
const stdin = process.stdin
|
||||
let buff = ''
|
||||
function run() {
|
||||
const stdin = process.stdin
|
||||
stdin.setEncoding('utf8')
|
||||
|
||||
if (stdin.isTTY) {
|
||||
main(buff)
|
||||
let buff = ''
|
||||
|
||||
if (stdin.isTTY) {
|
||||
main('')
|
||||
}
|
||||
|
||||
stdin.on('readable', () => {
|
||||
let chunk
|
||||
|
||||
while ((chunk = stdin.read())) {
|
||||
buff += chunk
|
||||
}
|
||||
})
|
||||
|
||||
stdin.on('end', () => {
|
||||
main(buff)
|
||||
})
|
||||
}
|
||||
|
||||
stdin.setEncoding('utf8')
|
||||
|
||||
stdin.on('readable', () => {
|
||||
let chunk
|
||||
|
||||
while ((chunk = stdin.read())) {
|
||||
buff += chunk
|
||||
}
|
||||
})
|
||||
|
||||
stdin.on('end', () => {
|
||||
main(buff)
|
||||
})
|
||||
run()
|
||||
|
@ -15,6 +15,9 @@
|
||||
"test": "ava",
|
||||
"release": "pkg . --out-path dist && release-it --github.release --github.assets=dist/*"
|
||||
},
|
||||
"pkg": {
|
||||
"scripts": "node_modules/neo-blessed/lib/**/*.js"
|
||||
},
|
||||
"keywords": [
|
||||
"json",
|
||||
"cli"
|
||||
@ -23,7 +26,11 @@
|
||||
"node": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medv/prettyjson": "^1.0.1"
|
||||
"@medv/prettyjson": "^1.0.1",
|
||||
"chalk": "^2.4.1",
|
||||
"indent-string": "^3.2.0",
|
||||
"neo-blessed": "^0.2.0",
|
||||
"string-width": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^0.25.0",
|
||||
|
Loading…
Reference in New Issue
Block a user