Compare commits

...

42 Commits

Author SHA1 Message Date
lilihx 95009b06b6
add flag: -h, --help (#218)
Change-Id: I6d5648b84536c22e518413d0360047431b1acccc
2 years ago
Anton Medvedev 751e5865da Simplify reducers and add fast path to js reducer 2 years ago
Anton Medvedev bd02783f75 Split reducers docs 2 years ago
github-actions c3f58aa915 Release 24.0.0 2 years ago
Anton Medvedev 126340bcf0 Update doc.md 2 years ago
Anton Medvedev 06ff419622 Update doc.md 2 years ago
Anton Medvedev 92cf975206 Update README.md 2 years ago
Anton Medvedev 158687e890 Update doc.md 2 years ago
Anton Medvedev 14c79c1d12 Update doc.md 2 years ago
Anton Medvedev 918b4e5e3b Update doc.md 2 years ago
Anton Medvedev 2428d76e80 Add doc about config 2 years ago
Anton Medvedev 06f612bfb9 Update README.md 2 years ago
Anton Medvedev 76e67edc41 Update doc.md 2 years ago
Anton Medvedev 534e0db987 Add doc 2 years ago
Anton Medvedev 4d0dbf4b94 Add FX_SHOW_SIZE config option 2 years ago
github-actions 1198ae9984 Release 23.2.0 2 years ago
Anton Medvedev d622f6b85d Update release.yml 2 years ago
Anton Medvedev d7b5ab7200 Refactor reducers 2 years ago
Anton Medvedev a58152469f Drop flags 2 years ago
Gabriel M. Dutra 9cf72f0407
Adding installation for FeeBSD (#204) 2 years ago
github-actions 0b9019a2c5 Release 23.1.0 2 years ago
Anton Medvedev 7c72e04453 Add TODO 2 years ago
Anton Medvedev 477f89c783 Add goja 2 years ago
Anton Medvedev f474859d9b Add TODO 2 years ago
Anton Medvedev 9421363ebd Ignore mouse clicks on status bar 2 years ago
Anton Medvedev 92c4b93f15 Better search cursor jumps 2 years ago
Anton Medvedev 95c55f9827 Add brew to release.yml 2 years ago
Anton Medvedev d443625dd6
Update README.md 2 years ago
Anton Medvedev 0d5fa2e41a
Update README.md 2 years ago
github-actions 79ddfe0ea3 Release 23.0.1 2 years ago
Anton Medvedev 06f75b062e Fix command name 2 years ago
github-actions 64349cb97c Release 23.0.0 2 years ago
Anton Medvedev 5e8d67a5fb Add default reducer 2 years ago
Anton Medvedev 2e30f1ec9a Add TODO 2 years ago
Anton Medvedev 3b9ac1d023 Add streaming support 2 years ago
Kacper Bąk 7028357eba
fix: assertion check in switch (#198) 2 years ago
Anton Medvedev 0cccf2e063 Update README.md 2 years ago
Anton Medvedev 204c2988c8 Update preview 2 years ago
Anton Medvedev 1e4ee58691 Add go mod download 2 years ago
github-actions 61e8cf97cf Release 22.0.10 2 years ago
Anton Medvedev 04693455c4 Fix release.yml 2 years ago
github-actions f87f89440c Release RELEASE_VERSION 2 years ago

@ -10,6 +10,8 @@ let goarch = [
let name = (GOOS, GOARCH) => `fx_${GOOS}_${GOARCH}` + (GOOS === 'windows' ? '.exe' : '')
await $`go mod download`
await Promise.all(
goos.flatMap(GOOS =>
goarch.map(GOARCH =>

@ -30,9 +30,9 @@ jobs:
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 -a "$RELEASE_VERSION" -m "Release $RELEASE_VERSION" --force
git push --follow-tags --force
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 ./...
@ -80,3 +80,37 @@ jobs:
store_login: ${{ secrets.STORE_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: stable
brew:
needs: [commit]
runs-on: macos-latest
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
with:
test-bot: false
- name: Cache Homebrew Bundler RubyGems
id: cache
uses: actions/cache@v2
with:
path: ${{ steps.set-up-homebrew.outputs.gems-path }}
key: ${{ runner.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}
restore-keys: ${{ runner.os }}-rubygems-
- name: Install Homebrew Bundler RubyGems
if: steps.cache.outputs.cache-hit != 'true'
run: brew install-bundler-gems
- name: Configure Git user
uses: Homebrew/actions/git-user-config@master
- name: Update brew
run: brew update
- name: Bump formulae
uses: Homebrew/actions/bump-formulae@master
with:
token: ${{ secrets.MY_TOKEN }}
formulae: fx

@ -1,10 +1,11 @@
<p align="center"><img src="https://medv.io/assets/fx.gif" width="562" alt="fx example"></p>
<p align="center"><a href="https://fx.wtf"><img src="https://medv.io/assets/fx/fx-preview.gif" width="500" alt="fx preview"></a></p>
_* Function eXecution_
## Features
- Interactive viewer
- Mouse support
- Streaming support
- Preserves key order
- Preserves big numbers
@ -23,6 +24,9 @@ scoop install fx
pacman -S fx
```
```bash
pkg install fx
```
```bash
go install github.com/antonmedv/fx@latest
```
@ -52,27 +56,24 @@ curl ... | fx .
### Reducers
Write reducers in your favorite language: [JavaScript](docs/reducers.md#node) (default),
[Python](docs/reducers.md#python), or [Ruby](docs/reducers.md#ruby).
Write reducers in your favorite language: [JavaScript](doc/js.md) (default),
[Python](doc/python.md), or [Ruby](doc/ruby.md).
```bash
export FX_LANG=node
fx data.json '.filter(x => x.startsWith("a"))'
```
```bash
export FX_LANG=python
fx data.json '[x["age"] + i for i in range(10)]'
```
```bash
export FX_LANG=ruby
fx data.json 'x.to_a.map {|x| x[1]}'
```
## Documentation
See full [documentation](https://github.com/antonmedv/fx/blob/master/DOCS.md).
See full [documentation](doc/doc.md).
## Themes
@ -83,8 +84,10 @@ to `9`:
export FX_THEME=9
```
<img width="1214" alt="themes" src="docs/images/themes.png">
<img width="1214" alt="themes" src="doc/images/themes.png">
Add your own themes in [theme.go](pkg/theme/theme.go) file.
## License
[MIT](https://github.com/antonmedv/fx/blob/master/LICENSE)
[MIT](LICENSE)

@ -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. |

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Before

Width:  |  Height:  |  Size: 659 KiB

After

Width:  |  Height:  |  Size: 659 KiB

@ -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')
> ```

@ -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
```

@ -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
```

@ -1,19 +0,0 @@
# Reducers
Fx takes a few arguments after the file name, and converts them to a reducer.
## Node
Access all lodash (or ramda, etc) methods by using [.fxrc](#using-fxrc) file.
```bash
$ fx data.json 'groupBy("commit.committer.name")' 'mapValues(_.size)'
```
## Python
TODO
## Ruby
TODO

@ -6,6 +6,7 @@ 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
@ -16,6 +17,8 @@ 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
@ -23,7 +26,8 @@ require (
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-20220422013727-9388b58f7150 // 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
)

@ -12,8 +12,26 @@ github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0
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=
@ -46,6 +64,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
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=
@ -56,12 +76,21 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w
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-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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=

@ -2,36 +2,41 @@ package main
import (
"fmt"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/lipgloss"
"reflect"
"strings"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/lipgloss"
)
func usage(keyMap KeyMap) string {
title := lipgloss.NewStyle().Bold(true)
return fmt.Sprintf(`fx - terminal JSON viewer
pad := lipgloss.NewStyle().PaddingLeft(4)
return fmt.Sprintf(`
%v
Terminal JSON viewer
%v
fx data.json
fx data.json .field
curl ... | fx
%v
fx data.json
fx data.json .field
curl ... | fx
%v
%v
%v
-h, --help print help
-v, --version print version
--print-code print code of the reducer
%v
%v
[https://fx.wtf]
%v
[https://fx.wtf]
`,
title.Render("fx "+version),
title.Render("Usage"),
title.Render("Flags"),
title.Render("Key Bindings"),
strings.Join(
keyMapInfo(
keyMap,
lipgloss.NewStyle().PaddingLeft(2),
),
"\n",
),
strings.Join(keyMapInfo(keyMap, pad), "\n"),
title.Render("More info"),
)
}

@ -8,7 +8,6 @@ import (
"path"
"runtime/pprof"
"strings"
"time"
. "github.com/antonmedv/fx/pkg/dict"
. "github.com/antonmedv/fx/pkg/json"
@ -22,12 +21,35 @@ import (
"github.com/muesli/termenv"
)
var (
flagHelp bool
flagVersion bool
flagPrintCode bool
)
func main() {
if len(os.Args) == 2 && (os.Args[1] == "-v" || os.Args[1] == "-V" || os.Args[1] == "--version") {
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)
@ -39,7 +61,6 @@ func main() {
panic(err)
}
}
themeId, ok := os.LookupEnv("FX_THEME")
if !ok {
themeId = "1"
@ -51,18 +72,23 @@ func main() {
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 args []string
var dec *json.Decoder
if stdinIsTty {
if len(os.Args) >= 2 {
filePath = os.Args[1]
f, err := os.Open(os.Args[1])
// 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:
@ -74,36 +100,68 @@ func main() {
}
fileName = path.Base(filePath)
dec = json.NewDecoder(f)
args = os.Args[2:]
args = args[1:]
}
} else {
dec = json.NewDecoder(os.Stdin)
args = os.Args[1:]
}
if dec == nil {
fmt.Println(usage(DefaultKeyMap()))
os.Exit(1)
}
dec.UseNumber()
jsonObject, err := Parse(dec)
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 && args[0] == "--print-code" {
fmt.Print(GenerateCode(args[1:]))
if len(args) > 0 && flagPrintCode {
fmt.Print(GenerateCode(lang, args, fxrc))
return
}
Reduce(jsonObject, args, theme)
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))
}
}
expand := map[string]bool{
"": true,
}
if array, ok := jsonObject.(Array); ok {
// Start interactive mode.
expand := map[string]bool{"": true}
if array, ok := object.(Array); ok {
for i := range array {
expand[accessor("", i)] = true
}
@ -111,7 +169,7 @@ func main() {
parents := map[string]string{}
children := map[string][]string{}
canBeExpanded := map[string]bool{}
Dfs(jsonObject, func(it Iterator) {
Dfs(object, func(it Iterator) {
parents[it.Path] = it.Parent
children[it.Parent] = append(children[it.Parent], it.Path)
switch it.Object.(type) {
@ -121,14 +179,13 @@ func main() {
canBeExpanded[it.Path] = len(it.Object.(Array)) > 0
}
})
input := textinput.New()
input.Prompt = ""
m := &model{
fileName: fileName,
theme: theme,
json: jsonObject,
json: object,
showSize: showSize,
width: 80,
height: 60,
mouseWheelDelta: 3,
@ -143,11 +200,6 @@ func main() {
searchInput: input,
}
m.collectSiblings(m.json, "")
// TODO: delete after tea can reopen stdin.
// https://github.com/charmbracelet/bubbletea/issues/302
time.Sleep(100 * time.Millisecond)
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
if err := p.Start(); err != nil {
panic(err)
@ -165,6 +217,7 @@ type model struct {
footerHeight int
wrap bool
theme Theme
showSize bool // Show number of elements in preview
fileName string
json interface{}
@ -396,6 +449,10 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
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] {

@ -11,25 +11,25 @@ import (
func PrettyPrint(v interface{}, level int, theme theme.Theme) string {
ident := strings.Repeat(" ", level)
subident := strings.Repeat(" ", level-1)
switch v.(type) {
switch v := v.(type) {
case nil:
return theme.Null("null")
case bool:
if v.(bool) {
if v {
return theme.Boolean("true")
} else {
return theme.Boolean("false")
}
case json.Number:
return theme.Number(v.(json.Number).String())
return theme.Number(v.String())
case string:
return theme.String(fmt.Sprintf("%q", v))
case *dict.Dict:
keys := v.(*dict.Dict).Keys
keys := v.Keys
if len(keys) == 0 {
return theme.Syntax("{}")
}
@ -37,7 +37,7 @@ func PrettyPrint(v interface{}, level int, theme theme.Theme) string {
output += "\n"
for i, k := range keys {
key := theme.Key(i, len(keys))(fmt.Sprintf("%q", k))
value, _ := v.(*dict.Dict).Get(k)
value, _ := v.Get(k)
delim := theme.Syntax(": ")
line := ident + key + delim + PrettyPrint(value, level+1, theme)
if i < len(keys)-1 {
@ -49,12 +49,12 @@ func PrettyPrint(v interface{}, level int, theme theme.Theme) string {
return output + subident + theme.Syntax("}")
case []interface{}:
slice := v.([]interface{})
slice := v
if len(slice) == 0 {
return theme.Syntax("[]")
}
output := theme.Syntax("[\n")
for i, value := range v.([]interface{}) {
for i, value := range v {
line := ident + PrettyPrint(value, level+1, theme)
if i < len(slice)-1 {
line += ",\n"

@ -6,28 +6,28 @@ import (
)
func Stringify(v interface{}) string {
switch v.(type) {
switch v := v.(type) {
case nil:
return "null"
case bool:
if v.(bool) {
if v {
return "true"
} else {
return "false"
}
case Number:
return v.(Number).String()
return v.String()
case string:
return fmt.Sprintf("%q", v)
case *Dict:
result := "{"
for i, key := range v.(*Dict).Keys {
line := fmt.Sprintf("%q", key) + ": " + Stringify(v.(*Dict).Values[key])
if i < len(v.(*Dict).Keys)-1 {
for i, key := range v.Keys {
line := fmt.Sprintf("%q", key) + ": " + Stringify(v.Values[key])
if i < len(v.Keys)-1 {
line += ","
}
result += line
@ -36,9 +36,9 @@ func Stringify(v interface{}) string {
case Array:
result := "["
for i, value := range v.(Array) {
for i, value := range v {
line := Stringify(value)
if i < len(v.(Array))-1 {
if i < len(v)-1 {
line += ","
}
result += line

@ -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
}

@ -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)
}
}

@ -6,12 +6,11 @@ import (
"os"
"os/exec"
"path"
"regexp"
"strings"
)
func CreateNodejs(args []string) *exec.Cmd {
cmd := exec.Command("node", "--input-type=module", "-e", nodejs(args))
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)
@ -25,21 +24,14 @@ func CreateNodejs(args []string) *exec.Cmd {
return cmd
}
//go:embed reduce.js
var templateJs string
//go:embed node.js
var templateNode string
func nodejs(args []string) string {
func nodejs(args []string, fxrc string) string {
rs := "\n"
for i, a := range args {
rs += " try {"
switch {
case a == ".":
rs += `
x = function ()
{ return this }
.call(x)
`
case flatMapRegex.MatchString(a):
code := fold(strings.Split(a, "[]"))
rs += fmt.Sprintf(
@ -87,28 +79,5 @@ func nodejs(args []string) string {
rs += " }\n"
}
fxrc := ""
home, err := os.UserHomeDir()
if err == nil {
b, err := os.ReadFile(path.Join(home, ".fxrc.js"))
if err == nil {
fxrc = "\n" + string(b)
}
}
return fmt.Sprintf(templateJs, fxrc, rs)
}
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:]))
return fmt.Sprintf(templateNode, fxrc, rs)
}

@ -11,26 +11,18 @@ func CreatePython(bin string, args []string) *exec.Cmd {
return cmd
}
//go:embed reduce.py
//go:embed python.py
var templatePython string
func python(args []string) string {
rs := "\n"
for i, a := range args {
rs += "try:"
switch {
case a == ".":
rs += `
x = x
`
default:
rs += fmt.Sprintf(
`
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)

@ -5,7 +5,6 @@ import (
_ "embed"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
@ -13,14 +12,12 @@ import (
. "github.com/antonmedv/fx/pkg/theme"
)
func GenerateCode(args []string) string {
lang, ok := os.LookupEnv("FX_LANG")
if !ok {
lang = "node"
}
func GenerateCode(lang string, args []string, fxrc string) string {
switch lang {
case "js":
return js(args, fxrc)
case "node":
return nodejs(args)
return nodejs(args, fxrc)
case "python", "python3":
return python(args)
case "ruby":
@ -30,15 +27,17 @@ func GenerateCode(args []string) string {
}
}
func Reduce(object interface{}, args []string, theme Theme) {
var cmd *exec.Cmd
lang, ok := os.LookupEnv("FX_LANG")
if !ok {
lang = "node"
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)
cmd = CreateNodejs(args, fxrc)
case "python", "python3":
cmd = CreatePython(lang, args)
case "ruby":
@ -47,25 +46,10 @@ func Reduce(object interface{}, args []string, theme Theme) {
panic("unknown lang")
}
cmd.Stdin = strings.NewReader(Stringify(object))
// TODO: Reimplement stringify with io.Reader.
cmd.Stdin = strings.NewReader(Stringify(input))
output, err := cmd.CombinedOutput()
if err == nil {
dec := json.NewDecoder(bytes.NewReader(output))
dec.UseNumber()
jsonObject, err := Parse(dec)
if err != nil {
fmt.Print(string(output))
return
}
if str, ok := jsonObject.(string); ok {
fmt.Println(str)
} else {
fmt.Println(PrettyPrint(jsonObject, 1, theme))
}
if dec.InputOffset() < int64(len(output)) {
fmt.Print(string(output[dec.InputOffset():]))
}
} else {
if err != nil {
exitCode := 1
status, ok := err.(*exec.ExitError)
if ok {
@ -74,24 +58,19 @@ func Reduce(object interface{}, args []string, theme Theme) {
fmt.Println(err.Error())
}
fmt.Print(string(output))
os.Exit(exitCode)
return exitCode
}
}
func trace(args []string, i int) (pre, post, pointer string) {
pre = strings.Join(args[:i], " ")
if len(pre) > 20 {
pre = "..." + pre[len(pre)-20:]
dec := json.NewDecoder(bytes.NewReader(output))
dec.UseNumber()
object, err := Parse(dec)
if err != nil {
fmt.Print(string(output))
return 0
}
post = strings.Join(args[i+1:], " ")
if len(post) > 20 {
post = post[:20] + "..."
Echo(object, theme)
if dec.InputOffset() < int64(len(output)) {
fmt.Print(string(output[dec.InputOffset():]))
}
pointer = fmt.Sprintf(
"%v %v %v",
strings.Repeat(" ", len(pre)),
strings.Repeat("^", len(args[i])),
strings.Repeat(" ", len(post)),
)
return
return 0
}

@ -11,25 +11,16 @@ func CreateRuby(args []string) *exec.Cmd {
return cmd
}
//go:embed reduce.rb
//go:embed ruby.rb
var templateRuby string
func ruby(args []string) string {
rs := "\n"
for i, a := range args {
rs += "begin"
switch {
case a == ".":
rs += `
x = x
`
default:
rs += fmt.Sprintf(
`
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)

@ -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
}

@ -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)
})
}
}

@ -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:]))
}

@ -2,10 +2,11 @@ package main
import (
"fmt"
"strings"
. "github.com/antonmedv/fx/pkg/dict"
. "github.com/antonmedv/fx/pkg/json"
"github.com/antonmedv/fx/pkg/theme"
"strings"
)
func (m *model) connect(path string, lineNumber int) {
@ -117,46 +118,61 @@ func (m *model) preview(v interface{}, path string, selectableValues bool) strin
if selectableValues && m.cursorPath() == path {
previewStyle = m.theme.Cursor
}
printValue := func(value interface{}) string {
switch value.(type) {
printValue := func(v interface{}) string {
switch v := v.(type) {
case nil, bool, Number:
return previewStyle(fmt.Sprintf("%v", value))
return previewStyle(fmt.Sprintf("%v", v))
case string:
return previewStyle(fmt.Sprintf("%q", value))
return previewStyle(fmt.Sprintf("%q", v))
case *Dict:
return previewStyle("{\u2026}")
if m.showSize {
return previewStyle(toLowerNumber(fmt.Sprintf("{\u2026%v\u2026}", len(v.Keys))))
} else {
return previewStyle("{\u2026}")
}
case Array:
return previewStyle("[\u2026]")
if m.showSize {
return previewStyle(toLowerNumber(fmt.Sprintf("[\u2026%v\u2026]", len(v))))
} else {
return previewStyle("[\u2026]")
}
}
return "..."
}
switch v.(type) {
switch v := v.(type) {
case *Dict:
output := m.printOpenBracket("{", searchResult, path, selectableValues)
keys := v.(*Dict).Keys
keys := v.Keys
for _, k := range keys {
key := fmt.Sprintf("%q", k)
output += previewStyle(key + ": ")
value, _ := v.(*Dict).Get(k)
value, _ := v.Get(k)
output += printValue(value)
break
}
if len(keys) > 1 {
output += previewStyle(", \u2026")
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)
slice := v.(Array)
for _, value := range slice {
for _, value := range v {
output += printValue(value)
break
}
if len(slice) > 1 {
output += previewStyle(", \u2026")
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

@ -2,9 +2,10 @@ package main
import (
"fmt"
"regexp"
. "github.com/antonmedv/fx/pkg/dict"
. "github.com/antonmedv/fx/pkg/json"
"regexp"
)
type searchResult struct {
@ -87,6 +88,7 @@ func (m *model) remapSearchResult(object interface{}, path string, pos int, inde
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
@ -206,10 +208,10 @@ func (m *model) jumpToSearchResult(at int) {
m.showCursor = false
m.searchResultsCursor = at % len(m.searchResults)
desiredPath := m.searchResults[m.searchResultsCursor].path
lineNumber, ok := m.pathToLineNumber[desiredPath]
_, ok := m.pathToLineNumber[desiredPath]
if ok {
m.cursor = m.pathToIndex[desiredPath]
m.SetOffset(lineNumber)
m.scrollDownToCursor()
m.render()
} else {
m.expandToPath(desiredPath)

@ -1,5 +1,5 @@
name: fx
version: 22.0.8
version: 24.0.0
summary: Terminal JSON viewer
description: Terminal JSON viewer
base: core18

@ -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
}
}
}

@ -2,6 +2,8 @@ package main
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
@ -33,3 +35,16 @@ func width(s string) int {
func accessor(path string, to interface{}) string {
return fmt.Sprintf("%v[%v]", path, to)
}
func toLowerNumber(s string) string {
var out strings.Builder
for _, r := range s {
switch {
case '0' <= r && r <= '9':
out.WriteRune('\u2080' + (r - '\u0030'))
default:
out.WriteRune(r)
}
}
return out.String()
}

@ -1,3 +1,3 @@
package main
const version = "22.0.8"
const version = "24.0.0"

Loading…
Cancel
Save