forked from Archives/fx
Compare commits
336 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 | ||
|
0f33ceb592 | ||
|
85f7c5724a | ||
|
44d95c6c8d | ||
|
7501e5a81b | ||
|
d283c6ec52 | ||
|
70385ff4f2 | ||
|
afada8f60a | ||
|
8812bf3393 | ||
|
9ebeb62c28 | ||
|
cebe263d3b | ||
|
7b2e45eeba | ||
|
22a394047f | ||
|
b99a05dd89 | ||
|
9dac9222e0 | ||
|
9907ec7e58 | ||
|
1742da02f7 | ||
|
8999dee7d6 | ||
|
ceb177163d | ||
|
fe57a92cfe | ||
|
9a4d897c8d | ||
|
e0feb840fa | ||
|
62eccdeb80 | ||
|
be55b22356 | ||
|
3e05caef82 | ||
|
eb6461f3a8 | ||
|
35d5d741f4 | ||
|
bb79c9812d | ||
|
3c891babf7 | ||
|
ef77520f99 | ||
|
225239bf3d | ||
|
a696018fa5 | ||
|
beedbfd36a | ||
|
935857230d | ||
|
6a7a628e03 | ||
|
9c1a7e8ae2 | ||
|
4986be32b5 | ||
|
83bed4a4ee | ||
|
73f051f532 | ||
|
cb5a16a90c | ||
|
9660cb1932 | ||
|
f569036983 | ||
|
780ab93897 | ||
|
c9f582d3ee | ||
|
048ce5e040 | ||
|
e7bb9f3c7d | ||
|
758ae8d1ea | ||
|
f22f8d052c | ||
|
a33c92a20a | ||
|
2598958557 | ||
|
4bfd5ccc42 | ||
|
669c36866b | ||
|
4d6db10684 | ||
|
f892e8869d | ||
|
b63088e94c | ||
|
6036e981ef | ||
|
d8d8909b9e | ||
|
9089e078f0 | ||
|
e062d567c6 | ||
|
40f88dcc91 | ||
|
b12ae5a243 | ||
|
c8cd6ceef5 | ||
|
a29d43017e | ||
|
b5762bef22 | ||
|
b93cda14d2 | ||
|
f7e180deb2 | ||
|
dc5fe9bf8f | ||
|
838210b044 | ||
|
30b683b5cd | ||
|
ce4c407e00 | ||
|
5e04d740aa | ||
|
5db1c910f8 | ||
|
82de76e1d4 | ||
|
74cf26fd0d | ||
|
a0c2931c57 | ||
|
f110142160 | ||
|
851427d8c8 | ||
|
a09b27c8e4 | ||
|
89faa8bdb4 | ||
|
ac4d3696a9 | ||
|
6a4a813562 | ||
|
5146fffcdd | ||
|
9e34edeb75 | ||
|
82d2c29924 | ||
|
351e96d292 | ||
|
ce0099b87d | ||
|
c4a450c128 | ||
|
f84962e218 | ||
|
92ac3463a8 | ||
|
b3741760d8 | ||
|
d86b06c609 | ||
|
9291ce4397 | ||
|
628450c1cb | ||
|
ef514017b5 | ||
|
620ca58208 | ||
|
d77114d23c | ||
|
3dd064ae96 | ||
|
df2de034bc | ||
|
eca69024ad | ||
|
9153dec145 | ||
|
961ee629f4 | ||
|
840766996e | ||
|
f777badd9a | ||
|
e4bb32d4b8 | ||
|
ba0cec06d2 | ||
|
e67564af9c | ||
|
35280be40f | ||
|
df12c3a9bc | ||
|
affe7d2b61 | ||
|
7988cbab8e | ||
|
4840cb9851 | ||
|
9bd442da48 | ||
|
17b30f2622 | ||
|
abd80a935d | ||
|
51bb5d9725 | ||
|
8a62b1234f | ||
|
43aaf9b2dc | ||
|
075ef2926b | ||
|
cf81a94ae2 | ||
|
0f0bcb4805 | ||
|
b5462c73d7 | ||
|
f38b4b5257 | ||
|
9fdcaaf564 | ||
|
a596db031f | ||
|
b5f9f43f43 | ||
|
b3133d9658 | ||
|
d980f37f40 | ||
|
df1cbbc2f1 | ||
|
cd2f03ad5e | ||
|
57d536d4a7 | ||
|
4b8a4bfe3c | ||
|
3a728d66e4 |
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 ./...
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
/node_modules/
|
||||
package-lock.json
|
@ -1,3 +0,0 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "9"
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Anton Medvedev
|
||||
Copyright (c) 2019 Anton Medvedev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
153
README.md
153
README.md
@ -1,126 +1,93 @@
|
||||
<img src="https://user-images.githubusercontent.com/141232/35405308-4b41f446-0238-11e8-86c1-21f407cc8460.png" height="100" alt="fx">
|
||||
<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>
|
||||
|
||||
# [![Build Status](https://travis-ci.org/antonmedv/fx.svg?branch=master)](https://travis-ci.org/antonmedv/fx)
|
||||
|
||||
Command-line JSON processing tool
|
||||
_* Function eXecution_
|
||||
|
||||
## Features
|
||||
|
||||
* Don't need to learn new syntax
|
||||
* Plain JavaScript
|
||||
* Formatting and highlighting
|
||||
- Mouse support
|
||||
- Streaming support
|
||||
- Preserves key order
|
||||
- Preserves big numbers
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
brew install fx
|
||||
```
|
||||
$ npm install -g fx
|
||||
```bash
|
||||
snap install fx
|
||||
```
|
||||
```bash
|
||||
scoop install fx
|
||||
```
|
||||
```bash
|
||||
pacman -S fx
|
||||
```
|
||||
```bash
|
||||
pkg install fx
|
||||
```
|
||||
```bash
|
||||
go install github.com/antonmedv/fx@latest
|
||||
```
|
||||
|
||||
Or download [pre-built binary](https://github.com/antonmedv/fx/releases).
|
||||
|
||||
## Usage
|
||||
|
||||
Pipe into `fx` any JSON and anonymous function for reducing it.
|
||||
Start the interactive viewer via:
|
||||
|
||||
```
|
||||
$ fx [code ...]
|
||||
```bash
|
||||
fx data.json
|
||||
```
|
||||
|
||||
Pretty print JSON without passing any arguments:
|
||||
```
|
||||
$ echo '{"key":"value"}' | fx
|
||||
{
|
||||
"key": "value"
|
||||
}
|
||||
Or
|
||||
|
||||
```bash
|
||||
curl ... | fx
|
||||
```
|
||||
|
||||
### Anonymous function
|
||||
Type `?` to see full list of key shortcuts.
|
||||
|
||||
Use an anonymous function as reducer which gets JSON and processes it:
|
||||
```
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo[0].bar'
|
||||
"value"
|
||||
Pretty print:
|
||||
|
||||
```bash
|
||||
curl ... | fx .
|
||||
```
|
||||
|
||||
### This Binding
|
||||
### Reducers
|
||||
|
||||
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:
|
||||
```
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx 'this.foo[0].bar'
|
||||
"value"
|
||||
Write reducers in your favorite language: [JavaScript](doc/js.md) (default),
|
||||
[Python](doc/python.md), or [Ruby](doc/ruby.md).
|
||||
|
||||
```bash
|
||||
fx data.json '.filter(x => x.startsWith("a"))'
|
||||
```
|
||||
|
||||
### Chain
|
||||
|
||||
You can pass any number of anonymous functions for reducing JSON:
|
||||
```
|
||||
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo' 'this[0]' 'this.bar'
|
||||
"value"
|
||||
```bash
|
||||
fx data.json '[x["age"] + i for i in range(10)]'
|
||||
```
|
||||
|
||||
### Generator
|
||||
|
||||
If passed code contains `yield` keyword, [generator expression](https://github.com/sebmarkbage/ecmascript-generator-expression)
|
||||
will be used:
|
||||
```
|
||||
$ curl ... | fx 'for (let user of this) if (user.login.startsWith("a")) yield user'
|
||||
```bash
|
||||
fx data.json 'x.to_a.map {|x| x[1]}'
|
||||
```
|
||||
|
||||
Access to JSON through `this` keyword:
|
||||
```
|
||||
$ echo '["a", "b"]' | fx 'yield* this'
|
||||
[
|
||||
"a",
|
||||
"b"
|
||||
]
|
||||
## Documentation
|
||||
|
||||
See full [documentation](doc/doc.md).
|
||||
|
||||
## Themes
|
||||
|
||||
Theme can be configured by setting environment variable `FX_THEME` from `1`
|
||||
to `9`:
|
||||
|
||||
```bash
|
||||
export FX_THEME=9
|
||||
```
|
||||
|
||||
```
|
||||
$ echo '["a", "b"]' | fx 'yield* this; yield "c";'
|
||||
[
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
```
|
||||
<img width="1214" alt="themes" src="doc/images/themes.png">
|
||||
|
||||
### Update
|
||||
|
||||
You can update existing JSON using spread operator:
|
||||
|
||||
```
|
||||
$ echo '{"count": 0}' | fx '{...this, count: 1}'
|
||||
{
|
||||
"count": 1
|
||||
}
|
||||
```
|
||||
|
||||
### Use npm package
|
||||
|
||||
Use any npm package by installing it globally:
|
||||
```
|
||||
$ npm install -g lodash
|
||||
$ cat package.json | fx 'require("lodash").keys(this.dependencies)'
|
||||
```
|
||||
|
||||
### Other examples
|
||||
|
||||
Convert object to array:
|
||||
```
|
||||
$ cat package.json | fx 'Object.keys(this.dependencies)'
|
||||
[
|
||||
"cardinal",
|
||||
"get-stdin",
|
||||
"meow"
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
## Related
|
||||
|
||||
* [jq](https://github.com/stedolan/jq) – cli JSON processor on C
|
||||
* [jsawk](https://github.com/micha/jsawk) – like awk, but for JSON
|
||||
* [json](https://github.com/trentm/json) – another JSON manipulating cli library
|
||||
Add your own themes in [theme.go](pkg/theme/theme.go) file.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
[MIT](LICENSE)
|
||||
|
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
|
||||
```
|
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")
|
||||
}
|
68
index.js
68
index.js
@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
const meow = require('meow')
|
||||
const stdin = require('get-stdin')
|
||||
const cardinal = require('cardinal')
|
||||
const theme = require('cardinal/themes/tomorrow-night')
|
||||
|
||||
const cli = meow(`
|
||||
Usage
|
||||
$ fx [code ...]
|
||||
|
||||
Examples
|
||||
$ echo '{"key": "value"}' | fx 'x => x.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}
|
||||
`)
|
||||
|
||||
|
||||
|
||||
const highlight = process.stdout.isTTY ? cardinal.highlight : x => x
|
||||
|
||||
async function main() {
|
||||
const text = await stdin()
|
||||
|
||||
if (text === '') {
|
||||
cli.showHelp()
|
||||
}
|
||||
|
||||
const json = JSON.parse(text)
|
||||
const result = cli.input.reduce(reduce, json)
|
||||
|
||||
if (typeof result === 'undefined') {
|
||||
console.log(undefined)
|
||||
} else {
|
||||
const text = JSON.stringify(result, null, 4)
|
||||
console.log(highlight(text, {theme}))
|
||||
}
|
||||
}
|
||||
|
||||
function reduce(json, code) {
|
||||
if (/^\w+\s*=>/.test(code)) {
|
||||
const fx = eval(code)
|
||||
return fx(json)
|
||||
} else if (/yield/.test(code)) {
|
||||
const fx = eval(`
|
||||
function fn() {
|
||||
const gen = (function*(){
|
||||
${code.replace(/\\\n/g, '')}
|
||||
}).call(this)
|
||||
return [...gen]
|
||||
}; fn
|
||||
`)
|
||||
return fx.call(json)
|
||||
} else {
|
||||
const fx = eval(`function fn() { return ${code} }; fn`)
|
||||
return fx.call(json)
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
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
|
||||
}
|
||||
}
|
32
package.json
32
package.json
@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "fx",
|
||||
"version": "1.0.1",
|
||||
"description": "Command-line JSON processing tool",
|
||||
"repository": "antonmedv/fx",
|
||||
"author": "Anton Medvedev <anton@medv.io>",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"fx": "index.js"
|
||||
},
|
||||
"files": [
|
||||
"index.js"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "ava"
|
||||
},
|
||||
"keywords": [
|
||||
"json",
|
||||
"cli"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=9"
|
||||
},
|
||||
"dependencies": {
|
||||
"cardinal": "^1.0.0",
|
||||
"get-stdin": "^5.0.1",
|
||||
"meow": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^0.24.0"
|
||||
}
|
||||
}
|
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
|
||||
}
|
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)
|
||||
}
|
20
snap/snapcraft.yaml
Normal file
20
snap/snapcraft.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
name: fx
|
||||
version: 24.0.0
|
||||
summary: Terminal JSON viewer
|
||||
description: Terminal JSON viewer
|
||||
base: core18
|
||||
grade: stable
|
||||
confinement: strict
|
||||
|
||||
apps:
|
||||
fx:
|
||||
command: fx
|
||||
plugs: [home, network]
|
||||
|
||||
parts:
|
||||
fx:
|
||||
plugin: go
|
||||
go-channel: 1.18/stable
|
||||
source: .
|
||||
source-type: git
|
||||
go-importpath: github.com/antonmedv/fx
|
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
|
||||
}
|
||||
}
|
||||
}
|
28
test.js
28
test.js
@ -1,28 +0,0 @@
|
||||
'use strict'
|
||||
const test = require('ava')
|
||||
const {execSync} = require('child_process')
|
||||
|
||||
function fx(json, code = '') {
|
||||
const output = execSync(`echo '${JSON.stringify(json)}' | node index.js ${code}`).toString('utf8')
|
||||
return JSON.parse(output)
|
||||
}
|
||||
|
||||
test('pass', t => {
|
||||
t.deepEqual(fx([{"greeting": "hello world"}]), [{"greeting": "hello world"}])
|
||||
})
|
||||
|
||||
test('anon func', t => {
|
||||
t.deepEqual(fx({"key": "value"}, "'x => x.key'"), 'value')
|
||||
})
|
||||
|
||||
test('this bind', t => {
|
||||
t.deepEqual(fx([1, 2, 3, 4, 5], "'this.map(x => x * this.length)'"), [5, 10, 15, 20, 25])
|
||||
})
|
||||
|
||||
test('generator', t => {
|
||||
t.deepEqual(fx([1, 2, 3, 4, 5], "'for (let i of this) if (i % 2 == 0) yield i'"), [2, 4])
|
||||
})
|
||||
|
||||
test('chain', t => {
|
||||
t.deepEqual(fx({"items": ["foo", "bar"]}, "'this.items' 'yield* this' 'x => x[1]'"), 'bar')
|
||||
})
|
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