mirror of https://github.com/antonmedv/fx
Delete old version
parent
eac8fb6912
commit
8761945f38
@ -1,38 +0,0 @@
|
||||
module github.com/antonmedv/fx
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbles v0.15.0
|
||||
github.com/charmbracelet/bubbletea v0.23.2
|
||||
github.com/charmbracelet/lipgloss v0.7.1
|
||||
github.com/dop251/goja v0.0.0-20230402114112-623f9dda9079
|
||||
github.com/mattn/go-isatty v0.0.18
|
||||
github.com/mazznoer/colorgrad v0.9.1
|
||||
github.com/muesli/termenv v0.15.1
|
||||
github.com/stretchr/testify v1.7.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.9.0 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mazznoer/csscolorparser v0.1.3 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/reflow v0.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/term v0.7.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||
)
|
@ -1,176 +0,0 @@
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
||||
github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E=
|
||||
github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
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/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI=
|
||||
github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=
|
||||
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/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
|
||||
github.com/charmbracelet/bubbletea v0.23.2 h1:vuUJ9HJ7b/COy4I30e8xDVQ+VRDUEFykIjryPfgsdps=
|
||||
github.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM=
|
||||
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
github.com/charmbracelet/harmonica v0.2.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/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
|
||||
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
|
||||
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
|
||||
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
|
||||
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
||||
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
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/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI=
|
||||
github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
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 v0.0.0-20230402114112-623f9dda9079 h1:xkbJGxVnk5sM8/LXeTKaBOfAZrI+iqvIPyH8oK1c6CQ=
|
||||
github.com/dop251/goja v0.0.0-20230402114112-623f9dda9079/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
|
||||
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/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
||||
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ=
|
||||
github.com/google/pprof v0.0.0-20230406165453-00490a63f317/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||
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-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
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/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/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/colorgrad v0.9.1 h1:MB80JYVndKWSMEM1beNqnuOowWGhoQc3DXWXkFp6JlM=
|
||||
github.com/mazznoer/colorgrad v0.9.1/go.mod h1:WX2R9wt9B47+txJZVVpM9LY+LAGIdi4lTI5wIyreDH4=
|
||||
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/mazznoer/csscolorparser v0.1.3 h1:vug4zh6loQxAUxfU1DZEu70gTPufDPspamZlHAkKcxE=
|
||||
github.com/mazznoer/csscolorparser v0.1.3/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/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
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/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
|
||||
github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8=
|
||||
github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
|
||||
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
|
||||
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/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
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=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/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/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
@ -1,71 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func usage(keyMap KeyMap) string {
|
||||
title := lipgloss.NewStyle().Bold(true)
|
||||
pad := lipgloss.NewStyle().PaddingLeft(4)
|
||||
return fmt.Sprintf(`
|
||||
%v
|
||||
Terminal JSON viewer
|
||||
|
||||
%v
|
||||
fx data.json
|
||||
fx data.json .field
|
||||
curl ... | fx
|
||||
|
||||
%v
|
||||
-h, --help print help
|
||||
-v, --version print version
|
||||
--print-code print code of the reducer
|
||||
|
||||
%v
|
||||
%v
|
||||
|
||||
%v
|
||||
[https://fx.wtf]
|
||||
`,
|
||||
title.Render("fx "+version),
|
||||
title.Render("Usage"),
|
||||
title.Render("Flags"),
|
||||
title.Render("Key Bindings"),
|
||||
strings.Join(keyMapInfo(keyMap, pad), "\n"),
|
||||
title.Render("More info"),
|
||||
)
|
||||
}
|
||||
|
||||
func keyMapInfo(keyMap KeyMap, style lipgloss.Style) []string {
|
||||
v := reflect.ValueOf(keyMap)
|
||||
fields := reflect.VisibleFields(v.Type())
|
||||
|
||||
keys := make([]string, 0)
|
||||
for i := range fields {
|
||||
k := v.Field(i).Interface().(key.Binding)
|
||||
str := k.Help().Key
|
||||
if width(str) == 0 {
|
||||
str = strings.Join(k.Keys(), ", ")
|
||||
}
|
||||
keys = append(keys, fmt.Sprintf("%v ", str))
|
||||
}
|
||||
|
||||
desc := make([]string, 0)
|
||||
for i := range fields {
|
||||
k := v.Field(i).Interface().(key.Binding)
|
||||
desc = append(desc, fmt.Sprintf("%v", k.Help().Desc))
|
||||
}
|
||||
|
||||
content := lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
strings.Join(keys, "\n"),
|
||||
strings.Join(desc, "\n"),
|
||||
)
|
||||
|
||||
return strings.Split(style.Render(content), "\n")
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
package main
|
||||
|
||||
import "github.com/charmbracelet/bubbles/key"
|
||||
|
||||
type KeyMap struct {
|
||||
Quit key.Binding
|
||||
Help key.Binding
|
||||
PageDown key.Binding
|
||||
PageUp key.Binding
|
||||
HalfPageUp key.Binding
|
||||
HalfPageDown key.Binding
|
||||
GotoTop key.Binding
|
||||
GotoBottom key.Binding
|
||||
Down key.Binding
|
||||
Up key.Binding
|
||||
Expand key.Binding
|
||||
Collapse key.Binding
|
||||
ExpandRecursively key.Binding
|
||||
CollapseRecursively key.Binding
|
||||
ExpandAll key.Binding
|
||||
CollapseAll key.Binding
|
||||
NextSibling key.Binding
|
||||
PrevSibling key.Binding
|
||||
ToggleWrap key.Binding
|
||||
Search key.Binding
|
||||
Next key.Binding
|
||||
Prev key.Binding
|
||||
}
|
||||
|
||||
func DefaultKeyMap() KeyMap {
|
||||
return KeyMap{
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("q", "ctrl+c", "esc"),
|
||||
key.WithHelp("", "exit program"),
|
||||
),
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("?"),
|
||||
key.WithHelp("", "show help"),
|
||||
),
|
||||
PageDown: key.NewBinding(
|
||||
key.WithKeys("pgdown", " ", "f"),
|
||||
key.WithHelp("pgdown, space, f", "page down"),
|
||||
),
|
||||
PageUp: key.NewBinding(
|
||||
key.WithKeys("pgup", "b"),
|
||||
key.WithHelp("pgup, b", "page up"),
|
||||
),
|
||||
HalfPageUp: key.NewBinding(
|
||||
key.WithKeys("u", "ctrl+u"),
|
||||
key.WithHelp("", "half page up"),
|
||||
),
|
||||
HalfPageDown: key.NewBinding(
|
||||
key.WithKeys("d", "ctrl+d"),
|
||||
key.WithHelp("", "half page down"),
|
||||
),
|
||||
GotoTop: key.NewBinding(
|
||||
key.WithKeys("g"),
|
||||
key.WithHelp("", "goto top"),
|
||||
),
|
||||
GotoBottom: key.NewBinding(
|
||||
key.WithKeys("G"),
|
||||
key.WithHelp("", "goto bottom"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("", "down"),
|
||||
),
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("", "up"),
|
||||
),
|
||||
Expand: key.NewBinding(
|
||||
key.WithKeys("right", "l"),
|
||||
key.WithHelp("", "expand"),
|
||||
),
|
||||
Collapse: key.NewBinding(
|
||||
key.WithKeys("left", "h"),
|
||||
key.WithHelp("", "collapse"),
|
||||
),
|
||||
ExpandRecursively: key.NewBinding(
|
||||
key.WithKeys("L"),
|
||||
key.WithHelp("", "expand recursively"),
|
||||
),
|
||||
CollapseRecursively: key.NewBinding(
|
||||
key.WithKeys("H"),
|
||||
key.WithHelp("", "collapse recursively"),
|
||||
),
|
||||
ExpandAll: key.NewBinding(
|
||||
key.WithKeys("e"),
|
||||
key.WithHelp("", "expand all"),
|
||||
),
|
||||
CollapseAll: key.NewBinding(
|
||||
key.WithKeys("E"),
|
||||
key.WithHelp("", "collapse all"),
|
||||
),
|
||||
NextSibling: key.NewBinding(
|
||||
key.WithKeys("J"),
|
||||
key.WithHelp("", "next sibling"),
|
||||
),
|
||||
PrevSibling: key.NewBinding(
|
||||
key.WithKeys("K"),
|
||||
key.WithHelp("", "previous sibling"),
|
||||
),
|
||||
ToggleWrap: key.NewBinding(
|
||||
key.WithKeys("z"),
|
||||
key.WithHelp("", "toggle strings wrap"),
|
||||
),
|
||||
Search: key.NewBinding(
|
||||
key.WithKeys("/"),
|
||||
key.WithHelp("", "search regexp"),
|
||||
),
|
||||
Next: key.NewBinding(
|
||||
key.WithKeys("n"),
|
||||
key.WithHelp("", "next search result"),
|
||||
),
|
||||
Prev: key.NewBinding(
|
||||
key.WithKeys("N"),
|
||||
key.WithHelp("", "prev search result"),
|
||||
),
|
||||
}
|
||||
}
|
@ -1,674 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
. "github.com/antonmedv/fx/pkg/reducer"
|
||||
. "github.com/antonmedv/fx/pkg/theme"
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
var (
|
||||
flagHelp bool
|
||||
flagVersion bool
|
||||
flagPrintCode bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
var args []string
|
||||
for _, arg := range os.Args[1:] {
|
||||
switch arg {
|
||||
case "-h", "--help":
|
||||
flagHelp = true
|
||||
case "-v", "-V", "--version":
|
||||
flagVersion = true
|
||||
case "--print-code":
|
||||
flagPrintCode = true
|
||||
default:
|
||||
args = append(args, arg)
|
||||
}
|
||||
|
||||
}
|
||||
if flagHelp {
|
||||
fmt.Println(usage(DefaultKeyMap()))
|
||||
return
|
||||
}
|
||||
if flagVersion {
|
||||
fmt.Println(version)
|
||||
return
|
||||
}
|
||||
cpuProfile := os.Getenv("CPU_PROFILE")
|
||||
if cpuProfile != "" {
|
||||
f, err := os.Create(cpuProfile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = pprof.StartCPUProfile(f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
themeId, ok := os.LookupEnv("FX_THEME")
|
||||
if !ok {
|
||||
themeId = "1"
|
||||
}
|
||||
theme, ok := Themes[themeId]
|
||||
if !ok {
|
||||
theme = Themes["1"]
|
||||
}
|
||||
if termenv.ColorProfile() == termenv.Ascii {
|
||||
theme = Themes["0"]
|
||||
}
|
||||
var showSize bool
|
||||
if s, ok := os.LookupEnv("FX_SHOW_SIZE"); ok {
|
||||
if s == "true" {
|
||||
showSize = true
|
||||
}
|
||||
}
|
||||
|
||||
stdinIsTty := isatty.IsTerminal(os.Stdin.Fd())
|
||||
stdoutIsTty := isatty.IsTerminal(os.Stdout.Fd())
|
||||
filePath := ""
|
||||
fileName := ""
|
||||
var dec *json.Decoder
|
||||
if stdinIsTty {
|
||||
// Nothing was piped, maybe file argument?
|
||||
if len(args) >= 1 {
|
||||
filePath = args[0]
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *fs.PathError:
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
default:
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
fileName = path.Base(filePath)
|
||||
dec = json.NewDecoder(f)
|
||||
args = args[1:]
|
||||
}
|
||||
} else {
|
||||
dec = json.NewDecoder(os.Stdin)
|
||||
}
|
||||
if dec == nil {
|
||||
fmt.Println(usage(DefaultKeyMap()))
|
||||
os.Exit(1)
|
||||
}
|
||||
dec.UseNumber()
|
||||
object, err := Parse(dec)
|
||||
if err != nil {
|
||||
fmt.Println("JSON Parse Error:", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
lang, ok := os.LookupEnv("FX_LANG")
|
||||
if !ok {
|
||||
lang = "js"
|
||||
}
|
||||
var fxrc string
|
||||
if lang == "js" || lang == "node" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
b, err := os.ReadFile(path.Join(home, ".fxrc.js"))
|
||||
if err == nil {
|
||||
fxrc = "\n" + string(b)
|
||||
if lang == "js" {
|
||||
parts := strings.SplitN(fxrc, "// nodejs:", 2)
|
||||
fxrc = parts[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if dec.More() {
|
||||
os.Exit(stream(dec, object, lang, args, theme, fxrc))
|
||||
}
|
||||
if len(args) > 0 || !stdoutIsTty {
|
||||
if len(args) > 0 && flagPrintCode {
|
||||
fmt.Print(GenerateCode(lang, args, fxrc))
|
||||
return
|
||||
}
|
||||
if lang == "js" {
|
||||
simplePath, ok := SplitSimplePath(args)
|
||||
if ok {
|
||||
output := GetBySimplePath(object, simplePath)
|
||||
Echo(output, theme)
|
||||
os.Exit(0)
|
||||
}
|
||||
vm, fn, err := CreateJS(args, fxrc)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(ReduceJS(vm, fn, object, theme))
|
||||
} else {
|
||||
os.Exit(Reduce(object, lang, args, theme, fxrc))
|
||||
}
|
||||
}
|
||||
|
||||
// Start interactive mode.
|
||||
expand := map[string]bool{"": true}
|
||||
if array, ok := object.(Array); ok {
|
||||
for i := range array {
|
||||
expand[accessor("", i)] = true
|
||||
}
|
||||
}
|
||||
parents := map[string]string{}
|
||||
children := map[string][]string{}
|
||||
canBeExpanded := map[string]bool{}
|
||||
Dfs(object, func(it Iterator) {
|
||||
parents[it.Path] = it.Parent
|
||||
children[it.Parent] = append(children[it.Parent], it.Path)
|
||||
switch it.Object.(type) {
|
||||
case *Dict:
|
||||
canBeExpanded[it.Path] = len(it.Object.(*Dict).Keys) > 0
|
||||
case Array:
|
||||
canBeExpanded[it.Path] = len(it.Object.(Array)) > 0
|
||||
}
|
||||
})
|
||||
input := textinput.New()
|
||||
input.Prompt = ""
|
||||
m := &model{
|
||||
fileName: fileName,
|
||||
theme: theme,
|
||||
json: object,
|
||||
showSize: showSize,
|
||||
width: 80,
|
||||
height: 60,
|
||||
mouseWheelDelta: 3,
|
||||
keyMap: DefaultKeyMap(),
|
||||
expandedPaths: expand,
|
||||
canBeExpanded: canBeExpanded,
|
||||
parents: parents,
|
||||
children: children,
|
||||
nextSiblings: map[string]string{},
|
||||
prevSiblings: map[string]string{},
|
||||
wrap: true,
|
||||
searchInput: input,
|
||||
}
|
||||
m.collectSiblings(m.json, "")
|
||||
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
|
||||
if err := p.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if cpuProfile != "" {
|
||||
pprof.StopCPUProfile()
|
||||
}
|
||||
os.Exit(m.exitCode)
|
||||
}
|
||||
|
||||
type model struct {
|
||||
exitCode int
|
||||
width, height int
|
||||
windowHeight int
|
||||
footerHeight int
|
||||
wrap bool
|
||||
theme Theme
|
||||
showSize bool // Show number of elements in preview
|
||||
|
||||
fileName string
|
||||
json interface{}
|
||||
lines []string
|
||||
|
||||
mouseWheelDelta int // Number of lines the mouse wheel will scroll
|
||||
offset int // offset is the vertical scroll position
|
||||
|
||||
keyMap KeyMap
|
||||
showHelp bool
|
||||
|
||||
expandedPaths map[string]bool // set of expanded paths
|
||||
canBeExpanded map[string]bool // set of path => can be expanded (i.e. dict or array)
|
||||
paths []string // array of paths on screen
|
||||
pathToLineNumber map[string]int // map of path => line Number
|
||||
pathToIndex map[string]int // map of path => index in m.paths
|
||||
lineNumberToPath map[int]string // map of line Number => path
|
||||
parents map[string]string // map of subpath => parent path
|
||||
children map[string][]string // map of path => child paths
|
||||
nextSiblings, prevSiblings map[string]string // map of path => sibling path
|
||||
cursor int // cursor in [0, len(m.paths)]
|
||||
showCursor bool
|
||||
|
||||
searchInput textinput.Model
|
||||
searchRegexCompileError string
|
||||
showSearchResults bool
|
||||
searchResults []*searchResult
|
||||
searchResultsCursor int
|
||||
highlightIndex map[string]*rangeGroup
|
||||
}
|
||||
|
||||
func (m *model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.windowHeight = msg.Height
|
||||
m.searchInput.Width = msg.Width - 2 // minus prompt
|
||||
m.render()
|
||||
|
||||
case tea.MouseMsg:
|
||||
switch msg.Type {
|
||||
case tea.MouseWheelUp:
|
||||
m.LineUp(m.mouseWheelDelta)
|
||||
case tea.MouseWheelDown:
|
||||
m.LineDown(m.mouseWheelDelta)
|
||||
}
|
||||
}
|
||||
|
||||
if m.searchInput.Focused() {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.Type {
|
||||
case tea.KeyEsc:
|
||||
m.searchInput.Blur()
|
||||
m.clearSearchResults()
|
||||
m.render()
|
||||
|
||||
case tea.KeyEnter:
|
||||
m.doSearch(m.searchInput.Value())
|
||||
}
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.searchInput, cmd = m.searchInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.keyMap.PageDown):
|
||||
m.ViewDown()
|
||||
m.moveCursorToView()
|
||||
m.render()
|
||||
case key.Matches(msg, m.keyMap.PageUp):
|
||||
m.ViewUp()
|
||||
m.moveCursorToView()
|
||||
m.render()
|
||||
case key.Matches(msg, m.keyMap.HalfPageDown):
|
||||
m.HalfViewDown()
|
||||
m.moveCursorToView()
|
||||
m.render()
|
||||
case key.Matches(msg, m.keyMap.HalfPageUp):
|
||||
m.HalfViewUp()
|
||||
m.moveCursorToView()
|
||||
m.render()
|
||||
case key.Matches(msg, m.keyMap.GotoTop):
|
||||
m.GotoTop()
|
||||
m.moveCursorToTop()
|
||||
m.render()
|
||||
case key.Matches(msg, m.keyMap.GotoBottom):
|
||||
m.GotoBottom()
|
||||
m.moveCursorToBottom()
|
||||
m.render()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) moveCursorToView() {
|
||||
top := 0
|
||||
bottom := 0
|
||||
|
||||
// find the first path in the viewport
|
||||
for i := 0; i < m.height && top == 0; i++ {
|
||||
top = m.pathToIndex[m.lineNumberToPath[m.offset+i]]
|
||||
}
|
||||
|
||||
// find the last path in the viewport
|
||||
for i := 1; i < m.height && bottom == 0; i++ {
|
||||
bottom = m.pathToIndex[m.lineNumberToPath[m.offset+m.height-i]]
|
||||
}
|
||||
|
||||
m.showCursor = true
|
||||
m.cursor = clamp(m.cursor, bottom, top)
|
||||
}
|
||||
|
||||
func (m *model) moveCursorToTop() {
|
||||
m.showCursor = true
|
||||
m.cursor = 0
|
||||
}
|
||||
|
||||
func (m *model) moveCursorToBottom() {
|
||||
m.showCursor = true
|
||||
m.cursor = len(m.paths) - 1
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package dict
|
||||
|
||||
type Dict struct {
|
||||
Keys []string
|
||||
Values map[string]interface{}
|
||||
}
|
||||
|
||||
func NewDict() *Dict {
|
||||
return &Dict{
|
||||
Keys: make([]string, 0),
|
||||
Values: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dict) Get(key string) (interface{}, bool) {
|
||||
val, exists := d.Values[key]
|
||||
return val, exists
|
||||
}
|
||||
|
||||
func (d *Dict) Set(key string, value interface{}) {
|
||||
_, exists := d.Values[key]
|
||||
if !exists {
|
||||
d.Keys = append(d.Keys, key)
|
||||
}
|
||||
d.Values[key] = value
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
package dict
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_dict(t *testing.T) {
|
||||
d := NewDict()
|
||||
d.Set("number", 3)
|
||||
v, _ := d.Get("number")
|
||||
if v.(int) != 3 {
|
||||
t.Error("Set number")
|
||||
}
|
||||
// string
|
||||
d.Set("string", "x")
|
||||
v, _ = d.Get("string")
|
||||
if v.(string) != "x" {
|
||||
t.Error("Set string")
|
||||
}
|
||||
// string slice
|
||||
d.Set("strings", []string{
|
||||
"t",
|
||||
"u",
|
||||
})
|
||||
v, _ = d.Get("strings")
|
||||
if v.([]string)[0] != "t" {
|
||||
t.Error("Set strings first index")
|
||||
}
|
||||
if v.([]string)[1] != "u" {
|
||||
t.Error("Set strings second index")
|
||||
}
|
||||
// mixed slice
|
||||
d.Set("mixed", []interface{}{
|
||||
1,
|
||||
"1",
|
||||
})
|
||||
v, _ = d.Get("mixed")
|
||||
if v.([]interface{})[0].(int) != 1 {
|
||||
t.Error("Set mixed int")
|
||||
}
|
||||
if v.([]interface{})[1].(string) != "1" {
|
||||
t.Error("Set mixed string")
|
||||
}
|
||||
// overriding existing key
|
||||
d.Set("number", 4)
|
||||
v, _ = d.Get("number")
|
||||
if v.(int) != 4 {
|
||||
t.Error("Override existing key")
|
||||
}
|
||||
// Keys
|
||||
expectedKeys := []string{
|
||||
"number",
|
||||
"string",
|
||||
"strings",
|
||||
"mixed",
|
||||
}
|
||||
for i, key := range d.Keys {
|
||||
if key != expectedKeys[i] {
|
||||
t.Error("Keys method", key, "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
for i, key := range expectedKeys {
|
||||
if key != expectedKeys[i] {
|
||||
t.Error("Keys method", key, "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
)
|
||||
|
||||
func Parse(dec *json.Decoder) (interface{}, error) {
|
||||
token, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if delim, ok := token.(json.Delim); ok {
|
||||
switch delim {
|
||||
case '{':
|
||||
return decodeDict(dec)
|
||||
case '[':
|
||||
return decodeArray(dec)
|
||||
}
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func decodeDict(dec *json.Decoder) (*Dict, error) {
|
||||
d := NewDict()
|
||||
for {
|
||||
token, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if delim, ok := token.(json.Delim); ok && delim == '}' {
|
||||
return d, nil
|
||||
}
|
||||
key := token.(string)
|
||||
token, err = dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var value interface{} = token
|
||||
if delim, ok := token.(json.Delim); ok {
|
||||
switch delim {
|
||||
case '{':
|
||||
value, err = decodeDict(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case '[':
|
||||
value, err = decodeArray(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
d.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeArray(dec *json.Decoder) ([]interface{}, error) {
|
||||
slice := make(Array, 0)
|
||||
for index := 0; ; index++ {
|
||||
token, err := dec.Token()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if delim, ok := token.(json.Delim); ok {
|
||||
switch delim {
|
||||
case '{':
|
||||
value, err := decodeDict(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slice = append(slice, value)
|
||||
case '[':
|
||||
value, err := decodeArray(dec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
slice = append(slice, value)
|
||||
case ']':
|
||||
return slice, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
slice = append(slice, token)
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_parse(t *testing.T) {
|
||||
input := `{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"a": 3,
|
||||
"slice": [{"z": "z", "1": "1"}]
|
||||
}`
|
||||
|
||||
p, err := Parse(json.NewDecoder(strings.NewReader(input)))
|
||||
if err != nil {
|
||||
t.Error("JSON parse error", err)
|
||||
}
|
||||
o := p.(*Dict)
|
||||
|
||||
expectedKeys := []string{
|
||||
"a",
|
||||
"b",
|
||||
"slice",
|
||||
}
|
||||
for i := range o.Keys {
|
||||
if o.Keys[i] != expectedKeys[i] {
|
||||
t.Error("Wrong key order ", i, o.Keys[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
|
||||
s, ok := o.Get("slice")
|
||||
if !ok {
|
||||
t.Error("slice missing")
|
||||
}
|
||||
a := s.(Array)
|
||||
z := a[0].(*Dict)
|
||||
|
||||
expectedKeys = []string{
|
||||
"z",
|
||||
"1",
|
||||
}
|
||||
for i := range z.Keys {
|
||||
if z.Keys[i] != expectedKeys[i] {
|
||||
t.Error("Wrong key order for nested map ", i, z.Keys[i], "!=", expectedKeys[i])
|
||||
}
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/antonmedv/fx/pkg/dict"
|
||||
"github.com/antonmedv/fx/pkg/theme"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func PrettyPrint(v interface{}, level int, theme theme.Theme) string {
|
||||
ident := strings.Repeat(" ", level)
|
||||
subident := strings.Repeat(" ", level-1)
|
||||
switch v := v.(type) {
|
||||
case nil:
|
||||
return theme.Null("null")
|
||||
|
||||
case bool:
|
||||
if v {
|
||||
return theme.Boolean("true")
|
||||
} else {
|
||||
return theme.Boolean("false")
|
||||
}
|
||||
|
||||
case json.Number:
|
||||
return theme.Number(v.String())
|
||||
|
||||
case string:
|
||||
return theme.String(fmt.Sprintf("%q", v))
|
||||
|
||||
case *dict.Dict:
|
||||
keys := v.Keys
|
||||
if len(keys) == 0 {
|
||||
return theme.Syntax("{}")
|
||||
}
|
||||
output := theme.Syntax("{")
|
||||
output += "\n"
|
||||
for i, k := range keys {
|
||||
key := theme.Key(i, len(keys))(fmt.Sprintf("%q", k))
|
||||
value, _ := v.Get(k)
|
||||
delim := theme.Syntax(": ")
|
||||
line := ident + key + delim + PrettyPrint(value, level+1, theme)
|
||||
if i < len(keys)-1 {
|
||||
line += theme.Syntax(",")
|
||||
}
|
||||
line += "\n"
|
||||
output += line
|
||||
}
|
||||
return output + subident + theme.Syntax("}")
|
||||
|
||||
case []interface{}:
|
||||
slice := v
|
||||
if len(slice) == 0 {
|
||||
return theme.Syntax("[]")
|
||||
}
|
||||
output := theme.Syntax("[\n")
|
||||
for i, value := range v {
|
||||
line := ident + PrettyPrint(value, level+1, theme)
|
||||
if i < len(slice)-1 {
|
||||
line += ",\n"
|
||||
} else {
|
||||
line += "\n"
|
||||
}
|
||||
output += line
|
||||
}
|
||||
return output + subident + theme.Syntax("]")
|
||||
|
||||
default:
|
||||
return "unknown type"
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
)
|
||||
|
||||
func Stringify(v interface{}) string {
|
||||
switch v := v.(type) {
|
||||
case nil:
|
||||
return "null"
|
||||
|
||||
case bool:
|
||||
if v {
|
||||
return "true"
|
||||
} else {
|
||||
return "false"
|
||||
}
|
||||
|
||||
case Number:
|
||||
return v.String()
|
||||
|
||||
case string:
|
||||
return fmt.Sprintf("%q", v)
|
||||
|
||||
case *Dict:
|
||||
result := "{"
|
||||
for i, key := range v.Keys {
|
||||
line := fmt.Sprintf("%q", key) + ": " + Stringify(v.Values[key])
|
||||
if i < len(v.Keys)-1 {
|
||||
line += ","
|
||||
}
|
||||
result += line
|
||||
}
|
||||
return result + "}"
|
||||
|
||||
case Array:
|
||||
result := "["
|
||||
for i, value := range v {
|
||||
line := Stringify(value)
|
||||
if i < len(v)-1 {
|
||||
line += ","
|
||||
}
|
||||
result += line
|
||||
}
|
||||
return result + "]"
|
||||
|
||||
default:
|
||||
return "unknown type"
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_stringify(t *testing.T) {
|
||||
t.Run("dict", func(t *testing.T) {
|
||||
arg := NewDict()
|
||||
arg.Set("a", Number("1"))
|
||||
arg.Set("b", Number("2"))
|
||||
want := `{"a": 1,"b": 2}`
|
||||
if got := Stringify(arg); got != want {
|
||||
t.Errorf("stringify() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("array", func(t *testing.T) {
|
||||
arg := Array{Number("1"), Number("2")}
|
||||
want := `[1,2]`
|
||||
if got := Stringify(arg); got != want {
|
||||
t.Errorf("stringify() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
t.Run("array_with_dict", func(t *testing.T) {
|
||||
arg := Array{NewDict(), Array{}}
|
||||
want := `[{},[]]`
|
||||
if got := Stringify(arg); got != want {
|
||||
t.Errorf("stringify() = %v, want %v", got, want)
|
||||
}
|
||||
})
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
)
|
||||
|
||||
type Iterator struct {
|
||||
Object interface{}
|
||||
Path, Parent string
|
||||
}
|
||||
|
||||
func Dfs(object interface{}, f func(it Iterator)) {
|
||||
sub(Iterator{Object: object}, f)
|
||||
}
|
||||
|
||||
func sub(it Iterator, f func(it Iterator)) {
|
||||
f(it)
|
||||
switch it.Object.(type) {
|
||||
case *Dict:
|
||||
keys := it.Object.(*Dict).Keys
|
||||
for _, k := range keys {
|
||||
subpath := it.Path + "." + k
|
||||
value, _ := it.Object.(*Dict).Get(k)
|
||||
sub(Iterator{
|
||||
Object: value,
|
||||
Path: subpath,
|
||||
Parent: it.Path,
|
||||
}, f)
|
||||
}
|
||||
|
||||
case Array:
|
||||
slice := it.Object.(Array)
|
||||
for i, value := range slice {
|
||||
subpath := accessor(it.Path, i)
|
||||
sub(Iterator{
|
||||
Object: value,
|
||||
Path: subpath,
|
||||
Parent: it.Path,
|
||||
}, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func accessor(path string, to interface{}) string {
|
||||
return fmt.Sprintf("%v[%v]", path, to)
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package json
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Number = json.Number
|
||||
type Array = []interface{}
|
@ -1,98 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
. "github.com/antonmedv/fx/pkg/theme"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
//go:embed js.js
|
||||
var templateJs string
|
||||
|
||||
func js(args []string, fxrc string) string {
|
||||
rs := "\n"
|
||||
for i, a := range args {
|
||||
rs += " try {"
|
||||
switch {
|
||||
case flatMapRegex.MatchString(a):
|
||||
code := fold(strings.Split(a, "[]"))
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
x = (
|
||||
%v
|
||||
)(x)
|
||||
`, code)
|
||||
|
||||
case strings.HasPrefix(a, ".["):
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
x = function ()
|
||||
{ return this%v }
|
||||
.call(x)
|
||||
`, a[1:])
|
||||
|
||||
case strings.HasPrefix(a, "."):
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
x = function ()
|
||||
{ return this%v }
|
||||
.call(x)
|
||||
`, a)
|
||||
|
||||
default:
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
let f = function ()
|
||||
{ return %v }
|
||||
.call(x)
|
||||
x = typeof f === 'function' ? f(x) : f
|
||||
`, a)
|
||||
}
|
||||
// Generate a beautiful error message.
|
||||
rs += " } catch (e) {\n"
|
||||
pre, post, pointer := trace(args, i)
|
||||
rs += fmt.Sprintf(
|
||||
" throw `\\n ${%q} ${%q} ${%q}\\n %v\\n\\n${e.stack || e}`\n",
|
||||
pre, a, post, pointer,
|
||||
)
|
||||
rs += " }\n"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(templateJs, fxrc, rs)
|
||||
}
|
||||
|
||||
func CreateJS(args []string, fxrc string) (*goja.Runtime, goja.Callable, error) {
|
||||
vm := goja.New()
|
||||
_, err := vm.RunString(js(args, fxrc))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
fn, ok := goja.AssertFunction(vm.Get("reduce"))
|
||||
if !ok {
|
||||
panic("Not a function")
|
||||
}
|
||||
return vm, fn, nil
|
||||
}
|
||||
|
||||
func ReduceJS(vm *goja.Runtime, reduce goja.Callable, input interface{}, theme Theme) int {
|
||||
value, err := reduce(goja.Undefined(), vm.ToValue(Stringify(input)))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
output := value.String()
|
||||
dec := json.NewDecoder(strings.NewReader(output))
|
||||
dec.UseNumber()
|
||||
object, err := Parse(dec)
|
||||
if err != nil {
|
||||
fmt.Print(output)
|
||||
return 0
|
||||
}
|
||||
Echo(object, theme)
|
||||
return 0
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
// .fxrc.js %v
|
||||
|
||||
function reduce(input) {
|
||||
let x = JSON.parse(input)
|
||||
|
||||
// Reducers %v
|
||||
if (typeof x === 'undefined') {
|
||||
return 'null'
|
||||
} else {
|
||||
return JSON.stringify(x)
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CreateNodejs(args []string, fxrc string) *exec.Cmd {
|
||||
cmd := exec.Command("node", "--input-type=module", "-e", nodejs(args, fxrc))
|
||||
nodePath, exist := os.LookupEnv("NODE_PATH")
|
||||
if exist {
|
||||
cmd.Dir = path.Dir(nodePath)
|
||||
}
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "NODE_OPTIONS=--max-old-space-size=8192")
|
||||
workingDir, err := os.Getwd()
|
||||
if err == nil {
|
||||
cmd.Env = append(cmd.Env, "FX_CWD="+workingDir)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
//go:embed node.js
|
||||
var templateNode string
|
||||
|
||||
func nodejs(args []string, fxrc string) string {
|
||||
rs := "\n"
|
||||
for i, a := range args {
|
||||
rs += " try {"
|
||||
switch {
|
||||
case flatMapRegex.MatchString(a):
|
||||
code := fold(strings.Split(a, "[]"))
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
x = (
|
||||
%v
|
||||
)(x)
|
||||
`, code)
|
||||
|
||||
case strings.HasPrefix(a, ".["):
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
x = function ()
|
||||
{ return this%v }
|
||||
.call(x)
|
||||
`, a[1:])
|
||||
|
||||
case strings.HasPrefix(a, "."):
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
x = function ()
|
||||
{ return this%v }
|
||||
.call(x)
|
||||
`, a)
|
||||
|
||||
default:
|
||||
rs += fmt.Sprintf(
|
||||
`
|
||||
let f = function ()
|
||||
{ return %v }
|
||||
.call(x)
|
||||
x = typeof f === 'function' ? f(x) : f
|
||||
`, a)
|
||||
}
|
||||
rs += `
|
||||
x = await x
|
||||
`
|
||||
// Generate a beautiful error message.
|
||||
rs += " } catch (e) {\n"
|
||||
pre, post, pointer := trace(args, i)
|
||||
rs += fmt.Sprintf(
|
||||
" throw `\\n ${%q} ${%q} ${%q}\\n %v\\n\\n${e.stack || e}`\n",
|
||||
pre, a, post, pointer,
|
||||
)
|
||||
rs += " }\n"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(templateNode, fxrc, rs)
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
import os from 'node:os'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import {createRequire} from 'node:module'
|
||||
|
||||
const cwd = process.env.FX_CWD ? process.env.FX_CWD : process.cwd()
|
||||
const require = createRequire(cwd)
|
||||
|
||||
// .fxrc.js %v
|
||||
|
||||
void async function () {
|
||||
process.chdir(cwd)
|
||||
|
||||
let buffer = ''
|
||||
process.stdin.setEncoding('utf8')
|
||||
for await (let chunk of process.stdin) {
|
||||
buffer += chunk
|
||||
}
|
||||
let x = JSON.parse(buffer)
|
||||
|
||||
// Reducers %v
|
||||
|
||||
if (typeof x === 'undefined') {
|
||||
process.stderr.write('undefined')
|
||||
} else {
|
||||
process.stdout.write(JSON.stringify(x))
|
||||
}
|
||||
}().catch(err => {
|
||||
console.error(err)
|
||||
process.exitCode = 1
|
||||
})
|
@ -1,38 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func CreatePython(bin string, args []string) *exec.Cmd {
|
||||
cmd := exec.Command(bin, "-c", python(args))
|
||||
return cmd
|
||||
}
|
||||
|
||||
//go:embed python.py
|
||||
var templatePython string
|
||||
|
||||
func python(args []string) string {
|
||||
rs := "\n"
|
||||
for i, a := range args {
|
||||
rs += fmt.Sprintf(
|
||||
`try:
|
||||
f = (lambda x: (%v))(x)
|
||||
x = f(x) if callable(f) else f
|
||||
`, a)
|
||||
|
||||
// Generate a beautiful error message.
|
||||
rs += "except Exception as e:\n"
|
||||
pre, post, pointer := trace(args, i)
|
||||
rs += fmt.Sprintf(
|
||||
` sys.stderr.write('\n {} {} {}\n %v\n\n{}\n'.format(%q, %q, %q, e))
|
||||
sys.exit(1)`,
|
||||
pointer,
|
||||
pre, a, post,
|
||||
)
|
||||
rs += "\n"
|
||||
}
|
||||
return fmt.Sprintf(templatePython, rs)
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import json, sys, os
|
||||
x = json.load(sys.stdin)
|
||||
|
||||
# Reducers %v
|
||||
|
||||
try:
|
||||
print(json.dumps(x))
|
||||
except:
|
||||
print(json.dumps(list(x)))
|
@ -1,76 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
. "github.com/antonmedv/fx/pkg/theme"
|
||||
)
|
||||
|
||||
func GenerateCode(lang string, args []string, fxrc string) string {
|
||||
switch lang {
|
||||
case "js":
|
||||
return js(args, fxrc)
|
||||
case "node":
|
||||
return nodejs(args, fxrc)
|
||||
case "python", "python3":
|
||||
return python(args)
|
||||
case "ruby":
|
||||
return ruby(args)
|
||||
default:
|
||||
panic("unknown lang")
|
||||
}
|
||||
}
|
||||
|
||||
func Reduce(input interface{}, lang string, args []string, theme Theme, fxrc string) int {
|
||||
path, ok := SplitSimplePath(args)
|
||||
if ok {
|
||||
output := GetBySimplePath(input, path)
|
||||
Echo(output, theme)
|
||||
return 0
|
||||
}
|
||||
var cmd *exec.Cmd
|
||||
switch lang {
|
||||
case "node":
|
||||
cmd = CreateNodejs(args, fxrc)
|
||||
case "python", "python3":
|
||||
cmd = CreatePython(lang, args)
|
||||
case "ruby":
|
||||
cmd = CreateRuby(args)
|
||||
default:
|
||||
panic("unknown lang")
|
||||
}
|
||||
|
||||
// TODO: Reimplement stringify with io.Reader.
|
||||
cmd.Stdin = strings.NewReader(Stringify(input))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
exitCode := 1
|
||||
status, ok := err.(*exec.ExitError)
|
||||
if ok {
|
||||
exitCode = status.ExitCode()
|
||||
} else {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
fmt.Print(string(output))
|
||||
return exitCode
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(bytes.NewReader(output))
|
||||
dec.UseNumber()
|
||||
object, err := Parse(dec)
|
||||
if err != nil {
|
||||
fmt.Print(string(output))
|
||||
return 0
|
||||
}
|
||||
Echo(object, theme)
|
||||
if dec.InputOffset() < int64(len(output)) {
|
||||
fmt.Print(string(output[dec.InputOffset():]))
|
||||
}
|
||||
return 0
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func CreateRuby(args []string) *exec.Cmd {
|
||||
cmd := exec.Command("ruby", "-e", ruby(args))
|
||||
return cmd
|
||||
}
|
||||
|
||||
//go:embed ruby.rb
|
||||
var templateRuby string
|
||||
|
||||
func ruby(args []string) string {
|
||||
rs := "\n"
|
||||
for i, a := range args {
|
||||
rs += fmt.Sprintf(
|
||||
`begin
|
||||
x = lambda {|x| %v }.call(x)
|
||||
`, a)
|
||||
// Generate a beautiful error message.
|
||||
rs += "rescue Exception => e\n"
|
||||
pre, post, pointer := trace(args, i)
|
||||
rs += fmt.Sprintf(
|
||||
` STDERR.puts "\n #{%q} #{%q} #{%q}\n %v\n\n#{e}\n"
|
||||
exit(1)
|
||||
`,
|
||||
pre, a, post,
|
||||
pointer,
|
||||
)
|
||||
rs += "end\n"
|
||||
}
|
||||
return fmt.Sprintf(templateRuby, rs)
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
require 'json'
|
||||
x = JSON.parse(STDIN.read)
|
||||
|
||||
# Reducers %v
|
||||
|
||||
puts JSON.generate(x)
|
@ -1,215 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"unicode"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
)
|
||||
|
||||
type state int
|
||||
|
||||
const (
|
||||
start state = iota
|
||||
unknown
|
||||
propOrIndex
|
||||
prop
|
||||
index
|
||||
indexEnd
|
||||
number
|
||||
doubleQuote
|
||||
doubleQuoteEscape
|
||||
singleQuote
|
||||
singleQuoteEscape
|
||||
)
|
||||
|
||||
func SplitSimplePath(args []string) ([]interface{}, bool) {
|
||||
path := make([]interface{}, 0)
|
||||
for _, arg := range args {
|
||||
s := ""
|
||||
state := start
|
||||
for _, ch := range arg {
|
||||
switch state {
|
||||
|
||||
case start:
|
||||
switch {
|
||||
case ch == 'x':
|
||||
state = unknown
|
||||
case ch == '.':
|
||||
state = propOrIndex
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case unknown:
|
||||
switch {
|
||||
case ch == '.':
|
||||
state = prop
|
||||
s = ""
|
||||
case ch == '[':
|
||||
state = index
|
||||
s = ""
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case propOrIndex:
|
||||
switch {
|
||||
case isProp(ch):
|
||||
state = prop
|
||||
s = string(ch)
|
||||
case ch == '[':
|
||||
state = index
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case prop:
|
||||
switch {
|
||||
case isProp(ch):
|
||||
s += string(ch)
|
||||
case ch == '.':
|
||||
state = prop
|
||||
path = append(path, s)
|
||||
s = ""
|
||||
case ch == '[':
|
||||
state = index
|
||||
path = append(path, s)
|
||||
s = ""
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case index:
|
||||
switch {
|
||||
case unicode.IsDigit(ch):
|
||||
state = number
|
||||
s = string(ch)
|
||||
case ch == '"':
|
||||
state = doubleQuote
|
||||
s = ""
|
||||
case ch == '\'':
|
||||
state = singleQuote
|
||||
s = ""
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case indexEnd:
|
||||
switch {
|
||||
case ch == ']':
|
||||
state = unknown
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case number:
|
||||
switch {
|
||||
case unicode.IsDigit(ch):
|
||||
s += string(ch)
|
||||
case ch == ']':
|
||||
state = unknown
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return path, false
|
||||
}
|
||||
path = append(path, n)
|
||||
s = ""
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case doubleQuote:
|
||||
switch ch {
|
||||
case '"':
|
||||
state = indexEnd
|
||||
path = append(path, s)
|
||||
s = ""
|
||||
case '\\':
|
||||
state = doubleQuoteEscape
|
||||
default:
|
||||
s += string(ch)
|
||||
}
|
||||
|
||||
case doubleQuoteEscape:
|
||||
switch ch {
|
||||
case '"':
|
||||
state = doubleQuote
|
||||
s += string(ch)
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
|
||||
case singleQuote:
|
||||
switch ch {
|
||||
case '\'':
|
||||
state = indexEnd
|
||||
path = append(path, s)
|
||||
s = ""
|
||||
case '\\':
|
||||
state = singleQuoteEscape
|
||||
s += string(ch)
|
||||
default:
|
||||
s += string(ch)
|
||||
}
|
||||
|
||||
case singleQuoteEscape:
|
||||
switch ch {
|
||||
case '\'':
|
||||
state = singleQuote
|
||||
s += string(ch)
|
||||
default:
|
||||
return path, false
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(s) > 0 {
|
||||
if state == prop {
|
||||
path = append(path, s)
|
||||
} else {
|
||||
return path, false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return path, true
|
||||
}
|
||||
|
||||
func isProp(ch rune) bool {
|
||||
return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == '$'
|
||||
}
|
||||
|
||||
func GetBySimplePath(object interface{}, path []interface{}) interface{} {
|
||||
for _, get := range path {
|
||||
switch get := get.(type) {
|
||||
case string:
|
||||
switch o := object.(type) {
|
||||
case *Dict:
|
||||
object = o.Values[get]
|
||||
case string:
|
||||
if get == "length" {
|
||||
object = Number(strconv.Itoa(len([]rune(o))))
|
||||
} else {
|
||||
object = nil
|
||||
}
|
||||
case Array:
|
||||
if get == "length" {
|
||||
object = Number(strconv.Itoa(len(o)))
|
||||
} else {
|
||||
object = nil
|
||||
}
|
||||
default:
|
||||
object = nil
|
||||
}
|
||||
case int:
|
||||
switch o := object.(type) {
|
||||
case Array:
|
||||
object = o[get]
|
||||
default:
|
||||
object = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return object
|
||||
}
|
@ -1,137 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_splitPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
args []string
|
||||
want []interface{}
|
||||
}{
|
||||
{
|
||||
args: []string{},
|
||||
want: []interface{}{},
|
||||
},
|
||||
{
|
||||
args: []string{"."},
|
||||
want: []interface{}{},
|
||||
},
|
||||
{
|
||||
args: []string{"x"},
|
||||
want: []interface{}{},
|
||||
},
|
||||
{
|
||||
args: []string{".foo"},
|
||||
want: []interface{}{"foo"},
|
||||
},
|
||||
{
|
||||
args: []string{"x.foo"},
|
||||
want: []interface{}{"foo"},
|
||||
},
|
||||
{
|
||||
args: []string{"x[42]"},
|
||||
want: []interface{}{42},
|
||||
},
|
||||
{
|
||||
args: []string{".[42]"},
|
||||
want: []interface{}{42},
|
||||
},
|
||||
{
|
||||
args: []string{".42"},
|
||||
want: []interface{}{"42"},
|
||||
},
|
||||
{
|
||||
args: []string{".физ"},
|
||||
want: []interface{}{"физ"},
|
||||
},
|
||||
{
|
||||
args: []string{".foo.bar"},
|
||||
want: []interface{}{"foo", "bar"},
|
||||
},
|
||||
{
|
||||
args: []string{".foo", ".bar"},
|
||||
want: []interface{}{"foo", "bar"},
|
||||
},
|
||||
{
|
||||
args: []string{".foo[42]"},
|
||||
want: []interface{}{"foo", 42},
|
||||
},
|
||||
{
|
||||
args: []string{".foo[42].bar"},
|
||||
want: []interface{}{"foo", 42, "bar"},
|
||||
},
|
||||
{
|
||||
args: []string{".foo[1][2]"},
|
||||
want: []interface{}{"foo", 1, 2},
|
||||
},
|
||||
{
|
||||
args: []string{".foo[\"bar\"]"},
|
||||
want: []interface{}{"foo", "bar"},
|
||||
},
|
||||
{
|
||||
args: []string{".foo[\"bar\\\"\"]"},
|
||||
want: []interface{}{"foo", "bar\""},
|
||||
},
|
||||
{
|
||||
args: []string{".foo['bar']['baz\\'']"},
|
||||
want: []interface{}{"foo", "bar", "baz\\'"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
|
||||
path, ok := SplitSimplePath(tt.args)
|
||||
require.Equal(t, tt.want, path)
|
||||
require.True(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_splitPath_negative(t *testing.T) {
|
||||
tests := []struct {
|
||||
args []string
|
||||
}{
|
||||
{
|
||||
args: []string{"./"},
|
||||
},
|
||||
{
|
||||
args: []string{"x/"},
|
||||
},
|
||||
{
|
||||
args: []string{"1+1"},
|
||||
},
|
||||
{
|
||||
args: []string{"x[42"},
|
||||
},
|
||||
{
|
||||
args: []string{".i % 2"},
|
||||
},
|
||||
{
|
||||
args: []string{"x[for x]"},
|
||||
},
|
||||
{
|
||||
args: []string{"x['y'."},
|
||||
},
|
||||
{
|
||||
args: []string{"x[0?"},
|
||||
},
|
||||
{
|
||||
args: []string{"x[\"\\u"},
|
||||
},
|
||||
{
|
||||
args: []string{"x['\\n"},
|
||||
},
|
||||
{
|
||||
args: []string{"x[9999999999999999999999999999999999999]"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(strings.Join(tt.args, " "), func(t *testing.T) {
|
||||
path, ok := SplitSimplePath(tt.args)
|
||||
require.False(t, ok, path)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package reducer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
. "github.com/antonmedv/fx/pkg/theme"
|
||||
)
|
||||
|
||||
func Echo(object interface{}, theme Theme) {
|
||||
if s, ok := object.(string); ok {
|
||||
fmt.Println(s)
|
||||
} else {
|
||||
fmt.Println(PrettyPrint(object, 1, theme))
|
||||
}
|
||||
}
|
||||
|
||||
func trace(args []string, i int) (pre, post, pointer string) {
|
||||
pre = strings.Join(args[:i], " ")
|
||||
if len(pre) > 20 {
|
||||
pre = "..." + pre[len(pre)-20:]
|
||||
}
|
||||
post = strings.Join(args[i+1:], " ")
|
||||
if len(post) > 20 {
|
||||
post = post[:20] + "..."
|
||||
}
|
||||
pointer = fmt.Sprintf(
|
||||
"%v %v %v",
|
||||
strings.Repeat(" ", len(pre)),
|
||||
strings.Repeat("^", len(args[i])),
|
||||
strings.Repeat(" ", len(post)),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
var flatMapRegex = regexp.MustCompile("^(\\.\\w*)+\\[]")
|
||||
|
||||
func fold(s []string) string {
|
||||
if len(s) == 1 {
|
||||
return "x => x" + s[0]
|
||||
}
|
||||
obj := s[0]
|
||||
if obj == "." {
|
||||
obj = "x"
|
||||
} else {
|
||||
obj = "x" + obj
|
||||
}
|
||||
return fmt.Sprintf("x => Object.values(%v).flatMap(%v)", obj, fold(s[1:]))
|
||||
}
|
@ -1,185 +0,0 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/mazznoer/colorgrad"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Theme struct {
|
||||
Cursor Color
|
||||
Syntax Color
|
||||
Preview Color
|
||||
StatusBar Color
|
||||
Search Color
|
||||
Key func(i, len int) Color
|
||||
String Color
|
||||
Null Color
|
||||
Boolean Color
|
||||
Number Color
|
||||
}
|
||||
type Color func(s ...string) string
|
||||
|
||||
var (
|
||||
defaultCursor = lipgloss.NewStyle().Reverse(true).Render
|
||||
defaultPreview = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("8")).Render
|
||||
defaultStatusBar = lipgloss.NewStyle().Background(lipgloss.Color("7")).Foreground(lipgloss.Color("0")).Render
|
||||
defaultSearch = lipgloss.NewStyle().Background(lipgloss.Color("11")).Foreground(lipgloss.Color("16")).Render
|
||||
defaultNull = fg("8")
|
||||
)
|
||||
|
||||
var Themes = map[string]Theme{
|
||||
"0": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: noColor,
|
||||
StatusBar: noColor,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return noColor },
|
||||
String: noColor,
|
||||
Null: noColor,
|
||||
Boolean: noColor,
|
||||
Number: noColor,
|
||||
},
|
||||
"1": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return boldFg("4") },
|
||||
String: boldFg("2"),
|
||||
Null: defaultNull,
|
||||
Boolean: boldFg("3"),
|
||||
Number: boldFg("6"),
|
||||
},
|
||||
"2": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return fg("#00F5D4") },
|
||||
String: fg("#00BBF9"),
|
||||
Null: defaultNull,
|
||||
Boolean: fg("#F15BB5"),
|
||||
Number: fg("#9B5DE5"),
|
||||
},
|
||||
"3": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return fg("#faf0ca") },
|
||||
String: fg("#f4d35e"),
|
||||
Null: defaultNull,
|
||||
Boolean: fg("#ee964b"),
|
||||
Number: fg("#ee964b"),
|
||||
},
|
||||
"4": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return fg("#4D96FF") },
|
||||
String: fg("#6BCB77"),
|
||||
Null: defaultNull,
|
||||
Boolean: fg("#FF6B6B"),
|
||||
Number: fg("#FFD93D"),
|
||||
},
|
||||
"5": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return boldFg("42") },
|
||||
String: boldFg("213"),
|
||||
Null: defaultNull,
|
||||
Boolean: boldFg("201"),
|
||||
Number: boldFg("201"),
|
||||
},
|
||||
"6": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return gradient("rgb(125,110,221)", "rgb(90%,45%,97%)", "hsl(229,79%,85%)") },
|
||||
String: fg("195"),
|
||||
Null: defaultNull,
|
||||
Boolean: fg("195"),
|
||||
Number: fg("195"),
|
||||
},
|
||||
"7": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: func(_, _ int) Color { return gradient("rgb(123,216,96)", "rgb(255,255,255)") },
|
||||
String: noColor,
|
||||
Null: defaultNull,
|
||||
Boolean: noColor,
|
||||
Number: noColor,
|
||||
},
|
||||
"8": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: gradientKeys("#ff0000", "#ff8700", "#ffd300", "#deff0a", "#a1ff0a", "#0aff99", "#0aefff", "#147df5", "#580aff", "#be0aff"),
|
||||
String: noColor,
|
||||
Null: defaultNull,
|
||||
Boolean: noColor,
|
||||
Number: noColor,
|
||||
},
|
||||
"9": {
|
||||
Cursor: defaultCursor,
|
||||
Syntax: noColor,
|
||||
Preview: defaultPreview,
|
||||
StatusBar: defaultStatusBar,
|
||||
Search: defaultSearch,
|
||||
Key: gradientKeys("rgb(34,126,34)", "rgb(168,251,60)"),
|
||||
String: gradient("rgb(34,126,34)", "rgb(168,251,60)"),
|
||||
Null: defaultNull,
|
||||
Boolean: noColor,
|
||||
Number: noColor,
|
||||
},
|
||||
}
|
||||
|
||||
func noColor(s ...string) string {
|
||||
return s[0]
|
||||
}
|
||||
|
||||
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[0])
|
||||
colors := grad.ColorfulColors(uint(len(runes)))
|
||||
var out strings.Builder
|
||||
for i, r := range runes {
|
||||
style := lipgloss.NewStyle().Foreground(lipgloss.Color(colors[i].Hex()))
|
||||
out.WriteString(style.Render(string(r)))
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
}
|
||||
|
||||
func gradientKeys(colors ...string) func(i, len int) Color {
|
||||
grad, _ := colorgrad.NewGradient().HtmlColors(colors...).Build()
|
||||
return func(i, len int) Color {
|
||||
return lipgloss.NewStyle().Foreground(lipgloss.Color(grad.At(float64(i) / float64(len)).Hex())).Render
|
||||
}
|
||||
}
|
@ -1,296 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
"github.com/antonmedv/fx/pkg/theme"
|
||||
)
|
||||
|
||||
func (m *model) connect(path string, lineNumber int) {
|
||||
if _, exist := m.pathToLineNumber[path]; exist {
|
||||
return
|
||||
}
|
||||
m.paths = append(m.paths, path)
|
||||
m.pathToIndex[path] = len(m.paths) - 1
|
||||
m.pathToLineNumber[path] = lineNumber
|
||||
m.lineNumberToPath[lineNumber] = path
|
||||
}
|
||||
|
||||
func (m *model) print(v interface{}, level, lineNumber, keyEndPos int, path string, selectableValues bool) []string {
|
||||
m.connect(path, lineNumber)
|
||||
ident := strings.Repeat(" ", level)
|
||||
subident := strings.Repeat(" ", level-1)
|
||||
highlight := m.highlightIndex[path]
|
||||
var searchValue []*foundRange
|
||||
if highlight != nil {
|
||||
searchValue = highlight.value
|
||||
}
|
||||
|
||||
switch v.(type) {
|
||||
case nil:
|
||||
return []string{merge(m.explode("null", searchValue, m.theme.Null, path, selectableValues))}
|
||||
|
||||
case bool:
|
||||
if v.(bool) {
|
||||
return []string{merge(m.explode("true", searchValue, m.theme.Boolean, path, selectableValues))}
|
||||
} else {
|
||||
return []string{merge(m.explode("false", searchValue, m.theme.Boolean, path, selectableValues))}
|
||||
}
|
||||
|
||||
case Number:
|
||||
return []string{merge(m.explode(v.(Number).String(), searchValue, m.theme.Number, path, selectableValues))}
|
||||
|
||||
case string:
|
||||
line := fmt.Sprintf("%q", v)
|
||||
chunks := m.explode(line, searchValue, m.theme.String, path, selectableValues)
|
||||
if m.wrap && keyEndPos+width(line) > m.width {
|
||||
return wrapLines(chunks, keyEndPos, m.width, subident)
|
||||
}
|
||||
// No wrap
|
||||
return []string{merge(chunks)}
|
||||
|
||||
case *Dict:
|
||||
if !m.expandedPaths[path] {
|
||||
return []string{m.preview(v, path, selectableValues)}
|
||||
}
|
||||
output := []string{m.printOpenBracket("{", highlight, path, selectableValues)}
|
||||
lineNumber++ // bracket is on separate line
|
||||
keys := v.(*Dict).Keys
|
||||
for i, k := range keys {
|
||||
subpath := path + "." + k
|
||||
highlight := m.highlightIndex[subpath]
|
||||
var keyRanges, delimRanges []*foundRange
|
||||
if highlight != nil {
|
||||
keyRanges = highlight.key
|
||||
delimRanges = highlight.delim
|
||||
}
|
||||
m.connect(subpath, lineNumber)
|
||||
key := fmt.Sprintf("%q", k)
|
||||
keyTheme := m.theme.Key(i, len(keys))
|
||||
key = merge(m.explode(key, keyRanges, keyTheme, subpath, true))
|
||||
value, _ := v.(*Dict).Get(k)
|
||||
delim := merge(m.explode(": ", delimRanges, m.theme.Syntax, subpath, false))
|
||||
keyEndPos := width(ident) + width(key) + width(delim)
|
||||
lines := m.print(value, level+1, lineNumber, keyEndPos, subpath, false)
|
||||
lines[0] = ident + key + delim + lines[0]
|
||||
if i < len(keys)-1 {
|
||||
lines[len(lines)-1] += m.printComma(",", highlight)
|
||||
}
|
||||
output = append(output, lines...)
|
||||
lineNumber += len(lines)
|
||||
}
|
||||
output = append(output, subident+m.printCloseBracket("}", highlight, path, false))
|
||||
return output
|
||||
|
||||
case Array:
|
||||
if !m.expandedPaths[path] {
|
||||
return []string{m.preview(v, path, selectableValues)}
|
||||
}
|
||||
output := []string{m.printOpenBracket("[", highlight, path, selectableValues)}
|
||||
lineNumber++ // bracket is on separate line
|
||||
slice := v.(Array)
|
||||
for i, value := range slice {
|
||||
subpath := fmt.Sprintf("%v[%v]", path, i)
|
||||
s := m.highlightIndex[subpath]
|
||||
m.connect(subpath, lineNumber)
|
||||
lines := m.print(value, level+1, lineNumber, width(ident), subpath, true)
|
||||
lines[0] = ident + lines[0]
|
||||
if i < len(slice)-1 {
|
||||
lines[len(lines)-1] += m.printComma(",", s)
|
||||
}
|
||||
lineNumber += len(lines)
|
||||
output = append(output, lines...)
|
||||
}
|
||||
output = append(output, subident+m.printCloseBracket("]", highlight, path, false))
|
||||
return output
|
||||
|
||||
default:
|
||||
return []string{"unknown type"}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) preview(v interface{}, path string, selectableValues bool) string {
|
||||
searchResult := m.highlightIndex[path]
|
||||
previewStyle := m.theme.Preview
|
||||
if selectableValues && m.cursorPath() == path {
|
||||
previewStyle = m.theme.Cursor
|
||||
}
|
||||
printValue := func(v interface{}) string {
|
||||
switch v := v.(type) {
|
||||
case nil, bool, Number:
|
||||
return previewStyle(fmt.Sprintf("%v", v))
|
||||
case string:
|
||||
return previewStyle(fmt.Sprintf("%q", v))
|
||||
case *Dict:
|
||||
if m.showSize {
|
||||
return previewStyle(toLowerNumber(fmt.Sprintf("{\u2026%v\u2026}", len(v.Keys))))
|
||||
} else {
|
||||
return previewStyle("{\u2026}")
|
||||
}
|
||||
case Array:
|
||||
if m.showSize {
|
||||
return previewStyle(toLowerNumber(fmt.Sprintf("[\u2026%v\u2026]", len(v))))
|
||||
} else {
|
||||
return previewStyle("[\u2026]")
|
||||
}
|
||||
}
|
||||
return "..."
|
||||
}
|
||||
|
||||
switch v := v.(type) {
|
||||
case *Dict:
|
||||
output := m.printOpenBracket("{", searchResult, path, selectableValues)
|
||||
keys := v.Keys
|
||||
for _, k := range keys {
|
||||
key := fmt.Sprintf("%q", k)
|
||||
output += previewStyle(key + ": ")
|
||||
value, _ := v.Get(k)
|
||||
output += printValue(value)
|
||||
break
|
||||
}
|
||||
if len(keys) > 1 {
|
||||
if m.showSize {
|
||||
output += previewStyle(toLowerNumber(fmt.Sprintf(", \u2026%v\u2026", len(v.Keys)-1)))
|
||||
} else {
|
||||
output += previewStyle(", \u2026")
|
||||
}
|
||||
}
|
||||
output += m.printCloseBracket("}", searchResult, path, selectableValues)
|
||||
return output
|
||||
|
||||
case Array:
|
||||
output := m.printOpenBracket("[", searchResult, path, selectableValues)
|
||||
for _, value := range v {
|
||||
output += printValue(value)
|
||||
break
|
||||
}
|
||||
if len(v) > 1 {
|
||||
if m.showSize {
|
||||
output += previewStyle(toLowerNumber(fmt.Sprintf(", \u2026%v\u2026", len(v)-1)))
|
||||
} else {
|
||||
output += previewStyle(", \u2026")
|
||||
}
|
||||
}
|
||||
output += m.printCloseBracket("]", searchResult, path, selectableValues)
|
||||
return output
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
func wrapLines(chunks []withStyle, keyEndPos, mWidth int, subident string) []string {
|
||||
wrappedLines := make([]string, 0)
|
||||
currentLine := ""
|
||||
ident := "" // First line stays on the same line with a "key",
|
||||
pos := keyEndPos // so no ident is needed. Start counting from the "key" offset.
|
||||
for _, chunk := range chunks {
|
||||
buffer := ""
|
||||
for _, ch := range chunk.value {
|
||||
buffer += string(ch)
|
||||
if pos == mWidth-1 {
|
||||
wrappedLines = append(wrappedLines, ident+currentLine+chunk.Render(buffer))
|
||||
currentLine = ""
|
||||
buffer = ""
|
||||
pos = width(subident) // Start counting from ident.
|
||||
ident = subident // After first line, add ident to all.
|
||||
} else {
|
||||
pos++
|
||||
}
|
||||
}
|
||||
currentLine += chunk.Render(buffer)
|
||||
}
|
||||
if width(currentLine) > 0 {
|
||||
wrappedLines = append(wrappedLines, subident+currentLine)
|
||||
}
|
||||
return wrappedLines
|
||||
}
|
||||
|
||||
func (w withStyle) Render(s string) string {
|
||||
return w.style(s)
|
||||
}
|
||||
|
||||
func (m *model) printOpenBracket(line string, s *rangeGroup, path string, selectableValues bool) string {
|
||||
if selectableValues && m.cursorPath() == path {
|
||||
return m.theme.Cursor(line)
|
||||
}
|
||||
if s != nil && s.openBracket != nil {
|
||||
if s.openBracket.parent.index == m.searchResultsCursor {
|
||||
return m.theme.Cursor(line)
|
||||
} else {
|
||||
return m.theme.Search(line)
|
||||
}
|
||||
} else {
|
||||
return m.theme.Syntax(line)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) printCloseBracket(line string, s *rangeGroup, path string, selectableValues bool) string {
|
||||
if selectableValues && m.cursorPath() == path {
|
||||
return m.theme.Cursor(line)
|
||||
}
|
||||
if s != nil && s.closeBracket != nil {
|
||||
if s.closeBracket.parent.index == m.searchResultsCursor {
|
||||
return m.theme.Cursor(line)
|
||||
} else {
|
||||
return m.theme.Search(line)
|
||||
}
|
||||
} else {
|
||||
return m.theme.Syntax(line)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) printComma(line string, s *rangeGroup) string {
|
||||
if s != nil && s.comma != nil {
|
||||
if s.comma.parent.index == m.searchResultsCursor {
|
||||
return m.theme.Cursor(line)
|
||||
} else {
|
||||
return m.theme.Search(line)
|
||||
}
|
||||
} else {
|
||||
return m.theme.Syntax(line)
|
||||
}
|
||||
}
|
||||
|
||||
type withStyle struct {
|
||||
value string
|
||||
style theme.Color
|
||||
}
|
||||
|
||||
func (m *model) explode(line string, highlightRanges []*foundRange, defaultStyle theme.Color, path string, selectable bool) []withStyle {
|
||||
if selectable && m.cursorPath() == path && m.showCursor {
|
||||
return []withStyle{{line, m.theme.Cursor}}
|
||||
}
|
||||
|
||||
out := make([]withStyle, 0, 1)
|
||||
pos := 0
|
||||
for _, r := range highlightRanges {
|
||||
style := m.theme.Search
|
||||
if r.parent.index == m.searchResultsCursor {
|
||||
style = m.theme.Cursor
|
||||
}
|
||||
out = append(out, withStyle{
|
||||
value: line[pos:r.start],
|
||||
style: defaultStyle,
|
||||
})
|
||||
out = append(out, withStyle{
|
||||
value: line[r.start:r.end],
|
||||
style: style,
|
||||
})
|
||||
pos = r.end
|
||||
}
|
||||
out = append(out, withStyle{
|
||||
value: line[pos:],
|
||||
style: defaultStyle,
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
func merge(chunks []withStyle) string {
|
||||
out := ""
|
||||
for _, chunk := range chunks {
|
||||
out += chunk.Render(chunk.value)
|
||||
}
|
||||
return out
|
||||
}
|
@ -1,249 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
)
|
||||
|
||||
type searchResult struct {
|
||||
path string
|
||||
index int
|
||||
ranges []*foundRange
|
||||
}
|
||||
|
||||
type rangeKind int
|
||||
|
||||
const (
|
||||
keyRange rangeKind = 1 + iota
|
||||
valueRange
|
||||
delimRange
|
||||
openBracketRange
|
||||
closeBracketRange
|
||||
commaRange
|
||||
)
|
||||
|
||||
type foundRange struct {
|
||||
parent *searchResult
|
||||
// Range needs separate path, as for one searchResult's path
|
||||
// there can be multiple ranges for different paths (within parent path).
|
||||
path string
|
||||
start, end int
|
||||
kind rangeKind
|
||||
}
|
||||
|
||||
type rangeGroup struct {
|
||||
key []*foundRange
|
||||
value []*foundRange
|
||||
delim []*foundRange
|
||||
openBracket *foundRange
|
||||
closeBracket *foundRange
|
||||
comma *foundRange
|
||||
}
|
||||
|
||||
func (m *model) clearSearchResults() {
|
||||
m.searchRegexCompileError = ""
|
||||
m.searchResults = nil
|
||||
m.highlightIndex = nil
|
||||
}
|
||||
|
||||
func (m *model) doSearch(s string) {
|
||||
m.clearSearchResults()
|
||||
re, err := regexp.Compile("(?i)" + s)
|
||||
if err != nil {
|
||||
m.searchRegexCompileError = err.Error()
|
||||
m.searchInput.Blur()
|
||||
return
|
||||
}
|
||||
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
|
||||
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
|
||||
m.indexSearchResults()
|
||||
m.searchInput.Blur()
|
||||
m.showSearchResults = true
|
||||
m.jumpToSearchResult(0)
|
||||
}
|
||||
|
||||
func (m *model) remapSearchResult(object interface{}, path string, pos int, indexes [][]int, id int, current *searchResult) (int, int, *searchResult) {
|
||||
switch object.(type) {
|
||||
case nil:
|
||||
s := "null"
|
||||
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
|
||||
return pos + len(s), id, current
|
||||
|
||||
case bool:
|
||||
var s string
|
||||
if object.(bool) {
|
||||
s = "true"
|
||||
} else {
|
||||
s = "false"
|
||||
}
|
||||
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
|
||||
return pos + len(s), id, current
|
||||
|
||||
case Number:
|
||||
s := object.(Number).String()
|
||||
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
|
||||
return pos + len(s), id, current
|
||||
|
||||
case string:
|
||||
// TODO: Wrap the string, save a line number according to current wrap or no wrap mode.
|
||||
s := fmt.Sprintf("%q", object)
|
||||
id, current = m.findRanges(valueRange, s, path, pos, indexes, id, current)
|
||||
return pos + len(s), id, current
|
||||
|
||||
case *Dict:
|
||||
id, current = m.findRanges(openBracketRange, "{", path, pos, indexes, id, current)
|
||||
pos++ // {
|
||||
for i, k := range object.(*Dict).Keys {
|
||||
subpath := path + "." + k
|
||||
|
||||
key := fmt.Sprintf("%q", k)
|
||||
id, current = m.findRanges(keyRange, key, subpath, pos, indexes, id, current)
|
||||
pos += len(key)
|
||||
|
||||
delim := ": "
|
||||
id, current = m.findRanges(delimRange, delim, subpath, pos, indexes, id, current)
|
||||
pos += len(delim)
|
||||
|
||||
pos, id, current = m.remapSearchResult(object.(*Dict).Values[k], subpath, pos, indexes, id, current)
|
||||
if i < len(object.(*Dict).Keys)-1 {
|
||||
comma := ","
|
||||
id, current = m.findRanges(commaRange, comma, subpath, pos, indexes, id, current)
|
||||
pos += len(comma)
|
||||
}
|
||||
}
|
||||
id, current = m.findRanges(closeBracketRange, "}", path, pos, indexes, id, current)
|
||||
pos++ // }
|
||||
return pos, id, current
|
||||
|
||||
case Array:
|
||||
id, current = m.findRanges(openBracketRange, "[", path, pos, indexes, id, current)
|
||||
pos++ // [
|
||||
for i, v := range object.(Array) {
|
||||
subpath := fmt.Sprintf("%v[%v]", path, i)
|
||||
pos, id, current = m.remapSearchResult(v, subpath, pos, indexes, id, current)
|
||||
if i < len(object.(Array))-1 {
|
||||
comma := ","
|
||||
id, current = m.findRanges(commaRange, comma, subpath, pos, indexes, id, current)
|
||||
pos += len(comma)
|
||||
}
|
||||
}
|
||||
id, current = m.findRanges(closeBracketRange, "]", path, pos, indexes, id, current)
|
||||
pos++ // ]
|
||||
return pos, id, current
|
||||
|
||||
default:
|
||||
panic("unexpected object type")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) findRanges(kind rangeKind, s string, path string, pos int, indexes [][]int, id int, current *searchResult) (int, *searchResult) {
|
||||
for ; id < len(indexes); id++ {
|
||||
start, end := indexes[id][0]-pos, indexes[id][1]-pos
|
||||
if end <= 0 {
|
||||
current = nil
|
||||
continue
|
||||
}
|
||||
if start < len(s) {
|
||||
if current == nil {
|
||||
current = &searchResult{
|
||||
path: path,
|
||||
index: len(m.searchResults),
|
||||
}
|
||||
m.searchResults = append(m.searchResults, current)
|
||||
}
|
||||
found := &foundRange{
|
||||
parent: current,
|
||||
path: path,
|
||||
start: max(start, 0),
|
||||
end: min(end, len(s)),
|
||||
kind: kind,
|
||||
}
|
||||
current.ranges = append(current.ranges, found)
|
||||
if end < len(s) {
|
||||
current = nil
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return id, current
|
||||
}
|
||||
|
||||
func (m *model) indexSearchResults() {
|
||||
m.highlightIndex = map[string]*rangeGroup{}
|
||||
for _, s := range m.searchResults {
|
||||
for _, r := range s.ranges {
|
||||
highlight, exist := m.highlightIndex[r.path]
|
||||
if !exist {
|
||||
highlight = &rangeGroup{}
|
||||
m.highlightIndex[r.path] = highlight
|
||||
}
|
||||
switch r.kind {
|
||||
case keyRange:
|
||||
highlight.key = append(highlight.key, r)
|
||||
case valueRange:
|
||||
highlight.value = append(highlight.value, r)
|
||||
case delimRange:
|
||||
highlight.delim = append(highlight.delim, r)
|
||||
case openBracketRange:
|
||||
highlight.openBracket = r
|
||||
case closeBracketRange:
|
||||
highlight.closeBracket = r
|
||||
case commaRange:
|
||||
highlight.comma = r
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) jumpToSearchResult(at int) {
|
||||
if len(m.searchResults) == 0 {
|
||||
return
|
||||
}
|
||||
m.showCursor = false
|
||||
m.searchResultsCursor = at % len(m.searchResults)
|
||||
desiredPath := m.searchResults[m.searchResultsCursor].path
|
||||
_, ok := m.pathToLineNumber[desiredPath]
|
||||
if ok {
|
||||
m.cursor = m.pathToIndex[desiredPath]
|
||||
m.scrollDownToCursor()
|
||||
m.render()
|
||||
} else {
|
||||
m.expandToPath(desiredPath)
|
||||
m.render()
|
||||
m.jumpToSearchResult(at)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) expandToPath(path string) {
|
||||
m.expandedPaths[path] = true
|
||||
if path != "" {
|
||||
m.expandToPath(m.parents[path])
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) nextSearchResult() {
|
||||
if len(m.searchResults) > 0 {
|
||||
m.jumpToSearchResult((m.searchResultsCursor + 1) % len(m.searchResults))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) prevSearchResult() {
|
||||
i := m.searchResultsCursor - 1
|
||||
if i < 0 {
|
||||
i = len(m.searchResults) - 1
|
||||
}
|
||||
m.jumpToSearchResult(i)
|
||||
}
|
||||
|
||||
func (m *model) resultsCursorPath() string {
|
||||
if len(m.searchResults) == 0 {
|
||||
return "?"
|
||||
}
|
||||
return m.searchResults[m.searchResultsCursor].path
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
. "github.com/antonmedv/fx/pkg/dict"
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
"github.com/stretchr/testify/require"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_search_values(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
object interface{}
|
||||
want *foundRange
|
||||
}{
|
||||
{name: "null", object: nil},
|
||||
{name: "true", object: true},
|
||||
{name: "false", object: false},
|
||||
{name: "Number", object: Number("42")},
|
||||
{name: "string", object: "Hello, World!"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := &model{
|
||||
json: tt.object,
|
||||
}
|
||||
re, _ := regexp.Compile(".+")
|
||||
str := Stringify(m.json)
|
||||
indexes := re.FindAllStringIndex(str, -1)
|
||||
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
|
||||
|
||||
s := &searchResult{path: ""}
|
||||
s.ranges = append(s.ranges, &foundRange{
|
||||
parent: s,
|
||||
path: "",
|
||||
start: 0,
|
||||
end: len(str),
|
||||
kind: valueRange,
|
||||
})
|
||||
require.Equal(t, []*searchResult{s}, m.searchResults)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_search_array(t *testing.T) {
|
||||
msg := `
|
||||
["first","second"]
|
||||
^^^^^ ^^^^^^
|
||||
`
|
||||
m := &model{
|
||||
json: Array{"first", "second"},
|
||||
}
|
||||
re, _ := regexp.Compile("\\w+")
|
||||
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
|
||||
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
|
||||
|
||||
s1 := &searchResult{path: "[0]"}
|
||||
s1.ranges = append(s1.ranges,
|
||||
&foundRange{
|
||||
parent: s1,
|
||||
path: "[0]",
|
||||
start: 1,
|
||||
end: 6,
|
||||
kind: valueRange,
|
||||
},
|
||||
)
|
||||
s2 := &searchResult{path: "[1]", index: 1}
|
||||
s2.ranges = append(s2.ranges,
|
||||
&foundRange{
|
||||
parent: s2,
|
||||
path: "[1]",
|
||||
start: 1,
|
||||
end: 7,
|
||||
kind: valueRange,
|
||||
},
|
||||
)
|
||||
require.Equal(t, []*searchResult{s1, s2}, m.searchResults, msg)
|
||||
}
|
||||
|
||||
func Test_search_between_array(t *testing.T) {
|
||||
msg := `
|
||||
["first","second"]
|
||||
^^^^^^^^^^^^^^
|
||||
`
|
||||
m := &model{
|
||||
json: Array{"first", "second"},
|
||||
}
|
||||
re, _ := regexp.Compile("\\w.+\\w")
|
||||
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
|
||||
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
|
||||
|
||||
s := &searchResult{path: "[0]"}
|
||||
s.ranges = append(s.ranges,
|
||||
&foundRange{
|
||||
parent: s,
|
||||
path: "[0]",
|
||||
start: 1,
|
||||
end: 7,
|
||||
kind: valueRange,
|
||||
},
|
||||
&foundRange{
|
||||
parent: s,
|
||||
path: "[0]",
|
||||
start: 0,
|
||||
end: 1,
|
||||
kind: commaRange,
|
||||
},
|
||||
&foundRange{
|
||||
parent: s,
|
||||
path: "[1]",
|
||||
start: 0,
|
||||
end: 7,
|
||||
kind: valueRange,
|
||||
},
|
||||
)
|
||||
require.Equal(t, []*searchResult{s}, m.searchResults, msg)
|
||||
}
|
||||
|
||||
func Test_search_dict(t *testing.T) {
|
||||
msg := `
|
||||
{"key": "hello world"}
|
||||
^^^^^ ^^^^^^^^^^^^^
|
||||
`
|
||||
d := NewDict()
|
||||
d.Set("key", "hello world")
|
||||
m := &model{
|
||||
json: d,
|
||||
}
|
||||
re, _ := regexp.Compile("\"[\\w\\s]+\"")
|
||||
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
|
||||
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
|
||||
|
||||
s1 := &searchResult{path: ".key"}
|
||||
s1.ranges = append(s1.ranges,
|
||||
&foundRange{
|
||||
parent: s1,
|
||||
path: ".key",
|
||||
start: 0,
|
||||
end: 5,
|
||||
kind: keyRange,
|
||||
},
|
||||
)
|
||||
s2 := &searchResult{path: ".key", index: 1}
|
||||
s2.ranges = append(s2.ranges,
|
||||
&foundRange{
|
||||
parent: s2,
|
||||
path: ".key",
|
||||
start: 0,
|
||||
end: 13,
|
||||
kind: valueRange,
|
||||
},
|
||||
)
|
||||
require.Equal(t, []*searchResult{s1, s2}, m.searchResults, msg)
|
||||
}
|
||||
|
||||
func Test_search_dict_with_array(t *testing.T) {
|
||||
msg := `
|
||||
{"first": [1,2],"second": []}
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
`
|
||||
d := NewDict()
|
||||
d.Set("first", Array{Number("1"), Number("2")})
|
||||
d.Set("second", Array{})
|
||||
m := &model{
|
||||
json: d,
|
||||
}
|
||||
re, _ := regexp.Compile(".+")
|
||||
indexes := re.FindAllStringIndex(Stringify(m.json), -1)
|
||||
m.remapSearchResult(m.json, "", 0, indexes, 0, nil)
|
||||
|
||||
s := &searchResult{path: ""}
|
||||
s.ranges = append(s.ranges,
|
||||
/* { */ &foundRange{parent: s, path: "", start: 0, end: 1, kind: openBracketRange},
|
||||
/* "first" */ &foundRange{parent: s, path: ".first", start: 0, end: 7, kind: keyRange},
|
||||
/* : */ &foundRange{parent: s, path: ".first", start: 0, end: 2, kind: delimRange},
|
||||
/* [ */ &foundRange{parent: s, path: ".first", start: 0, end: 1, kind: openBracketRange},
|
||||
/* 1 */ &foundRange{parent: s, path: ".first[0]", start: 0, end: 1, kind: valueRange},
|
||||
/* , */ &foundRange{parent: s, path: ".first[0]", start: 0, end: 1, kind: commaRange},
|
||||
/* 2 */ &foundRange{parent: s, path: ".first[1]", start: 0, end: 1, kind: valueRange},
|
||||
/* ] */ &foundRange{parent: s, path: ".first", start: 0, end: 1, kind: closeBracketRange},
|
||||
/* , */ &foundRange{parent: s, path: ".first", start: 0, end: 1, kind: commaRange},
|
||||
/* "second" */ &foundRange{parent: s, path: ".second", start: 0, end: 8, kind: keyRange},
|
||||
/* : */ &foundRange{parent: s, path: ".second", start: 0, end: 2, kind: delimRange},
|
||||
/* [ */ &foundRange{parent: s, path: ".second", start: 0, end: 1, kind: openBracketRange},
|
||||
/* ] */ &foundRange{parent: s, path: ".second", start: 0, end: 1, kind: closeBracketRange},
|
||||
/* } */ &foundRange{parent: s, path: "", start: 0, end: 1, kind: closeBracketRange},
|
||||
)
|
||||
require.Equal(t, []*searchResult{s}, m.searchResults, msg)
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
. "github.com/antonmedv/fx/pkg/json"
|
||||
. "github.com/antonmedv/fx/pkg/reducer"
|
||||
. "github.com/antonmedv/fx/pkg/theme"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func stream(dec *json.Decoder, object interface{}, lang string, args []string, theme Theme, fxrc string) int {
|
||||
var vm *goja.Runtime
|
||||
var fn goja.Callable
|
||||
var err error
|
||||
if lang == "js" {
|
||||
vm, fn, err = CreateJS(args, fxrc)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
for {
|
||||
if object != nil {
|
||||
if lang == "js" {
|
||||
ReduceJS(vm, fn, object, theme)
|
||||
} else {
|
||||
Reduce(object, lang, args, theme, fxrc)
|
||||
}
|
||||
}
|
||||
object, err = Parse(dec)
|
||||
if err == io.EOF {
|
||||
return 0
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println("JSON Parse Error:", err.Error())
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func clamp(v, low, high int) int {
|
||||
if high < low {
|
||||
low, high = high, low
|
||||
}
|
||||
return min(high, max(low, v))
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func width(s string) int {
|
||||
return lipgloss.Width(s)
|
||||
}
|
||||
|
||||
func accessor(path string, to interface{}) string {
|
||||
return fmt.Sprintf("%v[%v]", path, to)
|
||||
}
|
||||
|
||||
func toLowerNumber(s string) string {
|
||||
var out strings.Builder
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case '0' <= r && r <= '9':
|
||||
out.WriteRune('\u2080' + (r - '\u0030'))
|
||||
default:
|
||||
out.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_clamp(t *testing.T) {
|
||||
got := clamp(1, 2, 3)
|
||||
if got != 2 {
|
||||
t.Errorf("clamp() = %v, want 2", got)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_max(t *testing.T) {
|
||||
got := max(1, 2)
|
||||
if got != 2 {
|
||||
t.Errorf("max() = %v, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_min(t *testing.T) {
|
||||
got := min(1, 2)
|
||||
if got != 1 {
|
||||
t.Errorf("min() = %v, want 1", got)
|
||||
}
|
||||
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
package main
|
||||
|
||||
const version = "24.1.0"
|
@ -1,128 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
func (m *model) AtTop() bool {
|
||||
return m.offset <= 0
|
||||
}
|
||||
|
||||
func (m *model) AtBottom() bool {
|
||||
return m.offset >= m.maxYOffset()
|
||||
}
|
||||
|
||||
func (m *model) PastBottom() bool {
|
||||
return m.offset > m.maxYOffset()
|
||||
}
|
||||
|
||||
func (m *model) ScrollPercent() float64 {
|
||||
if m.height >= len(m.lines) {
|
||||
return 1.0
|
||||
}
|
||||
y := float64(m.offset)
|
||||
h := float64(m.height)
|
||||
t := float64(len(m.lines) - 1)
|
||||
v := y / (t - h)
|
||||
return math.Max(0.0, math.Min(1.0, v))
|
||||
}
|
||||
|
||||
func (m *model) maxYOffset() int {
|
||||
return max(0, len(m.lines)-m.height)
|
||||
}
|
||||
|
||||
func (m *model) visibleLines() (lines []string) {
|
||||
if len(m.lines) > 0 {
|
||||
top := max(0, m.offset)
|
||||
bottom := clamp(m.offset+m.height, top, len(m.lines))
|
||||
lines = m.lines[top:bottom]
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (m *model) SetOffset(n int) {
|
||||
m.offset = clamp(n, 0, m.maxYOffset())
|
||||
}
|
||||
|
||||
func (m *model) ViewDown() {
|
||||
if m.AtBottom() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(m.offset + m.height)
|
||||
}
|
||||
|
||||
func (m *model) ViewUp() {
|
||||
if m.AtTop() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(m.offset - m.height)
|
||||
}
|
||||
|
||||
func (m *model) HalfViewDown() {
|
||||
if m.AtBottom() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(m.offset + m.height/2)
|
||||
}
|
||||
|
||||
func (m *model) HalfViewUp() {
|
||||
if m.AtTop() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(m.offset - m.height/2)
|
||||
}
|
||||
|
||||
func (m *model) LineDown(n int) {
|
||||
if m.AtBottom() || n == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the Number of lines by which we're going to scroll isn't
|
||||
// greater than the Number of lines we actually have left before we reach
|
||||
// the bottom.
|
||||
m.SetOffset(m.offset + n)
|
||||
}
|
||||
|
||||
func (m *model) LineUp(n int) {
|
||||
if m.AtTop() || n == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the Number of lines by which we're going to scroll isn't
|
||||
// greater than the Number of lines we are from the top.
|
||||
m.SetOffset(m.offset - n)
|
||||
}
|
||||
|
||||
func (m *model) GotoTop() {
|
||||
if m.AtTop() {
|
||||
return
|
||||
}
|
||||
|
||||
m.SetOffset(0)
|
||||
}
|
||||
|
||||
func (m *model) GotoBottom() {
|
||||
m.SetOffset(m.maxYOffset())
|
||||
}
|
||||
|
||||
func (m *model) scrollDownToCursor() {
|
||||
at := m.cursorLineNumber()
|
||||
if m.offset <= at { // cursor is lower
|
||||
m.LineDown(max(0, at-(m.offset+m.height-1))) // minus one is due to cursorLineNumber() starts from 0
|
||||
} else {
|
||||
m.SetOffset(at)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) scrollUpToCursor() {
|
||||
at := m.cursorLineNumber()
|
||||
if at < m.offset+m.height { // cursor is above
|
||||
m.LineUp(max(0, m.offset-at))
|
||||
} else {
|
||||
m.SetOffset(at)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue