From 35a720af9bede3bb9454bdb320af77c3699d78cc Mon Sep 17 00:00:00 2001 From: Alexandre Bourget Date: Tue, 6 Aug 2024 09:59:00 -0400 Subject: [PATCH] Rework of the `substreams gui` --- cmd/substreams/gui.go | 2 + go.mod | 32 +-- go.sum | 58 +++--- tui2/common/common.go | 3 + tui2/common/component.go | 2 + tui2/common/help.go | 33 +++ tui2/common/modals.go | 34 ++- tui2/components/blocksearch/blocksearch.go | 89 ++++---- tui2/components/dataentry/dataentry.go | 75 +++++++ .../navigator.go => modgraph/modgraph.go} | 2 +- .../modgraph_test.go} | 2 +- tui2/components/modsearch/modsearch.go | 197 ++++++------------ tui2/components/search/search.go | 57 ++--- tui2/footer/footer.go | 3 + tui2/keymap/keymap.go | 6 + tui2/pages/output/output.go | 61 +++--- tui2/pages/progress/keys.go | 3 - tui2/pages/request/request.go | 117 ++++++----- tui2/pages/request/validation.go | 24 +++ tui2/stream/stream.go | 3 +- tui2/styles/overlay.go | 186 +++++++++++++++++ tui2/styles/styles.go | 4 +- tui2/tabs/tabs.go | 7 +- tui2/ui.go | 75 ++++--- 24 files changed, 703 insertions(+), 372 deletions(-) create mode 100644 tui2/common/help.go create mode 100644 tui2/components/dataentry/dataentry.go rename tui2/components/{explorer/navigator.go => modgraph/modgraph.go} (99%) rename tui2/components/{explorer/navigator_test.go => modgraph/modgraph_test.go} (99%) create mode 100644 tui2/pages/request/validation.go create mode 100644 tui2/styles/overlay.go diff --git a/cmd/substreams/gui.go b/cmd/substreams/gui.go index 703f7071f..30cb04c91 100644 --- a/cmd/substreams/gui.go +++ b/cmd/substreams/gui.go @@ -153,6 +153,7 @@ func runGui(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("stop block: %w", err) } + rawStopBlock, _ := cmd.Flags().GetString("stop-block") if readFromModule { // need to tweak the stop block here sb, err := graph.ModuleInitialBlock(outputModule) @@ -182,6 +183,7 @@ func runGui(cmd *cobra.Command, args []string) error { Cursor: cursor, StartBlock: startBlock, StopBlock: stopBlock, + RawStopBlock: rawStopBlock, FinalBlocksOnly: sflags.MustGetBool(cmd, "final-blocks-only"), Params: params, ReaderOptions: readerOptions, diff --git a/go.mod b/go.mod index 69e215199..43a374f0e 100644 --- a/go.mod +++ b/go.mod @@ -35,11 +35,11 @@ require ( github.com/bmatcuk/doublestar/v4 v4.6.1 github.com/bytecodealliance/wasmtime-go/v4 v4.0.0 github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.26.3 + github.com/charmbracelet/bubbletea v0.26.6 github.com/charmbracelet/glamour v0.7.0 - github.com/charmbracelet/huh v0.4.2 - github.com/charmbracelet/huh/spinner v0.0.0-20240506212404-0a3504046bcb - github.com/charmbracelet/lipgloss v0.11.0 + github.com/charmbracelet/huh v0.5.2 + github.com/charmbracelet/huh/spinner v0.0.0-20240806005253-b7436a76999a + github.com/charmbracelet/lipgloss v0.12.1 github.com/docker/cli v24.0.6+incompatible github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.13.0 @@ -70,7 +70,7 @@ require ( go.opentelemetry.io/otel v1.24.0 go.opentelemetry.io/otel/trace v1.24.0 go.uber.org/atomic v1.10.0 - golang.org/x/mod v0.12.0 + golang.org/x/mod v0.17.0 golang.org/x/net v0.23.0 golang.org/x/oauth2 v0.18.0 google.golang.org/grpc v1.64.0 @@ -83,11 +83,11 @@ require ( github.com/alecthomas/chroma/v2 v2.8.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bits-and-blooms/bitset v1.12.0 // indirect - github.com/bobg/go-generics/v2 v2.1.1 // indirect + github.com/bobg/go-generics/v2 v2.2.2 // indirect github.com/catppuccin/go v0.2.0 // indirect - github.com/charmbracelet/x/ansi v0.1.1 // indirect - github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a // indirect - github.com/charmbracelet/x/input v0.1.1 // indirect + github.com/charmbracelet/x/ansi v0.1.4 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/input v0.1.3 // indirect github.com/charmbracelet/x/term v0.1.1 // indirect github.com/charmbracelet/x/windows v0.1.2 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect @@ -96,9 +96,11 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-sqlite3 v1.14.16 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect go.uber.org/goleak v1.3.0 // indirect @@ -157,12 +159,12 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-ieproxy v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 github.com/muesli/cancelreader v0.2.2 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/openzipkin/zipkin-go v0.4.2 // indirect @@ -193,11 +195,11 @@ require ( go.opentelemetry.io/otel/sdk v1.23.1 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/crypto v0.23.0 // indirect - golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/term v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect + golang.org/x/text v0.16.0 // indirect google.golang.org/api v0.172.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect diff --git a/go.sum b/go.sum index ea4fbd2e0..e1bd531fe 100644 --- a/go.sum +++ b/go.sum @@ -153,6 +153,8 @@ github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwN github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bobg/go-generics/v2 v2.1.1 h1:4rN9upY6Xm4TASSMeH+NzUghgO4h/SbNrQphIjRd/R0= github.com/bobg/go-generics/v2 v2.1.1/go.mod h1:iPMSRVFlzkJSYOCXQ0n92RA3Vxw0RBv2E8j9ZODXgHk= +github.com/bobg/go-generics/v2 v2.2.2 h1:cHTV51Vr/wSlwiNWvncz66E4QtoRw9qXZeEiLAmwqW8= +github.com/bobg/go-generics/v2 v2.2.2/go.mod h1:ieOJ1ARFvk+HfMKbW1DT5UzJ/CJPKoiRm17QKK82bRE= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= @@ -166,24 +168,24 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.26.3 h1:iXyGvI+FfOWqkB2V07m1DF3xxQijxjY2j8PqiXYqasg= -github.com/charmbracelet/bubbletea v0.26.3/go.mod h1:bpZHfDHTYJC5g+FBK+ptJRCQotRC+Dhh3AoMxa/2+3Q= +github.com/charmbracelet/bubbletea v0.26.6 h1:zTCWSuST+3yZYZnVSvbXwKOPRSNZceVeqpzOLN2zq1s= +github.com/charmbracelet/bubbletea v0.26.6/go.mod h1:dz8CWPlfCCGLFbBlTY4N7bjLiyOGDJEnd2Muu7pOWhk= github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= -github.com/charmbracelet/huh v0.4.2 h1:5wLkwrA58XDAfEZsJzNQlfJ+K8N9+wYwvR5FOM7jXFM= -github.com/charmbracelet/huh v0.4.2/go.mod h1:g9OXBgtY3zRV4ahnVih9bZE+1yGYN+y2C9Q6L2P+WM0= -github.com/charmbracelet/huh/spinner v0.0.0-20240506212404-0a3504046bcb h1:stie/OtFrYi9zvPOaHeShSm25MJ118hPUWUDJTxmtb4= -github.com/charmbracelet/huh/spinner v0.0.0-20240506212404-0a3504046bcb/go.mod h1:Zxt9FH6togK9kY71pRJGtmyNkJ1eIWdK1gRaXrS/FKA= -github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= -github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= -github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= -github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a h1:lOpqe2UvPmlln41DGoii7wlSZ/q8qGIon5JJ8Biu46I= -github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/huh v0.5.2 h1:ofeNkJ4iaFnzv46Njhx896DzLUe/j0L2QAf8znwzX4c= +github.com/charmbracelet/huh v0.5.2/go.mod h1:Sf7dY0oAn6N/e3sXJFtFX9hdQLrUdO3z7AYollG9bAM= +github.com/charmbracelet/huh/spinner v0.0.0-20240806005253-b7436a76999a h1:SnIdR+7ApTLK5Dc3DKQf6GGbb3TsnNAKtp183DhZyp8= +github.com/charmbracelet/huh/spinner v0.0.0-20240806005253-b7436a76999a/go.mod h1:9VssyY5pUozMRmDYlLYV20QMMcA2sHg3qnaB6PvdIm8= +github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= +github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= +github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= +github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE= github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a/go.mod h1:YBotIGhfoWhHDlnUpJMkjebGV2pdGRCn1Y4/Nk/vVcU= -github.com/charmbracelet/x/input v0.1.1 h1:YDOJaTUKCqtGnq9PHzx3pkkl4pXDOANUHmhH3DqMtM4= -github.com/charmbracelet/x/input v0.1.1/go.mod h1:jvdTVUnNWj/RD6hjC4FsoB0SeZCJ2ZBkiuFP9zXvZI0= +github.com/charmbracelet/x/input v0.1.3 h1:oy4TMhyGQsYs/WWJwu1ELUMFnjiUAXwtDf048fHbCkg= +github.com/charmbracelet/x/input v0.1.3/go.mod h1:1gaCOyw1KI9e2j00j/BBZ4ErzRZqa05w0Ghn83yIhKU= github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= @@ -408,6 +410,8 @@ 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 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= @@ -435,16 +439,20 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= @@ -502,6 +510,8 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz github.com/rs/cors v1.10.0 h1:62NOS1h+r8p1mW6FM0FSB0exioXLhd/sh15KpjWBZ+8= github.com/rs/cors v1.10.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= +github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk= github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= github.com/sethvargo/go-retry v0.2.3 h1:oYlgvIvsju3jNbottWABtbnoLC+GDtLdBHxKWxQm/iU= @@ -681,8 +691,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -709,8 +719,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -853,8 +863,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -871,8 +881,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/tui2/common/common.go b/tui2/common/common.go index 2b313bda8..66b3d6527 100644 --- a/tui2/common/common.go +++ b/tui2/common/common.go @@ -11,3 +11,6 @@ func (c *Common) SetSize(width, height int) { c.Width = width c.Height = height } + +func (c *Common) GetWidth() int { return c.Width } +func (c *Common) GetHeight() int { return c.Height } diff --git a/tui2/common/component.go b/tui2/common/component.go index ed8b9bf05..213dfcaad 100644 --- a/tui2/common/component.go +++ b/tui2/common/component.go @@ -10,4 +10,6 @@ type Component interface { tea.Model help.KeyMap SetSize(width, height int) + GetWidth() int + GetHeight() int } diff --git a/tui2/common/help.go b/tui2/common/help.go new file mode 100644 index 000000000..5c07bf36a --- /dev/null +++ b/tui2/common/help.go @@ -0,0 +1,33 @@ +package common + +import "github.com/charmbracelet/bubbles/key" + +func ShortToFullHelp(comp Component) [][]key.Binding { + shortHelp := comp.ShortHelp() + fullHelp := make([][]key.Binding, len(shortHelp)) + for i, binding := range shortHelp { + fullHelp[i] = []key.Binding{binding} + } + return fullHelp +} + +type SimpleHelp struct { + Bindings []key.Binding +} + +func NewSimpleHelp(bindings ...key.Binding) SimpleHelp { + return SimpleHelp{Bindings: bindings} +} + +func (h SimpleHelp) ShortHelp() []key.Binding { + return h.Bindings +} + +func (h SimpleHelp) FullHelp() [][]key.Binding { + shortHelp := h.ShortHelp() + fullHelp := make([][]key.Binding, len(shortHelp)) + for i, binding := range shortHelp { + fullHelp[i] = []key.Binding{binding} + } + return fullHelp +} diff --git a/tui2/common/modals.go b/tui2/common/modals.go index 620f0d71e..c64a8ad9e 100644 --- a/tui2/common/modals.go +++ b/tui2/common/modals.go @@ -1,14 +1,42 @@ package common -import tea "github.com/charmbracelet/bubbletea" +import ( + tea "github.com/charmbracelet/bubbletea" +) -type ModalUpdateFunc func(msg tea.Msg) (tea.Model, tea.Cmd) +type SetModalComponentMsg Component +type CancelModalMsg struct{} // Emitted by components to close itselt + +type IsInlineModal interface { + IsInlineModal() +} + +func SetModalComponentCmd(comp Component) tea.Cmd { + return func() tea.Msg { + return SetModalComponentMsg(comp) + } +} +func CancelModalCmd() tea.Cmd { + return func() tea.Msg { + return CancelModalMsg{} + } +} -type SetModalUpdateFuncMsg ModalUpdateFunc type UpdateSeenModulesMsg []string type ModuleSelectedMsg string // Emitted to inform all components that a new module has been selected. type BlockSelectedMsg uint64 // Emitted to inform all components that a new block has been selected. +type SetRequestValue struct { + Field string + Value string +} + +func SetRequestValueCmd(field, value string) tea.Cmd { + return func() tea.Msg { + return SetRequestValue{Field: field, Value: value} + } +} + func EmitModuleSelectedMsg(moduleName string) tea.Cmd { return func() tea.Msg { return ModuleSelectedMsg(moduleName) diff --git a/tui2/components/blocksearch/blocksearch.go b/tui2/components/blocksearch/blocksearch.go index da76fac78..6df6230ab 100644 --- a/tui2/components/blocksearch/blocksearch.go +++ b/tui2/components/blocksearch/blocksearch.go @@ -6,7 +6,9 @@ import ( "github.com/streamingfast/substreams/tui2/components/blockselect" "github.com/streamingfast/substreams/tui2/components/search" + "github.com/streamingfast/substreams/tui2/keymap" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" @@ -46,44 +48,60 @@ func (s *BlockSearch) SetSize(w, h int) { s.input.Width = w } func (s *BlockSearch) Init() tea.Cmd { + s.input.Focus() + s.input.SetValue("") + s.Current = "" + s.input.Prompt = "go-to block: " + s.historyPointer = 0 return nil } +func (s *BlockSearch) ShortHelp() []key.Binding { + return []key.Binding{ + keymap.GeneralSearchEnter, + keymap.GeneralSearchBackspace, + } +} +func (s *BlockSearch) FullHelp() [][]key.Binding { return common.ShortToFullHelp(s) } + func (s *BlockSearch) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + + // TODO: change into a simple `huh` form, in a Modal dialog, take the output and dispatch the message. + var cmds []tea.Cmd msgSwitch: switch msg := msg.(type) { case tea.KeyMsg: - if s.input.Focused() { - switch msg.String() { - case "enter": - if s.input.Value() == "" { - s.input.Blur() - cmds = append(cmds, s.cancelModal(), s.clearSearch) - } else { - newQuery := s.input.Value() - s.Current = newQuery - s.History = append(s.History, newQuery) - uintQuery, err := s.CheckValidQuery() - if err != nil { - break - } - cmds = append(cmds, s.cancelModal()) - cmds = append(cmds, func() tea.Msg { return blockselect.BlockChangedMsg(uintQuery) }) - cmds = append(cmds, s.clearSearch) - s.input.Blur() - break msgSwitch - } - case "backspace": - if s.input.Value() == "" { - s.input.Blur() - cmds = append(cmds, s.cancelModal(), s.clearSearch) + if !s.input.Focused() { + break + } + switch msg.String() { + case "enter": + if s.input.Value() == "" { + s.input.Blur() + cmds = append(cmds, common.CancelModalCmd()) + } else { + newQuery := s.input.Value() + s.Current = newQuery + s.History = append(s.History, newQuery) + uintQuery, err := s.CheckValidQuery() + if err != nil { + break } + s.input.Blur() + cmds = append(cmds, func() tea.Msg { return blockselect.BlockChangedMsg(uintQuery) }) + cmds = append(cmds, common.CancelModalCmd()) + break msgSwitch + } + case "backspace": + if s.input.Value() == "" { + s.input.Blur() + cmds = append(cmds, common.CancelModalCmd(), s.clearSearch) } - var cmd tea.Cmd - s.input, cmd = s.input.Update(msg) - cmds = append(cmds, cmd) } + var cmd tea.Cmd + s.input, cmd = s.input.Update(msg) + cmds = append(cmds, cmd) } return s, tea.Batch(cmds...) @@ -97,23 +115,6 @@ func (s *BlockSearch) clearSearch() tea.Msg { return search.SearchClearedMsg(true) } -func (s *BlockSearch) InitInput() tea.Cmd { - s.input.Focus() - s.input.SetValue("") - s.Current = "" - s.input.Prompt = "go-to block: " - s.historyPointer = 0 - return func() tea.Msg { - return common.SetModalUpdateFuncMsg(s.Update) - } -} - -func (s *BlockSearch) cancelModal() tea.Cmd { - return func() tea.Msg { - return common.SetModalUpdateFuncMsg(nil) - } -} - func (s *BlockSearch) SetMatchCount(count int) { s.timesFound = count } diff --git a/tui2/components/dataentry/dataentry.go b/tui2/components/dataentry/dataentry.go new file mode 100644 index 000000000..4d7cbd1d9 --- /dev/null +++ b/tui2/components/dataentry/dataentry.go @@ -0,0 +1,75 @@ +package dataentry + +import ( + "log" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" + "github.com/streamingfast/substreams/tui2/common" + "github.com/streamingfast/substreams/tui2/keymap" +) + +type DataEntry struct { + common.Common + common.SimpleHelp + Input *huh.Input + Form *huh.Form +} + +func New(c common.Common, field string, validation func(input string) error) *DataEntry { + input := huh.NewInput(). + Validate(validation). + Key(field) + input.WithAccessible(true) + + form := huh.NewForm(huh.NewGroup(input).WithShowErrors(true)).WithTheme(huh.ThemeCharm()) + + return &DataEntry{ + Common: c, + SimpleHelp: common.NewSimpleHelp(keymap.EscapeModal, keymap.EnterAcceptValue), + Input: input, + Form: form, + } +} + +func (m *DataEntry) Init() tea.Cmd { + return m.Form.Init() +} + +func (m *DataEntry) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + //var cmds []tea.Cmd + var cmd tea.Cmd + model, cmd := m.Form.Update(msg) + m.Form = model.(*huh.Form) + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "esc": + log.Println("Escape this thing") + return m, common.CancelModalCmd() + case "enter": + log.Println("Enter in data entry", m.Input.GetKey(), m.Input.GetValue(), m.Input.Error(), m.Form.Errors()) + if m.Input.Error() == nil { + val := m.Input.GetValue().(string) + return m, tea.Batch(common.SetRequestValueCmd(m.Input.GetKey(), val), common.CancelModalCmd()) + } + } + } + log.Println("keys in dataentry", m.Input.GetValue()) + return m, cmd +} + +func (m *DataEntry) View() string { + return m.Form.View() +} + +func (m *DataEntry) SetSize(w, h int) { + m.Common.SetSize(w, h) + m.Form.WithWidth(w).WithHeight(6) +} + +func (m *DataEntry) GetHeight() int { + return lipgloss.Height(m.View()) +} diff --git a/tui2/components/explorer/navigator.go b/tui2/components/modgraph/modgraph.go similarity index 99% rename from tui2/components/explorer/navigator.go rename to tui2/components/modgraph/modgraph.go index 88cf8e744..d9ef50de0 100644 --- a/tui2/components/explorer/navigator.go +++ b/tui2/components/modgraph/modgraph.go @@ -1,4 +1,4 @@ -package explorer +package modgraph import ( "fmt" diff --git a/tui2/components/explorer/navigator_test.go b/tui2/components/modgraph/modgraph_test.go similarity index 99% rename from tui2/components/explorer/navigator_test.go rename to tui2/components/modgraph/modgraph_test.go index b864735c9..fa38a4988 100644 --- a/tui2/components/explorer/navigator_test.go +++ b/tui2/components/modgraph/modgraph_test.go @@ -1,4 +1,4 @@ -package explorer +package modgraph import ( "testing" diff --git a/tui2/components/modsearch/modsearch.go b/tui2/components/modsearch/modsearch.go index 819494153..90131430e 100644 --- a/tui2/components/modsearch/modsearch.go +++ b/tui2/components/modsearch/modsearch.go @@ -1,42 +1,50 @@ package modsearch import ( - "log" - - "github.com/charmbracelet/bubbles/textinput" - "github.com/charmbracelet/bubbles/viewport" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" "github.com/streamingfast/substreams/tui2/common" + "github.com/streamingfast/substreams/tui2/keymap" ) -type DisableModuleSearch bool -type ApplyModuleSearchQueryMsg string -type UpdateModuleSearchQueryMsg string - type ModuleSearch struct { - common.Common - input textinput.Model - - matchesView viewport.Model - - seenModules []string - matchingModules []string - highlightedIndex int + list.Model } func New(c common.Common) *ModuleSearch { - input := textinput.New() - input.Placeholder = "" - input.Prompt = "/" - input.CharLimit = 256 - input.Width = 80 - return &ModuleSearch{ - Common: c, - input: input, - matchesView: viewport.New(24, 79), + delegate := list.NewDefaultDelegate() + delegate.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd { + it, ok := m.SelectedItem().(item) + if !ok { + return nil + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "enter": + return tea.Sequence(common.EmitModuleSelectedMsg(it.Title()), common.CancelModalCmd()) + } + } + return nil + } + delegate.ShowDescription = false + delegate.SetSpacing(0) + lst := list.New(nil, delegate, c.Width, c.Height) + lst.SetStatusBarItemName("module", "modules") + lst.Title = "Select module (/ to filter)" + //lst.SetFilteringEnabled(true) + lst.DisableQuitKeybindings() + lst.AdditionalShortHelpKeys = func() []key.Binding { + return []key.Binding{keymap.EscapeModal} } + mod := &ModuleSearch{ + Model: lst, + } + mod.SetSize(c.Width, c.Height) + return mod } func (m *ModuleSearch) Init() tea.Cmd { @@ -44,129 +52,54 @@ func (m *ModuleSearch) Init() tea.Cmd { } func (m *ModuleSearch) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd -msgSwitch: switch msg := msg.(type) { - case common.UpdateSeenModulesMsg: - log.Println("Updated seen modules!", msg) - m.seenModules = msg case tea.KeyMsg: - if m.input.Focused() { - switch msg.String() { - case "enter": - m.input.Blur() - cmds = append(cmds, m.cancelModuleModal(), m.emitDisableMsg, common.EmitModuleSelectedMsg(m.selectedModule())) - break msgSwitch - case "backspace": - if m.input.Value() == "" { - cmds = append(cmds, m.cancelModuleModal(), m.emitDisableMsg) - } - case "up": - if m.highlightedIndex != 0 { - m.highlightedIndex-- - } - case "down": - if m.highlightedIndex != len(m.matchingModules)-1 { - m.highlightedIndex++ - } - default: - m.highlightedIndex = 0 - } - var cmd tea.Cmd - m.input, cmd = m.input.Update(msg) - cmds = append(cmds, cmd) - m.updateViewport() + switch msg.String() { + case "esc": + return m, common.CancelModalCmd() } } - return m, tea.Batch(cmds...) + var cmd tea.Cmd + m.Model, cmd = m.Model.Update(msg) + return m, cmd } -func (m *ModuleSearch) selectedModule() string { - if m.highlightedIndex >= len(m.matchingModules) { - return "" - } - return m.matchingModules[m.highlightedIndex] +func (m *ModuleSearch) View() string { + return m.Model.View() } -func (m *ModuleSearch) updateViewport() { - m.matchesView.SetContent(m.renderMatches(m.input.Value())) +func (m *ModuleSearch) SetSize(w, h int) { + m.Model.SetSize( + min(w-12, 60), + min(h-6, 20)-3, + ) } +func (m *ModuleSearch) GetWidth() int { return m.Model.Width() } +func (m *ModuleSearch) GetHeight() int { return m.Model.Height() } -func (m *ModuleSearch) InitInput() tea.Cmd { - m.input.Focus() - m.input.SetValue("") - m.updateViewport() - m.highlightedIndex = 0 - return func() tea.Msg { - return common.SetModalUpdateFuncMsg(m.Update) - } +type item struct { + modName string } -func (m *ModuleSearch) renderMatches(query string) string { - var matchingMods []string - for _, mod := range m.seenModules { - if containsPortions(mod, query) { - matchingMods = append(matchingMods, mod) - } - } - if len(matchingMods) == 0 { - m.highlightedIndex = 0 - return "" - } - - if m.highlightedIndex >= len(matchingMods) { - m.highlightedIndex = len(matchingMods) - 1 - } - - m.matchingModules = matchingMods +func (i item) FilterValue() string { return i.modName } +func (i item) Title() string { return i.modName } +func (i item) Description() string { return "none" } - var rows []string - maxHeight := m.matchesView.Height - 2 - for idx, modName := range matchingMods { - if idx >= maxHeight { - break - } - if idx == m.highlightedIndex { - rows = append(rows, lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("12")).Render(modName)) - } else { - rows = append(rows, modName) - } +func (m *ModuleSearch) SetListItems(mods []string) { + var matchingMods []list.Item + for _, mod := range mods { + matchingMods = append(matchingMods, item{modName: mod}) } - - out := lipgloss.JoinVertical(0, rows...) - return out + m.Model.SetItems(matchingMods) + m.Model.SettingFilter() } -func containsPortions(modName, query string) bool { - queryIndex := 0 - for _, r := range modName { - if queryIndex < len(query) && r == rune(query[queryIndex]) { - queryIndex++ +func (m *ModuleSearch) SetSelected(moduleName string) { + for idx, it := range m.Model.Items() { + if it.(item).modName == moduleName { + m.Model.Select(idx) + return } } - return queryIndex == len(query) -} - -func (m *ModuleSearch) View() string { - return lipgloss.JoinVertical(0, - m.input.View(), - lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true).Width(m.Width).Render(m.matchesView.View()), - ) -} - -func (m *ModuleSearch) SetSize(w, h int) { - m.Common.SetSize(w, h) - m.matchesView.Width = w - m.matchesView.Height = h - 3 -} - -func (m *ModuleSearch) cancelModuleModal() tea.Cmd { - return func() tea.Msg { - return common.SetModalUpdateFuncMsg(nil) - } -} - -func (m *ModuleSearch) emitDisableMsg() tea.Msg { - return DisableModuleSearch(true) } diff --git a/tui2/components/search/search.go b/tui2/components/search/search.go index 6d7b31cf9..d284286b2 100644 --- a/tui2/components/search/search.go +++ b/tui2/components/search/search.go @@ -3,10 +3,12 @@ package search import ( "fmt" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/streamingfast/substreams/tui2/common" + "github.com/streamingfast/substreams/tui2/keymap" ) type UpdateMatchingBlocks map[uint64]bool @@ -49,9 +51,28 @@ func (s *Search) SetSize(w, h int) { s.input.Width = w } func (s *Search) Init() tea.Cmd { + s.input.Focus() + s.input.SetValue("") + s.jqMode = false + s.Current = SearchQuery{} + s.applyPrompt() + s.historyPointer = 0 return nil } +func (s *Search) ShortHelp() []key.Binding { + out := []key.Binding{ + keymap.GeneralSearchEnter, + keymap.GeneralSearchBackspace, + keymap.UpDown, + } + if s.input.Value() == "" { + out = append(out, keymap.SearchSwitchJQ) + } + return out +} +func (s *Search) FullHelp() [][]key.Binding { return common.ShortToFullHelp(s) } + func (s *Search) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd msgSwitch: @@ -92,7 +113,7 @@ msgSwitch: JQMode: s.jqMode, } - cmds = append(cmds, s.CancelModal()) + cmds = append(cmds, common.CancelModalCmd()) if newQuery.Query != "" { s.Current = newQuery @@ -106,7 +127,7 @@ msgSwitch: case "backspace": if s.input.Value() == "" { s.input.Blur() - cmds = append(cmds, s.CancelModal(), s.clearSearch) + cmds = append(cmds, common.CancelModalCmd(), s.clearSearch) } } @@ -119,13 +140,7 @@ msgSwitch: return s, tea.Batch(cmds...) } -func (s *Search) applyPrompt() { - if s.jqMode { - s.input.Prompt = "jq: " - } else { - s.input.Prompt = "/" - } -} +func (s *Search) IsInlineModal() {} func (s *Search) View() string { if !s.input.Focused() { @@ -135,26 +150,16 @@ func (s *Search) View() string { } } -func (s *Search) clearSearch() tea.Msg { - return SearchClearedMsg(true) -} - -func (s *Search) InitInput() tea.Cmd { - s.input.Focus() - s.input.SetValue("") - s.jqMode = false - s.Current = SearchQuery{} - s.applyPrompt() - s.historyPointer = 0 - return func() tea.Msg { - return common.SetModalUpdateFuncMsg(s.Update) +func (s *Search) applyPrompt() { + if s.jqMode { + s.input.Prompt = "jq: " + } else { + s.input.Prompt = "/" } } -func (s *Search) CancelModal() tea.Cmd { - return func() tea.Msg { - return common.SetModalUpdateFuncMsg(nil) - } +func (s *Search) clearSearch() tea.Msg { + return SearchClearedMsg(true) } func (s *Search) SetMatchCount(count int) { diff --git a/tui2/footer/footer.go b/tui2/footer/footer.go index cb791d433..e9f8d1ec2 100644 --- a/tui2/footer/footer.go +++ b/tui2/footer/footer.go @@ -1,6 +1,8 @@ package footer import ( + "log" + "github.com/charmbracelet/bubbles/help" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -53,6 +55,7 @@ func (f *Footer) Init() tea.Cmd { func (f *Footer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case UpdateKeyMapMsg: + log.Println("Updating keymap in footer") f.keymap = msg } return f, nil diff --git a/tui2/keymap/keymap.go b/tui2/keymap/keymap.go index f4cc68baa..2db7403c6 100644 --- a/tui2/keymap/keymap.go +++ b/tui2/keymap/keymap.go @@ -3,6 +3,12 @@ package keymap import "github.com/charmbracelet/bubbles/key" var k = key.WithKeys("") +var EscapeModal = key.NewBinding(key.WithHelp("esc", "Dismiss dialog"), k) +var SearchSwitchJQ = key.NewBinding(key.WithHelp("/", "Switch to jq search mode (on empty line)"), k) +var GeneralSearchEnter = key.NewBinding(key.WithHelp("enter", "Search"), k) +var EnterAcceptValue = key.NewBinding(key.WithHelp("enter", "Accept value"), k) +var ModSearchEnter = key.NewBinding(key.WithHelp("enter", "Use module"), k) +var GeneralSearchBackspace = key.NewBinding(key.WithHelp("backspace", "Cancel search with empty box"), k) var TabShiftTab = key.NewBinding(key.WithHelp("tab/shift-tab", "Main nav"), k) var PrevNextModule = key.NewBinding(key.WithHelp("u/i", "Modules nav"), k) var PrevNextBlock = key.NewBinding(key.WithHelp("o/p", "Blocks nav"), k) diff --git a/tui2/pages/output/output.go b/tui2/pages/output/output.go index 40f1435c6..afb94604e 100644 --- a/tui2/pages/output/output.go +++ b/tui2/pages/output/output.go @@ -3,18 +3,17 @@ package output import ( "sort" - "github.com/streamingfast/substreams/tui2/components/explorer" - "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/jhump/protoreflect/dynamic" - "github.com/streamingfast/substreams/tui2/components/blocksearch" "github.com/streamingfast/substreams/manifest" pbsubstreamsrpc "github.com/streamingfast/substreams/pb/sf/substreams/rpc/v2" "github.com/streamingfast/substreams/tui2/common" + "github.com/streamingfast/substreams/tui2/components/blocksearch" "github.com/streamingfast/substreams/tui2/components/blockselect" + "github.com/streamingfast/substreams/tui2/components/modgraph" "github.com/streamingfast/substreams/tui2/components/modsearch" "github.com/streamingfast/substreams/tui2/components/modselect" "github.com/streamingfast/substreams/tui2/components/search" @@ -23,6 +22,7 @@ import ( type Output struct { common.Common + *request.Config msgDescs map[string]*manifest.ModuleDescriptor messageFactory *dynamic.MessageFactory @@ -46,9 +46,8 @@ type Output struct { active request.BlockContext // module + block outputViewYoffset map[request.BlockContext]int - moduleSearchEnabled bool - moduleSearchView *modsearch.ModuleSearch - //moduleSearchView + moduleSearchView *modsearch.ModuleSearch + outputModule string logsEnabled bool @@ -64,17 +63,18 @@ type Output struct { blockSearchCtx *blocksearch.BlockSearch moduleNavigatorMode bool - moduleNavigator *explorer.Navigator + moduleNavigator *modgraph.Navigator } func New(c common.Common, config *request.Config) (*Output, error) { - nav, err := explorer.New(config.OutputModule, c, explorer.WithModuleGraph(config.Graph)) + nav, err := modgraph.New(config.OutputModule, c, modgraph.WithModuleGraph(config.Graph)) if err != nil { return nil, err } output := &Output{ Common: c, + Config: config, blocksPerModule: make(map[string][]uint64), payloads: make(map[request.BlockContext]*pbsubstreamsrpc.AnyModuleOutput), blockIDs: make(map[uint64]string), @@ -111,7 +111,7 @@ func (o *Output) SetSize(w, h int) { o.outputView.Height = h - 11 o.moduleNavigator.FrameHeight = h - 11 - o.moduleSearchView.SetSize(w, o.outputView.Height) + o.moduleSearchView.SetSize(w, h) outputViewTopBorder := 1 o.outputView.Height = h - o.moduleSelector.Height - o.blockSelector.Height - outputViewTopBorder o.searchCtx.SetSize(w, h) @@ -126,14 +126,15 @@ func (o *Output) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case search.SearchClearedMsg: o.searchEnabled = false - o.blockSearchEnabled = false o.setOutputViewContent(true) - case modsearch.DisableModuleSearch: - o.moduleSearchEnabled = false + case common.UpdateSeenModulesMsg: + o.moduleSearchView.SetListItems(msg) case search.UpdateMatchingBlocks: o.searchBlockNumsWithMatches = o.orderMatchingBlocks(msg) + o.blockSelector.Update(msg) case search.AddMatchingBlock: o.searchBlockNumsWithMatches = append(o.searchBlockNumsWithMatches, uint64(msg)) + o.blockSelector.Update(msg) case request.NewRequestInstance: o.errReceived = nil o.msgDescs = msg.MsgDescs @@ -141,6 +142,7 @@ func (o *Output) Update(msg tea.Msg) (tea.Model, tea.Cmd) { o.payloads = make(map[request.BlockContext]*pbsubstreamsrpc.AnyModuleOutput) o.blockIDs = make(map[uint64]string) o.outputView.SetContent("") + o.blockSelector.Update(msg) case *pbsubstreamsrpc.BlockScopedData: blockNum := msg.Clock.Number @@ -202,6 +204,9 @@ func (o *Output) Update(msg tea.Msg) (tea.Model, tea.Cmd) { o.outputView.YOffset = o.outputViewYoffset[o.active] o.setOutputViewContent(true) cmds = append(cmds, o.updateMatchingBlocks()) + _, _ = o.moduleNavigator.Update(msg) + o.moduleSearchView.SetSelected(string(msg)) + case blockselect.BlockChangedMsg: if o.hasDataForBlock(uint64(msg)) { newBlock := uint64(msg) @@ -221,17 +226,16 @@ func (o *Output) Update(msg tea.Msg) (tea.Model, tea.Cmd) { o.setOutputViewContent(true) case "=": o.blockSearchEnabled = !o.blockSearchEnabled - cmds = append(cmds, o.blockSearchCtx.InitInput()) + cmds = append(cmds, common.SetModalComponentCmd(o.blockSearchCtx)) case "L": o.logsEnabled = !o.logsEnabled o.setOutputViewContent(true) case "m": - o.moduleSearchEnabled = true o.setOutputViewContent(true) - return o, o.moduleSearchView.InitInput() + cmds = append(cmds, common.SetModalComponentCmd(o.moduleSearchView)) case "/": o.searchEnabled = true - cmds = append(cmds, o.searchCtx.InitInput()) + cmds = append(cmds, common.SetModalComponentCmd(o.searchCtx)) case "F": o.bytesRepresentation = (o.bytesRepresentation + 1) % 3 o.setOutputViewContent(true) @@ -264,19 +268,11 @@ func (o *Output) Update(msg tea.Msg) (tea.Model, tea.Cmd) { o.setOutputViewContent(false) } - _, cmd := o.moduleSearchView.Update(msg) - cmds = append(cmds, cmd) - - _, cmd = o.moduleSelector.Update(msg) - cmds = append(cmds, cmd) - - _, cmd = o.blockSelector.Update(msg) - cmds = append(cmds, cmd) - + var cmd tea.Cmd o.outputView, cmd = o.outputView.Update(msg) cmds = append(cmds, cmd) - _, cmd = o.moduleNavigator.Update(msg) + _, cmd = o.moduleSelector.Update(msg) cmds = append(cmds, cmd) return o, tea.Batch(cmds...) @@ -336,20 +332,13 @@ func (o *Output) View() string { var searchLine string if o.searchEnabled { + // This to be managed by the Modal Dialog box searchLine = o.searchCtx.View() } - if o.blockSearchEnabled { - searchLine = o.blockSearchCtx.View() - } o.setOutputViewContent(false) - var middleBlock string - if o.moduleSearchEnabled { - middleBlock = o.moduleSearchView.View() - } else { - middleBlock = o.outputView.View() - } + middleSection := o.outputView.View() if o.moduleNavigatorMode { return lipgloss.JoinVertical(0, @@ -365,7 +354,7 @@ func (o *Output) View() string { o.moduleSelector.View(), o.blockSelector.View(), "", - middleBlock, + middleSection, searchLine, ) return out diff --git a/tui2/pages/progress/keys.go b/tui2/pages/progress/keys.go index 815762d65..9a85617b8 100644 --- a/tui2/pages/progress/keys.go +++ b/tui2/pages/progress/keys.go @@ -34,9 +34,6 @@ func (p *Progress) FullHelp() [][]key.Binding { }, { keymap.Help, - keymap.TabShiftTab, - }, - { keymap.Quit, }, } diff --git a/tui2/pages/request/request.go b/tui2/pages/request/request.go index eb26f0b3e..b5dc10d52 100644 --- a/tui2/pages/request/request.go +++ b/tui2/pages/request/request.go @@ -2,13 +2,15 @@ package request import ( "fmt" + "log" "path/filepath" - "regexp" "strings" "github.com/streamingfast/substreams/client" "github.com/streamingfast/substreams/manifest" pbsubstreamsrpc "github.com/streamingfast/substreams/pb/sf/substreams/rpc/v2" + "github.com/streamingfast/substreams/tui2/components/dataentry" + "github.com/streamingfast/substreams/tui2/components/modsearch" "github.com/streamingfast/substreams/tui2/replaylog" streamui "github.com/streamingfast/substreams/tui2/stream" "github.com/streamingfast/substreams/tui2/styles" @@ -16,7 +18,6 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" - "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/table" @@ -41,6 +42,7 @@ type Config struct { DebugModulesInitialSnapshot []string StartBlock int64 StopBlock uint64 + RawStopBlock string FinalBlocksOnly bool Headers map[string]string OutputModule string @@ -63,8 +65,8 @@ type Instance struct { type Request struct { common.Common + *Config - form *huh.Form formStartBlock string formStopBlock string formEndpoint string @@ -80,59 +82,23 @@ type Request struct { params map[string][]string } -func New(c common.Common) *Request { +func New(c common.Common, conf *Config) *Request { return &Request{ Common: c, + Config: conf, manifestView: viewport.New(24, 80), } } func (r *Request) Init() tea.Cmd { - r.form = huh.NewForm( - huh.NewGroup( - huh.NewSelect[string](). - Key("module"). - Value(&r.formModuleSelected). - Inline(true). - Options(huh.NewOptions("map_things_events_calls", "graph_out")...). - Title("Select module to stream:"), - huh.NewInput(). - Key("start_block"). - Value(&r.formStartBlock). - Inline(true). - Validate(func(s string) error { - if !regexp.MustCompile(`^\d+$`).MatchString(s) { - return fmt.Errorf("specify only numbers") - } - return nil - }). - Title("Enter the start block number:"), - huh.NewInput(). - Key("stop_block"). - Inline(true). - Value(&r.formStopBlock). - Validate(func(s string) error { - if !regexp.MustCompile(`^[\+\-]?\d+$`).MatchString(s) { - return fmt.Errorf("specify only numbers, optionally prefixed by - or +") - } - return nil - }). - Title("Stream to block:"). - Description("You can specify relative block numbers with - (to head) or + (to start block) prefixes."), - ), - ) return tea.Batch( r.manifestView.Init(), - r.form.Init(), ) } func (r *Request) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd - _, cmd := r.form.Update(msg) - cmds = append(cmds, cmd) - switch msg := msg.(type) { case NewRequestInstance: r.RequestSummary = msg.RequestSummary @@ -145,6 +111,19 @@ func (r *Request) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } r.setModulesViewContent() + + case common.SetRequestValue: + log.Println("Received value", msg.Field, msg.Value) + switch msg.Field { + case "start-block": + r.formStartBlock = msg.Value + case "stop-block": + r.formStopBlock = msg.Value + } + case common.ModuleSelectedMsg: + log.Println("Setting selected module:", msg) + r.Config.OutputModule = string(msg) + case tea.KeyMsg: // COuld we support: // `s` to change `start block` @@ -166,9 +145,30 @@ func (r *Request) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // `a` for advanced options (shows Final Blocks Only switcher) // `c` to change cursor - var cmd tea.Cmd - r.manifestView, cmd = r.manifestView.Update(msg) - cmds = append(cmds, cmd) + switch msg.String() { + case "s": + comp := dataentry.New(r.Common, "start-block", validateNumbersOnly) + comp.Input.Prompt("Enter the start block number: "). + Description("Block from which to start streaming. Numbers only\n\n") + cmds = append(cmds, common.SetModalComponentCmd(comp)) + case "t": + comp := dataentry.New(r.Common, "stop-block", validateNumberOrRelativeValue) + comp.Input.Prompt("Enter the stop block number: "). + Description("Enter numbers only, with an optional - or + prefix.\n\nYou can specify relative block numbers with - (to head) or + (to start block) prefixes.\n") + cmds = append(cmds, common.SetModalComponentCmd(comp)) + case "m": + comp := modsearch.New(r.Common) + comp.SetListItems(r.Config.Graph.Modules()) + cmds = append(cmds, common.SetModalComponentCmd(comp)) + case "e": + case "a": + + default: + var cmd tea.Cmd + r.manifestView, cmd = r.manifestView.Update(msg) + cmds = append(cmds, cmd) + } + case *pbsubstreamsrpc.SessionInit: r.traceId = msg.TraceId r.resolvedStartBlock = msg.ResolvedStartBlock @@ -182,14 +182,12 @@ func (r *Request) SetSize(w, h int) { r.Common.SetSize(w, h) summaryHeight := lipgloss.Height(r.renderRequestSummary()) - formHeight := lipgloss.Height(r.renderForm()) - r.manifestView.Height = max(h-summaryHeight-formHeight-2 /* for borders */, 0) + r.manifestView.Height = max(h-summaryHeight-2 /* for borders */, 0) r.manifestView.Width = w } func (r *Request) View() string { out := lipgloss.JoinVertical(0, - r.renderForm(), r.renderRequestSummary(), r.renderManifestView(), ) @@ -197,10 +195,6 @@ func (r *Request) View() string { return out } -func (r *Request) renderForm() string { - return lipgloss.NewStyle().MaxHeight(6).Render(r.form.View()) -} - func (r *Request) renderManifestView() string { return lipgloss.NewStyle(). Border(lipgloss.NormalBorder(), true). @@ -226,10 +220,15 @@ func (r *Request) renderRequestSummary() string { paramsStrings = append(paramsStrings, fmt.Sprintf("%s=%s", k, v)) } + startBlock := fmt.Sprintf("%d%s", r.resolvedStartBlock, handoffStr) + startBlock = r.formStartBlock + rows := [][]string{ {"Package:", summary.Manifest}, - {"Endpoint:", summary.Endpoint}, - {"Start Block:", fmt.Sprintf("%d%s", r.resolvedStartBlock, handoffStr)}, + {"[m] Module:", r.Config.OutputModule}, + {"[e] Endpoint:", summary.Endpoint}, + {"[s] Start Block:", startBlock}, + {"[t] Stop Block:", r.Config.RawStopBlock}, {"Parameters:", fmt.Sprintf("%v", summary.Params)}, {"Production mode:", fmt.Sprintf("%v", summary.ProductionMode)}, {"Trace ID:", r.traceId}, @@ -277,7 +276,17 @@ func (r *Request) getViewportContent() (string, error) { output += fmt.Sprintf("%s\n\n", module.Name) output += fmt.Sprintf(" Initial block: %v\n", module.InitialBlock) output += fmt.Sprintln(" Inputs: ") - for i := range module.Inputs { + for i, input := range module.Inputs { + _ = input + // switch input.(type) { + + // } + // switch module.Inputs[i].(type) { + // case *pbsubstreams.ModuleInputBlock: + // case *pbsubstreams.ModuleInputCursor: + // case *pbsubstreams.ModuleInputParams: + // case *pbsubstreams.ModuleInputParams: + // } if module.Inputs[i].GetParams() != nil && r.params[module.Name] != nil { output += fmt.Sprintf(" - params: [%s]\n", strings.Join(r.params[module.Name], ", ")) } else { diff --git a/tui2/pages/request/validation.go b/tui2/pages/request/validation.go new file mode 100644 index 000000000..cf2069724 --- /dev/null +++ b/tui2/pages/request/validation.go @@ -0,0 +1,24 @@ +package request + +import ( + "fmt" + "regexp" +) + +func validateNumbersOnly(in string) error { + for _, r := range in { + if r < '0' || r > '9' { + return fmt.Errorf("only numbers are allowed") + } + } + return nil +} + +var relativeNumbers = regexp.MustCompile(`^[\+\-]?\d+$`) + +func validateNumberOrRelativeValue(in string) error { + if !relativeNumbers.MatchString(in) { + return fmt.Errorf("only numbers allowed, optionally prefixed by - or +") + } + return nil +} diff --git a/tui2/stream/stream.go b/tui2/stream/stream.go index 77609e2f7..783f70e1d 100644 --- a/tui2/stream/stream.go +++ b/tui2/stream/stream.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "log" tea "github.com/charmbracelet/bubbletea" "google.golang.org/grpc" @@ -170,7 +169,7 @@ func (s *Stream) routeNextMessage(resp *pbsubstreamsrpc.Response) tea.Msg { case *pbsubstreamsrpc.Response_BlockScopedData: return m.BlockScopedData case *pbsubstreamsrpc.Response_Progress: - log.Printf("Progress response: %T %v", resp, resp) + //log.Printf("Progress response: %T %v", resp, resp) return m.Progress case *pbsubstreamsrpc.Response_DebugSnapshotData: return m.DebugSnapshotData diff --git a/tui2/styles/overlay.go b/tui2/styles/overlay.go new file mode 100644 index 000000000..a726d61a4 --- /dev/null +++ b/tui2/styles/overlay.go @@ -0,0 +1,186 @@ +package styles + +// From: https://github.com/charmbracelet/lipgloss/pull/102/files +// pulling in a few deps from lipgloss, until this is addressed: +// https://github.com/charmbracelet/lipgloss/pull/102#issuecomment-2188983884 + +import ( + "bytes" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/mattn/go-runewidth" + "github.com/muesli/ansi" + "github.com/muesli/reflow/truncate" + "github.com/muesli/termenv" +) + +func PlaceOverlay( + x, y int, + fg, bg string, + shadow bool, opts ...WhitespaceOption, +) string { + fgLines, fgWidth := getLines(fg) + bgLines, bgWidth := getLines(bg) + bgHeight := len(bgLines) + fgHeight := len(fgLines) + + if shadow { + var shadowbg string = "" + shadowchar := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#333333")). + Render("░") + for i := 0; i <= fgHeight; i++ { + if i == 0 { + shadowbg += " " + strings.Repeat(" ", fgWidth) + "\n" + } else { + shadowbg += " " + strings.Repeat(shadowchar, fgWidth) + "\n" + } + } + + fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...) + fgLines, fgWidth = getLines(fg) + fgHeight = len(fgLines) + } + + if fgWidth >= bgWidth && fgHeight >= bgHeight { + // FIXME: return fg or bg? + return fg + } + // TODO: allow placement outside of the bg box? + x = clamp(x, 0, bgWidth-fgWidth) + y = clamp(y, 0, bgHeight-fgHeight) + + ws := &whitespace{} + for _, opt := range opts { + opt(ws) + } + + var b strings.Builder + for i, bgLine := range bgLines { + if i > 0 { + b.WriteByte('\n') + } + if i < y || i >= y+fgHeight { + b.WriteString(bgLine) + continue + } + + pos := 0 + if x > 0 { + left := truncate.String(bgLine, uint(x)) + pos = ansi.PrintableRuneWidth(left) + b.WriteString(left) + if pos < x { + b.WriteString(ws.render(x - pos)) + pos = x + } + } + + fgLine := fgLines[i-y] + b.WriteString(fgLine) + pos += ansi.PrintableRuneWidth(fgLine) + + right := cutLeft(bgLine, pos) + bgWidth := ansi.PrintableRuneWidth(bgLine) + rightWidth := ansi.PrintableRuneWidth(right) + if rightWidth <= bgWidth-pos { + b.WriteString(ws.render(bgWidth - rightWidth - pos)) + } + + b.WriteString(right) + } + + return b.String() +} + +// cutLeft cuts printable characters from the left. +// This function is heavily based on muesli's ansi and truncate packages. +func cutLeft(s string, cutWidth int) string { + var ( + pos int + isAnsi bool + ab bytes.Buffer + b bytes.Buffer + ) + for _, c := range s { + var w int + if c == ansi.Marker || isAnsi { + isAnsi = true + ab.WriteRune(c) + if ansi.IsTerminator(c) { + isAnsi = false + if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) { + ab.Reset() + } + } + } else { + w = runewidth.RuneWidth(c) + } + + if pos >= cutWidth { + if b.Len() == 0 { + if ab.Len() > 0 { + b.Write(ab.Bytes()) + } + if pos-cutWidth > 1 { + b.WriteByte(' ') + continue + } + } + b.WriteRune(c) + } + pos += w + } + return b.String() +} + +func getLines(s string) (lines []string, widest int) { + lines = strings.Split(s, "\n") + + for _, l := range lines { + w := ansi.PrintableRuneWidth(l) + if widest < w { + widest = w + } + } + + return lines, widest +} + +func clamp(v, lower, upper int) int { + return min(max(v, lower), upper) +} + +type whitespace struct { + style termenv.Style + chars string +} + +func (w whitespace) render(width int) string { + if w.chars == "" { + w.chars = " " + } + + r := []rune(w.chars) + j := 0 + b := strings.Builder{} + + for i := 0; i < width; { + b.WriteRune(r[j]) + j++ + if j >= len(r) { + j = 0 + } + i += ansi.PrintableRuneWidth(string(r[j])) + } + + short := width - ansi.PrintableRuneWidth(b.String()) + if short > 0 { + b.WriteString(strings.Repeat(" ", short)) + } + + return w.style.Styled(b.String()) +} + +type WhitespaceOption func(*whitespace) diff --git a/tui2/styles/styles.go b/tui2/styles/styles.go index c5406b84e..62f10cfa4 100644 --- a/tui2/styles/styles.go +++ b/tui2/styles/styles.go @@ -184,7 +184,7 @@ var ( } Logo = lipgloss.NewStyle(). - Border(lipgloss.Border{Bottom: "─", Left: "│", BottomLeft: "┴", BottomRight: "┘", TopLeft: "╷"}). + Border(lipgloss.Border{Bottom: "─", Left: "", BottomLeft: "─", BottomRight: "─", TopLeft: ""}). Padding(0, 1). Margin(0). Foreground(lipgloss.AdaptiveColor{Dark: "1", Light: "9"}).Bold(true) @@ -207,6 +207,8 @@ var ( RequestEvenRow = RequestCell.Foreground(lightGray) RequestRight = RequestCell.Align(lipgloss.Right) + ModalBox = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.AdaptiveColor{Dark: "205", Light: "213"}) + BlockSelect = blockSelectStyle{ Box: lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true), SelectedBlock: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Dark: "12", Light: "14"}).Bold(true), diff --git a/tui2/tabs/tabs.go b/tui2/tabs/tabs.go index 85cf33502..796fe0820 100644 --- a/tui2/tabs/tabs.go +++ b/tui2/tabs/tabs.go @@ -55,10 +55,10 @@ func (t *Tabs) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { - case "tab": + case "tab", "right": t.activeTab = (t.activeTab + 1) % len(t.tabs) cmds = append(cmds, t.activeTabCmd) - case "shift+tab": + case "shift+tab", "left": t.activeTab = (t.activeTab - 1 + len(t.tabs)) % len(t.tabs) cmds = append(cmds, t.activeTabCmd) } @@ -71,7 +71,6 @@ func (t *Tabs) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return t, tea.Batch(cmds...) } -// View implements tea.Model. func (t *Tabs) View() string { var tabs []string @@ -88,7 +87,7 @@ func (t *Tabs) View() string { logoTab := t.LogoStyle.Render("Substreams GUI") fill := max(t.Width-lipgloss.Width(tabsView)-lipgloss.Width(logoTab)+1, 0) - return lipgloss.JoinHorizontal(1.0, tabsView, strings.Repeat("─", fill), logoTab) + return lipgloss.JoinHorizontal(1.0, logoTab, tabsView, strings.Repeat("─", fill)) } func (t *Tabs) activeTabCmd() tea.Msg { diff --git a/tui2/ui.go b/tui2/ui.go index 25b7004f9..be024eaed 100644 --- a/tui2/ui.go +++ b/tui2/ui.go @@ -9,8 +9,6 @@ import ( "github.com/streamingfast/substreams/manifest" "github.com/streamingfast/substreams/tui2/common" - "github.com/streamingfast/substreams/tui2/components/modsearch" - "github.com/streamingfast/substreams/tui2/components/search" "github.com/streamingfast/substreams/tui2/footer" "github.com/streamingfast/substreams/tui2/pages/output" "github.com/streamingfast/substreams/tui2/pages/progress" @@ -38,11 +36,11 @@ type UI struct { requestConfig *request.Config // all boilerplate to pass down to refresh common.Common - currentModalFunc common.ModalUpdateFunc - pages []common.Component - activePage page - footer *footer.Footer - tabs *tabs.Tabs + modalComponent common.Component + pages []common.Component + activePage page + footer *footer.Footer + tabs *tabs.Tabs } func New(reqConfig *request.Config) (*UI, error) { @@ -55,7 +53,7 @@ func New(reqConfig *request.Config) (*UI, error) { ui := &UI{ Common: c, pages: []common.Component{ - request.New(c), + request.New(c, reqConfig), progress.New(c), out, }, @@ -110,21 +108,23 @@ func (ui *UI) update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.WindowSizeMsg: ui.forceRefresh() ui.SetSize(msg.Width, msg.Height) - case common.SetModalUpdateFuncMsg: - ui.currentModalFunc = common.ModalUpdateFunc(msg) - case search.ApplySearchQueryMsg: - ui.currentModalFunc = nil - case modsearch.ApplyModuleSearchQueryMsg: - ui.currentModalFunc = nil + case common.SetModalComponentMsg: + log.Printf("Setting modal component %T", msg) + if msg != nil { + cmds = append(cmds, msg.Init()) + } + ui.modalComponent = msg + ui.resize() + case common.CancelModalMsg: + ui.modalComponent = nil + ui.footer.SetKeyMap(ui.pages[ui.activePage]) case tea.KeyMsg: ui.forceRefresh() if msg.String() == "ctrl+c" { return ui, tea.Quit } - if ui.currentModalFunc != nil { - _, cmd := ui.currentModalFunc(msg) - cmds = append(cmds, cmd) - return ui, tea.Batch(cmds...) + if ui.modalComponent != nil { + break } switch msg.String() { case "q": @@ -167,13 +167,19 @@ func (ui *UI) update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, ui.stream.Update(msg)) } - _, cmd := ui.footer.Update(msg) - cmds = append(cmds, cmd) - _, cmd = ui.tabs.Update(msg) - cmds = append(cmds, cmd) - for _, pg := range ui.pages { - if _, cmd = pg.Update(msg); cmd != nil { - cmds = append(cmds, cmd) + if ui.modalComponent != nil { + m, cmd := ui.modalComponent.Update(msg) + ui.modalComponent = m.(common.Component) + cmds = append(cmds, cmd) + } else { + _, cmd := ui.footer.Update(msg) + cmds = append(cmds, cmd) + _, cmd = ui.tabs.Update(msg) + cmds = append(cmds, cmd) + for _, pg := range ui.pages { + if _, cmd = pg.Update(msg); cmd != nil { + cmds = append(cmds, cmd) + } } } @@ -188,6 +194,10 @@ func (ui *UI) resize() { footerHeight := ui.footer.Height() ui.footer.SetSize(ui.Width, footerHeight) ui.tabs.SetSize(ui.Width, 3) + if ui.modalComponent != nil { + // -2 for border at render time. + ui.modalComponent.SetSize(ui.Width-2, ui.Height-footerHeight-2) + } for _, pg := range ui.pages { pg.SetSize(ui.Width, ui.Height-ui.tabs.Height-footerHeight) } @@ -213,11 +223,24 @@ func (ui *UI) View() string { ui.tabs.LogoStyle = styles.Logo.Foreground(color) } - return lipgloss.JoinVertical(0, + main := lipgloss.JoinVertical(0, styles.Tabs.Render(ui.tabs.View()), ui.pages[ui.activePage].View(), ui.footer.View(), ) + + if ui.modalComponent != nil { + _, ok := ui.modalComponent.(common.IsInlineModal) + if !ok { + modalView := styles.ModalBox.Render(ui.modalComponent.View()) + width := lipgloss.Width(modalView) / 2 + height := lipgloss.Height(modalView) / 2 + main = styles.PlaceOverlay(width, height, modalView, main, true) + } + } + + return main + // TODO: render the modal on top, with its dimensions. } func (ui *UI) restartStream() tea.Cmd {