forked from Archives/fx
Compare commits
336 Commits
Author | SHA1 | Date |
---|---|---|
lilihx | 95009b06b6 | 2 years ago |
Anton Medvedev | 751e5865da | 2 years ago |
Anton Medvedev | bd02783f75 | 2 years ago |
github-actions | c3f58aa915 | 2 years ago |
Anton Medvedev | 126340bcf0 | 2 years ago |
Anton Medvedev | 06ff419622 | 2 years ago |
Anton Medvedev | 92cf975206 | 2 years ago |
Anton Medvedev | 158687e890 | 2 years ago |
Anton Medvedev | 14c79c1d12 | 2 years ago |
Anton Medvedev | 918b4e5e3b | 2 years ago |
Anton Medvedev | 2428d76e80 | 2 years ago |
Anton Medvedev | 06f612bfb9 | 2 years ago |
Anton Medvedev | 76e67edc41 | 2 years ago |
Anton Medvedev | 534e0db987 | 2 years ago |
Anton Medvedev | 4d0dbf4b94 | 2 years ago |
github-actions | 1198ae9984 | 2 years ago |
Anton Medvedev | d622f6b85d | 2 years ago |
Anton Medvedev | d7b5ab7200 | 2 years ago |
Anton Medvedev | a58152469f | 2 years ago |
Gabriel M. Dutra | 9cf72f0407 | 2 years ago |
github-actions | 0b9019a2c5 | 2 years ago |
Anton Medvedev | 7c72e04453 | 2 years ago |
Anton Medvedev | 477f89c783 | 2 years ago |
Anton Medvedev | f474859d9b | 2 years ago |
Anton Medvedev | 9421363ebd | 2 years ago |
Anton Medvedev | 92c4b93f15 | 2 years ago |
Anton Medvedev | 95c55f9827 | 2 years ago |
Anton Medvedev | d443625dd6 | 2 years ago |
Anton Medvedev | 0d5fa2e41a | 2 years ago |
github-actions | 79ddfe0ea3 | 2 years ago |
Anton Medvedev | 06f75b062e | 2 years ago |
github-actions | 64349cb97c | 2 years ago |
Anton Medvedev | 5e8d67a5fb | 2 years ago |
Anton Medvedev | 2e30f1ec9a | 2 years ago |
Anton Medvedev | 3b9ac1d023 | 2 years ago |
Kacper Bąk | 7028357eba | 2 years ago |
Anton Medvedev | 0cccf2e063 | 2 years ago |
Anton Medvedev | 204c2988c8 | 2 years ago |
Anton Medvedev | 1e4ee58691 | 2 years ago |
github-actions | 61e8cf97cf | 2 years ago |
Anton Medvedev | 04693455c4 | 2 years ago |
github-actions | f87f89440c | 2 years ago |
Anton Medvedev | 33f25646c5 | 2 years ago |
Anton Medvedev | a406596b46 | 2 years ago |
github-actions | 684d5d59d0 | 2 years ago |
Anton Medvedev | b0045d03a9 | 2 years ago |
Anton Medvedev | a01be39389 | 2 years ago |
Anton Medvedev | 918ba6b566 | 2 years ago |
Anton Medvedev | 710cf58310 | 2 years ago |
Anton Medvedev | 8386ba7d91 | 2 years ago |
Anton Medvedev | f6c561c411 | 2 years ago |
Anton Medvedev | 09aaaa9e3a | 2 years ago |
Anton Medvedev | e3ca218491 | 2 years ago |
Anton Medvedev | 3b0826c3c9 | 2 years ago |
Anton Medvedev | 506961e9d8 | 2 years ago |
Anton Medvedev | 6e1dd65b3e | 2 years ago |
Anton Medvedev | ed5ed858d4 | 2 years ago |
Caleb Maclennan | 6ef3aa5573 | 2 years ago |
Anton Medvedev | 915c1e9fa7 | 2 years ago |
Anton Medvedev | 77b2632aeb | 2 years ago |
Anton Medvedev | 36af65042c | 2 years ago |
Anton Medvedev | e05e917a6f | 2 years ago |
Anton Medvedev | bdb1d00831 | 2 years ago |
Anton Medvedev | 3463842b8b | 2 years ago |
Anton Medvedev | fc257539df | 2 years ago |
Anton Medvedev | 0d556edd9e | 2 years ago |
Anton Medvedev | d7f54a5c80 | 2 years ago |
Anton Medvedev | 804becbdb5 | 2 years ago |
Anton Medvedev | f4daf98966 | 2 years ago |
Anton Medvedev | bae280436b | 2 years ago |
Anton Medvedev | f2a2a01589 | 2 years ago |
Anton Medvedev | 344f78d20a | 2 years ago |
Anton Medvedev | 5c448bf98f | 2 years ago |
Anton Medvedev | 89c9bc7f10 | 2 years ago |
Anton Medvedev | 8972572d79 | 2 years ago |
Anton Medvedev | dac5e9fc03 | 2 years ago |
Anton Medvedev | 1b1e76df01 | 2 years ago |
Anton Medvedev | 61eb6e5ec3 | 2 years ago |
Anton Medvedev | 1b09c93447 | 2 years ago |
Anton Medvedev | 73bf2d97cd | 2 years ago |
Anton Medvedev | e3aea24907 | 2 years ago |
Anton Medvedev | c642fbd28e | 2 years ago |
Anton Medvedev | 6ed6ba2ec4 | 2 years ago |
Anton Medvedev | bf166fafda | 2 years ago |
Alexandre GUIOT--VALENTIN | aa0964a12e | 2 years ago |
Ezequiel Moreno | a68c947949 | 2 years ago |
Anton Medvedev | 7d7bfd28fe | 2 years ago |
Anton Medvedev | 8bbc8ed564 | 2 years ago |
Anton Medvedev | 2e7f78f7c6 | 2 years ago |
Anton Medvedev | ae80a278b2 | 2 years ago |
Anton Medvedev | 49756d7ff4 | 2 years ago |
Anton Medvedev | 68212c0c7a | 2 years ago |
Anton Medvedev | 841eb4f1b5 | 2 years ago |
Anton Medvedev | d682c0b49f | 2 years ago |
Anton Medvedev | afd7b96af5 | 2 years ago |
Anton Medvedev | fa226f2ae8 | 2 years ago |
Anton Medvedev | e6042cb698 | 2 years ago |
Anton Medvedev | 65a1608462 | 2 years ago |
Yury Shulaev | 8a9e1f39d5 | 3 years ago |
Anton Medvedev | e14e56fa06 | 4 years ago |
Sébastien HOUZÉ | 2315f509b8 | 4 years ago |
Andrey Shilov | 3e0cc4f61b | 4 years ago |
Anton Medvedev | 604512a410 | 4 years ago |
Anton Medvedev | 2dc104887b | 4 years ago |
Anton Medvedev | 6db594864e | 4 years ago |
Anton Medvedev | 0115d90a36 | 4 years ago |
Anton Medvedev | 043208a748 | 4 years ago |
Anton Medvedev | d289037340 | 4 years ago |
Anton Medvedev | afa4088e99 | 4 years ago |
Anton Medvedev | e430d5019b | 4 years ago |
Anton Medvedev | 6f100280a6 | 4 years ago |
Anton Medvedev | f47c8e8c67 | 4 years ago |
Anton Medvedev | 19a9d34dad | 4 years ago |
Anton Medvedev | d455165d25 | 4 years ago |
Anton Medvedev | 5521a99e69 | 4 years ago |
Anton Medvedev | f1eaaa9c5e | 4 years ago |
Anton Medvedev | 333dd38353 | 4 years ago |
Anton Medvedev | c6bc621c57 | 4 years ago |
Anton Medvedev | ac5678eb92 | 4 years ago |
Anton Medvedev | 0307f129f5 | 5 years ago |
Anton Medvedev | dfc4815dbf | 5 years ago |
Anton Medvedev | 7ed688602e | 5 years ago |
Anton Medvedev | f7a37e5b98 | 5 years ago |
Anton Medvedev | e7619aeece | 5 years ago |
Anton Medvedev | e5aef9ff32 | 5 years ago |
Anton Medvedev | 973ceb92da | 5 years ago |
Anton Medvedev | 1744e178fa | 5 years ago |
Anton Medvedev | 57f4069b4b | 5 years ago |
Anton Medvedev | 7fffb5db9d | 5 years ago |
Anton Medvedev | 001252c1ca | 5 years ago |
Anton Medvedev | de3d60b01d | 5 years ago |
Anton Medvedev | 290c2a0185 | 5 years ago |
Anton Medvedev | ed6130907e | 5 years ago |
Anton Medvedev | 6da433446c | 5 years ago |
Anton Medvedev | c456134a71 | 5 years ago |
Anton Medvedev | 7f53e96a93 | 5 years ago |
Anton Medvedev | c7057cce2d | 5 years ago |
Anton Medvedev | ca6e4def1a | 5 years ago |
Anton Medvedev | 7426016d6b | 5 years ago |
Anton Medvedev | 98d886da20 | 5 years ago |
Anton Medvedev | 81c71e1d2d | 5 years ago |
Anton Medvedev | d12daf3c83 | 5 years ago |
Anton Medvedev | 58324051ae | 5 years ago |
Anton Medvedev | f60d7aa89c | 5 years ago |
Anton Medvedev | 9713a057c1 | 5 years ago |
Anton Medvedev | 6b4e7254c5 | 5 years ago |
Anton Medvedev | a39e80b5b0 | 5 years ago |
Anton Medvedev | 314843362e | 5 years ago |
Anton Medvedev | a0fe5aae43 | 5 years ago |
Anton Medvedev | a16fa4560a | 5 years ago |
Anton Medvedev | e8fd8353fe | 5 years ago |
Anton Medvedev | 33ea17be93 | 5 years ago |
Nate Eagleson | 027b1a8dad | 5 years ago |
Anton Medvedev | 51837b4831 | 6 years ago |
Anton Medvedev | 2cf71999ae | 6 years ago |
Anton Medvedev | 7d23c8d811 | 6 years ago |
Anton Medvedev | ae0b739796 | 6 years ago |
Anton Medvedev | 332cb40a90 | 6 years ago |
Anton Medvedev | acf0255e1d | 6 years ago |
Anton Medvedev | 0889b9683d | 6 years ago |
Anton Medvedev | 575666fd1e | 6 years ago |
Anton Medvedev | 83efa1d109 | 6 years ago |
Anton Medvedev | fd2a30b79e | 6 years ago |
Ross Hadden | d216e02cb3 | 6 years ago |
Anton Medvedev | 35526cc4d3 | 6 years ago |
Anton Medvedev | 5c228ccd77 | 6 years ago |
Anton Medvedev | 42c81fd484 | 6 years ago |
Anton Medvedev | 0382a0b2d7 | 6 years ago |
Anton Medvedev | 772bb83a19 | 6 years ago |
Anton Medvedev | e4e41c7391 | 6 years ago |
Anton Medvedev | 776d4d34a2 | 6 years ago |
Anton Medvedev | 73298e88a4 | 6 years ago |
Anton Medvedev | b1f393013c | 6 years ago |
Anton Medvedev | 2618fdaa56 | 6 years ago |
Anton Medvedev | 1ecc258ca6 | 6 years ago |
Anton Medvedev | 33f2e861ff | 6 years ago |
Anton Medvedev | ce4b19cce5 | 6 years ago |
Anton Medvedev | 407c034f3b | 6 years ago |
Anton Medvedev | 6d2a1d9947 | 6 years ago |
Anton Medvedev | 592e799e10 | 6 years ago |
Anton Medvedev | 4fd6fdf39f | 6 years ago |
Anton Medvedev | 7099560512 | 6 years ago |
Anton Medvedev | bfb8aa4534 | 6 years ago |
Anton Medvedev | 47b5bb2366 | 6 years ago |
Anton Medvedev | d6cfe05fed | 6 years ago |
Anton Medvedev | 9f48ff7011 | 6 years ago |
Anton Medvedev | 4fd5e21432 | 6 years ago |
Anton Medvedev | 7c01d735a7 | 6 years ago |
Anton Medvedev | 0e60886e0d | 6 years ago |
Anton Medvedev | cdd3d5abbe | 6 years ago |
Anton Medvedev | d571194fc8 | 6 years ago |
Anton Medvedev | 6236a5f942 | 6 years ago |
Anton Medvedev | c116bb749b | 6 years ago |
Anton Medvedev | c8a6c4af0b | 6 years ago |
Anton Medvedev | a7b89365bf | 6 years ago |
Anton Medvedev | 12ff839365 | 6 years ago |
Anton Medvedev | ce0b1a5cc6 | 6 years ago |
Anton Medvedev | 8bd892c11e | 6 years ago |
Anton Medvedev | 4f3a52fa32 | 6 years ago |
Sylvain Pace | 3da7e22893 | 6 years ago |
Anton Medvedev | f321b8a58f | 6 years ago |
Anton Medvedev | ff452e2b5b | 6 years ago |
Anton Medvedev | 2d3becc9e4 | 6 years ago |
Anton Medvedev | 07f8e6e18d | 6 years ago |
Anton Medvedev | 95b40b31b8 | 6 years ago |
Anton Medvedev | 0f33ceb592 | 6 years ago |
Anton Medvedev | 85f7c5724a | 6 years ago |
Anton Medvedev | 44d95c6c8d | 6 years ago |
Anton Medvedev | 7501e5a81b | 6 years ago |
Anton Medvedev | d283c6ec52 | 6 years ago |
Anton Medvedev | 70385ff4f2 | 6 years ago |
Anton Medvedev | afada8f60a | 6 years ago |
Anton Medvedev | 8812bf3393 | 6 years ago |
Anton Medvedev | 9ebeb62c28 | 6 years ago |
Anton Medvedev | cebe263d3b | 6 years ago |
Conor Grocock | 7b2e45eeba | 6 years ago |
Sven Efftinge | 22a394047f | 6 years ago |
Abdul Rauf | b99a05dd89 | 6 years ago |
Anton Medvedev | 9dac9222e0 | 6 years ago |
Wynn Netherland | 9907ec7e58 | 6 years ago |
Anton Medvedev | 1742da02f7 | 6 years ago |
Anton Medvedev | 8999dee7d6 | 6 years ago |
Anton Medvedev | ceb177163d | 6 years ago |
Anton Medvedev | fe57a92cfe | 6 years ago |
Anton Medvedev | 9a4d897c8d | 6 years ago |
Anton Medvedev | e0feb840fa | 6 years ago |
Anton Medvedev | 62eccdeb80 | 6 years ago |
Anton Medvedev | be55b22356 | 6 years ago |
Anton Medvedev | 3e05caef82 | 6 years ago |
Anton Medvedev | eb6461f3a8 | 6 years ago |
Anton Medvedev | 35d5d741f4 | 6 years ago |
Anton Medvedev | bb79c9812d | 6 years ago |
Anton Medvedev | 3c891babf7 | 6 years ago |
Anton Medvedev | ef77520f99 | 6 years ago |
Anton Medvedev | 225239bf3d | 6 years ago |
Anton Medvedev | a696018fa5 | 6 years ago |
Anton Medvedev | beedbfd36a | 6 years ago |
Anton Medvedev | 935857230d | 6 years ago |
Anton Medvedev | 6a7a628e03 | 6 years ago |
Anton Medvedev | 9c1a7e8ae2 | 6 years ago |
Anton Medvedev | 4986be32b5 | 6 years ago |
Alan Pope | 83bed4a4ee | 6 years ago |
Anton Medvedev | 73f051f532 | 6 years ago |
Alan Pope | cb5a16a90c | 6 years ago |
Anton Medvedev | 9660cb1932 | 6 years ago |
Anton Medvedev | f569036983 | 6 years ago |
Anton Medvedev | 780ab93897 | 6 years ago |
Anton Medvedev | c9f582d3ee | 6 years ago |
Anton Medvedev | 048ce5e040 | 6 years ago |
Anton Medvedev | e7bb9f3c7d | 6 years ago |
Anton Medvedev | 758ae8d1ea | 6 years ago |
Anton Medvedev | f22f8d052c | 6 years ago |
Anton Medvedev | a33c92a20a | 6 years ago |
Anton Medvedev | 2598958557 | 6 years ago |
Anton Medvedev | 4bfd5ccc42 | 6 years ago |
Anton Medvedev | 669c36866b | 6 years ago |
Anton Medvedev | 4d6db10684 | 6 years ago |
Alex | f892e8869d | 6 years ago |
Anton Medvedev | b63088e94c | 6 years ago |
Anton Medvedev | 6036e981ef | 6 years ago |
Anton Medvedev | d8d8909b9e | 6 years ago |
Anton Medvedev | 9089e078f0 | 6 years ago |
Anton Medvedev | e062d567c6 | 6 years ago |
Anton Medvedev | 40f88dcc91 | 6 years ago |
Anton Medvedev | b12ae5a243 | 6 years ago |
Anton Medvedev | c8cd6ceef5 | 6 years ago |
Anton Medvedev | a29d43017e | 6 years ago |
Anton Medvedev | b5762bef22 | 6 years ago |
Anton Medvedev | b93cda14d2 | 6 years ago |
Anton Medvedev | f7e180deb2 | 6 years ago |
Anton Medvedev | dc5fe9bf8f | 6 years ago |
Anton Medvedev | 838210b044 | 6 years ago |
Anton Medvedev | 30b683b5cd | 6 years ago |
Anton Medvedev | ce4c407e00 | 6 years ago |
Anton Medvedev | 5e04d740aa | 6 years ago |
Anton Medvedev | 5db1c910f8 | 6 years ago |
Anton Medvedev | 82de76e1d4 | 6 years ago |
Anton Medvedev | 74cf26fd0d | 6 years ago |
Anton Medvedev | a0c2931c57 | 6 years ago |
Anton Medvedev | f110142160 | 6 years ago |
Anton Medvedev | 851427d8c8 | 6 years ago |
Anton Medvedev | a09b27c8e4 | 6 years ago |
Anton Medvedev | 89faa8bdb4 | 6 years ago |
Anton Medvedev | ac4d3696a9 | 6 years ago |
Anton Medvedev | 6a4a813562 | 6 years ago |
Anton Medvedev | 5146fffcdd | 6 years ago |
Steven | 9e34edeb75 | 6 years ago |
Anton Medvedev | 82d2c29924 | 6 years ago |
RDIL | 351e96d292 | 6 years ago |
Anton Medvedev | ce0099b87d | 6 years ago |
Anton Medvedev | c4a450c128 | 6 years ago |
Anton Medvedev | f84962e218 | 6 years ago |
Anton Medvedev | 92ac3463a8 | 6 years ago |
Anton Medvedev | b3741760d8 | 6 years ago |
Anton Medvedev | d86b06c609 | 6 years ago |
Anton Medvedev | 9291ce4397 | 6 years ago |
Daniel Ruf | 628450c1cb | 6 years ago |
Anton Medvedev | ef514017b5 | 6 years ago |
Anton Medvedev | 620ca58208 | 6 years ago |
Anton Medvedev | d77114d23c | 6 years ago |
Anton Medvedev | 3dd064ae96 | 6 years ago |
Anton Medvedev | df2de034bc | 6 years ago |
Anton Medvedev | eca69024ad | 6 years ago |
Anton Medvedev | 9153dec145 | 6 years ago |
Anton Medvedev | 961ee629f4 | 6 years ago |
Anton Medvedev | 840766996e | 6 years ago |
Anton Medvedev | f777badd9a | 6 years ago |
Anton Medvedev | e4bb32d4b8 | 6 years ago |
Anton Medvedev | ba0cec06d2 | 6 years ago |
Anton Medvedev | e67564af9c | 6 years ago |
Anton Medvedev | 35280be40f | 6 years ago |
Anton Medvedev | df12c3a9bc | 6 years ago |
Anton Medvedev | affe7d2b61 | 6 years ago |
Anton Medvedev | 7988cbab8e | 6 years ago |
Anton Medvedev | 4840cb9851 | 6 years ago |
Anton Medvedev | 9bd442da48 | 6 years ago |
Anton Medvedev | 17b30f2622 | 6 years ago |
Anton Medvedev | abd80a935d | 6 years ago |
offirmo | 51bb5d9725 | 6 years ago |
Anton Medvedev | 8a62b1234f | 6 years ago |
Anton Medvedev | 43aaf9b2dc | 7 years ago |
Anton Medvedev | 075ef2926b | 7 years ago |
Anton Medvedev | cf81a94ae2 | 7 years ago |
Anton Medvedev | 0f0bcb4805 | 7 years ago |
Anton Medvedev | b5462c73d7 | 7 years ago |
Matthew Adams | f38b4b5257 | 7 years ago |
Anton Medvedev | 9fdcaaf564 | 7 years ago |
Anton Medvedev | a596db031f | 7 years ago |
Anton Medvedev | b5f9f43f43 | 7 years ago |
Anton Medvedev | b3133d9658 | 7 years ago |
Anton Medvedev | d980f37f40 | 7 years ago |
Anton Medvedev | df1cbbc2f1 | 7 years ago |
Anton Medvedev | cd2f03ad5e | 7 years ago |
Anton Medvedev | 57d536d4a7 | 7 years ago |
Anton Medvedev | 4b8a4bfe3c | 7 years ago |
Anton Medvedev | 3a728d66e4 | 7 years ago |
@ -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)}`)))
|
@ -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
|
@ -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 ./...
|
@ -1,2 +0,0 @@
|
|||||||
/node_modules/
|
|
||||||
package-lock.json
|
|
@ -1,3 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- "9"
|
|
@ -0,0 +1,66 @@
|
|||||||
|
# Documentation
|
||||||
|
|
||||||
|
The **fx** can work in two modes: as a reducer or an interactive viewer.
|
||||||
|
|
||||||
|
To start the interactive mode pipe a JSON into **fx**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ curl ... | fx
|
||||||
|
```
|
||||||
|
|
||||||
|
Or you can pass a filename as the first parameter:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ fx data.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reducers
|
||||||
|
|
||||||
|
Use [JavaScript](js.md), [Python](python.md), or [Ruby](ruby.md).
|
||||||
|
|
||||||
|
## Streaming mode
|
||||||
|
|
||||||
|
The **fx** supports line-delimited JSON streaming or concatenated JSON streaming.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ echo '
|
||||||
|
> {"message": "hello"}
|
||||||
|
> {"message": "world!"}
|
||||||
|
> ' | fx .message
|
||||||
|
hello
|
||||||
|
world!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interactive mode
|
||||||
|
|
||||||
|
Type `?` to see the full list of available shortcuts while in the interactive mode.
|
||||||
|
|
||||||
|
### Search
|
||||||
|
|
||||||
|
Press `/` and type regexp pattern to search in the current JSON.
|
||||||
|
Search is performed on the internal representation of the JSON without newlines.
|
||||||
|
|
||||||
|
Type `n` to jump to the next result, and `N` to the previous
|
||||||
|
|
||||||
|
### Selecting text
|
||||||
|
|
||||||
|
You can't just select text in fx. This is due to the fact that all mouse events are
|
||||||
|
redirected to stdin. To be able to select again you need to instruct your terminal
|
||||||
|
not to do it. This can be done by holding special keys while selecting:
|
||||||
|
|
||||||
|
| Key | Terminal |
|
||||||
|
|------------------|---------------|
|
||||||
|
| `Option`+`Mouse` | iTerm2, Hyper |
|
||||||
|
| `Fn`+`Mouse` | Terminal.app |
|
||||||
|
| `Shift`+`Mouse` | Linux |
|
||||||
|
|
||||||
|
|
||||||
|
## Configs
|
||||||
|
|
||||||
|
Next configs available for **fx** via environment variables.
|
||||||
|
|
||||||
|
| Name | Values | Description |
|
||||||
|
|----------------|-----------------------------------------------------|-------------------------------------------------------|
|
||||||
|
| `FX_LANG` | `js` (default), `node`, `python`, `python3`, `ruby` | Reducer type. |
|
||||||
|
| `FX_THEME` | `0` (disable colors), `1` (default), `2..9` | Color theme. |
|
||||||
|
| `FX_SHOW_SIZE` | `true` or `false` (default) | Show size of arrays and object in collapsed previews. |
|
Binary file not shown.
After Width: | Height: | Size: 411 KiB |
Binary file not shown.
After Width: | Height: | Size: 659 KiB |
@ -0,0 +1,120 @@
|
|||||||
|
# JavaScript Reducers
|
||||||
|
|
||||||
|
If any additional arguments were passed, fx converts them into a function which
|
||||||
|
takes the JSON as an argument named `x`.
|
||||||
|
|
||||||
|
By default, fx uses builtin JavaScript VM ([goja](https://github.com/dop251/goja)),
|
||||||
|
but also can be used with node.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export FX_LANG=js # Default
|
||||||
|
```
|
||||||
|
|
||||||
|
Or for usage with node:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export FX_LANG=node
|
||||||
|
```
|
||||||
|
|
||||||
|
An example of anonymous function used as a reducer:
|
||||||
|
```sh
|
||||||
|
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo[0].bar'
|
||||||
|
value
|
||||||
|
```
|
||||||
|
|
||||||
|
The same reducer function can be simplified to:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x.foo[0].bar'
|
||||||
|
value
|
||||||
|
```
|
||||||
|
|
||||||
|
Each argument treated as a reducer function.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x.foo' 'x[0]' 'x.bar'
|
||||||
|
value
|
||||||
|
```
|
||||||
|
|
||||||
|
Update JSON using the spread operator:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ echo '{"name": "fx", "count": 0}' | fx '{...this, count: 1}'
|
||||||
|
{
|
||||||
|
"name": "fx",
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dot
|
||||||
|
|
||||||
|
Fx supports simple JS-like syntax for accessing data, which can be used with any `FX_LANG`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
|
||||||
|
value
|
||||||
|
```
|
||||||
|
|
||||||
|
## .fxrc.js
|
||||||
|
|
||||||
|
Create _.fxrc.js_ file in `$HOME` directory, and define some useful functions.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// .fxrc.js
|
||||||
|
function upper(s) {
|
||||||
|
return s.toUpperCase()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ cat data.json | fx .name upper
|
||||||
|
ANTON
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export FX_LANG=node
|
||||||
|
```
|
||||||
|
|
||||||
|
Use any npm package by installing it globally. Create _.fxrc.js_ file in `$HOME`
|
||||||
|
directory, and require any packages or define global functions. For example,
|
||||||
|
to access all lodash methods without `_` prefix, put next line into your
|
||||||
|
_.fxrc.js_ file:
|
||||||
|
|
||||||
|
```js
|
||||||
|
Object.assign(global, require('lodash/fp'))
|
||||||
|
```
|
||||||
|
|
||||||
|
And now you will be able to call all lodash methods. For example, see who's been
|
||||||
|
committing to react recently:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl 'https://api.github.com/repos/facebook/react/commits?per_page=100' \
|
||||||
|
| fx 'groupBy("commit.author.name")' 'mapValues(size)' toPairs 'sortBy(1)' reverse 'take(10)' fromPairs
|
||||||
|
```
|
||||||
|
|
||||||
|
> To be able to require global modules make sure you have correct `NODE_PATH` env variable.
|
||||||
|
> ```sh
|
||||||
|
> export NODE_PATH=`npm root -g`
|
||||||
|
> ```
|
||||||
|
|
||||||
|
The _.fxrc.js_ file supports both: `import` and `require`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// .fxrc.js
|
||||||
|
import 'zx/globals'
|
||||||
|
const _ = require('lodash')
|
||||||
|
```
|
||||||
|
|
||||||
|
> If you want to use _.fxrc.js_ for both `FX_LANG=js` and `FX_LANG=node`,
|
||||||
|
> separate parts by `// nodejs:` comment:
|
||||||
|
> ```js
|
||||||
|
> // .fxrc.js
|
||||||
|
> function upper(s) {
|
||||||
|
> return s.toUpperCase()
|
||||||
|
> }
|
||||||
|
> // nodejs:
|
||||||
|
> import 'zx/globals'
|
||||||
|
> const _ = require('lodash')
|
||||||
|
> ```
|
@ -0,0 +1,27 @@
|
|||||||
|
# Python Reducers
|
||||||
|
|
||||||
|
If any additional arguments was passed, **fx** converts it to a function which
|
||||||
|
takes the JSON as an argument named `x`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export FX_LANG=python
|
||||||
|
```
|
||||||
|
Or
|
||||||
|
```sh
|
||||||
|
export FX_LANG=python3
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fx data.json '[x["age"] + i for i in range(10)]'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dot
|
||||||
|
|
||||||
|
Fx supports simple syntax for accessing data, which can be used with any `FX_LANG`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
|
||||||
|
value
|
||||||
|
```
|
@ -0,0 +1,23 @@
|
|||||||
|
# Ruby Reducers
|
||||||
|
|
||||||
|
If any additional arguments was passed, **fx** converts it to a function which
|
||||||
|
takes the JSON as an argument named `x`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export FX_LANG=ruby
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fx data.json 'x.to_a.map {|x| x[1]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dot
|
||||||
|
|
||||||
|
Fx supports simple syntax for accessing data, which can be used with any `FX_LANG`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
|
||||||
|
value
|
||||||
|
```
|
@ -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
|
||||||
|
)
|
@ -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=
|
@ -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")
|
||||||
|
}
|
@ -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()
|
|
@ -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"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package json
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type Number = json.Number
|
||||||
|
type Array = []interface{}
|
@ -0,0 +1,98 @@
|
|||||||
|
package reducer
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
. "github.com/antonmedv/fx/pkg/json"
|
||||||
|
. "github.com/antonmedv/fx/pkg/theme"
|
||||||
|
"github.com/dop251/goja"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed js.js
|
||||||
|
var templateJs string
|
||||||
|
|
||||||
|
func js(args []string, fxrc string) string {
|
||||||
|
rs := "\n"
|
||||||
|
for i, a := range args {
|
||||||
|
rs += " try {"
|
||||||
|
switch {
|
||||||
|
case flatMapRegex.MatchString(a):
|
||||||
|
code := fold(strings.Split(a, "[]"))
|
||||||
|
rs += fmt.Sprintf(
|
||||||
|
`
|
||||||
|
x = (
|
||||||
|
%v
|
||||||
|
)(x)
|
||||||
|
`, code)
|
||||||
|
|
||||||
|
case strings.HasPrefix(a, ".["):
|
||||||
|
rs += fmt.Sprintf(
|
||||||
|
`
|
||||||
|
x = function ()
|
||||||
|
{ return this%v }
|
||||||
|
.call(x)
|
||||||
|
`, a[1:])
|
||||||
|
|
||||||
|
case strings.HasPrefix(a, "."):
|
||||||
|
rs += fmt.Sprintf(
|
||||||
|
`
|
||||||
|
x = function ()
|
||||||
|
{ return this%v }
|
||||||
|
.call(x)
|
||||||
|
`, a)
|
||||||
|
|
||||||
|
default:
|
||||||
|
rs += fmt.Sprintf(
|
||||||
|
`
|
||||||
|
let f = function ()
|
||||||
|
{ return %v }
|
||||||
|
.call(x)
|
||||||
|
x = typeof f === 'function' ? f(x) : f
|
||||||
|
`, a)
|
||||||
|
}
|
||||||
|
// Generate a beautiful error message.
|
||||||
|
rs += " } catch (e) {\n"
|
||||||
|
pre, post, pointer := trace(args, i)
|
||||||
|
rs += fmt.Sprintf(
|
||||||
|
" throw `\\n ${%q} ${%q} ${%q}\\n %v\\n\\n${e.stack || e}`\n",
|
||||||
|
pre, a, post, pointer,
|
||||||
|
)
|
||||||
|
rs += " }\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(templateJs, fxrc, rs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateJS(args []string, fxrc string) (*goja.Runtime, goja.Callable, error) {
|
||||||
|
vm := goja.New()
|
||||||
|
_, err := vm.RunString(js(args, fxrc))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
fn, ok := goja.AssertFunction(vm.Get("reduce"))
|
||||||
|
if !ok {
|
||||||
|
panic("Not a function")
|
||||||
|
}
|
||||||
|
return vm, fn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReduceJS(vm *goja.Runtime, reduce goja.Callable, input interface{}, theme Theme) int {
|
||||||
|
value, err := reduce(goja.Undefined(), vm.ToValue(Stringify(input)))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
output := value.String()
|
||||||
|
dec := json.NewDecoder(strings.NewReader(output))
|
||||||
|
dec.UseNumber()
|
||||||
|
object, err := Parse(dec)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Print(output)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
Echo(object, theme)
|
||||||
|
return 0
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
// .fxrc.js %v
|
||||||
|
|
||||||
|
function reduce(input) {
|
||||||
|
let x = JSON.parse(input)
|
||||||
|
|
||||||
|
// Reducers %v
|
||||||
|
if (typeof x === 'undefined') {
|
||||||
|
return 'null'
|
||||||
|
} else {
|
||||||
|
return JSON.stringify(x)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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
|
||||||
|
})
|
@ -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)
|
||||||
|
}
|
@ -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)))
|
@ -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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
require 'json'
|
||||||
|
x = JSON.parse(STDIN.read)
|
||||||
|
|
||||||
|
# Reducers %v
|
||||||
|
|
||||||
|
puts JSON.generate(x)
|
@ -0,0 +1,215 @@
|
|||||||
|
package reducer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
. "github.com/antonmedv/fx/pkg/dict"
|
||||||
|
. "github.com/antonmedv/fx/pkg/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type state int
|
||||||
|
|
||||||
|
const (
|
||||||
|
start state = iota
|
||||||
|
unknown
|
||||||
|
propOrIndex
|
||||||
|
prop
|
||||||
|
index
|
||||||
|
indexEnd
|
||||||
|
number
|
||||||
|
doubleQuote
|
||||||
|
doubleQuoteEscape
|
||||||
|
singleQuote
|
||||||
|
singleQuoteEscape
|
||||||
|
)
|
||||||
|
|
||||||
|
func SplitSimplePath(args []string) ([]interface{}, bool) {
|
||||||
|
path := make([]interface{}, 0)
|
||||||
|
for _, arg := range args {
|
||||||
|
s := ""
|
||||||
|
state := start
|
||||||
|
for _, ch := range arg {
|
||||||
|
switch state {
|
||||||
|
|
||||||
|
case start:
|
||||||
|
switch {
|
||||||
|
case ch == 'x':
|
||||||
|
state = unknown
|
||||||
|
case ch == '.':
|
||||||
|
state = propOrIndex
|
||||||
|
default:
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
|
||||||
|
case unknown:
|
||||||
|
switch {
|
||||||
|
case ch == '.':
|
||||||
|
state = prop
|
||||||
|
s = ""
|
||||||
|
case ch == '[':
|
||||||
|
state = index
|
||||||
|
s = ""
|
||||||
|
default:
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
|
||||||
|
case propOrIndex:
|
||||||
|
switch {
|
||||||
|
case isProp(ch):
|
||||||
|
state = prop
|
||||||
|
s = string(ch)
|
||||||
|
case ch == '[':
|
||||||
|
state = index
|
||||||
|
default:
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
|
||||||
|
case prop:
|
||||||
|
switch {
|
||||||
|
case isProp(ch):
|
||||||
|
s += string(ch)
|
||||||
|
case ch == '.':
|
||||||
|
state = prop
|
||||||
|
path = append(path, s)
|
||||||
|
s = ""
|
||||||
|
case ch == '[':
|
||||||
|
state = index
|
||||||
|
path = append(path, s)
|
||||||
|
s = ""
|
||||||
|
default:
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
|
||||||
|
case index:
|
||||||
|
switch {
|
||||||
|
case unicode.IsDigit(ch):
|
||||||
|
state = number
|
||||||
|
s = string(ch)
|
||||||
|
case ch == '"':
|
||||||
|
state = doubleQuote
|
||||||
|
s = ""
|
||||||
|
case ch == '\'':
|
||||||
|
state = singleQuote
|
||||||
|
s = ""
|
||||||
|
default:
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
|
||||||
|
case indexEnd:
|
||||||
|
switch {
|
||||||
|
case ch == ']':
|
||||||
|
state = unknown
|
||||||
|
default:
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
|
||||||
|
case number:
|
||||||
|
switch {
|
||||||
|
case unicode.IsDigit(ch):
|
||||||
|
s += string(ch)
|
||||||
|
case ch == ']':
|
||||||
|
state = unknown
|
||||||
|
n, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
path = append(path, n)
|
||||||
|
s = ""
|
||||||
|
default:
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
|
||||||
|
case doubleQuote:
|
||||||
|
switch ch {
|
||||||
|
case '"':
|
||||||
|
state = indexEnd
|
||||||
|
path = append(path, s)
|
||||||
|
s = ""
|
||||||
|
case '\\':
|
||||||
|
state = doubleQuoteEscape
|
||||||
|
default:
|
||||||
|
s += string(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
case doubleQuoteEscape:
|
||||||
|
switch ch {
|
||||||
|
case '"':
|
||||||
|
state = doubleQuote
|
||||||
|
s += string(ch)
|
||||||
|
default:
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
|
||||||
|
case singleQuote:
|
||||||
|
switch ch {
|
||||||
|
case '\'':
|
||||||
|
state = indexEnd
|
||||||
|
path = append(path, s)
|
||||||
|
s = ""
|
||||||
|
case '\\':
|
||||||
|
state = singleQuoteEscape
|
||||||
|
s += string(ch)
|
||||||
|
default:
|
||||||
|
s += string(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
case singleQuoteEscape:
|
||||||
|
switch ch {
|
||||||
|
case '\'':
|
||||||
|
state = singleQuote
|
||||||
|
s += string(ch)
|
||||||
|
default:
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(s) > 0 {
|
||||||
|
if state == prop {
|
||||||
|
path = append(path, s)
|
||||||
|
} else {
|
||||||
|
return path, false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isProp(ch rune) bool {
|
||||||
|
return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == '$'
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBySimplePath(object interface{}, path []interface{}) interface{} {
|
||||||
|
for _, get := range path {
|
||||||
|
switch get := get.(type) {
|
||||||
|
case string:
|
||||||
|
switch o := object.(type) {
|
||||||
|
case *Dict:
|
||||||
|
object = o.Values[get]
|
||||||
|
case string:
|
||||||
|
if get == "length" {
|
||||||
|
object = Number(strconv.Itoa(len([]rune(o))))
|
||||||
|
} else {
|
||||||
|
object = nil
|
||||||
|
}
|
||||||
|
case Array:
|
||||||
|
if get == "length" {
|
||||||
|
object = Number(strconv.Itoa(len(o)))
|
||||||
|
} else {
|
||||||
|
object = nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
object = nil
|
||||||
|
}
|
||||||
|
case int:
|
||||||
|
switch o := object.(type) {
|
||||||
|
case Array:
|
||||||
|
object = o[get]
|
||||||
|
default:
|
||||||
|
object = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
package reducer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_splitPath(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
args []string
|
||||||
|
want []interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
args: []string{},
|
||||||
|
want: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"."},
|
||||||
|
want: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"x"},
|
||||||
|
want: []interface{}{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{".foo"},
|
||||||
|
want: []interface{}{"foo"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"x.foo"},
|
||||||
|
want: []interface{}{"foo"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"x[42]"},
|
||||||
|
want: []interface{}{42},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{".[42]"},
|
||||||
|
want: []interface{}{42},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{".42"},
|
||||||
|
want: []interface{}{"42"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{".физ"},
|
||||||
|
want: []interface{}{"физ"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{".foo.bar"},
|
||||||
|
want: []interface{}{"foo", "bar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{".foo", ".bar"},
|
||||||
|
want: []interface{}{"foo", "bar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{".foo[42]"},
|
||||||
|
want: []interface{}{"foo", 42},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{".foo[42].bar"},
|
||||||
|
want: []interface{}{"foo", 42, "bar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{".foo[1][2]"},
|
||||||
|
want: []interface{}{"foo", 1, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{".foo[\"bar\"]"},
|
||||||
|
want: []interface{}{"foo", "bar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{".foo[\"bar\\\"\"]"},
|
||||||
|
want: []interface{}{"foo", "bar\""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{".foo['bar']['baz\\'']"},
|
||||||
|
want: []interface{}{"foo", "bar", "baz\\'"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
|
||||||
|
path, ok := SplitSimplePath(tt.args)
|
||||||
|
require.Equal(t, tt.want, path)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_splitPath_negative(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
args []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
args: []string{"./"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"x/"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"1+1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"x[42"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{".i % 2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"x[for x]"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"x['y'."},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"x[0?"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"x[\"\\u"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"x['\\n"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: []string{"x[9999999999999999999999999999999999999]"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
|
||||||
|
path, ok := SplitSimplePath(tt.args)
|
||||||
|
require.False(t, ok, path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package reducer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
. "github.com/antonmedv/fx/pkg/json"
|
||||||
|
. "github.com/antonmedv/fx/pkg/theme"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Echo(object interface{}, theme Theme) {
|
||||||
|
if s, ok := object.(string); ok {
|
||||||
|
fmt.Println(s)
|
||||||
|
} else {
|
||||||
|
fmt.Println(PrettyPrint(object, 1, theme))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func trace(args []string, i int) (pre, post, pointer string) {
|
||||||
|
pre = strings.Join(args[:i], " ")
|
||||||
|
if len(pre) > 20 {
|
||||||
|
pre = "..." + pre[len(pre)-20:]
|
||||||
|
}
|
||||||
|
post = strings.Join(args[i+1:], " ")
|
||||||
|
if len(post) > 20 {
|
||||||
|
post = post[:20] + "..."
|
||||||
|
}
|
||||||
|
pointer = fmt.Sprintf(
|
||||||
|
"%v %v %v",
|
||||||
|
strings.Repeat(" ", len(pre)),
|
||||||
|
strings.Repeat("^", len(args[i])),
|
||||||
|
strings.Repeat(" ", len(post)),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var flatMapRegex = regexp.MustCompile("^(\\.\\w*)+\\[]")
|
||||||
|
|
||||||
|
func fold(s []string) string {
|
||||||
|
if len(s) == 1 {
|
||||||
|
return "x => x" + s[0]
|
||||||
|
}
|
||||||
|
obj := s[0]
|
||||||
|
if obj == "." {
|
||||||
|
obj = "x"
|
||||||
|
} else {
|
||||||
|
obj = "x" + obj
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("x => Object.values(%v).flatMap(%v)", obj, fold(s[1:]))
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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')
|
|
||||||
})
|
|
@ -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()
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
const version = "24.0.0"
|
@ -0,0 +1,128 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *model) AtTop() bool {
|
||||||
|
return m.offset <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) AtBottom() bool {
|
||||||
|
return m.offset >= m.maxYOffset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) PastBottom() bool {
|
||||||
|
return m.offset > m.maxYOffset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) ScrollPercent() float64 {
|
||||||
|
if m.height >= len(m.lines) {
|
||||||
|
return 1.0
|
||||||
|
}
|
||||||
|
y := float64(m.offset)
|
||||||
|
h := float64(m.height)
|
||||||
|
t := float64(len(m.lines) - 1)
|
||||||
|
v := y / (t - h)
|
||||||
|
return math.Max(0.0, math.Min(1.0, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) maxYOffset() int {
|
||||||
|
return max(0, len(m.lines)-m.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) visibleLines() (lines []string) {
|
||||||
|
if len(m.lines) > 0 {
|
||||||
|
top := max(0, m.offset)
|
||||||
|
bottom := clamp(m.offset+m.height, top, len(m.lines))
|
||||||
|
lines = m.lines[top:bottom]
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) SetOffset(n int) {
|
||||||
|
m.offset = clamp(n, 0, m.maxYOffset())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) ViewDown() {
|
||||||
|
if m.AtBottom() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SetOffset(m.offset + m.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) ViewUp() {
|
||||||
|
if m.AtTop() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SetOffset(m.offset - m.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) HalfViewDown() {
|
||||||
|
if m.AtBottom() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SetOffset(m.offset + m.height/2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) HalfViewUp() {
|
||||||
|
if m.AtTop() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SetOffset(m.offset - m.height/2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) LineDown(n int) {
|
||||||
|
if m.AtBottom() || n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the Number of lines by which we're going to scroll isn't
|
||||||
|
// greater than the Number of lines we actually have left before we reach
|
||||||
|
// the bottom.
|
||||||
|
m.SetOffset(m.offset + n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) LineUp(n int) {
|
||||||
|
if m.AtTop() || n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the Number of lines by which we're going to scroll isn't
|
||||||
|
// greater than the Number of lines we are from the top.
|
||||||
|
m.SetOffset(m.offset - n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) GotoTop() {
|
||||||
|
if m.AtTop() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SetOffset(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) GotoBottom() {
|
||||||
|
m.SetOffset(m.maxYOffset())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) scrollDownToCursor() {
|
||||||
|
at := m.cursorLineNumber()
|
||||||
|
if m.offset <= at { // cursor is lower
|
||||||
|
m.LineDown(max(0, at-(m.offset+m.height-1))) // minus one is due to cursorLineNumber() starts from 0
|
||||||
|
} else {
|
||||||
|
m.SetOffset(at)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) scrollUpToCursor() {
|
||||||
|
at := m.cursorLineNumber()
|
||||||
|
if at < m.offset+m.height { // cursor is above
|
||||||
|
m.LineUp(max(0, m.offset-at))
|
||||||
|
} else {
|
||||||
|
m.SetOffset(at)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue