package loader import ( "database/sql" "encoding/json" "fmt" tea "github.com/charmbracelet/bubbletea" "github.com/mitchellh/mapstructure" "github.com/muesli/termenv" "github.com/spf13/viper" "io/ioutil" "os" "pgm/config" "pgm/data" "pgm/db" "pgm/logger" "pgm/ui" "strconv" "strings" "time" ) type host struct { tic int t string choices []string choice int chosen bool Quitting bool Frames int File string isTest bool } var ( term = termenv.ColorProfile() subtle = makeFgStyle("241") dot = colorFg(" • ", "236") ) func Loader(t []string, s string, b string) error { var hostChoice host var isT bool isT = setTest(s) logger.Logger("[LOG] is loader test: " + strconv.FormatBool(hostChoice.isTest)) hostChoice = host{60, s, t, 0, false, false, 0, b, isT} if err := tea.NewProgram(hostChoice).Start(); err != nil { if isT == true { return nil } logger.Logger("[ERROR] tea error occured") os.Exit(1) } return nil } // method to enable go tests if set to true // loader function will test that it can load the ui to chose a database // and quits the ui func setTest(s string) bool { logger.Logger("[LOG] is test: " + s) if s == "test" { return true } return false } 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] m.chosen = true return m, Frame() } case tickMsg: if m.isTest == true { m.File = "test" m.Quitting = true return m, tea.Quit } 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 { logger.Logger("[LOG] view initialized") var s string if m.Quitting { logger.Logger("[LOG] leaving pgm") return "leaving pgm..." } if !m.chosen { s = choicesView(m) } else { tea.Quit() LoadUi(m) } return s } // Passes our config to the screen file. // screen is tasked with rendering more in depth views. func LoadUi(m host) { var g data.HostDetails var h string h, err := os.UserHomeDir() if err != nil { logger.Logger("[ERROR] uable to set home dir: " + err.Error()) os.Exit(0) } config.ReadConfig() l := viper.GetString("hosts_dir") filepath := h + "/" + l + "/" + m.File loadhost(filepath, &g) Screener(g) } // 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{} }) } // loads a yaml file into the correct format for consumption. // errs the pipeline step if there is not a configuration in this repository. func loadhost(c string, p *data.HostDetails) *data.HostDetails { logger.Logger("[LOG] file path string is: " + c) cfg, err := ioutil.ReadFile(c) if err != nil { logger.Logger("[ERROR] unable to find this filepath: " + err.Error()) os.Exit(1) } json.Unmarshal(cfg, &p) return p } func Screener(g data.HostDetails) error { var scn Scr var dbConn *sql.DB logger.Logger("[LOG] starting screen") s := data.ViperReturnKey("UserSessions") mapstructure.Decode(s, &scn) dbConn = db.DbConn(g) switch g.Username { case "test": dbConn.Close() return nil } ui.GraphicUi(dbConn) return nil } // Simple struct for a view in the UI. type Scr struct { Query string `json:"query"` Columns int `json:"columns"` UIType string `json:"uiType"` Title string `json:"title"` Quitting bool } func (m Scr) Init() tea.Cmd { return tick() } func (m Scr) View() string { return m.Query } func (m Scr) 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 } } return m, nil }