Compare commits

..

336 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
Anton Medvedev
0f33ceb592 Release 11.0.1 2019-01-30 12:23:27 +07:00
Anton Medvedev
85f7c5724a Update README.md 2019-01-30 12:22:59 +07:00
Anton Medvedev
44d95c6c8d Fix bug with synchronous reopening of tty on unix 2019-01-30 12:21:39 +07:00
Anton Medvedev
7501e5a81b Style fix 2019-01-30 10:33:13 +07:00
Anton Medvedev
d283c6ec52 Release 11.0.0 2019-01-30 10:26:06 +07:00
Anton Medvedev
70385ff4f2 Update snap version 2019-01-30 10:24:29 +07:00
Anton Medvedev
afada8f60a Update README.md 2019-01-30 10:22:08 +07:00
Anton Medvedev
8812bf3393 Add compression of assets 2019-01-30 10:17:39 +07:00
Anton Medvedev
9ebeb62c28 Update LICENSE 2019-01-30 10:17:39 +07:00
Anton Medvedev
cebe263d3b Update deps 2019-01-30 10:17:39 +07:00
Conor Grocock
7b2e45eeba Added h and help argument support (#78) 2019-01-30 10:16:47 +07:00
Sven Efftinge
22a394047f Added gitpod config (#71) 2019-01-30 10:16:22 +07:00
Abdul Rauf
b99a05dd89 feat: add cmd support (#77)
Tested using node v8.15.0 in cmd
Note: Tests are still failing in cmd
2019-01-30 09:34:25 +07:00
Anton Medvedev
9dac9222e0
Update README.md 2019-01-21 00:49:26 +07:00
Wynn Netherland
9907ec7e58 Fix small typo 2019-01-08 12:36:15 +07:00
Anton Medvedev
1742da02f7
Update docs.md 2018-12-26 14:34:24 +07:00
Anton Medvedev
8999dee7d6 Remove unused var 2018-12-21 14:04:51 +07:00
Anton Medvedev
ceb177163d Add bfs 2018-12-21 14:00:18 +07:00
Anton Medvedev
fe57a92cfe Use generator for getting paths 2018-12-21 13:46:16 +07:00
Anton Medvedev
9a4d897c8d
Update snapcraft.yaml 2018-12-19 23:30:27 +07:00
Anton Medvedev
e0feb840fa
Update snapcraft.yaml 2018-12-19 23:23:57 +07:00
Anton Medvedev
62eccdeb80
Update README.md 2018-12-19 17:01:50 +07:00
Anton Medvedev
be55b22356 Add snap 2018-12-17 13:33:26 +07:00
Anton Medvedev
3e05caef82 Release 10.0.0 2018-12-15 15:22:31 +07:00
Anton Medvedev
eb6461f3a8 Fix style on search cancel 2018-12-15 15:04:22 +07:00
Anton Medvedev
35d5d741f4 Search on keys, not paths 2018-12-15 14:52:48 +07:00
Anton Medvedev
bb79c9812d Implement for advance highlighting 2018-12-15 14:52:48 +07:00
Anton Medvedev
3c891babf7 Update docs.md 2018-12-15 14:52:48 +07:00
Anton Medvedev
ef77520f99 Show status bar if pattern not found 2018-12-15 14:52:48 +07:00
Anton Medvedev
225239bf3d Forgive errors to user 2018-12-15 14:52:48 +07:00
Anton Medvedev
a696018fa5 Fix pattern creation 2018-12-15 14:52:48 +07:00
Anton Medvedev
beedbfd36a Separate regexp generation 2018-12-15 14:52:48 +07:00
Anton Medvedev
935857230d Update package.json 2018-12-15 14:52:48 +07:00
Anton Medvedev
6a7a628e03 Make current highlight path different 2018-12-15 14:52:48 +07:00
Anton Medvedev
9c1a7e8ae2 Remove console.error 2018-12-15 14:52:48 +07:00
Anton Medvedev
4986be32b5 Search feature 2018-12-15 14:52:48 +07:00
Alan Pope
83bed4a4ee Only build on supported architectures (#55)
As per comment in #53 - looks like nodejs isn't supported on [s390x]https://launchpadlibrarian.net/401523666/buildlog_snap_ubuntu_xenial_s390x_d862f303dc85a1886a1db80b5e691216-xenial_BUILDING.txt.gz) and [ppc64el](https://launchpadlibrarian.net/401523830/buildlog_snap_ubuntu_xenial_ppc64el_d862f303dc85a1886a1db80b5e691216-xenial_BUILDING.txt.gz). This change tells the build system to only build on the listed supported architectures.
2018-12-15 00:15:01 +07:00
Anton Medvedev
73f051f532
Update snapcraft.yaml 2018-12-14 22:16:32 +07:00
Alan Pope
cb5a16a90c Add snapcraft support for building snaps (#53)
* Add support to build snaps

* Update snapcraft.yaml

* Update snapcraft.yaml
2018-12-14 22:09:08 +07:00
Anton Medvedev
9660cb1932
Update docs.md 2018-12-13 12:43:12 +07:00
Anton Medvedev
f569036983
Update README.md 2018-12-13 12:40:21 +07:00
Anton Medvedev
780ab93897
Update docs.md 2018-12-12 10:40:13 +07:00
Anton Medvedev
c9f582d3ee
Use shields.io 2018-12-11 19:27:22 +07:00
Anton Medvedev
048ce5e040 Release 9.0.2 2018-12-11 15:15:41 +07:00
Anton Medvedev
e7bb9f3c7d Fix autocomplete for keys starting with upper case 2018-12-11 15:15:12 +07:00
Anton Medvedev
758ae8d1ea Release 9.0.1 2018-12-08 16:40:51 +07:00
Anton Medvedev
f22f8d052c Fix hanging cursor after collapse all 2018-12-08 16:38:59 +07:00
Anton Medvedev
a33c92a20a Release 9.0.0 2018-12-07 23:43:56 +07:00
Anton Medvedev
2598958557 Add better detection of functions in reduce 2018-12-07 23:33:55 +07:00
Anton Medvedev
4bfd5ccc42 Fix autocomplete for single letter keys 2018-12-07 22:52:50 +07:00
Anton Medvedev
669c36866b Improve JSON.stringify compatibility 2018-12-07 22:51:10 +07:00
Anton Medvedev
4d6db10684 Release 8.1.0 2018-12-05 09:55:15 +07:00
Alex
f892e8869d Add vim motions to box.key (j/k/h/l) 2018-12-05 09:54:00 +07:00
Anton Medvedev
b63088e94c
Update README.md 2018-12-03 23:05:21 +07:00
Anton Medvedev
6036e981ef
Update docs.md 2018-12-03 12:14:41 +07:00
Anton Medvedev
d8d8909b9e
Change gif 2018-12-03 12:08:40 +07:00
Anton Medvedev
9089e078f0 Release 8.0.0 2018-12-02 23:26:18 +07:00
Anton Medvedev
e062d567c6 Use LTS node 2018-12-02 23:24:47 +07:00
Anton Medvedev
40f88dcc91 Hide on escape 2018-12-02 23:24:47 +07:00
Anton Medvedev
b12ae5a243 Make autocomplete configurable 2018-12-02 23:24:47 +07:00
Anton Medvedev
c8cd6ceef5 Make autocomplete configurable 2018-12-02 23:24:47 +07:00
Anton Medvedev
a29d43017e Update README.md 2018-12-02 23:24:47 +07:00
Anton Medvedev
b5762bef22 Update README.md 2018-12-02 23:24:47 +07:00
Anton Medvedev
b93cda14d2 Update README.md 2018-12-02 23:24:47 +07:00
Anton Medvedev
f7e180deb2 Update README.md 2018-12-02 23:24:47 +07:00
Anton Medvedev
dc5fe9bf8f Separate docs 2018-12-02 23:24:47 +07:00
Anton Medvedev
838210b044 Update package.json 2018-12-02 23:24:47 +07:00
Anton Medvedev
30b683b5cd Better auto replacement 2018-12-02 23:24:47 +07:00
Anton Medvedev
ce4c407e00 Update package.json 2018-12-02 23:24:47 +07:00
Anton Medvedev
5e04d740aa Better navigation for complex JSON keys 2018-12-02 23:24:47 +07:00
Anton Medvedev
5db1c910f8 Add autocomplete 2018-12-02 23:24:47 +07:00
Anton Medvedev
82de76e1d4 Separate build and release 2018-12-02 23:24:47 +07:00
Anton Medvedev
74cf26fd0d Improved rendering speed 2018-12-02 23:24:47 +07:00
Anton Medvedev
a0c2931c57 Improve DX 2018-12-02 23:24:47 +07:00
Anton Medvedev
f110142160 New features
- Arrow navigation
- Interactive digger
- Themes support
2018-12-02 23:24:47 +07:00
Anton Medvedev
851427d8c8
Update README.md 2018-11-26 12:36:37 +07:00
Anton Medvedev
a09b27c8e4
Add brew 2018-11-16 18:07:22 +07:00
Anton Medvedev
89faa8bdb4
Update README.md 2018-11-14 17:44:56 +07:00
Anton Medvedev
ac4d3696a9
Update README.md 2018-11-14 17:44:37 +07:00
Anton Medvedev
6a4a813562
Update package.json 2018-11-13 18:26:10 +07:00
Anton Medvedev
5146fffcdd
Update README.md 2018-11-13 13:27:56 +07:00
Steven
9e34edeb75 Add badges to README (#33) 2018-11-13 12:05:13 +07:00
Anton Medvedev
82d2c29924
Update README.md 2018-11-12 13:30:19 +07:00
RDIL
351e96d292 Add link to license in readme file (#32) 2018-11-12 08:57:11 +07:00
Anton Medvedev
ce0099b87d Release 3.1.0 2018-11-11 14:34:15 +07:00
Anton Medvedev
c4a450c128 Update package.json 2018-11-11 14:33:24 +07:00
Anton Medvedev
f84962e218 Update README.md 2018-11-11 14:30:53 +07:00
Anton Medvedev
92ac3463a8 Update README.md 2018-11-11 14:28:56 +07:00
Anton Medvedev
b3741760d8
Add .fxrc support (#22) 2018-11-10 17:31:26 +07:00
Anton Medvedev
d86b06c609
Update README.md 2018-11-09 23:58:15 +07:00
Anton Medvedev
9291ce4397 Limit expand all 2018-11-08 15:09:24 +07:00
Daniel Ruf
628450c1cb ci: test Node.js 8, 10 and 11 (#26) 2018-11-08 14:42:32 +07:00
Anton Medvedev
ef514017b5 Release 3.0.3 2018-11-08 10:03:30 +07:00
Anton Medvedev
620ca58208 Turn off blessed tags 2018-11-08 09:48:06 +07:00
Anton Medvedev
d77114d23c Release 3.0.2 2018-11-03 22:30:01 +07:00
Anton Medvedev
3dd064ae96 Fix bug with expanding null var 2018-11-03 22:29:34 +07:00
Anton Medvedev
df2de034bc Release 3.0.1 2018-11-03 21:08:27 +07:00
Anton Medvedev
eca69024ad Add missing file to package.json 2018-11-03 21:07:43 +07:00
Anton Medvedev
9153dec145 Release 3.0.0 2018-11-03 21:04:42 +07:00
Anton Medvedev
961ee629f4 Add interactive mode 2018-11-03 21:03:52 +07:00
Anton Medvedev
840766996e Release 2.0.2 2018-10-26 18:09:58 +07:00
Anton Medvedev
f777badd9a Remove --max-old-space-size=8192 2018-10-26 18:06:37 +07:00
Anton Medvedev
e4bb32d4b8 Release 2.0.1 2018-10-26 13:46:08 +07:00
Anton Medvedev
ba0cec06d2 Update packages 2018-10-26 13:45:29 +07:00
Anton Medvedev
e67564af9c Release 2.0.0 2018-09-15 00:01:23 +07:00
Anton Medvedev
35280be40f Improve startup time 2018-09-15 00:00:36 +07:00
Anton Medvedev
df12c3a9bc Release 1.1.3 2018-09-14 14:30:44 +07:00
Anton Medvedev
affe7d2b61 Increase nodejs heap size 2018-09-14 14:30:03 +07:00
Anton Medvedev
7988cbab8e Release 1.1.2 2018-08-30 22:13:42 +07:00
Anton Medvedev
4840cb9851 Update deps 2018-08-30 22:12:44 +07:00
Anton Medvedev
9bd442da48 Add dot feature 2018-08-30 22:09:12 +07:00
Anton Medvedev
17b30f2622 Release 1.1.1 2018-06-26 22:02:08 +07:00
Anton Medvedev
abd80a935d Use @medv/prettyjson 2018-06-26 21:59:04 +07:00
offirmo
51bb5d9725 lower node.js version requirements (#6)
* lower node.js version requirements

See https://github.com/antonmedv/fx/issues/5

* lower node requirements in Travic
2018-06-26 17:36:28 +07:00
Anton Medvedev
8a62b1234f
Update README.md 2018-06-22 13:09:48 +07:00
Anton Medvedev
43aaf9b2dc
Delete data.json 2018-03-20 18:42:51 +07:00
Anton Medvedev
075ef2926b Release 1.1.0 2018-03-20 00:20:25 +07:00
Anton Medvedev
cf81a94ae2 Add ? shortcut 2018-03-20 00:15:05 +07:00
Anton Medvedev
0f0bcb4805 Update README.md 2018-03-20 00:06:12 +07:00
Anton Medvedev
b5462c73d7 Update README.md 2018-03-20 00:03:16 +07:00
Matthew Adams
f38b4b5257 Add ymlx (#1)
* Add ymlx

I wrote `ymlx` after being inspired by `fx` by copying then massaging.

* Update README.md
2018-03-19 08:13:53 +07:00
Anton Medvedev
9fdcaaf564 Release 1.0.4 2018-03-13 10:30:19 +07:00
Anton Medvedev
a596db031f Use jsome instead of cardinal 2018-03-13 10:28:40 +07:00
Anton Medvedev
b5f9f43f43
Update README.md 2018-01-30 20:20:11 +07:00
Anton Medvedev
b3133d9658
Update README.md 2018-01-27 06:51:21 +07:00
Anton Medvedev
d980f37f40 Release 1.0.3 2018-01-27 06:44:47 +07:00
Anton Medvedev
df1cbbc2f1 Add standalone binary dist 2018-01-27 06:44:19 +07:00
Anton Medvedev
cd2f03ad5e Update README.md 2018-01-26 23:15:56 +07:00
Anton Medvedev
57d536d4a7 Release 1.0.2 2018-01-26 23:14:41 +07:00
Anton Medvedev
4b8a4bfe3c Write undefined to stderr 2018-01-26 23:13:58 +07:00
Anton Medvedev
3a728d66e4 Add jl 2018-01-26 22:38:13 +07:00
52 changed files with 3829 additions and 227 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 ./...

2
.gitignore vendored
View File

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

View File

@ -1,3 +0,0 @@
language: node_js
node_js:
- "9"

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018 Anton Medvedev
Copyright (c) 2019 Anton Medvedev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

153
README.md
View File

@ -1,126 +1,93 @@
<img src="https://user-images.githubusercontent.com/141232/35405308-4b41f446-0238-11e8-86c1-21f407cc8460.png" height="100" alt="fx">
<p align="center"><a href="https://fx.wtf"><img src="https://medv.io/assets/fx/fx-preview.gif" width="500" alt="fx preview"></a></p>
# [![Build Status](https://travis-ci.org/antonmedv/fx.svg?branch=master)](https://travis-ci.org/antonmedv/fx)
Command-line JSON processing tool
_* Function eXecution_
## Features
* Don't need to learn new syntax
* Plain JavaScript
* Formatting and highlighting
- Mouse support
- Streaming support
- Preserves key order
- Preserves big numbers
## Install
```bash
brew install fx
```
$ npm install -g fx
```bash
snap install fx
```
```bash
scoop install fx
```
```bash
pacman -S fx
```
```bash
pkg install fx
```
```bash
go install github.com/antonmedv/fx@latest
```
Or download [pre-built binary](https://github.com/antonmedv/fx/releases).
## Usage
Pipe into `fx` any JSON and anonymous function for reducing it.
Start the interactive viewer via:
```
$ fx [code ...]
```bash
fx data.json
```
Pretty print JSON without passing any arguments:
```
$ echo '{"key":"value"}' | fx
{
"key": "value"
}
Or
```bash
curl ... | fx
```
### Anonymous function
Type `?` to see full list of key shortcuts.
Use an anonymous function as reducer which gets JSON and processes it:
```
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo[0].bar'
"value"
Pretty print:
```bash
curl ... | fx .
```
### This Binding
### Reducers
If you don't pass anonymous function `param => ...`, code will be automatically transformed into anonymous function.
And you can get access to JSON by `this` keyword:
```
$ echo '{"foo": [{"bar": "value"}]}' | fx 'this.foo[0].bar'
"value"
Write reducers in your favorite language: [JavaScript](doc/js.md) (default),
[Python](doc/python.md), or [Ruby](doc/ruby.md).
```bash
fx data.json '.filter(x => x.startsWith("a"))'
```
### Chain
You can pass any number of anonymous functions for reducing JSON:
```
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo' 'this[0]' 'this.bar'
"value"
```bash
fx data.json '[x["age"] + i for i in range(10)]'
```
### Generator
If passed code contains `yield` keyword, [generator expression](https://github.com/sebmarkbage/ecmascript-generator-expression)
will be used:
```
$ curl ... | fx 'for (let user of this) if (user.login.startsWith("a")) yield user'
```bash
fx data.json 'x.to_a.map {|x| x[1]}'
```
Access to JSON through `this` keyword:
```
$ echo '["a", "b"]' | fx 'yield* this'
[
"a",
"b"
]
## Documentation
See full [documentation](doc/doc.md).
## Themes
Theme can be configured by setting environment variable `FX_THEME` from `1`
to `9`:
```bash
export FX_THEME=9
```
```
$ echo '["a", "b"]' | fx 'yield* this; yield "c";'
[
"a",
"b",
"c"
]
```
<img width="1214" alt="themes" src="doc/images/themes.png">
### Update
You can update existing JSON using spread operator:
```
$ echo '{"count": 0}' | fx '{...this, count: 1}'
{
"count": 1
}
```
### Use npm package
Use any npm package by installing it globally:
```
$ npm install -g lodash
$ cat package.json | fx 'require("lodash").keys(this.dependencies)'
```
### Other examples
Convert object to array:
```
$ cat package.json | fx 'Object.keys(this.dependencies)'
[
"cardinal",
"get-stdin",
"meow"
]
```
## Related
* [jq](https://github.com/stedolan/jq) cli JSON processor on C
* [jsawk](https://github.com/micha/jsawk) like awk, but for JSON
* [json](https://github.com/trentm/json) another JSON manipulating cli library
Add your own themes in [theme.go](pkg/theme/theme.go) file.
## License
MIT
[MIT](LICENSE)

66
doc/doc.md Normal file
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
```

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,68 +0,0 @@
#!/usr/bin/env node
'use strict';
const meow = require('meow')
const stdin = require('get-stdin')
const cardinal = require('cardinal')
const theme = require('cardinal/themes/tomorrow-night')
const cli = meow(`
Usage
$ fx [code ...]
Examples
$ echo '{"key": "value"}' | fx 'x => x.key'
"value"
$ echo '[1,2,3]' | fx 'this.map(x => x * 2)'
[2, 4, 6]
$ echo '{"items": ["one", "two"]}' | fx 'this.items' 'this[1]'
"two"
$ echo '{"count": 0}' | fx '{...this, count: 1}'
{"count": 1}
`)
const highlight = process.stdout.isTTY ? cardinal.highlight : x => x
async function main() {
const text = await stdin()
if (text === '') {
cli.showHelp()
}
const json = JSON.parse(text)
const result = cli.input.reduce(reduce, json)
if (typeof result === 'undefined') {
console.log(undefined)
} else {
const text = JSON.stringify(result, null, 4)
console.log(highlight(text, {theme}))
}
}
function reduce(json, code) {
if (/^\w+\s*=>/.test(code)) {
const fx = eval(code)
return fx(json)
} else if (/yield/.test(code)) {
const fx = eval(`
function fn() {
const gen = (function*(){
${code.replace(/\\\n/g, '')}
}).call(this)
return [...gen]
}; fn
`)
return fx.call(json)
} else {
const fx = eval(`function fn() { return ${code} }; fn`)
return fx.call(json)
}
}
main()

121
keymap.go Normal file
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,32 +0,0 @@
{
"name": "fx",
"version": "1.0.1",
"description": "Command-line JSON processing tool",
"repository": "antonmedv/fx",
"author": "Anton Medvedev <anton@medv.io>",
"license": "MIT",
"bin": {
"fx": "index.js"
},
"files": [
"index.js"
],
"scripts": {
"test": "ava"
},
"keywords": [
"json",
"cli"
],
"engines": {
"node": ">=9"
},
"dependencies": {
"cardinal": "^1.0.0",
"get-stdin": "^5.0.1",
"meow": "^4.0.0"
},
"devDependencies": {
"ava": "^0.24.0"
}
}

26
pkg/dict/dict.go Normal file
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
}

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

20
snap/snapcraft.yaml Normal file
View File

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

42
stream.go Normal file
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
}
}
}

28
test.js
View File

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

50
util.go Normal file
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)
}
}