diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9dfce13 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +# Environment Variables +CGO =1 +SRC =cmd +BUILD =build +PREFIX=$(GOPATH)/bin/ + +version?="0.0.0" +commit =`if [ -d ./.git ]; then git rev-list -1 HEAD | head -c 8; else echo release-build; fi` +date =`date "+%Y-%m-%d"` +package =main +ldflags ="-X $(package).commit=$(commit) -X $(package).version=$(version) -X $(package).date=$(date)" + +default: darwin + +main: darwin + +linux: clean + env CGO_ENABLED=$(CGO) GOOS=$@ go build -ldflags $(ldflags) -o $(BUILD)/shiny-pancake $(SRC)/main.go + +darwin: clean + env CGO_ENABLED=$(CGO) GOOS=$@ go build -ldflags $(ldflags) -o $(BUILD)/shiny-pancake $(SRC)/main.go + +clean: + rm -f $(BUILD)/* + touch $(BUILD)/.keep + +install: + mv $(BUILD)/shiny-pancake $(PREFIX) diff --git a/cmd/add.go b/cmd/add.go index 470c2a0..964364a 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -25,6 +25,7 @@ import ( "github.com/spf13/viper" "io/ioutil" "log" + "shiny-pancake/database" "os" "shiny-pancake/logger" @@ -89,7 +90,7 @@ func configWriter(f *tview.Form){ var fp string var hostFile string var cfg []byte - var c HostDetails + var c database.HostDetails var hd string hd = viper.GetString("hostsDirectory") @@ -119,7 +120,7 @@ func configWriter(f *tview.Form){ } file, err := os.Create(fp) - c = HostDetails{ip[3], hostFile, ip[1], ip[2]} + c = database.HostDetails{ip[3], hostFile, ip[1], ip[2]} if err != nil { log.Fatal("unable to create file: " + fp) } @@ -141,9 +142,3 @@ func configWriter(f *tview.Form){ } -type HostDetails struct { - Secret string `json:"secret"` - Hostname string `json:"hostname"` - DatabaseName string `json:"databaseName"` - Username string `json:"username"` -} \ No newline at end of file diff --git a/cmd/load.go b/cmd/load.go index b6284e0..31a7e73 100644 --- a/cmd/load.go +++ b/cmd/load.go @@ -16,12 +16,46 @@ limitations under the License. package cmd import ( + "encoding/json" "fmt" - + tea "github.com/charmbracelet/bubbletea" + "github.com/muesli/termenv" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" + "io/ioutil" + "log" + "os" + "shiny-pancake/database" + "shiny-pancake/internal/gui" + "shiny-pancake/logger" + "strconv" + "strings" + "time" + ) -// loadCmd represents the load command +type host struct { + tic int + t string + choices []string + choice int + chosen bool + Quitting bool + Frames int + File chan string + + +} + +var ( + term = termenv.ColorProfile() + subtle = makeFgStyle("241") + dot = colorFg(" • ", "236") +) + + +// billyCmd represents the billy command var loadCmd = &cobra.Command{ Use: "load", Short: "A brief description of your command", @@ -32,10 +66,44 @@ Cobra is a CLI library for Go that empowers applications. This application is a tool to generate the needed files to quickly create a Cobra application.`, Run: func(cmd *cobra.Command, args []string) { - fmt.Println("load called") + var title string + var pgHosts []string + var fn string + + result := make(chan string, 1) + + pgHosts = readHosts() + title = "choose db to load, \"l\" or enter will load your choice" + var hostChoice host + hostChoice = host{60, title, pgHosts, 0, false, false, 0, result } + if err := tea.NewProgram(hostChoice).Start(); err != nil { + os.Exit(1) + } + + // Print out the final choice. + if fn = <-result; len(fn) !=0 { + var hostDir string + hostDir = viper.GetString("hostsDirectory") + file, _ := ioutil.ReadFile(hostDir + "/"+ fn) + data := database.HostDetails{} + err := json.Unmarshal(file, &data) + if err != nil { + var l logger.Log + l = logger.Log{"error", logrus.Fields{"loadFailed": fn }, "config not loaded"} + logger.Lgr(&l) + fmt.Println("invalid config file. ") + os.Exit(1) + } + var l logger.Log + l = logger.Log{"info", logrus.Fields{"loaded": fn }, "config loading to ui"} + logger.Lgr(&l) + gui.Gui(data) + } + }, } + func init() { rootCmd.AddCommand(loadCmd) @@ -43,9 +111,166 @@ func init() { // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: - // loadCmd.PersistentFlags().String("foo", "", "A help for foo") + // billyCmd.PersistentFlags().String("foo", "", "A help for foo") // Cobra supports local flags which will only run when this command // is called directly, e.g.: - // loadCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + // billyCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} + + + +func readHosts() []string { + var hosts []string + var hostsDir string + hostsDir = viper.GetString("hostsDirectory") + file, err := os.Open(hostsDir) + if err != nil { + var l logger.Log + l = logger.Log{"error", logrus.Fields{"filepath": hostsDir}, "unable to access directory"} + logger.Lgr(&l) + os.Exit(0) + + } + defer file.Close() + + hosts, err = file.Readdirnames(0) // 0 to read all files and folders + if err != nil { + log.Fatal(err) + } + return hosts +} + + + +func (m host) Init() tea.Cmd { + return tick() +} + +func choicesUpdate(msg tea.Msg, m host) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + + case tea.KeyMsg: + switch msg.String() { + case "q", "esc": + m.Quitting = true + return m, tea.Quit + case "j", "down": + m.choice += 1 + if m.choice > len(m.choices) { + m.choice = len(m.choices) + } + case "k", "up": + m.choice -= 1 + if m.choice < 0 { + m.choice = 0 + } + case "l", "enter": + m.File <- m.choices[m.choice] + return m, tea.Quit + } + + case tickMsg: + if m.tic == 0 { + m.Quitting = true + return m, tea.Quit + } + m.tic -= 1 + return m, tick() + } + return m, nil +} + +// Update loop for the second view after a choice has been made +func (m host) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if msg, ok := msg.(tea.KeyMsg); ok { + k := msg.String() + if k == "q" || k == "esc" || k == "ctrl+c" { + m.Quitting = true + return m, tea.Quit + } + } + + if !m.chosen { + return choicesUpdate(msg, m) + } + return m, tea.Quit +} + +// Views return a string based on data in the host. That string which will be +// rendered to the terminal. +func (m host) View() string { + var s string + if m.Quitting { + return "leaving program..." + + } + if !m.chosen { + s = choicesView(m) + } else { + m.Quitting = true + return "leaving program..." + } + return s +} + + + +// Creates the menu for choosing a file from your configs. +// Exits after 60 seconds. +// Gets Title from outside in the load method if this... should ever be needed elsewhere. +func choicesView(m host) string { + var menu string + c := m.choice + + tpl := m.t + "\n\n" + tpl += "%s\n\n" + tpl += "menu exits in %s seconds\n\n" + tpl += subtle("j/k, up/down: select") + dot + subtle("enter: choose") + dot + subtle("q, esc: quit") + for i, s := range m.choices { + menu = menu + "::" + checkbox(s, c == i) + } + mn := strings.Replace(menu, "::", "\n", -1) + choices := fmt.Sprintf( + mn, + ) + + return fmt.Sprintf(tpl, choices, colorFg(strconv.Itoa(m.tic), "79")) +} + +// the rest of these functions are to do with looks and aesthetics + +// returns a neat checkbox +func checkbox(label string, checked bool) string { + if checked { + return colorFg("[x] "+label, "212") + } + return fmt.Sprintf("[ ] %s", label) +} + +// fun colors +func colorFg(val, color string) string { + return termenv.String(val).Foreground(term.Color(color)).String() +} + +// Return a function that will colorize the foreground of a given string. +func makeFgStyle(color string) func(string) string { + return termenv.Style{}.Foreground(term.Color(color)).Styled +} + +// Frame event +func Frame() tea.Cmd { + return tea.Tick(time.Second/60, func(time.Time) tea.Msg { + return FrameMsg{} + }) +} + +type tickMsg struct{} +type FrameMsg struct{} + +// exits if no choice is selected in a minute +func tick() tea.Cmd { + return tea.Tick(time.Second, func(time.Time) tea.Msg { + return tickMsg{} + }) } diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..17fedf6 --- /dev/null +++ b/database/database.go @@ -0,0 +1,68 @@ +package database + +import ( + _ "github.com/lib/pq" +) + +//func DbConn(c HostDetails) (*sql.DB, error) { +// var h string +// var u string +// var pw string +// var dbn string +// var psqlInfo string +// h = c.Hostname +// u = c.Username +// pw = c.Secret +// dbn = c.DatabaseName +// +// logger.Logger("[LOG] connection details: host = " + h + " username = " + u) +// +// found, testUserName := data.ViperPgmConfig("test_user", "r") +// if found == true { +// logger.Logger("[LOG] found test user in .pgm.yaml") +// } +// psqlInfo = fmt.Sprintf("host=%s port=%d user=%s "+ +// "password=%s dbname=%s sslmode=disable", +// h, 5432, u, pw, dbn) +// db, err := sql.Open("postgres", psqlInfo) +// if err != nil { +// var db *sql.DB +// switch u { +// case testUserName: +// logger.Logger("[ERROR] go tests expects to connect with the docker compose database." + +// "[ERROR] run \"docker-compose up\" and re-run tests") +// err := errors.New("unable to ping db" + dbn + "local db is closed, please run docker-compose up") +// return db, err +// default: +// logger.Logger("[ERROR] db error: " + err.Error()) +// return db, err +// } +// +// } +// logger.Logger("[LOG] pinging db " + dbn) +// ctx := context.Background() +// err = db.PingContext(ctx) +// +// if err != nil { +// var db *sql.DB +// switch u { +// case testUserName: +// logger.Logger("[ERROR] db error, unable to ping: " + err.Error()+ +// "\n[ERROR] ensure docker database is running and re-run tests") +// fmt.Println("local db is closed, please run docker-compose up") +// err1 := errors.New("unable to ping db" + dbn) +// return db, err1 +// default: +// logger.Logger("[ERROR] db error: " + err.Error()) +// return db, err +// } +// } +// return db, nil +//} + +type HostDetails struct { + Secret string `json:"secret"` + Hostname string `json:"hostname"` + DatabaseName string `json:"databaseName"` + Username string `json:"username"` +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod index 00206c3..4492d73 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,11 @@ module shiny-pancake go 1.15 require ( + github.com/charmbracelet/bubbletea v0.12.2 github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591 + github.com/lib/pq v1.8.0 github.com/mitchellh/go-homedir v1.1.0 + github.com/muesli/termenv v0.7.4 github.com/rivo/tview v0.0.0-20201018122409-d551c850a743 github.com/sirupsen/logrus v1.7.0 github.com/spf13/cobra v1.1.1 diff --git a/go.sum b/go.sum index ea7e9df..11251cc 100644 --- a/go.sum +++ b/go.sum @@ -24,7 +24,11 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/charmbracelet/bubbletea v0.12.2 h1:y9Yo2Pv8tcm3mAJsWONGsmHhzrbNxJVxpVtemikxE9A= +github.com/charmbracelet/bubbletea v0.12.2/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc= +github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -61,6 +65,7 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -105,11 +110,14 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -125,12 +133,16 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8= +github.com/muesli/termenv v0.7.4 h1:/pBqvU5CpkY53tU0vVn+xgs2ZTX63aH5nY+SSps5Xa8= +github.com/muesli/termenv v0.7.4/go.mod h1:pZ7qY9l3F7e5xsAOS0zCew2tME+p7bWeBkotCEcIIcc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -186,6 +198,8 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -243,6 +257,9 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7 h1:XtNJkfEjb4zR3q20BBBcYUykVOEMgZeIUOpBPfNYgxg= golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/gui/data.go b/internal/gui/data.go new file mode 100644 index 0000000..0256e2a --- /dev/null +++ b/internal/gui/data.go @@ -0,0 +1,367 @@ +package gui + +import ( + "database/sql" + "fmt" + "github.com/sirupsen/logrus" + "reflect" + "shiny-pancake/database" + "shiny-pancake/logger" + "strconv" + "sync" + "time" + + "github.com/gdamore/tcell/v2" + _ "github.com/lib/pq" + "github.com/rivo/tview" +) + +const ( + batchSize = 80 // The number of rows loaded per batch. + finderPage = "*finder*" // The name of the Finder page. +) + +var ( + app *tview.Application // The tview application. + pages *tview.Pages // The application pages. + finderFocus tview.Primitive // The primitive in the Finder that last had focus. +) + +// Main entry point. +func data(host database.HostDetails) { + var h string + var u string + var pw string + var dbn string + var psqlInfo string + h = host.Hostname + u = host.Username + pw = host.Secret + dbn = host.DatabaseName + // Start the application. + psqlInfo = fmt.Sprintf("host=%s port=%d user=%s "+ + "password=%s dbname=%s sslmode=disable", + h, 5432, u, pw, dbn) + app = tview.NewApplication() + finder(psqlInfo) + if err := app.Run(); err != nil { + fmt.Printf("Error running application: %s\n", err) + } +} + +// Sets up a "Finder" used to navigate the databases, tables, and columns. +func finder(connString string) { + // Create the basic objects. + databases := tview.NewList().ShowSecondaryText(false) + databases.SetBorder(true).SetTitle("Databases") + columns := tview.NewTable().SetBorders(true) + columns.SetBorder(true).SetTitle("Columns") + tables := tview.NewList() + tables.ShowSecondaryText(false). + SetDoneFunc(func() { + tables.Clear() + columns.Clear() + app.SetFocus(databases) + }) + tables.SetBorder(true).SetTitle("Tables") + + // Create the layout. + flex := tview.NewFlex(). + AddItem(databases, 0, 1, true). + AddItem(tables, 0, 1, false). + AddItem(columns, 0, 3, false) + + // We keep one connection pool per database. + dbMutex := sync.Mutex{} + dbs := make(map[string]*sql.DB) + getDB := func(database string) *sql.DB { + // Connect to a new database. + dbMutex.Lock() + defer dbMutex.Unlock() + if db, ok := dbs[database]; ok { + return db + } + db, err := sql.Open("postgres", connString) + if err != nil { + panic(err) + } + dbs[database] = db + return db + } + + // Get a list of all databases. + generalDB, err := sql.Open("postgres", connString) + if err != nil { + panic(err) + } + defer generalDB.Close() // We really close the DB because we only use it for this one query. + rows, err := generalDB.Query("select datname from pg_database where datistemplate = false") + if err != nil { + panic(err) + } + defer rows.Close() + for rows.Next() { + var dbName string + if err := rows.Scan(&dbName); err != nil { + panic(err) + } + databases.AddItem(dbName, "", 0, func() { + // A database was selected. Show all of its tables. + columns.Clear() + tables.Clear() + db := getDB(dbName) + t, err := db.Query("select table_name from information_schema.tables where table_schema not in ('information_schema','pg_catalog')") + if err != nil { + panic(err) + } + defer t.Close() + for t.Next() { + var tableName string + if err := t.Scan(&tableName); err != nil { + panic(err) + } + tables.AddItem(tableName, "", 0, nil) + } + if err := t.Err(); err != nil { + panic(err) + } + app.SetFocus(tables) + + // When the user navigates to a table, show its columns. + tables.SetChangedFunc(func(i int, tableName string, t string, s rune) { + // A table was selected. Show its columns. + columns.Clear() + c, err := db.Query(` + select + c.column_name, + c.is_nullable, + c.data_type, + c.character_maximum_length, + c.numeric_precision, + c.numeric_scale, + c.ordinal_position, + tc.constraint_type pkey + from + information_schema.columns c + left join + information_schema.constraint_column_usage as ccu + on + c.table_schema = ccu.table_schema + and c.table_name = ccu.table_name + and c.column_name = ccu.column_name + left join + information_schema.table_constraints as tc + on + ccu.constraint_schema = tc.constraint_schema + and ccu.constraint_name = tc.constraint_name + where + c.table_schema not in ('information_schema','pg_catalog') + and c.table_name = $1 + `, tableName) + if err != nil { + var l logger.Log + l = logger.Log{"error", logrus.Fields{"database": err.Error() }, "an error occured in view."} + logger.Lgr(&l) + } + defer c.Close() + columns.SetCell(0, 0, &tview.TableCell{Text: "Name", Align: tview.AlignCenter, Color: tcell.ColorYellow}). + SetCell(0, 1, &tview.TableCell{Text: "Type", Align: tview.AlignCenter, Color: tcell.ColorYellow}). + SetCell(0, 2, &tview.TableCell{Text: "Size", Align: tview.AlignCenter, Color: tcell.ColorYellow}). + SetCell(0, 3, &tview.TableCell{Text: "Null", Align: tview.AlignCenter, Color: tcell.ColorYellow}). + SetCell(0, 4, &tview.TableCell{Text: "Constraint", Align: tview.AlignCenter, Color: tcell.ColorYellow}) + for c.Next() { + var ( + columnName, isNullable, dataType string + constraintType sql.NullString + size, numericPrecision, numericScale sql.NullInt64 + ordinalPosition int + ) + if err := c.Scan(&columnName, + &isNullable, + &dataType, + &size, + &numericPrecision, + &numericScale, + &ordinalPosition, + &constraintType, + ); err != nil { + panic(err) + } + sizeText := "" + if size.Valid { + sizeText = strconv.Itoa(int(size.Int64)) + } else if numericPrecision.Valid { + sizeText = strconv.Itoa(int(numericPrecision.Int64)) + if numericScale.Valid { + sizeText += "," + strconv.Itoa(int(numericScale.Int64)) + } + } + color := tcell.ColorWhite + if constraintType.Valid { + color = map[string]tcell.Color{ + "CHECK": tcell.ColorGreen, + "FOREIGN KEY": tcell.ColorDarkMagenta, + "PRIMARY KEY": tcell.ColorRed, + "UNIQUE": tcell.ColorDarkCyan, + }[constraintType.String] + } + columns.SetCell(ordinalPosition, 0, &tview.TableCell{Text: columnName, Color: color}). + SetCell(ordinalPosition, 1, &tview.TableCell{Text: dataType, Color: color}). + SetCell(ordinalPosition, 2, &tview.TableCell{Text: sizeText, Align: tview.AlignRight, Color: color}). + SetCell(ordinalPosition, 3, &tview.TableCell{Text: isNullable, Align: tview.AlignRight, Color: color}). + SetCell(ordinalPosition, 4, &tview.TableCell{Text: constraintType.String, Align: tview.AlignLeft, Color: color}) + } + if err := c.Err(); err != nil { + panic(err) + } + }) + tables.SetCurrentItem(0) // Trigger the initial selection. + + // When the user selects a table, show its content. + tables.SetSelectedFunc(func(i int, tableName string, t string, s rune) { + content(db, dbName, tableName) + }) + }) + } + if err := rows.Err(); err != nil { + panic(err) + } + + // Set up the pages and show the Finder. + pages = tview.NewPages(). + AddPage(finderPage, flex, true, true) + app.SetRoot(pages, true) +} + +// Shows the contents of the given table. +func content(db *sql.DB, dbName, tableName string) { + finderFocus = app.GetFocus() + + // If this page already exists, just show it. + if pages.HasPage(dbName + "." + tableName) { + pages.SwitchToPage(dbName + "." + tableName) + return + } + var l logger.Log + l = logger.Log{"info", logrus.Fields{"database": tableName }, "selected table to view"} + logger.Lgr(&l) + var schemaName string + err := db.QueryRow(fmt.Sprintf("select table_schema from information_schema.tables where table_schema not in ('information_schema','pg_catalog') and table_name = '%s'", tableName)).Scan(&schemaName) + if err != nil { + panic(err) + } + + // We display the data in a table embedded in a frame. + table := tview.NewTable(). + SetFixed(1, 0). + SetBordersColor(tcell.ColorYellow) + frame := tview.NewFrame(table). + SetBorders(0, 0, 0, 0, 0, 0) + frame.SetBorder(true). + SetTitle(fmt.Sprintf(`Contents of table "%s.%s"`,schemaName, tableName)) + + // How many rows does this table have? + var rowCount int + err = db.QueryRow(fmt.Sprintf("select count(*) from %s.%s", schemaName, tableName)).Scan(&rowCount) + if err != nil { + panic(err) + } + + // Load a batch of rows. + loadRows := func(offset int) { + rows, err := db.Query(fmt.Sprintf("select * from %s.%s limit $1 offset $2", schemaName, tableName), batchSize, offset) + if err != nil { + panic(err) + } + defer rows.Close() + + // The first row in the table is the list of column names. + columnNames, err := rows.Columns() + if err != nil { + panic(err) + } + for index, name := range columnNames { + table.SetCell(0, index, &tview.TableCell{Text: name, Align: tview.AlignCenter, Color: tcell.ColorYellow}) + } + + // Read the rows. + columns := make([]interface{}, len(columnNames)) + columnPointers := make([]interface{}, len(columns)) + for index := range columnPointers { + columnPointers[index] = &columns[index] + } + for rows.Next() { + // Read the columns. + err := rows.Scan(columnPointers...) + if err != nil { + panic(err) + } + + // Transfer them to the table. + row := table.GetRowCount() + for index, column := range columns { + switch value := column.(type) { + case int64: + table.SetCell(row, index, &tview.TableCell{Text: strconv.Itoa(int(value)), Align: tview.AlignRight, Color: tcell.ColorDarkCyan}) + case float64: + table.SetCell(row, index, &tview.TableCell{Text: strconv.FormatFloat(value, 'f', 2, 64), Align: tview.AlignRight, Color: tcell.ColorDarkCyan}) + case string: + table.SetCellSimple(row, index, value) + case time.Time: + t := value.Format("2006-01-02") + table.SetCell(row, index, &tview.TableCell{Text: t, Align: tview.AlignRight, Color: tcell.ColorDarkMagenta}) + case []uint8: + str := make([]byte, len(value)) + for index, num := range value { + str[index] = byte(num) + } + table.SetCell(row, index, &tview.TableCell{Text: string(str), Align: tview.AlignRight, Color: tcell.ColorGreen}) + case nil: + table.SetCell(row, index, &tview.TableCell{Text: "NULL", Align: tview.AlignCenter, Color: tcell.ColorRed}) + default: + // We've encountered a type that we don't know yet. + t := reflect.TypeOf(value) + str := "?nil?" + if t != nil { + str = "?" + t.String() + "?" + } + table.SetCellSimple(row, index, str) + } + } + } + if err := rows.Err(); err != nil { + panic(err) + } + + // Show how much we've loaded. + frame.Clear() + loadMore := "" + if table.GetRowCount()-1 < rowCount { + loadMore = " - press Enter to load more" + } + loadMore = fmt.Sprintf("Loaded %d of %d rows%s. esc to return to previous menu", table.GetRowCount()-1, rowCount, loadMore) + frame.AddText(loadMore, false, tview.AlignCenter, tcell.ColorYellow) + } + + // Load the first batch of rows. + loadRows(0) + + // Handle key presses. + table.SetDoneFunc(func(key tcell.Key) { + switch key { + case tcell.KeyEscape: + // Go back to Finder. + pages.SwitchToPage(finderPage) + if finderFocus != nil { + app.SetFocus(finderFocus) + } + case tcell.KeyEnter: + // Load the next batch of rows. + loadRows(table.GetRowCount() - 1) + table.ScrollToEnd() + } + }) + + // Add a new page and show it. + pages.AddPage(dbName+"."+tableName, frame, true, true) +} diff --git a/internal/gui/gui.go b/internal/gui/gui.go new file mode 100644 index 0000000..d419212 --- /dev/null +++ b/internal/gui/gui.go @@ -0,0 +1,30 @@ +package gui + +import "github.com/rivo/tview" + +type panels struct { + currentPanel int + panel []panel +} + + +type state struct { + panels panels + tabBar *tview.TextView + resources resources + stopChans map[string]chan int +} + +func newState() *state { + return &state{ + stopChans: make(map[string]chan int), + } +} + +type Gui struct { + app *tview.Application + pages *tview.Pages + state *state + db *db.Database + //statsLocations *statsLocations +} \ No newline at end of file diff --git a/internal/gui/state.go b/internal/gui/state.go new file mode 100644 index 0000000..194757a --- /dev/null +++ b/internal/gui/state.go @@ -0,0 +1 @@ +package gui diff --git a/main b/main index af293bf..3efd364 100755 Binary files a/main and b/main differ