Compare commits

..

No commits in common. 'master' and 'js-version' have entirely different histories.

@ -1,23 +0,0 @@
let goos = [
'linux',
'darwin',
'windows',
]
let goarch = [
'amd64',
'arm64',
]
let name = (GOOS, GOARCH) => `fx_${GOOS}_${GOARCH}` + (GOOS === 'windows' ? '.exe' : '')
await $`go mod download`
await Promise.all(
goos.flatMap(GOOS =>
goarch.map(GOARCH =>
$`GOOS=${GOOS} GOARCH=${GOARCH} go build -o ${name(GOOS, GOARCH)}`)))
await Promise.all(
goos.flatMap(GOOS =>
goarch.map(GOARCH =>
$`gh release upload ${process.env.RELEASE_VERSION} ${name(GOOS, GOARCH)}`)))

@ -1,116 +0,0 @@
name: release
on:
release:
types:
- created
jobs:
commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: master
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Get Version
run: echo "RELEASE_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: Update Version
shell: bash
run: |
set -x
sed -i "s/version = .*/version = \"${RELEASE_VERSION}\"/" version.go
sed -i "s/version: .*/version: ${RELEASE_VERSION}/" snap/snapcraft.yaml
git add version.go snap/snapcraft.yaml
git config --global user.email "github-actions@github.com"
git config --global user.name "github-actions"
git commit -m "Release $RELEASE_VERSION"
git tag "$RELEASE_VERSION" --force
git push --atomic --force origin master "$RELEASE_VERSION"
- name: Test
run: go test -v ./...
binary:
needs: [commit]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: master
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Get Version
run: echo "RELEASE_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: Build and Upload
env:
FORCE_COLOR: 3
GITHUB_TOKEN: ${{ secrets.MY_TOKEN }}
run: npx zx .github/workflows/release.mjs
snap:
needs: [commit]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: master
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
- uses: snapcore/action-build@v1
id: build
- uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.STORE_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: stable
brew:
needs: [commit]
runs-on: macos-latest
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
with:
test-bot: false
- name: Cache Homebrew Bundler RubyGems
id: cache
uses: actions/cache@v2
with:
path: ${{ steps.set-up-homebrew.outputs.gems-path }}
key: ${{ runner.os }}-rubygems-${{ steps.set-up-homebrew.outputs.gems-hash }}
restore-keys: ${{ runner.os }}-rubygems-
- name: Install Homebrew Bundler RubyGems
if: steps.cache.outputs.cache-hit != 'true'
run: brew install-bundler-gems
- name: Configure Git user
uses: Homebrew/actions/git-user-config@master
- name: Update brew
run: brew update
- name: Bump formulae
uses: Homebrew/actions/bump-formulae@master
with:
token: ${{ secrets.MY_TOKEN }}
formulae: fx

@ -1,21 +0,0 @@
name: test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
- name: Build
run: go build -v ./...

3
.gitignore vendored

@ -0,0 +1,3 @@
/node_modules/
/dist/
package-lock.json

@ -0,0 +1,4 @@
language: node_js
node_js:
- "node"
- "11"

@ -0,0 +1,331 @@
# Documentation
* [Getting started](#getting-started)
* [Usage](#usage)
+ [Anonymous function](#anonymous-function)
+ [Binding](#binding)
+ [Dot](#dot)
+ [Map](#map)
+ [Chaining](#chaining)
+ [Updating](#updating)
+ [Edit-in-place](#edit-in-place)
+ [Using packages](#using-packages)
* [Using .fxrc](#using-fxrc)
+ [Query language](#query-language)
* [Formatting](#formatting)
* [Other examples](#other-examples)
* [Streaming mode](#streaming-mode)
+ [Filtering](#filtering)
* [Interactive mode](#interactive-mode)
+ [Searching](#searching)
+ [Selecting text](#selecting-text)
* [Memory Usage](#memory-usage)
## Getting started
`fx` can work in two modes: cli and interactive. To start interactive mode pipe any JSON into `fx`:
```bash
$ curl ... | fx
```
Or you can pass a filename as the first parameter:
```bash
$ fx my.json
```
If any argument was passed, `fx` will apply it and prints to stdout.
## Usage
### Anonymous function
Use an anonymous function as reducer which gets JSON and processes it:
```bash
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo[0].bar'
value
```
### Binding
If you don't pass anonymous function `param => ...`, code will be automatically transformed into anonymous function.
And you can get access to JSON by `this` keyword:
```bash
$ echo '{"foo": [{"bar": "value"}]}' | fx 'this.foo[0].bar'
value
```
### Dot
It is possible to omit `this` keyword:
```bash
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
value
```
If a single dot is passed, the input JSON will be formatted but otherwise unaltered:
```bash
$ echo '{"foo": "bar"}' | fx .
{
"foo": "bar"
}
```
### Map
One of the frequent operations is mapping some function on an array. For example, to extract some values.
```
[
{
"author": {
"name": "antonmedv"
}
},
{...},
...
]
```
And we want to collect names of each object in the array. We can do this by mapping anonymous function:
```bash
$ cat ... | fx '.map(x => x.author.name)'
```
Or we can do the same by using jq-like syntax:
```bash
$ cat ... | fx .[].author.name
[
"antonmedv",
...
]
```
> Note what `[]` can be applied to map object values.
> ```bash
> $ echo '{"foo": 1, "bar": 2}' | fx .[]
> [1, 2]
> ```
### Chaining
You can pass any number of anonymous functions for reducing JSON:
```bash
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo' 'this[0]' 'this.bar'
value
```
### Updating
You can update existing JSON using the spread operator:
```bash
$ echo '{"count": 0}' | fx '{...this, count: 1}'
{
"count": 1
}
```
### Edit-in-place
`fx` provides a function `save` which will save everything in place and return saved object.
This function can be only used with filename as first argument to `fx` command.
Usage:
```bash
fx data.json '{...this, count: this.count+1}' save .count
```
### Using packages
Use any npm package by installing it globally:
```bash
$ npm install -g lodash
$ cat package.json | fx 'require("lodash").keys(this.dependencies)'
```
## Using .fxrc
Create _.fxrc_ file in `$HOME` directory, and require any packages or define global functions.
For example, access all lodash methods without `_` prefix. Put in your `.fxrc` file:
```js
Object.assign(global, require('lodash/fp'))
```
And now you will be able to call all lodash methods. For example, see who's been committing to react recently:
```bash
curl 'https://api.github.com/repos/facebook/react/commits?per_page=100' \
| fx 'groupBy("commit.author.name")' 'mapValues(size)' toPairs 'sortBy(1)' reverse 'take(10)' fromPairs
```
> To be able require global modules make sure you have correct `NODE_PATH` env variable.
> ```bash
> export NODE_PATH=`npm root -g`
> ```
### Query language
If you want to use query language, for example [jsonata](http://jsonata.org/) you can use helper function like this:
```js
global.jsonata = expr => require('jsonata')(expr).evaluate
```
And use it like this:
```bash
curl ... | fx 'jsonata("$sum(Order.Product.(Price * Quantity))")'
```
Instead you can create next alias in _.bashrc_ file:
```bash
alias jsonata='FX_APPLY=jsonata fx'
```
And now all code arguments to `jsonata` will be passed through `jsonata` helper. And now you can use it like this:
```bash
curl ... | jsonata '$sum(Order.Product.(Price * Quantity))'
```
## Formatting
If you need output other than JSON (for example arguments for xargs), do not return anything from the reducer.
`undefined` value is printed into stderr by default.
```bash
echo '[]' | fx 'void 0'
undefined
```
```bash
echo '[1,2,3]' | fx 'this.forEach(x => console.log(+x))' 2>/dev/null | xargs echo
1 2 3
```
## Other examples
Convert object to array:
```bash
$ cat package.json | fx 'Object.keys(this.dependencies)'
```
Or by two functions:
```bash
$ cat package.json | fx .dependencies Object.keys
```
By the way, fx has shortcut for `Object.keys`. Previous example can be rewritten as:
```bash
$ cat package.json | fx .dependencies ?
```
## Streaming mode
`fx` supports line-delimited JSON and concatenated JSON streaming.
```bash
$ kubectl logs ... | fx .message
```
> Note what is object lacks `message` field, _undefined_ will be printed to stderr.
> This is useful to see if you are skipping some objects. But if you want to hide them,
> redirect stderr to `/dev/null`.
### Filtering
Sometimes it is necessary to omit some messages in JSON stream, or select only specified log messages.
For this purpose, `fx` has special helpers `select`/`filter`, pass function into it to select/filter JSON messages.
```bash
$ kubectl logs ... | fx 'select(x => x.status == 500)' .message
```
```bash
$ kubectl logs ... | fx 'filter(x => x.status < 499)' .message
```
> If `filter`/`select` overridden in _.fxrc_ you still able to access them with prefix:
> `std.select(cb)` or `std.filter(cd)`
## Interactive mode
Click on fields to expand or collapse JSON tree, use mouse wheel to scroll view.
Next commands available in interactive mode:
| Key | Command |
|-------------------------------|----------------------------------------------|
| `q` or `Esc` or `Ctrl`+`c` | Exit |
| `up` or `k` | Move cursor up |
| `down` or `j` | Move cursor down |
| `left` or `h` | Collapse |
| `right` or `l` | Expand |
| `Shift`+`right` or `L` | Expand all under cursor |
| `Shift`+`left` or `K` | Collapse all under cursor |
| `e` | Expand all |
| `E` | Collapse all |
| `g` | Scroll to top |
| `G` | Scroll to bottom |
| `.` | Edit filter |
| `/` | Search |
| `n` | Find next |
| `p` | Exit and print JSON to stdout |
| `P` | Exit and print fully expanded JSON to stdout |
These commands are available when editing the filter:
| Key | Command |
|-------------------------------|-------------------------|
| `Enter` | Apply filter |
| `Ctrl`+`u` | Clear filter |
| `Ctrl`+`w` | Delete last part |
| `up`/`down` | Select autocomplete |
### Searching
Press `/` and type regexp pattern to search in current JSON. Search work with currently applied filter.
Examples of pattern and corresponding regexp:
| Pattern | RegExp |
|------------|-------------|
| `/apple` | `/apple/ig` |
| `/apple/` | `/apple/` |
| `/apple/u` | `/apple/u` |
| `/\w+` | `/\w+/ig` |
### Selecting text
You may found what you can't just select text in fx. This is due the fact that all mouse events redirected to stdin. To be able select again you need instruct your terminal not to do it. This can be done by holding special keys while selecting:
| Key | Terminal |
|------------------|---------------|
| `Option`+`Mouse` | iTerm2, Hyper |
| `Fn`+`Mouse` | Terminal.app |
| `Shift`+`Mouse` | Linux |
> Note what you can press `p`/`P` to print everything to stdout and select if there.
## Memory Usage
You may find that sometimes, on really big JSON files, fx prints an error message like this:
```
FATAL ERROR: JavaScript heap out of memory
```
V8 limits memory usage to around 2 GB by default. You can increase the limit by putting this line in your _.profile_:
```bash
export NODE_OPTIONS='--max-old-space-size=8192'
```

@ -1,93 +1,106 @@
<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>
<p align="center"><a href="http://fx.wtf"><img src="https://medv.io/assets/fx-logo.png" height="100" alt="fx logo"></a></p>
<p align="center"><img src="https://medv.io/assets/fx.gif" width="562" alt="fx example"></p>
_* Function eXecution_
[![Build Status](https://travis-ci.org/antonmedv/fx.svg?branch=master)](https://travis-ci.org/antonmedv/fx)
[![Npm Version](https://img.shields.io/npm/v/fx.svg)](https://www.npmjs.com/package/fx)
[![Brew Version](https://img.shields.io/homebrew/v/fx.svg)](https://formulae.brew.sh/formula/fx)
Command-line JSON processing tool
## Features
- Mouse support
- Streaming support
- Preserves key order
- Preserves big numbers
* Easy to use
* Standalone binary
* Interactive mode 🎉
* Streaming support 🌊
## Install
```bash
brew install fx
npm install -g fx
```
Or via Homebrew
```bash
snap install fx
brew install fx
```
Or download standalone binary from [releases](https://github.com/antonmedv/fx/releases)
## Usage
Start [interactive mode](https://github.com/antonmedv/fx/blob/master/DOCS.md#interactive-mode) without passing any arguments.
```bash
scoop install fx
$ curl ... | fx
```
Or by passing filename as first argument.
```bash
pacman -S fx
$ fx data.json
```
Pass a few JSON files.
```bash
pkg install fx
cat foo.json bar.json baz.json | fx .message
```
Use full power of JavaScript.
```bash
go install github.com/antonmedv/fx@latest
$ curl ... | fx '.filter(x => x.startsWith("a"))'
```
Or download [pre-built binary](https://github.com/antonmedv/fx/releases).
## Usage
Start the interactive viewer via:
Access all lodash (or ramda, etc) methods by using [.fxrc](https://github.com/antonmedv/fx/blob/master/DOCS.md#using-fxrc) file.
```bash
fx data.json
$ curl ... | fx '_.groupBy("commit.committer.name")' '_.mapValues(_.size)'
```
Or
Update JSON using spread operator.
```bash
curl ... | fx
$ echo '{"count": 0}' | fx '{...this, count: 1}'
{
"count": 1
}
```
Type `?` to see full list of key shortcuts.
Pretty print:
Extract values from maps.
```bash
curl ... | fx .
$ fx commits.json | fx .[].author.name
```
### Reducers
Write reducers in your favorite language: [JavaScript](doc/js.md) (default),
[Python](doc/python.md), or [Ruby](doc/ruby.md).
Print formatted JSON to stdout.
```bash
fx data.json '.filter(x => x.startsWith("a"))'
$ curl ... | fx .
```
Pipe JSON logs stream into fx.
```bash
fx data.json '[x["age"] + i for i in range(10)]'
$ kubectl logs ... -f | fx .message
```
And try this:
```bash
fx data.json 'x.to_a.map {|x| x[1]}'
$ fx --life
```
## Documentation
See full [documentation](doc/doc.md).
See full [documentation](https://github.com/antonmedv/fx/blob/master/DOCS.md).
## Themes
## Links
Theme can be configured by setting environment variable `FX_THEME` from `1`
to `9`:
* [Discover how to use fx effectively](http://bit.ly/discover-how-to-use-fx-effectively)
* [Video tutorial](http://bit.ly/youtube-fx-tutorial)
```bash
export FX_THEME=9
```
## Related
<img width="1214" alt="themes" src="doc/images/themes.png">
* [gofx](https://github.com/antonmedv/gofx) fx-like JSON tool (*go*)
* [eat](https://github.com/antonmedv/eat) converts anything into JSON
* [ymlx](https://github.com/matthewadams/ymlx) fx-like YAML cli processor
* [fx-completion](https://github.com/antonmedv/fx-completion) bash completion for fx
* [fx-theme-monokai](https://github.com/antonmedv/fx-theme-monokai) monokai theme
* [fx-theme-night](https://github.com/antonmedv/fx-theme-night) night theme
Add your own themes in [theme.go](pkg/theme/theme.go) file.
## License
[MIT](LICENSE)
[MIT](https://github.com/antonmedv/fx/blob/master/LICENSE)

@ -0,0 +1,79 @@
'use strict'
// http://bit.ly/fx--life
const
run = f => setInterval(f, 16),
p = (s) => process.stdout.write(s),
esc = (...x) => x.map(i => p('\u001B[' + i)),
[upper, lower, full] = ['\u2580', '\u2584', '\u2588'],
{columns, rows} = process.stdout,
[w, h] = [columns, rows * 2],
a = Math.floor(w / 2) - 6, b = Math.floor(h / 2) - 7
let $ = Array(w * h).fill(false)
if (Date.now() % 3 === 0) {
$[1 + 5 * w] = $[1 + 6 * w] = $[2 + 5 * w] = $[2 + 6 * w] = $[12 + 5 * w] = $[12 + 6 * w] = $[12 + 7 * w] = $[13 + 4 * w] = $[13 + 8 * w] = $[14 + 3 * w] = $[14 + 9 * w] = $[15 + 4 * w] = $[15 + 8 * w] = $[16 + 5 * w] = $[16 + 6 * w] = $[16 + 7 * w] = $[17 + 5 * w] = $[17 + 6 * w] = $[17 + 7 * w] = $[22 + 3 * w] = $[22 + 4 * w] = $[22 + 5 * w] = $[23 + 2 * w] = $[23 + 3 * w] = $[23 + 5 * w] = $[23 + 6 * w] = $[24 + 2 * w] = $[24 + 3 * w] = $[24 + 5 * w] = $[24 + 6 * w] = $[25 + 2 * w] = $[25 + 3 * w] = $[25 + 4 * w] = $[25 + 5 * w] = $[25 + 6 * w] = $[26 + 1 * w] = $[26 + 2 * w] = $[26 + 6 * w] = $[26 + 7 * w] = $[35 + 3 * w] = $[35 + 4 * w] = $[36 + 3 * w] = $[36 + 4 * w] = true
} else if (Date.now() % 3 === 1) {
for (let i = 0; i < $.length; i-=-1)
if (Math.random() < 0.16) $[i] = true
} else {
$[a + 1 + (2 + b) * w] = $[a + 2 + (1 + b) * w] = $[a + 2 + (3 + b) * w] = $[a + 3 + (2 + b) * w] = $[a + 5 + (15 + b) * w] = $[a + 6 + (13 + b) * w] = $[a + 6 + (15 + b) * w] = $[a + 7 + (12 + b) * w] = $[a + 7 + (13 + b) * w] = $[a + 7 + (15 + b) * w] = $[a + 9 + (11 + b) * w] = $[a + 9 + (12 + b) * w] = $[a + 9 + (13 + b) * w] = true
}
function at(i, j) {
if (i < 0) i = h - 1
if (i >= h) i = 0
if (j < 0) j = w - 1
if (j >= w) j = 0
return $[i * w + j]
}
function neighbors(i, j) {
let c = 0
at(i - 1, j - 1) && c++
at(i - 1, j) && c++
at(i - 1, j + 1) && c++
at(i, j - 1) && c++
at(i, j + 1) && c++
at(i + 1, j - 1) && c++
at(i + 1, j) && c++
at(i + 1, j + 1) && c++
return c
}
run(() => {
esc('H')
let gen = Array(w * h).fill(false)
for (let i = 0; i < h; i-=-1) {
for (let j = 0; j < w; j-=-1) {
const n = neighbors(i, j)
const z = i * w + j
if ($[z]) {
if (n < 2) gen[z] = false
if (n === 2 || n === 3) gen[z] = true
if (n > 3) gen[z] = false
} else {
if (n === 3) gen[z] = true
}
}
}
$ = gen
for (let i = 0; i < rows; i-=-1) {
for (let j = 0; j < columns; j-=-1) {
if ($[i * 2 * w + j] && $[(i * 2 + 1) * w + j]) p(full)
else if ($[i * 2 * w + j] && !$[(i * 2 + 1) * w + j]) p(upper)
else if (!$[i * 2 * w + j] && $[(i * 2 + 1) * w + j]) p(lower)
else p(' ')
}
if (i !== rows - 1) p('\n')
}
})
esc('2J', '?25l')
process.on('SIGINT', () => {
esc('?25h')
process.exit(2)
})

@ -0,0 +1,26 @@
'use strict'
const chalk = require('chalk')
const noop = x => x
const list = {
fg: 'black',
bg: 'cyan',
selected: {
bg: 'magenta'
}
}
module.exports = {
space: global.FX_STYLE_SPACE || 2,
null: global.FX_STYLE_NULL || chalk.grey.bold,
number: global.FX_STYLE_NUMBER || chalk.cyan.bold,
boolean: global.FX_STYLE_BOOLEAN || chalk.yellow.bold,
string: global.FX_STYLE_STRING || chalk.green.bold,
key: global.FX_STYLE_KEY || chalk.blue.bold,
bracket: global.FX_STYLE_BRACKET || noop,
comma: global.FX_STYLE_COMMA || noop,
colon: global.FX_STYLE_COLON || noop,
list: global.FX_STYLE_LIST || list,
highlight: global.FX_STYLE_HIGHLIGHT || chalk.black.bgYellow,
highlightCurrent: global.FX_STYLE_HIGHLIGHT_CURRENT || chalk.inverse,
statusBar: global.FX_STYLE_STATUS_BAR || chalk.inverse,
}

@ -1,66 +0,0 @@
# Documentation
The **fx** can work in two modes: as a reducer or an interactive viewer.
To start the interactive mode pipe a JSON into **fx**:
```sh
$ curl ... | fx
```
Or you can pass a filename as the first parameter:
```sh
$ fx data.json
```
## Reducers
Use [JavaScript](js.md), [Python](python.md), or [Ruby](ruby.md).
## Streaming mode
The **fx** supports line-delimited JSON streaming or concatenated JSON streaming.
```sh
$ echo '
> {"message": "hello"}
> {"message": "world!"}
> ' | fx .message
hello
world!
```
## Interactive mode
Type `?` to see the full list of available shortcuts while in the interactive mode.
### Search
Press `/` and type regexp pattern to search in the current JSON.
Search is performed on the internal representation of the JSON without newlines.
Type `n` to jump to the next result, and `N` to the previous
### Selecting text
You can't just select text in fx. This is due to the fact that all mouse events are
redirected to stdin. To be able to select again you need to instruct your terminal
not to do it. This can be done by holding special keys while selecting:
| Key | Terminal |
|------------------|---------------|
| `Option`+`Mouse` | iTerm2, Hyper |
| `Fn`+`Mouse` | Terminal.app |
| `Shift`+`Mouse` | Linux |
## Configs
Next configs available for **fx** via environment variables.
| Name | Values | Description |
|----------------|-----------------------------------------------------|-------------------------------------------------------|
| `FX_LANG` | `js` (default), `node`, `python`, `python3`, `ruby` | Reducer type. |
| `FX_THEME` | `0` (disable colors), `1` (default), `2..9` | Color theme. |
| `FX_SHOW_SIZE` | `true` or `false` (default) | Show size of arrays and object in collapsed previews. |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 KiB

@ -1,120 +0,0 @@
# JavaScript Reducers
If any additional arguments were passed, fx converts them into a function which
takes the JSON as an argument named `x`.
By default, fx uses builtin JavaScript VM ([goja](https://github.com/dop251/goja)),
but also can be used with node.
```sh
export FX_LANG=js # Default
```
Or for usage with node:
```sh
export FX_LANG=node
```
An example of anonymous function used as a reducer:
```sh
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo[0].bar'
value
```
The same reducer function can be simplified to:
```sh
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x.foo[0].bar'
value
```
Each argument treated as a reducer function.
```sh
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x.foo' 'x[0]' 'x.bar'
value
```
Update JSON using the spread operator:
```sh
$ echo '{"name": "fx", "count": 0}' | fx '{...this, count: 1}'
{
"name": "fx",
"count": 1
}
```
## Dot
Fx supports simple JS-like syntax for accessing data, which can be used with any `FX_LANG`.
```sh
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
value
```
## .fxrc.js
Create _.fxrc.js_ file in `$HOME` directory, and define some useful functions.
```js
// .fxrc.js
function upper(s) {
return s.toUpperCase()
}
```
```sh
$ cat data.json | fx .name upper
ANTON
```
## Node
```sh
export FX_LANG=node
```
Use any npm package by installing it globally. Create _.fxrc.js_ file in `$HOME`
directory, and require any packages or define global functions. For example,
to access all lodash methods without `_` prefix, put next line into your
_.fxrc.js_ file:
```js
Object.assign(global, require('lodash/fp'))
```
And now you will be able to call all lodash methods. For example, see who's been
committing to react recently:
```sh
curl 'https://api.github.com/repos/facebook/react/commits?per_page=100' \
| fx 'groupBy("commit.author.name")' 'mapValues(size)' toPairs 'sortBy(1)' reverse 'take(10)' fromPairs
```
> To be able to require global modules make sure you have correct `NODE_PATH` env variable.
> ```sh
> export NODE_PATH=`npm root -g`
> ```
The _.fxrc.js_ file supports both: `import` and `require`.
```js
// .fxrc.js
import 'zx/globals'
const _ = require('lodash')
```
> If you want to use _.fxrc.js_ for both `FX_LANG=js` and `FX_LANG=node`,
> separate parts by `// nodejs:` comment:
> ```js
> // .fxrc.js
> function upper(s) {
> return s.toUpperCase()
> }
> // nodejs:
> import 'zx/globals'
> const _ = require('lodash')
> ```

@ -1,27 +0,0 @@
# Python Reducers
If any additional arguments was passed, **fx** converts it to a function which
takes the JSON as an argument named `x`.
```sh
export FX_LANG=python
```
Or
```sh
export FX_LANG=python3
```
Example:
```sh
fx data.json '[x["age"] + i for i in range(10)]'
```
## Dot
Fx supports simple syntax for accessing data, which can be used with any `FX_LANG`.
```sh
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
value
```

@ -1,23 +0,0 @@
# Ruby Reducers
If any additional arguments was passed, **fx** converts it to a function which
takes the JSON as an argument named `x`.
```sh
export FX_LANG=ruby
```
Example:
```sh
fx data.json 'x.to_a.map {|x| x[1]}'
```
## Dot
Fx supports simple syntax for accessing data, which can be used with any `FX_LANG`.
```sh
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
value
```

@ -0,0 +1,35 @@
'use strict'
function* find(v, regex, path = []) {
if (typeof v === 'undefined' || v === null) {
return
}
if (Array.isArray(v)) {
let i = 0
for (let value of v) {
yield* find(value, regex, path.concat(['[' + i++ + ']']))
}
return
}
if (typeof v === 'object' && v.constructor === Object) {
const entries = Object.entries(v)
for (let [key, value] of entries) {
const nextPath = path.concat(['.' + key])
if (regex.test(key)) {
yield nextPath
}
yield* find(value, regex, nextPath)
}
return
}
if (regex.test(v)) {
yield path
}
}
module.exports = find

740
fx.js

@ -0,0 +1,740 @@
'use strict'
const fs = require('fs')
const tty = require('tty')
const blessed = require('@medv/blessed')
const stringWidth = require('string-width')
const reduce = require('./reduce')
const print = require('./print')
const find = require('./find')
const config = require('./config')
module.exports = function start(filename, source, prev = {}) {
// Current rendered object on a screen.
let json = prev.json || source
// Contains map from row number to expand path.
// Example: {0: '', 1: '.foo', 2: '.foo[0]'}
let index = new Map()
// Contains expanded paths. Example: ['', '.foo']
// Empty string represents root path.
const expanded = prev.expanded || new Set()
expanded.add('')
// Current filter code.
let currentCode = null
// Current search regexp and generator.
let highlight = null
let findGen = null
let currentPath = null
let ttyReadStream, ttyWriteStream
// Reopen tty
if (process.platform === 'win32') {
const cfs = process.binding('fs')
ttyReadStream = tty.ReadStream(cfs.open('conin$', fs.constants.O_RDWR | fs.constants.O_EXCL, 0o666))
ttyWriteStream = tty.WriteStream(cfs.open('conout$', fs.constants.O_RDWR | fs.constants.O_EXCL, 0o666))
} else {
const ttyFd = fs.openSync('/dev/tty', 'r+')
ttyReadStream = tty.ReadStream(ttyFd)
ttyWriteStream = tty.WriteStream(ttyFd)
}
const program = blessed.program({
input: ttyReadStream,
output: ttyWriteStream,
})
const screen = blessed.screen({
program: program,
smartCSR: true,
fullUnicode: true,
})
const box = blessed.box({
parent: screen,
tags: false,
left: 0,
top: 0,
width: '100%',
height: '100%',
mouse: true,
keys: true,
vi: true,
ignoreArrows: true,
alwaysScroll: true,
scrollable: true,
})
const input = blessed.textbox({
parent: screen,
bottom: 0,
left: 0,
height: 1,
width: '100%',
})
const search = blessed.textbox({
parent: screen,
bottom: 0,
left: 0,
height: 1,
width: '100%',
})
const statusBar = blessed.box({
parent: screen,
tags: false,
bottom: 0,
left: 0,
height: 1,
width: '100%',
})
const autocomplete = blessed.list({
parent: screen,
width: 6,
height: 7,
left: 1,
bottom: 1,
style: config.list,
})
screen.title = filename
box.focus()
input.hide()
search.hide()
statusBar.hide()
autocomplete.hide()
process.stdout.on('resize', () => {
// Blessed has a bug with resizing the terminal. I tried my best to fix it but was not succeeded.
// For now exit and print seem like a reasonable alternative, as it not usable after resize.
// If anyone can fix this bug it will be cool.
printJson({expanded})
})
screen.key(['escape', 'q', 'C-c'], function () {
exit()
})
input.on('submit', function () {
if (autocomplete.hidden) {
const code = input.getValue()
if (/^\//.test(code)) {
// Forgive a mistake to the user. This looks like user wanted to search something.
apply('')
applyPattern(code)
} else {
apply(code)
}
} else {
// Autocomplete selected
let code = input.getValue()
let replace = autocomplete.getSelected()
if (/^[a-z]\w*$/i.test(replace)) {
replace = '.' + replace
} else {
replace = `["${replace}"]`
}
code = code.replace(/\.\w*$/, replace)
input.setValue(code)
autocomplete.hide()
update(code)
// Keep editing code
input.readInput()
}
})
input.on('cancel', function () {
if (autocomplete.hidden) {
const code = input.getValue()
apply(code)
} else {
// Autocomplete not selected
autocomplete.hide()
screen.render()
// Keep editing code
input.readInput()
}
})
input.on('update', function (code) {
if (currentCode === code) {
return
}
currentCode = code
if (index.size < 10000) { // Don't live update in we have a big JSON file.
update(code)
}
complete(code)
})
input.key('up', function () {
if (!autocomplete.hidden) {
autocomplete.up()
screen.render()
}
})
input.key('down', function () {
if (!autocomplete.hidden) {
autocomplete.down()
screen.render()
}
})
input.key('C-u', function () {
input.setValue('')
update('')
render()
})
input.key('C-w', function () {
let code = input.getValue()
code = code.replace(/[\.\[][^\.\[]*$/, '')
input.setValue(code)
update(code)
render()
})
search.on('submit', function (pattern) {
applyPattern(pattern)
})
search.on('cancel', function () {
highlight = null
currentPath = null
search.hide()
search.setValue('')
box.height = '100%'
box.focus()
program.cursorPos(0, 0)
render()
})
box.key('.', function () {
hideStatusBar()
box.height = '100%-1'
input.show()
if (input.getValue() === '') {
input.setValue('.')
complete('.')
}
input.readInput()
screen.render()
})
box.key('/', function () {
hideStatusBar()
box.height = '100%-1'
search.show()
search.setValue('/')
search.readInput()
screen.render()
})
box.key('e', function () {
hideStatusBar()
expanded.clear()
for (let path of dfs(json)) {
if (expanded.size < 1000) {
expanded.add(path)
} else {
break
}
}
render()
})
box.key('S-e', function () {
hideStatusBar()
expanded.clear()
expanded.add('')
render()
// Make sure cursor stay on JSON object.
const [n] = getLine(program.y)
if (typeof n === 'undefined' || !index.has(n)) {
// No line under cursor
let rest = [...index.keys()]
if (rest.length > 0) {
const next = Math.max(...rest)
let y = box.getScreenNumber(next) - box.childBase
if (y <= 0) {
y = 0
}
const line = box.getScreenLine(y + box.childBase)
program.cursorPos(y, line.search(/\S/))
}
}
})
box.key('n', function () {
hideStatusBar()
findNext()
})
box.key(['up', 'k'], function () {
hideStatusBar()
program.showCursor()
const [n] = getLine(program.y)
let next
for (let [i,] of index) {
if (i < n && (typeof next === 'undefined' || i > next)) {
next = i
}
}
if (typeof next !== 'undefined') {
let y = box.getScreenNumber(next) - box.childBase
if (y <= 0) {
box.scroll(-1)
screen.render()
y = 0
}
const line = box.getScreenLine(y + box.childBase)
program.cursorPos(y, line.search(/\S/))
}
})
box.key(['down', 'j'], function () {
hideStatusBar()
program.showCursor()
const [n] = getLine(program.y)
let next
for (let [i,] of index) {
if (i > n && (typeof next === 'undefined' || i < next)) {
next = i
}
}
if (typeof next !== 'undefined') {
let y = box.getScreenNumber(next) - box.childBase
if (y >= box.height) {
box.scroll(1)
screen.render()
y = box.height - 1
}
const line = box.getScreenLine(y + box.childBase)
program.cursorPos(y, line.search(/\S/))
}
})
box.key(['right', 'l'], function () {
hideStatusBar()
const [n, line] = getLine(program.y)
program.showCursor()
program.cursorPos(program.y, line.search(/\S/))
const path = index.get(n)
if (!expanded.has(path)) {
expanded.add(path)
render()
}
})
// Expand everything under cursor.
box.key(['S-right', 'S-l'], function () {
hideStatusBar()
const [n, line] = getLine(program.y)
program.showCursor()
program.cursorPos(program.y, line.search(/\S/))
const path = index.get(n)
const subJson = reduce(json, 'this' + path)
for (let p of dfs(subJson, path)) {
if (expanded.size < 1000) {
expanded.add(p)
} else {
break
}
}
render()
})
box.key(['S-left', 'S-k'], function () {
hideStatusBar()
const [n, line] = getLine(program.y)
program.showCursor()
program.cursorPos(program.y, line.search(/\S/))
const path = index.get(n)
if (!path) {
// collapse on top level (should render like after run)
expanded.clear()
expanded.add('')
} else {
const subJson = reduce(json, 'this' + path)
for (let p of dfs(subJson, path)) {
if (expanded.size < 1000) {
expanded.delete(p)
} else {
break
}
}
}
render()
})
box.key(['left', 'h'], function () {
hideStatusBar()
const [n, line] = getLine(program.y)
program.showCursor()
program.cursorPos(program.y, line.search(/\S/))
// Find path at current cursor position.
const path = index.get(n)
if (expanded.has(path)) {
// Collapse current path.
expanded.delete(path)
render()
} else {
// If there is no expanded paths on current line,
// collapse parent path of current location.
if (typeof path === 'string') {
// Trip last part (".foo", "[0]") to get parent path.
const parentPath = path.replace(/(\.[^\[\].]+|\[\d+\])$/, '')
if (expanded.has(parentPath)) {
expanded.delete(parentPath)
render()
// Find line number of parent path, and if we able to find it,
// move cursor to this position of just collapsed parent path.
for (let y = program.y; y >= 0; --y) {
const [n, line] = getLine(y)
if (index.get(n) === parentPath) {
program.cursorPos(y, line.search(/\S/))
break
}
}
}
}
}
})
box.on('click', function (mouse) {
hideStatusBar()
const [n, line] = getLine(mouse.y)
if (mouse.x >= stringWidth(line)) {
return
}
program.hideCursor()
program.cursorPos(mouse.y, line.search(/\S/))
autocomplete.hide()
const path = index.get(n)
if (expanded.has(path)) {
expanded.delete(path)
} else {
expanded.add(path)
}
render()
})
box.on('scroll', function () {
hideStatusBar()
})
box.key('p', function () {
printJson({expanded})
})
box.key('S-p', function () {
printJson()
})
function printJson(options = {}) {
screen.destroy()
program.disableMouse()
program.destroy()
setTimeout(() => {
const [text] = print(json, options)
console.log(text)
process.exit(0)
}, 10)
}
function getLine(y) {
const dy = box.childBase + y
const n = box.getNumber(dy)
const line = box.getScreenLine(dy)
if (typeof line === 'undefined') {
return [n, '']
}
return [n, line]
}
function apply(code) {
if (code && code.length !== 0) {
try {
json = reduce(source, code)
} catch (e) {
// pass
}
} else {
box.height = '100%'
input.hide()
json = source
}
box.focus()
program.cursorPos(0, 0)
render()
}
function complete(inputCode) {
const match = inputCode.match(/\.(\w*)$/)
const code = /^\.\w*$/.test(inputCode) ? '.' : inputCode.replace(/\.\w*$/, '')
let json
try {
json = reduce(source, code)
} catch (e) {
}
if (match) {
if (typeof json === 'object' && json.constructor === Object) {
const keys = Object.keys(json)
.filter(key => key.startsWith(match[1]))
.slice(0, 1000) // With lots of items, list takes forever to render.
// Hide if there is nothing to show or
// don't show if there is complete match.
if (keys.length === 0 || (keys.length === 1 && keys[0] === match[1])) {
autocomplete.hide()
return
}
autocomplete.width = Math.max(...keys.map(key => key.length)) + 1
autocomplete.height = Math.min(7, keys.length)
autocomplete.left = Math.min(
screen.width - autocomplete.width,
code.length === 1 ? 1 : code.length + 1
)
let selectFirst = autocomplete.items.length !== keys.length
autocomplete.setItems(keys)
if (selectFirst) {
autocomplete.select(autocomplete.items.length - 1)
}
if (autocomplete.hidden) {
autocomplete.show()
}
} else {
autocomplete.clearItems()
autocomplete.hide()
}
}
}
function update(code) {
if (code && code.length !== 0) {
try {
const pretender = reduce(source, code)
if (
typeof pretender !== 'undefined'
&& typeof pretender !== 'function'
&& !(pretender instanceof RegExp)
) {
json = pretender
}
} catch (e) {
// pass
}
}
if (code === '') {
json = source
}
if (highlight) {
findGen = find(json, highlight)
}
render()
}
function applyPattern(pattern) {
let regex
let m = pattern.match(/^\/(.*)\/([gimuy]*)$/)
if (m) {
try {
regex = new RegExp(m[1], m[2])
} catch (e) {
// Wrong regexp.
}
} else {
m = pattern.match(/^\/(.*)$/)
if (m) {
try {
regex = new RegExp(m[1], 'gi')
} catch (e) {
// Wrong regexp.
}
}
}
highlight = regex
search.hide()
if (highlight) {
findGen = find(json, highlight)
findNext()
} else {
findGen = null
currentPath = null
}
search.setValue('')
box.height = '100%'
box.focus()
program.cursorPos(0, 0)
render()
}
function findNext() {
if (!findGen) {
return
}
const {value: path, done} = findGen.next()
if (done) {
showStatusBar('Pattern not found')
} else {
currentPath = ''
for (let p of path) {
expanded.add(currentPath += p)
}
render()
for (let [k, v] of index) {
if (v === currentPath) {
let y = box.getScreenNumber(k)
// Scroll one line up for better view and make sure it's not negative.
if (--y < 0) {
y = 0
}
box.scrollTo(y)
screen.render()
}
}
// Set cursor to current path.
// We need timeout here to give our terminal some time.
// Without timeout first cursorPos call does not working,
// it looks like an ugly hack and it is an ugly hack.
setTimeout(() => {
for (let [k, v] of index) {
if (v === currentPath) {
let y = box.getScreenNumber(k) - box.childBase
if (y <= 0) {
y = 0
}
const line = box.getScreenLine(y + box.childBase)
program.cursorPos(y, line.search(/\S/))
}
}
}, 100)
}
}
function showStatusBar(status) {
statusBar.show()
statusBar.setContent(config.statusBar(` ${status} `))
screen.render()
}
function hideStatusBar() {
if (!statusBar.hidden) {
statusBar.hide()
statusBar.setContent('')
screen.render()
}
}
function render() {
let content
[content, index] = print(json, {expanded, highlight, currentPath})
if (typeof content === 'undefined') {
content = 'undefined'
}
box.setContent(content)
screen.render()
}
function exit() {
// If exit program immediately, stdin may still receive
// mouse events which will be printed in stdout.
program.disableMouse()
setTimeout(() => process.exit(0), 10)
}
render()
}
function* bfs(json) {
const queue = [[json, '']]
while (queue.length > 0) {
const [v, path] = queue.shift()
if (!v) {
continue
}
if (Array.isArray(v)) {
yield path
let i = 0
for (let item of v) {
const p = path + '[' + (i++) + ']'
queue.push([item, p])
}
}
if (typeof v === 'object' && v.constructor === Object) {
yield path
for (let [key, value] of Object.entries(v)) {
const p = path + '.' + key
queue.push([value, p])
}
}
}
}
function* dfs(v, path = '') {
if (!v) {
return
}
if (Array.isArray(v)) {
yield path
let i = 0
for (let item of v) {
yield* dfs(item, path + '[' + (i++) + ']')
}
}
if (typeof v === 'object' && v.constructor === Object) {
yield path
for (let [key, value] of Object.entries(v)) {
yield* dfs(value, path + '.' + key)
}
}
}

@ -1,33 +0,0 @@
module github.com/antonmedv/fx
go 1.17
require (
github.com/charmbracelet/bubbles v0.10.3
github.com/charmbracelet/bubbletea v0.20.0
github.com/charmbracelet/lipgloss v0.5.0
github.com/dop251/goja v0.0.0-20220501172647-e1eca0b61fa9
github.com/mattn/go-isatty v0.0.14
github.com/mazznoer/colorgrad v0.8.1
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739
github.com/stretchr/testify v1.7.1
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mazznoer/csscolorparser v0.1.2 // indirect
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)

@ -1,96 +0,0 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/charmbracelet/bubbles v0.10.3 h1:fKarbRaObLn/DCsZO4Y3vKCwRUzynQD9L+gGev1E/ho=
github.com/charmbracelet/bubbles v0.10.3/go.mod h1:jOA+DUF1rjZm7gZHcNyIVW+YrBPALKfpGVdJu8UiJsA=
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
github.com/charmbracelet/bubbletea v0.20.0 h1:/b8LEPgCbNr7WWZ2LuE/BV1/r4t5PyYJtDb+J3vpwxc=
github.com/charmbracelet/bubbletea v0.20.0/go.mod h1:zpkze1Rioo4rJELjRyGlm9T2YNou1Fm4LIJQSa5QMEM=
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DATR2/8t8=
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20220501172647-e1eca0b61fa9 h1:BXEAWJOT2C6ex9iOzVnrYWMFjTRccNs7p8fpLCLLcm0=
github.com/dop251/goja v0.0.0-20220501172647-e1eca0b61fa9/go.mod h1:TQJQ+ZNyFVvUtUEtCZxBhfWiH7RJqR3EivNmvD6Waik=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mazznoer/colorgrad v0.8.1 h1:Bw/ks+KujOOg9E6YQvPqSqTLryiFnwliAH5VMZarSTI=
github.com/mazznoer/colorgrad v0.8.1/go.mod h1:xCjvoNkXHJIAPOUMSMrXkFdxTGQqk8zMYS3e5hSLghA=
github.com/mazznoer/csscolorparser v0.1.0/go.mod h1:Aj22+L/rYN/Y6bj3bYqO3N6g1dtdHtGfQ32xZ5PJQic=
github.com/mazznoer/csscolorparser v0.1.2 h1:/UBHuQg792ePmGFzTQAC9u+XbFr7/HzP/Gj70Phyz2A=
github.com/mazznoer/csscolorparser v0.1.2/go.mod h1:Aj22+L/rYN/Y6bj3bYqO3N6g1dtdHtGfQ32xZ5PJQic=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

@ -1,71 +0,0 @@
package main
import (
"fmt"
"reflect"
"strings"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/lipgloss"
)
func usage(keyMap KeyMap) string {
title := lipgloss.NewStyle().Bold(true)
pad := lipgloss.NewStyle().PaddingLeft(4)
return fmt.Sprintf(`
%v
Terminal JSON viewer
%v
fx data.json
fx data.json .field
curl ... | fx
%v
-h, --help print help
-v, --version print version
--print-code print code of the reducer
%v
%v
%v
[https://fx.wtf]
`,
title.Render("fx "+version),
title.Render("Usage"),
title.Render("Flags"),
title.Render("Key Bindings"),
strings.Join(keyMapInfo(keyMap, pad), "\n"),
title.Render("More info"),
)
}
func keyMapInfo(keyMap KeyMap, style lipgloss.Style) []string {
v := reflect.ValueOf(keyMap)
fields := reflect.VisibleFields(v.Type())
keys := make([]string, 0)
for i := range fields {
k := v.Field(i).Interface().(key.Binding)
str := k.Help().Key
if width(str) == 0 {
str = strings.Join(k.Keys(), ", ")
}
keys = append(keys, fmt.Sprintf("%v ", str))
}
desc := make([]string, 0)
for i := range fields {
k := v.Field(i).Interface().(key.Binding)
desc = append(desc, fmt.Sprintf("%v", k.Help().Desc))
}
content := lipgloss.JoinHorizontal(
lipgloss.Top,
strings.Join(keys, "\n"),
strings.Join(desc, "\n"),
)
return strings.Split(style.Render(content), "\n")
}

@ -0,0 +1,177 @@
#!/usr/bin/env node
'use strict'
const os = require('os')
const fs = require('fs')
const path = require('path')
const JSON = require('lossless-json')
JSON.config({circularRefs: false})
const std = require('./std')
try {
require(path.join(os.homedir(), '.fxrc')) // Should be required before config.js usage.
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') {
throw err
}
}
const print = require('./print')
const reduce = require('./reduce')
const stream = require('./stream')
const usage = `
Usage
$ fx [code ...]
Examples
$ echo '{"key": "value"}' | fx 'x => x.key'
value
$ echo '{"key": "value"}' | fx .key
value
$ echo '[1,2,3]' | fx 'this.map(x => x * 2)'
[2, 4, 6]
$ echo '{"items": ["one", "two"]}' | fx 'this.items' 'this[1]'
two
$ echo '{"count": 0}' | fx '{...this, count: 1}'
{"count": 1}
$ echo '{"foo": 1, "bar": 2}' | fx ?
["foo", "bar"]
`
const {stdin, stdout, stderr} = process
const args = process.argv.slice(2)
void function main() {
stdin.setEncoding('utf8')
if (stdin.isTTY) {
handle('')
return
}
const reader = stream(stdin, apply)
stdin.on('readable', reader.read)
stdin.on('end', () => {
if (!reader.isStream()) {
handle(reader.value())
}
})
}()
function handle(input) {
let filename = 'fx'
if (input === '') {
if (args.length === 0 || (args.length === 1 && (args[0] === '-h' || args[0] === '--help'))) {
stderr.write(usage)
process.exit(2)
}
if (args.length === 1 && (args[0] === '-v' || args[0] === '--version')) {
stderr.write(require('./package.json').version + '\n')
process.exit(0)
}
if (args.length === 1 && args[0] === '--life') {
require('./bang')
return
}
input = fs.readFileSync(args[0]).toString('utf8')
filename = path.basename(args[0])
global.FX_FILENAME = filename
args.shift()
}
let json
try {
json = JSON.parse(input)
} catch (e) {
printError(e, input)
process.exit(1)
}
if (args.length === 0 && stdout.isTTY) {
require('./fx')(filename, json)
return
}
apply(json)
}
function apply(json) {
let output = json
for (let [i, code] of args.entries()) {
try {
output = reduce(output, code)
} catch (e) {
if (e === std.skip) {
return
}
stderr.write(`${snippet(i, code)}\n${e.stack || e}\n`)
process.exit(1)
}
}
if (typeof output === 'undefined') {
stderr.write('undefined\n')
} else if (typeof output === 'string') {
console.log(output)
} else {
const [text] = print(output)
console.log(text)
}
}
function snippet(i, code) {
let pre = args.slice(0, i).join(' ')
let post = args.slice(i + 1).join(' ')
if (pre.length > 20) {
pre = '...' + pre.substring(pre.length - 20)
}
if (post.length > 20) {
post = post.substring(0, 20) + '...'
}
const chalk = require('chalk')
return `\n ${pre} ${chalk.red.underline(code)} ${post}\n`
}
function printError(e, input) {
if (e.char) {
let lineNumber = 1, start = e.char - 70, end = e.char + 50
if (start < 0) start = 0
if (end > input.length) end = input.length
for (let i = 0; i < input.length && i < start; i++) {
if (input[i] === '\n') lineNumber++
}
let lines = input
.substring(start, end)
.split('\n')
if (lines.length > 1) {
lines = lines.slice(1)
lineNumber++
}
const chalk = require('chalk')
process.stderr.write(`\n`)
for (let line of lines) {
process.stderr.write(` ${chalk.yellow(lineNumber)} ${line}\n`)
lineNumber++
}
process.stderr.write(`\n`)
}
process.stderr.write(e.toString() + '\n')
}

@ -1,121 +0,0 @@
package main
import "github.com/charmbracelet/bubbles/key"
type KeyMap struct {
Quit key.Binding
Help key.Binding
PageDown key.Binding
PageUp key.Binding
HalfPageUp key.Binding
HalfPageDown key.Binding
GotoTop key.Binding
GotoBottom key.Binding
Down key.Binding
Up key.Binding
Expand key.Binding
Collapse key.Binding
ExpandRecursively key.Binding
CollapseRecursively key.Binding
ExpandAll key.Binding
CollapseAll key.Binding
NextSibling key.Binding
PrevSibling key.Binding
ToggleWrap key.Binding
Search key.Binding
Next key.Binding
Prev key.Binding
}
func DefaultKeyMap() KeyMap {
return KeyMap{
Quit: key.NewBinding(
key.WithKeys("q", "ctrl+c", "esc"),
key.WithHelp("", "exit program"),
),
Help: key.NewBinding(
key.WithKeys("?"),
key.WithHelp("", "show help"),
),
PageDown: key.NewBinding(
key.WithKeys("pgdown", " ", "f"),
key.WithHelp("pgdown, space, f", "page down"),
),
PageUp: key.NewBinding(
key.WithKeys("pgup", "b"),
key.WithHelp("pgup, b", "page up"),
),
HalfPageUp: key.NewBinding(
key.WithKeys("u", "ctrl+u"),
key.WithHelp("", "half page up"),
),
HalfPageDown: key.NewBinding(
key.WithKeys("d", "ctrl+d"),
key.WithHelp("", "half page down"),
),
GotoTop: key.NewBinding(
key.WithKeys("g"),
key.WithHelp("", "goto top"),
),
GotoBottom: key.NewBinding(
key.WithKeys("G"),
key.WithHelp("", "goto bottom"),
),
Down: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("", "down"),
),
Up: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("", "up"),
),
Expand: key.NewBinding(
key.WithKeys("right", "l"),
key.WithHelp("", "expand"),
),
Collapse: key.NewBinding(
key.WithKeys("left", "h"),
key.WithHelp("", "collapse"),
),
ExpandRecursively: key.NewBinding(
key.WithKeys("L"),
key.WithHelp("", "expand recursively"),
),
CollapseRecursively: key.NewBinding(
key.WithKeys("H"),
key.WithHelp("", "collapse recursively"),
),
ExpandAll: key.NewBinding(
key.WithKeys("e"),
key.WithHelp("", "expand all"),
),
CollapseAll: key.NewBinding(
key.WithKeys("E"),
key.WithHelp("", "collapse all"),
),
NextSibling: key.NewBinding(
key.WithKeys("J"),
key.WithHelp("", "next sibling"),
),
PrevSibling: key.NewBinding(
key.WithKeys("K"),
key.WithHelp("", "previous sibling"),
),
ToggleWrap: key.NewBinding(
key.WithKeys("z"),
key.WithHelp("", "toggle strings wrap"),
),
Search: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("", "search regexp"),
),
Next: key.NewBinding(
key.WithKeys("n"),
key.WithHelp("", "next search result"),
),
Prev: key.NewBinding(
key.WithKeys("N"),
key.WithHelp("", "prev search result"),
),
}
}

@ -1,634 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"io/fs"
"os"
"path"
"runtime/pprof"
"strings"
. "github.com/antonmedv/fx/pkg/dict"
. "github.com/antonmedv/fx/pkg/json"
. "github.com/antonmedv/fx/pkg/reducer"
. "github.com/antonmedv/fx/pkg/theme"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/mattn/go-isatty"
"github.com/muesli/termenv"
)
var (
flagHelp bool
flagVersion bool
flagPrintCode bool
)
func main() {
var args []string
for _, arg := range os.Args[1:] {
switch arg {
case "-h", "--help":
flagHelp = true
case "-v", "-V", "--version":
flagVersion = true
case "--print-code":
flagPrintCode = true
default:
args = append(args, arg)
}
}
if flagHelp {
fmt.Println(usage(DefaultKeyMap()))
return
}
if flagVersion {
fmt.Println(version)
return
}
cpuProfile := os.Getenv("CPU_PROFILE")
if cpuProfile != "" {
f, err := os.Create(cpuProfile)
if err != nil {
panic(err)
}
err = pprof.StartCPUProfile(f)
if err != nil {
panic(err)
}
}
themeId, ok := os.LookupEnv("FX_THEME")
if !ok {
themeId = "1"
}
theme, ok := Themes[themeId]
if !ok {
theme = Themes["1"]
}
if termenv.ColorProfile() == termenv.Ascii {
theme = Themes["0"]
}
var showSize bool
if s, ok := os.LookupEnv("FX_SHOW_SIZE"); ok {
if s == "true" {
showSize = true
}
}
stdinIsTty := isatty.IsTerminal(os.Stdin.Fd())
stdoutIsTty := isatty.IsTerminal(os.Stdout.Fd())
filePath := ""
fileName := ""
var dec *json.Decoder
if stdinIsTty {
// Nothing was piped, maybe file argument?
if len(args) >= 1 {
filePath = args[0]
f, err := os.Open(filePath)
if err != nil {
switch err.(type) {
case *fs.PathError:
fmt.Println(err)
os.Exit(1)
default:
panic(err)
}
}
fileName = path.Base(filePath)
dec = json.NewDecoder(f)
args = args[1:]
}
} else {
dec = json.NewDecoder(os.Stdin)
}
if dec == nil {
fmt.Println(usage(DefaultKeyMap()))
os.Exit(1)
}
dec.UseNumber()
object, err := Parse(dec)
if err != nil {
fmt.Println("JSON Parse Error:", err.Error())
os.Exit(1)
}
lang, ok := os.LookupEnv("FX_LANG")
if !ok {
lang = "js"
}
var fxrc string
if lang == "js" || lang == "node" {
home, err := os.UserHomeDir()
if err == nil {
b, err := os.ReadFile(path.Join(home, ".fxrc.js"))
if err == nil {
fxrc = "\n" + string(b)
if lang == "js" {
parts := strings.SplitN(fxrc, "// nodejs:", 2)
fxrc = parts[0]
}
}
}
}
if dec.More() {
os.Exit(stream(dec, object, lang, args, theme, fxrc))
}
if len(args) > 0 || !stdoutIsTty {
if len(args) > 0 && flagPrintCode {
fmt.Print(GenerateCode(lang, args, fxrc))
return
}
if lang == "js" {
simplePath, ok := SplitSimplePath(args)
if ok {
output := GetBySimplePath(object, simplePath)
Echo(output, theme)
os.Exit(0)
}
vm, fn, err := CreateJS(args, fxrc)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
os.Exit(ReduceJS(vm, fn, object, theme))
} else {
os.Exit(Reduce(object, lang, args, theme, fxrc))
}
}
// Start interactive mode.
expand := map[string]bool{"": true}
if array, ok := object.(Array); ok {
for i := range array {
expand[accessor("", i)] = true
}
}
parents := map[string]string{}
children := map[string][]string{}
canBeExpanded := map[string]bool{}
Dfs(object, func(it Iterator) {
parents[it.Path] = it.Parent
children[it.Parent] = append(children[it.Parent], it.Path)
switch it.Object.(type) {
case *Dict:
canBeExpanded[it.Path] = len(it.Object.(*Dict).Keys) > 0
case Array:
canBeExpanded[it.Path] = len(it.Object.(Array)) > 0
}
})
input := textinput.New()
input.Prompt = ""
m := &model{
fileName: fileName,
theme: theme,
json: object,
showSize: showSize,
width: 80,
height: 60,
mouseWheelDelta: 3,
keyMap: DefaultKeyMap(),
expandedPaths: expand,
canBeExpanded: canBeExpanded,
parents: parents,
children: children,
nextSiblings: map[string]string{},
prevSiblings: map[string]string{},
wrap: true,
searchInput: input,
}
m.collectSiblings(m.json, "")
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
if err := p.Start(); err != nil {
panic(err)
}
if cpuProfile != "" {
pprof.StopCPUProfile()
}
os.Exit(m.exitCode)
}
type model struct {
exitCode int
width, height int
windowHeight int
footerHeight int
wrap bool
theme Theme
showSize bool // Show number of elements in preview
fileName string
json interface{}
lines []string
mouseWheelDelta int // Number of lines the mouse wheel will scroll
offset int // offset is the vertical scroll position
keyMap KeyMap
showHelp bool
expandedPaths map[string]bool // set of expanded paths
canBeExpanded map[string]bool // set of path => can be expanded (i.e. dict or array)
paths []string // array of paths on screen
pathToLineNumber map[string]int // map of path => line Number
pathToIndex map[string]int // map of path => index in m.paths
lineNumberToPath map[int]string // map of line Number => path
parents map[string]string // map of subpath => parent path
children map[string][]string // map of path => child paths
nextSiblings, prevSiblings map[string]string // map of path => sibling path
cursor int // cursor in [0, len(m.paths)]
showCursor bool
searchInput textinput.Model
searchRegexCompileError string
showSearchResults bool
searchResults []*searchResult
searchResultsCursor int
highlightIndex map[string]*rangeGroup
}
func (m *model) Init() tea.Cmd {
return nil
}
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
m.windowHeight = msg.Height
m.searchInput.Width = msg.Width - 2 // minus prompt
m.render()
case tea.MouseMsg:
switch msg.Type {
case tea.MouseWheelUp:
m.LineUp(m.mouseWheelDelta)
case tea.MouseWheelDown:
m.LineDown(m.mouseWheelDelta)
}
}
if m.searchInput.Focused() {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyEsc:
m.searchInput.Blur()
m.clearSearchResults()
m.render()
case tea.KeyEnter:
m.doSearch(m.searchInput.Value())
}
}
var cmd tea.Cmd
m.searchInput, cmd = m.searchInput.Update(msg)
return m, cmd
}
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keyMap.PageDown):
m.ViewDown()
case key.Matches(msg, m.keyMap.PageUp):
m.ViewUp()
case key.Matches(msg, m.keyMap.HalfPageDown):
m.HalfViewDown()
case key.Matches(msg, m.keyMap.HalfPageUp):
m.HalfViewUp()
case key.Matches(msg, m.keyMap.GotoTop):
m.GotoTop()
case key.Matches(msg, m.keyMap.GotoBottom):
m.GotoBottom()
}
}
if m.showHelp {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keyMap.Quit):
m.showHelp = false
m.render()
case key.Matches(msg, m.keyMap.Down):
m.LineDown(1)
case key.Matches(msg, m.keyMap.Up):
m.LineUp(1)
}
}
return m, nil
}
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keyMap.Quit):
m.exitCode = 0
return m, tea.Quit
case key.Matches(msg, m.keyMap.Help):
m.GotoTop()
m.showHelp = !m.showHelp
m.render()
case key.Matches(msg, m.keyMap.Down):
m.down()
m.render()
m.scrollDownToCursor()
case key.Matches(msg, m.keyMap.Up):
m.up()
m.render()
m.scrollUpToCursor()
case key.Matches(msg, m.keyMap.NextSibling):
nextSiblingPath, ok := m.nextSiblings[m.cursorPath()]
if ok {
m.showCursor = true
m.cursor = m.pathToIndex[nextSiblingPath]
} else {
m.down()
}
m.render()
m.scrollDownToCursor()
case key.Matches(msg, m.keyMap.PrevSibling):
prevSiblingPath, ok := m.prevSiblings[m.cursorPath()]
if ok {
m.showCursor = true
m.cursor = m.pathToIndex[prevSiblingPath]
} else {
m.up()
}
m.render()
m.scrollUpToCursor()
case key.Matches(msg, m.keyMap.Expand):
m.showCursor = true
if m.canBeExpanded[m.cursorPath()] {
m.expandedPaths[m.cursorPath()] = true
}
m.render()
case key.Matches(msg, m.keyMap.ExpandRecursively):
m.showCursor = true
if m.canBeExpanded[m.cursorPath()] {
m.expandRecursively(m.cursorPath())
}
m.render()
case key.Matches(msg, m.keyMap.Collapse):
m.showCursor = true
if m.canBeExpanded[m.cursorPath()] && m.expandedPaths[m.cursorPath()] {
m.expandedPaths[m.cursorPath()] = false
} else {
parentPath, ok := m.parents[m.cursorPath()]
if ok {
m.expandedPaths[parentPath] = false
m.cursor = m.pathToIndex[parentPath]
}
}
m.render()
m.scrollUpToCursor()
case key.Matches(msg, m.keyMap.CollapseRecursively):
m.showCursor = true
if m.canBeExpanded[m.cursorPath()] && m.expandedPaths[m.cursorPath()] {
m.collapseRecursively(m.cursorPath())
} else {
parentPath, ok := m.parents[m.cursorPath()]
if ok {
m.collapseRecursively(parentPath)
m.cursor = m.pathToIndex[parentPath]
}
}
m.render()
m.scrollUpToCursor()
case key.Matches(msg, m.keyMap.ToggleWrap):
m.wrap = !m.wrap
m.render()
case key.Matches(msg, m.keyMap.ExpandAll):
Dfs(m.json, func(it Iterator) {
switch it.Object.(type) {
case *Dict, Array:
m.expandedPaths[it.Path] = true
}
})
m.render()
case key.Matches(msg, m.keyMap.CollapseAll):
m.expandedPaths = map[string]bool{
"": true,
}
m.render()
case key.Matches(msg, m.keyMap.Search):
m.showSearchResults = false
m.searchRegexCompileError = ""
m.searchInput.Focus()
m.render()
return m, textinput.Blink
case key.Matches(msg, m.keyMap.Next):
if m.showSearchResults {
m.nextSearchResult()
}
case key.Matches(msg, m.keyMap.Prev):
if m.showSearchResults {
m.prevSearchResult()
}
}
case tea.MouseMsg:
switch msg.Type {
case tea.MouseLeft:
m.showCursor = true
if msg.Y >= m.height {
// Clicked on status bar or search input.
break
}
clickedPath, ok := m.lineNumberToPath[m.offset+msg.Y]
if ok {
if m.canBeExpanded[clickedPath] {
m.expandedPaths[clickedPath] = !m.expandedPaths[clickedPath]
}
m.cursor = m.pathToIndex[clickedPath]
m.render()
}
}
}
return m, nil
}
func (m *model) View() string {
lines := m.visibleLines()
extraLines := ""
if len(lines) < m.height {
extraLines = strings.Repeat("\n", max(0, m.height-len(lines)))
}
if m.showHelp {
statusBar := "Press Esc or q to close help."
statusBar += strings.Repeat(" ", max(0, m.width-width(statusBar)))
statusBar = m.theme.StatusBar(statusBar)
return strings.Join(lines, "\n") + extraLines + "\n" + statusBar
}
statusBar := m.cursorPath() + " "
statusBar += strings.Repeat(" ", max(0, m.width-width(statusBar)-width(m.fileName)))
statusBar += m.fileName
statusBar = m.theme.StatusBar(statusBar)
output := strings.Join(lines, "\n") + extraLines + "\n" + statusBar
if m.searchInput.Focused() {
output += "\n/" + m.searchInput.View()
}
if len(m.searchRegexCompileError) > 0 {
output += fmt.Sprintf("\n/%v/i %v", m.searchInput.Value(), m.searchRegexCompileError)
}
if m.showSearchResults {
if len(m.searchResults) == 0 {
output += fmt.Sprintf("\n/%v/i not found", m.searchInput.Value())
} else {
output += fmt.Sprintf("\n/%v/i found: [%v/%v]", m.searchInput.Value(), m.searchResultsCursor+1, len(m.searchResults))
}
}
return output
}
func (m *model) recalculateViewportHeight() {
m.height = m.windowHeight
m.height-- // status bar
if !m.showHelp {
if m.searchInput.Focused() {
m.height--
}
if m.showSearchResults {
m.height--
}
if len(m.searchRegexCompileError) > 0 {
m.height--
}
}
}
func (m *model) render() {
m.recalculateViewportHeight()
if m.showHelp {
m.lines = keyMapInfo(m.keyMap, lipgloss.NewStyle().PaddingLeft(4).PaddingTop(2).PaddingBottom(2))
return
}
m.paths = make([]string, 0)
m.pathToIndex = make(map[string]int, 0)
if m.pathToLineNumber == nil {
m.pathToLineNumber = make(map[string]int, 0)
} else {
m.pathToLineNumber = make(map[string]int, len(m.pathToLineNumber))
}
if m.lineNumberToPath == nil {
m.lineNumberToPath = make(map[int]string, 0)
} else {
m.lineNumberToPath = make(map[int]string, len(m.lineNumberToPath))
}
m.lines = m.print(m.json, 1, 0, 0, "", true)
if m.offset > len(m.lines)-1 {
m.GotoBottom()
}
}
func (m *model) cursorPath() string {
if m.cursor == 0 {
return ""
}
if 0 <= m.cursor && m.cursor < len(m.paths) {
return m.paths[m.cursor]
}
return "?"
}
func (m *model) cursorLineNumber() int {
if 0 <= m.cursor && m.cursor < len(m.paths) {
return m.pathToLineNumber[m.paths[m.cursor]]
}
return -1
}
func (m *model) expandRecursively(path string) {
if m.canBeExpanded[path] {
m.expandedPaths[path] = true
for _, childPath := range m.children[path] {
if childPath != "" {
m.expandRecursively(childPath)
}
}
}
}
func (m *model) collapseRecursively(path string) {
m.expandedPaths[path] = false
for _, childPath := range m.children[path] {
if childPath != "" {
m.collapseRecursively(childPath)
}
}
}
func (m *model) collectSiblings(v interface{}, path string) {
switch v.(type) {
case *Dict:
prev := ""
for _, k := range v.(*Dict).Keys {
subpath := path + "." + k
if prev != "" {
m.nextSiblings[prev] = subpath
m.prevSiblings[subpath] = prev
}
prev = subpath
value, _ := v.(*Dict).Get(k)
m.collectSiblings(value, subpath)
}
case Array:
prev := ""
for i, value := range v.(Array) {
subpath := fmt.Sprintf("%v[%v]", path, i)
if prev != "" {
m.nextSiblings[prev] = subpath
m.prevSiblings[subpath] = prev
}
prev = subpath
m.collectSiblings(value, subpath)
}
}
}
func (m *model) down() {
m.showCursor = true
if m.cursor < len(m.paths)-1 { // scroll till last element in m.paths
m.cursor++
} else {
// at the bottom of viewport maybe some hidden brackets, lets scroll to see them
if !m.AtBottom() {
m.LineDown(1)
}
}
if m.cursor >= len(m.paths) {
m.cursor = len(m.paths) - 1
}
}
func (m *model) up() {
m.showCursor = true
if m.cursor > 0 {
m.cursor--
}
if m.cursor >= len(m.paths) {
m.cursor = len(m.paths) - 1
}
}

@ -0,0 +1,53 @@
{
"name": "fx",
"version": "20.0.2",
"description": "Command-line JSON viewer",
"repository": "antonmedv/fx",
"homepage": "https://fx.wtf",
"author": "Anton Medvedev <anton@medv.io>",
"license": "MIT",
"bin": {
"fx": "index.js"
},
"files": [
"*.js"
],
"scripts": {
"test": "ava",
"build": "pkg . --out-path dist",
"zip": "cd dist && find . -name 'fx-*' -exec zip '{}.zip' '{}' \\;",
"release": "release-it --github.release --github.assets=dist/*.zip",
"brew:release": "brew bump-formula-pr --url=https://registry.npmjs.org/fx/-/fx-`fx package.json .version`.tgz --no-audit fx"
},
"release-it": {
"hooks": {
"after:bump": "rm -rf ./dist && npm run build && npm run zip"
}
},
"keywords": [
"json",
"viewer",
"cli",
"terminal",
"term",
"console",
"ascii",
"unicode",
"blessed"
],
"engines": {
"node": ">=8"
},
"dependencies": {
"@medv/blessed": "^2.0.1",
"chalk": "^4.1.0",
"indent-string": "^4.0.0",
"lossless-json": "^1.0.4",
"string-width": "^4.2.0"
},
"devDependencies": {
"ava": "^3.12.1",
"pkg": "^4.4.9",
"release-it": "^14.0.1"
}
}

@ -1,26 +0,0 @@
package dict
type Dict struct {
Keys []string
Values map[string]interface{}
}
func NewDict() *Dict {
return &Dict{
Keys: make([]string, 0),
Values: make(map[string]interface{}),
}
}
func (d *Dict) Get(key string) (interface{}, bool) {
val, exists := d.Values[key]
return val, exists
}
func (d *Dict) Set(key string, value interface{}) {
_, exists := d.Values[key]
if !exists {
d.Keys = append(d.Keys, key)
}
d.Values[key] = value
}

@ -1,65 +0,0 @@
package dict
import "testing"
func Test_dict(t *testing.T) {
d := NewDict()
d.Set("number", 3)
v, _ := d.Get("number")
if v.(int) != 3 {
t.Error("Set number")
}
// string
d.Set("string", "x")
v, _ = d.Get("string")
if v.(string) != "x" {
t.Error("Set string")
}
// string slice
d.Set("strings", []string{
"t",
"u",
})
v, _ = d.Get("strings")
if v.([]string)[0] != "t" {
t.Error("Set strings first index")
}
if v.([]string)[1] != "u" {
t.Error("Set strings second index")
}
// mixed slice
d.Set("mixed", []interface{}{
1,
"1",
})
v, _ = d.Get("mixed")
if v.([]interface{})[0].(int) != 1 {
t.Error("Set mixed int")
}
if v.([]interface{})[1].(string) != "1" {
t.Error("Set mixed string")
}
// overriding existing key
d.Set("number", 4)
v, _ = d.Get("number")
if v.(int) != 4 {
t.Error("Override existing key")
}
// Keys
expectedKeys := []string{
"number",
"string",
"strings",
"mixed",
}
for i, key := range d.Keys {
if key != expectedKeys[i] {
t.Error("Keys method", key, "!=", expectedKeys[i])
}
}
for i, key := range expectedKeys {
if key != expectedKeys[i] {
t.Error("Keys method", key, "!=", expectedKeys[i])
}
}
}

@ -1,86 +0,0 @@
package json
import (
"encoding/json"
. "github.com/antonmedv/fx/pkg/dict"
)
func Parse(dec *json.Decoder) (interface{}, error) {
token, err := dec.Token()
if err != nil {
return nil, err
}
if delim, ok := token.(json.Delim); ok {
switch delim {
case '{':
return decodeDict(dec)
case '[':
return decodeArray(dec)
}
}
return token, nil
}
func decodeDict(dec *json.Decoder) (*Dict, error) {
d := NewDict()
for {
token, err := dec.Token()
if err != nil {
return nil, err
}
if delim, ok := token.(json.Delim); ok && delim == '}' {
return d, nil
}
key := token.(string)
token, err = dec.Token()
if err != nil {
return nil, err
}
var value interface{} = token
if delim, ok := token.(json.Delim); ok {
switch delim {
case '{':
value, err = decodeDict(dec)
if err != nil {
return nil, err
}
case '[':
value, err = decodeArray(dec)
if err != nil {
return nil, err
}
}
}
d.Set(key, value)
}
}
func decodeArray(dec *json.Decoder) ([]interface{}, error) {
slice := make(Array, 0)
for index := 0; ; index++ {
token, err := dec.Token()
if err != nil {
return nil, err
}
if delim, ok := token.(json.Delim); ok {
switch delim {
case '{':
value, err := decodeDict(dec)
if err != nil {
return nil, err
}
slice = append(slice, value)
case '[':
value, err := decodeArray(dec)
if err != nil {
return nil, err
}
slice = append(slice, value)
case ']':
return slice, nil
}
continue
}
slice = append(slice, token)
}
}

@ -1,51 +0,0 @@
package json
import (
"encoding/json"
. "github.com/antonmedv/fx/pkg/dict"
"strings"
"testing"
)
func Test_parse(t *testing.T) {
input := `{
"a": 1,
"b": 2,
"a": 3,
"slice": [{"z": "z", "1": "1"}]
}`
p, err := Parse(json.NewDecoder(strings.NewReader(input)))
if err != nil {
t.Error("JSON parse error", err)
}
o := p.(*Dict)
expectedKeys := []string{
"a",
"b",
"slice",
}
for i := range o.Keys {
if o.Keys[i] != expectedKeys[i] {
t.Error("Wrong key order ", i, o.Keys[i], "!=", expectedKeys[i])
}
}
s, ok := o.Get("slice")
if !ok {
t.Error("slice missing")
}
a := s.(Array)
z := a[0].(*Dict)
expectedKeys = []string{
"z",
"1",
}
for i := range z.Keys {
if z.Keys[i] != expectedKeys[i] {
t.Error("Wrong key order for nested map ", i, z.Keys[i], "!=", expectedKeys[i])
}
}
}

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

@ -1,51 +0,0 @@
package json
import (
"fmt"
. "github.com/antonmedv/fx/pkg/dict"
)
func Stringify(v interface{}) string {
switch v := v.(type) {
case nil:
return "null"
case bool:
if v {
return "true"
} else {
return "false"
}
case Number:
return v.String()
case string:
return fmt.Sprintf("%q", v)
case *Dict:
result := "{"
for i, key := range v.Keys {
line := fmt.Sprintf("%q", key) + ": " + Stringify(v.Values[key])
if i < len(v.Keys)-1 {
line += ","
}
result += line
}
return result + "}"
case Array:
result := "["
for i, value := range v {
line := Stringify(value)
if i < len(v)-1 {
line += ","
}
result += line
}
return result + "]"
default:
return "unknown type"
}
}

@ -1,32 +0,0 @@
package json
import (
. "github.com/antonmedv/fx/pkg/dict"
"testing"
)
func Test_stringify(t *testing.T) {
t.Run("dict", func(t *testing.T) {
arg := NewDict()
arg.Set("a", Number("1"))
arg.Set("b", Number("2"))
want := `{"a": 1,"b": 2}`
if got := Stringify(arg); got != want {
t.Errorf("stringify() = %v, want %v", got, want)
}
})
t.Run("array", func(t *testing.T) {
arg := Array{Number("1"), Number("2")}
want := `[1,2]`
if got := Stringify(arg); got != want {
t.Errorf("stringify() = %v, want %v", got, want)
}
})
t.Run("array_with_dict", func(t *testing.T) {
arg := Array{NewDict(), Array{}}
want := `[{},[]]`
if got := Stringify(arg); got != want {
t.Errorf("stringify() = %v, want %v", got, want)
}
})
}

@ -1,47 +0,0 @@
package json
import (
"fmt"
. "github.com/antonmedv/fx/pkg/dict"
)
type Iterator struct {
Object interface{}
Path, Parent string
}
func Dfs(object interface{}, f func(it Iterator)) {
sub(Iterator{Object: object}, f)
}
func sub(it Iterator, f func(it Iterator)) {
f(it)
switch it.Object.(type) {
case *Dict:
keys := it.Object.(*Dict).Keys
for _, k := range keys {
subpath := it.Path + "." + k
value, _ := it.Object.(*Dict).Get(k)
sub(Iterator{
Object: value,
Path: subpath,
Parent: it.Path,
}, f)
}
case Array:
slice := it.Object.(Array)
for i, value := range slice {
subpath := accessor(it.Path, i)
sub(Iterator{
Object: value,
Path: subpath,
Parent: it.Path,
}, f)
}
}
}
func accessor(path string, to interface{}) string {
return fmt.Sprintf("%v[%v]", path, to)
}

@ -1,6 +0,0 @@
package json
import "encoding/json"
type Number = json.Number
type Array = []interface{}

@ -1,98 +0,0 @@
package reducer
import (
_ "embed"
"encoding/json"
"fmt"
"strings"
. "github.com/antonmedv/fx/pkg/json"
. "github.com/antonmedv/fx/pkg/theme"
"github.com/dop251/goja"
)
//go:embed js.js
var templateJs string
func js(args []string, fxrc string) string {
rs := "\n"
for i, a := range args {
rs += " try {"
switch {
case flatMapRegex.MatchString(a):
code := fold(strings.Split(a, "[]"))
rs += fmt.Sprintf(
`
x = (
%v
)(x)
`, code)
case strings.HasPrefix(a, ".["):
rs += fmt.Sprintf(
`
x = function ()
{ return this%v }
.call(x)
`, a[1:])
case strings.HasPrefix(a, "."):
rs += fmt.Sprintf(
`
x = function ()
{ return this%v }
.call(x)
`, a)
default:
rs += fmt.Sprintf(
`
let f = function ()
{ return %v }
.call(x)
x = typeof f === 'function' ? f(x) : f
`, a)
}
// Generate a beautiful error message.
rs += " } catch (e) {\n"
pre, post, pointer := trace(args, i)
rs += fmt.Sprintf(
" throw `\\n ${%q} ${%q} ${%q}\\n %v\\n\\n${e.stack || e}`\n",
pre, a, post, pointer,
)
rs += " }\n"
}
return fmt.Sprintf(templateJs, fxrc, rs)
}
func CreateJS(args []string, fxrc string) (*goja.Runtime, goja.Callable, error) {
vm := goja.New()
_, err := vm.RunString(js(args, fxrc))
if err != nil {
return nil, nil, err
}
fn, ok := goja.AssertFunction(vm.Get("reduce"))
if !ok {
panic("Not a function")
}
return vm, fn, nil
}
func ReduceJS(vm *goja.Runtime, reduce goja.Callable, input interface{}, theme Theme) int {
value, err := reduce(goja.Undefined(), vm.ToValue(Stringify(input)))
if err != nil {
fmt.Println(err)
return 1
}
output := value.String()
dec := json.NewDecoder(strings.NewReader(output))
dec.UseNumber()
object, err := Parse(dec)
if err != nil {
fmt.Print(output)
return 0
}
Echo(object, theme)
return 0
}

@ -1,12 +0,0 @@
// .fxrc.js %v
function reduce(input) {
let x = JSON.parse(input)
// Reducers %v
if (typeof x === 'undefined') {
return 'null'
} else {
return JSON.stringify(x)
}
}

@ -1,83 +0,0 @@
package reducer
import (
_ "embed"
"fmt"
"os"
"os/exec"
"path"
"strings"
)
func CreateNodejs(args []string, fxrc string) *exec.Cmd {
cmd := exec.Command("node", "--input-type=module", "-e", nodejs(args, fxrc))
nodePath, exist := os.LookupEnv("NODE_PATH")
if exist {
cmd.Dir = path.Dir(nodePath)
}
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "NODE_OPTIONS=--max-old-space-size=8192")
workingDir, err := os.Getwd()
if err == nil {
cmd.Env = append(cmd.Env, "FX_CWD="+workingDir)
}
return cmd
}
//go:embed node.js
var templateNode string
func nodejs(args []string, fxrc string) string {
rs := "\n"
for i, a := range args {
rs += " try {"
switch {
case flatMapRegex.MatchString(a):
code := fold(strings.Split(a, "[]"))
rs += fmt.Sprintf(
`
x = (
%v
)(x)
`, code)
case strings.HasPrefix(a, ".["):
rs += fmt.Sprintf(
`
x = function ()
{ return this%v }
.call(x)
`, a[1:])
case strings.HasPrefix(a, "."):
rs += fmt.Sprintf(
`
x = function ()
{ return this%v }
.call(x)
`, a)
default:
rs += fmt.Sprintf(
`
let f = function ()
{ return %v }
.call(x)
x = typeof f === 'function' ? f(x) : f
`, a)
}
rs += `
x = await x
`
// Generate a beautiful error message.
rs += " } catch (e) {\n"
pre, post, pointer := trace(args, i)
rs += fmt.Sprintf(
" throw `\\n ${%q} ${%q} ${%q}\\n %v\\n\\n${e.stack || e}`\n",
pre, a, post, pointer,
)
rs += " }\n"
}
return fmt.Sprintf(templateNode, fxrc, rs)
}

@ -1,31 +0,0 @@
import os from 'node:os'
import fs from 'node:fs'
import path from 'node:path'
import {createRequire} from 'node:module'
const cwd = process.env.FX_CWD ? process.env.FX_CWD : process.cwd()
const require = createRequire(cwd)
// .fxrc.js %v
void async function () {
process.chdir(cwd)
let buffer = ''
process.stdin.setEncoding('utf8')
for await (let chunk of process.stdin) {
buffer += chunk
}
let x = JSON.parse(buffer)
// Reducers %v
if (typeof x === 'undefined') {
process.stderr.write('undefined')
} else {
process.stdout.write(JSON.stringify(x))
}
}().catch(err => {
console.error(err)
process.exitCode = 1
})

@ -1,38 +0,0 @@
package reducer
import (
_ "embed"
"fmt"
"os/exec"
)
func CreatePython(bin string, args []string) *exec.Cmd {
cmd := exec.Command(bin, "-c", python(args))
return cmd
}
//go:embed python.py
var templatePython string
func python(args []string) string {
rs := "\n"
for i, a := range args {
rs += fmt.Sprintf(
`try:
f = (lambda x: (%v))(x)
x = f(x) if callable(f) else f
`, a)
// Generate a beautiful error message.
rs += "except Exception as e:\n"
pre, post, pointer := trace(args, i)
rs += fmt.Sprintf(
` sys.stderr.write('\n {} {} {}\n %v\n\n{}\n'.format(%q, %q, %q, e))
sys.exit(1)`,
pointer,
pre, a, post,
)
rs += "\n"
}
return fmt.Sprintf(templatePython, rs)
}

@ -1,9 +0,0 @@
import json, sys, os
x = json.load(sys.stdin)
# Reducers %v
try:
print(json.dumps(x))
except:
print(json.dumps(list(x)))

@ -1,76 +0,0 @@
package reducer
import (
"bytes"
_ "embed"
"encoding/json"
"fmt"
"os/exec"
"strings"
. "github.com/antonmedv/fx/pkg/json"
. "github.com/antonmedv/fx/pkg/theme"
)
func GenerateCode(lang string, args []string, fxrc string) string {
switch lang {
case "js":
return js(args, fxrc)
case "node":
return nodejs(args, fxrc)
case "python", "python3":
return python(args)
case "ruby":
return ruby(args)
default:
panic("unknown lang")
}
}
func Reduce(input interface{}, lang string, args []string, theme Theme, fxrc string) int {
path, ok := SplitSimplePath(args)
if ok {
output := GetBySimplePath(input, path)
Echo(output, theme)
return 0
}
var cmd *exec.Cmd
switch lang {
case "node":
cmd = CreateNodejs(args, fxrc)
case "python", "python3":
cmd = CreatePython(lang, args)
case "ruby":
cmd = CreateRuby(args)
default:
panic("unknown lang")
}
// TODO: Reimplement stringify with io.Reader.
cmd.Stdin = strings.NewReader(Stringify(input))
output, err := cmd.CombinedOutput()
if err != nil {
exitCode := 1
status, ok := err.(*exec.ExitError)
if ok {
exitCode = status.ExitCode()
} else {
fmt.Println(err.Error())
}
fmt.Print(string(output))
return exitCode
}
dec := json.NewDecoder(bytes.NewReader(output))
dec.UseNumber()
object, err := Parse(dec)
if err != nil {
fmt.Print(string(output))
return 0
}
Echo(object, theme)
if dec.InputOffset() < int64(len(output)) {
fmt.Print(string(output[dec.InputOffset():]))
}
return 0
}

@ -1,37 +0,0 @@
package reducer
import (
_ "embed"
"fmt"
"os/exec"
)
func CreateRuby(args []string) *exec.Cmd {
cmd := exec.Command("ruby", "-e", ruby(args))
return cmd
}
//go:embed ruby.rb
var templateRuby string
func ruby(args []string) string {
rs := "\n"
for i, a := range args {
rs += fmt.Sprintf(
`begin
x = lambda {|x| %v }.call(x)
`, a)
// Generate a beautiful error message.
rs += "rescue Exception => e\n"
pre, post, pointer := trace(args, i)
rs += fmt.Sprintf(
` STDERR.puts "\n #{%q} #{%q} #{%q}\n %v\n\n#{e}\n"
exit(1)
`,
pre, a, post,
pointer,
)
rs += "end\n"
}
return fmt.Sprintf(templateRuby, rs)
}

@ -1,6 +0,0 @@
require 'json'
x = JSON.parse(STDIN.read)
# Reducers %v
puts JSON.generate(x)

@ -1,215 +0,0 @@
package reducer
import (
"strconv"
"unicode"
. "github.com/antonmedv/fx/pkg/dict"
. "github.com/antonmedv/fx/pkg/json"
)
type state int
const (
start state = iota
unknown
propOrIndex
prop
index
indexEnd
number
doubleQuote
doubleQuoteEscape
singleQuote
singleQuoteEscape
)
func SplitSimplePath(args []string) ([]interface{}, bool) {
path := make([]interface{}, 0)
for _, arg := range args {
s := ""
state := start
for _, ch := range arg {
switch state {
case start:
switch {
case ch == 'x':
state = unknown
case ch == '.':
state = propOrIndex
default:
return path, false
}
case unknown:
switch {
case ch == '.':
state = prop
s = ""
case ch == '[':
state = index
s = ""
default:
return path, false
}
case propOrIndex:
switch {
case isProp(ch):
state = prop
s = string(ch)
case ch == '[':
state = index
default:
return path, false
}
case prop:
switch {
case isProp(ch):
s += string(ch)
case ch == '.':
state = prop
path = append(path, s)
s = ""
case ch == '[':
state = index
path = append(path, s)
s = ""
default:
return path, false
}
case index:
switch {
case unicode.IsDigit(ch):
state = number
s = string(ch)
case ch == '"':
state = doubleQuote
s = ""
case ch == '\'':
state = singleQuote
s = ""
default:
return path, false
}
case indexEnd:
switch {
case ch == ']':
state = unknown
default:
return path, false
}
case number:
switch {
case unicode.IsDigit(ch):
s += string(ch)
case ch == ']':
state = unknown
n, err := strconv.Atoi(s)
if err != nil {
return path, false
}
path = append(path, n)
s = ""
default:
return path, false
}
case doubleQuote:
switch ch {
case '"':
state = indexEnd
path = append(path, s)
s = ""
case '\\':
state = doubleQuoteEscape
default:
s += string(ch)
}
case doubleQuoteEscape:
switch ch {
case '"':
state = doubleQuote
s += string(ch)
default:
return path, false
}
case singleQuote:
switch ch {
case '\'':
state = indexEnd
path = append(path, s)
s = ""
case '\\':
state = singleQuoteEscape
s += string(ch)
default:
s += string(ch)
}
case singleQuoteEscape:
switch ch {
case '\'':
state = singleQuote
s += string(ch)
default:
return path, false
}
}
}
if len(s) > 0 {
if state == prop {
path = append(path, s)
} else {
return path, false
}
}
}
return path, true
}
func isProp(ch rune) bool {
return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == '$'
}
func GetBySimplePath(object interface{}, path []interface{}) interface{} {
for _, get := range path {
switch get := get.(type) {
case string:
switch o := object.(type) {
case *Dict:
object = o.Values[get]
case string:
if get == "length" {
object = Number(strconv.Itoa(len([]rune(o))))
} else {
object = nil
}
case Array:
if get == "length" {
object = Number(strconv.Itoa(len(o)))
} else {
object = nil
}
default:
object = nil
}
case int:
switch o := object.(type) {
case Array:
object = o[get]
default:
object = nil
}
}
}
return object
}

@ -1,137 +0,0 @@
package reducer
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func Test_splitPath(t *testing.T) {
tests := []struct {
args []string
want []interface{}
}{
{
args: []string{},
want: []interface{}{},
},
{
args: []string{"."},
want: []interface{}{},
},
{
args: []string{"x"},
want: []interface{}{},
},
{
args: []string{".foo"},
want: []interface{}{"foo"},
},
{
args: []string{"x.foo"},
want: []interface{}{"foo"},
},
{
args: []string{"x[42]"},
want: []interface{}{42},
},
{
args: []string{".[42]"},
want: []interface{}{42},
},
{
args: []string{".42"},
want: []interface{}{"42"},
},
{
args: []string{".физ"},
want: []interface{}{"физ"},
},
{
args: []string{".foo.bar"},
want: []interface{}{"foo", "bar"},
},
{
args: []string{".foo", ".bar"},
want: []interface{}{"foo", "bar"},
},
{
args: []string{".foo[42]"},
want: []interface{}{"foo", 42},
},
{
args: []string{".foo[42].bar"},
want: []interface{}{"foo", 42, "bar"},
},
{
args: []string{".foo[1][2]"},
want: []interface{}{"foo", 1, 2},
},
{
args: []string{".foo[\"bar\"]"},
want: []interface{}{"foo", "bar"},
},
{
args: []string{".foo[\"bar\\\"\"]"},
want: []interface{}{"foo", "bar\""},
},
{
args: []string{".foo['bar']['baz\\'']"},
want: []interface{}{"foo", "bar", "baz\\'"},
},
}
for _, tt := range tests {
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
path, ok := SplitSimplePath(tt.args)
require.Equal(t, tt.want, path)
require.True(t, ok)
})
}
}
func Test_splitPath_negative(t *testing.T) {
tests := []struct {
args []string
}{
{
args: []string{"./"},
},
{
args: []string{"x/"},
},
{
args: []string{"1+1"},
},
{
args: []string{"x[42"},
},
{
args: []string{".i % 2"},
},
{
args: []string{"x[for x]"},
},
{
args: []string{"x['y'."},
},
{
args: []string{"x[0?"},
},
{
args: []string{"x[\"\\u"},
},
{
args: []string{"x['\\n"},
},
{
args: []string{"x[9999999999999999999999999999999999999]"},
},
}
for _, tt := range tests {
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
path, ok := SplitSimplePath(tt.args)
require.False(t, ok, path)
})
}
}

@ -1,51 +0,0 @@
package reducer
import (
"fmt"
"regexp"
"strings"
. "github.com/antonmedv/fx/pkg/json"
. "github.com/antonmedv/fx/pkg/theme"
)
func Echo(object interface{}, theme Theme) {
if s, ok := object.(string); ok {
fmt.Println(s)
} else {
fmt.Println(PrettyPrint(object, 1, theme))
}
}
func trace(args []string, i int) (pre, post, pointer string) {
pre = strings.Join(args[:i], " ")
if len(pre) > 20 {
pre = "..." + pre[len(pre)-20:]
}
post = strings.Join(args[i+1:], " ")
if len(post) > 20 {
post = post[:20] + "..."
}
pointer = fmt.Sprintf(
"%v %v %v",
strings.Repeat(" ", len(pre)),
strings.Repeat("^", len(args[i])),
strings.Repeat(" ", len(post)),
)
return
}
var flatMapRegex = regexp.MustCompile("^(\\.\\w*)+\\[]")
func fold(s []string) string {
if len(s) == 1 {
return "x => x" + s[0]
}
obj := s[0]
if obj == "." {
obj = "x"
} else {
obj = "x" + obj
}
return fmt.Sprintf("x => Object.values(%v).flatMap(%v)", obj, fold(s[1:]))
}

@ -1,185 +0,0 @@
package theme
import (
"github.com/charmbracelet/lipgloss"
"github.com/mazznoer/colorgrad"
"strings"
)
type Theme struct {
Cursor Color
Syntax Color
Preview Color
StatusBar Color
Search Color
Key func(i, len int) Color
String Color
Null Color
Boolean Color
Number Color
}
type Color func(s string) string
var (
defaultCursor = lipgloss.NewStyle().Reverse(true).Render
defaultPreview = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("8")).Render
defaultStatusBar = lipgloss.NewStyle().Background(lipgloss.Color("7")).Foreground(lipgloss.Color("0")).Render
defaultSearch = lipgloss.NewStyle().Background(lipgloss.Color("11")).Foreground(lipgloss.Color("16")).Render
defaultNull = fg("8")
)
var Themes = map[string]Theme{
"0": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: noColor,
StatusBar: noColor,
Search: defaultSearch,
Key: func(_, _ int) Color { return noColor },
String: noColor,
Null: noColor,
Boolean: noColor,
Number: noColor,
},
"1": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: func(_, _ int) Color { return boldFg("4") },
String: boldFg("2"),
Null: defaultNull,
Boolean: boldFg("3"),
Number: boldFg("6"),
},
"2": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: func(_, _ int) Color { return fg("#00F5D4") },
String: fg("#00BBF9"),
Null: defaultNull,
Boolean: fg("#F15BB5"),
Number: fg("#9B5DE5"),
},
"3": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: func(_, _ int) Color { return fg("#faf0ca") },
String: fg("#f4d35e"),
Null: defaultNull,
Boolean: fg("#ee964b"),
Number: fg("#ee964b"),
},
"4": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: func(_, _ int) Color { return fg("#4D96FF") },
String: fg("#6BCB77"),
Null: defaultNull,
Boolean: fg("#FF6B6B"),
Number: fg("#FFD93D"),
},
"5": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: func(_, _ int) Color { return boldFg("42") },
String: boldFg("213"),
Null: defaultNull,
Boolean: boldFg("201"),
Number: boldFg("201"),
},
"6": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: func(_, _ int) Color { return gradient("rgb(125,110,221)", "rgb(90%,45%,97%)", "hsl(229,79%,85%)") },
String: fg("195"),
Null: defaultNull,
Boolean: fg("195"),
Number: fg("195"),
},
"7": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: func(_, _ int) Color { return gradient("rgb(123,216,96)", "rgb(255,255,255)") },
String: noColor,
Null: defaultNull,
Boolean: noColor,
Number: noColor,
},
"8": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: gradientKeys("#ff0000", "#ff8700", "#ffd300", "#deff0a", "#a1ff0a", "#0aff99", "#0aefff", "#147df5", "#580aff", "#be0aff"),
String: noColor,
Null: defaultNull,
Boolean: noColor,
Number: noColor,
},
"9": {
Cursor: defaultCursor,
Syntax: noColor,
Preview: defaultPreview,
StatusBar: defaultStatusBar,
Search: defaultSearch,
Key: gradientKeys("rgb(34,126,34)", "rgb(168,251,60)"),
String: gradient("rgb(34,126,34)", "rgb(168,251,60)"),
Null: defaultNull,
Boolean: noColor,
Number: noColor,
},
}
func noColor(s string) string {
return s
}
func fg(color string) Color {
return lipgloss.NewStyle().Foreground(lipgloss.Color(color)).Render
}
func boldFg(color string) Color {
return lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(color)).Render
}
func gradient(colors ...string) Color {
grad, _ := colorgrad.NewGradient().HtmlColors(colors...).Build()
return func(s string) string {
runes := []rune(s)
colors := grad.ColorfulColors(uint(len(runes)))
var out strings.Builder
for i, r := range runes {
style := lipgloss.NewStyle().Foreground(lipgloss.Color(colors[i].Hex()))
out.WriteString(style.Render(string(r)))
}
return out.String()
}
}
func gradientKeys(colors ...string) func(i, len int) Color {
grad, _ := colorgrad.NewGradient().HtmlColors(colors...).Build()
return func(i, len int) Color {
return lipgloss.NewStyle().Foreground(lipgloss.Color(grad.At(float64(i) / float64(len)).Hex())).Render
}
}

@ -1,296 +0,0 @@
package main
import (
"fmt"
"strings"
. "github.com/antonmedv/fx/pkg/dict"
. "github.com/antonmedv/fx/pkg/json"
"github.com/antonmedv/fx/pkg/theme"
)
func (m *model) connect(path string, lineNumber int) {
if _, exist := m.pathToLineNumber[path]; exist {
return
}
m.paths = append(m.paths, path)
m.pathToIndex[path] = len(m.paths) - 1
m.pathToLineNumber[path] = lineNumber
m.lineNumberToPath[lineNumber] = path
}
func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path string, selectableValues bool) []string {
m.connect(path, lineNumber)
ident := strings.Repeat(" ", level)
subident := strings.Repeat(" ", level-1)
highlight := m.highlightIndex[path]
var searchValue []*foundRange
if highlight != nil {
searchValue = highlight.value
}
switch v.(type) {
case nil:
return []string{merge(m.explode("null", searchValue, m.theme.Null, path, selectableValues))}
case bool:
if v.(bool) {
return []string{merge(m.explode("true", searchValue, m.theme.Boolean, path, selectableValues))}
} else {
return []string{merge(m.explode("false", searchValue, m.theme.Boolean, path, selectableValues))}
}
case Number:
return []string{merge(m.explode(v.(Number).String(), searchValue, m.theme.Number, path, selectableValues))}
case string:
line := fmt.Sprintf("%q", v)
chunks := m.explode(line, searchValue, m.theme.String, path, selectableValues)
if m.wrap && keyEndPos+width(line) > m.width {
return wrapLines(chunks, keyEndPos, m.width, subident)
}
// No wrap
return []string{merge(chunks)}
case *Dict:
if !m.expandedPaths[path] {
return []string{m.preview(v, path, selectableValues)}
}
output := []string{m.printOpenBracket("{", highlight, path, selectableValues)}
lineNumber++ // bracket is on separate line
keys := v.(*Dict).Keys
for i, k := range keys {
subpath := path + "." + k
highlight := m.highlightIndex[subpath]
var keyRanges, delimRanges []*foundRange
if highlight != nil {
keyRanges = highlight.key
delimRanges = highlight.delim
}
m.connect(subpath, lineNumber)
key := fmt.Sprintf("%q", k)
keyTheme := m.theme.Key(i, len(keys))
key = merge(m.explode(key, keyRanges, keyTheme, subpath, true))
value, _ := v.(*Dict).Get(k)
delim := merge(m.explode(": ", delimRanges, m.theme.Syntax, subpath, false))
keyEndPos := width(ident) + width(key) + width(delim)
lines := m.print(value, level+1, lineNumber, keyEndPos, subpath, false)
lines[0] = ident + key + delim + lines[0]
if i < len(keys)-1 {
lines[len(lines)-1] += m.printComma(",", highlight)
}
output = append(output, lines...)
lineNumber += len(lines)
}
output = append(output, subident+m.printCloseBracket("}", highlight, path, false))
return output
case Array:
if !m.expandedPaths[path] {
return []string{m.preview(v, path, selectableValues)}
}
output := []string{m.printOpenBracket("[", highlight, path, selectableValues)}
lineNumber++ // bracket is on separate line
slice := v.(Array)
for i, value := range slice {
subpath := fmt.Sprintf("%v[%v]", path, i)
s := m.highlightIndex[subpath]
m.connect(subpath, lineNumber)
lines := m.print(value, level+1, lineNumber, width(ident), subpath, true)
lines[0] = ident + lines[0]
if i < len(slice)-1 {
lines[len(lines)-1] += m.printComma(",", s)
}
lineNumber += len(lines)
output = append(output, lines...)
}
output = append(output, subident+m.printCloseBracket("]", highlight, path, false))
return output
default:
return []string{"unknown type"}
}
}
func (m *model) preview(v interface{}, path string, selectableValues bool) string {
searchResult := m.highlightIndex[path]
previewStyle := m.theme.Preview
if selectableValues && m.cursorPath() == path {
previewStyle = m.theme.Cursor
}
printValue := func(v interface{}) string {
switch v := v.(type) {
case nil, bool, Number:
return previewStyle(fmt.Sprintf("%v", v))
case string:
return previewStyle(fmt.Sprintf("%q", v))
case *Dict:
if m.showSize {
return previewStyle(toLowerNumber(fmt.Sprintf("{\u2026%v\u2026}", len(v.Keys))))
} else {
return previewStyle("{\u2026}")
}
case Array:
if m.showSize {
return previewStyle(toLowerNumber(fmt.Sprintf("[\u2026%v\u2026]", len(v))))
} else {
return previewStyle("[\u2026]")
}
}
return "..."
}
switch v := v.(type) {
case *Dict:
output := m.printOpenBracket("{", searchResult, path, selectableValues)
keys := v.Keys
for _, k := range keys {
key := fmt.Sprintf("%q", k)
output += previewStyle(key + ": ")
value, _ := v.Get(k)
output += printValue(value)
break
}
if len(keys) > 1 {
if m.showSize {
output += previewStyle(toLowerNumber(fmt.Sprintf(", \u2026%v\u2026", len(v.Keys)-1)))
} else {
output += previewStyle(", \u2026")
}
}
output += m.printCloseBracket("}", searchResult, path, selectableValues)
return output
case Array:
output := m.printOpenBracket("[", searchResult, path, selectableValues)
for _, value := range v {
output += printValue(value)
break
}
if len(v) > 1 {
if m.showSize {
output += previewStyle(toLowerNumber(fmt.Sprintf(", \u2026%v\u2026", len(v)-1)))
} else {
output += previewStyle(", \u2026")
}
}
output += m.printCloseBracket("]", searchResult, path, selectableValues)
return output
}
return "?"
}
func wrapLines(chunks []withStyle, keyEndPos, mWidth int, subident string) []string {
wrappedLines := make([]string, 0)
currentLine := ""
ident := "" // First line stays on the same line with a "key",
pos := keyEndPos // so no ident is needed. Start counting from the "key" offset.
for _, chunk := range chunks {
buffer := ""
for _, ch := range chunk.value {
buffer += string(ch)
if pos == mWidth-1 {
wrappedLines = append(wrappedLines, ident+currentLine+chunk.Render(buffer))
currentLine = ""
buffer = ""
pos = width(subident) // Start counting from ident.
ident = subident // After first line, add ident to all.
} else {
pos++
}
}
currentLine += chunk.Render(buffer)
}
if width(currentLine) > 0 {
wrappedLines = append(wrappedLines, subident+currentLine)
}
return wrappedLines
}
func (w withStyle) Render(s string) string {
return w.style(s)
}
func (m *model) printOpenBracket(line string, s *rangeGroup, path string, selectableValues bool) string {
if selectableValues && m.cursorPath() == path {
return m.theme.Cursor(line)
}
if s != nil && s.openBracket != nil {
if s.openBracket.parent.index == m.searchResultsCursor {
return m.theme.Cursor(line)
} else {
return m.theme.Search(line)
}
} else {
return m.theme.Syntax(line)
}
}
func (m *model) printCloseBracket(line string, s *rangeGroup, path string, selectableValues bool) string {
if selectableValues && m.cursorPath() == path {
return m.theme.Cursor(line)
}
if s != nil && s.closeBracket != nil {
if s.closeBracket.parent.index == m.searchResultsCursor {
return m.theme.Cursor(line)
} else {
return m.theme.Search(line)
}
} else {
return m.theme.Syntax(line)
}
}
func (m *model) printComma(line string, s *rangeGroup) string {
if s != nil && s.comma != nil {
if s.comma.parent.index == m.searchResultsCursor {
return m.theme.Cursor(line)
} else {
return m.theme.Search(line)
}
} else {
return m.theme.Syntax(line)
}
}
type withStyle struct {
value string
style theme.Color
}
func (m *model) explode(line string, highlightRanges []*foundRange, defaultStyle theme.Color, path string, selectable bool) []withStyle {
if selectable && m.cursorPath() == path && m.showCursor {
return []withStyle{{line, m.theme.Cursor}}
}
out := make([]withStyle, 0, 1)
pos := 0
for _, r := range highlightRanges {
style := m.theme.Search
if r.parent.index == m.searchResultsCursor {
style = m.theme.Cursor
}
out = append(out, withStyle{
value: line[pos:r.start],
style: defaultStyle,
})
out = append(out, withStyle{
value: line[r.start:r.end],
style: style,
})
pos = r.end
}
out = append(out, withStyle{
value: line[pos:],
style: defaultStyle,
})
return out
}
func merge(chunks []withStyle) string {
out := ""
for _, chunk := range chunks {
out += chunk.Render(chunk.value)
}
return out
}

@ -0,0 +1,117 @@
'use strict'
const indent = require('indent-string')
const config = require('./config')
function format(value, style, highlightStyle, regexp, transform = x => x) {
if (!regexp) {
return style(transform(value))
}
const marked = value
.replace(regexp, s => '<highlight>' + s + '<highlight>')
return transform(marked)
.split(/<highlight>/g)
.map((s, i) => i % 2 !== 0 ? highlightStyle(s) : style(s))
.join('')
}
function print(input, options = {}) {
const {expanded, highlight, currentPath} = options
const index = new Map()
let row = 0
function doPrint(v, path = '') {
index.set(row, path)
// Code for highlighting parts become cumbersome.
// Maybe we should refactor this part.
const highlightStyle = (currentPath === path) ? config.highlightCurrent : config.highlight
const formatStyle = (v, style) => format(v, style, highlightStyle, highlight)
const formatText = (v, style, path) => {
const highlightStyle = (currentPath === path) ? config.highlightCurrent : config.highlight
return format(v, style, highlightStyle, highlight, JSON.stringify)
}
const eol = () => {
row++
return '\n'
}
if (typeof v === 'undefined') {
return void 0
}
if (v === null) {
return formatStyle(JSON.stringify(v), config.null)
}
if (typeof v === 'number' && Number.isFinite(v)) {
return formatStyle(JSON.stringify(v), config.number)
}
if (typeof v === 'object' && v.isLosslessNumber) {
return formatStyle(v.toString(), config.number)
}
if (typeof v === 'boolean') {
return formatStyle(JSON.stringify(v), config.boolean)
}
if (typeof v === 'string') {
return formatText(v, config.string, path)
}
if (Array.isArray(v)) {
let output = config.bracket('[')
const len = v.length
if (len > 0) {
if (expanded && !expanded.has(path)) {
output += '\u2026'
} else {
output += eol()
let i = 0
for (let item of v) {
const value = typeof item === 'undefined' ? null : item // JSON.stringify compatibility
output += indent(doPrint(value, path + '[' + i + ']'), config.space)
output += i++ < len - 1 ? config.comma(',') : ''
output += eol()
}
}
}
return output + config.bracket(']')
}
if (typeof v === 'object' && v.constructor === Object) {
let output = config.bracket('{')
const entries = Object.entries(v).filter(([key, value]) => typeof value !== 'undefined') // JSON.stringify compatibility
const len = entries.length
if (len > 0) {
if (expanded && !expanded.has(path)) {
output += '\u2026'
} else {
output += eol()
let i = 0
for (let [key, value] of entries) {
const part = formatText(key, config.key, path + '.' + key) + config.colon(':') + ' ' + doPrint(value, path + '.' + key)
output += indent(part, config.space)
output += i++ < len - 1 ? config.comma(',') : ''
output += eol()
}
}
}
return output + config.bracket('}')
}
return JSON.stringify(v, null, config.space)
}
return [doPrint(input), index]
}
module.exports = print

@ -0,0 +1,49 @@
'use strict'
const JSON = require('lossless-json') // override JSON for user's code
module.exports = function (json, code) {
if (process.env.FX_APPLY) {
return global[process.env.FX_APPLY](code)(json)
}
if ('.' === code) {
return json
}
if ('?' === code) {
return Object.keys(json)
}
if (/^(\.\w*)+\[]/.test(code)) {
function fold(s) {
if (s.length === 1) {
return 'x => x' + s[0]
}
let obj = s.shift()
obj = obj === '.' ? 'x' : 'x' + obj
return `x => Object.values(${obj}).flatMap(${fold(s)})`
}
code = fold(code.split('[]'))
}
if (/^\.\[/.test(code)) {
return eval(`function fn() {
return this${code.substring(1)}
}; fn`).call(json)
}
if (/^\./.test(code)) {
return eval(`function fn() {
return this${code}
}; fn`).call(json)
}
const fn = eval(`function fn() {
return ${code}
}; fn`).call(json)
if (typeof fn === 'function') {
return fn(json)
}
return fn
}

@ -1,249 +0,0 @@
package main
import (
"fmt"
"regexp"
. "github.com/antonmedv/fx/pkg/dict"
. "github.com/antonmedv/fx/pkg/json"
)
type searchResult struct {
path string
index int
ranges []*foundRange
}
type rangeKind int
const (
keyRange rangeKind = 1 + iota
valueRange
delimRange
openBracketRange
closeBracketRange
commaRange
)
type foundRange struct {
parent *searchResult
// Range needs separate path, as for one searchResult's path
// there can be multiple ranges for different paths (within parent path).
path string
start, end int
kind rangeKind
}
type rangeGroup struct {
key []*foundRange
value []*foundRange
delim []*foundRange
openBracket *foundRange
closeBracket *foundRange
comma *foundRange
}
func (m *model) clearSearchResults() {
m.searchRegexCompileError = ""
m.searchResults = nil
m.highlightIndex = nil
}
func (m *model) doSearch(s string) {
m.clearSearchResults()
re, err := regexp.Compile("(?i)" + s)
if err != nil {
m.searchRegexCompileError = err.Error()
m.searchInput.Blur()
return
}
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
m.indexSearchResults()
m.searchInput.Blur()
m.showSearchResults = true
m.jumpToSearchResult(0)
}
func (m *model) remapSearchResult(object interface{}, path string, pos int, indexes [][]int, id int, current *searchResult) (int, int, *searchResult) {
switch object.(type) {
case nil:
s := "null"
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
return pos + len(s), id, current
case bool:
var s string
if object.(bool) {
s = "true"
} else {
s = "false"
}
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
return pos + len(s), id, current
case Number:
s := object.(Number).String()
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
return pos + len(s), id, current
case string:
// TODO: Wrap the string, save a line number according to current wrap or no wrap mode.
s := fmt.Sprintf("%q", object)
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
return pos + len(s), id, current
case *Dict:
id, current = m.findRanges(openBracketRange, "{", path, pos, indexes, id, current)
pos++ // {
for i, k := range object.(*Dict).Keys {
subpath := path + "." + k
key := fmt.Sprintf("%q", k)
id, current = m.findRanges(keyRange, key, subpath, pos, indexes, id, current)
pos += len(key)
delim := ": "
id, current = m.findRanges(delimRange, delim, subpath, pos, indexes, id, current)
pos += len(delim)
pos, id, current = m.remapSearchResult(object.(*Dict).Values[k], subpath, pos, indexes, id, current)
if i < len(object.(*Dict).Keys)-1 {
comma := ","
id, current = m.findRanges(commaRange, comma, subpath, pos, indexes, id, current)
pos += len(comma)
}
}
id, current = m.findRanges(closeBracketRange, "}", path, pos, indexes, id, current)
pos++ // }
return pos, id, current
case Array:
id, current = m.findRanges(openBracketRange, "[", path, pos, indexes, id, current)
pos++ // [
for i, v := range object.(Array) {
subpath := fmt.Sprintf("%v[%v]", path, i)
pos, id, current = m.remapSearchResult(v, subpath, pos, indexes, id, current)
if i < len(object.(Array))-1 {
comma := ","
id, current = m.findRanges(commaRange, comma, subpath, pos, indexes, id, current)
pos += len(comma)
}
}
id, current = m.findRanges(closeBracketRange, "]", path, pos, indexes, id, current)
pos++ // ]
return pos, id, current
default:
panic("unexpected object type")
}
}
func (m *model) findRanges(kind rangeKind, s string, path string, pos int, indexes [][]int, id int, current *searchResult) (int, *searchResult) {
for ; id < len(indexes); id++ {
start, end := indexes[id][0]-pos, indexes[id][1]-pos
if end <= 0 {
current = nil
continue
}
if start < len(s) {
if current == nil {
current = &searchResult{
path: path,
index: len(m.searchResults),
}
m.searchResults = append(m.searchResults, current)
}
found := &foundRange{
parent: current,
path: path,
start: max(start, 0),
end: min(end, len(s)),
kind: kind,
}
current.ranges = append(current.ranges, found)
if end < len(s) {
current = nil
} else {
break
}
} else {
break
}
}
return id, current
}
func (m *model) indexSearchResults() {
m.highlightIndex = map[string]*rangeGroup{}
for _, s := range m.searchResults {
for _, r := range s.ranges {
highlight, exist := m.highlightIndex[r.path]
if !exist {
highlight = &rangeGroup{}
m.highlightIndex[r.path] = highlight
}
switch r.kind {
case keyRange:
highlight.key = append(highlight.key, r)
case valueRange:
highlight.value = append(highlight.value, r)
case delimRange:
highlight.delim = append(highlight.delim, r)
case openBracketRange:
highlight.openBracket = r
case closeBracketRange:
highlight.closeBracket = r
case commaRange:
highlight.comma = r
}
}
}
}
func (m *model) jumpToSearchResult(at int) {
if len(m.searchResults) == 0 {
return
}
m.showCursor = false
m.searchResultsCursor = at % len(m.searchResults)
desiredPath := m.searchResults[m.searchResultsCursor].path
_, ok := m.pathToLineNumber[desiredPath]
if ok {
m.cursor = m.pathToIndex[desiredPath]
m.scrollDownToCursor()
m.render()
} else {
m.expandToPath(desiredPath)
m.render()
m.jumpToSearchResult(at)
}
}
func (m *model) expandToPath(path string) {
m.expandedPaths[path] = true
if path != "" {
m.expandToPath(m.parents[path])
}
}
func (m *model) nextSearchResult() {
if len(m.searchResults) > 0 {
m.jumpToSearchResult((m.searchResultsCursor + 1) % len(m.searchResults))
}
}
func (m *model) prevSearchResult() {
i := m.searchResultsCursor - 1
if i < 0 {
i = len(m.searchResults) - 1
}
m.jumpToSearchResult(i)
}
func (m *model) resultsCursorPath() string {
if len(m.searchResults) == 0 {
return "?"
}
return m.searchResults[m.searchResultsCursor].path
}

@ -1,190 +0,0 @@
package main
import (
. "github.com/antonmedv/fx/pkg/dict"
. "github.com/antonmedv/fx/pkg/json"
"github.com/stretchr/testify/require"
"regexp"
"testing"
)
func Test_search_values(t *testing.T) {
tests := []struct {
name string
object interface{}
want *foundRange
}{
{name: "null", object: nil},
{name: "true", object: true},
{name: "false", object: false},
{name: "Number", object: Number("42")},
{name: "string", object: "Hello, World!"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &model{
json: tt.object,
}
re, _ := regexp.Compile(".+")
str := Stringify(m.json)
indexes := re.FindAllStringIndex(str, -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
s := &searchResult{path: ""}
s.ranges = append(s.ranges, &foundRange{
parent: s,
path: "",
start: 0,
end: len(str),
kind: valueRange,
})
require.Equal(t, []*searchResult{s}, m.searchResults)
})
}
}
func Test_search_array(t *testing.T) {
msg := `
["first","second"]
^^^^^ ^^^^^^
`
m := &model{
json: Array{"first", "second"},
}
re, _ := regexp.Compile("\\w+")
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
s1 := &searchResult{path: "[0]"}
s1.ranges = append(s1.ranges,
&foundRange{
parent: s1,
path: "[0]",
start: 1,
end: 6,
kind: valueRange,
},
)
s2 := &searchResult{path: "[1]", index: 1}
s2.ranges = append(s2.ranges,
&foundRange{
parent: s2,
path: "[1]",
start: 1,
end: 7,
kind: valueRange,
},
)
require.Equal(t, []*searchResult{s1, s2}, m.searchResults, msg)
}
func Test_search_between_array(t *testing.T) {
msg := `
["first","second"]
^^^^^^^^^^^^^^
`
m := &model{
json: Array{"first", "second"},
}
re, _ := regexp.Compile("\\w.+\\w")
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
s := &searchResult{path: "[0]"}
s.ranges = append(s.ranges,
&foundRange{
parent: s,
path: "[0]",
start: 1,
end: 7,
kind: valueRange,
},
&foundRange{
parent: s,
path: "[0]",
start: 0,
end: 1,
kind: commaRange,
},
&foundRange{
parent: s,
path: "[1]",
start: 0,
end: 7,
kind: valueRange,
},
)
require.Equal(t, []*searchResult{s}, m.searchResults, msg)
}
func Test_search_dict(t *testing.T) {
msg := `
{"key": "hello world"}
^^^^^ ^^^^^^^^^^^^^
`
d := NewDict()
d.Set("key", "hello world")
m := &model{
json: d,
}
re, _ := regexp.Compile("\"[\\w\\s]+\"")
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
s1 := &searchResult{path: ".key"}
s1.ranges = append(s1.ranges,
&foundRange{
parent: s1,
path: ".key",
start: 0,
end: 5,
kind: keyRange,
},
)
s2 := &searchResult{path: ".key", index: 1}
s2.ranges = append(s2.ranges,
&foundRange{
parent: s2,
path: ".key",
start: 0,
end: 13,
kind: valueRange,
},
)
require.Equal(t, []*searchResult{s1, s2}, m.searchResults, msg)
}
func Test_search_dict_with_array(t *testing.T) {
msg := `
{"first": [1,2],"second": []}
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
`
d := NewDict()
d.Set("first", Array{Number("1"), Number("2")})
d.Set("second", Array{})
m := &model{
json: d,
}
re, _ := regexp.Compile(".+")
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
s := &searchResult{path: ""}
s.ranges = append(s.ranges,
/* { */ &foundRange{parent: s, path: "", start: 0, end: 1, kind: openBracketRange},
/* "first" */ &foundRange{parent: s, path: ".first", start: 0, end: 7, kind: keyRange},
/* : */ &foundRange{parent: s, path: ".first", start: 0, end: 2, kind: delimRange},
/* [ */ &foundRange{parent: s, path: ".first", start: 0, end: 1, kind: openBracketRange},
/* 1 */ &foundRange{parent: s, path: ".first[0]", start: 0, end: 1, kind: valueRange},
/* , */ &foundRange{parent: s, path: ".first[0]", start: 0, end: 1, kind: commaRange},
/* 2 */ &foundRange{parent: s, path: ".first[1]", start: 0, end: 1, kind: valueRange},
/* ] */ &foundRange{parent: s, path: ".first", start: 0, end: 1, kind: closeBracketRange},
/* , */ &foundRange{parent: s, path: ".first", start: 0, end: 1, kind: commaRange},
/* "second" */ &foundRange{parent: s, path: ".second", start: 0, end: 8, kind: keyRange},
/* : */ &foundRange{parent: s, path: ".second", start: 0, end: 2, kind: delimRange},
/* [ */ &foundRange{parent: s, path: ".second", start: 0, end: 1, kind: openBracketRange},
/* ] */ &foundRange{parent: s, path: ".second", start: 0, end: 1, kind: closeBracketRange},
/* } */ &foundRange{parent: s, path: "", start: 0, end: 1, kind: closeBracketRange},
)
require.Equal(t, []*searchResult{s}, m.searchResults, msg)
}

@ -1,20 +0,0 @@
name: fx
version: 24.0.0
summary: Terminal JSON viewer
description: Terminal JSON viewer
base: core18
grade: stable
confinement: strict
apps:
fx:
command: fx
plugs: [home, network]
parts:
fx:
plugin: go
go-channel: 1.18/stable
source: .
source-type: git
go-importpath: github.com/antonmedv/fx

@ -0,0 +1,33 @@
'use strict'
const JSON = require('lossless-json')
const skip = Symbol('skip')
function select(cb) {
return json => {
if (!cb(json)) {
throw skip
}
return json
}
}
function filter(cb) {
return json => {
if (cb(json)) {
throw skip
}
return json
}
}
function save(json) {
if (!global.FX_FILENAME) {
throw "No filename provided.\nTo edit-in-place, specify JSON file as first argument."
}
require('fs').writeFileSync(global.FX_FILENAME, JSON.stringify(json, null, 2))
return json
}
Object.assign(exports, {skip, select, filter, save})
Object.assign(global, exports)
global.std = exports

@ -1,42 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"io"
. "github.com/antonmedv/fx/pkg/json"
. "github.com/antonmedv/fx/pkg/reducer"
. "github.com/antonmedv/fx/pkg/theme"
"github.com/dop251/goja"
)
func stream(dec *json.Decoder, object interface{}, lang string, args []string, theme Theme, fxrc string) int {
var vm *goja.Runtime
var fn goja.Callable
var err error
if lang == "js" {
vm, fn, err = CreateJS(args, fxrc)
if err != nil {
fmt.Println(err)
return 1
}
}
for {
if object != nil {
if lang == "js" {
ReduceJS(vm, fn, object, theme)
} else {
Reduce(object, lang, args, theme, fxrc)
}
}
object, err = Parse(dec)
if err == io.EOF {
return 0
}
if err != nil {
fmt.Println("JSON Parse Error:", err.Error())
return 1
}
}
}

@ -0,0 +1,84 @@
'use strict'
const JSON = require('lossless-json')
function apply(cb, input) {
let json
try {
json = JSON.parse(input)
} catch (e) {
process.stderr.write(e.toString() + '\n')
return
}
cb(json)
}
function stream(from, cb) {
let buff = ''
let lastChar = ''
let len = 0
let depth = 0
let isString = false
let count = 0
let head = ''
const check = (i) => {
if (depth <= 0) {
const input = buff.substring(0, len + i + 1)
if (count > 0) {
if (head !== '') {
apply(cb, head)
head = ''
}
apply(cb, input)
} else {
head = input
}
buff = buff.substring(len + i + 1)
len = -i - 1
count++
}
}
return {
isStream() {
return count > 1
},
value() {
return head + buff
},
read() {
let chunk
while ((chunk = from.read())) {
len = buff.length
buff += chunk
for (let i = 0; i < chunk.length; i++) {
if (isString) {
if (chunk[i] === '"' && ((i === 0 && lastChar !== '\\') || (i > 0 && chunk[i - 1] !== '\\'))) {
isString = false
check(i)
}
continue
}
if (chunk[i] === '{' || chunk[i] === '[') {
depth++
} else if (chunk[i] === '}' || chunk[i] === ']') {
depth--
check(i)
} else if (chunk[i] === '"') {
isString = true
}
}
lastChar = chunk[chunk.length - 1]
}
}
}
}
module.exports = stream

@ -0,0 +1,85 @@
'use strict'
const test = require('ava')
const {execSync} = require('child_process')
const stream = require('./stream')
function fx(json, code = '') {
return execSync(`echo '${JSON.stringify(json)}' | node index.js ${code}`).toString('utf8')
}
test('pass', t => {
const r = fx([{'greeting': 'hello world'}])
t.deepEqual(JSON.parse(r), [{'greeting': 'hello world'}])
})
test('anon func', t => {
const r = fx({'key': 'value'}, '\'function (x) { return x.key }\'')
t.is(r, 'value\n')
})
test('arrow func', t => {
const r = fx({'key': 'value'}, '\'x => x.key\'')
t.is(r, 'value\n')
})
test('arrow func ()', t => {
const r = fx({'key': 'value'}, '\'(x) => x.key\'')
t.is(r, 'value\n')
})
test('this bind', t => {
const r = fx([1, 2, 3, 4, 5], '\'this.map(x => x * this.length)\'')
t.deepEqual(JSON.parse(r), [5, 10, 15, 20, 25])
})
test('chain', t => {
const r = fx({'items': ['foo', 'bar']}, '\'this.items\' \'.\' \'x => x[1]\'')
t.is(r, 'bar\n')
})
test('file argument', t => {
const r = execSync(`node index.js package.json .name`).toString('utf8')
t.is(r, 'fx\n')
})
test('stream', t => {
const input = `
{"index": 0} {"index": 1}
{"index": 2, "quote": "\\""}
{"index": 3} "Hello" "world"
{"index": 6, "key": "one \\"two\\" three"}
`
t.plan(7 * (input.length - 1))
for (let i = 0; i < input.length; i++) {
const parts = [input.substring(0, i), input.substring(i)]
const reader = stream(
{
read() {
return parts.shift()
}
},
json => {
t.pass()
}
)
reader.read()
}
})
test('lossless number', t => {
const r = execSync(`echo '{"long": 123456789012345678901}' | node index.js .long`).toString('utf8')
t.is(r, '123456789012345678901\n')
})
test('value iterator', t => {
const r = fx({master: {foo: [{bar: [{val: 1}]}]}}, '.master.foo[].bar[].val')
t.deepEqual(JSON.parse(r), [1])
})
test('value iterator simple', t => {
const r = fx([{val:1},{val:2}], '.[].val')
t.deepEqual(JSON.parse(r), [1, 2])
})

@ -1,50 +0,0 @@
package main
import (
"fmt"
"strings"
"github.com/charmbracelet/lipgloss"
)
func clamp(v, low, high int) int {
if high < low {
low, high = high, low
}
return min(high, max(low, v))
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func width(s string) int {
return lipgloss.Width(s)
}
func accessor(path string, to interface{}) string {
return fmt.Sprintf("%v[%v]", path, to)
}
func toLowerNumber(s string) string {
var out strings.Builder
for _, r := range s {
switch {
case '0' <= r && r <= '9':
out.WriteRune('\u2080' + (r - '\u0030'))
default:
out.WriteRune(r)
}
}
return out.String()
}

@ -1,26 +0,0 @@
package main
import "testing"
func Test_clamp(t *testing.T) {
got := clamp(1, 2, 3)
if got != 2 {
t.Errorf("clamp() = %v, want 2", got)
}
}
func Test_max(t *testing.T) {
got := max(1, 2)
if got != 2 {
t.Errorf("max() = %v, want 2", got)
}
}
func Test_min(t *testing.T) {
got := min(1, 2)
if got != 1 {
t.Errorf("min() = %v, want 1", got)
}
}

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

@ -1,128 +0,0 @@
package main
import (
"math"
)
func (m *model) AtTop() bool {
return m.offset <= 0
}
func (m *model) AtBottom() bool {
return m.offset >= m.maxYOffset()
}
func (m *model) PastBottom() bool {
return m.offset > m.maxYOffset()
}
func (m *model) ScrollPercent() float64 {
if m.height >= len(m.lines) {
return 1.0
}
y := float64(m.offset)
h := float64(m.height)
t := float64(len(m.lines) - 1)
v := y / (t - h)
return math.Max(0.0, math.Min(1.0, v))
}
func (m *model) maxYOffset() int {
return max(0, len(m.lines)-m.height)
}
func (m *model) visibleLines() (lines []string) {
if len(m.lines) > 0 {
top := max(0, m.offset)
bottom := clamp(m.offset+m.height, top, len(m.lines))
lines = m.lines[top:bottom]
}
return lines
}
func (m *model) SetOffset(n int) {
m.offset = clamp(n, 0, m.maxYOffset())
}
func (m *model) ViewDown() {
if m.AtBottom() {
return
}
m.SetOffset(m.offset + m.height)
}
func (m *model) ViewUp() {
if m.AtTop() {
return
}
m.SetOffset(m.offset - m.height)
}
func (m *model) HalfViewDown() {
if m.AtBottom() {
return
}
m.SetOffset(m.offset + m.height/2)
}
func (m *model) HalfViewUp() {
if m.AtTop() {
return
}
m.SetOffset(m.offset - m.height/2)
}
func (m *model) LineDown(n int) {
if m.AtBottom() || n == 0 {
return
}
// Make sure the Number of lines by which we're going to scroll isn't
// greater than the Number of lines we actually have left before we reach
// the bottom.
m.SetOffset(m.offset + n)
}
func (m *model) LineUp(n int) {
if m.AtTop() || n == 0 {
return
}
// Make sure the Number of lines by which we're going to scroll isn't
// greater than the Number of lines we are from the top.
m.SetOffset(m.offset - n)
}
func (m *model) GotoTop() {
if m.AtTop() {
return
}
m.SetOffset(0)
}
func (m *model) GotoBottom() {
m.SetOffset(m.maxYOffset())
}
func (m *model) scrollDownToCursor() {
at := m.cursorLineNumber()
if m.offset <= at { // cursor is lower
m.LineDown(max(0, at-(m.offset+m.height-1))) // minus one is due to cursorLineNumber() starts from 0
} else {
m.SetOffset(at)
}
}
func (m *model) scrollUpToCursor() {
at := m.cursorLineNumber()
if at < m.offset+m.height { // cursor is above
m.LineUp(max(0, m.offset-at))
} else {
m.SetOffset(at)
}
}
Loading…
Cancel
Save