forked from Archives/fx
Compare commits
No commits in common. 'master' and 'js-version' have entirely different histories.
master
...
js-version
@ -1,23 +0,0 @@
|
||||
let goos = [
|
||||
'linux',
|
||||
'darwin',
|
||||
'windows',
|
||||
]
|
||||
let goarch = [
|
||||
'amd64',
|
||||
'arm64',
|
||||
]
|
||||
|
||||
let name = (GOOS, GOARCH) => `fx_${GOOS}_${GOARCH}` + (GOOS === 'windows' ? '.exe' : '')
|
||||
|
||||
await $`go mod download`
|
||||
|
||||
await Promise.all(
|
||||
goos.flatMap(GOOS =>
|
||||
goarch.map(GOARCH =>
|
||||
$`GOOS=${GOOS} GOARCH=${GOARCH} go build -o ${name(GOOS, GOARCH)}`)))
|
||||
|
||||
await Promise.all(
|
||||
goos.flatMap(GOOS =>
|
||||
goarch.map(GOARCH =>
|
||||
$`gh release upload ${process.env.RELEASE_VERSION} ${name(GOOS, GOARCH)}`)))
|
@ -1,116 +0,0 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
|
||||
jobs:
|
||||
commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
|
||||
- name: Get Version
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Update Version
|
||||
shell: bash
|
||||
run: |
|
||||
set -x
|
||||
sed -i "s/version = .*/version = \"${RELEASE_VERSION}\"/" version.go
|
||||
sed -i "s/version: .*/version: ${RELEASE_VERSION}/" snap/snapcraft.yaml
|
||||
git add version.go snap/snapcraft.yaml
|
||||
git config --global user.email "github-actions@github.com"
|
||||
git config --global user.name "github-actions"
|
||||
git commit -m "Release $RELEASE_VERSION"
|
||||
git tag "$RELEASE_VERSION" --force
|
||||
git push --atomic --force origin master "$RELEASE_VERSION"
|
||||
|
||||
- name: Test
|
||||
run: go test -v ./...
|
||||
|
||||
binary:
|
||||
needs: [commit]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
|
||||
- name: Get Version
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and Upload
|
||||
env:
|
||||
FORCE_COLOR: 3
|
||||
GITHUB_TOKEN: ${{ secrets.MY_TOKEN }}
|
||||
run: npx zx .github/workflows/release.mjs
|
||||
|
||||
snap:
|
||||
needs: [commit]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: master
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
|
||||
- uses: snapcore/action-build@v1
|
||||
id: build
|
||||
|
||||
- uses: snapcore/action-publish@v1
|
||||
with:
|
||||
store_login: ${{ secrets.STORE_LOGIN }}
|
||||
snap: ${{ steps.build.outputs.snap }}
|
||||
release: stable
|
||||
|
||||
brew:
|
||||
needs: [commit]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Set up Homebrew
|
||||
id: set-up-homebrew
|
||||
uses: Homebrew/actions/setup-homebrew@master
|
||||
with:
|
||||
test-bot: false
|
||||
|
||||
- name: Cache Homebrew Bundler RubyGems
|
||||
id: cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.set-up-homebrew.outputs.gems-path }}
|
||||
key: ${{ runner.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}
|
||||
restore-keys: ${{ runner.os }}-rubygems-
|
||||
|
||||
- name: Install Homebrew Bundler RubyGems
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: brew install-bundler-gems
|
||||
|
||||
- name: Configure Git user
|
||||
uses: Homebrew/actions/git-user-config@master
|
||||
|
||||
- name: Update brew
|
||||
run: brew update
|
||||
|
||||
- name: Bump formulae
|
||||
uses: Homebrew/actions/bump-formulae@master
|
||||
with:
|
||||
token: ${{ secrets.MY_TOKEN }}
|
||||
formulae: fx
|
@ -1,21 +0,0 @@
|
||||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.18
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
@ -0,0 +1,3 @@
|
||||
/node_modules/
|
||||
/dist/
|
||||
package-lock.json
|
@ -0,0 +1,4 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "node"
|
||||
- "11"
|
@ -0,0 +1,331 @@
|
||||
# Documentation
|
||||
|
||||
* [Getting started](#getting-started)
|
||||
* [Usage](#usage)
|
||||
+ [Anonymous function](#anonymous-function)
|
||||
+ [Binding](#binding)
|
||||
+ [Dot](#dot)
|
||||
+ [Map](#map)
|
||||
+ [Chaining](#chaining)
|
||||
+ [Updating](#updating)
|
||||
+ [Edit-in-place](#edit-in-place)
|
||||
+ [Using packages](#using-packages)
|
||||
* [Using .fxrc](#using-fxrc)
|
||||
+ [Query language](#query-language)
|
||||
* [Formatting](#formatting)
|
||||
* [Other examples](#other-examples)
|
||||
* [Streaming mode](#streaming-mode)
|
||||
+ [Filtering](#filtering)
|
||||
* [Interactive mode](#interactive-mode)
|
||||
+ [Searching](#searching)
|
||||
+ [Selecting text](#selecting-text)
|
||||
* [Memory Usage](#memory-usage)
|
||||
|
||||
## Getting started
|
||||
|
||||
`fx` can work in two modes: cli and interactive. To start interactive mode pipe any JSON into `fx`:
|
||||
|
||||
```bash
|
||||
$ curl ... | fx
|
||||
```
|
||||
|
||||
Or you can pass a filename as the first parameter:
|
||||
|
||||
```bash
|
||||
$ fx my.json
|
||||
```
|
||||
|
||||
If any argument was passed, `fx` will apply it and prints to stdout.
|
||||
|
||||
## Usage
|
||||
|
||||
### Anonymous function
|
||||
|
||||
Use an anonymous function as reducer which gets JSON and processes it:
|
||||
```bash
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo[0].bar'
|
||||
value
|
||||
```
|
||||
|
||||
### 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:
|
||||
```bash
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx 'this.foo[0].bar'
|
||||
value
|
||||
```
|
||||
|
||||
### Dot
|
||||
|
||||
It is possible to omit `this` keyword:
|
||||
```bash
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
|
||||
value
|
||||
```
|
||||
|
||||
If a single dot is passed, the input JSON will be formatted but otherwise unaltered:
|
||||
```bash
|
||||
$ echo '{"foo": "bar"}' | fx .
|
||||
{
|
||||
"foo": "bar"
|
||||
}
|
||||
```
|
||||
|
||||
### Map
|
||||
|
||||
One of the frequent operations is mapping some function on an array. For example, to extract some values.
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"author": {
|
||||
"name": "antonmedv"
|
||||
}
|
||||
},
|
||||
{...},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
And we want to collect names of each object in the array. We can do this by mapping anonymous function:
|
||||
|
||||
```bash
|
||||
$ cat ... | fx '.map(x => x.author.name)'
|
||||
```
|
||||
|
||||
Or we can do the same by using jq-like syntax:
|
||||
|
||||
```bash
|
||||
$ cat ... | fx .[].author.name
|
||||
[
|
||||
"antonmedv",
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
> Note what `[]` can be applied to map object values.
|
||||
> ```bash
|
||||
> $ echo '{"foo": 1, "bar": 2}' | fx .[]
|
||||
> [1, 2]
|
||||
> ```
|
||||
|
||||
|
||||
### Chaining
|
||||
|
||||
You can pass any number of anonymous functions for reducing JSON:
|
||||
```bash
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo' 'this[0]' 'this.bar'
|
||||
value
|
||||
```
|
||||
|
||||
### Updating
|
||||
|
||||
You can update existing JSON using the spread operator:
|
||||
|
||||
```bash
|
||||
$ echo '{"count": 0}' | fx '{...this, count: 1}'
|
||||
{
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
### Edit-in-place
|
||||
|
||||
`fx` provides a function `save` which will save everything in place and return saved object.
|
||||
This function can be only used with filename as first argument to `fx` command.
|
||||
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
fx data.json '{...this, count: this.count+1}' save .count
|
||||
```
|
||||
|
||||
### Using packages
|
||||
|
||||
Use any npm package by installing it globally:
|
||||
```bash
|
||||
$ npm install -g lodash
|
||||
$ cat package.json | fx 'require("lodash").keys(this.dependencies)'
|
||||
```
|
||||
|
||||
## Using .fxrc
|
||||
|
||||
Create _.fxrc_ file in `$HOME` directory, and require any packages or define global functions.
|
||||
|
||||
For example, access all lodash methods without `_` prefix. Put in your `.fxrc` file:
|
||||
|
||||
```js
|
||||
Object.assign(global, require('lodash/fp'))
|
||||
```
|
||||
|
||||
And now you will be able to call all lodash methods. For example, see who's been committing to react recently:
|
||||
|
||||
```bash
|
||||
curl 'https://api.github.com/repos/facebook/react/commits?per_page=100' \
|
||||
| fx 'groupBy("commit.author.name")' 'mapValues(size)' toPairs 'sortBy(1)' reverse 'take(10)' fromPairs
|
||||
```
|
||||
|
||||
> To be able require global modules make sure you have correct `NODE_PATH` env variable.
|
||||
> ```bash
|
||||
> export NODE_PATH=`npm root -g`
|
||||
> ```
|
||||
|
||||
### Query language
|
||||
|
||||
If you want to use query language, for example [jsonata](http://jsonata.org/) you can use helper function like this:
|
||||
|
||||
```js
|
||||
global.jsonata = expr => require('jsonata')(expr).evaluate
|
||||
```
|
||||
|
||||
And use it like this:
|
||||
|
||||
```bash
|
||||
curl ... | fx 'jsonata("$sum(Order.Product.(Price * Quantity))")'
|
||||
```
|
||||
|
||||
Instead you can create next alias in _.bashrc_ file:
|
||||
|
||||
```bash
|
||||
alias jsonata='FX_APPLY=jsonata fx'
|
||||
```
|
||||
|
||||
And now all code arguments to `jsonata` will be passed through `jsonata` helper. And now you can use it like this:
|
||||
|
||||
```bash
|
||||
curl ... | jsonata '$sum(Order.Product.(Price * Quantity))'
|
||||
```
|
||||
|
||||
## Formatting
|
||||
|
||||
If you need output other than JSON (for example arguments for xargs), do not return anything from the reducer.
|
||||
`undefined` value is printed into stderr by default.
|
||||
```bash
|
||||
echo '[]' | fx 'void 0'
|
||||
undefined
|
||||
```
|
||||
|
||||
```bash
|
||||
echo '[1,2,3]' | fx 'this.forEach(x => console.log(+x))' 2>/dev/null | xargs echo
|
||||
1 2 3
|
||||
```
|
||||
|
||||
## Other examples
|
||||
|
||||
Convert object to array:
|
||||
```bash
|
||||
$ cat package.json | fx 'Object.keys(this.dependencies)'
|
||||
```
|
||||
|
||||
Or by two functions:
|
||||
```bash
|
||||
$ cat package.json | fx .dependencies Object.keys
|
||||
```
|
||||
|
||||
By the way, fx has shortcut for `Object.keys`. Previous example can be rewritten as:
|
||||
|
||||
```bash
|
||||
$ cat package.json | fx .dependencies ?
|
||||
```
|
||||
|
||||
## Streaming mode
|
||||
|
||||
`fx` supports line-delimited JSON and concatenated JSON streaming.
|
||||
|
||||
```bash
|
||||
$ kubectl logs ... | fx .message
|
||||
```
|
||||
|
||||
> Note what is object lacks `message` field, _undefined_ will be printed to stderr.
|
||||
> This is useful to see if you are skipping some objects. But if you want to hide them,
|
||||
> redirect stderr to `/dev/null`.
|
||||
|
||||
### Filtering
|
||||
|
||||
Sometimes it is necessary to omit some messages in JSON stream, or select only specified log messages.
|
||||
For this purpose, `fx` has special helpers `select`/`filter`, pass function into it to select/filter JSON messages.
|
||||
|
||||
```bash
|
||||
$ kubectl logs ... | fx 'select(x => x.status == 500)' .message
|
||||
```
|
||||
|
||||
```bash
|
||||
$ kubectl logs ... | fx 'filter(x => x.status < 499)' .message
|
||||
```
|
||||
|
||||
> If `filter`/`select` overridden in _.fxrc_ you still able to access them with prefix:
|
||||
> `std.select(cb)` or `std.filter(cd)`
|
||||
|
||||
## Interactive mode
|
||||
|
||||
Click on fields to expand or collapse JSON tree, use mouse wheel to scroll view.
|
||||
|
||||
Next commands available in interactive mode:
|
||||
|
||||
| Key | Command |
|
||||
|-------------------------------|----------------------------------------------|
|
||||
| `q` or `Esc` or `Ctrl`+`c` | Exit |
|
||||
| `up` or `k` | Move cursor up |
|
||||
| `down` or `j` | Move cursor down |
|
||||
| `left` or `h` | Collapse |
|
||||
| `right` or `l` | Expand |
|
||||
| `Shift`+`right` or `L` | Expand all under cursor |
|
||||
| `Shift`+`left` or `K` | Collapse all under cursor |
|
||||
| `e` | Expand all |
|
||||
| `E` | Collapse all |
|
||||
| `g` | Scroll to top |
|
||||
| `G` | Scroll to bottom |
|
||||
| `.` | Edit filter |
|
||||
| `/` | Search |
|
||||
| `n` | Find next |
|
||||
| `p` | Exit and print JSON to stdout |
|
||||
| `P` | Exit and print fully expanded JSON to stdout |
|
||||
|
||||
These commands are available when editing the filter:
|
||||
|
||||
| Key | Command |
|
||||
|-------------------------------|-------------------------|
|
||||
| `Enter` | Apply filter |
|
||||
| `Ctrl`+`u` | Clear filter |
|
||||
| `Ctrl`+`w` | Delete last part |
|
||||
| `up`/`down` | Select autocomplete |
|
||||
|
||||
### Searching
|
||||
|
||||
Press `/` and type regexp pattern to search in current JSON. Search work with currently applied filter.
|
||||
|
||||
Examples of pattern and corresponding regexp:
|
||||
|
||||
| Pattern | RegExp |
|
||||
|------------|-------------|
|
||||
| `/apple` | `/apple/ig` |
|
||||
| `/apple/` | `/apple/` |
|
||||
| `/apple/u` | `/apple/u` |
|
||||
| `/\w+` | `/\w+/ig` |
|
||||
|
||||
### Selecting text
|
||||
|
||||
You may found what you can't just select text in fx. This is due the fact that all mouse events redirected to stdin. To be able select again you need instruct your terminal not to do it. This can be done by holding special keys while selecting:
|
||||
|
||||
| Key | Terminal |
|
||||
|------------------|---------------|
|
||||
| `Option`+`Mouse` | iTerm2, Hyper |
|
||||
| `Fn`+`Mouse` | Terminal.app |
|
||||
| `Shift`+`Mouse` | Linux |
|
||||
|
||||
> Note what you can press `p`/`P` to print everything to stdout and select if there.
|
||||
|
||||
## Memory Usage
|
||||
|
||||
You may find that sometimes, on really big JSON files, fx prints an error message like this:
|
||||
|
||||
```
|
||||
FATAL ERROR: JavaScript heap out of memory
|
||||
```
|
||||
|
||||
V8 limits memory usage to around 2 GB by default. You can increase the limit by putting this line in your _.profile_:
|
||||
|
||||
```bash
|
||||
export NODE_OPTIONS='--max-old-space-size=8192'
|
||||
```
|
@ -0,0 +1,79 @@
|
||||
'use strict'
|
||||
// http://bit.ly/fx--life
|
||||
|
||||
const
|
||||
run = f => setInterval(f, 16),
|
||||
p = (s) => process.stdout.write(s),
|
||||
esc = (...x) => x.map(i => p('\u001B[' + i)),
|
||||
[upper, lower, full] = ['\u2580', '\u2584', '\u2588'],
|
||||
{columns, rows} = process.stdout,
|
||||
[w, h] = [columns, rows * 2],
|
||||
a = Math.floor(w / 2) - 6, b = Math.floor(h / 2) - 7
|
||||
|
||||
let $ = Array(w * h).fill(false)
|
||||
if (Date.now() % 3 === 0) {
|
||||
$[1 + 5 * w] = $[1 + 6 * w] = $[2 + 5 * w] = $[2 + 6 * w] = $[12 + 5 * w] = $[12 + 6 * w] = $[12 + 7 * w] = $[13 + 4 * w] = $[13 + 8 * w] = $[14 + 3 * w] = $[14 + 9 * w] = $[15 + 4 * w] = $[15 + 8 * w] = $[16 + 5 * w] = $[16 + 6 * w] = $[16 + 7 * w] = $[17 + 5 * w] = $[17 + 6 * w] = $[17 + 7 * w] = $[22 + 3 * w] = $[22 + 4 * w] = $[22 + 5 * w] = $[23 + 2 * w] = $[23 + 3 * w] = $[23 + 5 * w] = $[23 + 6 * w] = $[24 + 2 * w] = $[24 + 3 * w] = $[24 + 5 * w] = $[24 + 6 * w] = $[25 + 2 * w] = $[25 + 3 * w] = $[25 + 4 * w] = $[25 + 5 * w] = $[25 + 6 * w] = $[26 + 1 * w] = $[26 + 2 * w] = $[26 + 6 * w] = $[26 + 7 * w] = $[35 + 3 * w] = $[35 + 4 * w] = $[36 + 3 * w] = $[36 + 4 * w] = true
|
||||
} else if (Date.now() % 3 === 1) {
|
||||
for (let i = 0; i < $.length; i-=-1)
|
||||
if (Math.random() < 0.16) $[i] = true
|
||||
} else {
|
||||
$[a + 1 + (2 + b) * w] = $[a + 2 + (1 + b) * w] = $[a + 2 + (3 + b) * w] = $[a + 3 + (2 + b) * w] = $[a + 5 + (15 + b) * w] = $[a + 6 + (13 + b) * w] = $[a + 6 + (15 + b) * w] = $[a + 7 + (12 + b) * w] = $[a + 7 + (13 + b) * w] = $[a + 7 + (15 + b) * w] = $[a + 9 + (11 + b) * w] = $[a + 9 + (12 + b) * w] = $[a + 9 + (13 + b) * w] = true
|
||||
}
|
||||
|
||||
function at(i, j) {
|
||||
if (i < 0) i = h - 1
|
||||
if (i >= h) i = 0
|
||||
if (j < 0) j = w - 1
|
||||
if (j >= w) j = 0
|
||||
return $[i * w + j]
|
||||
}
|
||||
|
||||
function neighbors(i, j) {
|
||||
let c = 0
|
||||
at(i - 1, j - 1) && c++
|
||||
at(i - 1, j) && c++
|
||||
at(i - 1, j + 1) && c++
|
||||
at(i, j - 1) && c++
|
||||
at(i, j + 1) && c++
|
||||
at(i + 1, j - 1) && c++
|
||||
at(i + 1, j) && c++
|
||||
at(i + 1, j + 1) && c++
|
||||
return c
|
||||
}
|
||||
|
||||
run(() => {
|
||||
esc('H')
|
||||
|
||||
let gen = Array(w * h).fill(false)
|
||||
for (let i = 0; i < h; i-=-1) {
|
||||
for (let j = 0; j < w; j-=-1) {
|
||||
const n = neighbors(i, j)
|
||||
const z = i * w + j
|
||||
if ($[z]) {
|
||||
if (n < 2) gen[z] = false
|
||||
if (n === 2 || n === 3) gen[z] = true
|
||||
if (n > 3) gen[z] = false
|
||||
} else {
|
||||
if (n === 3) gen[z] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
$ = gen
|
||||
|
||||
for (let i = 0; i < rows; i-=-1) {
|
||||
for (let j = 0; j < columns; j-=-1) {
|
||||
if ($[i * 2 * w + j] && $[(i * 2 + 1) * w + j]) p(full)
|
||||
else if ($[i * 2 * w + j] && !$[(i * 2 + 1) * w + j]) p(upper)
|
||||
else if (!$[i * 2 * w + j] && $[(i * 2 + 1) * w + j]) p(lower)
|
||||
else p(' ')
|
||||
}
|
||||
if (i !== rows - 1) p('\n')
|
||||
}
|
||||
})
|
||||
|
||||
esc('2J', '?25l')
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
esc('?25h')
|
||||
process.exit(2)
|
||||
})
|
@ -0,0 +1,26 @@
|
||||
'use strict'
|
||||
const chalk = require('chalk')
|
||||
const noop = x => x
|
||||
const list = {
|
||||
fg: 'black',
|
||||
bg: 'cyan',
|
||||
selected: {
|
||||
bg: 'magenta'
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
list: global.FX_STYLE_LIST || list,
|
||||
highlight: global.FX_STYLE_HIGHLIGHT || chalk.black.bgYellow,
|
||||
highlightCurrent: global.FX_STYLE_HIGHLIGHT_CURRENT || chalk.inverse,
|
||||
statusBar: global.FX_STYLE_STATUS_BAR || chalk.inverse,
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
# Documentation
|
||||
|
||||
The **fx** can work in two modes: as a reducer or an interactive viewer.
|
||||
|
||||
To start the interactive mode pipe a JSON into **fx**:
|
||||
|
||||
```sh
|
||||
$ curl ... | fx
|
||||
```
|
||||
|
||||
Or you can pass a filename as the first parameter:
|
||||
|
||||
```sh
|
||||
$ fx data.json
|
||||
```
|
||||
|
||||
## Reducers
|
||||
|
||||
Use [JavaScript](js.md), [Python](python.md), or [Ruby](ruby.md).
|
||||
|
||||
## Streaming mode
|
||||
|
||||
The **fx** supports line-delimited JSON streaming or concatenated JSON streaming.
|
||||
|
||||
```sh
|
||||
$ echo '
|
||||
> {"message": "hello"}
|
||||
> {"message": "world!"}
|
||||
> ' | fx .message
|
||||
hello
|
||||
world!
|
||||
```
|
||||
|
||||
## Interactive mode
|
||||
|
||||
Type `?` to see the full list of available shortcuts while in the interactive mode.
|
||||
|
||||
### Search
|
||||
|
||||
Press `/` and type regexp pattern to search in the current JSON.
|
||||
Search is performed on the internal representation of the JSON without newlines.
|
||||
|
||||
Type `n` to jump to the next result, and `N` to the previous
|
||||
|
||||
### Selecting text
|
||||
|
||||
You can't just select text in fx. This is due to the fact that all mouse events are
|
||||
redirected to stdin. To be able to select again you need to instruct your terminal
|
||||
not to do it. This can be done by holding special keys while selecting:
|
||||
|
||||
| Key | Terminal |
|
||||
|------------------|---------------|
|
||||
| `Option`+`Mouse` | iTerm2, Hyper |
|
||||
| `Fn`+`Mouse` | Terminal.app |
|
||||
| `Shift`+`Mouse` | Linux |
|
||||
|
||||
|
||||
## Configs
|
||||
|
||||
Next configs available for **fx** via environment variables.
|
||||
|
||||
| Name | Values | Description |
|
||||
|----------------|-----------------------------------------------------|-------------------------------------------------------|
|
||||
| `FX_LANG` | `js` (default), `node`, `python`, `python3`, `ruby` | Reducer type. |
|
||||
| `FX_THEME` | `0` (disable colors), `1` (default), `2..9` | Color theme. |
|
||||
| `FX_SHOW_SIZE` | `true` or `false` (default) | Show size of arrays and object in collapsed previews. |
|
Binary file not shown.
Before Width: | Height: | Size: 411 KiB |
Binary file not shown.
Before Width: | Height: | Size: 659 KiB |
@ -1,120 +0,0 @@
|
||||
# JavaScript Reducers
|
||||
|
||||
If any additional arguments were passed, fx converts them into a function which
|
||||
takes the JSON as an argument named `x`.
|
||||
|
||||
By default, fx uses builtin JavaScript VM ([goja](https://github.com/dop251/goja)),
|
||||
but also can be used with node.
|
||||
|
||||
```sh
|
||||
export FX_LANG=js # Default
|
||||
```
|
||||
|
||||
Or for usage with node:
|
||||
|
||||
```sh
|
||||
export FX_LANG=node
|
||||
```
|
||||
|
||||
An example of anonymous function used as a reducer:
|
||||
```sh
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo[0].bar'
|
||||
value
|
||||
```
|
||||
|
||||
The same reducer function can be simplified to:
|
||||
|
||||
```sh
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x.foo[0].bar'
|
||||
value
|
||||
```
|
||||
|
||||
Each argument treated as a reducer function.
|
||||
|
||||
```sh
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x.foo' 'x[0]' 'x.bar'
|
||||
value
|
||||
```
|
||||
|
||||
Update JSON using the spread operator:
|
||||
|
||||
```sh
|
||||
$ echo '{"name": "fx", "count": 0}' | fx '{...this, count: 1}'
|
||||
{
|
||||
"name": "fx",
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
## Dot
|
||||
|
||||
Fx supports simple JS-like syntax for accessing data, which can be used with any `FX_LANG`.
|
||||
|
||||
```sh
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
|
||||
value
|
||||
```
|
||||
|
||||
## .fxrc.js
|
||||
|
||||
Create _.fxrc.js_ file in `$HOME` directory, and define some useful functions.
|
||||
|
||||
```js
|
||||
// .fxrc.js
|
||||
function upper(s) {
|
||||
return s.toUpperCase()
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
$ cat data.json | fx .name upper
|
||||
ANTON
|
||||
```
|
||||
|
||||
## Node
|
||||
|
||||
```sh
|
||||
export FX_LANG=node
|
||||
```
|
||||
|
||||
Use any npm package by installing it globally. Create _.fxrc.js_ file in `$HOME`
|
||||
directory, and require any packages or define global functions. For example,
|
||||
to access all lodash methods without `_` prefix, put next line into your
|
||||
_.fxrc.js_ file:
|
||||
|
||||
```js
|
||||
Object.assign(global, require('lodash/fp'))
|
||||
```
|
||||
|
||||
And now you will be able to call all lodash methods. For example, see who's been
|
||||
committing to react recently:
|
||||
|
||||
```sh
|
||||
curl 'https://api.github.com/repos/facebook/react/commits?per_page=100' \
|
||||
| fx 'groupBy("commit.author.name")' 'mapValues(size)' toPairs 'sortBy(1)' reverse 'take(10)' fromPairs
|
||||
```
|
||||
|
||||
> To be able to require global modules make sure you have correct `NODE_PATH` env variable.
|
||||
> ```sh
|
||||
> export NODE_PATH=`npm root -g`
|
||||
> ```
|
||||
|
||||
The _.fxrc.js_ file supports both: `import` and `require`.
|
||||
|
||||
```js
|
||||
// .fxrc.js
|
||||
import 'zx/globals'
|
||||
const _ = require('lodash')
|
||||
```
|
||||
|
||||
> If you want to use _.fxrc.js_ for both `FX_LANG=js` and `FX_LANG=node`,
|
||||
> separate parts by `// nodejs:` comment:
|
||||
> ```js
|
||||
> // .fxrc.js
|
||||
> function upper(s) {
|
||||
> return s.toUpperCase()
|
||||
> }
|
||||
> // nodejs:
|
||||
> import 'zx/globals'
|
||||
> const _ = require('lodash')
|
||||
> ```
|
@ -1,27 +0,0 @@
|
||||
# Python Reducers
|
||||
|
||||
If any additional arguments was passed, **fx** converts it to a function which
|
||||
takes the JSON as an argument named `x`.
|
||||
|
||||
```sh
|
||||
export FX_LANG=python
|
||||
```
|
||||
Or
|
||||
```sh
|
||||
export FX_LANG=python3
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
fx data.json '[x["age"] + i for i in range(10)]'
|
||||
```
|
||||
|
||||
## Dot
|
||||
|
||||
Fx supports simple syntax for accessing data, which can be used with any `FX_LANG`.
|
||||
|
||||
```sh
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
|
||||
value
|
||||
```
|
@ -1,23 +0,0 @@
|
||||
# Ruby Reducers
|
||||
|
||||
If any additional arguments was passed, **fx** converts it to a function which
|
||||
takes the JSON as an argument named `x`.
|
||||
|
||||
```sh
|
||||
export FX_LANG=ruby
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
fx data.json 'x.to_a.map {|x| x[1]}'
|
||||
```
|
||||
|
||||
## Dot
|
||||
|
||||
Fx supports simple syntax for accessing data, which can be used with any `FX_LANG`.
|
||||
|
||||
```sh
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
|
||||
value
|
||||
```
|
@ -0,0 +1,35 @@
|
||||
'use strict'
|
||||
|
||||
function* find(v, regex, path = []) {
|
||||
if (typeof v === 'undefined' || v === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(v)) {
|
||||
let i = 0
|
||||
for (let value of v) {
|
||||
yield* find(value, regex, path.concat(['[' + i++ + ']']))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof v === 'object' && v.constructor === Object) {
|
||||
const entries = Object.entries(v)
|
||||
for (let [key, value] of entries) {
|
||||
const nextPath = path.concat(['.' + key])
|
||||
|
||||
if (regex.test(key)) {
|
||||
yield nextPath
|
||||
}
|
||||
|
||||
yield* find(value, regex, nextPath)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (regex.test(v)) {
|
||||
yield path
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = find
|
@ -0,0 +1,740 @@
|
||||
'use strict'
|
||||
const fs = require('fs')
|
||||
const tty = require('tty')
|
||||
const blessed = require('@medv/blessed')
|
||||
const stringWidth = require('string-width')
|
||||
const reduce = require('./reduce')
|
||||
const print = require('./print')
|
||||
const find = require('./find')
|
||||
const config = require('./config')
|
||||
|
||||
module.exports = function start(filename, source, prev = {}) {
|
||||
// Current rendered object on a screen.
|
||||
let json = prev.json || source
|
||||
|
||||
// Contains map from row number to expand path.
|
||||
// Example: {0: '', 1: '.foo', 2: '.foo[0]'}
|
||||
let index = new Map()
|
||||
|
||||
// Contains expanded paths. Example: ['', '.foo']
|
||||
// Empty string represents root path.
|
||||
const expanded = prev.expanded || new Set()
|
||||
expanded.add('')
|
||||
|
||||
// Current filter code.
|
||||
let currentCode = null
|
||||
|
||||
// Current search regexp and generator.
|
||||
let highlight = null
|
||||
let findGen = null
|
||||
let currentPath = null
|
||||
|
||||
let ttyReadStream, ttyWriteStream
|
||||
|
||||
// Reopen tty
|
||||
if (process.platform === 'win32') {
|
||||
const cfs = process.binding('fs')
|
||||
ttyReadStream = tty.ReadStream(cfs.open('conin$', fs.constants.O_RDWR | fs.constants.O_EXCL, 0o666))
|
||||
ttyWriteStream = tty.WriteStream(cfs.open('conout$', fs.constants.O_RDWR | fs.constants.O_EXCL, 0o666))
|
||||
} else {
|
||||
const ttyFd = fs.openSync('/dev/tty', 'r+')
|
||||
ttyReadStream = tty.ReadStream(ttyFd)
|
||||
ttyWriteStream = tty.WriteStream(ttyFd)
|
||||
}
|
||||
|
||||
const program = blessed.program({
|
||||
input: ttyReadStream,
|
||||
output: ttyWriteStream,
|
||||
})
|
||||
|
||||
const screen = blessed.screen({
|
||||
program: program,
|
||||
smartCSR: true,
|
||||
fullUnicode: true,
|
||||
})
|
||||
|
||||
const box = blessed.box({
|
||||
parent: screen,
|
||||
tags: false,
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
mouse: true,
|
||||
keys: true,
|
||||
vi: true,
|
||||
ignoreArrows: true,
|
||||
alwaysScroll: true,
|
||||
scrollable: true,
|
||||
})
|
||||
|
||||
const input = blessed.textbox({
|
||||
parent: screen,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
height: 1,
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
const search = blessed.textbox({
|
||||
parent: screen,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
height: 1,
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
const statusBar = blessed.box({
|
||||
parent: screen,
|
||||
tags: false,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
height: 1,
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
const autocomplete = blessed.list({
|
||||
parent: screen,
|
||||
width: 6,
|
||||
height: 7,
|
||||
left: 1,
|
||||
bottom: 1,
|
||||
style: config.list,
|
||||
})
|
||||
|
||||
screen.title = filename
|
||||
box.focus()
|
||||
input.hide()
|
||||
search.hide()
|
||||
statusBar.hide()
|
||||
autocomplete.hide()
|
||||
|
||||
process.stdout.on('resize', () => {
|
||||
// Blessed has a bug with resizing the terminal. I tried my best to fix it but was not succeeded.
|
||||
// For now exit and print seem like a reasonable alternative, as it not usable after resize.
|
||||
// If anyone can fix this bug it will be cool.
|
||||
printJson({expanded})
|
||||
})
|
||||
|
||||
screen.key(['escape', 'q', 'C-c'], function () {
|
||||
exit()
|
||||
})
|
||||
|
||||
input.on('submit', function () {
|
||||
if (autocomplete.hidden) {
|
||||
const code = input.getValue()
|
||||
if (/^\//.test(code)) {
|
||||
// Forgive a mistake to the user. This looks like user wanted to search something.
|
||||
apply('')
|
||||
applyPattern(code)
|
||||
} else {
|
||||
apply(code)
|
||||
}
|
||||
} else {
|
||||
// Autocomplete selected
|
||||
let code = input.getValue()
|
||||
let replace = autocomplete.getSelected()
|
||||
if (/^[a-z]\w*$/i.test(replace)) {
|
||||
replace = '.' + replace
|
||||
} else {
|
||||
replace = `["${replace}"]`
|
||||
}
|
||||
code = code.replace(/\.\w*$/, replace)
|
||||
|
||||
input.setValue(code)
|
||||
autocomplete.hide()
|
||||
update(code)
|
||||
|
||||
// Keep editing code
|
||||
input.readInput()
|
||||
}
|
||||
})
|
||||
|
||||
input.on('cancel', function () {
|
||||
if (autocomplete.hidden) {
|
||||
const code = input.getValue()
|
||||
apply(code)
|
||||
} else {
|
||||
// Autocomplete not selected
|
||||
autocomplete.hide()
|
||||
screen.render()
|
||||
|
||||
// Keep editing code
|
||||
input.readInput()
|
||||
}
|
||||
})
|
||||
|
||||
input.on('update', function (code) {
|
||||
if (currentCode === code) {
|
||||
return
|
||||
}
|
||||
currentCode = code
|
||||
if (index.size < 10000) { // Don't live update in we have a big JSON file.
|
||||
update(code)
|
||||
}
|
||||
complete(code)
|
||||
})
|
||||
|
||||
input.key('up', function () {
|
||||
if (!autocomplete.hidden) {
|
||||
autocomplete.up()
|
||||
screen.render()
|
||||
}
|
||||
})
|
||||
|
||||
input.key('down', function () {
|
||||
if (!autocomplete.hidden) {
|
||||
autocomplete.down()
|
||||
screen.render()
|
||||
}
|
||||
})
|
||||
|
||||
input.key('C-u', function () {
|
||||
input.setValue('')
|
||||
update('')
|
||||
render()
|
||||
})
|
||||
|
||||
input.key('C-w', function () {
|
||||
let code = input.getValue()
|
||||
code = code.replace(/[\.\[][^\.\[]*$/, '')
|
||||
input.setValue(code)
|
||||
update(code)
|
||||
render()
|
||||
})
|
||||
|
||||
search.on('submit', function (pattern) {
|
||||
applyPattern(pattern)
|
||||
})
|
||||
|
||||
search.on('cancel', function () {
|
||||
highlight = null
|
||||
currentPath = null
|
||||
|
||||
search.hide()
|
||||
search.setValue('')
|
||||
|
||||
box.height = '100%'
|
||||
box.focus()
|
||||
|
||||
program.cursorPos(0, 0)
|
||||
render()
|
||||
})
|
||||
|
||||
box.key('.', function () {
|
||||
hideStatusBar()
|
||||
box.height = '100%-1'
|
||||
input.show()
|
||||
if (input.getValue() === '') {
|
||||
input.setValue('.')
|
||||
complete('.')
|
||||
}
|
||||
input.readInput()
|
||||
screen.render()
|
||||
})
|
||||
|
||||
box.key('/', function () {
|
||||
hideStatusBar()
|
||||
box.height = '100%-1'
|
||||
search.show()
|
||||
search.setValue('/')
|
||||
search.readInput()
|
||||
screen.render()
|
||||
})
|
||||
|
||||
box.key('e', function () {
|
||||
hideStatusBar()
|
||||
expanded.clear()
|
||||
for (let path of dfs(json)) {
|
||||
if (expanded.size < 1000) {
|
||||
expanded.add(path)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
render()
|
||||
})
|
||||
|
||||
box.key('S-e', function () {
|
||||
hideStatusBar()
|
||||
expanded.clear()
|
||||
expanded.add('')
|
||||
render()
|
||||
|
||||
// Make sure cursor stay on JSON object.
|
||||
const [n] = getLine(program.y)
|
||||
if (typeof n === 'undefined' || !index.has(n)) {
|
||||
// No line under cursor
|
||||
let rest = [...index.keys()]
|
||||
if (rest.length > 0) {
|
||||
const next = Math.max(...rest)
|
||||
let y = box.getScreenNumber(next) - box.childBase
|
||||
if (y <= 0) {
|
||||
y = 0
|
||||
}
|
||||
const line = box.getScreenLine(y + box.childBase)
|
||||
program.cursorPos(y, line.search(/\S/))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
box.key('n', function () {
|
||||
hideStatusBar()
|
||||
findNext()
|
||||
})
|
||||
|
||||
box.key(['up', 'k'], function () {
|
||||
hideStatusBar()
|
||||
program.showCursor()
|
||||
const [n] = getLine(program.y)
|
||||
|
||||
let next
|
||||
for (let [i,] of index) {
|
||||
if (i < n && (typeof next === 'undefined' || i > next)) {
|
||||
next = i
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof next !== 'undefined') {
|
||||
let y = box.getScreenNumber(next) - box.childBase
|
||||
if (y <= 0) {
|
||||
box.scroll(-1)
|
||||
screen.render()
|
||||
y = 0
|
||||
}
|
||||
|
||||
const line = box.getScreenLine(y + box.childBase)
|
||||
program.cursorPos(y, line.search(/\S/))
|
||||
}
|
||||
})
|
||||
|
||||
box.key(['down', 'j'], function () {
|
||||
hideStatusBar()
|
||||
program.showCursor()
|
||||
const [n] = getLine(program.y)
|
||||
|
||||
let next
|
||||
for (let [i,] of index) {
|
||||
if (i > n && (typeof next === 'undefined' || i < next)) {
|
||||
next = i
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof next !== 'undefined') {
|
||||
let y = box.getScreenNumber(next) - box.childBase
|
||||
if (y >= box.height) {
|
||||
box.scroll(1)
|
||||
screen.render()
|
||||
y = box.height - 1
|
||||
}
|
||||
|
||||
const line = box.getScreenLine(y + box.childBase)
|
||||
program.cursorPos(y, line.search(/\S/))
|
||||
}
|
||||
})
|
||||
|
||||
box.key(['right', 'l'], function () {
|
||||
hideStatusBar()
|
||||
const [n, line] = getLine(program.y)
|
||||
program.showCursor()
|
||||
program.cursorPos(program.y, line.search(/\S/))
|
||||
const path = index.get(n)
|
||||
if (!expanded.has(path)) {
|
||||
expanded.add(path)
|
||||
render()
|
||||
}
|
||||
})
|
||||
|
||||
// Expand everything under cursor.
|
||||
box.key(['S-right', 'S-l'], function () {
|
||||
hideStatusBar()
|
||||
const [n, line] = getLine(program.y)
|
||||
program.showCursor()
|
||||
program.cursorPos(program.y, line.search(/\S/))
|
||||
const path = index.get(n)
|
||||
const subJson = reduce(json, 'this' + path)
|
||||
for (let p of dfs(subJson, path)) {
|
||||
if (expanded.size < 1000) {
|
||||
expanded.add(p)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
render()
|
||||
})
|
||||
|
||||
box.key(['S-left', 'S-k'], function () {
|
||||
hideStatusBar()
|
||||
const [n, line] = getLine(program.y)
|
||||
program.showCursor()
|
||||
program.cursorPos(program.y, line.search(/\S/))
|
||||
const path = index.get(n)
|
||||
if (!path) {
|
||||
// collapse on top level (should render like after run)
|
||||
expanded.clear()
|
||||
expanded.add('')
|
||||
} else {
|
||||
const subJson = reduce(json, 'this' + path)
|
||||
for (let p of dfs(subJson, path)) {
|
||||
if (expanded.size < 1000) {
|
||||
expanded.delete(p)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
render()
|
||||
})
|
||||
|
||||
box.key(['left', 'h'], function () {
|
||||
hideStatusBar()
|
||||
const [n, line] = getLine(program.y)
|
||||
program.showCursor()
|
||||
program.cursorPos(program.y, line.search(/\S/))
|
||||
|
||||
// Find path at current cursor position.
|
||||
const path = index.get(n)
|
||||
|
||||
if (expanded.has(path)) {
|
||||
// Collapse current path.
|
||||
expanded.delete(path)
|
||||
render()
|
||||
} else {
|
||||
// If there is no expanded paths on current line,
|
||||
// collapse parent path of current location.
|
||||
if (typeof path === 'string') {
|
||||
// Trip last part (".foo", "[0]") to get parent path.
|
||||
const parentPath = path.replace(/(\.[^\[\].]+|\[\d+\])$/, '')
|
||||
if (expanded.has(parentPath)) {
|
||||
expanded.delete(parentPath)
|
||||
render()
|
||||
|
||||
// Find line number of parent path, and if we able to find it,
|
||||
// move cursor to this position of just collapsed parent path.
|
||||
for (let y = program.y; y >= 0; --y) {
|
||||
const [n, line] = getLine(y)
|
||||
if (index.get(n) === parentPath) {
|
||||
program.cursorPos(y, line.search(/\S/))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
box.on('click', function (mouse) {
|
||||
hideStatusBar()
|
||||
const [n, line] = getLine(mouse.y)
|
||||
if (mouse.x >= stringWidth(line)) {
|
||||
return
|
||||
}
|
||||
|
||||
program.hideCursor()
|
||||
program.cursorPos(mouse.y, line.search(/\S/))
|
||||
autocomplete.hide()
|
||||
|
||||
const path = index.get(n)
|
||||
if (expanded.has(path)) {
|
||||
expanded.delete(path)
|
||||
} else {
|
||||
expanded.add(path)
|
||||
}
|
||||
render()
|
||||
})
|
||||
|
||||
box.on('scroll', function () {
|
||||
hideStatusBar()
|
||||
})
|
||||
|
||||
box.key('p', function () {
|
||||
printJson({expanded})
|
||||
})
|
||||
|
||||
box.key('S-p', function () {
|
||||
printJson()
|
||||
})
|
||||
|
||||
function printJson(options = {}) {
|
||||
screen.destroy()
|
||||
program.disableMouse()
|
||||
program.destroy()
|
||||
setTimeout(() => {
|
||||
const [text] = print(json, options)
|
||||
console.log(text)
|
||||
process.exit(0)
|
||||
}, 10)
|
||||
}
|
||||
|
||||
function getLine(y) {
|
||||
const dy = box.childBase + y
|
||||
const n = box.getNumber(dy)
|
||||
const line = box.getScreenLine(dy)
|
||||
if (typeof line === 'undefined') {
|
||||
return [n, '']
|
||||
}
|
||||
return [n, line]
|
||||
}
|
||||
|
||||
function apply(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()
|
||||
}
|
||||
|
||||
function complete(inputCode) {
|
||||
const match = inputCode.match(/\.(\w*)$/)
|
||||
const code = /^\.\w*$/.test(inputCode) ? '.' : inputCode.replace(/\.\w*$/, '')
|
||||
|
||||
let json
|
||||
try {
|
||||
json = reduce(source, code)
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
if (match) {
|
||||
if (typeof json === 'object' && json.constructor === Object) {
|
||||
const keys = Object.keys(json)
|
||||
.filter(key => key.startsWith(match[1]))
|
||||
.slice(0, 1000) // With lots of items, list takes forever to render.
|
||||
|
||||
// Hide if there is nothing to show or
|
||||
// don't show if there is complete match.
|
||||
if (keys.length === 0 || (keys.length === 1 && keys[0] === match[1])) {
|
||||
autocomplete.hide()
|
||||
return
|
||||
}
|
||||
|
||||
autocomplete.width = Math.max(...keys.map(key => key.length)) + 1
|
||||
autocomplete.height = Math.min(7, keys.length)
|
||||
autocomplete.left = Math.min(
|
||||
screen.width - autocomplete.width,
|
||||
code.length === 1 ? 1 : code.length + 1
|
||||
)
|
||||
|
||||
let selectFirst = autocomplete.items.length !== keys.length
|
||||
autocomplete.setItems(keys)
|
||||
|
||||
if (selectFirst) {
|
||||
autocomplete.select(autocomplete.items.length - 1)
|
||||
}
|
||||
if (autocomplete.hidden) {
|
||||
autocomplete.show()
|
||||
}
|
||||
} else {
|
||||
autocomplete.clearItems()
|
||||
autocomplete.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function update(code) {
|
||||
if (code && code.length !== 0) {
|
||||
try {
|
||||
const pretender = reduce(source, code)
|
||||
if (
|
||||
typeof pretender !== 'undefined'
|
||||
&& typeof pretender !== 'function'
|
||||
&& !(pretender instanceof RegExp)
|
||||
) {
|
||||
json = pretender
|
||||
}
|
||||
} catch (e) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
if (code === '') {
|
||||
json = source
|
||||
}
|
||||
|
||||
if (highlight) {
|
||||
findGen = find(json, highlight)
|
||||
}
|
||||
render()
|
||||
}
|
||||
|
||||
function applyPattern(pattern) {
|
||||
let regex
|
||||
let m = pattern.match(/^\/(.*)\/([gimuy]*)$/)
|
||||
if (m) {
|
||||
try {
|
||||
regex = new RegExp(m[1], m[2])
|
||||
} catch (e) {
|
||||
// Wrong regexp.
|
||||
}
|
||||
} else {
|
||||
m = pattern.match(/^\/(.*)$/)
|
||||
if (m) {
|
||||
try {
|
||||
regex = new RegExp(m[1], 'gi')
|
||||
} catch (e) {
|
||||
// Wrong regexp.
|
||||
}
|
||||
}
|
||||
}
|
||||
highlight = regex
|
||||
|
||||
search.hide()
|
||||
|
||||
if (highlight) {
|
||||
findGen = find(json, highlight)
|
||||
findNext()
|
||||
} else {
|
||||
findGen = null
|
||||
currentPath = null
|
||||
}
|
||||
search.setValue('')
|
||||
|
||||
box.height = '100%'
|
||||
box.focus()
|
||||
|
||||
program.cursorPos(0, 0)
|
||||
render()
|
||||
}
|
||||
|
||||
function findNext() {
|
||||
if (!findGen) {
|
||||
return
|
||||
}
|
||||
|
||||
const {value: path, done} = findGen.next()
|
||||
|
||||
if (done) {
|
||||
showStatusBar('Pattern not found')
|
||||
} else {
|
||||
|
||||
currentPath = ''
|
||||
for (let p of path) {
|
||||
expanded.add(currentPath += p)
|
||||
}
|
||||
render()
|
||||
|
||||
for (let [k, v] of index) {
|
||||
if (v === currentPath) {
|
||||
let y = box.getScreenNumber(k)
|
||||
|
||||
// Scroll one line up for better view and make sure it's not negative.
|
||||
if (--y < 0) {
|
||||
y = 0
|
||||
}
|
||||
|
||||
box.scrollTo(y)
|
||||
screen.render()
|
||||
}
|
||||
}
|
||||
|
||||
// Set cursor to current path.
|
||||
// We need timeout here to give our terminal some time.
|
||||
// Without timeout first cursorPos call does not working,
|
||||
// it looks like an ugly hack and it is an ugly hack.
|
||||
setTimeout(() => {
|
||||
for (let [k, v] of index) {
|
||||
if (v === currentPath) {
|
||||
let y = box.getScreenNumber(k) - box.childBase
|
||||
if (y <= 0) {
|
||||
y = 0
|
||||
}
|
||||
const line = box.getScreenLine(y + box.childBase)
|
||||
program.cursorPos(y, line.search(/\S/))
|
||||
}
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
function showStatusBar(status) {
|
||||
statusBar.show()
|
||||
statusBar.setContent(config.statusBar(` ${status} `))
|
||||
screen.render()
|
||||
}
|
||||
|
||||
function hideStatusBar() {
|
||||
if (!statusBar.hidden) {
|
||||
statusBar.hide()
|
||||
statusBar.setContent('')
|
||||
screen.render()
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
let content
|
||||
[content, index] = print(json, {expanded, highlight, currentPath})
|
||||
|
||||
if (typeof content === 'undefined') {
|
||||
content = 'undefined'
|
||||
}
|
||||
|
||||
box.setContent(content)
|
||||
screen.render()
|
||||
}
|
||||
|
||||
function exit() {
|
||||
// If exit program immediately, stdin may still receive
|
||||
// mouse events which will be printed in stdout.
|
||||
program.disableMouse()
|
||||
setTimeout(() => process.exit(0), 10)
|
||||
}
|
||||
|
||||
render()
|
||||
}
|
||||
|
||||
function* bfs(json) {
|
||||
const queue = [[json, '']]
|
||||
|
||||
while (queue.length > 0) {
|
||||
const [v, path] = queue.shift()
|
||||
|
||||
if (!v) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (Array.isArray(v)) {
|
||||
yield path
|
||||
let i = 0
|
||||
for (let item of v) {
|
||||
const p = path + '[' + (i++) + ']'
|
||||
queue.push([item, p])
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof v === 'object' && v.constructor === Object) {
|
||||
yield path
|
||||
for (let [key, value] of Object.entries(v)) {
|
||||
const p = path + '.' + key
|
||||
queue.push([value, p])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* dfs(v, path = '') {
|
||||
if (!v) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(v)) {
|
||||
yield path
|
||||
let i = 0
|
||||
for (let item of v) {
|
||||
yield* dfs(item, path + '[' + (i++) + ']')
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof v === 'object' && v.constructor === Object) {
|
||||
yield path
|
||||
for (let [key, value] of Object.entries(v)) {
|
||||
yield* dfs(value, path + '.' + key)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
module github.com/antonmedv/fx
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v0.10.3
|
||||
github.com/charmbracelet/bubbletea v0.20.0
|
||||
github.com/charmbracelet/lipgloss v0.5.0
|
||||
github.com/dop251/goja v0.0.0-20220501172647-e1eca0b61fa9
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/mazznoer/colorgrad v0.8.1
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739
|
||||
github.com/stretchr/testify v1.7.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/mazznoer/csscolorparser v0.1.2 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect
|
||||
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||
)
|
@ -1,96 +0,0 @@
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho=
|
||||
github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
|
||||
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
|
||||
github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc=
|
||||
github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM=
|
||||
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
|
||||
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
|
||||
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
|
||||
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
|
||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
|
||||
github.com/dop251/goja v0.0.0-20220501172647-e1eca0b61fa9 h1:BXEAWJOT2C6ex9iOzVnrYWMFjTRccNs7p8fpLCLLcm0=
|
||||
github.com/dop251/goja v0.0.0-20220501172647-e1eca0b61fa9/go.mod h1:TQJQ+ZNyFVvUtUEtCZxBhfWiH7RJqR3EivNmvD6Waik=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mazznoer/colorgrad v0.8.1 h1:Bw/ks+KujOOg9E6YQvPqSqTLryiFnwliAH5VMZarSTI=
|
||||
github.com/mazznoer/colorgrad v0.8.1/go.mod h1:xCjvoNkXHJIAPOUMSMrXkFdxTGQqk8zMYS3e5hSLghA=
|
||||
github.com/mazznoer/csscolorparser v0.1.0/go.mod h1:Aj22+L/rYN/Y6bj3bYqO3N6g1dtdHtGfQ32xZ5PJQic=
|
||||
github.com/mazznoer/csscolorparser v0.1.2 h1:/UBHuQg792ePmGFzTQAC9u+XbFr7/HzP/Gj70Phyz2A=
|
||||
github.com/mazznoer/csscolorparser v0.1.2/go.mod h1:Aj22+L/rYN/Y6bj3bYqO3N6g1dtdHtGfQ32xZ5PJQic=
|
||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
|
||||
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
|
||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
|
||||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
|
||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8=
|
||||
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
@ -1,71 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func usage(keyMap KeyMap) string {
|
||||
title := lipgloss.NewStyle().Bold(true)
|
||||
pad := lipgloss.NewStyle().PaddingLeft(4)
|
||||
return fmt.Sprintf(`
|
||||
%v
|
||||
Terminal JSON viewer
|
||||
|
||||
%v
|
||||
fx data.json
|
||||
fx data.json .field
|
||||
curl ... | fx
|
||||
|
||||
%v
|
||||
-h, --help print help
|
||||
-v, --version print version
|
||||
--print-code print code of the reducer
|
||||
|
||||
%v
|
||||
%v
|
||||
|
||||
%v
|
||||
[https://fx.wtf]
|
||||
`,
|
||||
title.Render("fx "+version),
|
||||
title.Render("Usage"),
|
||||
title.Render("Flags"),
|
||||
title.Render("Key Bindings"),
|
||||
strings.Join(keyMapInfo(keyMap, pad), "\n"),
|
||||
title.Render("More info"),
|
||||
)
|
||||
}
|
||||
|
||||
func keyMapInfo(keyMap KeyMap, style lipgloss.Style) []string {
|
||||
v := reflect.ValueOf(keyMap)
|
||||
fields := reflect.VisibleFields(v.Type())
|
||||
|
||||
keys := make([]string, 0)
|
||||
for i := range fields {
|
||||
k := v.Field(i).Interface().(key.Binding)
|
||||
str := k.Help().Key
|
||||
if width(str) == 0 {
|
||||
str = strings.Join(k.Keys(), ", ")
|
||||
}
|
||||
keys = append(keys, fmt.Sprintf("%v ", str))
|
||||
}
|
||||
|
||||
desc := make([]string, 0)
|
||||
for i := range fields {
|
||||
k := v.Field(i).Interface().(key.Binding)
|
||||
desc = append(desc, fmt.Sprintf("%v", k.Help().Desc))
|
||||
}
|
||||
|
||||
content := lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
strings.Join(keys, "\n"),
|
||||
strings.Join(desc, "\n"),
|
||||
)
|
||||
|
||||
return strings.Split(style.Render(content), "\n")
|
||||
}
|
@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict'
|
||||
const os = require('os')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const JSON = require('lossless-json')
|
||||
JSON.config({circularRefs: false})
|
||||
|
||||
const std = require('./std')
|
||||
|
||||
try {
|
||||
require(path.join(os.homedir(), '.fxrc')) // Should be required before config.js usage.
|
||||
} catch (err) {
|
||||
if (err.code !== 'MODULE_NOT_FOUND') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const print = require('./print')
|
||||
const reduce = require('./reduce')
|
||||
const stream = require('./stream')
|
||||
|
||||
const usage = `
|
||||
Usage
|
||||
$ fx [code ...]
|
||||
|
||||
Examples
|
||||
$ 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]
|
||||
|
||||
$ echo '{"items": ["one", "two"]}' | fx 'this.items' 'this[1]'
|
||||
two
|
||||
|
||||
$ echo '{"count": 0}' | fx '{...this, count: 1}'
|
||||
{"count": 1}
|
||||
|
||||
$ echo '{"foo": 1, "bar": 2}' | fx ?
|
||||
["foo", "bar"]
|
||||
|
||||
`
|
||||
|
||||
const {stdin, stdout, stderr} = process
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
|
||||
void function main() {
|
||||
stdin.setEncoding('utf8')
|
||||
|
||||
if (stdin.isTTY) {
|
||||
handle('')
|
||||
return
|
||||
}
|
||||
|
||||
const reader = stream(stdin, apply)
|
||||
|
||||
stdin.on('readable', reader.read)
|
||||
stdin.on('end', () => {
|
||||
if (!reader.isStream()) {
|
||||
handle(reader.value())
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
|
||||
function handle(input) {
|
||||
let filename = 'fx'
|
||||
|
||||
if (input === '') {
|
||||
if (args.length === 0 || (args.length === 1 && (args[0] === '-h' || args[0] === '--help'))) {
|
||||
stderr.write(usage)
|
||||
process.exit(2)
|
||||
}
|
||||
if (args.length === 1 && (args[0] === '-v' || args[0] === '--version')) {
|
||||
stderr.write(require('./package.json').version + '\n')
|
||||
process.exit(0)
|
||||
}
|
||||
if (args.length === 1 && args[0] === '--life') {
|
||||
require('./bang')
|
||||
return
|
||||
}
|
||||
|
||||
input = fs.readFileSync(args[0]).toString('utf8')
|
||||
filename = path.basename(args[0])
|
||||
global.FX_FILENAME = filename
|
||||
args.shift()
|
||||
}
|
||||
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(input)
|
||||
} catch (e) {
|
||||
printError(e, input)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (args.length === 0 && stdout.isTTY) {
|
||||
require('./fx')(filename, json)
|
||||
return
|
||||
}
|
||||
|
||||
apply(json)
|
||||
}
|
||||
|
||||
function apply(json) {
|
||||
let output = json
|
||||
|
||||
for (let [i, code] of args.entries()) {
|
||||
try {
|
||||
output = reduce(output, code)
|
||||
} catch (e) {
|
||||
if (e === std.skip) {
|
||||
return
|
||||
}
|
||||
stderr.write(`${snippet(i, code)}\n${e.stack || e}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof output === 'undefined') {
|
||||
stderr.write('undefined\n')
|
||||
} else if (typeof output === 'string') {
|
||||
console.log(output)
|
||||
} else {
|
||||
const [text] = print(output)
|
||||
console.log(text)
|
||||
}
|
||||
}
|
||||
|
||||
function snippet(i, code) {
|
||||
let pre = args.slice(0, i).join(' ')
|
||||
let post = args.slice(i + 1).join(' ')
|
||||
if (pre.length > 20) {
|
||||
pre = '...' + pre.substring(pre.length - 20)
|
||||
}
|
||||
if (post.length > 20) {
|
||||
post = post.substring(0, 20) + '...'
|
||||
}
|
||||
const chalk = require('chalk')
|
||||
return `\n ${pre} ${chalk.red.underline(code)} ${post}\n`
|
||||
}
|
||||
|
||||
function printError(e, input) {
|
||||
if (e.char) {
|
||||
let lineNumber = 1, start = e.char - 70, end = e.char + 50
|
||||
if (start < 0) start = 0
|
||||
if (end > input.length) end = input.length
|
||||
|
||||
for (let i = 0; i < input.length && i < start; i++) {
|
||||
if (input[i] === '\n') lineNumber++
|
||||
}
|
||||
|
||||
let lines = input
|
||||
.substring(start, end)
|
||||
.split('\n')
|
||||
|
||||
if (lines.length > 1) {
|
||||
lines = lines.slice(1)
|
||||
lineNumber++
|
||||
}
|
||||
|
||||
const chalk = require('chalk')
|
||||
process.stderr.write(`\n`)
|
||||
for (let line of lines) {
|
||||
process.stderr.write(` ${chalk.yellow(lineNumber)} ${line}\n`)
|
||||
lineNumber++
|
||||
}
|
||||
process.stderr.write(`\n`)
|
||||
}
|
||||
process.stderr.write(e.toString() + '\n')
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
package main
|
||||
|
||||
import "github.com/charmbracelet/bubbles/key"
|
||||
|
||||
type KeyMap struct {
|
||||
Quit key.Binding
|
||||
Help key.Binding
|
||||
PageDown key.Binding
|
||||
PageUp key.Binding
|
||||
HalfPageUp key.Binding
|
||||
HalfPageDown key.Binding
|
||||
GotoTop key.Binding
|
||||
GotoBottom key.Binding
|
||||
Down key.Binding
|
||||
Up key.Binding
|
||||
Expand key.Binding
|
||||
Collapse key.Binding
|
||||
ExpandRecursively key.Binding
|
||||
CollapseRecursively key.Binding
|
||||
ExpandAll key.Binding
|
||||
CollapseAll key.Binding
|
||||
NextSibling key.Binding
|
||||
PrevSibling key.Binding
|
||||
ToggleWrap key.Binding
|
||||
Search key.Binding
|
||||
Next key.Binding
|
||||
Prev key.Binding
|
||||
}
|
||||
|
||||
func DefaultKeyMap() KeyMap {
|
||||
return KeyMap{
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("q", "ctrl+c", "esc"),
|
||||
key.WithHelp("", "exit program"),
|
||||
),
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("?"),
|
||||
key.WithHelp("", "show help"),
|
||||
),
|
||||
PageDown: key.NewBinding(
|
||||
key.WithKeys("pgdown", " ", "f"),
|
||||
key.WithHelp("pgdown, space, f", "page down"),
|
||||
),
|
||||
PageUp: key.NewBinding(
|
||||
key.WithKeys("pgup", "b"),
|
||||
key.WithHelp("pgup, b", "page up"),
|
||||
),
|
||||
HalfPageUp: key.NewBinding(
|
||||
key.WithKeys("u", "ctrl+u"),
|
||||
key.WithHelp("", "half page up"),
|
||||
),
|
||||
HalfPageDown: key.NewBinding(
|
||||
key.WithKeys("d", "ctrl+d"),
|
||||
key.WithHelp("", "half page down"),
|
||||
),
|
||||
GotoTop: key.NewBinding(
|
||||
key.WithKeys("g"),
|
||||
key.WithHelp("", "goto top"),
|
||||
),
|
||||
GotoBottom: key.NewBinding(
|
||||
key.WithKeys("G"),
|
||||
key.WithHelp("", "goto bottom"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("", "down"),
|
||||
),
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("", "up"),
|
||||
),
|
||||
Expand: key.NewBinding(
|
||||
key.WithKeys("right", "l"),
|
||||
key.WithHelp("", "expand"),
|
||||
),
|
||||
Collapse: key.NewBinding(
|
||||
key.WithKeys("left", "h"),
|
||||
key.WithHelp("", "collapse"),
|
||||
),
|
||||
ExpandRecursively: key.NewBinding(
|
||||
key.WithKeys("L"),
|
||||
key.WithHelp("", "expand recursively"),
|
||||
),
|
||||
CollapseRecursively: key.NewBinding(
|
||||
key.WithKeys("H"),
|
||||
key.WithHelp("", "collapse recursively"),
|
||||
),
|
||||
ExpandAll: key.NewBinding(
|
||||
key.WithKeys("e"),
|
||||
key.WithHelp("", "expand all"),
|
||||
),
|
||||
CollapseAll: key.NewBinding(
|
||||
key.WithKeys("E"),
|
||||
key.WithHelp("", "collapse all"),
|
||||
),
|
||||
NextSibling: key.NewBinding(
|
||||
key.WithKeys("J"),
|
||||
key.WithHelp("", "next sibling"),
|
||||
),
|
||||
PrevSibling: key.NewBinding(
|
||||
key.WithKeys("K"),
|
||||
key.WithHelp("", "previous sibling"),
|
||||
),
|
||||
ToggleWrap: key.NewBinding(
|
||||
key.WithKeys("z"),
|
||||
key.WithHelp("", "toggle strings wrap"),
|
||||
),
|
||||
Search: key.NewBinding(
|
||||
key.WithKeys("/"),
|
||||
key.WithHelp("", "search regexp"),
|
||||
),
|
||||
Next: key.NewBinding(
|
||||
key.WithKeys("n"),
|
||||
key.WithHelp("", "next search result"),
|
||||
),
|
||||
Prev: key.NewBinding(
|
||||
key.WithKeys("N"),
|
||||
key.WithHelp("", "prev search result"),
|
||||
),
|
||||
}
|
||||
}
|
@ -1,634 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
. "github.com/antonmedv/fx/pkg/reducer"
|
||||
. "github.com/antonmedv/fx/pkg/theme"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
var (
|
||||
flagHelp bool
|
||||
flagVersion bool
|
||||
flagPrintCode bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
var args []string
|
||||
for _, arg := range os.Args[1:] {
|
||||
switch arg {
|
||||
case "-h", "--help":
|
||||
flagHelp = true
|
||||
case "-v", "-V", "--version":
|
||||
flagVersion = true
|
||||
case "--print-code":
|
||||
flagPrintCode = true
|
||||
default:
|
||||
args = append(args, arg)
|
||||
}
|
||||
|
||||
}
|
||||
if flagHelp {
|
||||
fmt.Println(usage(DefaultKeyMap()))
|
||||
return
|
||||
}
|
||||
if flagVersion {
|
||||
fmt.Println(version)
|
||||
return
|
||||
}
|
||||
cpuProfile := os.Getenv("CPU_PROFILE")
|
||||
if cpuProfile != "" {
|
||||
f, err := os.Create(cpuProfile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = pprof.StartCPUProfile(f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
themeId, ok := os.LookupEnv("FX_THEME")
|
||||
if !ok {
|
||||
themeId = "1"
|
||||
}
|
||||
theme, ok := Themes[themeId]
|
||||
if !ok {
|
||||
theme = Themes["1"]
|
||||
}
|
||||
if termenv.ColorProfile() == termenv.Ascii {
|
||||
theme = Themes["0"]
|
||||
}
|
||||
var showSize bool
|
||||
if s, ok := os.LookupEnv("FX_SHOW_SIZE"); ok {
|
||||
if s == "true" {
|
||||
showSize = true
|
||||
}
|
||||
}
|
||||
|
||||
stdinIsTty := isatty.IsTerminal(os.Stdin.Fd())
|
||||
stdoutIsTty := isatty.IsTerminal(os.Stdout.Fd())
|
||||
filePath := ""
|
||||
fileName := ""
|
||||
var dec *json.Decoder
|
||||
if stdinIsTty {
|
||||
// Nothing was piped, maybe file argument?
|
||||
if len(args) >= 1 {
|
||||
filePath = args[0]
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *fs.PathError:
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
default:
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
fileName = path.Base(filePath)
|
||||
dec = json.NewDecoder(f)
|
||||
args = args[1:]
|
||||
}
|
||||
} else {
|
||||
dec = json.NewDecoder(os.Stdin)
|
||||
}
|
||||
if dec == nil {
|
||||
fmt.Println(usage(DefaultKeyMap()))
|
||||
os.Exit(1)
|
||||
}
|
||||
dec.UseNumber()
|
||||
object, err := Parse(dec)
|
||||
if err != nil {
|
||||
fmt.Println("JSON Parse Error:", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
lang, ok := os.LookupEnv("FX_LANG")
|
||||
if !ok {
|
||||
lang = "js"
|
||||
}
|
||||
var fxrc string
|
||||
if lang == "js" || lang == "node" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
b, err := os.ReadFile(path.Join(home, ".fxrc.js"))
|
||||
if err == nil {
|
||||
fxrc = "\n" + string(b)
|
||||
if lang == "js" {
|
||||
parts := strings.SplitN(fxrc, "// nodejs:", 2)
|
||||
fxrc = parts[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if dec.More() {
|
||||
os.Exit(stream(dec, object, lang, args, theme, fxrc))
|
||||
}
|
||||
if len(args) > 0 || !stdoutIsTty {
|
||||
if len(args) > 0 && flagPrintCode {
|
||||
fmt.Print(GenerateCode(lang, args, fxrc))
|
||||
return
|
||||
}
|
||||
if lang == "js" {
|
||||
simplePath, ok := SplitSimplePath(args)
|
||||
if ok {
|
||||
output := GetBySimplePath(object, simplePath)
|
||||
Echo(output, theme)
|
||||
os.Exit(0)
|
||||
}
|
||||
vm, fn, err := CreateJS(args, fxrc)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(ReduceJS(vm, fn, object, theme))
|
||||
} else {
|
||||
os.Exit(Reduce(object, lang, args, theme, fxrc))
|
||||
}
|
||||
}
|
||||
|
||||
// Start interactive mode.
|
||||
expand := map[string]bool{"": true}
|
||||
if array, ok := object.(Array); ok {
|
||||
for i := range array {
|
||||
expand[accessor("", i)] = true
|
||||
}
|
||||
}
|
||||
parents := map[string]string{}
|
||||
children := map[string][]string{}
|
||||
canBeExpanded := map[string]bool{}
|
||||
Dfs(object, func(it Iterator) {
|
||||
parents[it.Path] = it.Parent
|
||||
children[it.Parent] = append(children[it.Parent], it.Path)
|
||||
switch it.Object.(type) {
|
||||
case *Dict:
|
||||
canBeExpanded[it.Path] = len(it.Object.(*Dict).Keys) > 0
|
||||
case Array:
|
||||
canBeExpanded[it.Path] = len(it.Object.(Array)) > 0
|
||||
}
|
||||
})
|
||||
input := textinput.New()
|
||||
input.Prompt = ""
|
||||
m := &model{
|
||||
fileName: fileName,
|
||||
theme: theme,
|
||||
json: object,
|
||||
showSize: showSize,
|
||||
width: 80,
|
||||
height: 60,
|
||||
mouseWheelDelta: 3,
|
||||
keyMap: DefaultKeyMap(),
|
||||
expandedPaths: expand,
|
||||
canBeExpanded: canBeExpanded,
|
||||
parents: parents,
|
||||
children: children,
|
||||
nextSiblings: map[string]string{},
|
||||
prevSiblings: map[string]string{},
|
||||
wrap: true,
|
||||
searchInput: input,
|
||||
}
|
||||
m.collectSiblings(m.json, "")
|
||||
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
|
||||
if err := p.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if cpuProfile != "" {
|
||||
pprof.StopCPUProfile()
|
||||
}
|
||||
os.Exit(m.exitCode)
|
||||
}
|
||||
|
||||
type model struct {
|
||||
exitCode int
|
||||
width, height int
|
||||
windowHeight int
|
||||
footerHeight int
|
||||
wrap bool
|
||||
theme Theme
|
||||
showSize bool // Show number of elements in preview
|
||||
|
||||
fileName string
|
||||
json interface{}
|
||||
lines []string
|
||||
|
||||
mouseWheelDelta int // Number of lines the mouse wheel will scroll
|
||||
offset int // offset is the vertical scroll position
|
||||
|
||||
keyMap KeyMap
|
||||
showHelp bool
|
||||
|
||||
expandedPaths map[string]bool // set of expanded paths
|
||||
canBeExpanded map[string]bool // set of path => can be expanded (i.e. dict or array)
|
||||
paths []string // array of paths on screen
|
||||
pathToLineNumber map[string]int // map of path => line Number
|
||||
pathToIndex map[string]int // map of path => index in m.paths
|
||||
lineNumberToPath map[int]string // map of line Number => path
|
||||
parents map[string]string // map of subpath => parent path
|
||||
children map[string][]string // map of path => child paths
|
||||
nextSiblings, prevSiblings map[string]string // map of path => sibling path
|
||||
cursor int // cursor in [0, len(m.paths)]
|
||||
showCursor bool
|
||||
|
||||
searchInput textinput.Model
|
||||
searchRegexCompileError string
|
||||
showSearchResults bool
|
||||
searchResults []*searchResult
|
||||
searchResultsCursor int
|
||||
highlightIndex map[string]*rangeGroup
|
||||
}
|
||||
|
||||
func (m *model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.windowHeight = msg.Height
|
||||
m.searchInput.Width = msg.Width - 2 // minus prompt
|
||||
m.render()
|
||||
|
||||
case tea.MouseMsg:
|
||||
switch msg.Type {
|
||||
case tea.MouseWheelUp:
|
||||
m.LineUp(m.mouseWheelDelta)
|
||||
case tea.MouseWheelDown:
|
||||
m.LineDown(m.mouseWheelDelta)
|
||||
}
|
||||
}
|
||||
|
||||
if m.searchInput.Focused() {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
case tea.KeyEsc:
|
||||
m.searchInput.Blur()
|
||||
m.clearSearchResults()
|
||||
m.render()
|
||||
|
||||
case tea.KeyEnter:
|
||||
m.doSearch(m.searchInput.Value())
|
||||
}
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.searchInput, cmd = m.searchInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.keyMap.PageDown):
|
||||
m.ViewDown()
|
||||
case key.Matches(msg, m.keyMap.PageUp):
|
||||
m.ViewUp()
|
||||
case key.Matches(msg, m.keyMap.HalfPageDown):
|
||||
m.HalfViewDown()
|
||||
case key.Matches(msg, m.keyMap.HalfPageUp):
|
||||
m.HalfViewUp()
|
||||
case key.Matches(msg, m.keyMap.GotoTop):
|
||||
m.GotoTop()
|
||||
case key.Matches(msg, m.keyMap.GotoBottom):
|
||||
m.GotoBottom()
|
||||
}
|
||||
}
|
||||
|
||||
if m.showHelp {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.keyMap.Quit):
|
||||
m.showHelp = false
|
||||
m.render()
|
||||
case key.Matches(msg, m.keyMap.Down):
|
||||
m.LineDown(1)
|
||||
case key.Matches(msg, m.keyMap.Up):
|
||||
m.LineUp(1)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.keyMap.Quit):
|
||||
m.exitCode = 0
|
||||
return m, tea.Quit
|
||||
|
||||
case key.Matches(msg, m.keyMap.Help):
|
||||
m.GotoTop()
|
||||
m.showHelp = !m.showHelp
|
||||
m.render()
|
||||
|
||||
case key.Matches(msg, m.keyMap.Down):
|
||||
m.down()
|
||||
m.render()
|
||||
m.scrollDownToCursor()
|
||||
|
||||
case key.Matches(msg, m.keyMap.Up):
|
||||
m.up()
|
||||
m.render()
|
||||
m.scrollUpToCursor()
|
||||
|
||||
case key.Matches(msg, m.keyMap.NextSibling):
|
||||
nextSiblingPath, ok := m.nextSiblings[m.cursorPath()]
|
||||
if ok {
|
||||
m.showCursor = true
|
||||
m.cursor = m.pathToIndex[nextSiblingPath]
|
||||
} else {
|
||||
m.down()
|
||||
}
|
||||
m.render()
|
||||
m.scrollDownToCursor()
|
||||
|
||||
case key.Matches(msg, m.keyMap.PrevSibling):
|
||||
prevSiblingPath, ok := m.prevSiblings[m.cursorPath()]
|
||||
if ok {
|
||||
m.showCursor = true
|
||||
m.cursor = m.pathToIndex[prevSiblingPath]
|
||||
} else {
|
||||
m.up()
|
||||
}
|
||||
m.render()
|
||||
m.scrollUpToCursor()
|
||||
|
||||
case key.Matches(msg, m.keyMap.Expand):
|
||||
m.showCursor = true
|
||||
if m.canBeExpanded[m.cursorPath()] {
|
||||
m.expandedPaths[m.cursorPath()] = true
|
||||
}
|
||||
m.render()
|
||||
|
||||
case key.Matches(msg, m.keyMap.ExpandRecursively):
|
||||
m.showCursor = true
|
||||
if m.canBeExpanded[m.cursorPath()] {
|
||||
m.expandRecursively(m.cursorPath())
|
||||
}
|
||||
m.render()
|
||||
|
||||
case key.Matches(msg, m.keyMap.Collapse):
|
||||
m.showCursor = true
|
||||
if m.canBeExpanded[m.cursorPath()] && m.expandedPaths[m.cursorPath()] {
|
||||
m.expandedPaths[m.cursorPath()] = false
|
||||
} else {
|
||||
parentPath, ok := m.parents[m.cursorPath()]
|
||||
if ok {
|
||||
m.expandedPaths[parentPath] = false
|
||||
m.cursor = m.pathToIndex[parentPath]
|
||||
}
|
||||
}
|
||||
m.render()
|
||||
m.scrollUpToCursor()
|
||||
|
||||
case key.Matches(msg, m.keyMap.CollapseRecursively):
|
||||
m.showCursor = true
|
||||
if m.canBeExpanded[m.cursorPath()] && m.expandedPaths[m.cursorPath()] {
|
||||
m.collapseRecursively(m.cursorPath())
|
||||
} else {
|
||||
parentPath, ok := m.parents[m.cursorPath()]
|
||||
if ok {
|
||||
m.collapseRecursively(parentPath)
|
||||
m.cursor = m.pathToIndex[parentPath]
|
||||
}
|
||||
}
|
||||
m.render()
|
||||
m.scrollUpToCursor()
|
||||
|
||||
case key.Matches(msg, m.keyMap.ToggleWrap):
|
||||
m.wrap = !m.wrap
|
||||
m.render()
|
||||
|
||||
case key.Matches(msg, m.keyMap.ExpandAll):
|
||||
Dfs(m.json, func(it Iterator) {
|
||||
switch it.Object.(type) {
|
||||
case *Dict, Array:
|
||||
m.expandedPaths[it.Path] = true
|
||||
}
|
||||
})
|
||||
m.render()
|
||||
|
||||
case key.Matches(msg, m.keyMap.CollapseAll):
|
||||
m.expandedPaths = map[string]bool{
|
||||
"": true,
|
||||
}
|
||||
m.render()
|
||||
|
||||
case key.Matches(msg, m.keyMap.Search):
|
||||
m.showSearchResults = false
|
||||
m.searchRegexCompileError = ""
|
||||
m.searchInput.Focus()
|
||||
m.render()
|
||||
return m, textinput.Blink
|
||||
|
||||
case key.Matches(msg, m.keyMap.Next):
|
||||
if m.showSearchResults {
|
||||
m.nextSearchResult()
|
||||
}
|
||||
|
||||
case key.Matches(msg, m.keyMap.Prev):
|
||||
if m.showSearchResults {
|
||||
m.prevSearchResult()
|
||||
}
|
||||
}
|
||||
|
||||
case tea.MouseMsg:
|
||||
switch msg.Type {
|
||||
case tea.MouseLeft:
|
||||
m.showCursor = true
|
||||
if msg.Y >= m.height {
|
||||
// Clicked on status bar or search input.
|
||||
break
|
||||
}
|
||||
clickedPath, ok := m.lineNumberToPath[m.offset+msg.Y]
|
||||
if ok {
|
||||
if m.canBeExpanded[clickedPath] {
|
||||
m.expandedPaths[clickedPath] = !m.expandedPaths[clickedPath]
|
||||
}
|
||||
m.cursor = m.pathToIndex[clickedPath]
|
||||
m.render()
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *model) View() string {
|
||||
lines := m.visibleLines()
|
||||
extraLines := ""
|
||||
if len(lines) < m.height {
|
||||
extraLines = strings.Repeat("\n", max(0, m.height-len(lines)))
|
||||
}
|
||||
if m.showHelp {
|
||||
statusBar := "Press Esc or q to close help."
|
||||
statusBar += strings.Repeat(" ", max(0, m.width-width(statusBar)))
|
||||
statusBar = m.theme.StatusBar(statusBar)
|
||||
return strings.Join(lines, "\n") + extraLines + "\n" + statusBar
|
||||
}
|
||||
statusBar := m.cursorPath() + " "
|
||||
statusBar += strings.Repeat(" ", max(0, m.width-width(statusBar)-width(m.fileName)))
|
||||
statusBar += m.fileName
|
||||
statusBar = m.theme.StatusBar(statusBar)
|
||||
output := strings.Join(lines, "\n") + extraLines + "\n" + statusBar
|
||||
if m.searchInput.Focused() {
|
||||
output += "\n/" + m.searchInput.View()
|
||||
}
|
||||
if len(m.searchRegexCompileError) > 0 {
|
||||
output += fmt.Sprintf("\n/%v/i %v", m.searchInput.Value(), m.searchRegexCompileError)
|
||||
}
|
||||
if m.showSearchResults {
|
||||
if len(m.searchResults) == 0 {
|
||||
output += fmt.Sprintf("\n/%v/i not found", m.searchInput.Value())
|
||||
} else {
|
||||
output += fmt.Sprintf("\n/%v/i found: [%v/%v]", m.searchInput.Value(), m.searchResultsCursor+1, len(m.searchResults))
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func (m *model) recalculateViewportHeight() {
|
||||
m.height = m.windowHeight
|
||||
m.height-- // status bar
|
||||
if !m.showHelp {
|
||||
if m.searchInput.Focused() {
|
||||
m.height--
|
||||
}
|
||||
if m.showSearchResults {
|
||||
m.height--
|
||||
}
|
||||
if len(m.searchRegexCompileError) > 0 {
|
||||
m.height--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) render() {
|
||||
m.recalculateViewportHeight()
|
||||
|
||||
if m.showHelp {
|
||||
m.lines = keyMapInfo(m.keyMap, lipgloss.NewStyle().PaddingLeft(4).PaddingTop(2).PaddingBottom(2))
|
||||
return
|
||||
}
|
||||
|
||||
m.paths = make([]string, 0)
|
||||
m.pathToIndex = make(map[string]int, 0)
|
||||
if m.pathToLineNumber == nil {
|
||||
m.pathToLineNumber = make(map[string]int, 0)
|
||||
} else {
|
||||
m.pathToLineNumber = make(map[string]int, len(m.pathToLineNumber))
|
||||
}
|
||||
if m.lineNumberToPath == nil {
|
||||
m.lineNumberToPath = make(map[int]string, 0)
|
||||
} else {
|
||||
m.lineNumberToPath = make(map[int]string, len(m.lineNumberToPath))
|
||||
}
|
||||
m.lines = m.print(m.json, 1, 0, 0, "", true)
|
||||
|
||||
if m.offset > len(m.lines)-1 {
|
||||
m.GotoBottom()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) cursorPath() string {
|
||||
if m.cursor == 0 {
|
||||
return ""
|
||||
}
|
||||
if 0 <= m.cursor && m.cursor < len(m.paths) {
|
||||
return m.paths[m.cursor]
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
func (m *model) cursorLineNumber() int {
|
||||
if 0 <= m.cursor && m.cursor < len(m.paths) {
|
||||
return m.pathToLineNumber[m.paths[m.cursor]]
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (m *model) expandRecursively(path string) {
|
||||
if m.canBeExpanded[path] {
|
||||
m.expandedPaths[path] = true
|
||||
for _, childPath := range m.children[path] {
|
||||
if childPath != "" {
|
||||
m.expandRecursively(childPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) collapseRecursively(path string) {
|
||||
m.expandedPaths[path] = false
|
||||
for _, childPath := range m.children[path] {
|
||||
if childPath != "" {
|
||||
m.collapseRecursively(childPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) collectSiblings(v interface{}, path string) {
|
||||
switch v.(type) {
|
||||
case *Dict:
|
||||
prev := ""
|
||||
for _, k := range v.(*Dict).Keys {
|
||||
subpath := path + "." + k
|
||||
if prev != "" {
|
||||
m.nextSiblings[prev] = subpath
|
||||
m.prevSiblings[subpath] = prev
|
||||
}
|
||||
prev = subpath
|
||||
value, _ := v.(*Dict).Get(k)
|
||||
m.collectSiblings(value, subpath)
|
||||
}
|
||||
|
||||
case Array:
|
||||
prev := ""
|
||||
for i, value := range v.(Array) {
|
||||
subpath := fmt.Sprintf("%v[%v]", path, i)
|
||||
if prev != "" {
|
||||
m.nextSiblings[prev] = subpath
|
||||
m.prevSiblings[subpath] = prev
|
||||
}
|
||||
prev = subpath
|
||||
m.collectSiblings(value, subpath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) down() {
|
||||
m.showCursor = true
|
||||
if m.cursor < len(m.paths)-1 { // scroll till last element in m.paths
|
||||
m.cursor++
|
||||
} else {
|
||||
// at the bottom of viewport maybe some hidden brackets, lets scroll to see them
|
||||
if !m.AtBottom() {
|
||||
m.LineDown(1)
|
||||
}
|
||||
}
|
||||
if m.cursor >= len(m.paths) {
|
||||
m.cursor = len(m.paths) - 1
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) up() {
|
||||
m.showCursor = true
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
if m.cursor >= len(m.paths) {
|
||||
m.cursor = len(m.paths) - 1
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "fx",
|
||||
"version": "20.0.2",
|
||||
"description": "Command-line JSON viewer",
|
||||
"repository": "antonmedv/fx",
|
||||
"homepage": "https://fx.wtf",
|
||||
"author": "Anton Medvedev <anton@medv.io>",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"fx": "index.js"
|
||||
},
|
||||
"files": [
|
||||
"*.js"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "ava",
|
||||
"build": "pkg . --out-path dist",
|
||||
"zip": "cd dist && find . -name 'fx-*' -exec zip '{}.zip' '{}' \\;",
|
||||
"release": "release-it --github.release --github.assets=dist/*.zip",
|
||||
"brew:release": "brew bump-formula-pr --url=https://registry.npmjs.org/fx/-/fx-`fx package.json .version`.tgz --no-audit fx"
|
||||
},
|
||||
"release-it": {
|
||||
"hooks": {
|
||||
"after:bump": "rm -rf ./dist && npm run build && npm run zip"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"json",
|
||||
"viewer",
|
||||
"cli",
|
||||
"terminal",
|
||||
"term",
|
||||
"console",
|
||||
"ascii",
|
||||
"unicode",
|
||||
"blessed"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medv/blessed": "^2.0.1",
|
||||
"chalk": "^4.1.0",
|
||||
"indent-string": "^4.0.0",
|
||||
"lossless-json": "^1.0.4",
|
||||
"string-width": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^3.12.1",
|
||||
"pkg": "^4.4.9",
|
||||
"release-it": "^14.0.1"
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package dict
|
||||
|
||||
type Dict struct {
|
||||
Keys []string
|
||||
Values map[string]interface{}
|
||||
}
|
||||
|
||||
func NewDict() *Dict {
|
||||
return &Dict{
|
||||
Keys: make([]string, 0),
|
||||
Values: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dict) Get(key string) (interface{}, bool) {
|
||||
val, exists := d.Values[key]
|
||||
return val, exists
|
||||
}
|
||||
|
||||
func (d *Dict) Set(key string, value interface{}) {
|
||||
_, exists := d.Values[key]
|
||||
if !exists {
|
||||
d.Keys = append(d.Keys, key)
|
||||
}
|
||||
d.Values[key] = value
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
package dict
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_dict(t *testing.T) {
|
||||
d := NewDict()
|
||||
d.Set("number", 3)
|
||||
v, _ := d.Get("number")
|
||||
if v.(int) != 3 {
|
||||
t.Error("Set number")
|
||||
}
|
||||
// string
|
||||
d.Set("string", "x")
|
||||
v, _ = d.Get("string")
|
||||
if v.(string) != "x" {
|
||||
t.Error("Set string")
|
||||
}
|
||||
// string slice
|
||||
d.Set("strings", []string{
|
||||
"t",
|
||||
"u",
|
||||
})
|
||||
v, _ = d.Get("strings")
|
||||
if v.([]string)[0] != "t" {
|
||||
t.Error("Set strings first index")
|
||||
}
|
||||
if v.([]string)[1] != "u" {
|
||||
t.Error("Set strings second index")
|
||||
}
|
||||
// mixed slice
|
||||
d.Set("mixed", []interface{}{
|
||||
1,
|
||||
"1",
|
||||
})
|
||||
v, _ = d.Get("mixed")
|
||||
if v.([]interface{})[0].(int) != 1 {
|
||||
t.Error("Set mixed int")
|
||||
}
|
||||
if v.([]interface{})[1].(string) != "1" {
|
||||
t.Error("Set mixed string")
|
||||
}
|
||||
// overriding existing key
|
||||
d.Set("number", 4)
|
||||
v, _ = d.Get("number")
|
||||
if v.(int) != 4 {
|
||||
t.Error("Override existing key")
|
||||
}
|
||||
// Keys
|
||||
expectedKeys := []string{
|
||||
"number",
|
||||
"string",
|
||||
"strings",
|
||||
"mixed",
|
||||
}
|
||||
for i, key := range d.Keys {
|
||||
if key != expectedKeys[i] {
|
||||
t.Error("Keys method", key, "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
for i, key := range expectedKeys {
|
||||
if key != expectedKeys[i] {
|
||||
t.Error("Keys method", key, "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
)
|
||||
|
||||
func Parse(dec *json.Decoder) (interface{}, error) {
|
||||
token, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if delim, ok := token.(json.Delim); ok {
|
||||
switch delim {
|
||||
case '{':
|
||||
return decodeDict(dec)
|
||||
case '[':
|
||||
return decodeArray(dec)
|
||||
}
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func decodeDict(dec *json.Decoder) (*Dict, error) {
|
||||
d := NewDict()
|
||||
for {
|
||||
token, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if delim, ok := token.(json.Delim); ok && delim == '}' {
|
||||
return d, nil
|
||||
}
|
||||
key := token.(string)
|
||||
token, err = dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var value interface{} = token
|
||||
if delim, ok := token.(json.Delim); ok {
|
||||
switch delim {
|
||||
case '{':
|
||||
value, err = decodeDict(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case '[':
|
||||
value, err = decodeArray(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
d.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeArray(dec *json.Decoder) ([]interface{}, error) {
|
||||
slice := make(Array, 0)
|
||||
for index := 0; ; index++ {
|
||||
token, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if delim, ok := token.(json.Delim); ok {
|
||||
switch delim {
|
||||
case '{':
|
||||
value, err := decodeDict(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slice = append(slice, value)
|
||||
case '[':
|
||||
value, err := decodeArray(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slice = append(slice, value)
|
||||
case ']':
|
||||
return slice, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
slice = append(slice, token)
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_parse(t *testing.T) {
|
||||
input := `{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"a": 3,
|
||||
"slice": [{"z": "z", "1": "1"}]
|
||||
}`
|
||||
|
||||
p, err := Parse(json.NewDecoder(strings.NewReader(input)))
|
||||
if err != nil {
|
||||
t.Error("JSON parse error", err)
|
||||
}
|
||||
o := p.(*Dict)
|
||||
|
||||
expectedKeys := []string{
|
||||
"a",
|
||||
"b",
|
||||
"slice",
|
||||
}
|
||||
for i := range o.Keys {
|
||||
if o.Keys[i] != expectedKeys[i] {
|
||||
t.Error("Wrong key order ", i, o.Keys[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
|
||||
s, ok := o.Get("slice")
|
||||
if !ok {
|
||||
t.Error("slice missing")
|
||||
}
|
||||
a := s.(Array)
|
||||
z := a[0].(*Dict)
|
||||
|
||||
expectedKeys = []string{
|
||||
"z",
|
||||
"1",
|
||||
}
|
||||
for i := range z.Keys {
|
||||
if z.Keys[i] != expectedKeys[i] {
|
||||
t.Error("Wrong key order for nested map ", i, z.Keys[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/antonmedv/fx/pkg/dict"
|
||||
"github.com/antonmedv/fx/pkg/theme"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func PrettyPrint(v interface{}, level int, theme theme.Theme) string {
|
||||
ident := strings.Repeat(" ", level)
|
||||
subident := strings.Repeat(" ", level-1)
|
||||
switch v := v.(type) {
|
||||
case nil:
|
||||
return theme.Null("null")
|
||||
|
||||
case bool:
|
||||
if v {
|
||||
return theme.Boolean("true")
|
||||
} else {
|
||||
return theme.Boolean("false")
|
||||
}
|
||||
|
||||
case json.Number:
|
||||
return theme.Number(v.String())
|
||||
|
||||
case string:
|
||||
return theme.String(fmt.Sprintf("%q", v))
|
||||
|
||||
case *dict.Dict:
|
||||
keys := v.Keys
|
||||
if len(keys) == 0 {
|
||||
return theme.Syntax("{}")
|
||||
}
|
||||
output := theme.Syntax("{")
|
||||
output += "\n"
|
||||
for i, k := range keys {
|
||||
key := theme.Key(i, len(keys))(fmt.Sprintf("%q", k))
|
||||
value, _ := v.Get(k)
|
||||
delim := theme.Syntax(": ")
|
||||
line := ident + key + delim + PrettyPrint(value, level+1, theme)
|
||||
if i < len(keys)-1 {
|
||||
line += theme.Syntax(",")
|
||||
}
|
||||
line += "\n"
|
||||
output += line
|
||||
}
|
||||
return output + subident + theme.Syntax("}")
|
||||
|
||||
case []interface{}:
|
||||
slice := v
|
||||
if len(slice) == 0 {
|
||||
return theme.Syntax("[]")
|
||||
}
|
||||
output := theme.Syntax("[\n")
|
||||
for i, value := range v {
|
||||
line := ident + PrettyPrint(value, level+1, theme)
|
||||
if i < len(slice)-1 {
|
||||
line += ",\n"
|
||||
} else {
|
||||
line += "\n"
|
||||
}
|
||||
output += line
|
||||
}
|
||||
return output + subident + theme.Syntax("]")
|
||||
|
||||
default:
|
||||
return "unknown type"
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
)
|
||||
|
||||
func Stringify(v interface{}) string {
|
||||
switch v := v.(type) {
|
||||
case nil:
|
||||
return "null"
|
||||
|
||||
case bool:
|
||||
if v {
|
||||
return "true"
|
||||
} else {
|
||||
return "false"
|
||||
}
|
||||
|
||||
case Number:
|
||||
return v.String()
|
||||
|
||||
case string:
|
||||
return fmt.Sprintf("%q", v)
|
||||
|
||||
case *Dict:
|
||||
result := "{"
|
||||
for i, key := range v.Keys {
|
||||
line := fmt.Sprintf("%q", key) + ": " + Stringify(v.Values[key])
|
||||
if i < len(v.Keys)-1 {
|
||||
line += ","
|
||||
}
|
||||
result += line
|
||||
}
|
||||
return result + "}"
|
||||
|
||||
case Array:
|
||||
result := "["
|
||||
for i, value := range v {
|
||||
line := Stringify(value)
|
||||
if i < len(v)-1 {
|
||||
line += ","
|
||||
}
|
||||
result += line
|
||||
}
|
||||
return result + "]"
|
||||
|
||||
default:
|
||||
return "unknown type"
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_stringify(t *testing.T) {
|
||||
t.Run("dict", func(t *testing.T) {
|
||||
arg := NewDict()
|
||||
arg.Set("a", Number("1"))
|
||||
arg.Set("b", Number("2"))
|
||||
want := `{"a": 1,"b": 2}`
|
||||
if got := Stringify(arg); got != want {
|
||||
t.Errorf("stringify() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("array", func(t *testing.T) {
|
||||
arg := Array{Number("1"), Number("2")}
|
||||
want := `[1,2]`
|
||||
if got := Stringify(arg); got != want {
|
||||
t.Errorf("stringify() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("array_with_dict", func(t *testing.T) {
|
||||
arg := Array{NewDict(), Array{}}
|
||||
want := `[{},[]]`
|
||||
if got := Stringify(arg); got != want {
|
||||
t.Errorf("stringify() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
)
|
||||
|
||||
type Iterator struct {
|
||||
Object interface{}
|
||||
Path, Parent string
|
||||
}
|
||||
|
||||
func Dfs(object interface{}, f func(it Iterator)) {
|
||||
sub(Iterator{Object: object}, f)
|
||||
}
|
||||
|
||||
func sub(it Iterator, f func(it Iterator)) {
|
||||
f(it)
|
||||
switch it.Object.(type) {
|
||||
case *Dict:
|
||||
keys := it.Object.(*Dict).Keys
|
||||
for _, k := range keys {
|
||||
subpath := it.Path + "." + k
|
||||
value, _ := it.Object.(*Dict).Get(k)
|
||||
sub(Iterator{
|
||||
Object: value,
|
||||
Path: subpath,
|
||||
Parent: it.Path,
|
||||
}, f)
|
||||
}
|
||||
|
||||
case Array:
|
||||
slice := it.Object.(Array)
|
||||
for i, value := range slice {
|
||||
subpath := accessor(it.Path, i)
|
||||
sub(Iterator{
|
||||
Object: value,
|
||||
Path: subpath,
|
||||
Parent: it.Path,
|
||||
}, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func accessor(path string, to interface{}) string {
|
||||
return fmt.Sprintf("%v[%v]", path, to)
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package json
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Number = json.Number
|
||||
type Array = []interface{}
|
@ -1,98 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
. "github.com/antonmedv/fx/pkg/theme"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
//go:embed js.js
|
||||
var templateJs string
|
||||
|
||||
func js(args []string, fxrc string) string {
|
||||
rs := "\n"
|
||||
for i, a := range args {
|
||||
rs += " try {"
|
||||
switch {
|
||||
case flatMapRegex.MatchString(a):
|
||||
code := fold(strings.Split(a, "[]"))
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
x = (
|
||||
%v
|
||||
)(x)
|
||||
`, code)
|
||||
|
||||
case strings.HasPrefix(a, ".["):
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
x = function ()
|
||||
{ return this%v }
|
||||
.call(x)
|
||||
`, a[1:])
|
||||
|
||||
case strings.HasPrefix(a, "."):
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
x = function ()
|
||||
{ return this%v }
|
||||
.call(x)
|
||||
`, a)
|
||||
|
||||
default:
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
let f = function ()
|
||||
{ return %v }
|
||||
.call(x)
|
||||
x = typeof f === 'function' ? f(x) : f
|
||||
`, a)
|
||||
}
|
||||
// Generate a beautiful error message.
|
||||
rs += " } catch (e) {\n"
|
||||
pre, post, pointer := trace(args, i)
|
||||
rs += fmt.Sprintf(
|
||||
" throw `\\n ${%q} ${%q} ${%q}\\n %v\\n\\n${e.stack || e}`\n",
|
||||
pre, a, post, pointer,
|
||||
)
|
||||
rs += " }\n"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(templateJs, fxrc, rs)
|
||||
}
|
||||
|
||||
func CreateJS(args []string, fxrc string) (*goja.Runtime, goja.Callable, error) {
|
||||
vm := goja.New()
|
||||
_, err := vm.RunString(js(args, fxrc))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
fn, ok := goja.AssertFunction(vm.Get("reduce"))
|
||||
if !ok {
|
||||
panic("Not a function")
|
||||
}
|
||||
return vm, fn, nil
|
||||
}
|
||||
|
||||
func ReduceJS(vm *goja.Runtime, reduce goja.Callable, input interface{}, theme Theme) int {
|
||||
value, err := reduce(goja.Undefined(), vm.ToValue(Stringify(input)))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
output := value.String()
|
||||
dec := json.NewDecoder(strings.NewReader(output))
|
||||
dec.UseNumber()
|
||||
object, err := Parse(dec)
|
||||
if err != nil {
|
||||
fmt.Print(output)
|
||||
return 0
|
||||
}
|
||||
Echo(object, theme)
|
||||
return 0
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
// .fxrc.js %v
|
||||
|
||||
function reduce(input) {
|
||||
let x = JSON.parse(input)
|
||||
|
||||
// Reducers %v
|
||||
if (typeof x === 'undefined') {
|
||||
return 'null'
|
||||
} else {
|
||||
return JSON.stringify(x)
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CreateNodejs(args []string, fxrc string) *exec.Cmd {
|
||||
cmd := exec.Command("node", "--input-type=module", "-e", nodejs(args, fxrc))
|
||||
nodePath, exist := os.LookupEnv("NODE_PATH")
|
||||
if exist {
|
||||
cmd.Dir = path.Dir(nodePath)
|
||||
}
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "NODE_OPTIONS=--max-old-space-size=8192")
|
||||
workingDir, err := os.Getwd()
|
||||
if err == nil {
|
||||
cmd.Env = append(cmd.Env, "FX_CWD="+workingDir)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
//go:embed node.js
|
||||
var templateNode string
|
||||
|
||||
func nodejs(args []string, fxrc string) string {
|
||||
rs := "\n"
|
||||
for i, a := range args {
|
||||
rs += " try {"
|
||||
switch {
|
||||
case flatMapRegex.MatchString(a):
|
||||
code := fold(strings.Split(a, "[]"))
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
x = (
|
||||
%v
|
||||
)(x)
|
||||
`, code)
|
||||
|
||||
case strings.HasPrefix(a, ".["):
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
x = function ()
|
||||
{ return this%v }
|
||||
.call(x)
|
||||
`, a[1:])
|
||||
|
||||
case strings.HasPrefix(a, "."):
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
x = function ()
|
||||
{ return this%v }
|
||||
.call(x)
|
||||
`, a)
|
||||
|
||||
default:
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
let f = function ()
|
||||
{ return %v }
|
||||
.call(x)
|
||||
x = typeof f === 'function' ? f(x) : f
|
||||
`, a)
|
||||
}
|
||||
rs += `
|
||||
x = await x
|
||||
`
|
||||
// Generate a beautiful error message.
|
||||
rs += " } catch (e) {\n"
|
||||
pre, post, pointer := trace(args, i)
|
||||
rs += fmt.Sprintf(
|
||||
" throw `\\n ${%q} ${%q} ${%q}\\n %v\\n\\n${e.stack || e}`\n",
|
||||
pre, a, post, pointer,
|
||||
)
|
||||
rs += " }\n"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(templateNode, fxrc, rs)
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import os from 'node:os'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import {createRequire} from 'node:module'
|
||||
|
||||
const cwd = process.env.FX_CWD ? process.env.FX_CWD : process.cwd()
|
||||
const require = createRequire(cwd)
|
||||
|
||||
// .fxrc.js %v
|
||||
|
||||
void async function () {
|
||||
process.chdir(cwd)
|
||||
|
||||
let buffer = ''
|
||||
process.stdin.setEncoding('utf8')
|
||||
for await (let chunk of process.stdin) {
|
||||
buffer += chunk
|
||||
}
|
||||
let x = JSON.parse(buffer)
|
||||
|
||||
// Reducers %v
|
||||
|
||||
if (typeof x === 'undefined') {
|
||||
process.stderr.write('undefined')
|
||||
} else {
|
||||
process.stdout.write(JSON.stringify(x))
|
||||
}
|
||||
}().catch(err => {
|
||||
console.error(err)
|
||||
process.exitCode = 1
|
||||
})
|
@ -1,38 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func CreatePython(bin string, args []string) *exec.Cmd {
|
||||
cmd := exec.Command(bin, "-c", python(args))
|
||||
return cmd
|
||||
}
|
||||
|
||||
//go:embed python.py
|
||||
var templatePython string
|
||||
|
||||
func python(args []string) string {
|
||||
rs := "\n"
|
||||
for i, a := range args {
|
||||
rs += fmt.Sprintf(
|
||||
`try:
|
||||
f = (lambda x: (%v))(x)
|
||||
x = f(x) if callable(f) else f
|
||||
`, a)
|
||||
|
||||
// Generate a beautiful error message.
|
||||
rs += "except Exception as e:\n"
|
||||
pre, post, pointer := trace(args, i)
|
||||
rs += fmt.Sprintf(
|
||||
` sys.stderr.write('\n {} {} {}\n %v\n\n{}\n'.format(%q, %q, %q, e))
|
||||
sys.exit(1)`,
|
||||
pointer,
|
||||
pre, a, post,
|
||||
)
|
||||
rs += "\n"
|
||||
}
|
||||
return fmt.Sprintf(templatePython, rs)
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import json, sys, os
|
||||
x = json.load(sys.stdin)
|
||||
|
||||
# Reducers %v
|
||||
|
||||
try:
|
||||
print(json.dumps(x))
|
||||
except:
|
||||
print(json.dumps(list(x)))
|
@ -1,76 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
. "github.com/antonmedv/fx/pkg/theme"
|
||||
)
|
||||
|
||||
func GenerateCode(lang string, args []string, fxrc string) string {
|
||||
switch lang {
|
||||
case "js":
|
||||
return js(args, fxrc)
|
||||
case "node":
|
||||
return nodejs(args, fxrc)
|
||||
case "python", "python3":
|
||||
return python(args)
|
||||
case "ruby":
|
||||
return ruby(args)
|
||||
default:
|
||||
panic("unknown lang")
|
||||
}
|
||||
}
|
||||
|
||||
func Reduce(input interface{}, lang string, args []string, theme Theme, fxrc string) int {
|
||||
path, ok := SplitSimplePath(args)
|
||||
if ok {
|
||||
output := GetBySimplePath(input, path)
|
||||
Echo(output, theme)
|
||||
return 0
|
||||
}
|
||||
var cmd *exec.Cmd
|
||||
switch lang {
|
||||
case "node":
|
||||
cmd = CreateNodejs(args, fxrc)
|
||||
case "python", "python3":
|
||||
cmd = CreatePython(lang, args)
|
||||
case "ruby":
|
||||
cmd = CreateRuby(args)
|
||||
default:
|
||||
panic("unknown lang")
|
||||
}
|
||||
|
||||
// TODO: Reimplement stringify with io.Reader.
|
||||
cmd.Stdin = strings.NewReader(Stringify(input))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
exitCode := 1
|
||||
status, ok := err.(*exec.ExitError)
|
||||
if ok {
|
||||
exitCode = status.ExitCode()
|
||||
} else {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
fmt.Print(string(output))
|
||||
return exitCode
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(bytes.NewReader(output))
|
||||
dec.UseNumber()
|
||||
object, err := Parse(dec)
|
||||
if err != nil {
|
||||
fmt.Print(string(output))
|
||||
return 0
|
||||
}
|
||||
Echo(object, theme)
|
||||
if dec.InputOffset() < int64(len(output)) {
|
||||
fmt.Print(string(output[dec.InputOffset():]))
|
||||
}
|
||||
return 0
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func CreateRuby(args []string) *exec.Cmd {
|
||||
cmd := exec.Command("ruby", "-e", ruby(args))
|
||||
return cmd
|
||||
}
|
||||
|
||||
//go:embed ruby.rb
|
||||
var templateRuby string
|
||||
|
||||
func ruby(args []string) string {
|
||||
rs := "\n"
|
||||
for i, a := range args {
|
||||
rs += fmt.Sprintf(
|
||||
`begin
|
||||
x = lambda {|x| %v }.call(x)
|
||||
`, a)
|
||||
// Generate a beautiful error message.
|
||||
rs += "rescue Exception => e\n"
|
||||
pre, post, pointer := trace(args, i)
|
||||
rs += fmt.Sprintf(
|
||||
` STDERR.puts "\n #{%q} #{%q} #{%q}\n %v\n\n#{e}\n"
|
||||
exit(1)
|
||||
`,
|
||||
pre, a, post,
|
||||
pointer,
|
||||
)
|
||||
rs += "end\n"
|
||||
}
|
||||
return fmt.Sprintf(templateRuby, rs)
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
require 'json'
|
||||
x = JSON.parse(STDIN.read)
|
||||
|
||||
# Reducers %v
|
||||
|
||||
puts JSON.generate(x)
|
@ -1,215 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"unicode"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
)
|
||||
|
||||
type state int
|
||||
|
||||
const (
|
||||
start state = iota
|
||||
unknown
|
||||
propOrIndex
|
||||
prop
|
||||
index
|
||||
indexEnd
|
||||
number
|
||||
doubleQuote
|
||||
doubleQuoteEscape
|
||||
singleQuote
|
||||
singleQuoteEscape
|
||||
)
|
||||
|
||||
func SplitSimplePath(args []string) ([]interface{}, bool) {
|
||||
path := make([]interface{}, 0)
|
||||
for _, arg := range args {
|
||||
s := ""
|
||||
state := start
|
||||
for _, ch := range arg {
|
||||
switch state {
|
||||
|
||||
case start:
|
||||
switch {
|
||||
case ch == 'x':
|
||||
state = unknown
|
||||
case ch == '.':
|
||||
state = propOrIndex
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case unknown:
|
||||
switch {
|
||||
case ch == '.':
|
||||
state = prop
|
||||
s = ""
|
||||
case ch == '[':
|
||||
state = index
|
||||
s = ""
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case propOrIndex:
|
||||
switch {
|
||||
case isProp(ch):
|
||||
state = prop
|
||||
s = string(ch)
|
||||
case ch == '[':
|
||||
state = index
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case prop:
|
||||
switch {
|
||||
case isProp(ch):
|
||||
s += string(ch)
|
||||
case ch == '.':
|
||||
state = prop
|
||||
path = append(path, s)
|
||||
s = ""
|
||||
case ch == '[':
|
||||
state = index
|
||||
path = append(path, s)
|
||||
s = ""
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case index:
|
||||
switch {
|
||||
case unicode.IsDigit(ch):
|
||||
state = number
|
||||
s = string(ch)
|
||||
case ch == '"':
|
||||
state = doubleQuote
|
||||
s = ""
|
||||
case ch == '\'':
|
||||
state = singleQuote
|
||||
s = ""
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case indexEnd:
|
||||
switch {
|
||||
case ch == ']':
|
||||
state = unknown
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case number:
|
||||
switch {
|
||||
case unicode.IsDigit(ch):
|
||||
s += string(ch)
|
||||
case ch == ']':
|
||||
state = unknown
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return path, false
|
||||
}
|
||||
path = append(path, n)
|
||||
s = ""
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case doubleQuote:
|
||||
switch ch {
|
||||
case '"':
|
||||
state = indexEnd
|
||||
path = append(path, s)
|
||||
s = ""
|
||||
case '\\':
|
||||
state = doubleQuoteEscape
|
||||
default:
|
||||
s += string(ch)
|
||||
}
|
||||
|
||||
case doubleQuoteEscape:
|
||||
switch ch {
|
||||
case '"':
|
||||
state = doubleQuote
|
||||
s += string(ch)
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case singleQuote:
|
||||
switch ch {
|
||||
case '\'':
|
||||
state = indexEnd
|
||||
path = append(path, s)
|
||||
s = ""
|
||||
case '\\':
|
||||
state = singleQuoteEscape
|
||||
s += string(ch)
|
||||
default:
|
||||
s += string(ch)
|
||||
}
|
||||
|
||||
case singleQuoteEscape:
|
||||
switch ch {
|
||||
case '\'':
|
||||
state = singleQuote
|
||||
s += string(ch)
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(s) > 0 {
|
||||
if state == prop {
|
||||
path = append(path, s)
|
||||
} else {
|
||||
return path, false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return path, true
|
||||
}
|
||||
|
||||
func isProp(ch rune) bool {
|
||||
return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == '$'
|
||||
}
|
||||
|
||||
func GetBySimplePath(object interface{}, path []interface{}) interface{} {
|
||||
for _, get := range path {
|
||||
switch get := get.(type) {
|
||||
case string:
|
||||
switch o := object.(type) {
|
||||
case *Dict:
|
||||
object = o.Values[get]
|
||||
case string:
|
||||
if get == "length" {
|
||||
object = Number(strconv.Itoa(len([]rune(o))))
|
||||
} else {
|
||||
object = nil
|
||||
}
|
||||
case Array:
|
||||
if get == "length" {
|
||||
object = Number(strconv.Itoa(len(o)))
|
||||
} else {
|
||||
object = nil
|
||||
}
|
||||
default:
|
||||
object = nil
|
||||
}
|
||||
case int:
|
||||
switch o := object.(type) {
|
||||
case Array:
|
||||
object = o[get]
|
||||
default:
|
||||
object = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_splitPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
args []string
|
||||
want []interface{}
|
||||
}{
|
||||
{
|
||||
args: []string{},
|
||||
want: []interface{}{},
|
||||
},
|
||||
{
|
||||
args: []string{"."},
|
||||
want: []interface{}{},
|
||||
},
|
||||
{
|
||||
args: []string{"x"},
|
||||
want: []interface{}{},
|
||||
},
|
||||
{
|
||||
args: []string{".foo"},
|
||||
want: []interface{}{"foo"},
|
||||
},
|
||||
{
|
||||
args: []string{"x.foo"},
|
||||
want: []interface{}{"foo"},
|
||||
},
|
||||
{
|
||||
args: []string{"x[42]"},
|
||||
want: []interface{}{42},
|
||||
},
|
||||
{
|
||||
args: []string{".[42]"},
|
||||
want: []interface{}{42},
|
||||
},
|
||||
{
|
||||
args: []string{".42"},
|
||||
want: []interface{}{"42"},
|
||||
},
|
||||
{
|
||||
args: []string{".физ"},
|
||||
want: []interface{}{"физ"},
|
||||
},
|
||||
{
|
||||
args: []string{".foo.bar"},
|
||||
want: []interface{}{"foo", "bar"},
|
||||
},
|
||||
{
|
||||
args: []string{".foo", ".bar"},
|
||||
want: []interface{}{"foo", "bar"},
|
||||
},
|
||||
{
|
||||
args: []string{".foo[42]"},
|
||||
want: []interface{}{"foo", 42},
|
||||
},
|
||||
{
|
||||
args: []string{".foo[42].bar"},
|
||||
want: []interface{}{"foo", 42, "bar"},
|
||||
},
|
||||
{
|
||||
args: []string{".foo[1][2]"},
|
||||
want: []interface{}{"foo", 1, 2},
|
||||
},
|
||||
{
|
||||
args: []string{".foo[\"bar\"]"},
|
||||
want: []interface{}{"foo", "bar"},
|
||||
},
|
||||
{
|
||||
args: []string{".foo[\"bar\\\"\"]"},
|
||||
want: []interface{}{"foo", "bar\""},
|
||||
},
|
||||
{
|
||||
args: []string{".foo['bar']['baz\\'']"},
|
||||
want: []interface{}{"foo", "bar", "baz\\'"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
|
||||
path, ok := SplitSimplePath(tt.args)
|
||||
require.Equal(t, tt.want, path)
|
||||
require.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_splitPath_negative(t *testing.T) {
|
||||
tests := []struct {
|
||||
args []string
|
||||
}{
|
||||
{
|
||||
args: []string{"./"},
|
||||
},
|
||||
{
|
||||
args: []string{"x/"},
|
||||
},
|
||||
{
|
||||
args: []string{"1+1"},
|
||||
},
|
||||
{
|
||||
args: []string{"x[42"},
|
||||
},
|
||||
{
|
||||
args: []string{".i % 2"},
|
||||
},
|
||||
{
|
||||
args: []string{"x[for x]"},
|
||||
},
|
||||
{
|
||||
args: []string{"x['y'."},
|
||||
},
|
||||
{
|
||||
args: []string{"x[0?"},
|
||||
},
|
||||
{
|
||||
args: []string{"x[\"\\u"},
|
||||
},
|
||||
{
|
||||
args: []string{"x['\\n"},
|
||||
},
|
||||
{
|
||||
args: []string{"x[9999999999999999999999999999999999999]"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
|
||||
path, ok := SplitSimplePath(tt.args)
|
||||
require.False(t, ok, path)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
. "github.com/antonmedv/fx/pkg/theme"
|
||||
)
|
||||
|
||||
func Echo(object interface{}, theme Theme) {
|
||||
if s, ok := object.(string); ok {
|
||||
fmt.Println(s)
|
||||
} else {
|
||||
fmt.Println(PrettyPrint(object, 1, theme))
|
||||
}
|
||||
}
|
||||
|
||||
func trace(args []string, i int) (pre, post, pointer string) {
|
||||
pre = strings.Join(args[:i], " ")
|
||||
if len(pre) > 20 {
|
||||
pre = "..." + pre[len(pre)-20:]
|
||||
}
|
||||
post = strings.Join(args[i+1:], " ")
|
||||
if len(post) > 20 {
|
||||
post = post[:20] + "..."
|
||||
}
|
||||
pointer = fmt.Sprintf(
|
||||
"%v %v %v",
|
||||
strings.Repeat(" ", len(pre)),
|
||||
strings.Repeat("^", len(args[i])),
|
||||
strings.Repeat(" ", len(post)),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
var flatMapRegex = regexp.MustCompile("^(\\.\\w*)+\\[]")
|
||||
|
||||
func fold(s []string) string {
|
||||
if len(s) == 1 {
|
||||
return "x => x" + s[0]
|
||||
}
|
||||
obj := s[0]
|
||||
if obj == "." {
|
||||
obj = "x"
|
||||
} else {
|
||||
obj = "x" + obj
|
||||
}
|
||||
return fmt.Sprintf("x => Object.values(%v).flatMap(%v)", obj, fold(s[1:]))
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/mazznoer/colorgrad"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Theme struct {
|
||||
Cursor Color
|
||||
Syntax Color
|
||||
Preview Color
|
||||
StatusBar Color
|
||||
Search Color
|
||||
Key func(i, len int) Color
|
||||
String Color
|
||||
Null Color
|
||||
Boolean Color
|
||||
Number Color
|
||||
}
|
||||
type Color func(s string) string
|
||||
|
||||
var (
|
||||
defaultCursor = lipgloss.NewStyle().Reverse(true).Render
|
||||
defaultPreview = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("8")).Render
|
||||
defaultStatusBar = lipgloss.NewStyle().Background(lipgloss.Color("7")).Foreground(lipgloss.Color("0")).Render
|
||||
defaultSearch = lipgloss.NewStyle().Background(lipgloss.Color("11")).Foreground(lipgloss.Color("16")).Render
|
||||
defaultNull = fg("8")
|
||||
)
|
||||
|
||||
var Themes = map[string]Theme{
|
||||
"0": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: noColor,
|
||||
StatusBar: noColor,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return noColor },
|
||||
String: noColor,
|
||||
Null: noColor,
|
||||
Boolean: noColor,
|
||||
Number: noColor,
|
||||
},
|
||||
"1": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return boldFg("4") },
|
||||
String: boldFg("2"),
|
||||
Null: defaultNull,
|
||||
Boolean: boldFg("3"),
|
||||
Number: boldFg("6"),
|
||||
},
|
||||
"2": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return fg("#00F5D4") },
|
||||
String: fg("#00BBF9"),
|
||||
Null: defaultNull,
|
||||
Boolean: fg("#F15BB5"),
|
||||
Number: fg("#9B5DE5"),
|
||||
},
|
||||
"3": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return fg("#faf0ca") },
|
||||
String: fg("#f4d35e"),
|
||||
Null: defaultNull,
|
||||
Boolean: fg("#ee964b"),
|
||||
Number: fg("#ee964b"),
|
||||
},
|
||||
"4": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return fg("#4D96FF") },
|
||||
String: fg("#6BCB77"),
|
||||
Null: defaultNull,
|
||||
Boolean: fg("#FF6B6B"),
|
||||
Number: fg("#FFD93D"),
|
||||
},
|
||||
"5": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return boldFg("42") },
|
||||
String: boldFg("213"),
|
||||
Null: defaultNull,
|
||||
Boolean: boldFg("201"),
|
||||
Number: boldFg("201"),
|
||||
},
|
||||
"6": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return gradient("rgb(125,110,221)", "rgb(90%,45%,97%)", "hsl(229,79%,85%)") },
|
||||
String: fg("195"),
|
||||
Null: defaultNull,
|
||||
Boolean: fg("195"),
|
||||
Number: fg("195"),
|
||||
},
|
||||
"7": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return gradient("rgb(123,216,96)", "rgb(255,255,255)") },
|
||||
String: noColor,
|
||||
Null: defaultNull,
|
||||
Boolean: noColor,
|
||||
Number: noColor,
|
||||
},
|
||||
"8": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: gradientKeys("#ff0000", "#ff8700", "#ffd300", "#deff0a", "#a1ff0a", "#0aff99", "#0aefff", "#147df5", "#580aff", "#be0aff"),
|
||||
String: noColor,
|
||||
Null: defaultNull,
|
||||
Boolean: noColor,
|
||||
Number: noColor,
|
||||
},
|
||||
"9": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: gradientKeys("rgb(34,126,34)", "rgb(168,251,60)"),
|
||||
String: gradient("rgb(34,126,34)", "rgb(168,251,60)"),
|
||||
Null: defaultNull,
|
||||
Boolean: noColor,
|
||||
Number: noColor,
|
||||
},
|
||||
}
|
||||
|
||||
func noColor(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func fg(color string) Color {
|
||||
return lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Render
|
||||
}
|
||||
|
||||
func boldFg(color string) Color {
|
||||
return lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(color)).Render
|
||||
}
|
||||
|
||||
func gradient(colors ...string) Color {
|
||||
grad, _ := colorgrad.NewGradient().HtmlColors(colors...).Build()
|
||||
return func(s string) string {
|
||||
runes := []rune(s)
|
||||
colors := grad.ColorfulColors(uint(len(runes)))
|
||||
var out strings.Builder
|
||||
for i, r := range runes {
|
||||
style := lipgloss.NewStyle().Foreground(lipgloss.Color(colors[i].Hex()))
|
||||
out.WriteString(style.Render(string(r)))
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
}
|
||||
|
||||
func gradientKeys(colors ...string) func(i, len int) Color {
|
||||
grad, _ := colorgrad.NewGradient().HtmlColors(colors...).Build()
|
||||
return func(i, len int) Color {
|
||||
return lipgloss.NewStyle().Foreground(lipgloss.Color(grad.At(float64(i) / float64(len)).Hex())).Render
|
||||
}
|
||||
}
|
@ -1,296 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
"github.com/antonmedv/fx/pkg/theme"
|
||||
)
|
||||
|
||||
func (m *model) connect(path string, lineNumber int) {
|
||||
if _, exist := m.pathToLineNumber[path]; exist {
|
||||
return
|
||||
}
|
||||
m.paths = append(m.paths, path)
|
||||
m.pathToIndex[path] = len(m.paths) - 1
|
||||
m.pathToLineNumber[path] = lineNumber
|
||||
m.lineNumberToPath[lineNumber] = path
|
||||
}
|
||||
|
||||
func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path string, selectableValues bool) []string {
|
||||
m.connect(path, lineNumber)
|
||||
ident := strings.Repeat(" ", level)
|
||||
subident := strings.Repeat(" ", level-1)
|
||||
highlight := m.highlightIndex[path]
|
||||
var searchValue []*foundRange
|
||||
if highlight != nil {
|
||||
searchValue = highlight.value
|
||||
}
|
||||
|
||||
switch v.(type) {
|
||||
case nil:
|
||||
return []string{merge(m.explode("null", searchValue, m.theme.Null, path, selectableValues))}
|
||||
|
||||
case bool:
|
||||
if v.(bool) {
|
||||
return []string{merge(m.explode("true", searchValue, m.theme.Boolean, path, selectableValues))}
|
||||
} else {
|
||||
return []string{merge(m.explode("false", searchValue, m.theme.Boolean, path, selectableValues))}
|
||||
}
|
||||
|
||||
case Number:
|
||||
return []string{merge(m.explode(v.(Number).String(), searchValue, m.theme.Number, path, selectableValues))}
|
||||
|
||||
case string:
|
||||
line := fmt.Sprintf("%q", v)
|
||||
chunks := m.explode(line, searchValue, m.theme.String, path, selectableValues)
|
||||
if m.wrap && keyEndPos+width(line) > m.width {
|
||||
return wrapLines(chunks, keyEndPos, m.width, subident)
|
||||
}
|
||||
// No wrap
|
||||
return []string{merge(chunks)}
|
||||
|
||||
case *Dict:
|
||||
if !m.expandedPaths[path] {
|
||||
return []string{m.preview(v, path, selectableValues)}
|
||||
}
|
||||
output := []string{m.printOpenBracket("{", highlight, path, selectableValues)}
|
||||
lineNumber++ // bracket is on separate line
|
||||
keys := v.(*Dict).Keys
|
||||
for i, k := range keys {
|
||||
subpath := path + "." + k
|
||||
highlight := m.highlightIndex[subpath]
|
||||
var keyRanges, delimRanges []*foundRange
|
||||
if highlight != nil {
|
||||
keyRanges = highlight.key
|
||||
delimRanges = highlight.delim
|
||||
}
|
||||
m.connect(subpath, lineNumber)
|
||||
key := fmt.Sprintf("%q", k)
|
||||
keyTheme := m.theme.Key(i, len(keys))
|
||||
key = merge(m.explode(key, keyRanges, keyTheme, subpath, true))
|
||||
value, _ := v.(*Dict).Get(k)
|
||||
delim := merge(m.explode(": ", delimRanges, m.theme.Syntax, subpath, false))
|
||||
keyEndPos := width(ident) + width(key) + width(delim)
|
||||
lines := m.print(value, level+1, lineNumber, keyEndPos, subpath, false)
|
||||
lines[0] = ident + key + delim + lines[0]
|
||||
if i < len(keys)-1 {
|
||||
lines[len(lines)-1] += m.printComma(",", highlight)
|
||||
}
|
||||
output = append(output, lines...)
|
||||
lineNumber += len(lines)
|
||||
}
|
||||
output = append(output, subident+m.printCloseBracket("}", highlight, path, false))
|
||||
return output
|
||||
|
||||
case Array:
|
||||
if !m.expandedPaths[path] {
|
||||
return []string{m.preview(v, path, selectableValues)}
|
||||
}
|
||||
output := []string{m.printOpenBracket("[", highlight, path, selectableValues)}
|
||||
lineNumber++ // bracket is on separate line
|
||||
slice := v.(Array)
|
||||
for i, value := range slice {
|
||||
subpath := fmt.Sprintf("%v[%v]", path, i)
|
||||
s := m.highlightIndex[subpath]
|
||||
m.connect(subpath, lineNumber)
|
||||
lines := m.print(value, level+1, lineNumber, width(ident), subpath, true)
|
||||
lines[0] = ident + lines[0]
|
||||
if i < len(slice)-1 {
|
||||
lines[len(lines)-1] += m.printComma(",", s)
|
||||
}
|
||||
lineNumber += len(lines)
|
||||
output = append(output, lines...)
|
||||
}
|
||||
output = append(output, subident+m.printCloseBracket("]", highlight, path, false))
|
||||
return output
|
||||
|
||||
default:
|
||||
return []string{"unknown type"}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) preview(v interface{}, path string, selectableValues bool) string {
|
||||
searchResult := m.highlightIndex[path]
|
||||
previewStyle := m.theme.Preview
|
||||
if selectableValues && m.cursorPath() == path {
|
||||
previewStyle = m.theme.Cursor
|
||||
}
|
||||
printValue := func(v interface{}) string {
|
||||
switch v := v.(type) {
|
||||
case nil, bool, Number:
|
||||
return previewStyle(fmt.Sprintf("%v", v))
|
||||
case string:
|
||||
return previewStyle(fmt.Sprintf("%q", v))
|
||||
case *Dict:
|
||||
if m.showSize {
|
||||
return previewStyle(toLowerNumber(fmt.Sprintf("{\u2026%v\u2026}", len(v.Keys))))
|
||||
} else {
|
||||
return previewStyle("{\u2026}")
|
||||
}
|
||||
case Array:
|
||||
if m.showSize {
|
||||
return previewStyle(toLowerNumber(fmt.Sprintf("[\u2026%v\u2026]", len(v))))
|
||||
} else {
|
||||
return previewStyle("[\u2026]")
|
||||
}
|
||||
}
|
||||
return "..."
|
||||
}
|
||||
|
||||
switch v := v.(type) {
|
||||
case *Dict:
|
||||
output := m.printOpenBracket("{", searchResult, path, selectableValues)
|
||||
keys := v.Keys
|
||||
for _, k := range keys {
|
||||
key := fmt.Sprintf("%q", k)
|
||||
output += previewStyle(key + ": ")
|
||||
value, _ := v.Get(k)
|
||||
output += printValue(value)
|
||||
break
|
||||
}
|
||||
if len(keys) > 1 {
|
||||
if m.showSize {
|
||||
output += previewStyle(toLowerNumber(fmt.Sprintf(", \u2026%v\u2026", len(v.Keys)-1)))
|
||||
} else {
|
||||
output += previewStyle(", \u2026")
|
||||
}
|
||||
}
|
||||
output += m.printCloseBracket("}", searchResult, path, selectableValues)
|
||||
return output
|
||||
|
||||
case Array:
|
||||
output := m.printOpenBracket("[", searchResult, path, selectableValues)
|
||||
for _, value := range v {
|
||||
output += printValue(value)
|
||||
break
|
||||
}
|
||||
if len(v) > 1 {
|
||||
if m.showSize {
|
||||
output += previewStyle(toLowerNumber(fmt.Sprintf(", \u2026%v\u2026", len(v)-1)))
|
||||
} else {
|
||||
output += previewStyle(", \u2026")
|
||||
}
|
||||
}
|
||||
output += m.printCloseBracket("]", searchResult, path, selectableValues)
|
||||
return output
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
func wrapLines(chunks []withStyle, keyEndPos, mWidth int, subident string) []string {
|
||||
wrappedLines := make([]string, 0)
|
||||
currentLine := ""
|
||||
ident := "" // First line stays on the same line with a "key",
|
||||
pos := keyEndPos // so no ident is needed. Start counting from the "key" offset.
|
||||
for _, chunk := range chunks {
|
||||
buffer := ""
|
||||
for _, ch := range chunk.value {
|
||||
buffer += string(ch)
|
||||
if pos == mWidth-1 {
|
||||
wrappedLines = append(wrappedLines, ident+currentLine+chunk.Render(buffer))
|
||||
currentLine = ""
|
||||
buffer = ""
|
||||
pos = width(subident) // Start counting from ident.
|
||||
ident = subident // After first line, add ident to all.
|
||||
} else {
|
||||
pos++
|
||||
}
|
||||
}
|
||||
currentLine += chunk.Render(buffer)
|
||||
}
|
||||
if width(currentLine) > 0 {
|
||||
wrappedLines = append(wrappedLines, subident+currentLine)
|
||||
}
|
||||
return wrappedLines
|
||||
}
|
||||
|
||||
func (w withStyle) Render(s string) string {
|
||||
return w.style(s)
|
||||
}
|
||||
|
||||
func (m *model) printOpenBracket(line string, s *rangeGroup, path string, selectableValues bool) string {
|
||||
if selectableValues && m.cursorPath() == path {
|
||||
return m.theme.Cursor(line)
|
||||
}
|
||||
if s != nil && s.openBracket != nil {
|
||||
if s.openBracket.parent.index == m.searchResultsCursor {
|
||||
return m.theme.Cursor(line)
|
||||
} else {
|
||||
return m.theme.Search(line)
|
||||
}
|
||||
} else {
|
||||
return m.theme.Syntax(line)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) printCloseBracket(line string, s *rangeGroup, path string, selectableValues bool) string {
|
||||
if selectableValues && m.cursorPath() == path {
|
||||
return m.theme.Cursor(line)
|
||||
}
|
||||
if s != nil && s.closeBracket != nil {
|
||||
if s.closeBracket.parent.index == m.searchResultsCursor {
|
||||
return m.theme.Cursor(line)
|
||||
} else {
|
||||
return m.theme.Search(line)
|
||||
}
|
||||
} else {
|
||||
return m.theme.Syntax(line)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) printComma(line string, s *rangeGroup) string {
|
||||
if s != nil && s.comma != nil {
|
||||
if s.comma.parent.index == m.searchResultsCursor {
|
||||
return m.theme.Cursor(line)
|
||||
} else {
|
||||
return m.theme.Search(line)
|
||||
}
|
||||
} else {
|
||||
return m.theme.Syntax(line)
|
||||
}
|
||||
}
|
||||
|
||||
type withStyle struct {
|
||||
value string
|
||||
style theme.Color
|
||||
}
|
||||
|
||||
func (m *model) explode(line string, highlightRanges []*foundRange, defaultStyle theme.Color, path string, selectable bool) []withStyle {
|
||||
if selectable && m.cursorPath() == path && m.showCursor {
|
||||
return []withStyle{{line, m.theme.Cursor}}
|
||||
}
|
||||
|
||||
out := make([]withStyle, 0, 1)
|
||||
pos := 0
|
||||
for _, r := range highlightRanges {
|
||||
style := m.theme.Search
|
||||
if r.parent.index == m.searchResultsCursor {
|
||||
style = m.theme.Cursor
|
||||
}
|
||||
out = append(out, withStyle{
|
||||
value: line[pos:r.start],
|
||||
style: defaultStyle,
|
||||
})
|
||||
out = append(out, withStyle{
|
||||
value: line[r.start:r.end],
|
||||
style: style,
|
||||
})
|
||||
pos = r.end
|
||||
}
|
||||
out = append(out, withStyle{
|
||||
value: line[pos:],
|
||||
style: defaultStyle,
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func merge(chunks []withStyle) string {
|
||||
out := ""
|
||||
for _, chunk := range chunks {
|
||||
out += chunk.Render(chunk.value)
|
||||
}
|
||||
return out
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
'use strict'
|
||||
const indent = require('indent-string')
|
||||
const config = require('./config')
|
||||
|
||||
function format(value, style, highlightStyle, regexp, transform = x => x) {
|
||||
if (!regexp) {
|
||||
return style(transform(value))
|
||||
}
|
||||
const marked = value
|
||||
.replace(regexp, s => '<highlight>' + s + '<highlight>')
|
||||
|
||||
return transform(marked)
|
||||
.split(/<highlight>/g)
|
||||
.map((s, i) => i % 2 !== 0 ? highlightStyle(s) : style(s))
|
||||
.join('')
|
||||
}
|
||||
|
||||
function print(input, options = {}) {
|
||||
const {expanded, highlight, currentPath} = options
|
||||
const index = new Map()
|
||||
let row = 0
|
||||
|
||||
function doPrint(v, path = '') {
|
||||
index.set(row, path)
|
||||
|
||||
// Code for highlighting parts become cumbersome.
|
||||
// Maybe we should refactor this part.
|
||||
const highlightStyle = (currentPath === path) ? config.highlightCurrent : config.highlight
|
||||
const formatStyle = (v, style) => format(v, style, highlightStyle, highlight)
|
||||
const formatText = (v, style, path) => {
|
||||
const highlightStyle = (currentPath === path) ? config.highlightCurrent : config.highlight
|
||||
return format(v, style, highlightStyle, highlight, JSON.stringify)
|
||||
}
|
||||
|
||||
const eol = () => {
|
||||
row++
|
||||
return '\n'
|
||||
}
|
||||
|
||||
if (typeof v === 'undefined') {
|
||||
return void 0
|
||||
}
|
||||
|
||||
if (v === null) {
|
||||
return formatStyle(JSON.stringify(v), config.null)
|
||||
}
|
||||
|
||||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||||
return formatStyle(JSON.stringify(v), config.number)
|
||||
}
|
||||
|
||||
if (typeof v === 'object' && v.isLosslessNumber) {
|
||||
return formatStyle(v.toString(), config.number)
|
||||
}
|
||||
|
||||
if (typeof v === 'boolean') {
|
||||
return formatStyle(JSON.stringify(v), config.boolean)
|
||||
|
||||
}
|
||||
|
||||
if (typeof v === 'string') {
|
||||
return formatText(v, config.string, path)
|
||||
}
|
||||
|
||||
if (Array.isArray(v)) {
|
||||
let output = config.bracket('[')
|
||||
const len = v.length
|
||||
|
||||
if (len > 0) {
|
||||
if (expanded && !expanded.has(path)) {
|
||||
output += '\u2026'
|
||||
} else {
|
||||
output += eol()
|
||||
let i = 0
|
||||
for (let item of v) {
|
||||
const value = typeof item === 'undefined' ? null : item // JSON.stringify compatibility
|
||||
output += indent(doPrint(value, path + '[' + i + ']'), config.space)
|
||||
output += i++ < len - 1 ? config.comma(',') : ''
|
||||
output += eol()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output + config.bracket(']')
|
||||
}
|
||||
|
||||
if (typeof v === 'object' && v.constructor === Object) {
|
||||
let output = config.bracket('{')
|
||||
|
||||
const entries = Object.entries(v).filter(([key, value]) => typeof value !== 'undefined') // JSON.stringify compatibility
|
||||
const len = entries.length
|
||||
|
||||
if (len > 0) {
|
||||
if (expanded && !expanded.has(path)) {
|
||||
output += '\u2026'
|
||||
} else {
|
||||
output += eol()
|
||||
let i = 0
|
||||
for (let [key, value] of entries) {
|
||||
const part = formatText(key, config.key, path + '.' + 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 JSON.stringify(v, null, config.space)
|
||||
}
|
||||
|
||||
return [doPrint(input), index]
|
||||
}
|
||||
|
||||
module.exports = print
|
@ -0,0 +1,49 @@
|
||||
'use strict'
|
||||
const JSON = require('lossless-json') // override JSON for user's code
|
||||
|
||||
module.exports = function (json, code) {
|
||||
if (process.env.FX_APPLY) {
|
||||
return global[process.env.FX_APPLY](code)(json)
|
||||
}
|
||||
|
||||
if ('.' === code) {
|
||||
return json
|
||||
}
|
||||
|
||||
if ('?' === code) {
|
||||
return Object.keys(json)
|
||||
}
|
||||
|
||||
if (/^(\.\w*)+\[]/.test(code)) {
|
||||
function fold(s) {
|
||||
if (s.length === 1) {
|
||||
return 'x => x' + s[0]
|
||||
}
|
||||
let obj = s.shift()
|
||||
obj = obj === '.' ? 'x' : 'x' + obj
|
||||
return `x => Object.values(${obj}).flatMap(${fold(s)})`
|
||||
}
|
||||
code = fold(code.split('[]'))
|
||||
}
|
||||
|
||||
if (/^\.\[/.test(code)) {
|
||||
return eval(`function fn() {
|
||||
return this${code.substring(1)}
|
||||
}; fn`).call(json)
|
||||
}
|
||||
|
||||
if (/^\./.test(code)) {
|
||||
return eval(`function fn() {
|
||||
return this${code}
|
||||
}; fn`).call(json)
|
||||
}
|
||||
|
||||
const fn = eval(`function fn() {
|
||||
return ${code}
|
||||
}; fn`).call(json)
|
||||
|
||||
if (typeof fn === 'function') {
|
||||
return fn(json)
|
||||
}
|
||||
return fn
|
||||
}
|
@ -1,249 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
)
|
||||
|
||||
type searchResult struct {
|
||||
path string
|
||||
index int
|
||||
ranges []*foundRange
|
||||
}
|
||||
|
||||
type rangeKind int
|
||||
|
||||
const (
|
||||
keyRange rangeKind = 1 + iota
|
||||
valueRange
|
||||
delimRange
|
||||
openBracketRange
|
||||
closeBracketRange
|
||||
commaRange
|
||||
)
|
||||
|
||||
type foundRange struct {
|
||||
parent *searchResult
|
||||
// Range needs separate path, as for one searchResult's path
|
||||
// there can be multiple ranges for different paths (within parent path).
|
||||
path string
|
||||
start, end int
|
||||
kind rangeKind
|
||||
}
|
||||
|
||||
type rangeGroup struct {
|
||||
key []*foundRange
|
||||
value []*foundRange
|
||||
delim []*foundRange
|
||||
openBracket *foundRange
|
||||
closeBracket *foundRange
|
||||
comma *foundRange
|
||||
}
|
||||
|
||||
func (m *model) clearSearchResults() {
|
||||
m.searchRegexCompileError = ""
|
||||
m.searchResults = nil
|
||||
m.highlightIndex = nil
|
||||
}
|
||||
|
||||
func (m *model) doSearch(s string) {
|
||||
m.clearSearchResults()
|
||||
re, err := regexp.Compile("(?i)" + s)
|
||||
if err != nil {
|
||||
m.searchRegexCompileError = err.Error()
|
||||
m.searchInput.Blur()
|
||||
return
|
||||
}
|
||||
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
|
||||
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
|
||||
m.indexSearchResults()
|
||||
m.searchInput.Blur()
|
||||
m.showSearchResults = true
|
||||
m.jumpToSearchResult(0)
|
||||
}
|
||||
|
||||
func (m *model) remapSearchResult(object interface{}, path string, pos int, indexes [][]int, id int, current *searchResult) (int, int, *searchResult) {
|
||||
switch object.(type) {
|
||||
case nil:
|
||||
s := "null"
|
||||
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
|
||||
return pos + len(s), id, current
|
||||
|
||||
case bool:
|
||||
var s string
|
||||
if object.(bool) {
|
||||
s = "true"
|
||||
} else {
|
||||
s = "false"
|
||||
}
|
||||
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
|
||||
return pos + len(s), id, current
|
||||
|
||||
case Number:
|
||||
s := object.(Number).String()
|
||||
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
|
||||
return pos + len(s), id, current
|
||||
|
||||
case string:
|
||||
// TODO: Wrap the string, save a line number according to current wrap or no wrap mode.
|
||||
s := fmt.Sprintf("%q", object)
|
||||
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
|
||||
return pos + len(s), id, current
|
||||
|
||||
case *Dict:
|
||||
id, current = m.findRanges(openBracketRange, "{", path, pos, indexes, id, current)
|
||||
pos++ // {
|
||||
for i, k := range object.(*Dict).Keys {
|
||||
subpath := path + "." + k
|
||||
|
||||
key := fmt.Sprintf("%q", k)
|
||||
id, current = m.findRanges(keyRange, key, subpath, pos, indexes, id, current)
|
||||
pos += len(key)
|
||||
|
||||
delim := ": "
|
||||
id, current = m.findRanges(delimRange, delim, subpath, pos, indexes, id, current)
|
||||
pos += len(delim)
|
||||
|
||||
pos, id, current = m.remapSearchResult(object.(*Dict).Values[k], subpath, pos, indexes, id, current)
|
||||
if i < len(object.(*Dict).Keys)-1 {
|
||||
comma := ","
|
||||
id, current = m.findRanges(commaRange, comma, subpath, pos, indexes, id, current)
|
||||
pos += len(comma)
|
||||
}
|
||||
}
|
||||
id, current = m.findRanges(closeBracketRange, "}", path, pos, indexes, id, current)
|
||||
pos++ // }
|
||||
return pos, id, current
|
||||
|
||||
case Array:
|
||||
id, current = m.findRanges(openBracketRange, "[", path, pos, indexes, id, current)
|
||||
pos++ // [
|
||||
for i, v := range object.(Array) {
|
||||
subpath := fmt.Sprintf("%v[%v]", path, i)
|
||||
pos, id, current = m.remapSearchResult(v, subpath, pos, indexes, id, current)
|
||||
if i < len(object.(Array))-1 {
|
||||
comma := ","
|
||||
id, current = m.findRanges(commaRange, comma, subpath, pos, indexes, id, current)
|
||||
pos += len(comma)
|
||||
}
|
||||
}
|
||||
id, current = m.findRanges(closeBracketRange, "]", path, pos, indexes, id, current)
|
||||
pos++ // ]
|
||||
return pos, id, current
|
||||
|
||||
default:
|
||||
panic("unexpected object type")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) findRanges(kind rangeKind, s string, path string, pos int, indexes [][]int, id int, current *searchResult) (int, *searchResult) {
|
||||
for ; id < len(indexes); id++ {
|
||||
start, end := indexes[id][0]-pos, indexes[id][1]-pos
|
||||
if end <= 0 {
|
||||
current = nil
|
||||
continue
|
||||
}
|
||||
if start < len(s) {
|
||||
if current == nil {
|
||||
current = &searchResult{
|
||||
path: path,
|
||||
index: len(m.searchResults),
|
||||
}
|
||||
m.searchResults = append(m.searchResults, current)
|
||||
}
|
||||
found := &foundRange{
|
||||
parent: current,
|
||||
path: path,
|
||||
start: max(start, 0),
|
||||
end: min(end, len(s)),
|
||||
kind: kind,
|
||||
}
|
||||
current.ranges = append(current.ranges, found)
|
||||
if end < len(s) {
|
||||
current = nil
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return id, current
|
||||
}
|
||||
|
||||
func (m *model) indexSearchResults() {
|
||||
m.highlightIndex = map[string]*rangeGroup{}
|
||||
for _, s := range m.searchResults {
|
||||
for _, r := range s.ranges {
|
||||
highlight, exist := m.highlightIndex[r.path]
|
||||
if !exist {
|
||||
highlight = &rangeGroup{}
|
||||
m.highlightIndex[r.path] = highlight
|
||||
}
|
||||
switch r.kind {
|
||||
case keyRange:
|
||||
highlight.key = append(highlight.key, r)
|
||||
case valueRange:
|
||||
highlight.value = append(highlight.value, r)
|
||||
case delimRange:
|
||||
highlight.delim = append(highlight.delim, r)
|
||||
case openBracketRange:
|
||||
highlight.openBracket = r
|
||||
case closeBracketRange:
|
||||
highlight.closeBracket = r
|
||||
case commaRange:
|
||||
highlight.comma = r
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) jumpToSearchResult(at int) {
|
||||
if len(m.searchResults) == 0 {
|
||||
return
|
||||
}
|
||||
m.showCursor = false
|
||||
m.searchResultsCursor = at % len(m.searchResults)
|
||||
desiredPath := m.searchResults[m.searchResultsCursor].path
|
||||
_, ok := m.pathToLineNumber[desiredPath]
|
||||
if ok {
|
||||
m.cursor = m.pathToIndex[desiredPath]
|
||||
m.scrollDownToCursor()
|
||||
m.render()
|
||||
} else {
|
||||
m.expandToPath(desiredPath)
|
||||
m.render()
|
||||
m.jumpToSearchResult(at)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) expandToPath(path string) {
|
||||
m.expandedPaths[path] = true
|
||||
if path != "" {
|
||||
m.expandToPath(m.parents[path])
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) nextSearchResult() {
|
||||
if len(m.searchResults) > 0 {
|
||||
m.jumpToSearchResult((m.searchResultsCursor + 1) % len(m.searchResults))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) prevSearchResult() {
|
||||
i := m.searchResultsCursor - 1
|
||||
if i < 0 {
|
||||
i = len(m.searchResults) - 1
|
||||
}
|
||||
m.jumpToSearchResult(i)
|
||||
}
|
||||
|
||||
func (m *model) resultsCursorPath() string {
|
||||
if len(m.searchResults) == 0 {
|
||||
return "?"
|
||||
}
|
||||
return m.searchResults[m.searchResultsCursor].path
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
"github.com/stretchr/testify/require"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_search_values(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
object interface{}
|
||||
want *foundRange
|
||||
}{
|
||||
{name: "null", object: nil},
|
||||
{name: "true", object: true},
|
||||
{name: "false", object: false},
|
||||
{name: "Number", object: Number("42")},
|
||||
{name: "string", object: "Hello, World!"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := &model{
|
||||
json: tt.object,
|
||||
}
|
||||
re, _ := regexp.Compile(".+")
|
||||
str := Stringify(m.json)
|
||||
indexes := re.FindAllStringIndex(str, -1)
|
||||
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
|
||||
|
||||
s := &searchResult{path: ""}
|
||||
s.ranges = append(s.ranges, &foundRange{
|
||||
parent: s,
|
||||
path: "",
|
||||
start: 0,
|
||||
end: len(str),
|
||||
kind: valueRange,
|
||||
})
|
||||
require.Equal(t, []*searchResult{s}, m.searchResults)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_search_array(t *testing.T) {
|
||||
msg := `
|
||||
["first","second"]
|
||||
^^^^^ ^^^^^^
|
||||
`
|
||||
m := &model{
|
||||
json: Array{"first", "second"},
|
||||
}
|
||||
re, _ := regexp.Compile("\\w+")
|
||||
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
|
||||
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
|
||||
|
||||
s1 := &searchResult{path: "[0]"}
|
||||
s1.ranges = append(s1.ranges,
|
||||
&foundRange{
|
||||
parent: s1,
|
||||
path: "[0]",
|
||||
start: 1,
|
||||
end: 6,
|
||||
kind: valueRange,
|
||||
},
|
||||
)
|
||||
s2 := &searchResult{path: "[1]", index: 1}
|
||||
s2.ranges = append(s2.ranges,
|
||||
&foundRange{
|
||||
parent: s2,
|
||||
path: "[1]",
|
||||
start: 1,
|
||||
end: 7,
|
||||
kind: valueRange,
|
||||
},
|
||||
)
|
||||
require.Equal(t, []*searchResult{s1, s2}, m.searchResults, msg)
|
||||
}
|
||||
|
||||
func Test_search_between_array(t *testing.T) {
|
||||
msg := `
|
||||
["first","second"]
|
||||
^^^^^^^^^^^^^^
|
||||
`
|
||||
m := &model{
|
||||
json: Array{"first", "second"},
|
||||
}
|
||||
re, _ := regexp.Compile("\\w.+\\w")
|
||||
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
|
||||
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
|
||||
|
||||
s := &searchResult{path: "[0]"}
|
||||
s.ranges = append(s.ranges,
|
||||
&foundRange{
|
||||
parent: s,
|
||||
path: "[0]",
|
||||
start: 1,
|
||||
end: 7,
|
||||
kind: valueRange,
|
||||
},
|
||||
&foundRange{
|
||||
parent: s,
|
||||
path: "[0]",
|
||||
start: 0,
|
||||
end: 1,
|
||||
kind: commaRange,
|
||||
},
|
||||
&foundRange{
|
||||
parent: s,
|
||||
path: "[1]",
|
||||
start: 0,
|
||||
end: 7,
|
||||
kind: valueRange,
|
||||
},
|
||||
)
|
||||
require.Equal(t, []*searchResult{s}, m.searchResults, msg)
|
||||
}
|
||||
|
||||
func Test_search_dict(t *testing.T) {
|
||||
msg := `
|
||||
{"key": "hello world"}
|
||||
^^^^^ ^^^^^^^^^^^^^
|
||||
`
|
||||
d := NewDict()
|
||||
d.Set("key", "hello world")
|
||||
m := &model{
|
||||
json: d,
|
||||
}
|
||||
re, _ := regexp.Compile("\"[\\w\\s]+\"")
|
||||
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
|
||||
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
|
||||
|
||||
s1 := &searchResult{path: ".key"}
|
||||
s1.ranges = append(s1.ranges,
|
||||
&foundRange{
|
||||
parent: s1,
|
||||
path: ".key",
|
||||
start: 0,
|
||||
end: 5,
|
||||
kind: keyRange,
|
||||
},
|
||||
)
|
||||
s2 := &searchResult{path: ".key", index: 1}
|
||||
s2.ranges = append(s2.ranges,
|
||||
&foundRange{
|
||||
parent: s2,
|
||||
path: ".key",
|
||||
start: 0,
|
||||
end: 13,
|
||||
kind: valueRange,
|
||||
},
|
||||
)
|
||||
require.Equal(t, []*searchResult{s1, s2}, m.searchResults, msg)
|
||||
}
|
||||
|
||||
func Test_search_dict_with_array(t *testing.T) {
|
||||
msg := `
|
||||
{"first": [1,2],"second": []}
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
`
|
||||
d := NewDict()
|
||||
d.Set("first", Array{Number("1"), Number("2")})
|
||||
d.Set("second", Array{})
|
||||
m := &model{
|
||||
json: d,
|
||||
}
|
||||
re, _ := regexp.Compile(".+")
|
||||
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
|
||||
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
|
||||
|
||||
s := &searchResult{path: ""}
|
||||
s.ranges = append(s.ranges,
|
||||
/* { */ &foundRange{parent: s, path: "", start: 0, end: 1, kind: openBracketRange},
|
||||
/* "first" */ &foundRange{parent: s, path: ".first", start: 0, end: 7, kind: keyRange},
|
||||
/* : */ &foundRange{parent: s, path: ".first", start: 0, end: 2, kind: delimRange},
|
||||
/* [ */ &foundRange{parent: s, path: ".first", start: 0, end: 1, kind: openBracketRange},
|
||||
/* 1 */ &foundRange{parent: s, path: ".first[0]", start: 0, end: 1, kind: valueRange},
|
||||
/* , */ &foundRange{parent: s, path: ".first[0]", start: 0, end: 1, kind: commaRange},
|
||||
/* 2 */ &foundRange{parent: s, path: ".first[1]", start: 0, end: 1, kind: valueRange},
|
||||
/* ] */ &foundRange{parent: s, path: ".first", start: 0, end: 1, kind: closeBracketRange},
|
||||
/* , */ &foundRange{parent: s, path: ".first", start: 0, end: 1, kind: commaRange},
|
||||
/* "second" */ &foundRange{parent: s, path: ".second", start: 0, end: 8, kind: keyRange},
|
||||
/* : */ &foundRange{parent: s, path: ".second", start: 0, end: 2, kind: delimRange},
|
||||
/* [ */ &foundRange{parent: s, path: ".second", start: 0, end: 1, kind: openBracketRange},
|
||||
/* ] */ &foundRange{parent: s, path: ".second", start: 0, end: 1, kind: closeBracketRange},
|
||||
/* } */ &foundRange{parent: s, path: "", start: 0, end: 1, kind: closeBracketRange},
|
||||
)
|
||||
require.Equal(t, []*searchResult{s}, m.searchResults, msg)
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
name: fx
|
||||
version: 24.0.0
|
||||
summary: Terminal JSON viewer
|
||||
description: Terminal JSON viewer
|
||||
base: core18
|
||||
grade: stable
|
||||
confinement: strict
|
||||
|
||||
apps:
|
||||
fx:
|
||||
command: fx
|
||||
plugs: [home, network]
|
||||
|
||||
parts:
|
||||
fx:
|
||||
plugin: go
|
||||
go-channel: 1.18/stable
|
||||
source: .
|
||||
source-type: git
|
||||
go-importpath: github.com/antonmedv/fx
|
@ -0,0 +1,33 @@
|
||||
'use strict'
|
||||
const JSON = require('lossless-json')
|
||||
const skip = Symbol('skip')
|
||||
|
||||
function select(cb) {
|
||||
return json => {
|
||||
if (!cb(json)) {
|
||||
throw skip
|
||||
}
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
function filter(cb) {
|
||||
return json => {
|
||||
if (cb(json)) {
|
||||
throw skip
|
||||
}
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
function save(json) {
|
||||
if (!global.FX_FILENAME) {
|
||||
throw "No filename provided.\nTo edit-in-place, specify JSON file as first argument."
|
||||
}
|
||||
require('fs').writeFileSync(global.FX_FILENAME, JSON.stringify(json, null, 2))
|
||||
return json
|
||||
}
|
||||
|
||||
Object.assign(exports, {skip, select, filter, save})
|
||||
Object.assign(global, exports)
|
||||
global.std = exports
|
@ -1,42 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
. "github.com/antonmedv/fx/pkg/reducer"
|
||||
. "github.com/antonmedv/fx/pkg/theme"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func stream(dec *json.Decoder, object interface{}, lang string, args []string, theme Theme, fxrc string) int {
|
||||
var vm *goja.Runtime
|
||||
var fn goja.Callable
|
||||
var err error
|
||||
if lang == "js" {
|
||||
vm, fn, err = CreateJS(args, fxrc)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
for {
|
||||
if object != nil {
|
||||
if lang == "js" {
|
||||
ReduceJS(vm, fn, object, theme)
|
||||
} else {
|
||||
Reduce(object, lang, args, theme, fxrc)
|
||||
}
|
||||
}
|
||||
object, err = Parse(dec)
|
||||
if err == io.EOF {
|
||||
return 0
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println("JSON Parse Error:", err.Error())
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
'use strict'
|
||||
const JSON = require('lossless-json')
|
||||
|
||||
function apply(cb, input) {
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(input)
|
||||
} catch (e) {
|
||||
process.stderr.write(e.toString() + '\n')
|
||||
return
|
||||
}
|
||||
cb(json)
|
||||
}
|
||||
|
||||
function stream(from, cb) {
|
||||
let buff = ''
|
||||
let lastChar = ''
|
||||
let len = 0
|
||||
let depth = 0
|
||||
let isString = false
|
||||
|
||||
let count = 0
|
||||
let head = ''
|
||||
const check = (i) => {
|
||||
if (depth <= 0) {
|
||||
const input = buff.substring(0, len + i + 1)
|
||||
|
||||
if (count > 0) {
|
||||
if (head !== '') {
|
||||
apply(cb, head)
|
||||
head = ''
|
||||
}
|
||||
|
||||
apply(cb, input)
|
||||
} else {
|
||||
head = input
|
||||
}
|
||||
|
||||
buff = buff.substring(len + i + 1)
|
||||
len = -i - 1
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isStream() {
|
||||
return count > 1
|
||||
},
|
||||
value() {
|
||||
return head + buff
|
||||
},
|
||||
read() {
|
||||
let chunk
|
||||
|
||||
while ((chunk = from.read())) {
|
||||
len = buff.length
|
||||
buff += chunk
|
||||
|
||||
for (let i = 0; i < chunk.length; i++) {
|
||||
if (isString) {
|
||||
if (chunk[i] === '"' && ((i === 0 && lastChar !== '\\') || (i > 0 && chunk[i - 1] !== '\\'))) {
|
||||
isString = false
|
||||
check(i)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (chunk[i] === '{' || chunk[i] === '[') {
|
||||
depth++
|
||||
} else if (chunk[i] === '}' || chunk[i] === ']') {
|
||||
depth--
|
||||
check(i)
|
||||
} else if (chunk[i] === '"') {
|
||||
isString = true
|
||||
}
|
||||
}
|
||||
|
||||
lastChar = chunk[chunk.length - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = stream
|
@ -0,0 +1,85 @@
|
||||
'use strict'
|
||||
const test = require('ava')
|
||||
const {execSync} = require('child_process')
|
||||
const stream = require('./stream')
|
||||
|
||||
function fx(json, code = '') {
|
||||
return execSync(`echo '${JSON.stringify(json)}' | node index.js ${code}`).toString('utf8')
|
||||
}
|
||||
|
||||
test('pass', t => {
|
||||
const r = fx([{'greeting': 'hello world'}])
|
||||
t.deepEqual(JSON.parse(r), [{'greeting': 'hello world'}])
|
||||
})
|
||||
|
||||
test('anon func', t => {
|
||||
const r = fx({'key': 'value'}, '\'function (x) { return x.key }\'')
|
||||
t.is(r, 'value\n')
|
||||
})
|
||||
|
||||
test('arrow func', t => {
|
||||
const r = fx({'key': 'value'}, '\'x => x.key\'')
|
||||
t.is(r, 'value\n')
|
||||
})
|
||||
|
||||
test('arrow func ()', t => {
|
||||
const r = fx({'key': 'value'}, '\'(x) => x.key\'')
|
||||
t.is(r, 'value\n')
|
||||
})
|
||||
|
||||
test('this bind', t => {
|
||||
const r = fx([1, 2, 3, 4, 5], '\'this.map(x => x * this.length)\'')
|
||||
t.deepEqual(JSON.parse(r), [5, 10, 15, 20, 25])
|
||||
})
|
||||
|
||||
test('chain', t => {
|
||||
const r = fx({'items': ['foo', 'bar']}, '\'this.items\' \'.\' \'x => x[1]\'')
|
||||
t.is(r, 'bar\n')
|
||||
})
|
||||
|
||||
test('file argument', t => {
|
||||
const r = execSync(`node index.js package.json .name`).toString('utf8')
|
||||
t.is(r, 'fx\n')
|
||||
})
|
||||
|
||||
test('stream', t => {
|
||||
const input = `
|
||||
{"index": 0} {"index": 1}
|
||||
{"index": 2, "quote": "\\""}
|
||||
{"index": 3} "Hello" "world"
|
||||
{"index": 6, "key": "one \\"two\\" three"}
|
||||
`
|
||||
t.plan(7 * (input.length - 1))
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const parts = [input.substring(0, i), input.substring(i)]
|
||||
|
||||
const reader = stream(
|
||||
{
|
||||
read() {
|
||||
return parts.shift()
|
||||
}
|
||||
},
|
||||
json => {
|
||||
t.pass()
|
||||
}
|
||||
)
|
||||
|
||||
reader.read()
|
||||
}
|
||||
})
|
||||
|
||||
test('lossless number', t => {
|
||||
const r = execSync(`echo '{"long": 123456789012345678901}' | node index.js .long`).toString('utf8')
|
||||
t.is(r, '123456789012345678901\n')
|
||||
})
|
||||
|
||||
test('value iterator', t => {
|
||||
const r = fx({master: {foo: [{bar: [{val: 1}]}]}}, '.master.foo[].bar[].val')
|
||||
t.deepEqual(JSON.parse(r), [1])
|
||||
})
|
||||
|
||||
test('value iterator simple', t => {
|
||||
const r = fx([{val:1},{val:2}], '.[].val')
|
||||
t.deepEqual(JSON.parse(r), [1, 2])
|
||||
})
|
@ -1,50 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func clamp(v, low, high int) int {
|
||||
if high < low {
|
||||
low, high = high, low
|
||||
}
|
||||
return min(high, max(low, v))
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func width(s string) int {
|
||||
return lipgloss.Width(s)
|
||||
}
|
||||
|
||||
func accessor(path string, to interface{}) string {
|
||||
return fmt.Sprintf("%v[%v]", path, to)
|
||||
}
|
||||
|
||||
func toLowerNumber(s string) string {
|
||||
var out strings.Builder
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case '0' <= r && r <= '9':
|
||||
out.WriteRune('\u2080' + (r - '\u0030'))
|
||||
default:
|
||||
out.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_clamp(t *testing.T) {
|
||||
got := clamp(1, 2, 3)
|
||||
if got != 2 {
|
||||
t.Errorf("clamp() = %v, want 2", got)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_max(t *testing.T) {
|
||||
got := max(1, 2)
|
||||
if got != 2 {
|
||||
t.Errorf("max() = %v, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_min(t *testing.T) {
|
||||
got := min(1, 2)
|
||||
if got != 1 {
|
||||
t.Errorf("min() = %v, want 1", got)
|
||||
}
|
||||
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
package main
|
||||
|
||||
const version = "24.0.0"
|
@ -1,128 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
func (m *model) AtTop() bool {
|
||||
return m.offset <= 0
|
||||
}
|
||||
|
||||
func (m *model) AtBottom() bool {
|
||||
return m.offset >= m.maxYOffset()
|
||||
}
|
||||
|
||||
func (m *model) PastBottom() bool {
|
||||
return m.offset > m.maxYOffset()
|
||||
}
|
||||
|
||||
func (m *model) ScrollPercent() float64 {
|
||||
if m.height >= len(m.lines) {
|
||||
return 1.0
|
||||
}
|
||||
y := float64(m.offset)
|
||||
h := float64(m.height)
|
||||
t := float64(len(m.lines) - 1)
|
||||
v := y / (t - h)
|
||||
return math.Max(0.0, math.Min(1.0, v))
|
||||
}
|
||||
|
||||
func (m *model) maxYOffset() int {
|
||||
return max(0, len(m.lines)-m.height)
|
||||
}
|
||||
|
||||
func (m *model) visibleLines() (lines []string) {
|
||||
if len(m.lines) > 0 {
|
||||
top := max(0, m.offset)
|
||||
bottom := clamp(m.offset+m.height, top, len(m.lines))
|
||||
lines = m.lines[top:bottom]
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (m *model) SetOffset(n int) {
|
||||
m.offset = clamp(n, 0, m.maxYOffset())
|
||||
}
|
||||
|
||||
func (m *model) ViewDown() {
|
||||
if m.AtBottom() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(m.offset + m.height)
|
||||
}
|
||||
|
||||
func (m *model) ViewUp() {
|
||||
if m.AtTop() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(m.offset - m.height)
|
||||
}
|
||||
|
||||
func (m *model) HalfViewDown() {
|
||||
if m.AtBottom() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(m.offset + m.height/2)
|
||||
}
|
||||
|
||||
func (m *model) HalfViewUp() {
|
||||
if m.AtTop() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(m.offset - m.height/2)
|
||||
}
|
||||
|
||||
func (m *model) LineDown(n int) {
|
||||
if m.AtBottom() || n == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the Number of lines by which we're going to scroll isn't
|
||||
// greater than the Number of lines we actually have left before we reach
|
||||
// the bottom.
|
||||
m.SetOffset(m.offset + n)
|
||||
}
|
||||
|
||||
func (m *model) LineUp(n int) {
|
||||
if m.AtTop() || n == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the Number of lines by which we're going to scroll isn't
|
||||
// greater than the Number of lines we are from the top.
|
||||
m.SetOffset(m.offset - n)
|
||||
}
|
||||
|
||||
func (m *model) GotoTop() {
|
||||
if m.AtTop() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(0)
|
||||
}
|
||||
|
||||
func (m *model) GotoBottom() {
|
||||
m.SetOffset(m.maxYOffset())
|
||||
}
|
||||
|
||||
func (m *model) scrollDownToCursor() {
|
||||
at := m.cursorLineNumber()
|
||||
if m.offset <= at { // cursor is lower
|
||||
m.LineDown(max(0, at-(m.offset+m.height-1))) // minus one is due to cursorLineNumber() starts from 0
|
||||
} else {
|
||||
m.SetOffset(at)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) scrollUpToCursor() {
|
||||
at := m.cursorLineNumber()
|
||||
if at < m.offset+m.height { // cursor is above
|
||||
m.LineUp(max(0, m.offset-at))
|
||||
} else {
|
||||
m.SetOffset(at)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue