forked from Archives/fx
Compare commits
205 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
95009b06b6 | ||
|
751e5865da | ||
|
bd02783f75 | ||
|
c3f58aa915 | ||
|
126340bcf0 | ||
|
06ff419622 | ||
|
92cf975206 | ||
|
158687e890 | ||
|
14c79c1d12 | ||
|
918b4e5e3b | ||
|
2428d76e80 | ||
|
06f612bfb9 | ||
|
76e67edc41 | ||
|
534e0db987 | ||
|
4d0dbf4b94 | ||
|
1198ae9984 | ||
|
d622f6b85d | ||
|
d7b5ab7200 | ||
|
a58152469f | ||
|
9cf72f0407 | ||
|
0b9019a2c5 | ||
|
7c72e04453 | ||
|
477f89c783 | ||
|
f474859d9b | ||
|
9421363ebd | ||
|
92c4b93f15 | ||
|
95c55f9827 | ||
|
d443625dd6 | ||
|
0d5fa2e41a | ||
|
79ddfe0ea3 | ||
|
06f75b062e | ||
|
64349cb97c | ||
|
5e8d67a5fb | ||
|
2e30f1ec9a | ||
|
3b9ac1d023 | ||
|
7028357eba | ||
|
0cccf2e063 | ||
|
204c2988c8 | ||
|
1e4ee58691 | ||
|
61e8cf97cf | ||
|
04693455c4 | ||
|
f87f89440c | ||
|
33f25646c5 | ||
|
a406596b46 | ||
|
684d5d59d0 | ||
|
b0045d03a9 | ||
|
a01be39389 | ||
|
918ba6b566 | ||
|
710cf58310 | ||
|
8386ba7d91 | ||
|
f6c561c411 | ||
|
09aaaa9e3a | ||
|
e3ca218491 | ||
|
3b0826c3c9 | ||
|
506961e9d8 | ||
|
6e1dd65b3e | ||
|
ed5ed858d4 | ||
|
6ef3aa5573 | ||
|
915c1e9fa7 | ||
|
77b2632aeb | ||
|
36af65042c | ||
|
e05e917a6f | ||
|
bdb1d00831 | ||
|
3463842b8b | ||
|
fc257539df | ||
|
0d556edd9e | ||
|
d7f54a5c80 | ||
|
804becbdb5 | ||
|
f4daf98966 | ||
|
bae280436b | ||
|
f2a2a01589 | ||
|
344f78d20a | ||
|
5c448bf98f | ||
|
89c9bc7f10 | ||
|
8972572d79 | ||
|
dac5e9fc03 | ||
|
1b1e76df01 | ||
|
61eb6e5ec3 | ||
|
1b09c93447 | ||
|
73bf2d97cd | ||
|
e3aea24907 | ||
|
c642fbd28e | ||
|
6ed6ba2ec4 | ||
|
bf166fafda | ||
|
aa0964a12e | ||
|
a68c947949 | ||
|
7d7bfd28fe | ||
|
8bbc8ed564 | ||
|
2e7f78f7c6 | ||
|
ae80a278b2 | ||
|
49756d7ff4 | ||
|
68212c0c7a | ||
|
841eb4f1b5 | ||
|
d682c0b49f | ||
|
afd7b96af5 | ||
|
fa226f2ae8 | ||
|
e6042cb698 | ||
|
65a1608462 | ||
|
8a9e1f39d5 | ||
|
e14e56fa06 | ||
|
2315f509b8 | ||
|
3e0cc4f61b | ||
|
604512a410 | ||
|
2dc104887b | ||
|
6db594864e | ||
|
0115d90a36 | ||
|
043208a748 | ||
|
d289037340 | ||
|
afa4088e99 | ||
|
e430d5019b | ||
|
6f100280a6 | ||
|
f47c8e8c67 | ||
|
19a9d34dad | ||
|
d455165d25 | ||
|
5521a99e69 | ||
|
f1eaaa9c5e | ||
|
333dd38353 | ||
|
c6bc621c57 | ||
|
ac5678eb92 | ||
|
0307f129f5 | ||
|
dfc4815dbf | ||
|
7ed688602e | ||
|
f7a37e5b98 | ||
|
e7619aeece | ||
|
e5aef9ff32 | ||
|
973ceb92da | ||
|
1744e178fa | ||
|
57f4069b4b | ||
|
7fffb5db9d | ||
|
001252c1ca | ||
|
de3d60b01d | ||
|
290c2a0185 | ||
|
ed6130907e | ||
|
6da433446c | ||
|
c456134a71 | ||
|
7f53e96a93 | ||
|
c7057cce2d | ||
|
ca6e4def1a | ||
|
7426016d6b | ||
|
98d886da20 | ||
|
81c71e1d2d | ||
|
d12daf3c83 | ||
|
58324051ae | ||
|
f60d7aa89c | ||
|
9713a057c1 | ||
|
6b4e7254c5 | ||
|
a39e80b5b0 | ||
|
314843362e | ||
|
a0fe5aae43 | ||
|
a16fa4560a | ||
|
e8fd8353fe | ||
|
33ea17be93 | ||
|
027b1a8dad | ||
|
51837b4831 | ||
|
2cf71999ae | ||
|
7d23c8d811 | ||
|
ae0b739796 | ||
|
332cb40a90 | ||
|
acf0255e1d | ||
|
0889b9683d | ||
|
575666fd1e | ||
|
83efa1d109 | ||
|
fd2a30b79e | ||
|
d216e02cb3 | ||
|
35526cc4d3 | ||
|
5c228ccd77 | ||
|
42c81fd484 | ||
|
0382a0b2d7 | ||
|
772bb83a19 | ||
|
e4e41c7391 | ||
|
776d4d34a2 | ||
|
73298e88a4 | ||
|
b1f393013c | ||
|
2618fdaa56 | ||
|
1ecc258ca6 | ||
|
33f2e861ff | ||
|
ce4b19cce5 | ||
|
407c034f3b | ||
|
6d2a1d9947 | ||
|
592e799e10 | ||
|
4fd6fdf39f | ||
|
7099560512 | ||
|
bfb8aa4534 | ||
|
47b5bb2366 | ||
|
d6cfe05fed | ||
|
9f48ff7011 | ||
|
4fd5e21432 | ||
|
7c01d735a7 | ||
|
0e60886e0d | ||
|
cdd3d5abbe | ||
|
d571194fc8 | ||
|
6236a5f942 | ||
|
c116bb749b | ||
|
c8a6c4af0b | ||
|
a7b89365bf | ||
|
12ff839365 | ||
|
ce0b1a5cc6 | ||
|
8bd892c11e | ||
|
4f3a52fa32 | ||
|
3da7e22893 | ||
|
f321b8a58f | ||
|
ff452e2b5b | ||
|
2d3becc9e4 | ||
|
07f8e6e18d | ||
|
95b40b31b8 |
23
.github/workflows/release.mjs
vendored
Normal file
23
.github/workflows/release.mjs
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
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)}`)))
|
116
.github/workflows/release.yml
vendored
Normal file
116
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
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
|
21
.github/workflows/test.yml
vendored
Normal file
21
.github/workflows/test.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
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 ./...
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
/node_modules/
|
||||
/dist/
|
||||
package-lock.json
|
@ -1,6 +0,0 @@
|
||||
tasks:
|
||||
- init: >
|
||||
npm install &&
|
||||
npm run build
|
||||
command: >
|
||||
./dist/fx-linux package.json
|
@ -1,5 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "node"
|
||||
- "10"
|
||||
- "8"
|
123
README.md
123
README.md
@ -1,120 +1,93 @@
|
||||
<p align="center"><img src="https://medv.io/assets/fx-logo.png" height="100" alt="fx logo"></p>
|
||||
<p align="center"><img src="https://medv.io/assets/fx.gif" width="562" alt="fx example"></p>
|
||||
<p align="center"><a href="https://fx.wtf"><img src="https://medv.io/assets/fx/fx-preview.gif" width="500" alt="fx preview"></a></p>
|
||||
|
||||
_* Function eXecution_
|
||||
|
||||
[![Build Status](https://travis-ci.org/antonmedv/fx.svg?branch=master)](https://travis-ci.org/antonmedv/fx)
|
||||
[![Npm Version](https://img.shields.io/npm/v/fx.svg)](https://www.npmjs.com/package/fx)
|
||||
[![Brew Version](https://img.shields.io/homebrew/v/fx.svg)](https://formulae.brew.sh/formula/fx)
|
||||
[![Snap Version](https://img.shields.io/badge/snap-11.0.1-blue.svg)](https://snapcraft.io/fx)
|
||||
|
||||
Command-line JSON processing tool
|
||||
|
||||
## Features
|
||||
|
||||
* Formatting and highlighting
|
||||
* Standalone binary
|
||||
* Interactive mode 🎉
|
||||
* Themes support 🎨
|
||||
- Mouse support
|
||||
- Streaming support
|
||||
- Preserves key order
|
||||
- Preserves big numbers
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
brew install fx
|
||||
```
|
||||
$ npm install -g fx
|
||||
```bash
|
||||
snap install fx
|
||||
```
|
||||
Or via Homebrew
|
||||
```bash
|
||||
scoop install fx
|
||||
```
|
||||
$ brew install fx
|
||||
```bash
|
||||
pacman -S fx
|
||||
```
|
||||
```bash
|
||||
pkg install fx
|
||||
```
|
||||
```bash
|
||||
go install github.com/antonmedv/fx@latest
|
||||
```
|
||||
|
||||
Or download standalone binary from [releases](https://github.com/antonmedv/fx/releases) page.
|
||||
|
||||
<p>
|
||||
<a href="https://www.patreon.com/antonmedv"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160"></a>
|
||||
<a href="https://www.wispay.io/t/ZQb" target="_blank"><img src="https://assets.wispay.io/wgt2_d_b.png" height="60"></a>
|
||||
</p>
|
||||
Or download [pre-built binary](https://github.com/antonmedv/fx/releases).
|
||||
|
||||
## Usage
|
||||
|
||||
Start [interactive mode](https://github.com/antonmedv/fx/blob/master/docs.md#interactive-mode) without passing any arguments.
|
||||
```
|
||||
$ curl ... | fx
|
||||
```
|
||||
Start the interactive viewer via:
|
||||
|
||||
Or by passing filename as first argument.
|
||||
```
|
||||
$ fx data.json
|
||||
```
|
||||
|
||||
Pipe into `fx` any JSON and anonymous function for reducing it.
|
||||
```bash
|
||||
$ curl ... | fx 'json => json.message'
|
||||
fx data.json
|
||||
```
|
||||
|
||||
Or same as above but short.
|
||||
Or
|
||||
|
||||
```bash
|
||||
$ curl ... | fx this.message
|
||||
$ curl ... | fx .message
|
||||
curl ... | fx
|
||||
```
|
||||
|
||||
Pass any numbers of arguments as code.
|
||||
Type `?` to see full list of key shortcuts.
|
||||
|
||||
Pretty print:
|
||||
|
||||
```bash
|
||||
$ curl ... | fx 'json => json.message' 'json => json.filter(x => x.startsWith("a"))'
|
||||
curl ... | fx .
|
||||
```
|
||||
|
||||
Access all lodash (or ramda, etc) methods by using [.fxrc](https://github.com/antonmedv/fx/blob/master/docs.md#using-fxrc) file.
|
||||
### Reducers
|
||||
|
||||
Write reducers in your favorite language: [JavaScript](doc/js.md) (default),
|
||||
[Python](doc/python.md), or [Ruby](doc/ruby.md).
|
||||
|
||||
```bash
|
||||
$ curl ... | fx '_.groupBy("commit.committer.name")' '_.mapValues(_.size)'
|
||||
fx data.json '.filter(x => x.startsWith("a"))'
|
||||
```
|
||||
|
||||
Update JSON using spread operator.
|
||||
```bash
|
||||
$ echo '{"count": 0}' | fx '{...this, count: 1}'
|
||||
{
|
||||
"count": 1
|
||||
}
|
||||
fx data.json '[x["age"] + i for i in range(10)]'
|
||||
```
|
||||
|
||||
Pretty print JSON with dot.
|
||||
```bash
|
||||
$ curl ... | fx .
|
||||
fx data.json 'x.to_a.map {|x| x[1]}'
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
See full [documentation](https://github.com/antonmedv/fx/blob/master/docs.md).
|
||||
See full [documentation](doc/doc.md).
|
||||
|
||||
## Links
|
||||
## Themes
|
||||
|
||||
* [Discover how to use fx effectively](https://medium.com/@antonmedv/discover-how-to-use-fx-effectively-668845d2a4ea)
|
||||
|
||||
## Related
|
||||
|
||||
* [xx](https://github.com/antonmedv/xx) - fx-like JSON tool (*go*)
|
||||
* [ymlx](https://github.com/matthewadams/ymlx) - fx-like YAML cli processor
|
||||
* [fx-theme-monokai](https://github.com/antonmedv/fx-theme-monokai) – monokai theme
|
||||
* [fx-theme-night](https://github.com/antonmedv/fx-theme-night) – night theme
|
||||
|
||||
## Contributing
|
||||
|
||||
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/antonmedv/fx)
|
||||
|
||||
Or clone locally and run:
|
||||
Theme can be configured by setting environment variable `FX_THEME` from `1`
|
||||
to `9`:
|
||||
|
||||
```bash
|
||||
# install dependencies
|
||||
npm install
|
||||
|
||||
# run fx
|
||||
node index.js package.json
|
||||
|
||||
# run the build
|
||||
npm run build
|
||||
|
||||
# try the built binary
|
||||
./dist/fx-linux package.json
|
||||
export FX_THEME=9
|
||||
```
|
||||
|
||||
<img width="1214" alt="themes" src="doc/images/themes.png">
|
||||
|
||||
Add your own themes in [theme.go](pkg/theme/theme.go) file.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/antonmedv/fx/blob/master/LICENSE)
|
||||
[MIT](LICENSE)
|
||||
|
26
config.js
26
config.js
@ -1,26 +0,0 @@
|
||||
'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,
|
||||
}
|
66
doc/doc.md
Normal file
66
doc/doc.md
Normal file
@ -0,0 +1,66 @@
|
||||
# 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. |
|
BIN
doc/images/preview.png
Normal file
BIN
doc/images/preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 411 KiB |
BIN
doc/images/themes.png
Normal file
BIN
doc/images/themes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 659 KiB |
120
doc/js.md
Normal file
120
doc/js.md
Normal file
@ -0,0 +1,120 @@
|
||||
# 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')
|
||||
> ```
|
27
doc/python.md
Normal file
27
doc/python.md
Normal file
@ -0,0 +1,27 @@
|
||||
# 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
|
||||
```
|
23
doc/ruby.md
Normal file
23
doc/ruby.md
Normal file
@ -0,0 +1,23 @@
|
||||
# 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
|
||||
```
|
223
docs.md
223
docs.md
@ -1,223 +0,0 @@
|
||||
# Documentation
|
||||
|
||||
`fx` can work in two modes: cli and interactive. To start interactive mode pipe into `fx` any JSON:
|
||||
|
||||
```bash
|
||||
$ curl ... | fx
|
||||
```
|
||||
|
||||
Or you can pass file argument as first parameter:
|
||||
|
||||
```bash
|
||||
$ fx my.json
|
||||
```
|
||||
|
||||
If any argument was passed, `fx` will apply it and prints to stdout.
|
||||
|
||||
## 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 single dot is passed, JSON will be processed without modification:
|
||||
```bash
|
||||
$ echo '{"foo": "bar"}' | fx .
|
||||
{
|
||||
"foo": "bar"
|
||||
}
|
||||
```
|
||||
|
||||
## Chain
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## Generator
|
||||
|
||||
If passed code contains `yield` keyword, [generator expression](https://github.com/sebmarkbage/ecmascript-generator-expression)
|
||||
will be used:
|
||||
```bash
|
||||
$ curl ... | fx 'for (let user of this) if (user.login.startsWith("a")) yield user'
|
||||
```
|
||||
|
||||
Access to JSON through `this` keyword:
|
||||
```bash
|
||||
$ echo '["a", "b"]' | fx 'yield* this'
|
||||
[
|
||||
"a",
|
||||
"b"
|
||||
]
|
||||
```
|
||||
|
||||
```bash
|
||||
$ echo '["a", "b"]' | fx 'yield* this; yield "c";'
|
||||
[
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
```
|
||||
|
||||
## Update
|
||||
|
||||
You can update existing JSON using spread operator:
|
||||
|
||||
```bash
|
||||
$ echo '{"count": 0}' | fx '{...this, count: 1}'
|
||||
{
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
## 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=/usr/local/lib/node_modules
|
||||
> ```
|
||||
|
||||
## Edit in place
|
||||
|
||||
Add next code to your _.fxrc_ file:
|
||||
|
||||
```js
|
||||
const fs = require('fs')
|
||||
|
||||
global.save = json => {
|
||||
fs.writeFileSync(process.argv[2], JSON.stringify(json, null, 2))
|
||||
return json
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
fx data.json '{...this, count: this.count+1}' save .count
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Formatting
|
||||
|
||||
If you need something different then JSON (for example arguments for xargs) do not return anything from 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)'
|
||||
[
|
||||
"@medv/prettyjson"
|
||||
]
|
||||
```
|
||||
|
||||
By the way, fx has shortcut for `Object.keys(this)`. Previous example can be rewritten as:
|
||||
|
||||
```bash
|
||||
$ cat package.json | fx this.dependencies ?
|
||||
```
|
||||
|
||||
## 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 |
|
||||
| `e`/`E` | Expand/Collapse all |
|
||||
| `g`/`G` | Goto top/bottom |
|
||||
| `up`/`down` or `k/j` | Move cursor up/down |
|
||||
| `left`/`right` or `h/l` | Expand/Collapse |
|
||||
| `.` | Edit filter |
|
||||
| `/` | Search |
|
||||
| `n` | Goto next found pattern |
|
||||
|
||||
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 |
|
||||
|
||||
### Search
|
||||
|
||||
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 |
|
35
find.js
35
find.js
@ -1,35 +0,0 @@
|
||||
'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
|
617
fx.js
617
fx.js
@ -1,617 +0,0 @@
|
||||
'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) {
|
||||
// Current rendered object on a screen.
|
||||
let 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 = new Set()
|
||||
expanded.add('')
|
||||
|
||||
// Current search regexp and generator.
|
||||
let highlight = null
|
||||
let findGen = null
|
||||
let currentPath = null
|
||||
|
||||
// Reopen tty
|
||||
let ttyReadStream
|
||||
let ttyWriteStream
|
||||
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()
|
||||
|
||||
screen.key(['escape', 'q', 'C-c'], function () {
|
||||
program.disableMouse() // If exit program immediately, stdin may still receive
|
||||
setTimeout(() => process.exit(0), 10) // mouse events which will be printed in stdout.
|
||||
})
|
||||
|
||||
screen.on('resize', function () {
|
||||
render()
|
||||
})
|
||||
|
||||
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) {
|
||||
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()
|
||||
let rest = [...index.keys()]
|
||||
|
||||
const [n] = getLine(program.y)
|
||||
if (typeof n !== 'undefined') {
|
||||
rest = rest.filter(i => i < n)
|
||||
}
|
||||
|
||||
if (rest.length > 0) {
|
||||
const next = Math.max(...rest)
|
||||
|
||||
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()
|
||||
let rest = [...index.keys()]
|
||||
|
||||
const [n] = getLine(program.y)
|
||||
if (typeof n !== 'undefined') {
|
||||
rest = rest.filter(i => i > n)
|
||||
}
|
||||
|
||||
if (rest.length > 0) {
|
||||
const next = Math.min(...rest)
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
|
||||
box.key(['left', 'h'], 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.delete(path)
|
||||
render()
|
||||
}
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
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]))
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if (highlight) {
|
||||
findGen = find(json, highlight)
|
||||
findNext()
|
||||
} else {
|
||||
findGen = null
|
||||
currentPath = null
|
||||
}
|
||||
|
||||
search.hide()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
go.mod
Normal file
33
go.mod
Normal file
@ -0,0 +1,33 @@
|
||||
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
|
||||
)
|
96
go.sum
Normal file
96
go.sum
Normal file
@ -0,0 +1,96 @@
|
||||
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=
|
71
help.go
Normal file
71
help.go
Normal file
@ -0,0 +1,71 @@
|
||||
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")
|
||||
}
|
98
index.js
98
index.js
@ -1,98 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict'
|
||||
const os = require('os')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const {stdin, stdout, stderr} = process
|
||||
try {
|
||||
require(path.join(os.homedir(), '.fxrc'))
|
||||
} catch (err) {
|
||||
if (err.code !== 'MODULE_NOT_FOUND') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
const print = require('./print')
|
||||
const reduce = require('./reduce')
|
||||
|
||||
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"]
|
||||
|
||||
`
|
||||
|
||||
function main(input) {
|
||||
let args = process.argv.slice(2)
|
||||
let filename = 'fx'
|
||||
|
||||
if (input === '') {
|
||||
if (args.length === 0 || (args.length === 1 && (args[0] === '-h' || args[0] === '--help'))) {
|
||||
stderr.write(usage)
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
input = fs.readFileSync(args[0])
|
||||
filename = path.basename(args[0])
|
||||
args = args.slice(1)
|
||||
}
|
||||
|
||||
const json = JSON.parse(input)
|
||||
|
||||
if (args.length === 0 && stdout.isTTY) {
|
||||
require('./fx')(filename, json)
|
||||
return
|
||||
}
|
||||
|
||||
const output = args.reduce(reduce, json)
|
||||
|
||||
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 run() {
|
||||
stdin.setEncoding('utf8')
|
||||
|
||||
if (stdin.isTTY) {
|
||||
main('')
|
||||
return
|
||||
}
|
||||
|
||||
let buff = ''
|
||||
stdin.on('readable', () => {
|
||||
let chunk
|
||||
|
||||
while ((chunk = stdin.read())) {
|
||||
buff += chunk
|
||||
}
|
||||
})
|
||||
|
||||
stdin.on('end', () => {
|
||||
main(buff)
|
||||
})
|
||||
}
|
||||
|
||||
run()
|
121
keymap.go
Normal file
121
keymap.go
Normal file
@ -0,0 +1,121 @@
|
||||
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"),
|
||||
),
|
||||
}
|
||||
}
|
634
main.go
Normal file
634
main.go
Normal file
@ -0,0 +1,634 @@
|
||||
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
|
||||
}
|
||||
}
|
50
package.json
50
package.json
@ -1,50 +0,0 @@
|
||||
{
|
||||
"name": "fx",
|
||||
"version": "11.0.1",
|
||||
"description": "Command-line JSON viewer",
|
||||
"repository": "antonmedv/fx",
|
||||
"author": "Anton Medvedev <anton@medv.io>",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"fx": "index.js"
|
||||
},
|
||||
"files": [
|
||||
"config.js",
|
||||
"find.js",
|
||||
"fx.js",
|
||||
"index.js",
|
||||
"print.js",
|
||||
"reduce.js"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "ava",
|
||||
"build": "pkg . --out-path dist -t node8-linux-x64,node8-macos-x64,node8-win-x64",
|
||||
"zip": "cd dist && find . -name 'fx-*' -exec zip '{}.zip' '{}' \\;",
|
||||
"release": "rm -rf ./dist && npm run build && npm run zip && release-it --github.release --github.assets=dist/*.zip"
|
||||
},
|
||||
"keywords": [
|
||||
"json",
|
||||
"viewer",
|
||||
"cli",
|
||||
"terminal",
|
||||
"term",
|
||||
"console",
|
||||
"ascii",
|
||||
"unicode",
|
||||
"blessed"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medv/blessed": "^2.0.0",
|
||||
"chalk": "^2.4.2",
|
||||
"indent-string": "^3.2.0",
|
||||
"string-width": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^1.2.0",
|
||||
"pkg": "^4.3.7",
|
||||
"release-it": "^10.0.7"
|
||||
}
|
||||
}
|
26
pkg/dict/dict.go
Normal file
26
pkg/dict/dict.go
Normal file
@ -0,0 +1,26 @@
|
||||
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
|
||||
}
|
65
pkg/dict/dict_test.go
Normal file
65
pkg/dict/dict_test.go
Normal file
@ -0,0 +1,65 @@
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
86
pkg/json/parse.go
Normal file
86
pkg/json/parse.go
Normal file
@ -0,0 +1,86 @@
|
||||
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)
|
||||
}
|
||||
}
|
51
pkg/json/parse_test.go
Normal file
51
pkg/json/parse_test.go
Normal file
@ -0,0 +1,51 @@
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
71
pkg/json/pretty_print.go
Normal file
71
pkg/json/pretty_print.go
Normal file
@ -0,0 +1,71 @@
|
||||
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"
|
||||
}
|
||||
}
|
51
pkg/json/stringify.go
Normal file
51
pkg/json/stringify.go
Normal file
@ -0,0 +1,51 @@
|
||||
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"
|
||||
}
|
||||
}
|
32
pkg/json/stringify_test.go
Normal file
32
pkg/json/stringify_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
47
pkg/json/traverse.go
Normal file
47
pkg/json/traverse.go
Normal file
@ -0,0 +1,47 @@
|
||||
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)
|
||||
}
|
6
pkg/json/types.go
Normal file
6
pkg/json/types.go
Normal file
@ -0,0 +1,6 @@
|
||||
package json
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Number = json.Number
|
||||
type Array = []interface{}
|
98
pkg/reducer/js.go
Normal file
98
pkg/reducer/js.go
Normal file
@ -0,0 +1,98 @@
|
||||
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
|
||||
}
|
12
pkg/reducer/js.js
Normal file
12
pkg/reducer/js.js
Normal file
@ -0,0 +1,12 @@
|
||||
// .fxrc.js %v
|
||||
|
||||
function reduce(input) {
|
||||
let x = JSON.parse(input)
|
||||
|
||||
// Reducers %v
|
||||
if (typeof x === 'undefined') {
|
||||
return 'null'
|
||||
} else {
|
||||
return JSON.stringify(x)
|
||||
}
|
||||
}
|
83
pkg/reducer/node.go
Normal file
83
pkg/reducer/node.go
Normal file
@ -0,0 +1,83 @@
|
||||
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)
|
||||
}
|
31
pkg/reducer/node.js
Normal file
31
pkg/reducer/node.js
Normal file
@ -0,0 +1,31 @@
|
||||
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
|
||||
})
|
38
pkg/reducer/python.go
Normal file
38
pkg/reducer/python.go
Normal file
@ -0,0 +1,38 @@
|
||||
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)
|
||||
}
|
9
pkg/reducer/python.py
Normal file
9
pkg/reducer/python.py
Normal file
@ -0,0 +1,9 @@
|
||||
import json, sys, os
|
||||
x = json.load(sys.stdin)
|
||||
|
||||
# Reducers %v
|
||||
|
||||
try:
|
||||
print(json.dumps(x))
|
||||
except:
|
||||
print(json.dumps(list(x)))
|
76
pkg/reducer/reduce.go
Normal file
76
pkg/reducer/reduce.go
Normal file
@ -0,0 +1,76 @@
|
||||
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
|
||||
}
|
37
pkg/reducer/ruby.go
Normal file
37
pkg/reducer/ruby.go
Normal file
@ -0,0 +1,37 @@
|
||||
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)
|
||||
}
|
6
pkg/reducer/ruby.rb
Normal file
6
pkg/reducer/ruby.rb
Normal file
@ -0,0 +1,6 @@
|
||||
require 'json'
|
||||
x = JSON.parse(STDIN.read)
|
||||
|
||||
# Reducers %v
|
||||
|
||||
puts JSON.generate(x)
|
215
pkg/reducer/simple.go
Normal file
215
pkg/reducer/simple.go
Normal file
@ -0,0 +1,215 @@
|
||||
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
|
||||
}
|
137
pkg/reducer/simple_test.go
Normal file
137
pkg/reducer/simple_test.go
Normal file
@ -0,0 +1,137 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
51
pkg/reducer/utils.go
Normal file
51
pkg/reducer/utils.go
Normal file
@ -0,0 +1,51 @@
|
||||
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:]))
|
||||
}
|
185
pkg/theme/theme.go
Normal file
185
pkg/theme/theme.go
Normal file
@ -0,0 +1,185 @@
|
||||
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
|
||||
}
|
||||
}
|
296
print.go
Normal file
296
print.go
Normal file
@ -0,0 +1,296 @@
|
||||
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
|
||||
}
|
113
print.js
113
print.js
@ -1,113 +0,0 @@
|
||||
'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(JSON.stringify(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(v, config.null)
|
||||
}
|
||||
|
||||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||||
return formatStyle(v, config.number)
|
||||
}
|
||||
|
||||
if (typeof v === 'boolean') {
|
||||
return formatStyle(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
|
36
reduce.js
36
reduce.js
@ -1,36 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
function reduce(json, code) {
|
||||
if (/^\./.test(code)) {
|
||||
const fx = eval(`function fn() {
|
||||
return ${code === '.' ? 'this' : 'this' + code}
|
||||
}; fn`)
|
||||
return fx.call(json)
|
||||
}
|
||||
|
||||
if ('?' === code) {
|
||||
return Object.keys(json)
|
||||
}
|
||||
|
||||
if (/yield\*?\s/.test(code)) {
|
||||
const fx = eval(`function fn() {
|
||||
const gen = (function*(){
|
||||
${code.replace(/\\\n/g, '')}
|
||||
}).call(this)
|
||||
return [...gen]
|
||||
}; fn`)
|
||||
return fx.call(json)
|
||||
}
|
||||
|
||||
const fx = eval(`function fn() {
|
||||
return ${code}
|
||||
}; fn`)
|
||||
|
||||
const fn = fx.call(json)
|
||||
if (typeof fn === 'function') {
|
||||
return fn(json)
|
||||
}
|
||||
return fn
|
||||
}
|
||||
|
||||
module.exports = reduce
|
249
search.go
Normal file
249
search.go
Normal file
@ -0,0 +1,249 @@
|
||||
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
|
||||
}
|
190
search_test.go
Normal file
190
search_test.go
Normal file
@ -0,0 +1,190 @@
|
||||
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,31 +1,20 @@
|
||||
name: fx
|
||||
base: core18 # the base snap is the execution environment for this snap
|
||||
version: git
|
||||
summary: Command-line tool and terminal JSON viewer
|
||||
description: |
|
||||
Command-line JSON processing tool
|
||||
Features
|
||||
* Formatting and highlighting
|
||||
* Interactive mode
|
||||
* Themes support
|
||||
|
||||
architectures:
|
||||
- build-on: i386
|
||||
- build-on: amd64
|
||||
- build-on: armhf
|
||||
- build-on: arm64
|
||||
|
||||
version: 24.0.0
|
||||
summary: Terminal JSON viewer
|
||||
description: Terminal JSON viewer
|
||||
base: core18
|
||||
grade: stable
|
||||
confinement: strict
|
||||
|
||||
parts:
|
||||
fx:
|
||||
plugin: nodejs
|
||||
node-engine: "8.14.1"
|
||||
source: .
|
||||
|
||||
apps:
|
||||
fx:
|
||||
command: fx
|
||||
plugs:
|
||||
- network
|
||||
plugs: [home, network]
|
||||
|
||||
parts:
|
||||
fx:
|
||||
plugin: go
|
||||
go-channel: 1.18/stable
|
||||
source: .
|
||||
source-type: git
|
||||
go-importpath: github.com/antonmedv/fx
|
||||
|
42
stream.go
Normal file
42
stream.go
Normal file
@ -0,0 +1,42 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
42
test.js
42
test.js
@ -1,42 +0,0 @@
|
||||
'use strict'
|
||||
const test = require('ava')
|
||||
const {execSync} = require('child_process')
|
||||
|
||||
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.deepEqual(r, 'value\n')
|
||||
})
|
||||
|
||||
test('arrow func', t => {
|
||||
const r = fx({"key": "value"}, "'x => x.key'")
|
||||
t.deepEqual(r, 'value\n')
|
||||
})
|
||||
|
||||
test('arrow func ()', t => {
|
||||
const r = fx({"key": "value"}, "'(x) => x.key'")
|
||||
t.deepEqual(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('generator', t => {
|
||||
const r = fx([1, 2, 3, 4, 5], "'for (let i of this) if (i % 2 == 0) yield i'")
|
||||
t.deepEqual(JSON.parse(r), [2, 4])
|
||||
})
|
||||
|
||||
test('chain', t => {
|
||||
const r = fx({"items": ["foo", "bar"]}, "'this.items' 'yield* this' 'x => x[1]'")
|
||||
t.deepEqual(r, 'bar\n')
|
||||
})
|
50
util.go
Normal file
50
util.go
Normal file
@ -0,0 +1,50 @@
|
||||
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()
|
||||
}
|
26
util_test.go
Normal file
26
util_test.go
Normal file
@ -0,0 +1,26 @@
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
3
version.go
Normal file
3
version.go
Normal file
@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
const version = "24.0.0"
|
128
viewport.go
Normal file
128
viewport.go
Normal file
@ -0,0 +1,128 @@
|
||||
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
Block a user