From 36a69c93eea3e21041f5c8782359845835ba7193 Mon Sep 17 00:00:00 2001 From: robertazzopardi Date: Sat, 12 Oct 2024 13:55:14 +0100 Subject: [PATCH 01/33] Updated makefile --- Makefile | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- go.sum | 2 ++ 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index de111c9..1d1146b 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,104 @@ -run: - go run . +# run: +# go run . +# +# tidy: +# go mod tidy +# +# lint: +# golangci-lint run +# Change these variables as necessary. +main_package_path = . +binary_name = $(awk '/^module/{print $2}' go.mod) + +# ==================================================================================== # +# HELPERS +# ==================================================================================== # + +## help: print this help message +.PHONY: help +help: + @echo 'Usage:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +.PHONY: confirm +confirm: + @echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ] + +.PHONY: no-dirty +no-dirty: + @test -z "$(shell git status --porcelain)" + + +# ==================================================================================== # +# QUALITY CONTROL +# ==================================================================================== # + +## audit: run quality control checks +.PHONY: audit +audit: test + go mod tidy -diff + go mod verify + test -z "$(shell gofmt -l .)" + go vet ./... + go run honnef.co/go/tools/cmd/staticcheck@latest -checks=all,-ST1000,-U1000 ./... + go run golang.org/x/vuln/cmd/govulncheck@latest ./... + +## test: run all tests +.PHONY: test +test: + go test -v -race -buildvcs ./... + +## test/cover: run all tests and display coverage +.PHONY: test/cover +test/cover: + go test -v -race -buildvcs -coverprofile=/tmp/coverage.out ./... + go tool cover -html=/tmp/coverage.out + + +# ==================================================================================== # +# DEVELOPMENT +# ==================================================================================== # + +## tidy: tidy modfiles and format .go files +.PHONY: tidy tidy: - go mod tidy + go mod tidy -v + go fmt ./... + +## build: build the application +.PHONY: build +build: + # Include additional build steps, like TypeScript, SCSS or Tailwind compilation here... + go build -o=/tmp/bin/${binary_name} ${main_package_path} + +## run: run the application +.PHONY: run +run: build + /tmp/bin/${binary_name} + +## run/live: run the application with reloading on file changes +.PHONY: run/live +run/live: + go run github.com/cosmtrek/air@v1.43.0 \ + --build.cmd "make build" --build.bin "/tmp/bin/${binary_name}" --build.delay "100" \ + --build.exclude_dir "" \ + --build.include_ext "go, tpl, tmpl, html, css, scss, js, ts, sql, jpeg, jpg, gif, png, bmp, svg, webp, ico" \ + --misc.clean_on_exit "true" + + +# ==================================================================================== # +# OPERATIONS +# ==================================================================================== # + +## push: push changes to the remote Git repository +.PHONY: push +push: confirm audit no-dirty + git push -lint: - golangci-lint run +## production/deploy: deploy the application to production +.PHONY: production/deploy +production/deploy: confirm audit no-dirty + GOOS=linux GOARCH=amd64 go build -ldflags='-s' -o=/tmp/bin/linux_amd64/${binary_name} ${main_package_path} + upx -5 /tmp/bin/linux_amd64/${binary_name} + # Include additional deployment steps here... diff --git a/go.sum b/go.sum index ae880ea..5ed667e 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= From 2466e93b9365620f30f57cae2b7c31e114935e53 Mon Sep 17 00:00:00 2001 From: Rob Date: Sun, 19 Jan 2025 22:59:28 +0000 Subject: [PATCH 02/33] Initial migration to tview --- Makefile | 105 +------------ docker-compose.yml => data/docker-compose.yml | 7 +- go.mod | 9 +- go.sum | 47 +++++- main.go | 139 +++++++++++++++--- 5 files changed, 179 insertions(+), 128 deletions(-) rename docker-compose.yml => data/docker-compose.yml (77%) diff --git a/Makefile b/Makefile index 1d1146b..d846faa 100644 --- a/Makefile +++ b/Makefile @@ -1,104 +1,9 @@ -# run: -# go run . -# -# tidy: -# go mod tidy -# -# lint: -# golangci-lint run +run: + go run . -# Change these variables as necessary. -main_package_path = . -binary_name = $(awk '/^module/{print $2}' go.mod) - -# ==================================================================================== # -# HELPERS -# ==================================================================================== # - -## help: print this help message -.PHONY: help -help: - @echo 'Usage:' - @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' - -.PHONY: confirm -confirm: - @echo -n 'Are you sure? [y/N] ' && read ans && [ $${ans:-N} = y ] - -.PHONY: no-dirty -no-dirty: - @test -z "$(shell git status --porcelain)" - - -# ==================================================================================== # -# QUALITY CONTROL -# ==================================================================================== # - -## audit: run quality control checks -.PHONY: audit -audit: test - go mod tidy -diff - go mod verify - test -z "$(shell gofmt -l .)" - go vet ./... - go run honnef.co/go/tools/cmd/staticcheck@latest -checks=all,-ST1000,-U1000 ./... - go run golang.org/x/vuln/cmd/govulncheck@latest ./... - -## test: run all tests -.PHONY: test -test: - go test -v -race -buildvcs ./... - -## test/cover: run all tests and display coverage -.PHONY: test/cover -test/cover: - go test -v -race -buildvcs -coverprofile=/tmp/coverage.out ./... - go tool cover -html=/tmp/coverage.out - - -# ==================================================================================== # -# DEVELOPMENT -# ==================================================================================== # - -## tidy: tidy modfiles and format .go files -.PHONY: tidy tidy: - go mod tidy -v - go fmt ./... - -## build: build the application -.PHONY: build -build: - # Include additional build steps, like TypeScript, SCSS or Tailwind compilation here... - go build -o=/tmp/bin/${binary_name} ${main_package_path} - -## run: run the application -.PHONY: run -run: build - /tmp/bin/${binary_name} - -## run/live: run the application with reloading on file changes -.PHONY: run/live -run/live: - go run github.com/cosmtrek/air@v1.43.0 \ - --build.cmd "make build" --build.bin "/tmp/bin/${binary_name}" --build.delay "100" \ - --build.exclude_dir "" \ - --build.include_ext "go, tpl, tmpl, html, css, scss, js, ts, sql, jpeg, jpg, gif, png, bmp, svg, webp, ico" \ - --misc.clean_on_exit "true" - - -# ==================================================================================== # -# OPERATIONS -# ==================================================================================== # + go mod tidy -## push: push changes to the remote Git repository -.PHONY: push -push: confirm audit no-dirty - git push +lint: + golangci-lint run -## production/deploy: deploy the application to production -.PHONY: production/deploy -production/deploy: confirm audit no-dirty - GOOS=linux GOARCH=amd64 go build -ldflags='-s' -o=/tmp/bin/linux_amd64/${binary_name} ${main_package_path} - upx -5 /tmp/bin/linux_amd64/${binary_name} - # Include additional deployment steps here... diff --git a/docker-compose.yml b/data/docker-compose.yml similarity index 77% rename from docker-compose.yml rename to data/docker-compose.yml index c4f0aef..696ec23 100644 --- a/docker-compose.yml +++ b/data/docker-compose.yml @@ -1,5 +1,8 @@ +version: "3" +name: termtable + services: - pgdb: + pg: image: postgres:latest restart: always environment: @@ -10,7 +13,7 @@ services: - "5432:5432" volumes: - termtabledata:/var/lib/postgresql/data - - ./data/postgres:/docker-entrypoint-initdb.d + - ./postgres:/docker-entrypoint-initdb.d volumes: termtabledata: diff --git a/go.mod b/go.mod index 0e35970..61030fa 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,13 @@ module github.com/robertazzopardi/termtable -go 1.22.0 +go 1.23.4 require ( github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.25.0 github.com/charmbracelet/lipgloss v0.10.0 github.com/jackc/pgx/v5 v5.5.5 + github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57 github.com/zalando/go-keyring v0.2.4 go.etcd.io/bbolt v1.3.9 ) @@ -17,6 +18,8 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/danieljoos/wincred v1.2.0 // indirect + github.com/gdamore/encoding v1.0.0 // indirect + github.com/gdamore/tcell/v2 v2.7.1 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -32,7 +35,7 @@ require ( github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect golang.org/x/crypto v0.17.0 // indirect golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 5ed667e..12aeb64 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= -github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -19,6 +17,10 @@ github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0S github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQbc= +github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -50,8 +52,11 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 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/tview v0.0.0-20241227133733-17b7edb88c57 h1:LmsF7Fk5jyEDhJk0fYIqdWNuTxSyid2W42A0L2YWjGE= +github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= @@ -63,22 +68,52 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68= github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= +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/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go index affb86b..46592a3 100644 --- a/main.go +++ b/main.go @@ -3,16 +3,30 @@ package main import ( "fmt" "io" - "log" + + // "log" "strings" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + // "github.com/gdamore/tcell/v2" ) type CurrentView string +const TERMTABLE_TEXT = ` + __ __ ___. .__ +_/ |_ ___________ ______/ |______ \_ |__ | | ____ +\ __\/ __ \_ __ \/ \ __\__ \ | __ \| | _/ __ \ + | | \ ___/| | \/ Y Y \ | / __ \| \_\ \ |_\ ___/ + |__| \___ >__| |__|_| /__| (____ /___ /____/\___ > + \/ \/ \/ \/ \/ +` + const ( DEFAULT CurrentView = "DEFAULT" NEW_CONNECTION CurrentView = "NEW_CONNECTION" @@ -194,24 +208,115 @@ func (m model) View() string { } } -func main() { - items := []list.Item{ - item("New Connection"), - item("Edit Connection"), - item("Join Existing"), - } +func hotkeys(app *tview.Application) *tview.List { + // hotkeyTable := tview.NewTable() + + // lorem := strings.Split("Lorem ipsum dolor", " ") + // // cols, rows := 5, 2 + // // word := 0 + // // for r := 0; r < rows; r++ { + // // for c := 0; c < cols; c++ { + // // color := tcell.ColorWhite + // // if c < 1 || r < 1 { + // // color = tcell.ColorYellow + // // } + // // word = (word + 1) % len(lorem) + // // } + // // } + + list := tview.NewList().ShowSecondaryText(false). + SetSelectedFocusOnly(true). + SetShortcutStyle(tcell.StyleDefault) + + list. + AddItem("List item 1", "", 0, nil). + AddItem("List item 2", "", 0, nil). + AddItem("List item 3", "", 0, nil). + AddItem("List item 4", "", 0, nil). + AddItem(" quit", "", 0, func() { + app.Stop() + }) + + list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Rune() { + case 'a': + list.SetCurrentItem(0) + app.Stop() + return nil + case 'b': + list.SetCurrentItem(1) + app.Stop() + return nil + } + return event + }) + + // hotkeyTable.SetCell(0, 0, + // tview.NewTableCell(lorem[word]). + // SetTextColor(color). + // SetAlign(tview.AlignCenter)) + + return list +} + +func currentConnectionInfo() *tview.List { + list := tview.NewList().ShowSecondaryText(false). + SetSelectedFocusOnly(true). + AddItem(" Name: ", "", 0, nil). + AddItem(" Database: ", "", 0, nil). + AddItem(" Host: ", "", 0, nil). + AddItem(" PORT: ", "", 0, nil). + AddItem(" USER: ", "Press to exit", 0, nil) - l := list.New(items, itemDelegate{}, defaultWidth, listHeight) - l.Title = "Welcome to TermTable" - l.SetShowStatusBar(false) - l.SetFilteringEnabled(false) - l.Styles.Title = titleStyle - l.Styles.PaginationStyle = paginationStyle - l.Styles.HelpStyle = helpStyle + return list +} + +func header(app *tview.Application) *tview.Flex { + connection := currentConnectionInfo() + + table := hotkeys(app) + + headerView := tview.NewFlex(). + AddItem(connection, 0, 1, false). + AddItem(table, 0, 2, false) + // AddItem(tview.NewBox(), 0, 2, false). + // AddItem(tview.NewBox(), 0, 3, false) + return headerView +} - m := model{list: l, currentView: DEFAULT} +func mainView(app *tview.Application) *tview.Flex { + flex := tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(header(app), 0, 1, false). + AddItem(tview.NewBox().SetBorder(true).SetTitle("Connections"), 0, 6, false) - if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil { - log.Fatal("Error running program:", err) + return flex +} + +func main() { + app := tview.NewApplication() + mainView := mainView(app) + + if err := app.SetRoot(mainView, true).SetFocus(mainView).Run(); err != nil { + panic(err) } + + // items := []list.Item{ + // item("New Connection"), + // item("Edit Connection"), + // item("Join Existing"), + // } + + // l := list.New(items, itemDelegate{}, defaultWidth, listHeight) + // l.Title = "Welcome to TermTable" + // l.SetShowStatusBar(false) + // l.SetFilteringEnabled(false) + // l.Styles.Title = titleStyle + // l.Styles.PaginationStyle = paginationStyle + // l.Styles.HelpStyle = helpStyle + + // m := model{list: l, currentView: DEFAULT} + + // if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil { + // log.Fatal("Error running program:", err) + // } } From dc291c8e6c8a622a9e24086ca3ecd6cb42a54091 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 28 Jan 2025 23:26:39 +0000 Subject: [PATCH 03/33] Added main layout and header content --- Makefile | 9 ---- main.go | 136 ++++++++++++++++++++++++++++++------------------------- 2 files changed, 75 insertions(+), 70 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index d846faa..0000000 --- a/Makefile +++ /dev/null @@ -1,9 +0,0 @@ -run: - go run . - -tidy: - go mod tidy - -lint: - golangci-lint run - diff --git a/main.go b/main.go index 46592a3..f8e7231 100644 --- a/main.go +++ b/main.go @@ -18,13 +18,21 @@ import ( type CurrentView string -const TERMTABLE_TEXT = ` - __ __ ___. .__ -_/ |_ ___________ ______/ |______ \_ |__ | | ____ -\ __\/ __ \_ __ \/ \ __\__ \ | __ \| | _/ __ \ - | | \ ___/| | \/ Y Y \ | / __ \| \_\ \ |_\ ___/ - |__| \___ >__| |__|_| /__| (____ /___ /____/\___ > - \/ \/ \/ \/ \/ +/* +________ ______ +\______ \ / __ \ ______ + | | \ > < / ___/ + | ` \/ -- \\___ \ +/_______ /\______ /____ > + \/ \/ \/ +*/ + +const APP_NAME = `________ ______ +\______ \ / __ \ ______ + | | \ > < / ___/ + | ` + "`" + ` \/ -- \\___ \ +/_______ /\______ /____ > + \/ \/ \/ ` const ( @@ -208,55 +216,54 @@ func (m model) View() string { } } -func hotkeys(app *tview.Application) *tview.List { - // hotkeyTable := tview.NewTable() - - // lorem := strings.Split("Lorem ipsum dolor", " ") - // // cols, rows := 5, 2 - // // word := 0 - // // for r := 0; r < rows; r++ { - // // for c := 0; c < cols; c++ { - // // color := tcell.ColorWhite - // // if c < 1 || r < 1 { - // // color = tcell.ColorYellow - // // } - // // word = (word + 1) % len(lorem) - // // } - // // } +type HotKey struct { + desc string + action func() + shortcut rune +} - list := tview.NewList().ShowSecondaryText(false). - SetSelectedFocusOnly(true). - SetShortcutStyle(tcell.StyleDefault) - - list. - AddItem("List item 1", "", 0, nil). - AddItem("List item 2", "", 0, nil). - AddItem("List item 3", "", 0, nil). - AddItem("List item 4", "", 0, nil). - AddItem(" quit", "", 0, func() { - app.Stop() - }) - - list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Rune() { - case 'a': - list.SetCurrentItem(0) - app.Stop() - return nil - case 'b': - list.SetCurrentItem(1) - app.Stop() - return nil +type HotKeys struct { + *tview.List + values []HotKey + currentOption int +} + +func NewHotkeys() *HotKeys { + list := tview.NewList(). + ShowSecondaryText(false).SetSelectedFocusOnly(true) + return &HotKeys{ + List: list, + values: []HotKey{}, + } +} + +func (r *HotKeys) AddHotKey(desc string, shortcut rune, action func()) *HotKeys { + r.values = append(r.values, HotKey{desc, action, shortcut}) + return r +} + +func (r *HotKeys) Draw(screen tcell.Screen) { + r.Box.DrawForSubclass(screen, r) + x, y, width, height := r.GetInnerRect() + + for index, hotkey := range r.values { + if index >= height { + break } - return event - }) - // hotkeyTable.SetCell(0, 0, - // tview.NewTableCell(lorem[word]). - // SetTextColor(color). - // SetAlign(tview.AlignCenter)) + line := fmt.Sprintf("<%s> %s", string(hotkey.shortcut), hotkey.desc) + tview.Print(screen, line, x, y+index, width, tview.AlignLeft, tcell.ColorYellow) + } +} - return list +func (r *HotKeys) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + return r.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + for _, hotkey := range r.values { + if event.Rune() == hotkey.shortcut && hotkey.action != nil { + hotkey.action() + } + } + }) } func currentConnectionInfo() *tview.List { @@ -271,22 +278,22 @@ func currentConnectionInfo() *tview.List { return list } -func header(app *tview.Application) *tview.Flex { +func header(hotkeyView *HotKeys) *tview.Flex { connection := currentConnectionInfo() - table := hotkeys(app) + appName := tview.NewTextView().SetText(APP_NAME).SetTextAlign(tview.AlignRight) headerView := tview.NewFlex(). AddItem(connection, 0, 1, false). - AddItem(table, 0, 2, false) - // AddItem(tview.NewBox(), 0, 2, false). - // AddItem(tview.NewBox(), 0, 3, false) + AddItem(hotkeyView, 0, 1, false). + AddItem(appName, 0, 1, false) + // AddItem(tview.NewBox(), 0, 3, false) return headerView } -func mainView(app *tview.Application) *tview.Flex { +func mainView(header *tview.Flex) *tview.Flex { flex := tview.NewFlex().SetDirection(tview.FlexRow). - AddItem(header(app), 0, 1, false). + AddItem(header, 0, 1, false). AddItem(tview.NewBox().SetBorder(true).SetTitle("Connections"), 0, 6, false) return flex @@ -294,9 +301,16 @@ func mainView(app *tview.Application) *tview.Flex { func main() { app := tview.NewApplication() - mainView := mainView(app) - if err := app.SetRoot(mainView, true).SetFocus(mainView).Run(); err != nil { + hotkeyView := NewHotkeys(). + AddHotKey("New Connection", 'n', nil). + AddHotKey("Quit", 'q', func() { app.Stop() }) + header := header(hotkeyView) + mainView := mainView(header) + + app.SetRoot(mainView, true).SetFocus(hotkeyView) + + if err := app.Run(); err != nil { panic(err) } From 8ad72d17415606de038208f210dc983fe02446b2 Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 29 Jan 2025 23:48:36 +0000 Subject: [PATCH 04/33] Changed name to D7s and added new connection form ui --- main.go | 68 +++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/main.go b/main.go index f8e7231..4c62f50 100644 --- a/main.go +++ b/main.go @@ -18,21 +18,12 @@ import ( type CurrentView string -/* -________ ______ -\______ \ / __ \ ______ - | | \ > < / ___/ - | ` \/ -- \\___ \ -/_______ /\______ /____ > - \/ \/ \/ -*/ - -const APP_NAME = `________ ______ -\______ \ / __ \ ______ - | | \ > < / ___/ - | ` + "`" + ` \/ -- \\___ \ -/_______ /\______ /____ > - \/ \/ \/ +const APP_NAME = `_________________ +\______ \______ \______ + | | \ / / ___/ + | ` + "`" + ` \/ /\___ \ +/_______ /____//____ > + \/ \/ ` const ( @@ -291,6 +282,33 @@ func header(hotkeyView *HotKeys) *tview.Flex { return headerView } +func newConnectionForm(app *tview.Application) *tview.Flex { + form := tview.NewForm(). + AddInputField("Name", "", 26, nil, nil). + AddInputField("Host", "", 26, nil, nil). + AddInputField("Port", "", 26, nil, nil). + AddInputField("User", "", 26, nil, nil). + AddPasswordField("Password", "", 26, '*', nil). + AddInputField("Database", "", 26, nil, nil). + AddButton("Save", nil). + AddButton("Test", nil). + AddButton("Connect", func() { + app.Stop() + }) + form.SetBorder(true) + form.SetButtonsAlign(tview.AlignRight) + + modal := tview.NewFlex(). + AddItem(nil, 0, 3, false). + AddItem(tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(nil, 0, 1, false). + AddItem(form, 0, 1, true). + AddItem(nil, 0, 1, false), 0, 2, true). + AddItem(nil, 0, 3, false) + + return modal +} + func mainView(header *tview.Flex) *tview.Flex { flex := tview.NewFlex().SetDirection(tview.FlexRow). AddItem(header, 0, 1, false). @@ -299,16 +317,28 @@ func mainView(header *tview.Flex) *tview.Flex { return flex } +const ( + MAIN_PAGE = "main" + NEW_CONNETION_FORM = "newConnection" +) + func main() { app := tview.NewApplication() - hotkeyView := NewHotkeys(). - AddHotKey("New Connection", 'n', nil). - AddHotKey("Quit", 'q', func() { app.Stop() }) + hotkeyView := NewHotkeys() header := header(hotkeyView) mainView := mainView(header) - app.SetRoot(mainView, true).SetFocus(hotkeyView) + pages := tview.NewPages(). + AddPage(MAIN_PAGE, mainView, true, true). + AddPage(NEW_CONNETION_FORM, newConnectionForm(app), true, false) + hotkeyView. + AddHotKey("New Connection", 'n', func() { + pages.ShowPage(NEW_CONNETION_FORM) + }). + AddHotKey("Quit", 'q', func() { app.Stop() }) + + app.SetRoot(pages, true).SetFocus(hotkeyView) if err := app.Run(); err != nil { panic(err) From 74526e93c467b7aa0b10f9822282f7d8fcd841bd Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 30 Jan 2025 23:18:29 +0000 Subject: [PATCH 05/33] Added logic back to display saved connections in a table in the main view --- db.go | 4 +- existing_connection.go | 2 +- keyring.go | 10 +++- main.go | 112 +++++++++++++++++++++++++++++++++-------- new_connection.go | 2 +- 5 files changed, 105 insertions(+), 25 deletions(-) diff --git a/db.go b/db.go index ab012dd..7121ce0 100644 --- a/db.go +++ b/db.go @@ -18,7 +18,7 @@ type Connection struct { Host string Port string User string - Pass string + Password string Database string Name string status ConnectionStatus @@ -26,7 +26,7 @@ type Connection struct { func (params Connection) ConnectionString() string { // urlExample := "postgres://username:password@localhost:5432/database_name" - return fmt.Sprintf("postgres://%s:%s@%s:%s/%s", params.User, params.Pass, params.Host, params.Port, params.Database) + return fmt.Sprintf("postgres://%s:%s@%s:%s/%s", params.User, params.Password, params.Host, params.Port, params.Database) } func (params *Connection) TestConnection() TestStatus { diff --git a/existing_connection.go b/existing_connection.go index 446ac2d..56093d9 100644 --- a/existing_connection.go +++ b/existing_connection.go @@ -75,7 +75,7 @@ func (m ExistingConnectionsModel) Update(msg tea.Msg) (ExistingConnectionsModel, } m.selectedConnection.User = user - m.selectedConnection.Pass = pass + m.selectedConnection.Password = pass break } diff --git a/keyring.go b/keyring.go index 0bb0f3d..bc66d8c 100644 --- a/keyring.go +++ b/keyring.go @@ -168,7 +168,7 @@ func parseKeyringPassword(password string) (string, string, error) { func SaveConnectionInKeyring(conn Connection) { // Save keyring part - password := createKeyringPassword(conn.User, conn.Pass) + password := createKeyringPassword(conn.User, conn.Password) err := keyring.Set(SERVICE, conn.Name, password) if err != nil { @@ -207,8 +207,16 @@ func ListConnections() ([]Connection, error) { if len(hostPortDb) != 3 { continue } + + user, password, err := GetConnectionFromKeyring(k) + if err != nil { + log.Fatal("Could not get user and password for db", err) + } + conn := Connection{ Name: k, + User: user, + Password: password, Host: hostPortDb[0], Port: hostPortDb[1], Database: hostPortDb[2], diff --git a/main.go b/main.go index 4c62f50..0670c8d 100644 --- a/main.go +++ b/main.go @@ -215,8 +215,7 @@ type HotKey struct { type HotKeys struct { *tview.List - values []HotKey - currentOption int + values []HotKey } func NewHotkeys() *HotKeys { @@ -258,13 +257,14 @@ func (r *HotKeys) InputHandler() func(event *tcell.EventKey, setFocus func(p tvi } func currentConnectionInfo() *tview.List { - list := tview.NewList().ShowSecondaryText(false). + list := tview.NewList(). + ShowSecondaryText(false). SetSelectedFocusOnly(true). - AddItem(" Name: ", "", 0, nil). - AddItem(" Database: ", "", 0, nil). - AddItem(" Host: ", "", 0, nil). - AddItem(" PORT: ", "", 0, nil). - AddItem(" USER: ", "Press to exit", 0, nil) + AddItem("Name: ", "", 0, nil). + AddItem("Host: ", "", 0, nil). + AddItem("PORT: ", "", 0, nil). + AddItem("USER: ", "Press to exit", 0, nil). + AddItem("Database: ", "", 0, nil) return list } @@ -278,7 +278,8 @@ func header(hotkeyView *HotKeys) *tview.Flex { AddItem(connection, 0, 1, false). AddItem(hotkeyView, 0, 1, false). AddItem(appName, 0, 1, false) - // AddItem(tview.NewBox(), 0, 3, false) + headerView.SetBorderPadding(0, 0, 1, 1) + return headerView } @@ -309,36 +310,107 @@ func newConnectionForm(app *tview.Application) *tview.Flex { return modal } -func mainView(header *tview.Flex) *tview.Flex { +const ( + MAIN_PAGE = "main" + NEW_CONNETION_FORM = "newConnection" + SAVED_CONNECTIONS = "savedConnections" +) + +func savedConnections() *tview.Box { + connectionsView := tview.NewBox().SetBorder(true).SetTitle("Connections") + + connections, err := ListConnections() + + if err != nil { + return connectionsView + } + + table := tview.NewTable() + + for i, conn := range connections { + table.SetCell(i, 0, tview.NewTableCell(conn.Name)) + table.SetCell(i, 1, tview.NewTableCell(conn.Host)) + table.SetCell(i, 2, tview.NewTableCell(conn.Port)) + table.SetCell(i, 3, tview.NewTableCell(conn.User)) + table.SetCell(i, 4, tview.NewTableCell(conn.Database)) + } + + table.SetBorderPadding(0, 0, 1, 1) + // table.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { + // if key == tcell.KeyEscape { + // app.Stop() + // } + // if key == tcell.KeyEnter { + // table.SetSelectable(true, true) + // } + // }).SetSelectedFunc(func(row int, column int) { + // table.GetCell(row, column).SetTextColor(tcell.ColorRed) + // table.SetSelectable(false, false) + // }) + + connectionsView.SetDrawFunc(func(screen tcell.Screen, x int, y int, w int, h int) (int, int, int, int) { + centerY := y + 1 + centerX := x + 1 + + table.SetRect(centerX, centerY, w-2, h-2) + table.Draw(screen) + + // Space for other content. + return x + 1, centerY + 1, w - 2, h - (centerY + 1 - y) + }) + + return connectionsView +} + +func contentView() *tview.Pages { + pages := tview.NewPages(). + AddPage(SAVED_CONNECTIONS, savedConnections(), true, true) + + return pages +} + +func mainLayout(header *tview.Flex) *tview.Flex { + pages := contentView() + flex := tview.NewFlex().SetDirection(tview.FlexRow). AddItem(header, 0, 1, false). - AddItem(tview.NewBox().SetBorder(true).SetTitle("Connections"), 0, 6, false) + AddItem(pages, 0, 6, false) return flex } -const ( - MAIN_PAGE = "main" - NEW_CONNETION_FORM = "newConnection" -) - func main() { app := tview.NewApplication() hotkeyView := NewHotkeys() header := header(hotkeyView) - mainView := mainView(header) + mainView := mainLayout(header) - pages := tview.NewPages(). + mainPages := tview.NewPages(). AddPage(MAIN_PAGE, mainView, true, true). AddPage(NEW_CONNETION_FORM, newConnectionForm(app), true, false) hotkeyView. AddHotKey("New Connection", 'n', func() { - pages.ShowPage(NEW_CONNETION_FORM) + mainPages.ShowPage(NEW_CONNETION_FORM) }). AddHotKey("Quit", 'q', func() { app.Stop() }) - app.SetRoot(pages, true).SetFocus(hotkeyView) + app.SetRoot(mainPages, true).SetFocus(mainView) + + app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + name, _ := mainPages.GetFrontPage() + + switch name { + case MAIN_PAGE: + if event.Rune() == 'q' { + app.Stop() + } + default: + + } + + return event + }) if err := app.Run(); err != nil { panic(err) diff --git a/new_connection.go b/new_connection.go index f4c7380..99ab8ab 100644 --- a/new_connection.go +++ b/new_connection.go @@ -127,7 +127,7 @@ func (m NewConnectionModel) Update(msg tea.Msg) (NewConnectionModel, tea.Cmd) { Host: m.inputs[0].Value(), Port: m.inputs[1].Value(), User: m.inputs[2].Value(), - Pass: m.inputs[3].Value(), + Password: m.inputs[3].Value(), Database: m.inputs[4].Value(), Name: m.inputs[5].Value(), status: DISCONNECTED, From faf350a314a3a4e9f1c54c4c2f19d9a9b5ca3084 Mon Sep 17 00:00:00 2001 From: Rob Date: Fri, 31 Jan 2025 00:27:09 +0000 Subject: [PATCH 06/33] Refactoring and restructuring of pages, layouts and events --- main.go | 98 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/main.go b/main.go index 0670c8d..afafd42 100644 --- a/main.go +++ b/main.go @@ -209,7 +209,6 @@ func (m model) View() string { type HotKey struct { desc string - action func() shortcut rune } @@ -227,8 +226,8 @@ func NewHotkeys() *HotKeys { } } -func (r *HotKeys) AddHotKey(desc string, shortcut rune, action func()) *HotKeys { - r.values = append(r.values, HotKey{desc, action, shortcut}) +func (r *HotKeys) AddHotKey(desc string, shortcut rune) *HotKeys { + r.values = append(r.values, HotKey{desc, shortcut}) return r } @@ -246,15 +245,15 @@ func (r *HotKeys) Draw(screen tcell.Screen) { } } -func (r *HotKeys) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { - return r.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { - for _, hotkey := range r.values { - if event.Rune() == hotkey.shortcut && hotkey.action != nil { - hotkey.action() - } - } - }) -} +// func (r *HotKeys) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { +// return r.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { +// for _, hotkey := range r.values { +// if event.Rune() == hotkey.shortcut && hotkey.action != nil { +// hotkey.action() +// } +// } +// }) +// } func currentConnectionInfo() *tview.List { list := tview.NewList(). @@ -327,15 +326,23 @@ func savedConnections() *tview.Box { table := tview.NewTable() + table.SetCell(0, 0, tview.NewTableCell("Name").SetExpansion(1)) + table.SetCell(0, 1, tview.NewTableCell("Host").SetExpansion(1)) + table.SetCell(0, 2, tview.NewTableCell("Port").SetExpansion(1)) + table.SetCell(0, 3, tview.NewTableCell("User").SetExpansion(1)) + table.SetCell(0, 4, tview.NewTableCell("Database").SetExpansion(1)) + for i, conn := range connections { - table.SetCell(i, 0, tview.NewTableCell(conn.Name)) - table.SetCell(i, 1, tview.NewTableCell(conn.Host)) - table.SetCell(i, 2, tview.NewTableCell(conn.Port)) - table.SetCell(i, 3, tview.NewTableCell(conn.User)) - table.SetCell(i, 4, tview.NewTableCell(conn.Database)) + table.SetCell(i+1, 0, tview.NewTableCell(conn.Name)) + table.SetCell(i+1, 1, tview.NewTableCell(conn.Host)) + table.SetCell(i+1, 2, tview.NewTableCell(conn.Port)) + table.SetCell(i+1, 3, tview.NewTableCell(conn.User)) + table.SetCell(i+1, 4, tview.NewTableCell(conn.Database)) } table.SetBorderPadding(0, 0, 1, 1) + + table.SetSelectable(true, false) // table.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { // if key == tcell.KeyEscape { // app.Stop() @@ -362,21 +369,25 @@ func savedConnections() *tview.Box { return connectionsView } -func contentView() *tview.Pages { +func mainContent() *tview.Pages { pages := tview.NewPages(). AddPage(SAVED_CONNECTIONS, savedConnections(), true, true) return pages } -func mainLayout(header *tview.Flex) *tview.Flex { - pages := contentView() +type Layout struct { + *tview.Flex + header *tview.Flex + content *tview.Pages +} - flex := tview.NewFlex().SetDirection(tview.FlexRow). +func newLayout(header *tview.Flex, content *tview.Pages) Layout { + view := tview.NewFlex().SetDirection(tview.FlexRow). AddItem(header, 0, 1, false). - AddItem(pages, 0, 6, false) + AddItem(content, 0, 6, false) - return flex + return Layout{view, header, content} } func main() { @@ -384,27 +395,38 @@ func main() { hotkeyView := NewHotkeys() header := header(hotkeyView) - mainView := mainLayout(header) + pages := mainContent() + mainView := newLayout(header, pages) - mainPages := tview.NewPages(). - AddPage(MAIN_PAGE, mainView, true, true). - AddPage(NEW_CONNETION_FORM, newConnectionForm(app), true, false) - hotkeyView. - AddHotKey("New Connection", 'n', func() { - mainPages.ShowPage(NEW_CONNETION_FORM) - }). - AddHotKey("Quit", 'q', func() { app.Stop() }) + mainPanels := tview.NewPages(). + AddPage(MAIN_PAGE, mainView, true, true) - app.SetRoot(mainPages, true).SetFocus(mainView) + hotkeyView. + AddHotKey("New Connection", 'n'). + AddHotKey("Quit", 'q') app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - name, _ := mainPages.GetFrontPage() + pageName, _ := mainPanels.GetFrontPage() + contentName, _ := mainView.content.GetFrontPage() - switch name { - case MAIN_PAGE: - if event.Rune() == 'q' { + switch contentName { + case SAVED_CONNECTIONS: + switch event.Rune() { + case 'q': app.Stop() + case 'n': + if pageName == NEW_CONNETION_FORM { + return event + } + addConnectionForm := newConnectionForm(app) + mainPanels.AddPage(NEW_CONNETION_FORM, addConnectionForm, true, true) + return nil } + + if event.Key() == tcell.KeyESC { + mainPanels.RemovePage(NEW_CONNETION_FORM) + } + default: } @@ -412,7 +434,7 @@ func main() { return event }) - if err := app.Run(); err != nil { + if err := app.SetRoot(mainPanels, true).SetFocus(mainView).Run(); err != nil { panic(err) } From 82b5e2bb67b7821935a0c21dabcf08421d3d2a4e Mon Sep 17 00:00:00 2001 From: Rob Date: Fri, 31 Jan 2025 19:47:28 +0000 Subject: [PATCH 07/33] Refactored out primitive, box with content --- main.go | 86 ++++++++++++++++++++++++++------------------------------- 1 file changed, 39 insertions(+), 47 deletions(-) diff --git a/main.go b/main.go index afafd42..1f40d83 100644 --- a/main.go +++ b/main.go @@ -315,22 +315,20 @@ const ( SAVED_CONNECTIONS = "savedConnections" ) -func savedConnections() *tview.Box { - connectionsView := tview.NewBox().SetBorder(true).SetTitle("Connections") - +func savedConnections() *tview.Table { connections, err := ListConnections() if err != nil { - return connectionsView + return nil } table := tview.NewTable() - table.SetCell(0, 0, tview.NewTableCell("Name").SetExpansion(1)) - table.SetCell(0, 1, tview.NewTableCell("Host").SetExpansion(1)) - table.SetCell(0, 2, tview.NewTableCell("Port").SetExpansion(1)) - table.SetCell(0, 3, tview.NewTableCell("User").SetExpansion(1)) - table.SetCell(0, 4, tview.NewTableCell("Database").SetExpansion(1)) + table.SetCell(0, 0, tview.NewTableCell("NAME").SetExpansion(1)) + table.SetCell(0, 1, tview.NewTableCell("HOST").SetExpansion(1)) + table.SetCell(0, 2, tview.NewTableCell("PORT").SetExpansion(1)) + table.SetCell(0, 3, tview.NewTableCell("USER").SetExpansion(1)) + table.SetCell(0, 4, tview.NewTableCell("DATABASE").SetExpansion(1)) for i, conn := range connections { table.SetCell(i+1, 0, tview.NewTableCell(conn.Name)) @@ -343,37 +341,29 @@ func savedConnections() *tview.Box { table.SetBorderPadding(0, 0, 1, 1) table.SetSelectable(true, false) - // table.Select(0, 0).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { - // if key == tcell.KeyEscape { - // app.Stop() - // } - // if key == tcell.KeyEnter { - // table.SetSelectable(true, true) - // } - // }).SetSelectedFunc(func(row int, column int) { - // table.GetCell(row, column).SetTextColor(tcell.ColorRed) - // table.SetSelectable(false, false) - // }) - - connectionsView.SetDrawFunc(func(screen tcell.Screen, x int, y int, w int, h int) (int, int, int, int) { - centerY := y + 1 - centerX := x + 1 - - table.SetRect(centerX, centerY, w-2, h-2) - table.Draw(screen) - - // Space for other content. - return x + 1, centerY + 1, w - 2, h - (centerY + 1 - y) - }) + table.Select(1, 0) - return connectionsView + return table } -func mainContent() *tview.Pages { - pages := tview.NewPages(). - AddPage(SAVED_CONNECTIONS, savedConnections(), true, true) +type ContentBox struct { + *tview.Box + content tview.Primitive +} + +func newContentBox(title string, content tview.Primitive) *ContentBox { + return &ContentBox{ + tview.NewBox().SetBorder(true).SetTitle(title), + content, + } +} + +func (b *ContentBox) Draw(screen tcell.Screen) { + b.Box.DrawForSubclass(screen, b) + x, y, w, h := b.GetInnerRect() - return pages + b.content.SetRect(x, y, w, h) + b.content.Draw(screen) } type Layout struct { @@ -393,20 +383,22 @@ func newLayout(header *tview.Flex, content *tview.Pages) Layout { func main() { app := tview.NewApplication() - hotkeyView := NewHotkeys() + hotkeyView := NewHotkeys(). + AddHotKey("New Connection", 'n'). + AddHotKey("Quit", 'q') header := header(hotkeyView) - pages := mainContent() + + connectionsView := newContentBox("Connections", savedConnections()) + + pages := tview.NewPages(). + AddPage(SAVED_CONNECTIONS, connectionsView, true, true) mainView := newLayout(header, pages) - mainPanels := tview.NewPages(). + mainScreens := tview.NewPages(). AddPage(MAIN_PAGE, mainView, true, true) - hotkeyView. - AddHotKey("New Connection", 'n'). - AddHotKey("Quit", 'q') - app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - pageName, _ := mainPanels.GetFrontPage() + pageName, _ := mainScreens.GetFrontPage() contentName, _ := mainView.content.GetFrontPage() switch contentName { @@ -419,12 +411,12 @@ func main() { return event } addConnectionForm := newConnectionForm(app) - mainPanels.AddPage(NEW_CONNETION_FORM, addConnectionForm, true, true) + mainScreens.AddPage(NEW_CONNETION_FORM, addConnectionForm, true, true) return nil } if event.Key() == tcell.KeyESC { - mainPanels.RemovePage(NEW_CONNETION_FORM) + mainScreens.RemovePage(NEW_CONNETION_FORM) } default: @@ -434,7 +426,7 @@ func main() { return event }) - if err := app.SetRoot(mainPanels, true).SetFocus(mainView).Run(); err != nil { + if err := app.SetRoot(mainScreens, true).SetFocus(connectionsView.content).Run(); err != nil { panic(err) } From 68c3fb1bdf2d8476a74c323b8b921a228351d846 Mon Sep 17 00:00:00 2001 From: Rob Date: Sat, 1 Feb 2025 21:07:27 +0000 Subject: [PATCH 08/33] Fixed inner table input handling due to focus --- main.go | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/main.go b/main.go index 1f40d83..3536533 100644 --- a/main.go +++ b/main.go @@ -245,16 +245,6 @@ func (r *HotKeys) Draw(screen tcell.Screen) { } } -// func (r *HotKeys) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { -// return r.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { -// for _, hotkey := range r.values { -// if event.Rune() == hotkey.shortcut && hotkey.action != nil { -// hotkey.action() -// } -// } -// }) -// } - func currentConnectionInfo() *tview.List { list := tview.NewList(). ShowSecondaryText(false). @@ -353,7 +343,7 @@ type ContentBox struct { func newContentBox(title string, content tview.Primitive) *ContentBox { return &ContentBox{ - tview.NewBox().SetBorder(true).SetTitle(title), + tview.NewBox().SetTitle(title), content, } } @@ -366,6 +356,12 @@ func (b *ContentBox) Draw(screen tcell.Screen) { b.content.Draw(screen) } +func (b *ContentBox) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + return b.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + b.content.InputHandler()(event, setFocus) + }) +} + type Layout struct { *tview.Flex header *tview.Flex @@ -390,15 +386,16 @@ func main() { connectionsView := newContentBox("Connections", savedConnections()) - pages := tview.NewPages(). - AddPage(SAVED_CONNECTIONS, connectionsView, true, true) - mainView := newLayout(header, pages) + contentPages := tview.NewPages(). + AddAndSwitchToPage(SAVED_CONNECTIONS, connectionsView, true) + contentPages.SetBorder(true) + mainView := newLayout(header, contentPages) - mainScreens := tview.NewPages(). - AddPage(MAIN_PAGE, mainView, true, true) + mainPages := tview.NewPages(). + AddAndSwitchToPage(MAIN_PAGE, mainView, true) app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - pageName, _ := mainScreens.GetFrontPage() + pageName, _ := mainPages.GetFrontPage() contentName, _ := mainView.content.GetFrontPage() switch contentName { @@ -411,12 +408,12 @@ func main() { return event } addConnectionForm := newConnectionForm(app) - mainScreens.AddPage(NEW_CONNETION_FORM, addConnectionForm, true, true) + mainPages.AddPage(NEW_CONNETION_FORM, addConnectionForm, true, true) return nil } if event.Key() == tcell.KeyESC { - mainScreens.RemovePage(NEW_CONNETION_FORM) + mainPages.RemovePage(NEW_CONNETION_FORM) } default: @@ -426,7 +423,7 @@ func main() { return event }) - if err := app.SetRoot(mainScreens, true).SetFocus(connectionsView.content).Run(); err != nil { + if err := app.SetRoot(mainPages, true).SetFocus(contentPages).Run(); err != nil { panic(err) } From fe832b9c0549b3d3edb8a3572069d96f6438fee4 Mon Sep 17 00:00:00 2001 From: Rob Date: Sun, 2 Feb 2025 00:49:06 +0000 Subject: [PATCH 09/33] Refactored saved connection table to a type --- main.go | 57 ++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/main.go b/main.go index 3536533..c590393 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,6 @@ import ( "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - // "github.com/gdamore/tcell/v2" ) type CurrentView string @@ -305,13 +304,12 @@ const ( SAVED_CONNECTIONS = "savedConnections" ) -func savedConnections() *tview.Table { - connections, err := ListConnections() - - if err != nil { - return nil - } +type ConnectionsTable struct { + *tview.Table + connections []Connection +} +func newConnectionsTable() *ConnectionsTable { table := tview.NewTable() table.SetCell(0, 0, tview.NewTableCell("NAME").SetExpansion(1)) @@ -320,20 +318,36 @@ func savedConnections() *tview.Table { table.SetCell(0, 3, tview.NewTableCell("USER").SetExpansion(1)) table.SetCell(0, 4, tview.NewTableCell("DATABASE").SetExpansion(1)) - for i, conn := range connections { - table.SetCell(i+1, 0, tview.NewTableCell(conn.Name)) - table.SetCell(i+1, 1, tview.NewTableCell(conn.Host)) - table.SetCell(i+1, 2, tview.NewTableCell(conn.Port)) - table.SetCell(i+1, 3, tview.NewTableCell(conn.User)) - table.SetCell(i+1, 4, tview.NewTableCell(conn.Database)) + table.SetBorderPadding(0, 0, 1, 1) + table.SetSelectable(true, false).Select(1, 0) + + connectionsTable := ConnectionsTable{table, []Connection{}} + connectionsTable.getConnections() + + return &connectionsTable +} + +func (ct *ConnectionsTable) getConnections() { + connections, err := ListConnections() + + if err != nil { + return } - table.SetBorderPadding(0, 0, 1, 1) + for i, conn := range connections { + ct.SetCell(i+1, 0, tview.NewTableCell(conn.Name)) + ct.SetCell(i+1, 1, tview.NewTableCell(conn.Host)) + ct.SetCell(i+1, 2, tview.NewTableCell(conn.Port)) + ct.SetCell(i+1, 3, tview.NewTableCell(conn.User)) + ct.SetCell(i+1, 4, tview.NewTableCell(conn.Database)) + } - table.SetSelectable(true, false) - table.Select(1, 0) + ct.connections = connections +} - return table +func (ct *ConnectionsTable) getConnection() Connection { + row, _ := ct.GetSelection() + return ct.connections[row] } type ContentBox struct { @@ -384,7 +398,8 @@ func main() { AddHotKey("Quit", 'q') header := header(hotkeyView) - connectionsView := newContentBox("Connections", savedConnections()) + connectionsTable := newConnectionsTable() + connectionsView := newContentBox("Connections", connectionsTable) contentPages := tview.NewPages(). AddAndSwitchToPage(SAVED_CONNECTIONS, connectionsView, true) @@ -412,8 +427,11 @@ func main() { return nil } - if event.Key() == tcell.KeyESC { + switch event.Key() { + case tcell.KeyESC: mainPages.RemovePage(NEW_CONNETION_FORM) + case tcell.KeyEnter: + fmt.Println(connectionsTable.getConnection()) } default: @@ -423,6 +441,7 @@ func main() { return event }) + // TODO somthing with focus is causing the extra border outline if err := app.SetRoot(mainPages, true).SetFocus(contentPages).Run(); err != nil { panic(err) } From e5846a6efbf1670799729bbe28b66b1a6a3f711d Mon Sep 17 00:00:00 2001 From: Rob Date: Sun, 2 Feb 2025 02:35:19 +0000 Subject: [PATCH 10/33] Wip db view table --- db.go | 7 +++ main.go | 53 +++++++++++---------- open_connection.go | 114 +++++++++++++++++---------------------------- 3 files changed, 79 insertions(+), 95 deletions(-) diff --git a/db.go b/db.go index 7121ce0..675ba80 100644 --- a/db.go +++ b/db.go @@ -24,6 +24,10 @@ type Connection struct { status ConnectionStatus } +func (c Connection) Row() []string { + return []string{c.Name, c.Host, c.Port, c.User, c.Database} +} + func (params Connection) ConnectionString() string { // urlExample := "postgres://username:password@localhost:5432/database_name" return fmt.Sprintf("postgres://%s:%s@%s:%s/%s", params.User, params.Password, params.Host, params.Port, params.Database) @@ -74,6 +78,7 @@ func (parmas Connection) GetTableNames() []string { } type Table struct { + name string fields []string values [][]string } @@ -115,6 +120,8 @@ func (params Connection) SelectAll(table string) (Table, error) { tableData.values = append(tableData.values, strValues) } + tableData.name = table + conn.Close(context.Background()) return tableData, nil diff --git a/main.go b/main.go index c590393..dc07a98 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,7 @@ const ( NEW_CONNECTION CurrentView = "NEW_CONNECTION" EDIT_CONNECTION CurrentView = "EDIT_CONNECTION" JOIN_EXISTING CurrentView = "JOIN_EXISTING" - DATABASE_VIEW CurrentView = "DATABASE_VIEW" + // DATABASE_VIEW CurrentView = "DATABASE_VIEW" ) const ( @@ -302,32 +302,34 @@ const ( MAIN_PAGE = "main" NEW_CONNETION_FORM = "newConnection" SAVED_CONNECTIONS = "savedConnections" + DATABASE_VIEW = "databaseView" ) -type ConnectionsTable struct { +var CONNECTION_TABLE_HEADERS = []string{"NAME", "HOST", "PORT", "USER", "DATABASE"} + +type DisplayTable struct { *tview.Table - connections []Connection + columns []string + rows []Connection } -func newConnectionsTable() *ConnectionsTable { +func newConnectionsTable(columns []string) *DisplayTable { table := tview.NewTable() - table.SetCell(0, 0, tview.NewTableCell("NAME").SetExpansion(1)) - table.SetCell(0, 1, tview.NewTableCell("HOST").SetExpansion(1)) - table.SetCell(0, 2, tview.NewTableCell("PORT").SetExpansion(1)) - table.SetCell(0, 3, tview.NewTableCell("USER").SetExpansion(1)) - table.SetCell(0, 4, tview.NewTableCell("DATABASE").SetExpansion(1)) + for i, header := range columns { + table.SetCell(0, i, tview.NewTableCell(header).SetExpansion(1)) + } table.SetBorderPadding(0, 0, 1, 1) table.SetSelectable(true, false).Select(1, 0) - connectionsTable := ConnectionsTable{table, []Connection{}} + connectionsTable := DisplayTable{table, columns, []Connection{}} connectionsTable.getConnections() return &connectionsTable } -func (ct *ConnectionsTable) getConnections() { +func (t *DisplayTable) getConnections() { connections, err := ListConnections() if err != nil { @@ -335,19 +337,18 @@ func (ct *ConnectionsTable) getConnections() { } for i, conn := range connections { - ct.SetCell(i+1, 0, tview.NewTableCell(conn.Name)) - ct.SetCell(i+1, 1, tview.NewTableCell(conn.Host)) - ct.SetCell(i+1, 2, tview.NewTableCell(conn.Port)) - ct.SetCell(i+1, 3, tview.NewTableCell(conn.User)) - ct.SetCell(i+1, 4, tview.NewTableCell(conn.Database)) + values := conn.Row() + for j, value := range values { + t.SetCell(i+1, j, tview.NewTableCell(value)) + } } - ct.connections = connections + t.rows = connections } -func (ct *ConnectionsTable) getConnection() Connection { - row, _ := ct.GetSelection() - return ct.connections[row] +func (t *DisplayTable) getConnection() Connection { + row, _ := t.GetSelection() + return t.rows[row] } type ContentBox struct { @@ -357,7 +358,7 @@ type ContentBox struct { func newContentBox(title string, content tview.Primitive) *ContentBox { return &ContentBox{ - tview.NewBox().SetTitle(title), + tview.NewBox().SetBorder(true).SetTitle(title), content, } } @@ -398,12 +399,11 @@ func main() { AddHotKey("Quit", 'q') header := header(hotkeyView) - connectionsTable := newConnectionsTable() + connectionsTable := newConnectionsTable(CONNECTION_TABLE_HEADERS) connectionsView := newContentBox("Connections", connectionsTable) contentPages := tview.NewPages(). AddAndSwitchToPage(SAVED_CONNECTIONS, connectionsView, true) - contentPages.SetBorder(true) mainView := newLayout(header, contentPages) mainPages := tview.NewPages(). @@ -431,7 +431,12 @@ func main() { case tcell.KeyESC: mainPages.RemovePage(NEW_CONNETION_FORM) case tcell.KeyEnter: - fmt.Println(connectionsTable.getConnection()) + connection := connectionsTable.getConnection() + db := NewOpenDatabase(connection) + // TODO pass rows + dbTable := newConnectionsTable(db.openTable.fields) + dbContent := newContentBox(db.openTable.name, dbTable) + contentPages.AddAndSwitchToPage(DATABASE_VIEW, dbContent, true) } default: diff --git a/open_connection.go b/open_connection.go index a23ac41..37e11d1 100644 --- a/open_connection.go +++ b/open_connection.go @@ -3,10 +3,11 @@ package main import ( "fmt" "io" + "log" "strings" "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/table" + // "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) @@ -68,29 +69,31 @@ func (d tableItemDelegate) Render(w io.Writer, m list.Model, index int, listItem } type OpenDatabase struct { - tables list.Model - viewMode ViewMode - selectedTable table.Model - params Connection + tables []string + viewMode ViewMode + // selectedTable table.Model + params Connection + openTable Table } func NewOpenDatabase(connParams Connection) OpenDatabase { databaseTables := connParams.GetTableNames() - listItems := []list.Item{} - for _, value := range databaseTables { - listItems = append(listItems, tableItem(value)) - } + // listItems := []list.Item{} + // for _, value := range databaseTables { + // listItems = append(listItems, tableItem(value)) + // } openDatabase := OpenDatabase{ - tables: list.New(listItems, tableItemDelegate{}, 14, 14), + // tables: list.New(listItems, tableItemDelegate{}, 14, 14), + tables: databaseTables, viewMode: TABLES, params: connParams, } - openDatabase.tables.SetShowHelp(false) - openDatabase.tables.SetShowTitle(false) - openDatabase.tables.SetShowStatusBar(false) + // openDatabase.tables.SetShowHelp(false) + // openDatabase.tables.SetShowTitle(false) + // openDatabase.tables.SetShowStatusBar(false) openDatabase.setOpenTable() @@ -98,60 +101,29 @@ func NewOpenDatabase(connParams Connection) OpenDatabase { } func (db *OpenDatabase) setOpenTable() { - selectedItem := db.tables.SelectedItem() - tableName := string(selectedItem.(tableItem)) + tableName := db.tables[0] - selectedTable, err := db.openTable(tableName) + table, err := db.params.SelectAll(tableName) + // selectedTable, err := db.openTable(tableName) if err != nil { - db.params.status = DISCONNECTED + // db.params.status = DISCONNECTED + log.Fatal("Could not connect to db", err) return } - db.selectedTable = selectedTable - db.selectedTable.SetWidth(width / 2) - db.selectedTable.SetHeight(height / 2) + db.openTable = table } -func (db OpenDatabase) openTable(tableName string) (table.Model, error) { - tableData, err := db.params.SelectAll(tableName) +// func (db OpenDatabase) openTable(tableName string) (Table, error) { +// return db.params.SelectAll(tableName) - if err != nil { - return db.selectedTable, err - } +// // if err != nil { +// // return Table{}, err +// // } - max_len := db.selectedTable.Width() / len(tableData.fields) - columns := make([]table.Column, len(tableData.fields)) - for i, field := range tableData.fields { - columns[i] = table.Column{Title: field, Width: max_len} - } - - rows := make([]table.Row, len(tableData.values)) - for i, value := range tableData.values { - rows[i] = make(table.Row, len(value)) - copy(rows[i], value) - } - - t := table.New( - table.WithColumns(columns), - table.WithRows(rows), - table.WithFocused(true), - ) - - s := table.DefaultStyles() - s.Header = s.Header. - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")). - BorderBottom(true). - Bold(false) - s.Selected = s.Selected. - Foreground(lipgloss.Color("229")). - Background(lipgloss.Color("57")). - Bold(false) - t.SetStyles(s) - - return t, nil -} +// // return tableData, nil +// } func (db OpenDatabase) Init() tea.Cmd { return nil @@ -179,9 +151,9 @@ func (db OpenDatabase) Update(msg tea.Msg) (OpenDatabase, tea.Cmd) { switch db.viewMode { case TABLES: - db.tables, cmd = db.tables.Update(msg) + // db.tables, cmd = db.tables.Update(msg) case OPEN: - db.selectedTable, cmd = db.selectedTable.Update(msg) + // db.selectedTable, cmd = db.selectedTable.Update(msg) } return db, cmd @@ -190,20 +162,20 @@ func (db OpenDatabase) Update(msg tea.Msg) (OpenDatabase, tea.Cmd) { func (db OpenDatabase) View() string { s := fmt.Sprintf("%s / %s\n\n", db.params.Name, db.params.Database) - tableLabels := db.tables.View() + // tableLabels := db.tables.View() - db.setOpenTable() - openTable := db.selectedTable.View() + // db.setOpenTable() + // openTable := db.selectedTable.View() - if db.viewMode == TABLES { - s += lipgloss.JoinHorizontal(lipgloss.Top, - focusedModelSideBarStyle.Render(tableLabels), - modelStyle.Render(openTable)) - } else { - s += lipgloss.JoinHorizontal(lipgloss.Top, - modelStyle.Render(tableLabels), - focusedModelStyle.Render(openTable)) - } + // if db.viewMode == TABLES { + // s += lipgloss.JoinHorizontal(lipgloss.Top, + // focusedModelSideBarStyle.Render(tableLabels), + // modelStyle.Render(openTable)) + // } else { + // s += lipgloss.JoinHorizontal(lipgloss.Top, + // modelStyle.Render(tableLabels), + // focusedModelStyle.Render(openTable)) + // } return paginationStyle.Render(s) } From 29f92e588e1e1f61c8382d1a21560209d5b8435a Mon Sep 17 00:00:00 2001 From: Rob Date: Sun, 2 Feb 2025 18:22:55 +0000 Subject: [PATCH 11/33] Hooked up table display --- main.go | 49 ++++++++++++++++++++++---- open_connection.go | 87 ++-------------------------------------------- 2 files changed, 45 insertions(+), 91 deletions(-) diff --git a/main.go b/main.go index dc07a98..d0ab0b4 100644 --- a/main.go +++ b/main.go @@ -346,9 +346,43 @@ func (t *DisplayTable) getConnections() { t.rows = connections } -func (t *DisplayTable) getConnection() Connection { +func (t *DisplayTable) getConnection() *Connection { row, _ := t.GetSelection() - return t.rows[row] + + if row == 0 { + return nil + } + + return &t.rows[row-1] +} + +type DbTable struct { + *tview.Table + table Table +} + +func newDbTable(table Table) *DbTable { + t := tview.NewTable() + + for i, header := range table.fields { + t.SetCell(0, i, tview.NewTableCell(header).SetExpansion(1)) + } + + t.SetBorderPadding(0, 0, 1, 1) + t.SetSelectable(true, false).Select(1, 0) + + connectionsTable := DbTable{t, table} + connectionsTable.getTableRows() + + return &connectionsTable +} + +func (t *DbTable) getTableRows() { + for i, conn := range t.table.values { + for j, value := range conn { + t.SetCell(i+1, j, tview.NewTableCell(value)) + } + } } type ContentBox struct { @@ -358,7 +392,7 @@ type ContentBox struct { func newContentBox(title string, content tview.Primitive) *ContentBox { return &ContentBox{ - tview.NewBox().SetBorder(true).SetTitle(title), + tview.NewBox().SetBorder(true).SetTitle(fmt.Sprintf(" %s ", title)), content, } } @@ -432,9 +466,12 @@ func main() { mainPages.RemovePage(NEW_CONNETION_FORM) case tcell.KeyEnter: connection := connectionsTable.getConnection() - db := NewOpenDatabase(connection) - // TODO pass rows - dbTable := newConnectionsTable(db.openTable.fields) + if connection == nil { + return event + } + + db := NewOpenDatabase(*connection) + dbTable := newDbTable(db.openTable) dbContent := newContentBox(db.openTable.name, dbTable) contentPages.AddAndSwitchToPage(DATABASE_VIEW, dbContent, true) } diff --git a/open_connection.go b/open_connection.go index 37e11d1..12aa21c 100644 --- a/open_connection.go +++ b/open_connection.go @@ -2,37 +2,9 @@ package main import ( "fmt" - "io" "log" - "strings" - "github.com/charmbracelet/bubbles/list" - // "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" -) - -var ( - modelStyle = lipgloss. - NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(GREY)) - focusedModelStyle = lipgloss. - NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(WHITE)) - - focusedModelSideBarStyle = lipgloss. - NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color(WHITE)) - - blurredModelSideBarStyle = lipgloss. - NewStyle(). - Foreground(lipgloss.Color(GREY)) - selectedTableStyle = lipgloss. - NewStyle(). - Foreground(lipgloss.Color(MAGENTA)) ) type ViewMode string @@ -43,35 +15,9 @@ const ( QUIT ViewMode = "QUIT" ) -type tableItem string - -func (i tableItem) FilterValue() string { return "" } - -type tableItemDelegate struct{} - -func (d tableItemDelegate) Height() int { return 1 } -func (d tableItemDelegate) Spacing() int { return 0 } -func (d tableItemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } -func (d tableItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { - str, ok := listItem.(tableItem) - if !ok { - return - } - - fn := blurredModelSideBarStyle.Render - if index == m.Index() { - fn = func(s ...string) string { - return selectedTableStyle.Render(strings.Join(s, " ")) - } - } - - fmt.Fprint(w, fn(string(str))) -} - type OpenDatabase struct { - tables []string - viewMode ViewMode - // selectedTable table.Model + tables []string + viewMode ViewMode params Connection openTable Table } @@ -79,22 +25,12 @@ type OpenDatabase struct { func NewOpenDatabase(connParams Connection) OpenDatabase { databaseTables := connParams.GetTableNames() - // listItems := []list.Item{} - // for _, value := range databaseTables { - // listItems = append(listItems, tableItem(value)) - // } - openDatabase := OpenDatabase{ - // tables: list.New(listItems, tableItemDelegate{}, 14, 14), tables: databaseTables, viewMode: TABLES, params: connParams, } - // openDatabase.tables.SetShowHelp(false) - // openDatabase.tables.SetShowTitle(false) - // openDatabase.tables.SetShowStatusBar(false) - openDatabase.setOpenTable() return openDatabase @@ -104,10 +40,8 @@ func (db *OpenDatabase) setOpenTable() { tableName := db.tables[0] table, err := db.params.SelectAll(tableName) - // selectedTable, err := db.openTable(tableName) if err != nil { - // db.params.status = DISCONNECTED log.Fatal("Could not connect to db", err) return } @@ -115,16 +49,6 @@ func (db *OpenDatabase) setOpenTable() { db.openTable = table } -// func (db OpenDatabase) openTable(tableName string) (Table, error) { -// return db.params.SelectAll(tableName) - -// // if err != nil { -// // return Table{}, err -// // } - -// // return tableData, nil -// } - func (db OpenDatabase) Init() tea.Cmd { return nil } @@ -149,13 +73,6 @@ func (db OpenDatabase) Update(msg tea.Msg) (OpenDatabase, tea.Cmd) { var cmd tea.Cmd - switch db.viewMode { - case TABLES: - // db.tables, cmd = db.tables.Update(msg) - case OPEN: - // db.selectedTable, cmd = db.selectedTable.Update(msg) - } - return db, cmd } From 220172736cbed86e96c1efd8ee7fc74fc93d9cc7 Mon Sep 17 00:00:00 2001 From: Rob Date: Sun, 2 Feb 2025 21:35:05 +0000 Subject: [PATCH 12/33] Removed references to bubbletea --- existing_connection.go | 166 ++++++++++++------------ go.mod | 16 +-- go.sum | 32 ----- main.go | 277 ++++++++++++++--------------------------- new_connection.go | 250 +++++-------------------------------- open_connection.go | 51 -------- 6 files changed, 203 insertions(+), 589 deletions(-) diff --git a/existing_connection.go b/existing_connection.go index 56093d9..7003b01 100644 --- a/existing_connection.go +++ b/existing_connection.go @@ -1,95 +1,87 @@ package main -import ( - "log" - - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" -) - type ExistingConnectionsModel struct { - list list.Model connections []Connection selectedConnection *Connection back bool } -func NewExistingConnectionsModel() ExistingConnectionsModel { - existingConnectionsModel := ExistingConnectionsModel{} - - connections, err := ListConnections() - - if err != nil { - return existingConnectionsModel - } - - items := []list.Item{} - - for _, conn := range connections { - items = append(items, item(conn.Name)) - } - - l := list.New(items, itemDelegate{}, defaultWidth, listHeight) - l.Title = "Choose a connection" - l.SetShowStatusBar(false) - l.SetFilteringEnabled(false) - l.Styles.Title = titleStyle - l.Styles.PaginationStyle = paginationStyle - l.Styles.HelpStyle = helpStyle - - existingConnectionsModel.list = l - existingConnectionsModel.connections = connections - - return existingConnectionsModel -} - -func (m ExistingConnectionsModel) Init() tea.Cmd { - return nil -} - -func (m ExistingConnectionsModel) Update(msg tea.Msg) (ExistingConnectionsModel, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.list.SetWidth(msg.Width) - return m, nil - - case tea.KeyMsg: - switch keypress := msg.String(); keypress { - case "q", "ctrl+c": - m.back = true - return m, nil - - case "enter": - i, ok := m.list.SelectedItem().(item) - if ok { - choice := string(i) - - for _, v := range m.connections { - if v.Name == choice { - m.selectedConnection = &v - - user, pass, err := GetConnectionFromKeyring(v.Name) - - if err != nil { - log.Fatal("Could not get user and password for connection from keyring: ", err) - } - - m.selectedConnection.User = user - m.selectedConnection.Password = pass - - break - } - } - } - return m, nil - } - } - - var cmd tea.Cmd - m.list, cmd = m.list.Update(msg) - return m, cmd -} - -func (m ExistingConnectionsModel) View() string { - return m.list.View() -} +// func NewExistingConnectionsModel() ExistingConnectionsModel { +// existingConnectionsModel := ExistingConnectionsModel{} + +// connections, err := ListConnections() + +// if err != nil { +// return existingConnectionsModel +// } + +// items := []list.Item{} + +// for _, conn := range connections { +// items = append(items, item(conn.Name)) +// } + +// l := list.New(items, itemDelegate{}, defaultWidth, listHeight) +// l.Title = "Choose a connection" +// l.SetShowStatusBar(false) +// l.SetFilteringEnabled(false) +// l.Styles.Title = titleStyle +// l.Styles.PaginationStyle = paginationStyle +// l.Styles.HelpStyle = helpStyle + +// existingConnectionsModel.list = l +// existingConnectionsModel.connections = connections + +// return existingConnectionsModel +// } + +// func (m ExistingConnectionsModel) Init() tea.Cmd { +// return nil +// } + +// func (m ExistingConnectionsModel) Update(msg tea.Msg) (ExistingConnectionsModel, tea.Cmd) { +// switch msg := msg.(type) { +// case tea.WindowSizeMsg: +// m.list.SetWidth(msg.Width) +// return m, nil + +// case tea.KeyMsg: +// switch keypress := msg.String(); keypress { +// case "q", "ctrl+c": +// m.back = true +// return m, nil + +// case "enter": +// i, ok := m.list.SelectedItem().(item) +// if ok { +// choice := string(i) + +// for _, v := range m.connections { +// if v.Name == choice { +// m.selectedConnection = &v + +// user, pass, err := GetConnectionFromKeyring(v.Name) + +// if err != nil { +// log.Fatal("Could not get user and password for connection from keyring: ", err) +// } + +// m.selectedConnection.User = user +// m.selectedConnection.Password = pass + +// break +// } +// } +// } +// return m, nil +// } +// } + +// var cmd tea.Cmd +// m.list, cmd = m.list.Update(msg) +// return m, cmd +// } + +// func (m ExistingConnectionsModel) View() string { +// return m.list.View() +// } diff --git a/go.mod b/go.mod index 61030fa..51f67c2 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,7 @@ module github.com/robertazzopardi/termtable go 1.23.4 require ( - github.com/charmbracelet/bubbles v0.18.0 - github.com/charmbracelet/bubbletea v0.25.0 - github.com/charmbracelet/lipgloss v0.10.0 + github.com/gdamore/tcell/v2 v2.7.1 github.com/jackc/pgx/v5 v5.5.5 github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57 github.com/zalando/go-keyring v0.2.4 @@ -14,27 +12,15 @@ require ( require ( github.com/alessio/shellescape v1.4.1 // indirect - github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/danieljoos/wincred v1.2.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect - github.com/gdamore/tcell/v2 v2.7.1 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.18 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect golang.org/x/crypto v0.17.0 // indirect - golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index 12aeb64..22b2a81 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,5 @@ github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -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/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.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= -github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= -github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= -github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= -github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -31,36 +19,18 @@ github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -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/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.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.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/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= -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.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= -github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 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/tview v0.0.0-20241227133733-17b7edb88c57 h1:LmsF7Fk5jyEDhJk0fYIqdWNuTxSyid2W42A0L2YWjGE= github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -93,9 +63,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/main.go b/main.go index d0ab0b4..5b8bbe6 100644 --- a/main.go +++ b/main.go @@ -2,17 +2,9 @@ package main import ( "fmt" - "io" - - // "log" - "strings" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" - - "github.com/charmbracelet/bubbles/list" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" ) type CurrentView string @@ -50,161 +42,95 @@ const ( LIGHT_GREY = "244" ) -var ( - titleStyle = lipgloss.NewStyle() - itemStyle = lipgloss.NewStyle().PaddingLeft(4) - paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) - quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 0) - helpStyle = blurredStyle.Copy().PaddingLeft(2) - cursorStyle = focusedItemStyle.Copy() - noStyle = lipgloss.NewStyle() - - selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color(MAGENTA)) - focusedItemStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(RED)) - focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(WHITE)) - blurredStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(GREY)) - successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(GREEN)) - errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(RED)) - - width int = 100 - height int = 100 -) - -type item string - -func (i item) FilterValue() string { return "" } - -type itemDelegate struct{} - -func (d itemDelegate) Height() int { return 1 } -func (d itemDelegate) Spacing() int { return 0 } -func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } -func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { - i, ok := listItem.(item) - if !ok { - return - } - - str := fmt.Sprintf("%d. %s", index+1, i) - - fn := itemStyle.Render - if index == m.Index() { - fn = func(s ...string) string { - return selectedItemStyle.Render("> " + strings.Join(s, " ")) - } - } - - fmt.Fprint(w, fn(str)) -} - -type model struct { - list list.Model - newConnectionModel NewConnectionModel - currentView CurrentView - currentConnection Connection - openDatabase OpenDatabase - existingConnections ExistingConnectionsModel -} - -func (m model) updateEvents(msg tea.Msg) (model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.list.SetWidth(msg.Width) - width = msg.Width - height = msg.Height - return m, nil - - case tea.KeyMsg: - switch keypress := msg.String(); keypress { - case "q", "ctrl+c": - return m, tea.Quit - - case "enter": - i, ok := m.list.SelectedItem().(item) - if ok { - switch string(i) { - case "New Connection": - m.currentView = NEW_CONNECTION - m.newConnectionModel = - InitialNewConnectionModel() - case "Edit Connection": - m.currentView = EDIT_CONNECTION - case "Join Existing": - m.currentView = JOIN_EXISTING - m.existingConnections = NewExistingConnectionsModel() - } - } - return m, nil - } - } - - var cmd tea.Cmd - m.list, cmd = m.list.Update(msg) - return m, cmd -} - -func (m model) Init() tea.Cmd { - return nil -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - - switch m.currentView { - case NEW_CONNECTION: - m.newConnectionModel, cmd = m.newConnectionModel.Update(msg) - if m.newConnectionModel.connection.status == CONNECTED { - m.currentView = DATABASE_VIEW - m.currentConnection = m.newConnectionModel.connection - m.openDatabase = NewOpenDatabase(m.currentConnection) - - SaveConnectionInKeyring(m.currentConnection) - } - - if m.newConnectionModel.action == CANCEL { - m.currentView = DEFAULT - } - - case DATABASE_VIEW: - m.openDatabase, cmd = m.openDatabase.Update(msg) - if m.openDatabase.viewMode == QUIT { - m.currentView = DEFAULT - m.openDatabase = OpenDatabase{} - } - - case JOIN_EXISTING: - m.existingConnections, cmd = m.existingConnections.Update(msg) - if m.existingConnections.selectedConnection != nil { - m.currentView = DATABASE_VIEW - m.currentConnection = *m.existingConnections.selectedConnection - m.openDatabase = NewOpenDatabase(m.currentConnection) - } - - if m.existingConnections.back { - m.currentView = DEFAULT - } - - case DEFAULT, EDIT_CONNECTION: - m, cmd = m.updateEvents(msg) - } - - return m, cmd -} - -func (m model) View() string { - switch m.currentView { - case NEW_CONNECTION: - return quitTextStyle.Render(m.newConnectionModel.View()) - case EDIT_CONNECTION: - return quitTextStyle.Render("Edit Connection") - case JOIN_EXISTING: - return quitTextStyle.Render(m.existingConnections.View()) - case DATABASE_VIEW: - return quitTextStyle.Render(m.openDatabase.View()) - default: - return "\n" + m.list.View() - } -} +// type model struct { +// list list.Model +// newConnectionModel NewConnectionModel +// currentView CurrentView +// currentConnection Connection +// openDatabase OpenDatabase +// existingConnections ExistingConnectionsModel +// } + +// func (m model) updateEvents(msg tea.Msg) (model, tea.Cmd) { +// switch msg := msg.(type) { +// case tea.WindowSizeMsg: +// m.list.SetWidth(msg.Width) +// width = msg.Width +// height = msg.Height +// return m, nil + +// case tea.KeyMsg: +// switch keypress := msg.String(); keypress { +// case "q", "ctrl+c": +// return m, tea.Quit + +// case "enter": +// i, ok := m.list.SelectedItem().(item) +// if ok { +// switch string(i) { +// case "New Connection": +// m.currentView = NEW_CONNECTION +// m.newConnectionModel = +// InitialNewConnectionModel() +// case "Edit Connection": +// m.currentView = EDIT_CONNECTION +// case "Join Existing": +// m.currentView = JOIN_EXISTING +// m.existingConnections = NewExistingConnectionsModel() +// } +// } +// return m, nil +// } +// } + +// var cmd tea.Cmd +// m.list, cmd = m.list.Update(msg) +// return m, cmd +// } + +// func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +// var cmd tea.Cmd + +// switch m.currentView { +// case NEW_CONNECTION: +// m.newConnectionModel, cmd = m.newConnectionModel.Update(msg) +// if m.newConnectionModel.connection.status == CONNECTED { +// m.currentView = DATABASE_VIEW +// m.currentConnection = m.newConnectionModel.connection +// m.openDatabase = NewOpenDatabase(m.currentConnection) + +// SaveConnectionInKeyring(m.currentConnection) +// } + +// if m.newConnectionModel.action == CANCEL { +// m.currentView = DEFAULT +// } + +// case DATABASE_VIEW: +// m.openDatabase, cmd = m.openDatabase.Update(msg) +// if m.openDatabase.viewMode == QUIT { +// m.currentView = DEFAULT +// m.openDatabase = OpenDatabase{} +// } + +// case JOIN_EXISTING: +// m.existingConnections, cmd = m.existingConnections.Update(msg) +// if m.existingConnections.selectedConnection != nil { +// m.currentView = DATABASE_VIEW +// m.currentConnection = *m.existingConnections.selectedConnection +// m.openDatabase = NewOpenDatabase(m.currentConnection) +// } + +// if m.existingConnections.back { +// m.currentView = DEFAULT +// } + +// case DEFAULT, EDIT_CONNECTION: +// m, cmd = m.updateEvents(msg) +// } + +// return m, cmd +// } type HotKey struct { desc string @@ -251,7 +177,7 @@ func currentConnectionInfo() *tview.List { AddItem("Name: ", "", 0, nil). AddItem("Host: ", "", 0, nil). AddItem("PORT: ", "", 0, nil). - AddItem("USER: ", "Press to exit", 0, nil). + AddItem("USER: ", "", 0, nil). AddItem("Database: ", "", 0, nil) return list @@ -430,6 +356,7 @@ func main() { hotkeyView := NewHotkeys(). AddHotKey("New Connection", 'n'). + AddHotKey("Edit Connection", 'e'). AddHotKey("Quit", 'q') header := header(hotkeyView) @@ -475,9 +402,7 @@ func main() { dbContent := newContentBox(db.openTable.name, dbTable) contentPages.AddAndSwitchToPage(DATABASE_VIEW, dbContent, true) } - default: - } return event @@ -487,24 +412,4 @@ func main() { if err := app.SetRoot(mainPages, true).SetFocus(contentPages).Run(); err != nil { panic(err) } - - // items := []list.Item{ - // item("New Connection"), - // item("Edit Connection"), - // item("Join Existing"), - // } - - // l := list.New(items, itemDelegate{}, defaultWidth, listHeight) - // l.Title = "Welcome to TermTable" - // l.SetShowStatusBar(false) - // l.SetFilteringEnabled(false) - // l.Styles.Title = titleStyle - // l.Styles.PaginationStyle = paginationStyle - // l.Styles.HelpStyle = helpStyle - - // m := model{list: l, currentView: DEFAULT} - - // if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil { - // log.Fatal("Error running program:", err) - // } } diff --git a/new_connection.go b/new_connection.go index 99ab8ab..48d197e 100644 --- a/new_connection.go +++ b/new_connection.go @@ -1,14 +1,5 @@ package main -import ( - "fmt" - "strings" - - "github.com/charmbracelet/bubbles/cursor" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" -) - type Action string const ( @@ -25,218 +16,41 @@ const ( NA TestStatus = "NA" ) -var ( - focusedButton = focusedStyle.Copy().Render("[ Submit ]") - blurredButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Submit")) - - focusedTestButton = focusedStyle.Copy().Render("[ Test ]") - blurredTestButton = fmt.Sprintf("[ %s ]", blurredStyle.Render("Test")) - errorTestButton = fmt.Sprintf("[ %s ]", errorStyle.Render("Test")) - successTestButton = fmt.Sprintf("[ %s ]", successStyle.Render("Test")) -) - type NewConnectionModel struct { - focusIndex int - inputs []textinput.Model - cursorMode cursor.Mode connection Connection testStatus TestStatus action Action } -func InitialNewConnectionModel() NewConnectionModel { - var newConnectionInputs = []string{ - "Host", - "Port", - "User", - "Pass", - "Database", - "Name", - } - m := NewConnectionModel{ - inputs: make([]textinput.Model, len(newConnectionInputs)), - action: SUBMIT, - testStatus: NA, - } - - var t textinput.Model - for i, value := range newConnectionInputs { - t = textinput.New() - t.Cursor.Style = cursorStyle - t.CharLimit = 32 - t.Placeholder = value - - if i == 0 { - t.Focus() - t.PromptStyle = focusedItemStyle - t.TextStyle = focusedItemStyle - } - - if value == "Pass" { - t.EchoMode = textinput.EchoPassword - t.EchoCharacter = '•' - } - - m.inputs[i] = t - } - - return m -} - -func (m NewConnectionModel) Init() tea.Cmd { - return textinput.Blink -} - -func (m NewConnectionModel) Update(msg tea.Msg) (NewConnectionModel, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "ctrl+c", "esc": - m.action = CANCEL - return m, nil - - // Change cursor mode - case "ctrl+r": - m.cursorMode++ - if m.cursorMode > cursor.CursorHide { - m.cursorMode = cursor.CursorBlink - } - cmds := make([]tea.Cmd, len(m.inputs)) - for i := range m.inputs { - cmds[i] = m.inputs[i].Cursor.SetMode(m.cursorMode) - } - return m, tea.Batch(cmds...) - - // Handle button actions - case "left", "right": - if m.focusIndex == len(m.inputs) { - if m.action == SUBMIT { - m.action = TEST - } else { - m.action = SUBMIT - } - } - - if m.testStatus != NA { - m.testStatus = NA - } - - case "enter": - if m.focusIndex == len(m.inputs) { - conn := Connection{ - Host: m.inputs[0].Value(), - Port: m.inputs[1].Value(), - User: m.inputs[2].Value(), - Password: m.inputs[3].Value(), - Database: m.inputs[4].Value(), - Name: m.inputs[5].Value(), - status: DISCONNECTED, - } - - switch m.action { - case SUBMIT: - if conn.TestConnection() == PASSED { - m.connection = conn - } - case TEST: - if m.testStatus == NA { - m.testStatus = conn.TestConnection() - } - - } - - } - - return m.updateInputStates() - - // Set focus to next input - case "tab", "shift+tab", "up", "down": - s := msg.String() - - // Cycle indexes - if s == "up" || s == "shift+tab" { - m.focusIndex-- - } else { - m.focusIndex++ - } - - if m.focusIndex > len(m.inputs) { - m.focusIndex = 0 - } else if m.focusIndex < 0 { - m.focusIndex = len(m.inputs) - } - - return m.updateInputStates() - } - } - - // Handle character input and blinking - cmd := m.updateInputs(msg) - - return m, cmd -} - -func (m *NewConnectionModel) updateInputStates() (NewConnectionModel, tea.Cmd) { - cmds := make([]tea.Cmd, len(m.inputs)) - for i := 0; i <= len(m.inputs)-1; i++ { - if i == m.focusIndex { - // Set focused state - cmds[i] = m.inputs[i].Focus() - m.inputs[i].PromptStyle = focusedItemStyle - m.inputs[i].TextStyle = focusedItemStyle - continue - } - // Remove focused state - m.inputs[i].Blur() - m.inputs[i].PromptStyle = noStyle - m.inputs[i].TextStyle = noStyle - } - - return *m, tea.Batch(cmds...) -} - -func (m *NewConnectionModel) updateInputs(msg tea.Msg) tea.Cmd { - cmds := make([]tea.Cmd, len(m.inputs)) - - // Only text inputs with Focus() set will respond, so it's safe to simply - // update all of them here without any further logic. - for i := range m.inputs { - m.inputs[i], cmds[i] = m.inputs[i].Update(msg) - } - - return tea.Batch(cmds...) -} - -func (m NewConnectionModel) View() string { - var b strings.Builder - - b.WriteString("New Connection\n\n") - - for i := range m.inputs { - b.WriteString(m.inputs[i].View()) - if i < len(m.inputs)-1 { - b.WriteRune('\n') - } - } - - submitButton := &blurredButton - testButton := &blurredTestButton - if m.focusIndex == len(m.inputs) { - switch m.action { - case SUBMIT: - submitButton = &focusedButton - case TEST: - switch m.testStatus { - case PASSED: - testButton = &successTestButton - case FAILED: - testButton = &errorTestButton - case NA: - testButton = &focusedTestButton - } - } - } - fmt.Fprintf(&b, "\n\n%s%s\n\n", *submitButton, *testButton) - - return paginationStyle.Render(b.String()) -} +// func (m NewConnectionModel) Update(msg tea.Msg) (NewConnectionModel, tea.Cmd) { +// switch msg := msg.(type) { +// case tea.KeyMsg: +// switch msg.String() { + +// case "enter": +// if m.focusIndex == len(m.inputs) { +// conn := Connection{ +// Host: m.inputs[0].Value(), +// Port: m.inputs[1].Value(), +// User: m.inputs[2].Value(), +// Password: m.inputs[3].Value(), +// Database: m.inputs[4].Value(), +// Name: m.inputs[5].Value(), +// status: DISCONNECTED, +// } + +// switch m.action { +// case SUBMIT: +// if conn.TestConnection() == PASSED { +// m.connection = conn +// } +// case TEST: +// if m.testStatus == NA { +// m.testStatus = conn.TestConnection() +// } + +// } + +// } + +// } diff --git a/open_connection.go b/open_connection.go index 12aa21c..4d1c643 100644 --- a/open_connection.go +++ b/open_connection.go @@ -1,10 +1,7 @@ package main import ( - "fmt" "log" - - tea "github.com/charmbracelet/bubbletea" ) type ViewMode string @@ -48,51 +45,3 @@ func (db *OpenDatabase) setOpenTable() { db.openTable = table } - -func (db OpenDatabase) Init() tea.Cmd { - return nil -} - -func (db OpenDatabase) Update(msg tea.Msg) (OpenDatabase, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "q", "ctrl+c": - db.viewMode = QUIT - return db, nil - - case "left", "right": - switch db.viewMode { - case TABLES: - db.viewMode = OPEN - case OPEN: - db.viewMode = TABLES - } - } - } - - var cmd tea.Cmd - - return db, cmd -} - -func (db OpenDatabase) View() string { - s := fmt.Sprintf("%s / %s\n\n", db.params.Name, db.params.Database) - - // tableLabels := db.tables.View() - - // db.setOpenTable() - // openTable := db.selectedTable.View() - - // if db.viewMode == TABLES { - // s += lipgloss.JoinHorizontal(lipgloss.Top, - // focusedModelSideBarStyle.Render(tableLabels), - // modelStyle.Render(openTable)) - // } else { - // s += lipgloss.JoinHorizontal(lipgloss.Top, - // modelStyle.Render(tableLabels), - // focusedModelStyle.Render(openTable)) - // } - - return paginationStyle.Render(s) -} From a3d437e3fe9d3dc6bdca5576bc6917da9f9ab5dd Mon Sep 17 00:00:00 2001 From: Rob Date: Sun, 2 Feb 2025 23:05:48 +0000 Subject: [PATCH 13/33] WIP improved input caputre with modals open and prepared the test connection button --- main.go | 88 ++++++++++++++++++++++++++++++++++------------- new_connection.go | 1 - 2 files changed, 65 insertions(+), 24 deletions(-) diff --git a/main.go b/main.go index 5b8bbe6..ad8b874 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strconv" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" @@ -183,33 +184,44 @@ func currentConnectionInfo() *tview.List { return list } -func header(hotkeyView *HotKeys) *tview.Flex { +func headerPanel(hotkeys *tview.Pages) *tview.Flex { connection := currentConnectionInfo() appName := tview.NewTextView().SetText(APP_NAME).SetTextAlign(tview.AlignRight) headerView := tview.NewFlex(). AddItem(connection, 0, 1, false). - AddItem(hotkeyView, 0, 1, false). + AddItem(hotkeys, 0, 1, false). AddItem(appName, 0, 1, false) headerView.SetBorderPadding(0, 0, 1, 1) return headerView } -func newConnectionForm(app *tview.Application) *tview.Flex { +func newConnectionForm(conn Connection, escapeFunc func()) *tview.Flex { form := tview.NewForm(). - AddInputField("Name", "", 26, nil, nil). - AddInputField("Host", "", 26, nil, nil). - AddInputField("Port", "", 26, nil, nil). - AddInputField("User", "", 26, nil, nil). - AddPasswordField("Password", "", 26, '*', nil). - AddInputField("Database", "", 26, nil, nil). + AddInputField("Name", conn.Name, 26, nil, func(text string) { conn.Name = text }). + AddInputField("Host", conn.Host, 26, nil, func(text string) { conn.Host = text }). + AddInputField("Port", conn.Port, 26, func(textToCheck string, lastChar rune) bool { + _, err := strconv.Atoi(textToCheck) + return err == nil + }, func(text string) { conn.Port = text }). + AddInputField("User", conn.User, 26, nil, func(text string) { conn.User = text }). + AddPasswordField("Password", conn.Password, 26, '*', func(text string) { conn.Password = text }). + AddInputField("Database", conn.Database, 26, nil, func(text string) { conn.Database = text }). AddButton("Save", nil). - AddButton("Test", nil). + AddButton("Test", func() { + testResult := conn.TestConnection() + switch testResult { + case PASSED: + // TODO show modal here for passed and failed jobs + case FAILED: + } + }). AddButton("Connect", func() { - app.Stop() + // TODO save and connect }) + form.SetBorder(true) form.SetButtonsAlign(tview.AlignRight) @@ -221,14 +233,21 @@ func newConnectionForm(app *tview.Application) *tview.Flex { AddItem(nil, 0, 1, false), 0, 2, true). AddItem(nil, 0, 3, false) + modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyESC { + escapeFunc() + } + return event + }) + return modal } const ( - MAIN_PAGE = "main" - NEW_CONNETION_FORM = "newConnection" - SAVED_CONNECTIONS = "savedConnections" - DATABASE_VIEW = "databaseView" + MAIN_PAGE = "main" + NEW_CONNECTION_FORM = "newConnection" + SAVED_CONNECTIONS = "savedConnections" + DATABASE_VIEW = "databaseView" ) var CONNECTION_TABLE_HEADERS = []string{"NAME", "HOST", "PORT", "USER", "DATABASE"} @@ -358,7 +377,8 @@ func main() { AddHotKey("New Connection", 'n'). AddHotKey("Edit Connection", 'e'). AddHotKey("Quit", 'q') - header := header(hotkeyView) + hotkeyPages := tview.NewPages().AddAndSwitchToPage("connectionHotkeys", hotkeyView, true) + header := headerPanel(hotkeyPages) connectionsTable := newConnectionsTable(CONNECTION_TABLE_HEADERS) connectionsView := newContentBox("Connections", connectionsTable) @@ -372,25 +392,47 @@ func main() { app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { pageName, _ := mainPages.GetFrontPage() - contentName, _ := mainView.content.GetFrontPage() + // contentName, _ := mainView.content.GetFrontPage() + currentHotkeys, _ := hotkeyPages.GetFrontPage() + + switch currentHotkeys { + case "connectionHotkeys": + if pageName == NEW_CONNECTION_FORM { + return event + } - switch contentName { - case SAVED_CONNECTIONS: switch event.Rune() { case 'q': app.Stop() case 'n': - if pageName == NEW_CONNETION_FORM { + if pageName == NEW_CONNECTION_FORM { return event } - addConnectionForm := newConnectionForm(app) - mainPages.AddPage(NEW_CONNETION_FORM, addConnectionForm, true, true) + + addConnectionForm := newConnectionForm(Connection{}, func() { + mainPages.RemovePage(NEW_CONNECTION_FORM) + app.SetFocus(contentPages) + }) + mainPages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) + return nil + case 'e': + connection := connectionsTable.getConnection() + if connection == nil { + return event + } + + addConnectionForm := newConnectionForm(*connection, func() { + mainPages.RemovePage(NEW_CONNECTION_FORM) + app.SetFocus(contentPages) + }) + mainPages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) return nil } switch event.Key() { case tcell.KeyESC: - mainPages.RemovePage(NEW_CONNETION_FORM) + mainPages.RemovePage(NEW_CONNECTION_FORM) + app.SetFocus(contentPages) case tcell.KeyEnter: connection := connectionsTable.getConnection() if connection == nil { diff --git a/new_connection.go b/new_connection.go index 48d197e..eed3389 100644 --- a/new_connection.go +++ b/new_connection.go @@ -13,7 +13,6 @@ type TestStatus string const ( PASSED TestStatus = "PASSED" FAILED TestStatus = "FAILED" - NA TestStatus = "NA" ) type NewConnectionModel struct { From 33ab76f1f9723e6ba5acd257df1d93a0bf802411 Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 5 Feb 2025 23:22:41 +0000 Subject: [PATCH 14/33] Allow current connection to be shown and to exit the connected to db --- main.go | 51 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/main.go b/main.go index ad8b874..f4fca05 100644 --- a/main.go +++ b/main.go @@ -26,11 +26,6 @@ const ( // DATABASE_VIEW CurrentView = "DATABASE_VIEW" ) -const ( - defaultWidth = 20 - listHeight = 14 -) - // Primary ansi colours const ( WHITE = "15" @@ -171,21 +166,21 @@ func (r *HotKeys) Draw(screen tcell.Screen) { } } -func currentConnectionInfo() *tview.List { +func currentConnectionInfo(conn Connection) *tview.List { list := tview.NewList(). ShowSecondaryText(false). SetSelectedFocusOnly(true). - AddItem("Name: ", "", 0, nil). - AddItem("Host: ", "", 0, nil). - AddItem("PORT: ", "", 0, nil). - AddItem("USER: ", "", 0, nil). - AddItem("Database: ", "", 0, nil) + AddItem(fmt.Sprintf("Name: %s", conn.Name), "", 0, nil). + AddItem(fmt.Sprintf("Host: %s", conn.Host), "", 0, nil). + AddItem(fmt.Sprintf("PORT: %s", conn.Port), "", 0, nil). + AddItem(fmt.Sprintf("USER: %s", conn.User), "", 0, nil). + AddItem(fmt.Sprintf("Database: %s", conn.Database), "", 0, nil) return list } -func headerPanel(hotkeys *tview.Pages) *tview.Flex { - connection := currentConnectionInfo() +func headerPanel(conn Connection, hotkeys *tview.Pages) *tview.Flex { + connection := currentConnectionInfo(conn) appName := tview.NewTextView().SetText(APP_NAME).SetTextAlign(tview.AlignRight) @@ -362,8 +357,8 @@ type Layout struct { content *tview.Pages } -func newLayout(header *tview.Flex, content *tview.Pages) Layout { - view := tview.NewFlex().SetDirection(tview.FlexRow). +func newLayout(direction int, header *tview.Flex, content *tview.Pages) Layout { + view := tview.NewFlex().SetDirection(direction). AddItem(header, 0, 1, false). AddItem(content, 0, 6, false) @@ -378,22 +373,22 @@ func main() { AddHotKey("Edit Connection", 'e'). AddHotKey("Quit", 'q') hotkeyPages := tview.NewPages().AddAndSwitchToPage("connectionHotkeys", hotkeyView, true) - header := headerPanel(hotkeyPages) + header := headerPanel(Connection{}, hotkeyPages) connectionsTable := newConnectionsTable(CONNECTION_TABLE_HEADERS) connectionsView := newContentBox("Connections", connectionsTable) contentPages := tview.NewPages(). AddAndSwitchToPage(SAVED_CONNECTIONS, connectionsView, true) - mainView := newLayout(header, contentPages) + mainView := newLayout(tview.FlexRow, header, contentPages) mainPages := tview.NewPages(). AddAndSwitchToPage(MAIN_PAGE, mainView, true) app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { pageName, _ := mainPages.GetFrontPage() - // contentName, _ := mainView.content.GetFrontPage() currentHotkeys, _ := hotkeyPages.GetFrontPage() + contentView, _ := contentPages.GetFrontPage() switch currentHotkeys { case "connectionHotkeys": @@ -431,9 +426,23 @@ func main() { switch event.Key() { case tcell.KeyESC: + if contentView == DATABASE_VIEW { + + newHeader := newLayout(tview.FlexRow, headerPanel(Connection{}, hotkeyPages), contentPages) + mainPages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) + + contentPages.RemovePage(DATABASE_VIEW) + app.SetFocus(contentPages) + return event + } + mainPages.RemovePage(NEW_CONNECTION_FORM) app.SetFocus(contentPages) case tcell.KeyEnter: + if contentView == DATABASE_VIEW { + return event + } + connection := connectionsTable.getConnection() if connection == nil { return event @@ -442,7 +451,12 @@ func main() { db := NewOpenDatabase(*connection) dbTable := newDbTable(db.openTable) dbContent := newContentBox(db.openTable.name, dbTable) + + newHeader := newLayout(tview.FlexRow, headerPanel(*connection, hotkeyPages), contentPages) + mainPages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) + contentPages.AddAndSwitchToPage(DATABASE_VIEW, dbContent, true) + app.SetFocus(contentPages) } default: } @@ -450,7 +464,6 @@ func main() { return event }) - // TODO somthing with focus is causing the extra border outline if err := app.SetRoot(mainPages, true).SetFocus(contentPages).Run(); err != nil { panic(err) } From 3bf943abb22d1575149adcc53043899a1130bc95 Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 5 Feb 2025 23:35:54 +0000 Subject: [PATCH 15/33] Added test and save logic to the new connection form --- main.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index f4fca05..ee998f9 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "log" "strconv" "github.com/gdamore/tcell/v2" @@ -204,7 +205,16 @@ func newConnectionForm(conn Connection, escapeFunc func()) *tview.Flex { AddInputField("User", conn.User, 26, nil, func(text string) { conn.User = text }). AddPasswordField("Password", conn.Password, 26, '*', func(text string) { conn.Password = text }). AddInputField("Database", conn.Database, 26, nil, func(text string) { conn.Database = text }). - AddButton("Save", nil). + AddButton("Save", func() { + testResult := conn.TestConnection() + if testResult == FAILED { + log.Fatal("Could not save connection because connection could not be established") + } + + SaveConnectionInKeyring(conn) + + // TODO remove new connection form and refresh connections view + }). AddButton("Test", func() { testResult := conn.TestConnection() switch testResult { @@ -214,6 +224,7 @@ func newConnectionForm(conn Connection, escapeFunc func()) *tview.Flex { } }). AddButton("Connect", func() { + // TODO save and connect }) From 3edc21325fe0913c20a4e2bb50b2cfe7c21392eb Mon Sep 17 00:00:00 2001 From: Rob Date: Sat, 8 Feb 2025 19:01:34 +0000 Subject: [PATCH 16/33] Refactored to an app struct --- main.go | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index ee998f9..a02721e 100644 --- a/main.go +++ b/main.go @@ -376,7 +376,14 @@ func newLayout(direction int, header *tview.Flex, content *tview.Pages) Layout { return Layout{view, header, content} } -func main() { +type App struct { + *tview.Application + conn Connection + pages *tview.Pages + // views +} + +func newApp() App { app := tview.NewApplication() hotkeyView := NewHotkeys(). @@ -393,11 +400,13 @@ func main() { AddAndSwitchToPage(SAVED_CONNECTIONS, connectionsView, true) mainView := newLayout(tview.FlexRow, header, contentPages) - mainPages := tview.NewPages(). + pages := tview.NewPages(). AddAndSwitchToPage(MAIN_PAGE, mainView, true) + app.SetRoot(pages, true).SetFocus(contentPages) + app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - pageName, _ := mainPages.GetFrontPage() + pageName, _ := pages.GetFrontPage() currentHotkeys, _ := hotkeyPages.GetFrontPage() contentView, _ := contentPages.GetFrontPage() @@ -416,10 +425,10 @@ func main() { } addConnectionForm := newConnectionForm(Connection{}, func() { - mainPages.RemovePage(NEW_CONNECTION_FORM) + pages.RemovePage(NEW_CONNECTION_FORM) app.SetFocus(contentPages) }) - mainPages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) + pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) return nil case 'e': connection := connectionsTable.getConnection() @@ -428,10 +437,10 @@ func main() { } addConnectionForm := newConnectionForm(*connection, func() { - mainPages.RemovePage(NEW_CONNECTION_FORM) + pages.RemovePage(NEW_CONNECTION_FORM) app.SetFocus(contentPages) }) - mainPages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) + pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) return nil } @@ -440,14 +449,14 @@ func main() { if contentView == DATABASE_VIEW { newHeader := newLayout(tview.FlexRow, headerPanel(Connection{}, hotkeyPages), contentPages) - mainPages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) + pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) contentPages.RemovePage(DATABASE_VIEW) app.SetFocus(contentPages) return event } - mainPages.RemovePage(NEW_CONNECTION_FORM) + pages.RemovePage(NEW_CONNECTION_FORM) app.SetFocus(contentPages) case tcell.KeyEnter: if contentView == DATABASE_VIEW { @@ -464,7 +473,7 @@ func main() { dbContent := newContentBox(db.openTable.name, dbTable) newHeader := newLayout(tview.FlexRow, headerPanel(*connection, hotkeyPages), contentPages) - mainPages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) + pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) contentPages.AddAndSwitchToPage(DATABASE_VIEW, dbContent, true) app.SetFocus(contentPages) @@ -475,7 +484,13 @@ func main() { return event }) - if err := app.SetRoot(mainPages, true).SetFocus(contentPages).Run(); err != nil { + return App{app, Connection{}, pages} +} + +func main() { + app := newApp() + + if err := app.Run(); err != nil { panic(err) } } From d8987f3b4347806458c5d6c1cc873abe987c28a3 Mon Sep 17 00:00:00 2001 From: Rob Date: Sat, 8 Feb 2025 19:03:53 +0000 Subject: [PATCH 17/33] Run golangci-lint --- db.go | 13 +++++++------ keyring.go | 19 ++++++++----------- main.go | 24 ++++++++++++++---------- open_connection.go | 2 +- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/db.go b/db.go index 675ba80..164e0d7 100644 --- a/db.go +++ b/db.go @@ -36,39 +36,41 @@ func (params Connection) ConnectionString() string { func (params *Connection) TestConnection() TestStatus { connectionString := params.ConnectionString() conn, err := pgx.Connect(context.Background(), connectionString) - if err != nil { params.status = DISCONNECTED + return FAILED } conn.Close(context.Background()) params.status = CONNECTED + return PASSED } func (parmas Connection) GetTableNames() []string { connectionString := parmas.ConnectionString() conn, err := pgx.Connect(context.Background(), connectionString) - if err != nil { return nil } rows, err := conn.Query(context.Background(), "SELECT table_name FROM information_schema.tables WHERE table_schema='public'") - if err != nil { return nil } var tableNames []string + for rows.Next() { var tableName string + err = rows.Scan(&tableName) if err != nil { return nil } + tableNames = append(tableNames, tableName) } @@ -86,13 +88,11 @@ type Table struct { func (params Connection) SelectAll(table string) (Table, error) { connectionString := params.ConnectionString() conn, err := pgx.Connect(context.Background(), connectionString) - if err != nil { return Table{}, err } - rows, err := conn.Query(context.Background(), fmt.Sprintf("SELECT * FROM %s", table)) - + rows, err := conn.Query(context.Background(), "SELECT * FROM "+table) if err != nil { return Table{}, err } @@ -101,6 +101,7 @@ func (params Connection) SelectAll(table string) (Table, error) { fieldDescriptions := rows.FieldDescriptions() tableData.fields = make([]string, len(fieldDescriptions)) + for i, field := range fieldDescriptions { tableData.fields[i] = field.Name } diff --git a/keyring.go b/keyring.go index bc66d8c..8599f93 100644 --- a/keyring.go +++ b/keyring.go @@ -20,15 +20,14 @@ const ( func getAndOrCreateLocalDb() (string, error) { homeDir, err := os.UserHomeDir() - if err != nil { return "", errors.New("Could not get home directory") } - localDb := fmt.Sprintf("%s/.termtable/connections.db", homeDir) + localDb := homeDir + "/.termtable/connections.db" if _, err := os.Stat(localDb); os.IsNotExist(err) { - err := os.Mkdir(filepath.Dir(localDb), 0755) + err := os.Mkdir(filepath.Dir(localDb), 0o755) if err != nil { log.Fatal("Could not create directory to store local db: ", err) } @@ -43,6 +42,7 @@ func createBucket(db *bolt.DB) error { if err != nil { return err } + defer func() { err = tx.Rollback() }() @@ -63,12 +63,11 @@ func createBucket(db *bolt.DB) error { func updateLocalDbConn(conn Connection) error { localDb, err := getAndOrCreateLocalDb() - if err != nil { return err } - db, err := bolt.Open(localDb, 0600, nil) + db, err := bolt.Open(localDb, 0o600, nil) if err != nil { log.Fatal(err) } @@ -82,6 +81,7 @@ func updateLocalDbConn(conn Connection) error { err = db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(LOCAL_BUCKET_NAME)) err := b.Put([]byte(conn.Name), []byte(fmt.Sprintf("%s:%s:%s", conn.Host, conn.Port, conn.Database))) + return err }) @@ -90,12 +90,11 @@ func updateLocalDbConn(conn Connection) error { func deleteLocalDbConn(name string) error { localDb, err := getAndOrCreateLocalDb() - if err != nil { return err } - db, err := bolt.Open(localDb, 0600, nil) + db, err := bolt.Open(localDb, 0o600, nil) if err != nil { log.Fatal(err) } @@ -109,6 +108,7 @@ func deleteLocalDbConn(name string) error { err = db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(LOCAL_BUCKET_NAME)) err := b.Delete([]byte(name)) + return err }) @@ -124,7 +124,7 @@ func listLocalDbConn() (map[string]string, error) { return connections, err } - db, err := bolt.Open(localDb, 0600, nil) + db, err := bolt.Open(localDb, 0o600, nil) if err != nil { log.Fatal(err) } @@ -170,14 +170,12 @@ func SaveConnectionInKeyring(conn Connection) { // Save keyring part password := createKeyringPassword(conn.User, conn.Password) err := keyring.Set(SERVICE, conn.Name, password) - if err != nil { log.Fatal("Could not save db credentials in keyring: ", err) } // Save rest to local storage err = updateLocalDbConn(conn) - if err != nil { log.Fatal("Could not set keyring info into local db: ", err) } @@ -185,7 +183,6 @@ func SaveConnectionInKeyring(conn Connection) { func GetConnectionFromKeyring(name string) (string, string, error) { password, err := keyring.Get(SERVICE, name) - if err != nil { log.Fatal("Could not get credentials for connection: ", err) } diff --git a/main.go b/main.go index a02721e..d90a3dd 100644 --- a/main.go +++ b/main.go @@ -24,10 +24,10 @@ const ( NEW_CONNECTION CurrentView = "NEW_CONNECTION" EDIT_CONNECTION CurrentView = "EDIT_CONNECTION" JOIN_EXISTING CurrentView = "JOIN_EXISTING" - // DATABASE_VIEW CurrentView = "DATABASE_VIEW" + // DATABASE_VIEW CurrentView = "DATABASE_VIEW". ) -// Primary ansi colours +// Primary ansi colours. const ( WHITE = "15" RED = "1" @@ -142,6 +142,7 @@ type HotKeys struct { func NewHotkeys() *HotKeys { list := tview.NewList(). ShowSecondaryText(false).SetSelectedFocusOnly(true) + return &HotKeys{ List: list, values: []HotKey{}, @@ -150,6 +151,7 @@ func NewHotkeys() *HotKeys { func (r *HotKeys) AddHotKey(desc string, shortcut rune) *HotKeys { r.values = append(r.values, HotKey{desc, shortcut}) + return r } @@ -171,11 +173,11 @@ func currentConnectionInfo(conn Connection) *tview.List { list := tview.NewList(). ShowSecondaryText(false). SetSelectedFocusOnly(true). - AddItem(fmt.Sprintf("Name: %s", conn.Name), "", 0, nil). - AddItem(fmt.Sprintf("Host: %s", conn.Host), "", 0, nil). - AddItem(fmt.Sprintf("PORT: %s", conn.Port), "", 0, nil). - AddItem(fmt.Sprintf("USER: %s", conn.User), "", 0, nil). - AddItem(fmt.Sprintf("Database: %s", conn.Database), "", 0, nil) + AddItem("Name: "+conn.Name, "", 0, nil). + AddItem("Host: "+conn.Host, "", 0, nil). + AddItem("PORT: "+conn.Port, "", 0, nil). + AddItem("USER: "+conn.User, "", 0, nil). + AddItem("Database: "+conn.Database, "", 0, nil) return list } @@ -200,6 +202,7 @@ func newConnectionForm(conn Connection, escapeFunc func()) *tview.Flex { AddInputField("Host", conn.Host, 26, nil, func(text string) { conn.Host = text }). AddInputField("Port", conn.Port, 26, func(textToCheck string, lastChar rune) bool { _, err := strconv.Atoi(textToCheck) + return err == nil }, func(text string) { conn.Port = text }). AddInputField("User", conn.User, 26, nil, func(text string) { conn.User = text }). @@ -224,7 +227,6 @@ func newConnectionForm(conn Connection, escapeFunc func()) *tview.Flex { } }). AddButton("Connect", func() { - // TODO save and connect }) @@ -243,6 +245,7 @@ func newConnectionForm(conn Connection, escapeFunc func()) *tview.Flex { if event.Key() == tcell.KeyESC { escapeFunc() } + return event }) @@ -282,7 +285,6 @@ func newConnectionsTable(columns []string) *DisplayTable { func (t *DisplayTable) getConnections() { connections, err := ListConnections() - if err != nil { return } @@ -429,6 +431,7 @@ func newApp() App { app.SetFocus(contentPages) }) pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) + return nil case 'e': connection := connectionsTable.getConnection() @@ -441,18 +444,19 @@ func newApp() App { app.SetFocus(contentPages) }) pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) + return nil } switch event.Key() { case tcell.KeyESC: if contentView == DATABASE_VIEW { - newHeader := newLayout(tview.FlexRow, headerPanel(Connection{}, hotkeyPages), contentPages) pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) contentPages.RemovePage(DATABASE_VIEW) app.SetFocus(contentPages) + return event } diff --git a/open_connection.go b/open_connection.go index 4d1c643..241e811 100644 --- a/open_connection.go +++ b/open_connection.go @@ -37,9 +37,9 @@ func (db *OpenDatabase) setOpenTable() { tableName := db.tables[0] table, err := db.params.SelectAll(tableName) - if err != nil { log.Fatal("Could not connect to db", err) + return } From b4cb935c1b618c030dc733cfe88f6ea11445bb4f Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 27 Feb 2025 22:40:21 +0000 Subject: [PATCH 18/33] Moved from bbolt to sqlite --- go.mod | 13 +++++- go.sum | 52 +++++++++++++++++++++--- keyring.go | 113 ++++++++++++++++++++++++----------------------------- main.go | 101 +++++++++++++++++++++++++++++------------------ 4 files changed, 171 insertions(+), 108 deletions(-) diff --git a/go.mod b/go.mod index 51f67c2..85c0967 100644 --- a/go.mod +++ b/go.mod @@ -7,21 +7,30 @@ require ( github.com/jackc/pgx/v5 v5.5.5 github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57 github.com/zalando/go-keyring v0.2.4 - go.etcd.io/bbolt v1.3.9 + modernc.org/sqlite v1.36.0 ) require ( github.com/alessio/shellescape v1.4.1 // indirect github.com/danieljoos/wincred v1.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/crypto v0.17.0 // indirect - golang.org/x/sys v0.17.0 // indirect + golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect + golang.org/x/sys v0.30.0 // indirect golang.org/x/term v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect + modernc.org/libc v1.61.13 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.8.2 // indirect ) diff --git a/go.sum b/go.sum index 22b2a81..eec57ec 100644 --- a/go.sum +++ b/go.sum @@ -5,12 +5,18 @@ github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0S github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQbc= github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= +github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -21,10 +27,16 @@ github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57 h1:LmsF7Fk5jyEDhJk0fYIqdWNuTxSyid2W42A0L2YWjGE= github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -41,14 +53,16 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68= github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= -go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= -go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= 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/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= +golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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= @@ -56,16 +70,18 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -81,8 +97,34 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm 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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= +modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= +modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo= +modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= +modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= +modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw= +modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= +modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= +modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.36.0 h1:EQXNRn4nIS+gfsKeUTymHIz1waxuv5BzU7558dHSfH8= +modernc.org/sqlite v1.36.0/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/keyring.go b/keyring.go index 8599f93..fda7ae3 100644 --- a/keyring.go +++ b/keyring.go @@ -1,6 +1,7 @@ package main import ( + "database/sql" "errors" "fmt" "log" @@ -9,55 +10,51 @@ import ( "strings" "github.com/zalando/go-keyring" - bolt "go.etcd.io/bbolt" + _ "modernc.org/sqlite" ) const ( SERVICE = "termtable-app" - - LOCAL_BUCKET_NAME = "database_connections" ) func getAndOrCreateLocalDb() (string, error) { homeDir, err := os.UserHomeDir() if err != nil { - return "", errors.New("Could not get home directory") + return "", errors.New("could not get home directory") } - localDb := homeDir + "/.termtable/connections.db" + dbDir := filepath.Join(homeDir, ".termtable") + localDb := filepath.Join(dbDir, "termtable.db") - if _, err := os.Stat(localDb); os.IsNotExist(err) { - err := os.Mkdir(filepath.Dir(localDb), 0o755) + if _, err := os.Stat(dbDir); os.IsNotExist(err) { + err := os.MkdirAll(dbDir, 0o755) if err != nil { log.Fatal("Could not create directory to store local db: ", err) } } - return localDb, nil -} - -func createBucket(db *bolt.DB) error { - // Start a writable transaction. - tx, err := db.Begin(true) - if err != nil { - return err - } - - defer func() { - err = tx.Rollback() - }() - - // Use the transaction... - _, err = tx.CreateBucketIfNotExists([]byte(LOCAL_BUCKET_NAME)) - if err != nil { - return err + // Create the database file if it doesn't exist + if _, err := os.Stat(localDb); os.IsNotExist(err) { + file, err := os.Create(localDb) + if err != nil { + return "", fmt.Errorf("could not create database file: %v", err) + } + file.Close() } - // Commit the transaction and check for error. - if err := tx.Commit(); err != nil { - return err - } + return localDb, nil +} +func initDb(db *sql.DB) error { + // Create the table if it doesn't exist + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS database_connections ( + name TEXT PRIMARY KEY, + host TEXT NOT NULL, + port TEXT NOT NULL, + database_name TEXT NOT NULL + ) + `) return err } @@ -67,23 +64,22 @@ func updateLocalDbConn(conn Connection) error { return err } - db, err := bolt.Open(localDb, 0o600, nil) + db, err := sql.Open("sqlite", localDb) if err != nil { log.Fatal(err) } defer db.Close() - err = createBucket(db) + err = initDb(db) if err != nil { return err } - err = db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(LOCAL_BUCKET_NAME)) - err := b.Put([]byte(conn.Name), []byte(fmt.Sprintf("%s:%s:%s", conn.Host, conn.Port, conn.Database))) - - return err - }) + // Insert or replace the connection + _, err = db.Exec( + "INSERT OR REPLACE INTO database_connections (name, host, port, database_name) VALUES (?, ?, ?, ?)", + conn.Name, conn.Host, conn.Port, conn.Database, + ) return err } @@ -94,62 +90,55 @@ func deleteLocalDbConn(name string) error { return err } - db, err := bolt.Open(localDb, 0o600, nil) + db, err := sql.Open("sqlite", localDb) if err != nil { log.Fatal(err) } defer db.Close() - err = createBucket(db) + err = initDb(db) if err != nil { return err } - err = db.Update(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(LOCAL_BUCKET_NAME)) - err := b.Delete([]byte(name)) - - return err - }) - + _, err = db.Exec("DELETE FROM database_connections WHERE name = ?", name) return err } func listLocalDbConn() (map[string]string, error) { - localDb, err := getAndOrCreateLocalDb() - connections := make(map[string]string) + localDb, err := getAndOrCreateLocalDb() if err != nil { return connections, err } - db, err := bolt.Open(localDb, 0o600, nil) + db, err := sql.Open("sqlite", localDb) if err != nil { log.Fatal(err) } defer db.Close() - err = createBucket(db) + err = initDb(db) if err != nil { return connections, err } - err = db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte(LOCAL_BUCKET_NAME)) - - c := b.Cursor() + rows, err := db.Query("SELECT name, host, port, database_name FROM database_connections") + if err != nil { + return connections, err + } + defer rows.Close() - for k, v := c.First(); k != nil; k, v = c.Next() { - key := string(k) - value := string(v) - connections[key] = value + for rows.Next() { + var name, host, port, database string + if err := rows.Scan(&name, &host, &port, &database); err != nil { + log.Fatal(err) } + connections[name] = fmt.Sprintf("%s:%s:%s", host, port, database) + } - return nil - }) - - return connections, err + return connections, rows.Err() } func createKeyringPassword(username string, password string) string { diff --git a/main.go b/main.go index d90a3dd..8c502d1 100644 --- a/main.go +++ b/main.go @@ -196,7 +196,7 @@ func headerPanel(conn Connection, hotkeys *tview.Pages) *tview.Flex { return headerView } -func newConnectionForm(conn Connection, escapeFunc func()) *tview.Flex { +func newConnectionForm(app *App, conn Connection, escapeFunc func()) *tview.Flex { form := tview.NewForm(). AddInputField("Name", conn.Name, 26, nil, func(text string) { conn.Name = text }). AddInputField("Host", conn.Host, 26, nil, func(text string) { conn.Host = text }). @@ -216,6 +216,7 @@ func newConnectionForm(conn Connection, escapeFunc func()) *tview.Flex { SaveConnectionInKeyring(conn) + escapeFunc() // TODO remove new connection form and refresh connections view }). AddButton("Test", func() { @@ -227,7 +228,17 @@ func newConnectionForm(conn Connection, escapeFunc func()) *tview.Flex { } }). AddButton("Connect", func() { - // TODO save and connect + // Test save open + testResult := conn.TestConnection() + if testResult == FAILED { + log.Fatal("Could not connect because connection could not be established") + } + + SaveConnectionInKeyring(conn) + + escapeFunc() + + app.openConnection(conn) }) form.SetBorder(true) @@ -380,9 +391,11 @@ func newLayout(direction int, header *tview.Flex, content *tview.Pages) Layout { type App struct { *tview.Application - conn Connection - pages *tview.Pages - // views + conn Connection + pages *tview.Pages + content *tview.Pages + hotkeys *tview.Pages + connections *DisplayTable } func newApp() App { @@ -392,25 +405,33 @@ func newApp() App { AddHotKey("New Connection", 'n'). AddHotKey("Edit Connection", 'e'). AddHotKey("Quit", 'q') - hotkeyPages := tview.NewPages().AddAndSwitchToPage("connectionHotkeys", hotkeyView, true) - header := headerPanel(Connection{}, hotkeyPages) + hotkeys := tview.NewPages(). + AddAndSwitchToPage("connectionHotkeys", hotkeyView, true) + header := headerPanel(Connection{}, hotkeys) connectionsTable := newConnectionsTable(CONNECTION_TABLE_HEADERS) connectionsView := newContentBox("Connections", connectionsTable) - contentPages := tview.NewPages(). + content := tview.NewPages(). AddAndSwitchToPage(SAVED_CONNECTIONS, connectionsView, true) - mainView := newLayout(tview.FlexRow, header, contentPages) + mainView := newLayout(tview.FlexRow, header, content) pages := tview.NewPages(). AddAndSwitchToPage(MAIN_PAGE, mainView, true) - app.SetRoot(pages, true).SetFocus(contentPages) + app.SetRoot(pages, true).SetFocus(content) + ctx := App{app, Connection{}, pages, content, hotkeys, connectionsTable} + ctx.setInputHandler() + + return ctx +} + +func (app *App) setInputHandler() { app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - pageName, _ := pages.GetFrontPage() - currentHotkeys, _ := hotkeyPages.GetFrontPage() - contentView, _ := contentPages.GetFrontPage() + pageName, _ := app.pages.GetFrontPage() + currentHotkeys, _ := app.hotkeys.GetFrontPage() + contentView, _ := app.content.GetFrontPage() switch currentHotkeys { case "connectionHotkeys": @@ -426,24 +447,24 @@ func newApp() App { return event } - addConnectionForm := newConnectionForm(Connection{}, func() { - pages.RemovePage(NEW_CONNECTION_FORM) - app.SetFocus(contentPages) + addConnectionForm := newConnectionForm(app, Connection{}, func() { + app.pages.RemovePage(NEW_CONNECTION_FORM) + app.SetFocus(app.content) }) - pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) + app.pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) return nil case 'e': - connection := connectionsTable.getConnection() + connection := app.connections.getConnection() if connection == nil { return event } - addConnectionForm := newConnectionForm(*connection, func() { - pages.RemovePage(NEW_CONNECTION_FORM) - app.SetFocus(contentPages) + addConnectionForm := newConnectionForm(app, *connection, func() { + app.pages.RemovePage(NEW_CONNECTION_FORM) + app.SetFocus(app.content) }) - pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) + app.pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) return nil } @@ -451,44 +472,46 @@ func newApp() App { switch event.Key() { case tcell.KeyESC: if contentView == DATABASE_VIEW { - newHeader := newLayout(tview.FlexRow, headerPanel(Connection{}, hotkeyPages), contentPages) - pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) + newHeader := newLayout(tview.FlexRow, headerPanel(Connection{}, app.hotkeys), app.content) + app.pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) - contentPages.RemovePage(DATABASE_VIEW) - app.SetFocus(contentPages) + app.content.RemovePage(DATABASE_VIEW) + app.SetFocus(app.content) return event } - pages.RemovePage(NEW_CONNECTION_FORM) - app.SetFocus(contentPages) + app.pages.RemovePage(NEW_CONNECTION_FORM) + app.SetFocus(app.content) case tcell.KeyEnter: if contentView == DATABASE_VIEW { return event } - connection := connectionsTable.getConnection() + connection := app.connections.getConnection() if connection == nil { return event } - db := NewOpenDatabase(*connection) - dbTable := newDbTable(db.openTable) - dbContent := newContentBox(db.openTable.name, dbTable) - - newHeader := newLayout(tview.FlexRow, headerPanel(*connection, hotkeyPages), contentPages) - pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) - - contentPages.AddAndSwitchToPage(DATABASE_VIEW, dbContent, true) - app.SetFocus(contentPages) + app.openConnection(*connection) } default: } return event }) +} + +func (app App) openConnection(connection Connection) { + db := NewOpenDatabase(connection) + dbTable := newDbTable(db.openTable) + dbContent := newContentBox(db.openTable.name, dbTable) + + newHeader := newLayout(tview.FlexRow, headerPanel(connection, app.hotkeys), app.content) + app.pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) - return App{app, Connection{}, pages} + app.content.AddAndSwitchToPage(DATABASE_VIEW, dbContent, true) + app.SetFocus(app.content) } func main() { From 284b1e92e10c052f5a5f943271f357be911808af Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 27 Feb 2025 23:04:06 +0000 Subject: [PATCH 19/33] Added delete function --- keyring.go | 10 +++++----- main.go | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/keyring.go b/keyring.go index fda7ae3..462a913 100644 --- a/keyring.go +++ b/keyring.go @@ -58,7 +58,7 @@ func initDb(db *sql.DB) error { return err } -func updateLocalDbConn(conn Connection) error { +func updateConnection(conn Connection) error { localDb, err := getAndOrCreateLocalDb() if err != nil { return err @@ -84,7 +84,7 @@ func updateLocalDbConn(conn Connection) error { return err } -func deleteLocalDbConn(name string) error { +func DeleteConnection(name string) error { localDb, err := getAndOrCreateLocalDb() if err != nil { return err @@ -105,7 +105,7 @@ func deleteLocalDbConn(name string) error { return err } -func listLocalDbConn() (map[string]string, error) { +func listConnections() (map[string]string, error) { connections := make(map[string]string) localDb, err := getAndOrCreateLocalDb() @@ -164,7 +164,7 @@ func SaveConnectionInKeyring(conn Connection) { } // Save rest to local storage - err = updateLocalDbConn(conn) + err = updateConnection(conn) if err != nil { log.Fatal("Could not set keyring info into local db: ", err) } @@ -180,7 +180,7 @@ func GetConnectionFromKeyring(name string) (string, string, error) { } func ListConnections() ([]Connection, error) { - connections, err := listLocalDbConn() + connections, err := listConnections() var conns []Connection diff --git a/main.go b/main.go index 8c502d1..0578860 100644 --- a/main.go +++ b/main.go @@ -228,7 +228,6 @@ func newConnectionForm(app *App, conn Connection, escapeFunc func()) *tview.Flex } }). AddButton("Connect", func() { - // Test save open testResult := conn.TestConnection() if testResult == FAILED { log.Fatal("Could not connect because connection could not be established") @@ -404,6 +403,7 @@ func newApp() App { hotkeyView := NewHotkeys(). AddHotKey("New Connection", 'n'). AddHotKey("Edit Connection", 'e'). + AddHotKey("Delete Connection", 'd'). AddHotKey("Quit", 'q') hotkeys := tview.NewPages(). AddAndSwitchToPage("connectionHotkeys", hotkeyView, true) @@ -466,6 +466,34 @@ func (app *App) setInputHandler() { }) app.pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) + return nil + case 'd': + // Delete connection + connection := app.connections.getConnection() + if connection == nil { + return event + } + + confirmDeleteModal := tview.NewModal(). + SetText("Are you sure you want to delete: " + connection.Name + "?"). + AddButtons([]string{"Cancel", "Confirm"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + if buttonLabel == "Confirm" { + err := DeleteConnection(connection.Name) + if err != nil { + log.Fatal("Could not delete connection", err) + } + + connectionsTable := newConnectionsTable(CONNECTION_TABLE_HEADERS) + connectionsView := newContentBox("Connections", connectionsTable) + + app.content.AddPage(SAVED_CONNECTIONS, connectionsView, true, true) + } + + app.pages.RemovePage("ConfirmDelete") + app.SetFocus(app.content) + }) + app.pages.AddPage("ConfirmDelete", confirmDeleteModal, true, true) return nil } @@ -484,7 +512,7 @@ func (app *App) setInputHandler() { app.pages.RemovePage(NEW_CONNECTION_FORM) app.SetFocus(app.content) case tcell.KeyEnter: - if contentView == DATABASE_VIEW { + if contentView == DATABASE_VIEW || pageName == "ConfirmDelete" { return event } From 741e03b17982eba52286d376ce854b5718bbbbd7 Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 27 Feb 2025 23:26:54 +0000 Subject: [PATCH 20/33] Allow refreshing the saved connections table --- main.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index 0578860..825c84d 100644 --- a/main.go +++ b/main.go @@ -217,7 +217,8 @@ func newConnectionForm(app *App, conn Connection, escapeFunc func()) *tview.Flex SaveConnectionInKeyring(conn) escapeFunc() - // TODO remove new connection form and refresh connections view + + app.refreshConnections() }). AddButton("Test", func() { testResult := conn.TestConnection() @@ -237,6 +238,8 @@ func newConnectionForm(app *App, conn Connection, escapeFunc func()) *tview.Flex escapeFunc() + app.refreshConnections() + app.openConnection(conn) }) @@ -409,11 +412,7 @@ func newApp() App { AddAndSwitchToPage("connectionHotkeys", hotkeyView, true) header := headerPanel(Connection{}, hotkeys) - connectionsTable := newConnectionsTable(CONNECTION_TABLE_HEADERS) - connectionsView := newContentBox("Connections", connectionsTable) - - content := tview.NewPages(). - AddAndSwitchToPage(SAVED_CONNECTIONS, connectionsView, true) + content := tview.NewPages() mainView := newLayout(tview.FlexRow, header, content) pages := tview.NewPages(). @@ -421,12 +420,20 @@ func newApp() App { app.SetRoot(pages, true).SetFocus(content) - ctx := App{app, Connection{}, pages, content, hotkeys, connectionsTable} + ctx := App{app, Connection{}, pages, content, hotkeys, &DisplayTable{}} ctx.setInputHandler() + ctx.refreshConnections() return ctx } +func (app *App) refreshConnections() { + connectionsTable := newConnectionsTable(CONNECTION_TABLE_HEADERS) + connectionsView := newContentBox("Connections", connectionsTable) + app.content.AddPage(SAVED_CONNECTIONS, connectionsView, true, true) + app.connections = connectionsTable +} + func (app *App) setInputHandler() { app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { pageName, _ := app.pages.GetFrontPage() @@ -484,10 +491,7 @@ func (app *App) setInputHandler() { log.Fatal("Could not delete connection", err) } - connectionsTable := newConnectionsTable(CONNECTION_TABLE_HEADERS) - connectionsView := newContentBox("Connections", connectionsTable) - - app.content.AddPage(SAVED_CONNECTIONS, connectionsView, true, true) + app.refreshConnections() } app.pages.RemovePage("ConfirmDelete") From b923ad31814f77599e57393288a2b33098635657 Mon Sep 17 00:00:00 2001 From: Rob Date: Fri, 28 Feb 2025 00:24:09 +0000 Subject: [PATCH 21/33] Fixed bug with editing connections by providing an id in the db --- db.go | 13 ++++++++++++- go.mod | 2 +- keyring.go | 33 +++++++++++++++++++++------------ main.go | 12 +++++------- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/db.go b/db.go index 164e0d7..1e4d6d6 100644 --- a/db.go +++ b/db.go @@ -3,7 +3,9 @@ package main import ( "context" "fmt" + "log" + "github.com/google/uuid" "github.com/jackc/pgx/v5" ) @@ -15,6 +17,7 @@ const ( ) type Connection struct { + ID uuid.UUID Host string Port string User string @@ -24,8 +27,16 @@ type Connection struct { status ConnectionStatus } +func NewConnection() Connection { + ID, err := uuid.NewV7() + if err != nil { + log.Fatal("Could not create id for connection", err) + } + return Connection{ID: ID} +} + func (c Connection) Row() []string { - return []string{c.Name, c.Host, c.Port, c.User, c.Database} + return []string{c.ID.String(), c.Name, c.Host, c.Port, c.User, c.Database} } func (params Connection) ConnectionString() string { diff --git a/go.mod b/go.mod index 85c0967..a5a9e32 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.4 require ( github.com/gdamore/tcell/v2 v2.7.1 + github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.5.5 github.com/rivo/tview v0.0.0-20241227133733-17b7edb88c57 github.com/zalando/go-keyring v0.2.4 @@ -16,7 +17,6 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect diff --git a/keyring.go b/keyring.go index 462a913..58a0406 100644 --- a/keyring.go +++ b/keyring.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" + "github.com/google/uuid" "github.com/zalando/go-keyring" _ "modernc.org/sqlite" ) @@ -49,7 +50,8 @@ func initDb(db *sql.DB) error { // Create the table if it doesn't exist _, err := db.Exec(` CREATE TABLE IF NOT EXISTS database_connections ( - name TEXT PRIMARY KEY, + id uuid PRIMARY KEY, + name TEXT UNIQUE NOT NULL, host TEXT NOT NULL, port TEXT NOT NULL, database_name TEXT NOT NULL @@ -77,8 +79,8 @@ func updateConnection(conn Connection) error { // Insert or replace the connection _, err = db.Exec( - "INSERT OR REPLACE INTO database_connections (name, host, port, database_name) VALUES (?, ?, ?, ?)", - conn.Name, conn.Host, conn.Port, conn.Database, + "INSERT OR REPLACE INTO database_connections (id, name, host, port, database_name) VALUES (?, ?, ?, ?, ?)", + conn.ID, conn.Name, conn.Host, conn.Port, conn.Database, ) return err @@ -124,18 +126,18 @@ func listConnections() (map[string]string, error) { return connections, err } - rows, err := db.Query("SELECT name, host, port, database_name FROM database_connections") + rows, err := db.Query("SELECT id, name, host, port, database_name FROM database_connections") if err != nil { return connections, err } defer rows.Close() for rows.Next() { - var name, host, port, database string - if err := rows.Scan(&name, &host, &port, &database); err != nil { + var id, name, host, port, database string + if err := rows.Scan(&id, &name, &host, &port, &database); err != nil { log.Fatal(err) } - connections[name] = fmt.Sprintf("%s:%s:%s", host, port, database) + connections[name] = fmt.Sprintf("%s:%s:%s:%s", id, host, port, database) } return connections, rows.Err() @@ -189,8 +191,8 @@ func ListConnections() ([]Connection, error) { } for k, v := range connections { - hostPortDb := strings.Split(v, ":") - if len(hostPortDb) != 3 { + components := strings.Split(v, ":") + if len(components) != 4 { continue } @@ -199,13 +201,20 @@ func ListConnections() ([]Connection, error) { log.Fatal("Could not get user and password for db", err) } + id, err := uuid.Parse(components[0]) + if err != nil { + log.Println(components[0]) + log.Fatal("Invalid uuid found for connection", err) + } + conn := Connection{ + ID: id, Name: k, User: user, Password: password, - Host: hostPortDb[0], - Port: hostPortDb[1], - Database: hostPortDb[2], + Host: components[1], + Port: components[2], + Database: components[3], } conns = append(conns, conn) } diff --git a/main.go b/main.go index 825c84d..78bc545 100644 --- a/main.go +++ b/main.go @@ -234,8 +234,6 @@ func newConnectionForm(app *App, conn Connection, escapeFunc func()) *tview.Flex log.Fatal("Could not connect because connection could not be established") } - SaveConnectionInKeyring(conn) - escapeFunc() app.refreshConnections() @@ -272,7 +270,7 @@ const ( DATABASE_VIEW = "databaseView" ) -var CONNECTION_TABLE_HEADERS = []string{"NAME", "HOST", "PORT", "USER", "DATABASE"} +var CONNECTION_TABLE_HEADERS = []string{"ID", "NAME", "HOST", "PORT", "USER", "DATABASE"} type DisplayTable struct { *tview.Table @@ -410,7 +408,7 @@ func newApp() App { AddHotKey("Quit", 'q') hotkeys := tview.NewPages(). AddAndSwitchToPage("connectionHotkeys", hotkeyView, true) - header := headerPanel(Connection{}, hotkeys) + header := headerPanel(NewConnection(), hotkeys) content := tview.NewPages() mainView := newLayout(tview.FlexRow, header, content) @@ -420,7 +418,7 @@ func newApp() App { app.SetRoot(pages, true).SetFocus(content) - ctx := App{app, Connection{}, pages, content, hotkeys, &DisplayTable{}} + ctx := App{app, NewConnection(), pages, content, hotkeys, &DisplayTable{}} ctx.setInputHandler() ctx.refreshConnections() @@ -454,7 +452,7 @@ func (app *App) setInputHandler() { return event } - addConnectionForm := newConnectionForm(app, Connection{}, func() { + addConnectionForm := newConnectionForm(app, NewConnection(), func() { app.pages.RemovePage(NEW_CONNECTION_FORM) app.SetFocus(app.content) }) @@ -504,7 +502,7 @@ func (app *App) setInputHandler() { switch event.Key() { case tcell.KeyESC: if contentView == DATABASE_VIEW { - newHeader := newLayout(tview.FlexRow, headerPanel(Connection{}, app.hotkeys), app.content) + newHeader := newLayout(tview.FlexRow, headerPanel(NewConnection(), app.hotkeys), app.content) app.pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) app.content.RemovePage(DATABASE_VIEW) From 69480e3542d72f09e951caa03f959c028206ce2e Mon Sep 17 00:00:00 2001 From: Rob Date: Fri, 28 Feb 2025 00:32:46 +0000 Subject: [PATCH 22/33] Removed unused comments --- existing_connection.go | 86 ---------------------------------------- main.go | 90 ------------------------------------------ new_connection.go | 39 ------------------ 3 files changed, 215 deletions(-) diff --git a/existing_connection.go b/existing_connection.go index 7003b01..06ab7d0 100644 --- a/existing_connection.go +++ b/existing_connection.go @@ -1,87 +1 @@ package main - -type ExistingConnectionsModel struct { - connections []Connection - selectedConnection *Connection - back bool -} - -// func NewExistingConnectionsModel() ExistingConnectionsModel { -// existingConnectionsModel := ExistingConnectionsModel{} - -// connections, err := ListConnections() - -// if err != nil { -// return existingConnectionsModel -// } - -// items := []list.Item{} - -// for _, conn := range connections { -// items = append(items, item(conn.Name)) -// } - -// l := list.New(items, itemDelegate{}, defaultWidth, listHeight) -// l.Title = "Choose a connection" -// l.SetShowStatusBar(false) -// l.SetFilteringEnabled(false) -// l.Styles.Title = titleStyle -// l.Styles.PaginationStyle = paginationStyle -// l.Styles.HelpStyle = helpStyle - -// existingConnectionsModel.list = l -// existingConnectionsModel.connections = connections - -// return existingConnectionsModel -// } - -// func (m ExistingConnectionsModel) Init() tea.Cmd { -// return nil -// } - -// func (m ExistingConnectionsModel) Update(msg tea.Msg) (ExistingConnectionsModel, tea.Cmd) { -// switch msg := msg.(type) { -// case tea.WindowSizeMsg: -// m.list.SetWidth(msg.Width) -// return m, nil - -// case tea.KeyMsg: -// switch keypress := msg.String(); keypress { -// case "q", "ctrl+c": -// m.back = true -// return m, nil - -// case "enter": -// i, ok := m.list.SelectedItem().(item) -// if ok { -// choice := string(i) - -// for _, v := range m.connections { -// if v.Name == choice { -// m.selectedConnection = &v - -// user, pass, err := GetConnectionFromKeyring(v.Name) - -// if err != nil { -// log.Fatal("Could not get user and password for connection from keyring: ", err) -// } - -// m.selectedConnection.User = user -// m.selectedConnection.Password = pass - -// break -// } -// } -// } -// return m, nil -// } -// } - -// var cmd tea.Cmd -// m.list, cmd = m.list.Update(msg) -// return m, cmd -// } - -// func (m ExistingConnectionsModel) View() string { -// return m.list.View() -// } diff --git a/main.go b/main.go index 78bc545..783d2e4 100644 --- a/main.go +++ b/main.go @@ -39,96 +39,6 @@ const ( LIGHT_GREY = "244" ) -// type model struct { -// list list.Model -// newConnectionModel NewConnectionModel -// currentView CurrentView -// currentConnection Connection -// openDatabase OpenDatabase -// existingConnections ExistingConnectionsModel -// } - -// func (m model) updateEvents(msg tea.Msg) (model, tea.Cmd) { -// switch msg := msg.(type) { -// case tea.WindowSizeMsg: -// m.list.SetWidth(msg.Width) -// width = msg.Width -// height = msg.Height -// return m, nil - -// case tea.KeyMsg: -// switch keypress := msg.String(); keypress { -// case "q", "ctrl+c": -// return m, tea.Quit - -// case "enter": -// i, ok := m.list.SelectedItem().(item) -// if ok { -// switch string(i) { -// case "New Connection": -// m.currentView = NEW_CONNECTION -// m.newConnectionModel = -// InitialNewConnectionModel() -// case "Edit Connection": -// m.currentView = EDIT_CONNECTION -// case "Join Existing": -// m.currentView = JOIN_EXISTING -// m.existingConnections = NewExistingConnectionsModel() -// } -// } -// return m, nil -// } -// } - -// var cmd tea.Cmd -// m.list, cmd = m.list.Update(msg) -// return m, cmd -// } - -// func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { -// var cmd tea.Cmd - -// switch m.currentView { -// case NEW_CONNECTION: -// m.newConnectionModel, cmd = m.newConnectionModel.Update(msg) -// if m.newConnectionModel.connection.status == CONNECTED { -// m.currentView = DATABASE_VIEW -// m.currentConnection = m.newConnectionModel.connection -// m.openDatabase = NewOpenDatabase(m.currentConnection) - -// SaveConnectionInKeyring(m.currentConnection) -// } - -// if m.newConnectionModel.action == CANCEL { -// m.currentView = DEFAULT -// } - -// case DATABASE_VIEW: -// m.openDatabase, cmd = m.openDatabase.Update(msg) -// if m.openDatabase.viewMode == QUIT { -// m.currentView = DEFAULT -// m.openDatabase = OpenDatabase{} -// } - -// case JOIN_EXISTING: -// m.existingConnections, cmd = m.existingConnections.Update(msg) -// if m.existingConnections.selectedConnection != nil { -// m.currentView = DATABASE_VIEW -// m.currentConnection = *m.existingConnections.selectedConnection -// m.openDatabase = NewOpenDatabase(m.currentConnection) -// } - -// if m.existingConnections.back { -// m.currentView = DEFAULT -// } - -// case DEFAULT, EDIT_CONNECTION: -// m, cmd = m.updateEvents(msg) -// } - -// return m, cmd -// } - type HotKey struct { desc string shortcut rune diff --git a/new_connection.go b/new_connection.go index eed3389..a0c7b1a 100644 --- a/new_connection.go +++ b/new_connection.go @@ -14,42 +14,3 @@ const ( PASSED TestStatus = "PASSED" FAILED TestStatus = "FAILED" ) - -type NewConnectionModel struct { - connection Connection - testStatus TestStatus - action Action -} - -// func (m NewConnectionModel) Update(msg tea.Msg) (NewConnectionModel, tea.Cmd) { -// switch msg := msg.(type) { -// case tea.KeyMsg: -// switch msg.String() { - -// case "enter": -// if m.focusIndex == len(m.inputs) { -// conn := Connection{ -// Host: m.inputs[0].Value(), -// Port: m.inputs[1].Value(), -// User: m.inputs[2].Value(), -// Password: m.inputs[3].Value(), -// Database: m.inputs[4].Value(), -// Name: m.inputs[5].Value(), -// status: DISCONNECTED, -// } - -// switch m.action { -// case SUBMIT: -// if conn.TestConnection() == PASSED { -// m.connection = conn -// } -// case TEST: -// if m.testStatus == NA { -// m.testStatus = conn.TestConnection() -// } - -// } - -// } - -// } From 109918172e062d6efb35f43934c1ae6f4aced0ee Mon Sep 17 00:00:00 2001 From: Rob Date: Sat, 1 Mar 2025 19:12:49 +0000 Subject: [PATCH 23/33] Refactored some components to separate files --- app.go | 164 ++++++++++++++++++++++++++++++++ layout.go | 17 ++++ main.go | 274 +----------------------------------------------------- table.go | 111 ++++++++++++++++++++++ 4 files changed, 293 insertions(+), 273 deletions(-) create mode 100644 app.go create mode 100644 layout.go create mode 100644 table.go diff --git a/app.go b/app.go new file mode 100644 index 0000000..baa690b --- /dev/null +++ b/app.go @@ -0,0 +1,164 @@ +package main + +import ( + "log" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +type App struct { + *tview.Application + conn Connection + pages *tview.Pages + content *tview.Pages + hotkeys *tview.Pages + connections *DisplayTable +} + +func NewApp() App { + app := tview.NewApplication() + + hotkeyView := NewHotkeys(). + AddHotKey("New Connection", 'n'). + AddHotKey("Edit Connection", 'e'). + AddHotKey("Delete Connection", 'd'). + AddHotKey("Quit", 'q') + hotkeys := tview.NewPages(). + AddAndSwitchToPage("connectionHotkeys", hotkeyView, true) + header := headerPanel(NewConnection(), hotkeys) + + content := tview.NewPages() + mainView := newLayout(tview.FlexRow, header, content) + + pages := tview.NewPages(). + AddAndSwitchToPage(MAIN_PAGE, mainView, true) + + app.SetRoot(pages, true).SetFocus(content) + + ctx := App{app, NewConnection(), pages, content, hotkeys, &DisplayTable{}} + ctx.setInputHandler() + ctx.refreshConnections() + + return ctx +} + +func (app *App) refreshConnections() { + connectionsTable := newConnectionsTable(CONNECTION_TABLE_HEADERS) + connectionsView := newContentBox("Connections", connectionsTable) + app.content.AddPage(SAVED_CONNECTIONS, connectionsView, true, true) + app.connections = connectionsTable +} + +func (app *App) setInputHandler() { + app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + pageName, _ := app.pages.GetFrontPage() + currentHotkeys, _ := app.hotkeys.GetFrontPage() + contentView, _ := app.content.GetFrontPage() + + switch currentHotkeys { + case "connectionHotkeys": + if pageName == NEW_CONNECTION_FORM { + return event + } + + switch event.Rune() { + case 'q': + app.Stop() + case 'n': + if pageName == NEW_CONNECTION_FORM { + return event + } + + addConnectionForm := newConnectionForm(app, NewConnection(), func() { + app.pages.RemovePage(NEW_CONNECTION_FORM) + app.SetFocus(app.content) + }) + app.pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) + + return nil + case 'e': + connection := app.connections.getConnection() + if connection == nil { + return event + } + + addConnectionForm := newConnectionForm(app, *connection, func() { + app.pages.RemovePage(NEW_CONNECTION_FORM) + app.SetFocus(app.content) + }) + app.pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) + + return nil + case 'd': + // Delete connection + connection := app.connections.getConnection() + if connection == nil { + return event + } + + confirmDeleteModal := tview.NewModal(). + SetText("Are you sure you want to delete: " + connection.Name + "?"). + AddButtons([]string{"Cancel", "Confirm"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + if buttonLabel == "Confirm" { + err := DeleteConnection(connection.Name) + if err != nil { + log.Fatal("Could not delete connection", err) + } + + app.refreshConnections() + } + + app.pages.RemovePage("ConfirmDelete") + app.SetFocus(app.content) + }) + app.pages.AddPage("ConfirmDelete", confirmDeleteModal, true, true) + return nil + } + + switch event.Key() { + case tcell.KeyESC: + if contentView == DATABASE_VIEW { + newHeader := newLayout(tview.FlexRow, headerPanel(NewConnection(), app.hotkeys), app.content) + app.pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) + + app.content.RemovePage(DATABASE_VIEW) + app.SetFocus(app.content) + + return event + } + + app.pages.RemovePage(NEW_CONNECTION_FORM) + app.pages.RemovePage("ConfirmDelete") + app.SetFocus(app.content) + case tcell.KeyEnter: + if contentView == DATABASE_VIEW || pageName == "ConfirmDelete" { + return event + } + + connection := app.connections.getConnection() + if connection == nil { + return event + } + + app.openConnection(*connection) + } + default: + } + + return event + }) +} + +func (app App) openConnection(connection Connection) { + db := NewOpenDatabase(connection) + dbTable := newDbTable(db.openTable) + dbContent := newContentBox(db.openTable.name, dbTable) + + newHeader := newLayout(tview.FlexRow, headerPanel(connection, app.hotkeys), app.content) + app.pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) + + app.content.AddAndSwitchToPage(DATABASE_VIEW, dbContent, true) + app.SetFocus(app.content) +} diff --git a/layout.go b/layout.go new file mode 100644 index 0000000..82c5bf9 --- /dev/null +++ b/layout.go @@ -0,0 +1,17 @@ +package main + +import "github.com/rivo/tview" + +type Layout struct { + *tview.Flex + header *tview.Flex + content *tview.Pages +} + +func newLayout(direction int, header *tview.Flex, content *tview.Pages) Layout { + view := tview.NewFlex().SetDirection(direction). + AddItem(header, 0, 1, false). + AddItem(content, 0, 6, false) + + return Layout{view, header, content} +} diff --git a/main.go b/main.go index 783d2e4..ddf31b0 100644 --- a/main.go +++ b/main.go @@ -182,280 +182,8 @@ const ( var CONNECTION_TABLE_HEADERS = []string{"ID", "NAME", "HOST", "PORT", "USER", "DATABASE"} -type DisplayTable struct { - *tview.Table - columns []string - rows []Connection -} - -func newConnectionsTable(columns []string) *DisplayTable { - table := tview.NewTable() - - for i, header := range columns { - table.SetCell(0, i, tview.NewTableCell(header).SetExpansion(1)) - } - - table.SetBorderPadding(0, 0, 1, 1) - table.SetSelectable(true, false).Select(1, 0) - - connectionsTable := DisplayTable{table, columns, []Connection{}} - connectionsTable.getConnections() - - return &connectionsTable -} - -func (t *DisplayTable) getConnections() { - connections, err := ListConnections() - if err != nil { - return - } - - for i, conn := range connections { - values := conn.Row() - for j, value := range values { - t.SetCell(i+1, j, tview.NewTableCell(value)) - } - } - - t.rows = connections -} - -func (t *DisplayTable) getConnection() *Connection { - row, _ := t.GetSelection() - - if row == 0 { - return nil - } - - return &t.rows[row-1] -} - -type DbTable struct { - *tview.Table - table Table -} - -func newDbTable(table Table) *DbTable { - t := tview.NewTable() - - for i, header := range table.fields { - t.SetCell(0, i, tview.NewTableCell(header).SetExpansion(1)) - } - - t.SetBorderPadding(0, 0, 1, 1) - t.SetSelectable(true, false).Select(1, 0) - - connectionsTable := DbTable{t, table} - connectionsTable.getTableRows() - - return &connectionsTable -} - -func (t *DbTable) getTableRows() { - for i, conn := range t.table.values { - for j, value := range conn { - t.SetCell(i+1, j, tview.NewTableCell(value)) - } - } -} - -type ContentBox struct { - *tview.Box - content tview.Primitive -} - -func newContentBox(title string, content tview.Primitive) *ContentBox { - return &ContentBox{ - tview.NewBox().SetBorder(true).SetTitle(fmt.Sprintf(" %s ", title)), - content, - } -} - -func (b *ContentBox) Draw(screen tcell.Screen) { - b.Box.DrawForSubclass(screen, b) - x, y, w, h := b.GetInnerRect() - - b.content.SetRect(x, y, w, h) - b.content.Draw(screen) -} - -func (b *ContentBox) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { - return b.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { - b.content.InputHandler()(event, setFocus) - }) -} - -type Layout struct { - *tview.Flex - header *tview.Flex - content *tview.Pages -} - -func newLayout(direction int, header *tview.Flex, content *tview.Pages) Layout { - view := tview.NewFlex().SetDirection(direction). - AddItem(header, 0, 1, false). - AddItem(content, 0, 6, false) - - return Layout{view, header, content} -} - -type App struct { - *tview.Application - conn Connection - pages *tview.Pages - content *tview.Pages - hotkeys *tview.Pages - connections *DisplayTable -} - -func newApp() App { - app := tview.NewApplication() - - hotkeyView := NewHotkeys(). - AddHotKey("New Connection", 'n'). - AddHotKey("Edit Connection", 'e'). - AddHotKey("Delete Connection", 'd'). - AddHotKey("Quit", 'q') - hotkeys := tview.NewPages(). - AddAndSwitchToPage("connectionHotkeys", hotkeyView, true) - header := headerPanel(NewConnection(), hotkeys) - - content := tview.NewPages() - mainView := newLayout(tview.FlexRow, header, content) - - pages := tview.NewPages(). - AddAndSwitchToPage(MAIN_PAGE, mainView, true) - - app.SetRoot(pages, true).SetFocus(content) - - ctx := App{app, NewConnection(), pages, content, hotkeys, &DisplayTable{}} - ctx.setInputHandler() - ctx.refreshConnections() - - return ctx -} - -func (app *App) refreshConnections() { - connectionsTable := newConnectionsTable(CONNECTION_TABLE_HEADERS) - connectionsView := newContentBox("Connections", connectionsTable) - app.content.AddPage(SAVED_CONNECTIONS, connectionsView, true, true) - app.connections = connectionsTable -} - -func (app *App) setInputHandler() { - app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - pageName, _ := app.pages.GetFrontPage() - currentHotkeys, _ := app.hotkeys.GetFrontPage() - contentView, _ := app.content.GetFrontPage() - - switch currentHotkeys { - case "connectionHotkeys": - if pageName == NEW_CONNECTION_FORM { - return event - } - - switch event.Rune() { - case 'q': - app.Stop() - case 'n': - if pageName == NEW_CONNECTION_FORM { - return event - } - - addConnectionForm := newConnectionForm(app, NewConnection(), func() { - app.pages.RemovePage(NEW_CONNECTION_FORM) - app.SetFocus(app.content) - }) - app.pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) - - return nil - case 'e': - connection := app.connections.getConnection() - if connection == nil { - return event - } - - addConnectionForm := newConnectionForm(app, *connection, func() { - app.pages.RemovePage(NEW_CONNECTION_FORM) - app.SetFocus(app.content) - }) - app.pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) - - return nil - case 'd': - // Delete connection - connection := app.connections.getConnection() - if connection == nil { - return event - } - - confirmDeleteModal := tview.NewModal(). - SetText("Are you sure you want to delete: " + connection.Name + "?"). - AddButtons([]string{"Cancel", "Confirm"}). - SetDoneFunc(func(buttonIndex int, buttonLabel string) { - if buttonLabel == "Confirm" { - err := DeleteConnection(connection.Name) - if err != nil { - log.Fatal("Could not delete connection", err) - } - - app.refreshConnections() - } - - app.pages.RemovePage("ConfirmDelete") - app.SetFocus(app.content) - }) - app.pages.AddPage("ConfirmDelete", confirmDeleteModal, true, true) - return nil - } - - switch event.Key() { - case tcell.KeyESC: - if contentView == DATABASE_VIEW { - newHeader := newLayout(tview.FlexRow, headerPanel(NewConnection(), app.hotkeys), app.content) - app.pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) - - app.content.RemovePage(DATABASE_VIEW) - app.SetFocus(app.content) - - return event - } - - app.pages.RemovePage(NEW_CONNECTION_FORM) - app.SetFocus(app.content) - case tcell.KeyEnter: - if contentView == DATABASE_VIEW || pageName == "ConfirmDelete" { - return event - } - - connection := app.connections.getConnection() - if connection == nil { - return event - } - - app.openConnection(*connection) - } - default: - } - - return event - }) -} - -func (app App) openConnection(connection Connection) { - db := NewOpenDatabase(connection) - dbTable := newDbTable(db.openTable) - dbContent := newContentBox(db.openTable.name, dbTable) - - newHeader := newLayout(tview.FlexRow, headerPanel(connection, app.hotkeys), app.content) - app.pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) - - app.content.AddAndSwitchToPage(DATABASE_VIEW, dbContent, true) - app.SetFocus(app.content) -} - func main() { - app := newApp() + app := NewApp() if err := app.Run(); err != nil { panic(err) diff --git a/table.go b/table.go new file mode 100644 index 0000000..c962258 --- /dev/null +++ b/table.go @@ -0,0 +1,111 @@ +package main + +import ( + "fmt" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +type DisplayTable struct { + *tview.Table + columns []string + rows []Connection +} + +func newConnectionsTable(columns []string) *DisplayTable { + table := tview.NewTable() + + for i, header := range columns { + table.SetCell(0, i, tview.NewTableCell(header).SetExpansion(1)) + } + + table.SetBorderPadding(0, 0, 1, 1) + table.SetSelectable(true, false).Select(1, 0) + + connectionsTable := DisplayTable{table, columns, []Connection{}} + connectionsTable.getConnections() + + return &connectionsTable +} + +func (t *DisplayTable) getConnections() { + connections, err := ListConnections() + if err != nil { + return + } + + for i, conn := range connections { + values := conn.Row() + for j, value := range values { + t.SetCell(i+1, j, tview.NewTableCell(value)) + } + } + + t.rows = connections +} + +func (t *DisplayTable) getConnection() *Connection { + row, _ := t.GetSelection() + + if row == 0 { + return nil + } + + return &t.rows[row-1] +} + +type DbTable struct { + *tview.Table + table Table +} + +func newDbTable(table Table) *DbTable { + t := tview.NewTable() + + for i, header := range table.fields { + t.SetCell(0, i, tview.NewTableCell(header).SetExpansion(1)) + } + + t.SetBorderPadding(0, 0, 1, 1) + t.SetSelectable(true, false).Select(1, 0) + + connectionsTable := DbTable{t, table} + connectionsTable.getTableRows() + + return &connectionsTable +} + +func (t *DbTable) getTableRows() { + for i, conn := range t.table.values { + for j, value := range conn { + t.SetCell(i+1, j, tview.NewTableCell(value)) + } + } +} + +type ContentBox struct { + *tview.Box + content tview.Primitive +} + +func newContentBox(title string, content tview.Primitive) *ContentBox { + return &ContentBox{ + tview.NewBox().SetBorder(true).SetTitle(fmt.Sprintf(" %s ", title)), + content, + } +} + +func (b *ContentBox) Draw(screen tcell.Screen) { + b.Box.DrawForSubclass(screen, b) + x, y, w, h := b.GetInnerRect() + + b.content.SetRect(x, y, w, h) + b.content.Draw(screen) +} + +func (b *ContentBox) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + return b.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + b.content.InputHandler()(event, setFocus) + }) +} From 2949cbb9187b760dd90f46bdc299e1a9117bd601 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 4 Mar 2025 22:30:41 +0000 Subject: [PATCH 24/33] Refactoring and separation of concerns --- app.go | 141 +++++++++++++++++++---------------- components.go | 116 +++++++++++++++++++++++++++++ constants.go | 45 +++++++++++ hotkeys.go | 53 +++++++++++++ keyring.go | 2 +- main.go | 182 --------------------------------------------- open_connection.go | 4 +- 7 files changed, 291 insertions(+), 252 deletions(-) create mode 100644 components.go create mode 100644 constants.go create mode 100644 hotkeys.go diff --git a/app.go b/app.go index baa690b..46c999b 100644 --- a/app.go +++ b/app.go @@ -56,101 +56,110 @@ func (app *App) setInputHandler() { currentHotkeys, _ := app.hotkeys.GetFrontPage() contentView, _ := app.content.GetFrontPage() - switch currentHotkeys { - case "connectionHotkeys": - if pageName == NEW_CONNECTION_FORM { - return event - } + // Skip handling if we're in the connection form + if pageName == NEW_CONNECTION_FORM { + return event + } + // Handle connection hotkeys + if currentHotkeys == "connectionHotkeys" { + // Handle rune-based hotkeys switch event.Rune() { case 'q': app.Stop() + return nil case 'n': - if pageName == NEW_CONNECTION_FORM { - return event - } - - addConnectionForm := newConnectionForm(app, NewConnection(), func() { - app.pages.RemovePage(NEW_CONNECTION_FORM) - app.SetFocus(app.content) - }) - app.pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) - + app.showNewConnectionForm(NewConnection()) return nil case 'e': connection := app.connections.getConnection() - if connection == nil { - return event + if connection != nil { + app.showNewConnectionForm(*connection) } - - addConnectionForm := newConnectionForm(app, *connection, func() { - app.pages.RemovePage(NEW_CONNECTION_FORM) - app.SetFocus(app.content) - }) - app.pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) - return nil case 'd': - // Delete connection connection := app.connections.getConnection() - if connection == nil { - return event + if connection != nil { + app.showDeleteConfirmation(*connection) } - - confirmDeleteModal := tview.NewModal(). - SetText("Are you sure you want to delete: " + connection.Name + "?"). - AddButtons([]string{"Cancel", "Confirm"}). - SetDoneFunc(func(buttonIndex int, buttonLabel string) { - if buttonLabel == "Confirm" { - err := DeleteConnection(connection.Name) - if err != nil { - log.Fatal("Could not delete connection", err) - } - - app.refreshConnections() - } - - app.pages.RemovePage("ConfirmDelete") - app.SetFocus(app.content) - }) - app.pages.AddPage("ConfirmDelete", confirmDeleteModal, true, true) return nil } + // Handle special keys switch event.Key() { case tcell.KeyESC: if contentView == DATABASE_VIEW { - newHeader := newLayout(tview.FlexRow, headerPanel(NewConnection(), app.hotkeys), app.content) - app.pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) - - app.content.RemovePage(DATABASE_VIEW) - app.SetFocus(app.content) - - return event + app.returnToConnectionsView() + return nil } - - app.pages.RemovePage(NEW_CONNECTION_FORM) - app.pages.RemovePage("ConfirmDelete") - app.SetFocus(app.content) + app.closeModals() + return nil case tcell.KeyEnter: - if contentView == DATABASE_VIEW || pageName == "ConfirmDelete" { - return event + if contentView != DATABASE_VIEW && pageName != CONFIRM_DELETE && pageName != CONNECTION_TEST { + connection := app.connections.getConnection() + if connection != nil { + app.openConnection(*connection) + } } - - connection := app.connections.getConnection() - if connection == nil { - return event - } - - app.openConnection(*connection) + return event } - default: } return event }) } +// Helper methods to clean up the input handler +func (app *App) showNewConnectionForm(connection Connection) { + addConnectionForm := newConnectionForm(app, connection, func() { + app.pages.RemovePage(NEW_CONNECTION_FORM) + app.SetFocus(app.content) + }) + app.pages.AddPage(NEW_CONNECTION_FORM, addConnectionForm, true, true) +} + +func (app *App) showDeleteConfirmation(connection Connection) { + confirmDeleteModal := tview.NewModal(). + SetText("Are you sure you want to delete: " + connection.Name + "?"). + AddButtons([]string{"Cancel", "Confirm"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + if buttonLabel == "Confirm" { + err := DeleteConnection(connection.Name) + if err != nil { + log.Fatal("Could not delete connection", err) + } + app.refreshConnections() + } + app.pages.RemovePage(CONFIRM_DELETE) + app.SetFocus(app.content) + }) + app.pages.AddPage(CONFIRM_DELETE, confirmDeleteModal, true, true) +} + +func (app *App) showInfoModal(page, body string, escapeFunc func()) { + confirmDeleteModal := tview.NewModal(). + SetText(body). + AddButtons([]string{"Ok"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + escapeFunc() + }) + + app.pages.AddPage(page, confirmDeleteModal, true, true) +} + +func (app *App) returnToConnectionsView() { + newHeader := newLayout(tview.FlexRow, headerPanel(NewConnection(), app.hotkeys), app.content) + app.pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) + app.content.RemovePage(DATABASE_VIEW) + app.SetFocus(app.content) +} + +func (app *App) closeModals() { + app.pages.RemovePage(NEW_CONNECTION_FORM) + app.pages.RemovePage(CONFIRM_DELETE) + app.SetFocus(app.content) +} + func (app App) openConnection(connection Connection) { db := NewOpenDatabase(connection) dbTable := newDbTable(db.openTable) diff --git a/components.go b/components.go new file mode 100644 index 0000000..1e97201 --- /dev/null +++ b/components.go @@ -0,0 +1,116 @@ +package main + +import ( + "strconv" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +// currentConnectionInfo creates a list view showing connection details +func currentConnectionInfo(conn Connection) *tview.List { + list := tview.NewList(). + ShowSecondaryText(false). + SetSelectedFocusOnly(true). + AddItem("Name: "+conn.Name, "", 0, nil). + AddItem("Host: "+conn.Host, "", 0, nil). + AddItem("PORT: "+conn.Port, "", 0, nil). + AddItem("USER: "+conn.User, "", 0, nil). + AddItem("Database: "+conn.Database, "", 0, nil) + + return list +} + +// headerPanel creates the header panel with connection info and hotkeys +func headerPanel(conn Connection, hotkeys *tview.Pages) *tview.Flex { + connection := currentConnectionInfo(conn) + + appName := tview.NewTextView().SetText(APP_NAME).SetTextAlign(tview.AlignRight) + + headerView := tview.NewFlex(). + AddItem(connection, 0, 1, false). + AddItem(hotkeys, 0, 1, false). + AddItem(appName, 0, 1, false) + headerView.SetBorderPadding(0, 0, 1, 1) + + return headerView +} + +// newConnectionForm creates a form for adding/editing connections +func newConnectionForm(app *App, conn Connection, escapeFunc func()) *tview.Flex { + form := tview.NewForm(). + AddInputField("Name", conn.Name, 26, nil, func(text string) { conn.Name = text }). + AddInputField("Host", conn.Host, 26, nil, func(text string) { conn.Host = text }). + AddInputField("Port", conn.Port, 26, func(textToCheck string, lastChar rune) bool { + _, err := strconv.Atoi(textToCheck) + + return err == nil + }, func(text string) { conn.Port = text }). + AddInputField("User", conn.User, 26, nil, func(text string) { conn.User = text }). + AddPasswordField("Password", conn.Password, 26, '*', func(text string) { conn.Password = text }). + AddInputField("Database", conn.Database, 26, nil, func(text string) { conn.Database = text }). + AddButton("Save", func() { + testResult := conn.TestConnection() + if testResult == FAILED { + app.showInfoModal(CONNECTION_TEST, "Could not save connection because connection could not be established.", func() { + app.pages.RemovePage(CONNECTION_TEST) + }) + return + } + + SaveConnectionInKeyring(conn) + + escapeFunc() + + app.refreshConnections() + }). + AddButton("Test", func() { + testResult := conn.TestConnection() + closeModal := func() { + app.pages.RemovePage(CONNECTION_TEST) + } + + switch testResult { + case PASSED: + app.showInfoModal(CONNECTION_TEST, "Connection successful!", closeModal) + case FAILED: + app.showInfoModal(CONNECTION_TEST, "Connection failed. Please check your settings.", closeModal) + } + }). + AddButton("Connect", func() { + testResult := conn.TestConnection() + if testResult == FAILED { + app.showInfoModal(CONNECTION_TEST, "Could not connect because connection could not be established", func() { + app.pages.RemovePage(CONNECTION_TEST) + }) + return + } + + escapeFunc() + + app.refreshConnections() + + app.openConnection(conn) + }) + + form.SetBorder(true) + form.SetButtonsAlign(tview.AlignRight) + + modal := tview.NewFlex(). + AddItem(nil, 0, 3, false). + AddItem(tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(nil, 0, 1, false). + AddItem(form, 0, 1, true). + AddItem(nil, 0, 1, false), 0, 2, true). + AddItem(nil, 0, 3, false) + + modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if event.Key() == tcell.KeyESC { + escapeFunc() + } + + return event + }) + + return modal +} diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..4172bef --- /dev/null +++ b/constants.go @@ -0,0 +1,45 @@ +package main + +// CurrentView represents the current view state of the application +type CurrentView string + +const APP_NAME = `_________________ +\______ \______ \______ + | | \ / / ___/ + | ` + "`" + ` \/ /\___ \ +/_______ /____//____ > + \/ \/ +` + +const ( + DEFAULT CurrentView = "DEFAULT" + NEW_CONNECTION CurrentView = "NEW_CONNECTION" + EDIT_CONNECTION CurrentView = "EDIT_CONNECTION" + JOIN_EXISTING CurrentView = "JOIN_EXISTING" + // DATABASE_VIEW CurrentView = "DATABASE_VIEW". +) + +// Primary ansi colours. +const ( + WHITE = "15" + RED = "1" + GREEN = "2" + YELLOW = "3" + BLUE = "4" + MAGENTA = "5" + GREY = "240" + LIGHT_GREY = "244" +) + +type PageName string + +const ( + MAIN_PAGE = "main" + NEW_CONNECTION_FORM = "newConnection" + SAVED_CONNECTIONS = "savedConnections" + DATABASE_VIEW = "databaseView" + CONNECTION_TEST = "ConntectionTest" + CONFIRM_DELETE = "ConfirmDelete" +) + +var CONNECTION_TABLE_HEADERS = []string{"ID", "NAME", "HOST", "PORT", "USER", "DATABASE"} diff --git a/hotkeys.go b/hotkeys.go new file mode 100644 index 0000000..8a4186b --- /dev/null +++ b/hotkeys.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +// HotKey represents a keyboard shortcut with description +type HotKey struct { + desc string + shortcut rune +} + +// HotKeys is a UI component for displaying keyboard shortcuts +type HotKeys struct { + *tview.List + values []HotKey +} + +// NewHotkeys creates a new HotKeys component +func NewHotkeys() *HotKeys { + list := tview.NewList(). + ShowSecondaryText(false).SetSelectedFocusOnly(true) + + return &HotKeys{ + List: list, + values: []HotKey{}, + } +} + +// AddHotKey adds a new hotkey to the component +func (r *HotKeys) AddHotKey(desc string, shortcut rune) *HotKeys { + r.values = append(r.values, HotKey{desc, shortcut}) + + return r +} + +// Draw renders the hotkeys component +func (r *HotKeys) Draw(screen tcell.Screen) { + r.Box.DrawForSubclass(screen, r) + x, y, width, height := r.GetInnerRect() + + for index, hotkey := range r.values { + if index >= height { + break + } + + line := fmt.Sprintf("<%s> %s", string(hotkey.shortcut), hotkey.desc) + tview.Print(screen, line, x, y+index, width, tview.AlignLeft, tcell.ColorYellow) + } +} diff --git a/keyring.go b/keyring.go index 58a0406..a1d087c 100644 --- a/keyring.go +++ b/keyring.go @@ -187,7 +187,7 @@ func ListConnections() ([]Connection, error) { var conns []Connection if err != nil { - return conns, errors.New("Could not list connections") + return conns, errors.New("could not list connections") } for k, v := range connections { diff --git a/main.go b/main.go index ddf31b0..6188c7c 100644 --- a/main.go +++ b/main.go @@ -1,187 +1,5 @@ package main -import ( - "fmt" - "log" - "strconv" - - "github.com/gdamore/tcell/v2" - "github.com/rivo/tview" -) - -type CurrentView string - -const APP_NAME = `_________________ -\______ \______ \______ - | | \ / / ___/ - | ` + "`" + ` \/ /\___ \ -/_______ /____//____ > - \/ \/ -` - -const ( - DEFAULT CurrentView = "DEFAULT" - NEW_CONNECTION CurrentView = "NEW_CONNECTION" - EDIT_CONNECTION CurrentView = "EDIT_CONNECTION" - JOIN_EXISTING CurrentView = "JOIN_EXISTING" - // DATABASE_VIEW CurrentView = "DATABASE_VIEW". -) - -// Primary ansi colours. -const ( - WHITE = "15" - RED = "1" - GREEN = "2" - YELLOW = "3" - BLUE = "4" - MAGENTA = "5" - GREY = "240" - LIGHT_GREY = "244" -) - -type HotKey struct { - desc string - shortcut rune -} - -type HotKeys struct { - *tview.List - values []HotKey -} - -func NewHotkeys() *HotKeys { - list := tview.NewList(). - ShowSecondaryText(false).SetSelectedFocusOnly(true) - - return &HotKeys{ - List: list, - values: []HotKey{}, - } -} - -func (r *HotKeys) AddHotKey(desc string, shortcut rune) *HotKeys { - r.values = append(r.values, HotKey{desc, shortcut}) - - return r -} - -func (r *HotKeys) Draw(screen tcell.Screen) { - r.Box.DrawForSubclass(screen, r) - x, y, width, height := r.GetInnerRect() - - for index, hotkey := range r.values { - if index >= height { - break - } - - line := fmt.Sprintf("<%s> %s", string(hotkey.shortcut), hotkey.desc) - tview.Print(screen, line, x, y+index, width, tview.AlignLeft, tcell.ColorYellow) - } -} - -func currentConnectionInfo(conn Connection) *tview.List { - list := tview.NewList(). - ShowSecondaryText(false). - SetSelectedFocusOnly(true). - AddItem("Name: "+conn.Name, "", 0, nil). - AddItem("Host: "+conn.Host, "", 0, nil). - AddItem("PORT: "+conn.Port, "", 0, nil). - AddItem("USER: "+conn.User, "", 0, nil). - AddItem("Database: "+conn.Database, "", 0, nil) - - return list -} - -func headerPanel(conn Connection, hotkeys *tview.Pages) *tview.Flex { - connection := currentConnectionInfo(conn) - - appName := tview.NewTextView().SetText(APP_NAME).SetTextAlign(tview.AlignRight) - - headerView := tview.NewFlex(). - AddItem(connection, 0, 1, false). - AddItem(hotkeys, 0, 1, false). - AddItem(appName, 0, 1, false) - headerView.SetBorderPadding(0, 0, 1, 1) - - return headerView -} - -func newConnectionForm(app *App, conn Connection, escapeFunc func()) *tview.Flex { - form := tview.NewForm(). - AddInputField("Name", conn.Name, 26, nil, func(text string) { conn.Name = text }). - AddInputField("Host", conn.Host, 26, nil, func(text string) { conn.Host = text }). - AddInputField("Port", conn.Port, 26, func(textToCheck string, lastChar rune) bool { - _, err := strconv.Atoi(textToCheck) - - return err == nil - }, func(text string) { conn.Port = text }). - AddInputField("User", conn.User, 26, nil, func(text string) { conn.User = text }). - AddPasswordField("Password", conn.Password, 26, '*', func(text string) { conn.Password = text }). - AddInputField("Database", conn.Database, 26, nil, func(text string) { conn.Database = text }). - AddButton("Save", func() { - testResult := conn.TestConnection() - if testResult == FAILED { - log.Fatal("Could not save connection because connection could not be established") - } - - SaveConnectionInKeyring(conn) - - escapeFunc() - - app.refreshConnections() - }). - AddButton("Test", func() { - testResult := conn.TestConnection() - switch testResult { - case PASSED: - // TODO show modal here for passed and failed jobs - case FAILED: - } - }). - AddButton("Connect", func() { - testResult := conn.TestConnection() - if testResult == FAILED { - log.Fatal("Could not connect because connection could not be established") - } - - escapeFunc() - - app.refreshConnections() - - app.openConnection(conn) - }) - - form.SetBorder(true) - form.SetButtonsAlign(tview.AlignRight) - - modal := tview.NewFlex(). - AddItem(nil, 0, 3, false). - AddItem(tview.NewFlex().SetDirection(tview.FlexRow). - AddItem(nil, 0, 1, false). - AddItem(form, 0, 1, true). - AddItem(nil, 0, 1, false), 0, 2, true). - AddItem(nil, 0, 3, false) - - modal.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Key() == tcell.KeyESC { - escapeFunc() - } - - return event - }) - - return modal -} - -const ( - MAIN_PAGE = "main" - NEW_CONNECTION_FORM = "newConnection" - SAVED_CONNECTIONS = "savedConnections" - DATABASE_VIEW = "databaseView" -) - -var CONNECTION_TABLE_HEADERS = []string{"ID", "NAME", "HOST", "PORT", "USER", "DATABASE"} - func main() { app := NewApp() diff --git a/open_connection.go b/open_connection.go index 241e811..9812542 100644 --- a/open_connection.go +++ b/open_connection.go @@ -1,8 +1,6 @@ package main -import ( - "log" -) +import "log" type ViewMode string From f85cf6bdf4a873a9911193f71922fe418aa44459 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 4 Mar 2025 22:57:30 +0000 Subject: [PATCH 25/33] Added dynamic hotkeys --- app.go | 49 ++++++++++++++++++++++++++++++++++++++++++------- hotkeys.go | 17 +++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/app.go b/app.go index 46c999b..22ecb8f 100644 --- a/app.go +++ b/app.go @@ -19,13 +19,14 @@ type App struct { func NewApp() App { app := tview.NewApplication() - hotkeyView := NewHotkeys(). - AddHotKey("New Connection", 'n'). - AddHotKey("Edit Connection", 'e'). - AddHotKey("Delete Connection", 'd'). - AddHotKey("Quit", 'q') + // Create pages for different hotkey sets + connectionHotkeys := GetConnectionHotkeys() + databaseHotkeys := GetDatabaseHotkeys() + hotkeys := tview.NewPages(). - AddAndSwitchToPage("connectionHotkeys", hotkeyView, true) + AddPage("databaseHotkeys", databaseHotkeys, true, false). + AddAndSwitchToPage("connectionHotkeys", connectionHotkeys, true) + header := headerPanel(NewConnection(), hotkeys) content := tview.NewPages() @@ -103,6 +104,26 @@ func (app *App) setInputHandler() { } return event } + } else if currentHotkeys == "databaseHotkeys" { + // Handle database view hotkeys + switch event.Rune() { + case 'q': + app.Stop() + return nil + case 'b': + app.returnToConnectionsView() + return nil + case 'r': + app.refreshCurrentConnection() + return nil + } + + // Handle special keys + switch event.Key() { + case tcell.KeyESC: + app.returnToConnectionsView() + return nil + } } return event @@ -128,6 +149,7 @@ func (app *App) showDeleteConfirmation(connection Connection) { if err != nil { log.Fatal("Could not delete connection", err) } + app.refreshConnections() } app.pages.RemovePage(CONFIRM_DELETE) @@ -147,7 +169,17 @@ func (app *App) showInfoModal(page, body string, escapeFunc func()) { app.pages.AddPage(page, confirmDeleteModal, true, true) } +func (app *App) refreshCurrentConnection() { + connection := app.connections.getConnection() + if connection != nil { + app.openConnection(*connection) + } +} + func (app *App) returnToConnectionsView() { + // Switch back to connection hotkeys + app.hotkeys.SwitchToPage("connectionHotkeys") + newHeader := newLayout(tview.FlexRow, headerPanel(NewConnection(), app.hotkeys), app.content) app.pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) app.content.RemovePage(DATABASE_VIEW) @@ -165,7 +197,10 @@ func (app App) openConnection(connection Connection) { dbTable := newDbTable(db.openTable) dbContent := newContentBox(db.openTable.name, dbTable) - newHeader := newLayout(tview.FlexRow, headerPanel(connection, app.hotkeys), app.content) + app.hotkeys.SwitchToPage("databaseHotkeys") + + layoutHeader := headerPanel(connection, app.hotkeys) + newHeader := newLayout(tview.FlexRow, layoutHeader, app.content) app.pages.AddPage(SAVED_CONNECTIONS, newHeader, true, true) app.content.AddAndSwitchToPage(DATABASE_VIEW, dbContent, true) diff --git a/hotkeys.go b/hotkeys.go index 8a4186b..068b134 100644 --- a/hotkeys.go +++ b/hotkeys.go @@ -51,3 +51,20 @@ func (r *HotKeys) Draw(screen tcell.Screen) { tview.Print(screen, line, x, y+index, width, tview.AlignLeft, tcell.ColorYellow) } } + +// GetConnectionHotkeys returns hotkeys for the connections view +func GetConnectionHotkeys() *HotKeys { + return NewHotkeys(). + AddHotKey("New Connection", 'n'). + AddHotKey("Edit Connection", 'e'). + AddHotKey("Delete Connection", 'd'). + AddHotKey("Quit", 'q') +} + +// GetDatabaseHotkeys returns hotkeys for the database view +func GetDatabaseHotkeys() *HotKeys { + return NewHotkeys(). + AddHotKey("Back to Connections", 'b'). + AddHotKey("Refresh Data", 'r'). + AddHotKey("Quit", 'q') +} From d4518f31e1a71afcae8d3f4a95d942b4b02bb1db Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 6 Mar 2025 23:47:36 +0000 Subject: [PATCH 26/33] Added more hotkeys --- hotkeys.go | 125 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 113 insertions(+), 12 deletions(-) diff --git a/hotkeys.go b/hotkeys.go index 068b134..a2d1c8b 100644 --- a/hotkeys.go +++ b/hotkeys.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "math" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" @@ -10,7 +11,7 @@ import ( // HotKey represents a keyboard shortcut with description type HotKey struct { desc string - shortcut rune + shortcut string } // HotKeys is a UI component for displaying keyboard shortcuts @@ -31,7 +32,7 @@ func NewHotkeys() *HotKeys { } // AddHotKey adds a new hotkey to the component -func (r *HotKeys) AddHotKey(desc string, shortcut rune) *HotKeys { +func (r *HotKeys) AddHotKey(desc string, shortcut string) *HotKeys { r.values = append(r.values, HotKey{desc, shortcut}) return r @@ -42,29 +43,129 @@ func (r *HotKeys) Draw(screen tcell.Screen) { r.Box.DrawForSubclass(screen, r) x, y, width, height := r.GetInnerRect() - for index, hotkey := range r.values { - if index >= height { + // Calculate how many columns we need + totalHotkeys := len(r.values) + if totalHotkeys == 0 { + return + } + + // Determine max hotkey text length for column width calculation + maxHotkeyLength := 0 + for _, hotkey := range r.values { + hotkeyText := fmt.Sprintf("<%s> %s", string(hotkey.shortcut), hotkey.desc) + if len(hotkeyText) > maxHotkeyLength { + maxHotkeyLength = len(hotkeyText) + } + } + + // Add some padding between columns + columnWidth := maxHotkeyLength + 4 + + // Calculate how many columns can fit in the available width + maxColumns := int(math.Max(1, float64(width)/float64(columnWidth))) + + // Calculate how many rows we need per column + rowsPerColumn := int(math.Ceil(float64(totalHotkeys) / float64(maxColumns))) + + // Ensure we don't exceed available height + if rowsPerColumn > height { + rowsPerColumn = height + maxColumns = int(math.Ceil(float64(totalHotkeys) / float64(rowsPerColumn))) + } + + // Draw hotkeys in columns + for i, hotkey := range r.values { + // Calculate column and row position + column := i / rowsPerColumn + row := i % rowsPerColumn + + // Skip if we've run out of columns that can fit in the width + if column >= maxColumns { break } + // Calculate x position for this column + colX := x + (column * columnWidth) + + // Draw the hotkey line := fmt.Sprintf("<%s> %s", string(hotkey.shortcut), hotkey.desc) - tview.Print(screen, line, x, y+index, width, tview.AlignLeft, tcell.ColorYellow) + tview.Print(screen, line, colX, y+row, columnWidth, tview.AlignLeft, tcell.ColorYellow) } } // GetConnectionHotkeys returns hotkeys for the connections view func GetConnectionHotkeys() *HotKeys { return NewHotkeys(). - AddHotKey("New Connection", 'n'). - AddHotKey("Edit Connection", 'e'). - AddHotKey("Delete Connection", 'd'). - AddHotKey("Quit", 'q') + AddHotKey("New Connection", "n"). + AddHotKey("Edit Connection", "e"). + AddHotKey("Delete Connection", "d"). + AddHotKey("Open Connection", "o"). + AddHotKey("Test Connection", "t"). + AddHotKey("Refresh Connections", "r"). + AddHotKey("Search", "/"). + AddHotKey("Sort by Name", "s"). + AddHotKey("Help", "h"). + AddHotKey("Quit", "q"). + AddHotKey("Up", "↑"). + AddHotKey("Down", "↓"). + AddHotKey("Enter", "⏎") } // GetDatabaseHotkeys returns hotkeys for the database view func GetDatabaseHotkeys() *HotKeys { return NewHotkeys(). - AddHotKey("Back to Connections", 'b'). - AddHotKey("Refresh Data", 'r'). - AddHotKey("Quit", 'q') + AddHotKey("Back to Connections", "b"). + AddHotKey("Refresh Data", "r"). + AddHotKey("Execute Query", "e"). + AddHotKey("Export Results", "x"). + AddHotKey("Filter Results", "f"). + AddHotKey("Copy Row", "c"). + AddHotKey("Copy Cell", "y"). + AddHotKey("Next Page", "n"). + AddHotKey("Previous Page", "p"). + AddHotKey("Toggle View Mode", "v"). + AddHotKey("Help", "h"). + AddHotKey("Quit", "q"). + AddHotKey("Up", "↑"). + AddHotKey("Down", "↓"). + AddHotKey("Left", "←"). + AddHotKey("Right", "→") +} + +// GetHelpHotkeys returns hotkeys for the help view +func GetHelpHotkeys() *HotKeys { + return NewHotkeys(). + AddHotKey("Back", "b"). + AddHotKey("Scroll Up", "↑"). + AddHotKey("Scroll Down", "↓"). + AddHotKey("Quit", "q") +} + +// GetFormHotkeys returns hotkeys for form views +func GetFormHotkeys() *HotKeys { + return NewHotkeys(). + AddHotKey("Next Field", "Tab"). + AddHotKey("Previous Field", "Shift+Tab"). + AddHotKey("Submit", "Enter"). + AddHotKey("Cancel", "Esc") +} + +// GetQueryHotkeys returns hotkeys for the query editor view +func GetQueryHotkeys() *HotKeys { + return NewHotkeys(). + AddHotKey("Execute", "Ctrl+e"). + AddHotKey("Save Query", "Ctrl+s"). + AddHotKey("Load Query", "Ctrl+o"). + AddHotKey("Clear", "Ctrl+l"). + AddHotKey("Exit Editor", "Esc"). + AddHotKey("History", "Ctrl+h") +} + +// GetExportHotkeys returns hotkeys for the export view +func GetExportHotkeys() *HotKeys { + return NewHotkeys(). + AddHotKey("Export as CSV", "c"). + AddHotKey("Export as JSON", "j"). + AddHotKey("Export as SQL", "s"). + AddHotKey("Cancel", "Esc") } From 2c97b151a304834716750f6c33336804b0613c9f Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 6 Mar 2025 23:53:17 +0000 Subject: [PATCH 27/33] Implementation stubbing for the hotkeys --- app.go | 402 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 402 insertions(+) diff --git a/app.go b/app.go index 22ecb8f..69e95a4 100644 --- a/app.go +++ b/app.go @@ -84,6 +84,46 @@ func (app *App) setInputHandler() { app.showDeleteConfirmation(*connection) } return nil + case 'o': + // Open selected connection + connection := app.connections.getConnection() + if connection != nil { + app.openConnection(*connection) + } + return nil + case 't': + // Test selected connection + connection := app.connections.getConnection() + if connection != nil { + testResult := connection.TestConnection() + closeModal := func() { + app.pages.RemovePage(CONNECTION_TEST) + } + + switch testResult { + case PASSED: + app.showInfoModal(CONNECTION_TEST, "Connection successful!", closeModal) + case FAILED: + app.showInfoModal(CONNECTION_TEST, "Connection failed. Please check your settings.", closeModal) + } + } + return nil + case 'r': + // Refresh connections list + app.refreshConnections() + return nil + case '/': + // Show search input + app.showSearchInput() + return nil + case 's': + // Sort connections by name + app.sortConnectionsByName() + return nil + case 'h': + // Show help view + app.showHelpView() + return nil } // Handle special keys @@ -116,6 +156,42 @@ func (app *App) setInputHandler() { case 'r': app.refreshCurrentConnection() return nil + case 'e': + // Execute custom query + app.showQueryEditor() + return nil + case 'x': + // Export results + app.showExportOptions() + return nil + case 'f': + // Filter results + app.showFilterInput() + return nil + case 'c': + // Copy row + app.copySelectedRow() + return nil + case 'y': + // Copy cell + app.copySelectedCell() + return nil + case 'n': + // Next page + app.goToNextPage() + return nil + case 'p': + // Previous page + app.goToPreviousPage() + return nil + case 'v': + // Toggle view mode + app.toggleViewMode() + return nil + case 'h': + // Show help view + app.showHelpView() + return nil } // Handle special keys @@ -124,6 +200,65 @@ func (app *App) setInputHandler() { app.returnToConnectionsView() return nil } + } else if currentHotkeys == "helpHotkeys" { + // Handle help view hotkeys + switch event.Rune() { + case 'q': + app.Stop() + return nil + case 'b': + app.closeHelpView() + return nil + } + + // Handle special keys + switch event.Key() { + case tcell.KeyESC: + app.closeHelpView() + return nil + } + } else if currentHotkeys == "queryHotkeys" { + // Handle query editor hotkeys + switch event.Key() { + case tcell.KeyESC: + app.closeQueryEditor() + return nil + case tcell.KeyCtrlE: + app.executeQuery() + return nil + case tcell.KeyCtrlS: + app.saveQuery() + return nil + case tcell.KeyCtrlO: + app.loadQuery() + return nil + case tcell.KeyCtrlL: + app.clearQuery() + return nil + case tcell.KeyCtrlH: + app.showQueryHistory() + return nil + } + } else if currentHotkeys == "exportHotkeys" { + // Handle export options hotkeys + switch event.Rune() { + case 'c': + app.exportAsCSV() + return nil + case 'j': + app.exportAsJSON() + return nil + case 's': + app.exportAsSQL() + return nil + } + + // Handle special keys + switch event.Key() { + case tcell.KeyESC: + app.closeExportOptions() + return nil + } } return event @@ -206,3 +341,270 @@ func (app App) openConnection(connection Connection) { app.content.AddAndSwitchToPage(DATABASE_VIEW, dbContent, true) app.SetFocus(app.content) } + +// Search functionality +func (app *App) showSearchInput() { + var inputField *tview.InputField + inputField = tview.NewInputField(). + SetLabel("Search: "). + SetFieldWidth(30). + SetDoneFunc(func(key tcell.Key) { + if key == tcell.KeyEnter { + searchTerm := inputField.GetText() + app.searchConnections(searchTerm) + app.pages.RemovePage("searchInput") + app.SetFocus(app.content) + } else if key == tcell.KeyEscape { + app.pages.RemovePage("searchInput") + app.SetFocus(app.content) + } + }) + + modal := tview.NewFlex(). + AddItem(nil, 0, 1, false). + AddItem(tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(nil, 0, 1, false). + AddItem(inputField, 3, 1, true). + AddItem(nil, 0, 1, false), 40, 1, true). + AddItem(nil, 0, 1, false) + + app.pages.AddPage("searchInput", modal, true, true) + app.SetFocus(inputField) +} + +func (app *App) searchConnections(term string) { + // Implementation would filter the connections table based on the search term + // This is a placeholder - actual implementation would depend on your data structure +} + +// Sorting functionality +func (app *App) sortConnectionsByName() { + // Implementation would sort the connections table by name + // This is a placeholder - actual implementation would depend on your data structure +} + +// Help view +func (app *App) showHelpView() { + helpText := tview.NewTextView(). + SetDynamicColors(true). + SetRegions(true). + SetWordWrap(true). + SetText(`[yellow]TermTable Help[white] + +[green]Connection View Hotkeys:[white] + New Connection - Create a new database connection + Edit Connection - Edit the selected connection + Delete Connection - Delete the selected connection + Open Connection - Open the selected connection + Test Connection - Test the selected connection + Refresh Connections - Refresh the list of connections + Search - Search for a connection + Sort by Name - Sort connections by name + Help - Show this help screen + Quit - Exit the application + +[green]Database View Hotkeys:[white] + Back to Connections - Return to the connections view + Refresh Data - Refresh the current data view + Execute Query - Open the query editor + Export Results - Export the current results + Filter Results - Filter the current results + Copy Row - Copy the selected row + Copy Cell - Copy the selected cell + Next Page - Go to the next page of results +

Previous Page - Go to the previous page of results + Toggle View Mode - Toggle between different view modes + Help - Show this help screen + Quit - Exit the application + +[green]Navigation:[white] +Use arrow keys to navigate tables and lists. +Press Enter to select or open an item. +Press Esc to go back or close a modal.`) + + helpView := tview.NewFrame(helpText). + SetBorders(0, 0, 0, 0, 0, 0). + AddText("Help", true, tview.AlignCenter, tcell.ColorYellow). + AddText("Press 'b' to go back", false, tview.AlignCenter, tcell.ColorWhite) + + // Create help hotkeys + helpHotkeys := GetHelpHotkeys() + app.hotkeys.AddAndSwitchToPage("helpHotkeys", helpHotkeys, true) + + // Save current view to return to it later + app.pages.AddPage("helpView", helpView, true, true) + app.SetFocus(helpView) +} + +func (app *App) closeHelpView() { + app.pages.RemovePage("helpView") + + // Switch back to previous hotkeys + contentView, _ := app.content.GetFrontPage() + if contentView == DATABASE_VIEW { + app.hotkeys.SwitchToPage("databaseHotkeys") + } else { + app.hotkeys.SwitchToPage("connectionHotkeys") + } + + app.SetFocus(app.content) +} + +// Query editor functionality +func (app *App) showQueryEditor() { + queryEditor := tview.NewTextArea(). + SetPlaceholder("Enter SQL query here..."). + SetWordWrap(true) + + queryFrame := tview.NewFrame(queryEditor). + SetBorders(0, 0, 0, 0, 0, 0). + AddText("SQL Query Editor", true, tview.AlignCenter, tcell.ColorYellow). + AddText("Ctrl+E: Execute | Ctrl+S: Save | Ctrl+O: Load | Ctrl+L: Clear | Esc: Exit", false, tview.AlignCenter, tcell.ColorWhite) + + // Create query hotkeys + queryHotkeys := GetQueryHotkeys() + app.hotkeys.AddAndSwitchToPage("queryHotkeys", queryHotkeys, true) + + app.pages.AddPage("queryEditor", queryFrame, true, true) + app.SetFocus(queryEditor) +} + +func (app *App) closeQueryEditor() { + app.pages.RemovePage("queryEditor") + app.hotkeys.SwitchToPage("databaseHotkeys") + app.SetFocus(app.content) +} + +func (app *App) executeQuery() { + // Implementation would execute the query in the editor + // This is a placeholder +} + +func (app *App) saveQuery() { + // Implementation would save the current query + // This is a placeholder +} + +func (app *App) loadQuery() { + // Implementation would load a saved query + // This is a placeholder +} + +func (app *App) clearQuery() { + // Implementation would clear the query editor + // This is a placeholder +} + +func (app *App) showQueryHistory() { + // Implementation would show query history + // This is a placeholder +} + +// Export functionality +func (app *App) showExportOptions() { + exportModal := tview.NewModal(). + SetText("Export Options"). + AddButtons([]string{"CSV", "JSON", "SQL", "Cancel"}). + SetDoneFunc(func(buttonIndex int, buttonLabel string) { + switch buttonLabel { + case "CSV": + app.exportAsCSV() + case "JSON": + app.exportAsJSON() + case "SQL": + app.exportAsSQL() + } + app.pages.RemovePage("exportOptions") + app.SetFocus(app.content) + }) + + // Create export hotkeys + exportHotkeys := GetExportHotkeys() + app.hotkeys.AddAndSwitchToPage("exportHotkeys", exportHotkeys, true) + + app.pages.AddPage("exportOptions", exportModal, true, true) +} + +func (app *App) closeExportOptions() { + app.pages.RemovePage("exportOptions") + app.hotkeys.SwitchToPage("databaseHotkeys") + app.SetFocus(app.content) +} + +func (app *App) exportAsCSV() { + // Implementation would export data as CSV + // This is a placeholder +} + +func (app *App) exportAsJSON() { + // Implementation would export data as JSON + // This is a placeholder +} + +func (app *App) exportAsSQL() { + // Implementation would export data as SQL + // This is a placeholder +} + +// Filter functionality +func (app *App) showFilterInput() { + var inputField *tview.InputField + inputField = tview.NewInputField(). + SetLabel("Filter: "). + SetFieldWidth(30). + SetDoneFunc(func(key tcell.Key) { + if key == tcell.KeyEnter { + filterTerm := inputField.GetText() + app.filterResults(filterTerm) + app.pages.RemovePage("filterInput") + app.SetFocus(app.content) + } else if key == tcell.KeyEscape { + app.pages.RemovePage("filterInput") + app.SetFocus(app.content) + } + }) + + modal := tview.NewFlex(). + AddItem(nil, 0, 1, false). + AddItem(tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(nil, 0, 1, false). + AddItem(inputField, 3, 1, true). + AddItem(nil, 0, 1, false), 40, 1, true). + AddItem(nil, 0, 1, false) + + app.pages.AddPage("filterInput", modal, true, true) + app.SetFocus(inputField) +} + +func (app *App) filterResults(term string) { + // Implementation would filter the results based on the filter term + // This is a placeholder +} + +// Copy functionality +func (app *App) copySelectedRow() { + // Implementation would copy the selected row to clipboard + // This is a placeholder +} + +func (app *App) copySelectedCell() { + // Implementation would copy the selected cell to clipboard + // This is a placeholder +} + +// Pagination functionality +func (app *App) goToNextPage() { + // Implementation would go to the next page of results + // This is a placeholder +} + +func (app *App) goToPreviousPage() { + // Implementation would go to the previous page of results + // This is a placeholder +} + +// View mode functionality +func (app *App) toggleViewMode() { + // Implementation would toggle between different view modes + // This is a placeholder +} From 84f96f6130f999d205b8300abbae0ffd56a3f407 Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 13 Mar 2025 19:44:54 +0000 Subject: [PATCH 28/33] Updated compose file to be runtime agnostic --- data/{docker-compose.yml => compose.yml} | 1 - 1 file changed, 1 deletion(-) rename data/{docker-compose.yml => compose.yml} (96%) diff --git a/data/docker-compose.yml b/data/compose.yml similarity index 96% rename from data/docker-compose.yml rename to data/compose.yml index 696ec23..3a00917 100644 --- a/data/docker-compose.yml +++ b/data/compose.yml @@ -1,4 +1,3 @@ -version: "3" name: termtable services: From fb489619c1c594011067c0c892580f89b4a7b6de Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 13 Mar 2025 23:42:40 +0000 Subject: [PATCH 29/33] Added initial search box and fixed styling for focused borders --- app.go | 51 ++++++++++++++++++++++++++++---------------------- components.go | 3 --- content_box.go | 33 ++++++++++++++++++++++++++++++++ hotkeys.go | 24 +----------------------- table.go | 33 +------------------------------- 5 files changed, 64 insertions(+), 80 deletions(-) create mode 100644 content_box.go diff --git a/app.go b/app.go index 69e95a4..fe39a04 100644 --- a/app.go +++ b/app.go @@ -17,9 +17,15 @@ type App struct { } func NewApp() App { + tview.Borders.HorizontalFocus = tview.BoxDrawingsLightHorizontal + tview.Borders.VerticalFocus = tview.BoxDrawingsLightVertical + tview.Borders.TopLeftFocus = tview.BoxDrawingsLightDownAndRight + tview.Borders.TopRightFocus = tview.BoxDrawingsLightDownAndLeft + tview.Borders.BottomLeftFocus = tview.BoxDrawingsLightUpAndRight + tview.Borders.BottomRightFocus = tview.BoxDrawingsLightUpAndLeft + app := tview.NewApplication() - // Create pages for different hotkey sets connectionHotkeys := GetConnectionHotkeys() databaseHotkeys := GetDatabaseHotkeys() @@ -47,7 +53,26 @@ func NewApp() App { func (app *App) refreshConnections() { connectionsTable := newConnectionsTable(CONNECTION_TABLE_HEADERS) connectionsView := newContentBox("Connections", connectionsTable) - app.content.AddPage(SAVED_CONNECTIONS, connectionsView, true, true) + + searchBar := tview.NewInputField(). + SetFieldWidth(0). + SetAcceptanceFunc(tview.InputFieldInteger). + SetDoneFunc(func(key tcell.Key) { + app.Stop() + }) + searchBar.SetBorder(true) + searchBar.SetFieldBackgroundColor(tcell.ColorNone) + container := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(searchBar, 3, 0, false). + AddItem(connectionsView, 0, 1, false) + + container.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + connectionsView.InputHandler()(event, func(p tview.Primitive) {}) + return event + }) + + app.content.AddPage(SAVED_CONNECTIONS, container, true, true) app.connections = connectionsTable } @@ -57,14 +82,11 @@ func (app *App) setInputHandler() { currentHotkeys, _ := app.hotkeys.GetFrontPage() contentView, _ := app.content.GetFrontPage() - // Skip handling if we're in the connection form if pageName == NEW_CONNECTION_FORM { return event } - // Handle connection hotkeys if currentHotkeys == "connectionHotkeys" { - // Handle rune-based hotkeys switch event.Rune() { case 'q': app.Stop() @@ -85,14 +107,12 @@ func (app *App) setInputHandler() { } return nil case 'o': - // Open selected connection connection := app.connections.getConnection() if connection != nil { app.openConnection(*connection) } return nil case 't': - // Test selected connection connection := app.connections.getConnection() if connection != nil { testResult := connection.TestConnection() @@ -109,19 +129,15 @@ func (app *App) setInputHandler() { } return nil case 'r': - // Refresh connections list app.refreshConnections() return nil case '/': - // Show search input app.showSearchInput() return nil case 's': - // Sort connections by name app.sortConnectionsByName() return nil - case 'h': - // Show help view + case '?': app.showHelpView() return nil } @@ -157,39 +173,30 @@ func (app *App) setInputHandler() { app.refreshCurrentConnection() return nil case 'e': - // Execute custom query app.showQueryEditor() return nil case 'x': - // Export results app.showExportOptions() return nil case 'f': - // Filter results app.showFilterInput() return nil case 'c': - // Copy row app.copySelectedRow() return nil case 'y': - // Copy cell app.copySelectedCell() return nil case 'n': - // Next page app.goToNextPage() return nil case 'p': - // Previous page app.goToPreviousPage() return nil case 'v': - // Toggle view mode app.toggleViewMode() return nil - case 'h': - // Show help view + case '?': app.showHelpView() return nil } diff --git a/components.go b/components.go index 1e97201..8981907 100644 --- a/components.go +++ b/components.go @@ -7,7 +7,6 @@ import ( "github.com/rivo/tview" ) -// currentConnectionInfo creates a list view showing connection details func currentConnectionInfo(conn Connection) *tview.List { list := tview.NewList(). ShowSecondaryText(false). @@ -21,7 +20,6 @@ func currentConnectionInfo(conn Connection) *tview.List { return list } -// headerPanel creates the header panel with connection info and hotkeys func headerPanel(conn Connection, hotkeys *tview.Pages) *tview.Flex { connection := currentConnectionInfo(conn) @@ -36,7 +34,6 @@ func headerPanel(conn Connection, hotkeys *tview.Pages) *tview.Flex { return headerView } -// newConnectionForm creates a form for adding/editing connections func newConnectionForm(app *App, conn Connection, escapeFunc func()) *tview.Flex { form := tview.NewForm(). AddInputField("Name", conn.Name, 26, nil, func(text string) { conn.Name = text }). diff --git a/content_box.go b/content_box.go new file mode 100644 index 0000000..679a0a4 --- /dev/null +++ b/content_box.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +type ContentBox struct { + *tview.Box + content tview.Primitive +} + +func newContentBox(title string, content tview.Primitive) *ContentBox { + box := tview.NewBox().SetBorder(true).SetTitle(fmt.Sprintf(" %s ", title)) + + return &ContentBox{box, content} +} + +func (b *ContentBox) Draw(screen tcell.Screen) { + b.Box.DrawForSubclass(screen, b) + x, y, w, h := b.GetInnerRect() + + b.content.SetRect(x, y, w, h) + b.content.Draw(screen) +} + +func (b *ContentBox) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + return b.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + b.content.InputHandler()(event, setFocus) + }) +} diff --git a/hotkeys.go b/hotkeys.go index a2d1c8b..3fb6912 100644 --- a/hotkeys.go +++ b/hotkeys.go @@ -8,19 +8,16 @@ import ( "github.com/rivo/tview" ) -// HotKey represents a keyboard shortcut with description type HotKey struct { desc string shortcut string } -// HotKeys is a UI component for displaying keyboard shortcuts type HotKeys struct { *tview.List values []HotKey } -// NewHotkeys creates a new HotKeys component func NewHotkeys() *HotKeys { list := tview.NewList(). ShowSecondaryText(false).SetSelectedFocusOnly(true) @@ -31,25 +28,21 @@ func NewHotkeys() *HotKeys { } } -// AddHotKey adds a new hotkey to the component func (r *HotKeys) AddHotKey(desc string, shortcut string) *HotKeys { r.values = append(r.values, HotKey{desc, shortcut}) return r } -// Draw renders the hotkeys component func (r *HotKeys) Draw(screen tcell.Screen) { r.Box.DrawForSubclass(screen, r) x, y, width, height := r.GetInnerRect() - // Calculate how many columns we need totalHotkeys := len(r.values) if totalHotkeys == 0 { return } - // Determine max hotkey text length for column width calculation maxHotkeyLength := 0 for _, hotkey := range r.values { hotkeyText := fmt.Sprintf("<%s> %s", string(hotkey.shortcut), hotkey.desc) @@ -58,42 +51,32 @@ func (r *HotKeys) Draw(screen tcell.Screen) { } } - // Add some padding between columns columnWidth := maxHotkeyLength + 4 - // Calculate how many columns can fit in the available width maxColumns := int(math.Max(1, float64(width)/float64(columnWidth))) - // Calculate how many rows we need per column rowsPerColumn := int(math.Ceil(float64(totalHotkeys) / float64(maxColumns))) - // Ensure we don't exceed available height if rowsPerColumn > height { rowsPerColumn = height maxColumns = int(math.Ceil(float64(totalHotkeys) / float64(rowsPerColumn))) } - // Draw hotkeys in columns for i, hotkey := range r.values { - // Calculate column and row position column := i / rowsPerColumn row := i % rowsPerColumn - // Skip if we've run out of columns that can fit in the width if column >= maxColumns { break } - // Calculate x position for this column colX := x + (column * columnWidth) - // Draw the hotkey line := fmt.Sprintf("<%s> %s", string(hotkey.shortcut), hotkey.desc) tview.Print(screen, line, colX, y+row, columnWidth, tview.AlignLeft, tcell.ColorYellow) } } -// GetConnectionHotkeys returns hotkeys for the connections view func GetConnectionHotkeys() *HotKeys { return NewHotkeys(). AddHotKey("New Connection", "n"). @@ -111,7 +94,6 @@ func GetConnectionHotkeys() *HotKeys { AddHotKey("Enter", "⏎") } -// GetDatabaseHotkeys returns hotkeys for the database view func GetDatabaseHotkeys() *HotKeys { return NewHotkeys(). AddHotKey("Back to Connections", "b"). @@ -124,7 +106,7 @@ func GetDatabaseHotkeys() *HotKeys { AddHotKey("Next Page", "n"). AddHotKey("Previous Page", "p"). AddHotKey("Toggle View Mode", "v"). - AddHotKey("Help", "h"). + AddHotKey("Help", "?"). AddHotKey("Quit", "q"). AddHotKey("Up", "↑"). AddHotKey("Down", "↓"). @@ -132,7 +114,6 @@ func GetDatabaseHotkeys() *HotKeys { AddHotKey("Right", "→") } -// GetHelpHotkeys returns hotkeys for the help view func GetHelpHotkeys() *HotKeys { return NewHotkeys(). AddHotKey("Back", "b"). @@ -141,7 +122,6 @@ func GetHelpHotkeys() *HotKeys { AddHotKey("Quit", "q") } -// GetFormHotkeys returns hotkeys for form views func GetFormHotkeys() *HotKeys { return NewHotkeys(). AddHotKey("Next Field", "Tab"). @@ -150,7 +130,6 @@ func GetFormHotkeys() *HotKeys { AddHotKey("Cancel", "Esc") } -// GetQueryHotkeys returns hotkeys for the query editor view func GetQueryHotkeys() *HotKeys { return NewHotkeys(). AddHotKey("Execute", "Ctrl+e"). @@ -161,7 +140,6 @@ func GetQueryHotkeys() *HotKeys { AddHotKey("History", "Ctrl+h") } -// GetExportHotkeys returns hotkeys for the export view func GetExportHotkeys() *HotKeys { return NewHotkeys(). AddHotKey("Export as CSV", "c"). diff --git a/table.go b/table.go index c962258..119405a 100644 --- a/table.go +++ b/table.go @@ -1,11 +1,6 @@ package main -import ( - "fmt" - - "github.com/gdamore/tcell/v2" - "github.com/rivo/tview" -) +import "github.com/rivo/tview" type DisplayTable struct { *tview.Table @@ -83,29 +78,3 @@ func (t *DbTable) getTableRows() { } } } - -type ContentBox struct { - *tview.Box - content tview.Primitive -} - -func newContentBox(title string, content tview.Primitive) *ContentBox { - return &ContentBox{ - tview.NewBox().SetBorder(true).SetTitle(fmt.Sprintf(" %s ", title)), - content, - } -} - -func (b *ContentBox) Draw(screen tcell.Screen) { - b.Box.DrawForSubclass(screen, b) - x, y, w, h := b.GetInnerRect() - - b.content.SetRect(x, y, w, h) - b.content.Draw(screen) -} - -func (b *ContentBox) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { - return b.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { - b.content.InputHandler()(event, setFocus) - }) -} From f39cbbb6f5dff53ac67bede42417b641167c5e26 Mon Sep 17 00:00:00 2001 From: Rob Date: Sat, 15 Mar 2025 23:55:01 +0000 Subject: [PATCH 30/33] Functional search bar ui --- app.go | 67 +++++++++++--------------------------------------- content_box.go | 53 +++++++++++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 58 deletions(-) diff --git a/app.go b/app.go index fe39a04..dc145e5 100644 --- a/app.go +++ b/app.go @@ -9,11 +9,12 @@ import ( type App struct { *tview.Application - conn Connection - pages *tview.Pages - content *tview.Pages - hotkeys *tview.Pages - connections *DisplayTable + conn Connection + pages *tview.Pages + content *tview.Pages + hotkeys *tview.Pages + connections *DisplayTable + connectionsView *ContentBox } func NewApp() App { @@ -43,7 +44,7 @@ func NewApp() App { app.SetRoot(pages, true).SetFocus(content) - ctx := App{app, NewConnection(), pages, content, hotkeys, &DisplayTable{}} + ctx := App{app, NewConnection(), pages, content, hotkeys, &DisplayTable{}, nil} ctx.setInputHandler() ctx.refreshConnections() @@ -54,26 +55,9 @@ func (app *App) refreshConnections() { connectionsTable := newConnectionsTable(CONNECTION_TABLE_HEADERS) connectionsView := newContentBox("Connections", connectionsTable) - searchBar := tview.NewInputField(). - SetFieldWidth(0). - SetAcceptanceFunc(tview.InputFieldInteger). - SetDoneFunc(func(key tcell.Key) { - app.Stop() - }) - searchBar.SetBorder(true) - searchBar.SetFieldBackgroundColor(tcell.ColorNone) - container := tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(searchBar, 3, 0, false). - AddItem(connectionsView, 0, 1, false) - - container.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - connectionsView.InputHandler()(event, func(p tview.Primitive) {}) - return event - }) - - app.content.AddPage(SAVED_CONNECTIONS, container, true, true) + app.content.AddPage(SAVED_CONNECTIONS, connectionsView, true, true) app.connections = connectionsTable + app.connectionsView = connectionsView } func (app *App) setInputHandler() { @@ -82,7 +66,10 @@ func (app *App) setInputHandler() { currentHotkeys, _ := app.hotkeys.GetFrontPage() contentView, _ := app.content.GetFrontPage() - if pageName == NEW_CONNECTION_FORM { + if pageName == NEW_CONNECTION_FORM || app.connectionsView.searchBar != nil { + if event.Key() == tcell.KeyEscape { + app.connectionsView.toggleSearchBar() + } return event } @@ -349,34 +336,8 @@ func (app App) openConnection(connection Connection) { app.SetFocus(app.content) } -// Search functionality func (app *App) showSearchInput() { - var inputField *tview.InputField - inputField = tview.NewInputField(). - SetLabel("Search: "). - SetFieldWidth(30). - SetDoneFunc(func(key tcell.Key) { - if key == tcell.KeyEnter { - searchTerm := inputField.GetText() - app.searchConnections(searchTerm) - app.pages.RemovePage("searchInput") - app.SetFocus(app.content) - } else if key == tcell.KeyEscape { - app.pages.RemovePage("searchInput") - app.SetFocus(app.content) - } - }) - - modal := tview.NewFlex(). - AddItem(nil, 0, 1, false). - AddItem(tview.NewFlex().SetDirection(tview.FlexRow). - AddItem(nil, 0, 1, false). - AddItem(inputField, 3, 1, true). - AddItem(nil, 0, 1, false), 40, 1, true). - AddItem(nil, 0, 1, false) - - app.pages.AddPage("searchInput", modal, true, true) - app.SetFocus(inputField) + app.connectionsView.toggleSearchBar() } func (app *App) searchConnections(term string) { diff --git a/content_box.go b/content_box.go index 679a0a4..35baadb 100644 --- a/content_box.go +++ b/content_box.go @@ -8,19 +8,47 @@ import ( ) type ContentBox struct { - *tview.Box - content tview.Primitive + *tview.Flex + content tview.Primitive + box *tview.Box + searchBar *tview.InputField } func newContentBox(title string, content tview.Primitive) *ContentBox { box := tview.NewBox().SetBorder(true).SetTitle(fmt.Sprintf(" %s ", title)) - return &ContentBox{box, content} + container := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(box, 0, 1, true) + + return &ContentBox{container, content, box, nil} +} + +func (b *ContentBox) toggleSearchBar() { + if b.searchBar != nil { + searchBar := b.GetItem(0) + b.searchBar = nil + b.RemoveItem(searchBar) + return + } + + // Add the search bar re-adding the content so ordering is preserved + searchBar := newSearchBar() + b.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + searchBar.InputHandler()(event, func(p tview.Primitive) {}) + return event + }) + b.searchBar = searchBar + content := b.GetItem(0) + b.RemoveItem(content) + b.AddItem(searchBar, 3, 0, true) + b.AddItem(content, 0, 1, false) } func (b *ContentBox) Draw(screen tcell.Screen) { - b.Box.DrawForSubclass(screen, b) - x, y, w, h := b.GetInnerRect() + b.Flex.Draw(screen) + + x, y, w, h := b.box.GetInnerRect() b.content.SetRect(x, y, w, h) b.content.Draw(screen) @@ -28,6 +56,21 @@ func (b *ContentBox) Draw(screen tcell.Screen) { func (b *ContentBox) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { return b.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + if b.searchBar != nil { + return + } + b.content.InputHandler()(event, setFocus) }) } + +func newSearchBar() *tview.InputField { + searchBar := tview.NewInputField(). + SetFieldWidth(0). + SetDoneFunc(func(key tcell.Key) { + }) + searchBar.SetBorder(true) + searchBar.SetFieldBackgroundColor(tcell.ColorNone) + + return searchBar +} From 7e3ebd32db59a8bdff6c71b9a522673f26cd848e Mon Sep 17 00:00:00 2001 From: Rob Date: Sun, 16 Mar 2025 19:30:45 +0000 Subject: [PATCH 31/33] Search logic for the connections --- app.go | 13 ++++++------- content_box.go | 9 ++++----- table.go | 33 ++++++++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/app.go b/app.go index dc145e5..24d8699 100644 --- a/app.go +++ b/app.go @@ -68,8 +68,10 @@ func (app *App) setInputHandler() { if pageName == NEW_CONNECTION_FORM || app.connectionsView.searchBar != nil { if event.Key() == tcell.KeyEscape { - app.connectionsView.toggleSearchBar() + app.connectionsView.toggleSearchBar(nil) + app.connections.getConnections() } + return event } @@ -337,12 +339,9 @@ func (app App) openConnection(connection Connection) { } func (app *App) showSearchInput() { - app.connectionsView.toggleSearchBar() -} - -func (app *App) searchConnections(term string) { - // Implementation would filter the connections table based on the search term - // This is a placeholder - actual implementation would depend on your data structure + app.connectionsView.toggleSearchBar(func(value string) { + app.connections.updateTable(value) + }) } // Sorting functionality diff --git a/content_box.go b/content_box.go index 35baadb..23e7f64 100644 --- a/content_box.go +++ b/content_box.go @@ -24,7 +24,7 @@ func newContentBox(title string, content tview.Primitive) *ContentBox { return &ContentBox{container, content, box, nil} } -func (b *ContentBox) toggleSearchBar() { +func (b *ContentBox) toggleSearchBar(searchFunc func(value string)) { if b.searchBar != nil { searchBar := b.GetItem(0) b.searchBar = nil @@ -33,7 +33,7 @@ func (b *ContentBox) toggleSearchBar() { } // Add the search bar re-adding the content so ordering is preserved - searchBar := newSearchBar() + searchBar := newSearchBar(searchFunc) b.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { searchBar.InputHandler()(event, func(p tview.Primitive) {}) return event @@ -64,11 +64,10 @@ func (b *ContentBox) InputHandler() func(event *tcell.EventKey, setFocus func(p }) } -func newSearchBar() *tview.InputField { +func newSearchBar(apply func(value string)) *tview.InputField { searchBar := tview.NewInputField(). SetFieldWidth(0). - SetDoneFunc(func(key tcell.Key) { - }) + SetChangedFunc(apply) searchBar.SetBorder(true) searchBar.SetFieldBackgroundColor(tcell.ColorNone) diff --git a/table.go b/table.go index 119405a..42ee24d 100644 --- a/table.go +++ b/table.go @@ -1,6 +1,10 @@ package main -import "github.com/rivo/tview" +import ( + "strings" + + "github.com/rivo/tview" +) type DisplayTable struct { *tview.Table @@ -50,6 +54,33 @@ func (t *DisplayTable) getConnection() *Connection { return &t.rows[row-1] } +func (t *DisplayTable) updateTable(filter string) { + t.Clear() + + for i, header := range t.columns { + t.SetCell(0, i, tview.NewTableCell(header).SetExpansion(1)) + } + + row := 1 + for _, conn := range t.rows { + lowerFilter := strings.ToLower(filter) + + if filter == "" || + strings.Contains(strings.ToLower(conn.Host), lowerFilter) || + strings.Contains(strings.ToLower(conn.User), lowerFilter) || + strings.Contains(strings.ToLower(conn.Database), lowerFilter) || + strings.Contains(strings.ToLower(conn.Name), lowerFilter) { + + values := conn.Row() + for j, value := range values { + t.SetCell(row, j, tview.NewTableCell(value)) + } + + row++ + } + } +} + type DbTable struct { *tview.Table table Table From 0721cc14c77d05a762c2c812a3d2f4fd88c4da29 Mon Sep 17 00:00:00 2001 From: Rob Date: Sat, 12 Apr 2025 18:22:19 +0100 Subject: [PATCH 32/33] Fixed bug when there are no tables in schema --- open_connection.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/open_connection.go b/open_connection.go index 9812542..431ef9e 100644 --- a/open_connection.go +++ b/open_connection.go @@ -4,15 +4,8 @@ import "log" type ViewMode string -const ( - TABLES ViewMode = "TABLES" - OPEN ViewMode = "OPEN" - QUIT ViewMode = "QUIT" -) - type OpenDatabase struct { tables []string - viewMode ViewMode params Connection openTable Table } @@ -21,9 +14,8 @@ func NewOpenDatabase(connParams Connection) OpenDatabase { databaseTables := connParams.GetTableNames() openDatabase := OpenDatabase{ - tables: databaseTables, - viewMode: TABLES, - params: connParams, + tables: databaseTables, + params: connParams, } openDatabase.setOpenTable() @@ -32,6 +24,10 @@ func NewOpenDatabase(connParams Connection) OpenDatabase { } func (db *OpenDatabase) setOpenTable() { + if len(db.tables) == 0 { + return + } + tableName := db.tables[0] table, err := db.params.SelectAll(tableName) From e6439b5fc121514c38936850a24dd73535d3a20b Mon Sep 17 00:00:00 2001 From: Rob Date: Sun, 13 Apr 2025 01:50:42 +0100 Subject: [PATCH 33/33] Refactoring and added schema view to the database flow --- app.go | 259 +++++++++++++++++++++++++-------------------- constants.go | 1 + db.go | 37 ++++++- open_connection.go | 49 +++++++-- table.go | 48 ++++++--- 5 files changed, 253 insertions(+), 141 deletions(-) diff --git a/app.go b/app.go index 24d8699..9501072 100644 --- a/app.go +++ b/app.go @@ -14,6 +14,7 @@ type App struct { content *tview.Pages hotkeys *tview.Pages connections *DisplayTable + databaseTable *DbTable connectionsView *ContentBox } @@ -44,7 +45,7 @@ func NewApp() App { app.SetRoot(pages, true).SetFocus(content) - ctx := App{app, NewConnection(), pages, content, hotkeys, &DisplayTable{}, nil} + ctx := App{app, NewConnection(), pages, content, hotkeys, &DisplayTable{}, nil, nil} ctx.setInputHandler() ctx.refreshConnections() @@ -75,128 +76,145 @@ func (app *App) setInputHandler() { return event } - if currentHotkeys == "connectionHotkeys" { - switch event.Rune() { - case 'q': - app.Stop() - return nil - case 'n': - app.showNewConnectionForm(NewConnection()) - return nil - case 'e': - connection := app.connections.getConnection() - if connection != nil { - app.showNewConnectionForm(*connection) - } - return nil - case 'd': - connection := app.connections.getConnection() - if connection != nil { - app.showDeleteConfirmation(*connection) - } - return nil - case 'o': - connection := app.connections.getConnection() - if connection != nil { - app.openConnection(*connection) + switch currentHotkeys { + case "connectionHotkeys": + { + switch event.Rune() { + case 'q': + app.Stop() + return nil + case 'n': + app.showNewConnectionForm(NewConnection()) + return nil + case 'e': + connection := app.connections.getConnection() + if connection != nil { + app.showNewConnectionForm(*connection) + } + return nil + case 'd': + connection := app.connections.getConnection() + if connection != nil { + app.showDeleteConfirmation(*connection) + } + return nil + case 'o': + connection := app.connections.getConnection() + if connection != nil { + app.openConnection(*connection) + } + return nil + case 't': + connection := app.connections.getConnection() + if connection != nil { + testResult := connection.TestConnection() + closeModal := func() { + app.pages.RemovePage(CONNECTION_TEST) + } + + switch testResult { + case PASSED: + app.showInfoModal(CONNECTION_TEST, "Connection successful!", closeModal) + case FAILED: + app.showInfoModal(CONNECTION_TEST, "Connection failed. Please check your settings.", closeModal) + } + } + return nil + case 'r': + app.refreshConnections() + return nil + case '/': + app.showSearchInput() + return nil + case 's': + app.sortConnectionsByName() + return nil + case '?': + app.showHelpView() + return nil } - return nil - case 't': - connection := app.connections.getConnection() - if connection != nil { - testResult := connection.TestConnection() - closeModal := func() { - app.pages.RemovePage(CONNECTION_TEST) + + // Handle special keys + switch event.Key() { + case tcell.KeyESC: + if contentView == DATABASE_VIEW { + app.returnToConnectionsView() + + return nil } + app.closeModals() - switch testResult { - case PASSED: - app.showInfoModal(CONNECTION_TEST, "Connection successful!", closeModal) - case FAILED: - app.showInfoModal(CONNECTION_TEST, "Connection failed. Please check your settings.", closeModal) + return nil + case tcell.KeyEnter: + if contentView != DATABASE_VIEW && pageName != CONFIRM_DELETE && pageName != CONNECTION_TEST { + connection := app.connections.getConnection() + if connection != nil { + app.openConnection(*connection) + } } + + return event } - return nil - case 'r': - app.refreshConnections() - return nil - case '/': - app.showSearchInput() - return nil - case 's': - app.sortConnectionsByName() - return nil - case '?': - app.showHelpView() - return nil } - - // Handle special keys - switch event.Key() { - case tcell.KeyESC: - if contentView == DATABASE_VIEW { + case "databaseHotkeys": + { + // Handle database view hotkeys + switch event.Rune() { + case 'q': + app.Stop() + return nil + case 'b': app.returnToConnectionsView() return nil + case 'r': + app.refreshCurrentConnection() + return nil + case 'e': + app.showQueryEditor() + return nil + case 'x': + app.showExportOptions() + return nil + case 'f': + app.showFilterInput() + return nil + case 'c': + app.copySelectedRow() + return nil + case 'y': + app.copySelectedCell() + return nil + case 'n': + app.goToNextPage() + return nil + case 'p': + app.goToPreviousPage() + return nil + case 'v': + app.toggleViewMode() + return nil + case '?': + app.showHelpView() + return nil } - app.closeModals() - return nil - case tcell.KeyEnter: - if contentView != DATABASE_VIEW && pageName != CONFIRM_DELETE && pageName != CONNECTION_TEST { - connection := app.connections.getConnection() - if connection != nil { - app.openConnection(*connection) + + // Handle special keys + switch event.Key() { + case tcell.KeyESC: + app.returnToConnectionsView() + return nil + case tcell.KeyEnter: + if contentView == DATABASE_VIEW && app.databaseTable.db.schema != "" { + app.databaseTable.showTables() + } else if contentView == DATABASE_VIEW && app.databaseTable.db.schema == "" { + app.databaseTable.showSchema() + } else { + panic(contentView) } - } - return event - } - } else if currentHotkeys == "databaseHotkeys" { - // Handle database view hotkeys - switch event.Rune() { - case 'q': - app.Stop() - return nil - case 'b': - app.returnToConnectionsView() - return nil - case 'r': - app.refreshCurrentConnection() - return nil - case 'e': - app.showQueryEditor() - return nil - case 'x': - app.showExportOptions() - return nil - case 'f': - app.showFilterInput() - return nil - case 'c': - app.copySelectedRow() - return nil - case 'y': - app.copySelectedCell() - return nil - case 'n': - app.goToNextPage() - return nil - case 'p': - app.goToPreviousPage() - return nil - case 'v': - app.toggleViewMode() - return nil - case '?': - app.showHelpView() - return nil - } - // Handle special keys - switch event.Key() { - case tcell.KeyESC: - app.returnToConnectionsView() - return nil + } } - } else if currentHotkeys == "helpHotkeys" { + case "helpHotkeys": // Handle help view hotkeys switch event.Rune() { case 'q': @@ -213,7 +231,8 @@ func (app *App) setInputHandler() { app.closeHelpView() return nil } - } else if currentHotkeys == "queryHotkeys" { + + case "queryHotkeys": // Handle query editor hotkeys switch event.Key() { case tcell.KeyESC: @@ -235,7 +254,8 @@ func (app *App) setInputHandler() { app.showQueryHistory() return nil } - } else if currentHotkeys == "exportHotkeys" { + + case "exportHotkeys": // Handle export options hotkeys switch event.Rune() { case 'c': @@ -255,6 +275,8 @@ func (app *App) setInputHandler() { app.closeExportOptions() return nil } + + return event } return event @@ -323,11 +345,13 @@ func (app *App) closeModals() { app.SetFocus(app.content) } -func (app App) openConnection(connection Connection) { +func (app *App) openConnection(connection Connection) { db := NewOpenDatabase(connection) - dbTable := newDbTable(db.openTable) + dbTable := newDbTable(db) dbContent := newContentBox(db.openTable.name, dbTable) + app.databaseTable = dbTable + app.hotkeys.SwitchToPage("databaseHotkeys") layoutHeader := headerPanel(connection, app.hotkeys) @@ -520,12 +544,13 @@ func (app *App) showFilterInput() { SetLabel("Filter: "). SetFieldWidth(30). SetDoneFunc(func(key tcell.Key) { - if key == tcell.KeyEnter { + switch key { + case tcell.KeyEnter: filterTerm := inputField.GetText() app.filterResults(filterTerm) app.pages.RemovePage("filterInput") app.SetFocus(app.content) - } else if key == tcell.KeyEscape { + case tcell.KeyEscape: app.pages.RemovePage("filterInput") app.SetFocus(app.content) } diff --git a/constants.go b/constants.go index 4172bef..a2deb3e 100644 --- a/constants.go +++ b/constants.go @@ -38,6 +38,7 @@ const ( NEW_CONNECTION_FORM = "newConnection" SAVED_CONNECTIONS = "savedConnections" DATABASE_VIEW = "databaseView" + SCHEMA_VIEW = "schemaView" CONNECTION_TEST = "ConntectionTest" CONFIRM_DELETE = "ConfirmDelete" ) diff --git a/db.go b/db.go index 1e4d6d6..493238b 100644 --- a/db.go +++ b/db.go @@ -60,18 +60,49 @@ func (params *Connection) TestConnection() TestStatus { return PASSED } -func (parmas Connection) GetTableNames() []string { - connectionString := parmas.ConnectionString() +func (params Connection) GetSchemas() []string { + connectionString := params.ConnectionString() conn, err := pgx.Connect(context.Background(), connectionString) if err != nil { return nil } - rows, err := conn.Query(context.Background(), "SELECT table_name FROM information_schema.tables WHERE table_schema='public'") + rows, err := conn.Query(context.Background(), "SELECT schema_name FROM information_schema.schemata") + if err != nil { + return nil + } + + var schemaNames []string + + for rows.Next() { + var schemaName string + + err = rows.Scan(&schemaName) + if err != nil { + return nil + } + + schemaNames = append(schemaNames, schemaName) + } + + conn.Close(context.Background()) + + return schemaNames +} + +func (parmas Connection) GetTableNames(schema string) []string { + connectionString := parmas.ConnectionString() + conn, err := pgx.Connect(context.Background(), connectionString) if err != nil { return nil } + query := fmt.Sprintf("SELECT table_name FROM information_schema.tables WHERE table_schema = '%s'", schema) + rows, err := conn.Query(context.Background(), query) + if err != nil { + return []string{"No tables in schema"} + } + var tableNames []string for rows.Next() { diff --git a/open_connection.go b/open_connection.go index 431ef9e..2a5e42a 100644 --- a/open_connection.go +++ b/open_connection.go @@ -1,34 +1,61 @@ package main -import "log" +import ( + "log" +) type ViewMode string type OpenDatabase struct { - tables []string params Connection openTable Table + schema string } func NewOpenDatabase(connParams Connection) OpenDatabase { - databaseTables := connParams.GetTableNames() - openDatabase := OpenDatabase{ - tables: databaseTables, params: connParams, } - openDatabase.setOpenTable() + openDatabase.setSchemas() return openDatabase } -func (db *OpenDatabase) setOpenTable() { - if len(db.tables) == 0 { +func (db *OpenDatabase) setSchemas() { + schemas := db.params.GetSchemas() + + db.setTable(schemas, "schema", "schema_names") +} + +func (db *OpenDatabase) getTablesInSchema() { + tables := db.params.GetTableNames(db.schema) + + db.setTable(tables, "table", "table_names") +} + +func (db *OpenDatabase) setTable(tables []string, name, title string) { + if len(tables) == 0 { + return + } + + rows := make([][]string, len(tables)) + + for i, table := range tables { + rows[i] = []string{table} + } + + db.openTable = Table{name: name, fields: []string{title}, values: rows} +} + +func (db *OpenDatabase) setOpenTable(index int) { + tables := db.params.GetTableNames(db.schema) + + if len(tables) <= index { return } - tableName := db.tables[0] + tableName := tables[index] table, err := db.params.SelectAll(tableName) if err != nil { @@ -39,3 +66,7 @@ func (db *OpenDatabase) setOpenTable() { db.openTable = table } + +func (db *OpenDatabase) setSchema(schema string) { + db.schema = schema +} diff --git a/table.go b/table.go index 42ee24d..0e25109 100644 --- a/table.go +++ b/table.go @@ -83,29 +83,53 @@ func (t *DisplayTable) updateTable(filter string) { type DbTable struct { *tview.Table - table Table + db OpenDatabase } -func newDbTable(table Table) *DbTable { +func newDbTable(db OpenDatabase) *DbTable { t := tview.NewTable() - - for i, header := range table.fields { - t.SetCell(0, i, tview.NewTableCell(header).SetExpansion(1)) - } - t.SetBorderPadding(0, 0, 1, 1) t.SetSelectable(true, false).Select(1, 0) - connectionsTable := DbTable{t, table} - connectionsTable.getTableRows() + connectionsTable := DbTable{t, db} + connectionsTable.setTableRows() return &connectionsTable } -func (t *DbTable) getTableRows() { - for i, conn := range t.table.values { - for j, value := range conn { +func (t *DbTable) setTableRows() { + table := t.db.openTable + + for i, header := range table.fields { + t.SetCell(0, i, tview.NewTableCell(header).SetExpansion(1)) + } + + for i, value := range t.db.openTable.values { + for j, value := range value { t.SetCell(i+1, j, tview.NewTableCell(value)) } } } + +func (t *DbTable) showSchema() { + row, col := t.GetSelection() + + cell := t.GetCell(row, col) + t.db.setSchema(cell.Text) + + t.db.getTablesInSchema() + + t.Clear().ScrollToBeginning() + + t.setTableRows() +} + +func (t *DbTable) showTables() { + row, _ := t.GetSelection() + + t.db.setOpenTable(row) + + t.Clear().ScrollToBeginning() + + t.setTableRows() +}