Compare commits

...

205 Commits

Author SHA1 Message Date
lilihx
95009b06b6
add flag: -h, --help (#218)
Change-Id: I6d5648b84536c22e518413d0360047431b1acccc
2022-07-05 14:06:44 +02:00
Anton Medvedev
751e5865da Simplify reducers and add fast path to js reducer 2022-05-10 21:59:59 +02:00
Anton Medvedev
bd02783f75 Split reducers docs 2022-05-10 21:39:34 +02:00
github-actions
c3f58aa915 Release 24.0.0 2022-05-08 18:31:27 +00:00
Anton Medvedev
126340bcf0 Update doc.md 2022-05-08 18:10:15 +02:00
Anton Medvedev
06ff419622 Update doc.md 2022-05-08 18:09:28 +02:00
Anton Medvedev
92cf975206 Update README.md 2022-05-08 17:53:55 +02:00
Anton Medvedev
158687e890 Update doc.md 2022-05-08 17:51:48 +02:00
Anton Medvedev
14c79c1d12 Update doc.md 2022-05-08 17:51:30 +02:00
Anton Medvedev
918b4e5e3b Update doc.md 2022-05-08 17:51:10 +02:00
Anton Medvedev
2428d76e80 Add doc about config 2022-05-08 17:49:17 +02:00
Anton Medvedev
06f612bfb9 Update README.md 2022-05-08 17:44:48 +02:00
Anton Medvedev
76e67edc41 Update doc.md 2022-05-08 17:34:33 +02:00
Anton Medvedev
534e0db987 Add doc 2022-05-08 17:03:22 +02:00
Anton Medvedev
4d0dbf4b94 Add FX_SHOW_SIZE config option 2022-05-08 16:15:13 +02:00
github-actions
1198ae9984 Release 23.2.0 2022-05-08 13:48:23 +00:00
Anton Medvedev
d622f6b85d Update release.yml 2022-05-08 15:45:18 +02:00
Anton Medvedev
d7b5ab7200 Refactor reducers 2022-05-08 15:13:09 +02:00
Anton Medvedev
a58152469f Drop flags 2022-05-08 14:20:04 +02:00
Gabriel M. Dutra
9cf72f0407
Adding installation for FeeBSD (#204) 2022-05-06 07:40:05 +02:00
github-actions
0b9019a2c5 Release 23.1.0 2022-05-05 07:04:21 +00:00
Anton Medvedev
7c72e04453 Add TODO 2022-05-04 00:07:01 +02:00
Anton Medvedev
477f89c783 Add goja 2022-05-04 00:00:52 +02:00
Anton Medvedev
f474859d9b Add TODO 2022-05-03 23:03:52 +02:00
Anton Medvedev
9421363ebd Ignore mouse clicks on status bar 2022-05-03 23:01:34 +02:00
Anton Medvedev
92c4b93f15 Better search cursor jumps 2022-05-03 22:57:23 +02:00
Anton Medvedev
95c55f9827 Add brew to release.yml 2022-05-03 08:31:18 +02:00
Anton Medvedev
d443625dd6
Update README.md 2022-05-03 08:18:16 +02:00
Anton Medvedev
0d5fa2e41a
Update README.md 2022-05-03 08:10:48 +02:00
github-actions
79ddfe0ea3 Release 23.0.1 2022-05-02 23:09:32 +00:00
Anton Medvedev
06f75b062e Fix command name 2022-05-03 01:08:42 +02:00
github-actions
64349cb97c Release 23.0.0 2022-05-02 23:05:05 +00:00
Anton Medvedev
5e8d67a5fb Add default reducer 2022-05-03 00:26:44 +02:00
Anton Medvedev
2e30f1ec9a Add TODO 2022-04-29 23:42:56 +02:00
Anton Medvedev
3b9ac1d023 Add streaming support 2022-04-29 23:17:39 +02:00
Kacper Bąk
7028357eba
fix: assertion check in switch (#198) 2022-04-28 00:02:18 +02:00
Anton Medvedev
0cccf2e063 Update README.md 2022-04-25 22:43:56 +02:00
Anton Medvedev
204c2988c8 Update preview 2022-04-25 22:41:54 +02:00
Anton Medvedev
1e4ee58691 Add go mod download 2022-04-25 21:41:31 +02:00
github-actions
61e8cf97cf Release 22.0.10 2022-04-25 19:38:54 +00:00
Anton Medvedev
04693455c4 Fix release.yml 2022-04-25 21:38:02 +02:00
github-actions
f87f89440c Release RELEASE_VERSION 2022-04-25 19:31:25 +00:00
Anton Medvedev
33f25646c5 Fix release.yml 2022-04-25 21:30:33 +02:00
Anton Medvedev
a406596b46 Fix release.yml 2022-04-25 21:25:34 +02:00
github-actions
684d5d59d0 Release RELEASE_VERSION 2022-04-25 19:23:41 +00:00
Anton Medvedev
b0045d03a9 Fix release.yml 2022-04-25 21:22:51 +02:00
Anton Medvedev
a01be39389 Fix release.yml 2022-04-25 21:03:15 +02:00
Anton Medvedev
918ba6b566 Fix release.yml 2022-04-25 20:59:36 +02:00
Anton Medvedev
710cf58310 Fix release.yml 2022-04-25 20:56:38 +02:00
Anton Medvedev
8386ba7d91 Fix release.yml 2022-04-25 20:49:04 +02:00
Anton Medvedev
f6c561c411 Update release.yml 2022-04-25 16:43:14 +02:00
Anton Medvedev
09aaaa9e3a Fix release script 2022-04-25 16:28:41 +02:00
Anton Medvedev
e3ca218491 Update release script 2022-04-25 16:22:42 +02:00
Anton Medvedev
3b0826c3c9 Add snapcraft.yaml 2022-04-24 12:56:09 +02:00
Anton Medvedev
506961e9d8 Update README.md 2022-04-23 19:50:11 +02:00
Anton Medvedev
6e1dd65b3e Release 22.0.0 2022-04-23 19:44:53 +02:00
Anton Medvedev
ed5ed858d4 Fix nodejs cwd 2022-04-23 12:20:45 +02:00
Caleb Maclennan
6ef3aa5573
Update install docs with known upstream packages (#195)
* Document Homebrew installation, already ustreamed

* Document Arch Linux official package installation

* Document Scoop package installation

* Clarify source vs. distro package installation instructions
2022-04-22 23:29:35 +02:00
Anton Medvedev
915c1e9fa7 Add async reducers 2022-04-22 22:28:07 +02:00
Anton Medvedev
77b2632aeb Print debug error message 2022-04-22 22:27:42 +02:00
Anton Medvedev
36af65042c Refactor Reduce to also output extra text 2022-04-22 15:49:51 +02:00
Anton Medvedev
e05e917a6f Update deps 2022-04-22 15:17:26 +02:00
Anton Medvedev
bdb1d00831 Add delay before booting up interactive view 2022-04-22 15:17:09 +02:00
Anton Medvedev
3463842b8b Fix node reducer 2022-04-22 15:16:43 +02:00
Anton Medvedev
fc257539df Use MY_TOKEN 2022-04-20 23:21:28 +02:00
Anton Medvedev
0d556edd9e Update release script 2022-04-20 23:16:58 +02:00
Anton Medvedev
d7f54a5c80 Add release script 2022-04-20 23:08:59 +02:00
Anton Medvedev
804becbdb5
Update release.yml 2022-04-19 23:59:34 +02:00
Anton Medvedev
f4daf98966
Create release.yml 2022-04-19 23:56:10 +02:00
Anton Medvedev
bae280436b
Create test.yml 2022-04-19 23:12:44 +02:00
Anton Medvedev
f2a2a01589 Update README.md 2022-04-18 23:20:53 +02:00
Anton Medvedev
344f78d20a Update README.md 2022-04-18 23:12:24 +02:00
Anton Medvedev
5c448bf98f Update README.md 2022-04-18 23:11:23 +02:00
Anton Medvedev
89c9bc7f10 Update README.md 2022-04-18 23:08:04 +02:00
Anton Medvedev
8972572d79 Update README.md 2022-04-18 23:07:36 +02:00
Anton Medvedev
dac5e9fc03 Update README.md 2022-04-18 23:04:23 +02:00
Anton Medvedev
1b1e76df01 Add ruby reducers 2022-04-18 23:02:55 +02:00
Anton Medvedev
61eb6e5ec3 Add ruby reducers 2022-04-18 22:57:05 +02:00
Anton Medvedev
1b09c93447 Add support for python reducers 2022-04-18 22:00:17 +02:00
Anton Medvedev
73bf2d97cd Add --version flag 2022-04-18 19:30:26 +02:00
Anton Medvedev
e3aea24907 Sort imports 2022-04-18 19:24:22 +02:00
Anton Medvedev
c642fbd28e Better usage output 2022-04-18 16:54:32 +02:00
Anton Medvedev
6ed6ba2ec4 Update README.md 2022-04-17 23:04:58 +02:00
Anton Medvedev
bf166fafda Move part of the code to pkg 2022-04-17 22:57:12 +02:00
Alexandre GUIOT--VALENTIN
aa0964a12e
Better handling of input file errors (#187)
* Handle error when no input is provided

* Improve error when input file can't be read
2022-04-17 17:49:54 +02:00
Ezequiel Moreno
a68c947949
Update README.md: install latest (#186)
set desired installation version to latest
2022-04-17 17:48:42 +02:00
Anton Medvedev
7d7bfd28fe
Update README.md 2022-04-16 22:21:52 +02:00
Anton Medvedev
8bbc8ed564 Add LICENSE 2022-04-16 22:16:19 +02:00
Anton Medvedev
2e7f78f7c6 Add README.md 2022-04-16 22:16:19 +02:00
Anton Medvedev
ae80a278b2 Add themes 2022-04-16 22:16:19 +02:00
Anton Medvedev
49756d7ff4 Better handling of search edge cases 2022-04-16 22:16:19 +02:00
Anton Medvedev
68212c0c7a Refactor search to use search ranges 2022-04-16 22:16:19 +02:00
Anton Medvedev
841eb4f1b5 Refactor to use separate path vars 2022-04-16 22:16:19 +02:00
Anton Medvedev
d682c0b49f Refactor search result printing 2022-04-16 22:16:19 +02:00
Anton Medvedev
afd7b96af5 Other features 2022-04-16 22:16:19 +02:00
Anton Medvedev
fa226f2ae8 Add reduce 2022-04-16 22:16:19 +02:00
Anton Medvedev
e6042cb698 Reimplement in go 2022-04-16 22:16:19 +02:00
Anton Medvedev
65a1608462 Delete js version 2022-04-16 22:16:19 +02:00
Yury Shulaev
8a9e1f39d5
Prevent "reduce" name collision (#177) 2022-01-02 09:44:25 +03:00
Anton Medvedev
e14e56fa06
Update README.md 2021-01-17 13:49:49 +03:00
Sébastien HOUZÉ
2315f509b8
fix: set exit code to zero when fx --version (#148)
As it's the current convention.
2020-11-19 00:38:42 +03:00
Andrey Shilov
3e0cc4f61b
Add shift left collapse (#144) 2020-10-26 13:50:09 +03:00
Anton Medvedev
604512a410
Delete FUNDING.yml 2020-10-24 00:38:55 +03:00
Anton Medvedev
2dc104887b
Create FUNDING.yml 2020-10-24 00:31:02 +03:00
Anton Medvedev
6db594864e
Fix typo 2020-09-22 21:50:11 +03:00
Anton Medvedev
0115d90a36
Add links 2020-09-22 21:48:44 +03:00
Anton Medvedev
043208a748 Release 20.0.2 2020-09-15 10:53:34 +02:00
Anton Medvedev
d289037340 Use LosslessJSON in reduce function 2020-09-15 10:44:43 +02:00
Anton Medvedev
afa4088e99 Release 20.0.1 2020-09-14 16:59:44 +02:00
Anton Medvedev
e430d5019b Fix edit-in-place save function to correctly work with lossless JSON 2020-09-14 16:48:55 +02:00
Anton Medvedev
6f100280a6 Release 20.0.0 2020-09-04 00:11:22 +02:00
Anton Medvedev
f47c8e8c67 Update travis node version 2020-09-04 00:00:43 +02:00
Anton Medvedev
19a9d34dad Add support for .[] 2020-09-03 23:54:32 +02:00
Anton Medvedev
d455165d25
Update DOCS.md 2020-09-03 18:46:06 +03:00
Anton Medvedev
5521a99e69 Release 19.0.1 2020-06-17 18:34:44 +03:00
Anton Medvedev
f1eaaa9c5e Use latest node for standalone bin 2020-06-17 18:34:10 +03:00
Anton Medvedev
333dd38353 Include all *.js files 2020-06-17 18:24:45 +03:00
Anton Medvedev
c6bc621c57 Release 19.0.0 2020-06-17 17:21:52 +03:00
Anton Medvedev
ac5678eb92 Add better error messages 2020-06-17 17:17:14 +03:00
Anton Medvedev
0307f129f5
Update DOCS.md 2020-03-20 10:39:04 +03:00
Anton Medvedev
dfc4815dbf Release 18.0.1 2020-02-12 16:37:20 +03:00
Anton Medvedev
7ed688602e Disable circular refs in LosslessJSON 2020-02-12 16:36:42 +03:00
Anton Medvedev
f7a37e5b98 Release 18.0.0 2020-01-09 12:53:49 +03:00
Anton Medvedev
e7619aeece Update DOCS.md 2020-01-09 12:51:16 +03:00
Anton Medvedev
e5aef9ff32 Add code snippet to error message 2020-01-09 12:03:01 +03:00
Anton Medvedev
973ceb92da Add @ as map operation prefix 2020-01-08 18:35:14 +03:00
Anton Medvedev
1744e178fa
Update install.sh 2020-01-08 15:38:41 +03:00
Anton Medvedev
57f4069b4b
Update README.md 2019-12-29 18:29:01 +03:00
Anton Medvedev
7fffb5db9d
Update package.json 2019-12-29 18:24:39 +03:00
Anton Medvedev
001252c1ca Release 17.0.0 2019-12-25 16:30:47 +03:00
Anton Medvedev
de3d60b01d Add support for query languages 2019-12-25 16:29:43 +03:00
Anton Medvedev
290c2a0185 Add support for .[0] syntax 2019-12-25 16:10:37 +03:00
Anton Medvedev
ed6130907e
Update README.md 2019-12-15 09:28:07 +07:00
Anton Medvedev
6da433446c
Update README.md 2019-12-15 09:26:59 +07:00
Anton Medvedev
c456134a71
Update README.md 2019-12-15 09:19:27 +07:00
Anton Medvedev
7f53e96a93 Add curl|bash 2019-12-15 09:12:49 +07:00
Anton Medvedev
c7057cce2d Add install.sh 2019-12-15 08:58:00 +07:00
Anton Medvedev
ca6e4def1a Release 16.0.0 2019-12-13 12:56:51 +07:00
Anton Medvedev
7426016d6b Add lossless json 2019-12-13 12:48:43 +07:00
Anton Medvedev
98d886da20 Release 15.0.1 2019-12-09 13:37:46 +07:00
Anton Medvedev
81c71e1d2d Fix missing file 2019-12-09 13:36:53 +07:00
Anton Medvedev
d12daf3c83 Release 15.0.0 2019-12-09 13:32:37 +07:00
Anton Medvedev
58324051ae Add comment about terminal resize 2019-12-09 13:31:13 +07:00
Anton Medvedev
f60d7aa89c Add edit-in-place feature 2019-12-09 13:24:33 +07:00
Anton Medvedev
9713a057c1 Update README.md 2019-12-07 17:09:58 +07:00
Anton Medvedev
6b4e7254c5
Fix some of terminal resize issues (#116) 2019-12-07 16:46:41 +07:00
Anton Medvedev
a39e80b5b0 Update after bump hook 2019-11-06 11:19:26 +03:00
Anton Medvedev
314843362e Release 14.1.0 2019-11-06 11:10:35 +03:00
Anton Medvedev
a0fe5aae43 Update usage 2019-11-06 11:07:28 +03:00
Anton Medvedev
a16fa4560a Update package.json 2019-11-06 11:07:13 +03:00
Anton Medvedev
e8fd8353fe Set cursor to found pattern line 2019-11-06 10:57:43 +03:00
Anton Medvedev
33ea17be93 Fix expand all under cursor and add docs 2019-11-06 10:16:22 +03:00
Nate Eagleson
027b1a8dad Improve grammar in DOCS.md (#108)
Just a bunch of tweaks to improve the document's readability.
2019-08-29 10:35:44 +07:00
Anton Medvedev
51837b4831 Remove Dockerfile 2019-04-04 13:34:54 +07:00
Anton Medvedev
2cf71999ae Use afterBump 2019-04-04 13:32:00 +07:00
Anton Medvedev
7d23c8d811 Release 2019-04-04 13:25:45 +07:00
Anton Medvedev
ae0b739796 Update package.json 2019-04-04 13:25:41 +07:00
Anton Medvedev
332cb40a90 Add bump before release 2019-04-04 13:23:03 +07:00
Anton Medvedev
acf0255e1d Improve up/down cursor movement 2019-04-04 13:19:46 +07:00
Anton Medvedev
0889b9683d Drain cache 2019-04-04 11:37:49 +07:00
Anton Medvedev
575666fd1e Release 14.0.0. 2019-04-04 11:30:30 +07:00
Anton Medvedev
83efa1d109 Add collapse/expand all under cursor functionality 2019-04-04 10:46:17 +07:00
Anton Medvedev
fd2a30b79e Refactor parent collapsing algorithm 2019-04-04 10:23:06 +07:00
Ross Hadden
d216e02cb3 Collapse from anywhere (#102)
* Collapse from anywhere within child, instead of just when highlighting the expanded node itself.

Signed-off-by: Ross Hadden <rosshadden@gmail.com>

* Moved cursor to parent.

Signed-off-by: Ross Hadden <rosshadden@gmail.com>

* Properly handled collapsing arrays.

Signed-off-by: Ross Hadden <rosshadden@gmail.com>
2019-04-04 10:07:13 +07:00
Anton Medvedev
35526cc4d3 Move select helper 2019-04-04 00:16:04 +07:00
Anton Medvedev
5c228ccd77 Update DOCS.md 2019-04-04 00:14:28 +07:00
Anton Medvedev
42c81fd484 Update deps 2019-04-04 00:14:28 +07:00
Anton Medvedev
0382a0b2d7 Catch stream errors gracefully 2019-04-04 00:14:28 +07:00
Anton Medvedev
772bb83a19
Update README.md 2019-03-09 12:04:43 +07:00
Anton Medvedev
e4e41c7391 Release 13.0.0 2019-03-08 23:30:49 +07:00
Anton Medvedev
776d4d34a2 Update README.md 2019-03-08 23:30:17 +07:00
Anton Medvedev
73298e88a4 Improve performance for really big JSON files 2019-03-08 23:18:55 +07:00
Anton Medvedev
b1f393013c
Update README.md 2019-03-08 14:11:21 +07:00
Anton Medvedev
2618fdaa56 Rename docs.md 2019-03-07 23:47:04 +07:00
Anton Medvedev
1ecc258ca6 Update toc 2019-03-07 23:45:37 +07:00
Anton Medvedev
33f2e861ff Add toc 2019-03-07 23:38:40 +07:00
Anton Medvedev
ce4b19cce5
Update docs.md 2019-03-07 18:29:05 +07:00
Anton Medvedev
407c034f3b Update README.md 2019-03-01 08:38:51 +07:00
Anton Medvedev
6d2a1d9947 Remove snap
Build system are failing for a week now. Unreliable distribution system.
2019-03-01 08:34:26 +07:00
Anton Medvedev
592e799e10
Update README.md 2019-02-28 23:21:19 +07:00
Anton Medvedev
4fd6fdf39f
Update README.md 2019-02-28 23:20:35 +07:00
Anton Medvedev
7099560512 Release 12.0.2 2019-02-26 02:07:55 +07:00
Anton Medvedev
bfb8aa4534 Fix bug in stream processor 2019-02-26 02:06:12 +07:00
Anton Medvedev
47b5bb2366 Release 12.0.1 2019-02-25 12:03:20 +07:00
Anton Medvedev
d6cfe05fed Fix bug with detecting escaped quote 2019-02-25 12:01:06 +07:00
Anton Medvedev
9f48ff7011 Release 12.0.0 2019-02-25 02:03:05 +07:00
Anton Medvedev
4fd5e21432 Update README.md 2019-02-25 02:01:39 +07:00
Anton Medvedev
7c01d735a7 Fix unexpected usage info on stream end 2019-02-25 01:58:09 +07:00
Anton Medvedev
0e60886e0d Update docs 2019-02-25 01:57:47 +07:00
Anton Medvedev
cdd3d5abbe Update README.md 2019-02-25 01:42:01 +07:00
Anton Medvedev
d571194fc8 Add streaming support 2019-02-25 01:41:01 +07:00
Anton Medvedev
6236a5f942 Refactor 2019-02-24 21:22:55 +07:00
Anton Medvedev
c116bb749b Add docker image 2019-02-24 15:58:28 +07:00
Anton Medvedev
c8a6c4af0b Update README.md 2019-02-24 15:01:52 +07:00
Anton Medvedev
a7b89365bf Add comment 2019-02-24 15:01:18 +07:00
Anton Medvedev
12ff839365 Update deps 2019-02-24 14:28:14 +07:00
Anton Medvedev
ce0b1a5cc6 Update README.md 2019-02-24 13:57:23 +07:00
Anton Medvedev
8bd892c11e Update README.md 2019-02-24 13:56:45 +07:00
Anton Medvedev
4f3a52fa32 Update README.md 2019-02-24 13:55:53 +07:00
Sylvain Pace
3da7e22893 Update docs.md (#89) 2019-02-19 20:51:14 +07:00
Anton Medvedev
f321b8a58f Release 11.1.0 2019-02-09 23:50:27 +07:00
Anton Medvedev
ff452e2b5b Update README.md 2019-02-09 23:49:36 +07:00
Anton Medvedev
2d3becc9e4 Add big bang of life 2019-02-09 23:48:13 +07:00
Anton Medvedev
07f8e6e18d Add version info 2019-02-09 20:12:42 +07:00
Anton Medvedev
95b40b31b8 Fix bug with empty pattern 2019-02-09 18:12:22 +07:00
58 changed files with 3809 additions and 1353 deletions

23
.github/workflows/release.mjs vendored Normal file
View File

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

116
.github/workflows/release.yml vendored Normal file
View File

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

21
.github/workflows/test.yml vendored Normal file
View File

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

3
.gitignore vendored
View File

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

View File

@ -1,6 +0,0 @@
tasks:
- init: >
npm install &&
npm run build
command: >
./dist/fx-linux package.json

View File

@ -1,5 +0,0 @@
language: node_js
node_js:
- "node"
- "10"
- "8"

123
README.md
View File

@ -1,120 +1,93 @@
<p align="center"><img src="https://medv.io/assets/fx-logo.png" height="100" alt="fx logo"></p>
<p align="center"><img src="https://medv.io/assets/fx.gif" width="562" alt="fx example"></p>
<p align="center"><a href="https://fx.wtf"><img src="https://medv.io/assets/fx/fx-preview.gif" width="500" alt="fx preview"></a></p>
_* Function eXecution_
[![Build Status](https://travis-ci.org/antonmedv/fx.svg?branch=master)](https://travis-ci.org/antonmedv/fx)
[![Npm Version](https://img.shields.io/npm/v/fx.svg)](https://www.npmjs.com/package/fx)
[![Brew Version](https://img.shields.io/homebrew/v/fx.svg)](https://formulae.brew.sh/formula/fx)
[![Snap Version](https://img.shields.io/badge/snap-11.0.1-blue.svg)](https://snapcraft.io/fx)
Command-line JSON processing tool
## Features
* Formatting and highlighting
* Standalone binary
* Interactive mode 🎉
* Themes support 🎨
- Mouse support
- Streaming support
- Preserves key order
- Preserves big numbers
## Install
```bash
brew install fx
```
$ npm install -g fx
```bash
snap install fx
```
Or via Homebrew
```bash
scoop install fx
```
$ brew install fx
```bash
pacman -S fx
```
```bash
pkg install fx
```
```bash
go install github.com/antonmedv/fx@latest
```
Or download standalone binary from [releases](https://github.com/antonmedv/fx/releases) page.
<p>
<a href="https://www.patreon.com/antonmedv"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160"></a>
<a href="https://www.wispay.io/t/ZQb" target="_blank"><img src="https://assets.wispay.io/wgt2_d_b.png" height="60"></a>
</p>
Or download [pre-built binary](https://github.com/antonmedv/fx/releases).
## Usage
Start [interactive mode](https://github.com/antonmedv/fx/blob/master/docs.md#interactive-mode) without passing any arguments.
```
$ curl ... | fx
```
Start the interactive viewer via:
Or by passing filename as first argument.
```
$ fx data.json
```
Pipe into `fx` any JSON and anonymous function for reducing it.
```bash
$ curl ... | fx 'json => json.message'
fx data.json
```
Or same as above but short.
Or
```bash
$ curl ... | fx this.message
$ curl ... | fx .message
curl ... | fx
```
Pass any numbers of arguments as code.
Type `?` to see full list of key shortcuts.
Pretty print:
```bash
$ curl ... | fx 'json => json.message' 'json => json.filter(x => x.startsWith("a"))'
curl ... | fx .
```
Access all lodash (or ramda, etc) methods by using [.fxrc](https://github.com/antonmedv/fx/blob/master/docs.md#using-fxrc) file.
### Reducers
Write reducers in your favorite language: [JavaScript](doc/js.md) (default),
[Python](doc/python.md), or [Ruby](doc/ruby.md).
```bash
$ curl ... | fx '_.groupBy("commit.committer.name")' '_.mapValues(_.size)'
fx data.json '.filter(x => x.startsWith("a"))'
```
Update JSON using spread operator.
```bash
$ echo '{"count": 0}' | fx '{...this, count: 1}'
{
"count": 1
}
fx data.json '[x["age"] + i for i in range(10)]'
```
Pretty print JSON with dot.
```bash
$ curl ... | fx .
fx data.json 'x.to_a.map {|x| x[1]}'
```
## Documentation
See full [documentation](https://github.com/antonmedv/fx/blob/master/docs.md).
See full [documentation](doc/doc.md).
## Links
## Themes
* [Discover how to use fx effectively](https://medium.com/@antonmedv/discover-how-to-use-fx-effectively-668845d2a4ea)
## Related
* [xx](https://github.com/antonmedv/xx) - fx-like JSON tool (*go*)
* [ymlx](https://github.com/matthewadams/ymlx) - fx-like YAML cli processor
* [fx-theme-monokai](https://github.com/antonmedv/fx-theme-monokai) monokai theme
* [fx-theme-night](https://github.com/antonmedv/fx-theme-night) night theme
## Contributing
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/antonmedv/fx)
Or clone locally and run:
Theme can be configured by setting environment variable `FX_THEME` from `1`
to `9`:
```bash
# install dependencies
npm install
# run fx
node index.js package.json
# run the build
npm run build
# try the built binary
./dist/fx-linux package.json
export FX_THEME=9
```
<img width="1214" alt="themes" src="doc/images/themes.png">
Add your own themes in [theme.go](pkg/theme/theme.go) file.
## License
[MIT](https://github.com/antonmedv/fx/blob/master/LICENSE)
[MIT](LICENSE)

View File

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

66
doc/doc.md Normal file
View File

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

BIN
doc/images/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

BIN
doc/images/themes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

120
doc/js.md Normal file
View File

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

27
doc/python.md Normal file
View File

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

23
doc/ruby.md Normal file
View File

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

223
docs.md
View File

@ -1,223 +0,0 @@
# Documentation
`fx` can work in two modes: cli and interactive. To start interactive mode pipe into `fx` any JSON:
```bash
$ curl ... | fx
```
Or you can pass file argument as first parameter:
```bash
$ fx my.json
```
If any argument was passed, `fx` will apply it and prints to stdout.
## Anonymous function
Use an anonymous function as reducer which gets JSON and processes it:
```bash
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo[0].bar'
value
```
## Binding
If you don't pass anonymous function `param => ...`, code will be automatically transformed into anonymous function.
And you can get access to JSON by `this` keyword:
```bash
$ echo '{"foo": [{"bar": "value"}]}' | fx 'this.foo[0].bar'
value
```
## Dot
It is possible to omit `this` keyword:
```bash
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
value
```
If single dot is passed, JSON will be processed without modification:
```bash
$ echo '{"foo": "bar"}' | fx .
{
"foo": "bar"
}
```
## Chain
You can pass any number of anonymous functions for reducing JSON:
```bash
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo' 'this[0]' 'this.bar'
value
```
## Generator
If passed code contains `yield` keyword, [generator expression](https://github.com/sebmarkbage/ecmascript-generator-expression)
will be used:
```bash
$ curl ... | fx 'for (let user of this) if (user.login.startsWith("a")) yield user'
```
Access to JSON through `this` keyword:
```bash
$ echo '["a", "b"]' | fx 'yield* this'
[
"a",
"b"
]
```
```bash
$ echo '["a", "b"]' | fx 'yield* this; yield "c";'
[
"a",
"b",
"c"
]
```
## Update
You can update existing JSON using spread operator:
```bash
$ echo '{"count": 0}' | fx '{...this, count: 1}'
{
"count": 1
}
```
## Using packages
Use any npm package by installing it globally:
```bash
$ npm install -g lodash
$ cat package.json | fx 'require("lodash").keys(this.dependencies)'
```
## Using .fxrc
Create _.fxrc_ file in `$HOME` directory, and require any packages or define global functions.
For example, access all lodash methods without `_` prefix. Put in your `.fxrc` file:
```js
Object.assign(global, require('lodash/fp'))
```
And now you will be able to call all lodash methods. For example, see who's been committing to react recently:
```bash
curl 'https://api.github.com/repos/facebook/react/commits?per_page=100' \
| fx 'groupBy("commit.author.name")' 'mapValues(size)' toPairs 'sortBy(1)' reverse 'take(10)' fromPairs
```
> To be able require global modules make sure you have correct `NODE_PATH` env variable.
> ```bash
> export NODE_PATH=/usr/local/lib/node_modules
> ```
## Edit in place
Add next code to your _.fxrc_ file:
```js
const fs = require('fs')
global.save = json => {
fs.writeFileSync(process.argv[2], JSON.stringify(json, null, 2))
return json
}
```
Usage:
```bash
fx data.json '{...this, count: this.count+1}' save .count
```
## Formatting
If you need something different then JSON (for example arguments for xargs) do not return anything from reducer.
`undefined` value is printed into stderr by default.
```bash
echo '[]' | fx 'void 0'
undefined
```
```bash
echo '[1,2,3]' | fx 'this.forEach(x => console.log(x))' 2>/dev/null | xargs echo
1 2 3
```
## Other examples
Convert object to array:
```bash
$ cat package.json | fx 'Object.keys(this.dependencies)'
[
"@medv/prettyjson"
]
```
By the way, fx has shortcut for `Object.keys(this)`. Previous example can be rewritten as:
```bash
$ cat package.json | fx this.dependencies ?
```
## Interactive mode
Click on fields to expand or collapse JSON tree, use mouse wheel to scroll view.
Next commands available in interactive mode:
| Key | Command |
|-------------------------------|-------------------------|
| `q` or `Esc` or `Ctrl`+`c` | Exit |
| `e`/`E` | Expand/Collapse all |
| `g`/`G` | Goto top/bottom |
| `up`/`down` or `k/j` | Move cursor up/down |
| `left`/`right` or `h/l` | Expand/Collapse |
| `.` | Edit filter |
| `/` | Search |
| `n` | Goto next found pattern |
These commands are available when editing the filter:
| Key | Command |
|-------------------------------|-------------------------|
| `Enter` | Apply filter |
| `Ctrl`+`u` | Clear filter |
| `Ctrl`+`w` | Delete last part |
| `up`/`down` | Select autocomplete |
### Search
Press `/` and type regexp pattern to search in current JSON. Search work with currently applied filter.
Examples of pattern and corresponding regexp:
| Pattern | RegExp |
|------------|-------------|
| `/apple` | `/apple/ig` |
| `/apple/` | `/apple/` |
| `/apple/u` | `/apple/u` |
| `/\w+` | `/\w+/ig` |
### Selecting text
You may found what you can't just select text in fx. This is due the fact that all mouse events redirected to stdin. To be able select again you need instruct your terminal not to do it. This can be done by holding special keys while selecting:
| Key | Terminal |
|------------------|---------------|
| `Option`+`Mouse` | iTerm2, Hyper |
| `Fn`+`Mouse` | Terminal.app |
| `Shift`+`Mouse` | Linux |

35
find.js
View File

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

617
fx.js
View File

@ -1,617 +0,0 @@
'use strict'
const fs = require('fs')
const tty = require('tty')
const blessed = require('@medv/blessed')
const stringWidth = require('string-width')
const reduce = require('./reduce')
const print = require('./print')
const find = require('./find')
const config = require('./config')
module.exports = function start(filename, source) {
// Current rendered object on a screen.
let json = source
// Contains map from row number to expand path.
// Example: {0: '', 1: '.foo', 2: '.foo[0]'}
let index = new Map()
// Contains expanded paths. Example: ['', '.foo']
// Empty string represents root path.
const expanded = new Set()
expanded.add('')
// Current search regexp and generator.
let highlight = null
let findGen = null
let currentPath = null
// Reopen tty
let ttyReadStream
let ttyWriteStream
if (process.platform === 'win32') {
const cfs = process.binding('fs')
ttyReadStream = tty.ReadStream(cfs.open('conin$', fs.constants.O_RDWR | fs.constants.O_EXCL, 0o666))
ttyWriteStream = tty.WriteStream(cfs.open('conout$', fs.constants.O_RDWR | fs.constants.O_EXCL, 0o666))
} else {
const ttyFd = fs.openSync('/dev/tty', 'r+')
ttyReadStream = tty.ReadStream(ttyFd)
ttyWriteStream = tty.WriteStream(ttyFd)
}
const program = blessed.program({
input: ttyReadStream,
output: ttyWriteStream,
})
const screen = blessed.screen({
program: program,
smartCSR: true,
fullUnicode: true,
})
const box = blessed.box({
parent: screen,
tags: false,
left: 0,
top: 0,
width: '100%',
height: '100%',
mouse: true,
keys: true,
vi: true,
ignoreArrows: true,
alwaysScroll: true,
scrollable: true,
})
const input = blessed.textbox({
parent: screen,
bottom: 0,
left: 0,
height: 1,
width: '100%',
})
const search = blessed.textbox({
parent: screen,
bottom: 0,
left: 0,
height: 1,
width: '100%',
})
const statusBar = blessed.box({
parent: screen,
tags: false,
bottom: 0,
left: 0,
height: 1,
width: '100%',
})
const autocomplete = blessed.list({
parent: screen,
width: 6,
height: 7,
left: 1,
bottom: 1,
style: config.list,
})
screen.title = filename
box.focus()
input.hide()
search.hide()
statusBar.hide()
autocomplete.hide()
screen.key(['escape', 'q', 'C-c'], function () {
program.disableMouse() // If exit program immediately, stdin may still receive
setTimeout(() => process.exit(0), 10) // mouse events which will be printed in stdout.
})
screen.on('resize', function () {
render()
})
input.on('submit', function () {
if (autocomplete.hidden) {
const code = input.getValue()
if (/^\//.test(code)) {
// Forgive a mistake to the user. This looks like user wanted to search something.
apply('')
applyPattern(code)
} else {
apply(code)
}
} else {
// Autocomplete selected
let code = input.getValue()
let replace = autocomplete.getSelected()
if (/^[a-z]\w*$/i.test(replace)) {
replace = '.' + replace
} else {
replace = `["${replace}"]`
}
code = code.replace(/\.\w*$/, replace)
input.setValue(code)
autocomplete.hide()
update(code)
// Keep editing code
input.readInput()
}
})
input.on('cancel', function () {
if (autocomplete.hidden) {
const code = input.getValue()
apply(code)
} else {
// Autocomplete not selected
autocomplete.hide()
screen.render()
// Keep editing code
input.readInput()
}
})
input.on('update', function (code) {
update(code)
complete(code)
})
input.key('up', function () {
if (!autocomplete.hidden) {
autocomplete.up()
screen.render()
}
})
input.key('down', function () {
if (!autocomplete.hidden) {
autocomplete.down()
screen.render()
}
})
input.key('C-u', function () {
input.setValue('')
update('')
render()
})
input.key('C-w', function () {
let code = input.getValue()
code = code.replace(/[\.\[][^\.\[]*$/, '')
input.setValue(code)
update(code)
render()
})
search.on('submit', function (pattern) {
applyPattern(pattern)
})
search.on('cancel', function () {
highlight = null
currentPath = null
search.hide()
search.setValue('')
box.height = '100%'
box.focus()
program.cursorPos(0, 0)
render()
})
box.key('.', function () {
hideStatusBar()
box.height = '100%-1'
input.show()
if (input.getValue() === '') {
input.setValue('.')
complete('.')
}
input.readInput()
screen.render()
})
box.key('/', function () {
hideStatusBar()
box.height = '100%-1'
search.show()
search.setValue('/')
search.readInput()
screen.render()
})
box.key('e', function () {
hideStatusBar()
expanded.clear()
for (let path of dfs(json)) {
if (expanded.size < 1000) {
expanded.add(path)
} else {
break
}
}
render()
})
box.key('S-e', function () {
hideStatusBar()
expanded.clear()
expanded.add('')
render()
// Make sure cursor stay on JSON object.
const [n] = getLine(program.y)
if (typeof n === 'undefined' || !index.has(n)) {
// No line under cursor
let rest = [...index.keys()]
if (rest.length > 0) {
const next = Math.max(...rest)
let y = box.getScreenNumber(next) - box.childBase
if (y <= 0) {
y = 0
}
const line = box.getScreenLine(y + box.childBase)
program.cursorPos(y, line.search(/\S/))
}
}
})
box.key('n', function () {
hideStatusBar()
findNext()
})
box.key(['up', 'k'], function () {
hideStatusBar()
program.showCursor()
let rest = [...index.keys()]
const [n] = getLine(program.y)
if (typeof n !== 'undefined') {
rest = rest.filter(i => i < n)
}
if (rest.length > 0) {
const next = Math.max(...rest)
let y = box.getScreenNumber(next) - box.childBase
if (y <= 0) {
box.scroll(-1)
screen.render()
y = 0
}
const line = box.getScreenLine(y + box.childBase)
program.cursorPos(y, line.search(/\S/))
}
})
box.key(['down', 'j'], function () {
hideStatusBar()
program.showCursor()
let rest = [...index.keys()]
const [n] = getLine(program.y)
if (typeof n !== 'undefined') {
rest = rest.filter(i => i > n)
}
if (rest.length > 0) {
const next = Math.min(...rest)
let y = box.getScreenNumber(next) - box.childBase
if (y >= box.height) {
box.scroll(1)
screen.render()
y = box.height - 1
}
const line = box.getScreenLine(y + box.childBase)
program.cursorPos(y, line.search(/\S/))
}
})
box.key(['right', 'l'], function () {
hideStatusBar()
const [n, line] = getLine(program.y)
program.showCursor()
program.cursorPos(program.y, line.search(/\S/))
const path = index.get(n)
if (!expanded.has(path)) {
expanded.add(path)
render()
}
})
box.key(['left', 'h'], function () {
hideStatusBar()
const [n, line] = getLine(program.y)
program.showCursor()
program.cursorPos(program.y, line.search(/\S/))
const path = index.get(n)
if (expanded.has(path)) {
expanded.delete(path)
render()
}
})
box.on('click', function (mouse) {
hideStatusBar()
const [n, line] = getLine(mouse.y)
if (mouse.x >= stringWidth(line)) {
return
}
program.hideCursor()
program.cursorPos(mouse.y, line.search(/\S/))
autocomplete.hide()
const path = index.get(n)
if (expanded.has(path)) {
expanded.delete(path)
} else {
expanded.add(path)
}
render()
})
box.on('scroll', function () {
hideStatusBar()
})
function getLine(y) {
const dy = box.childBase + y
const n = box.getNumber(dy)
const line = box.getScreenLine(dy)
if (typeof line === 'undefined') {
return [n, '']
}
return [n, line]
}
function apply(code) {
if (code && code.length !== 0) {
try {
json = reduce(source, code)
} catch (e) {
// pass
}
} else {
box.height = '100%'
input.hide()
json = source
}
box.focus()
program.cursorPos(0, 0)
render()
}
function complete(inputCode) {
const match = inputCode.match(/\.(\w*)$/)
const code = /^\.\w*$/.test(inputCode) ? '.' : inputCode.replace(/\.\w*$/, '')
let json
try {
json = reduce(source, code)
} catch (e) {
}
if (match) {
if (typeof json === 'object' && json.constructor === Object) {
const keys = Object.keys(json).filter(key => key.startsWith(match[1]))
// Hide if there is nothing to show or
// don't show if there is complete match.
if (keys.length === 0 || (keys.length === 1 && keys[0] === match[1])) {
autocomplete.hide()
return
}
autocomplete.width = Math.max(...keys.map(key => key.length)) + 1
autocomplete.height = Math.min(7, keys.length)
autocomplete.left = Math.min(
screen.width - autocomplete.width,
code.length === 1 ? 1 : code.length + 1
)
let selectFirst = autocomplete.items.length !== keys.length
autocomplete.setItems(keys)
if (selectFirst) {
autocomplete.select(autocomplete.items.length - 1)
}
if (autocomplete.hidden) {
autocomplete.show()
}
} else {
autocomplete.clearItems()
autocomplete.hide()
}
}
}
function update(code) {
if (code && code.length !== 0) {
try {
const pretender = reduce(source, code)
if (
typeof pretender !== 'undefined'
&& typeof pretender !== 'function'
&& !(pretender instanceof RegExp)
) {
json = pretender
}
} catch (e) {
// pass
}
}
if (code === '') {
json = source
}
findGen = find(json, highlight)
render()
}
function applyPattern(pattern) {
let regex
let m = pattern.match(/^\/(.*)\/([gimuy]*)$/)
if (m) {
try {
regex = new RegExp(m[1], m[2])
} catch (e) {
// Wrong regexp.
}
} else {
m = pattern.match(/^\/(.*)$/)
if (m) {
try {
regex = new RegExp(m[1], 'gi')
} catch (e) {
// Wrong regexp.
}
}
}
highlight = regex
if (highlight) {
findGen = find(json, highlight)
findNext()
} else {
findGen = null
currentPath = null
}
search.hide()
search.setValue('')
box.height = '100%'
box.focus()
program.cursorPos(0, 0)
render()
}
function findNext() {
if (!findGen) {
return
}
const {value: path, done} = findGen.next()
if (done) {
showStatusBar('Pattern not found')
} else {
currentPath = ''
for (let p of path) {
expanded.add(currentPath += p)
}
render()
for (let [k, v] of index) {
if (v === currentPath) {
let y = box.getScreenNumber(k)
// Scroll one line up for better view and make sure it's not negative.
if (--y < 0) {
y = 0
}
box.scrollTo(y)
screen.render()
}
}
}
}
function showStatusBar(status) {
statusBar.show()
statusBar.setContent(config.statusBar(` ${status} `))
screen.render()
}
function hideStatusBar() {
if (!statusBar.hidden) {
statusBar.hide()
statusBar.setContent('')
screen.render()
}
}
function render() {
let content
[content, index] = print(json, {expanded, highlight, currentPath})
if (typeof content === 'undefined') {
content = 'undefined'
}
box.setContent(content)
screen.render()
}
render()
}
function* bfs(json) {
const queue = [[json, '']]
while (queue.length > 0) {
const [v, path] = queue.shift()
if (!v) {
continue
}
if (Array.isArray(v)) {
yield path
let i = 0
for (let item of v) {
const p = path + '[' + (i++) + ']'
queue.push([item, p])
}
}
if (typeof v === 'object' && v.constructor === Object) {
yield path
for (let [key, value] of Object.entries(v)) {
const p = path + '.' + key
queue.push([value, p])
}
}
}
}
function* dfs(v, path = '') {
if (!v) {
return
}
if (Array.isArray(v)) {
yield path
let i = 0
for (let item of v) {
yield* dfs(item, path + '[' + (i++) + ']')
}
}
if (typeof v === 'object' && v.constructor === Object) {
yield path
for (let [key, value] of Object.entries(v)) {
yield* dfs(value, path + '.' + key)
}
}
}

33
go.mod Normal file
View File

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

96
go.sum Normal file
View File

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

71
help.go Normal file
View File

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

View File

@ -1,98 +0,0 @@
#!/usr/bin/env node
'use strict'
const os = require('os')
const fs = require('fs')
const path = require('path')
const {stdin, stdout, stderr} = process
try {
require(path.join(os.homedir(), '.fxrc'))
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') {
throw err
}
}
const print = require('./print')
const reduce = require('./reduce')
const usage = `
Usage
$ fx [code ...]
Examples
$ echo '{"key": "value"}' | fx 'x => x.key'
value
$ echo '{"key": "value"}' | fx .key
value
$ echo '[1,2,3]' | fx 'this.map(x => x * 2)'
[2, 4, 6]
$ echo '{"items": ["one", "two"]}' | fx 'this.items' 'this[1]'
two
$ echo '{"count": 0}' | fx '{...this, count: 1}'
{"count": 1}
$ echo '{"foo": 1, "bar": 2}' | fx ?
["foo", "bar"]
`
function main(input) {
let args = process.argv.slice(2)
let filename = 'fx'
if (input === '') {
if (args.length === 0 || (args.length === 1 && (args[0] === '-h' || args[0] === '--help'))) {
stderr.write(usage)
process.exit(2)
}
input = fs.readFileSync(args[0])
filename = path.basename(args[0])
args = args.slice(1)
}
const json = JSON.parse(input)
if (args.length === 0 && stdout.isTTY) {
require('./fx')(filename, json)
return
}
const output = args.reduce(reduce, json)
if (typeof output === 'undefined') {
stderr.write('undefined\n')
} else if (typeof output === 'string') {
console.log(output)
} else {
const [text] = print(output)
console.log(text)
}
}
function run() {
stdin.setEncoding('utf8')
if (stdin.isTTY) {
main('')
return
}
let buff = ''
stdin.on('readable', () => {
let chunk
while ((chunk = stdin.read())) {
buff += chunk
}
})
stdin.on('end', () => {
main(buff)
})
}
run()

121
keymap.go Normal file
View File

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

634
main.go Normal file
View File

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

View File

@ -1,50 +0,0 @@
{
"name": "fx",
"version": "11.0.1",
"description": "Command-line JSON viewer",
"repository": "antonmedv/fx",
"author": "Anton Medvedev <anton@medv.io>",
"license": "MIT",
"bin": {
"fx": "index.js"
},
"files": [
"config.js",
"find.js",
"fx.js",
"index.js",
"print.js",
"reduce.js"
],
"scripts": {
"test": "ava",
"build": "pkg . --out-path dist -t node8-linux-x64,node8-macos-x64,node8-win-x64",
"zip": "cd dist && find . -name 'fx-*' -exec zip '{}.zip' '{}' \\;",
"release": "rm -rf ./dist && npm run build && npm run zip && release-it --github.release --github.assets=dist/*.zip"
},
"keywords": [
"json",
"viewer",
"cli",
"terminal",
"term",
"console",
"ascii",
"unicode",
"blessed"
],
"engines": {
"node": ">=8"
},
"dependencies": {
"@medv/blessed": "^2.0.0",
"chalk": "^2.4.2",
"indent-string": "^3.2.0",
"string-width": "^3.0.0"
},
"devDependencies": {
"ava": "^1.2.0",
"pkg": "^4.3.7",
"release-it": "^10.0.7"
}
}

26
pkg/dict/dict.go Normal file
View File

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

65
pkg/dict/dict_test.go Normal file
View File

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

86
pkg/json/parse.go Normal file
View File

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

51
pkg/json/parse_test.go Normal file
View File

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

71
pkg/json/pretty_print.go Normal file
View File

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

51
pkg/json/stringify.go Normal file
View File

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

View File

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

47
pkg/json/traverse.go Normal file
View File

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

6
pkg/json/types.go Normal file
View File

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

98
pkg/reducer/js.go Normal file
View File

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

12
pkg/reducer/js.js Normal file
View File

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

83
pkg/reducer/node.go Normal file
View File

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

31
pkg/reducer/node.js Normal file
View File

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

38
pkg/reducer/python.go Normal file
View File

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

9
pkg/reducer/python.py Normal file
View File

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

76
pkg/reducer/reduce.go Normal file
View File

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

37
pkg/reducer/ruby.go Normal file
View File

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

6
pkg/reducer/ruby.rb Normal file
View File

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

215
pkg/reducer/simple.go Normal file
View File

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

137
pkg/reducer/simple_test.go Normal file
View File

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

51
pkg/reducer/utils.go Normal file
View File

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

185
pkg/theme/theme.go Normal file
View File

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

296
print.go Normal file
View File

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

113
print.js
View File

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

View File

@ -1,36 +0,0 @@
'use strict'
function reduce(json, code) {
if (/^\./.test(code)) {
const fx = eval(`function fn() {
return ${code === '.' ? 'this' : 'this' + code}
}; fn`)
return fx.call(json)
}
if ('?' === code) {
return Object.keys(json)
}
if (/yield\*?\s/.test(code)) {
const fx = eval(`function fn() {
const gen = (function*(){
${code.replace(/\\\n/g, '')}
}).call(this)
return [...gen]
}; fn`)
return fx.call(json)
}
const fx = eval(`function fn() {
return ${code}
}; fn`)
const fn = fx.call(json)
if (typeof fn === 'function') {
return fn(json)
}
return fn
}
module.exports = reduce

249
search.go Normal file
View File

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

190
search_test.go Normal file
View File

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

View File

@ -1,31 +1,20 @@
name: fx
base: core18 # the base snap is the execution environment for this snap
version: git
summary: Command-line tool and terminal JSON viewer
description: |
Command-line JSON processing tool
Features
* Formatting and highlighting
* Interactive mode
* Themes support
architectures:
- build-on: i386
- build-on: amd64
- build-on: armhf
- build-on: arm64
version: 24.0.0
summary: Terminal JSON viewer
description: Terminal JSON viewer
base: core18
grade: stable
confinement: strict
parts:
fx:
plugin: nodejs
node-engine: "8.14.1"
source: .
apps:
fx:
command: fx
plugs:
- network
plugs: [home, network]
parts:
fx:
plugin: go
go-channel: 1.18/stable
source: .
source-type: git
go-importpath: github.com/antonmedv/fx

42
stream.go Normal file
View File

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

42
test.js
View File

@ -1,42 +0,0 @@
'use strict'
const test = require('ava')
const {execSync} = require('child_process')
function fx(json, code = '') {
return execSync(`echo '${JSON.stringify(json)}' | node index.js ${code}`).toString('utf8')
}
test('pass', t => {
const r = fx([{"greeting": "hello world"}])
t.deepEqual(JSON.parse(r), [{"greeting": "hello world"}])
})
test('anon func', t => {
const r = fx({"key": "value"}, "'function (x) { return x.key }'")
t.deepEqual(r, 'value\n')
})
test('arrow func', t => {
const r = fx({"key": "value"}, "'x => x.key'")
t.deepEqual(r, 'value\n')
})
test('arrow func ()', t => {
const r = fx({"key": "value"}, "'(x) => x.key'")
t.deepEqual(r, 'value\n')
})
test('this bind', t => {
const r = fx([1, 2, 3, 4, 5], "'this.map(x => x * this.length)'")
t.deepEqual(JSON.parse(r), [5, 10, 15, 20, 25])
})
test('generator', t => {
const r = fx([1, 2, 3, 4, 5], "'for (let i of this) if (i % 2 == 0) yield i'")
t.deepEqual(JSON.parse(r), [2, 4])
})
test('chain', t => {
const r = fx({"items": ["foo", "bar"]}, "'this.items' 'yield* this' 'x => x[1]'")
t.deepEqual(r, 'bar\n')
})

50
util.go Normal file
View File

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

26
util_test.go Normal file
View File

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

3
version.go Normal file
View File

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

128
viewport.go Normal file
View File

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