Skip to content

Commit

Permalink
Add interactive mode
Browse files Browse the repository at this point in the history
  • Loading branch information
antonmedv committed Nov 3, 2018
1 parent 8407669 commit 961ee62
Show file tree
Hide file tree
Showing 4 changed files with 329 additions and 32 deletions.
50 changes: 38 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -10,6 +13,7 @@ Command-line JSON processing tool
* Plain JavaScript
* Formatting and highlighting
* Standalone binary
* Interactive mode 🎉

## Install

Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -106,7 +113,7 @@ $ echo '{"count": 0}' | fx '{...this, count: 1}'
}
```

### Use npm package
### Using packages

Use any npm package by installing it globally:
```
Expand Down Expand Up @@ -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

Expand Down
250 changes: 250 additions & 0 deletions fx.js
Original file line number Diff line number Diff line change
@@ -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()
}

0 comments on commit 961ee62

Please sign in to comment.