diff --git a/cmd/add.go b/cmd/add.go index 37c74fa..235954a 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -16,49 +16,40 @@ limitations under the License. package cmd import ( - "encoding/json" - "io/ioutil" - //"encoding/json" + //"io/ioutil" + + "encoding/json" "fmt" input "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" te "github.com/muesli/termenv" "github.com/spf13/cobra" "github.com/spf13/viper" + "io/ioutil" "log" "pgm/data" "pgm/logger" - //"io/ioutil" + //"github.com/spf13/viper" //"log" - "os" + //"pgm/data" //"pgm/logger" + + "os" ) -const focusedTextColor = "212" +const focusedTextColor = "205" var ( + color = te.ColorProfile().Color focusedPrompt = te.String("> ").Foreground(color("205")).String() blurredPrompt = "> " - color = te.ColorProfile().Color focusedSubmitButton = "[ " + te.String("Submit").Foreground(color("205")).String() + " ]" blurredSubmitButton = "[ " + te.String("Submit").Foreground(color("240")).String() + " ]" ) -type dupeFileMsg string - -type dbCfg struct { - index int - dbFileName input.Model - hostName input.Model - dbName input.Model - userName input.Model - secret input.Model - submitButton string -} - // addCmd represents the add command var addCmd = &cobra.Command{ Use: "add", @@ -70,17 +61,23 @@ 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) { - result := make(chan string, 1) + // This is where we'll listen for the choice the user makes in the Bubble + // Tea program. + var cfg []string + result := make(chan []string, 1) - if err := tea.NewProgram(initialModel()).Start(); err != nil { + if err := tea.NewProgram(initialModel(result)).Start(); err != nil { fmt.Printf("could not start program: %s\n", err) os.Exit(1) } - // Print out the final choice. - if r := <-result; r != "" { - fmt.Printf("\n---\nYou chose %s!\n", r) + if cfg = <-result; len(cfg) !=0 { + configWriter(cfg) } + + + + }, } @@ -99,59 +96,78 @@ func init() { } -func (m dbCfg) Init() tea.Cmd { +type hostCfgFile struct { + index int + fn input.Model + hostname input.Model + dbname input.Model + username input.Model + secret input.Model + submitButton string + Filename chan []string +} + +func initialModel(Filename chan []string ) hostCfgFile { + fn := input.NewModel() + fn.Placeholder = "filename (no spaces)" + fn.Focus() + fn.Prompt = focusedPrompt + fn.TextColor = focusedTextColor + fn.CharLimit = 32 + + hn := input.NewModel() + hn.Placeholder = "hostname" + hn.Prompt = blurredPrompt + hn.CharLimit = 64 + + dbn := input.NewModel() + dbn.Placeholder = "dbname" + dbn.Prompt = blurredPrompt + dbn.CharLimit = 64 + + un := input.NewModel() + un.Placeholder = "username" + un.Prompt = blurredPrompt + un.CharLimit = 30 + + + secret := input.NewModel() + secret.Placeholder = "secret" + secret.Prompt = blurredPrompt + secret.CharLimit = 99 + + return hostCfgFile{0, fn, hn, dbn, un, secret,blurredSubmitButton, Filename} + +} +func (m hostCfgFile) Init() tea.Cmd { return tea.Batch( - input.Blink(m.dbFileName), - input.Blink(m.hostName), - input.Blink(m.dbName), - input.Blink(m.userName), + input.Blink(m.fn), + input.Blink(m.hostname), + input.Blink(m.dbname), + input.Blink(m.username), input.Blink(m.secret), ) } - - -func (m dbCfg) View() string { - s := "\n" - - inputs := []string{ - input.View(m.dbFileName), - input.View(m.hostName), - input.View(m.dbName), - input.View(m.userName), - input.View(m.secret), - } - - for i := 0; i < len(inputs); i++ { - s += inputs[i] - if i < len(inputs)-1 { - s += "\n" - } - } - - s += "\n\n" + m.submitButton + "\n" - return s -} - - -func (m dbCfg) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m hostCfgFile) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { - case "ctrl+c": + case "ctrl+c", "esc": + close(m.Filename) return m, tea.Quit // Cycle between inputs case "tab", "shift+tab", "enter", "up", "down": inputs := []input.Model{ - m.dbFileName, - m.hostName, - m.dbName, - m.userName, + m.fn, + m.hostname, + m.dbname, + m.username, m.secret, } @@ -160,6 +176,11 @@ func (m dbCfg) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Did the user press enter while the submit button was focused? // If so, exit. if s == "enter" && m.index == len(inputs) { + var cfg []string + for _, i := range inputs{ + cfg = append(cfg, i.Value()) + } + m.Filename <- cfg return m, tea.Quit } @@ -190,10 +211,10 @@ func (m dbCfg) Update(msg tea.Msg) (tea.Model, tea.Cmd) { inputs[i].TextColor = "" } - m.dbFileName = inputs[0] - m.hostName = inputs[1] - m.dbName = inputs[2] - m.userName = inputs[3] + m.fn = inputs[0] + m.hostname = inputs[1] + m.dbname = inputs[2] + m.username = inputs[3] m.secret = inputs[4] if m.index == len(inputs) { @@ -211,68 +232,77 @@ func (m dbCfg) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } - -func updateInputs(msg tea.Msg, m dbCfg) (dbCfg, tea.Cmd) { +// Pass messages and models through to text input components. Only text inputs +// with Focus() set will respond, so it's safe to simply update all of them +// here without any further logic. +func updateInputs(msg tea.Msg, m hostCfgFile) (hostCfgFile, tea.Cmd) { var ( cmd tea.Cmd cmds []tea.Cmd ) - m.dbFileName, cmd = input.Update(msg, m.dbFileName) + m.fn, cmd = input.Update(msg, m.fn) cmds = append(cmds, cmd) - m.hostName, cmd = input.Update(msg, m.hostName) + m.hostname, cmd = input.Update(msg, m.hostname) cmds = append(cmds, cmd) - m.dbName, cmd = input.Update(msg, m.dbName) + m.dbname, cmd = input.Update(msg, m.dbname) cmds = append(cmds, cmd) - m.userName, cmd = input.Update(msg, m.userName) + m.username, cmd = input.Update(msg, m.username) cmds = append(cmds, cmd) m.secret, cmd = input.Update(msg, m.secret) + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) } -func initialModel() dbCfg { - dbFileName := input.NewModel() - dbFileName.Placeholder = "database file name (no spaces)" - dbFileName.Focus() +func (m hostCfgFile) View() string { + s := "\n" + inputs := []string{ + input.View(m.fn), + input.View(m.hostname), + input.View(m.dbname), + input.View(m.username), + input.View(m.secret), + } - hostName := input.NewModel() - hostName.Placeholder = "hostname" - - dbName := input.NewModel() - dbName.Placeholder = "database name on host" - - userName := input.NewModel() - userName.Placeholder = "username" - - secret := input.NewModel() - secret.Placeholder = "secret" - - return dbCfg{0, dbFileName, hostName, dbName, userName, secret, blurredSubmitButton} + for i := 0; i < len(inputs); i++ { + s += inputs[i] + if i < len(inputs)-1 { + s += "\n" + } + } + s += "\n\n" + m.submitButton + "\n" + return s } - - - -// Writes configs to .pgm/hosts directory -func configWriter(t []input.Model){ +// +//// Writes configs to .pgm/hosts directory +func configWriter(s []string){ h := viper.GetString("hostsDir") var c data.HostDetails var fp string var cfg []byte - c = data.HostDetails{t[4].Value(), t[1].Value(), t[2].Value(), t[3].Value()} - cfgFileName := t[0].Value() + var isEmpty bool + isEmpty = checkEmpty(s) + if isEmpty == true { + logger.Logger("[LOG] one or more fields was empty when writing file: " + s[0] ) + fmt.Println("one or more fields was empty when submitted, please try again") + os.Exit(0) + } + c = data.HostDetails{s[4], s[1], s[2], s[3]} + cfgFileName := s[0] if _, err := os.Stat(h + "/" + cfgFileName); os.IsNotExist(err) { fp = h + "/" + cfgFileName } else { - e := "dupe" - return dupeFileMsg(e) + logger.Logger("[LOG] duplicate file name found: " + cfgFileName) + os.Exit(1) } _ = cfg f, err := os.Create(fp) @@ -293,4 +323,17 @@ func configWriter(t []input.Model){ if lgd != true { fmt.Println("failed to log") } +} + +// looks through user inputs for zero length strings +// if any one is empty we do not accept the input and exit. +func checkEmpty(s []string ) bool { + for _, ip := range s { + if len(ip) < 1 { + return true + } else { + continue + } + } + return false } \ No newline at end of file diff --git a/cmd/load.go b/cmd/load.go index caae593..2f34465 100644 --- a/cmd/load.go +++ b/cmd/load.go @@ -16,13 +16,41 @@ limitations under the License. package cmd import ( + "encoding/json" + "fmt" + tea "github.com/charmbracelet/bubbletea" + "github.com/muesli/termenv" "github.com/spf13/cobra" - "pgm/data" - "pgm/loader" - "pgm/logger" + "io/ioutil" "os" + "pgm/data" + "pgm/logger" + "strconv" + "strings" + "time" + ) +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", @@ -34,20 +62,30 @@ 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) { - var dbcfg string var title string var pgHosts []string + + var fn string + result := make(chan string, 1) + pgHosts = data.ReadHosts() title = "choose db to load, \"l\" or enter will load your choice" - err := loader.Loader(pgHosts, title, dbcfg) - if err != nil { - logger.Logger("[ERROR] loading error for testing, ignore.") + var hostChoice host + hostChoice = host{60, title, pgHosts, 0, false, false, 0, result } + if err := tea.NewProgram(hostChoice).Start(); err != nil { + logger.Logger("[ERROR] tea error occured") + os.Exit(1) + } + + // Print out the final choice. + if fn = <-result; len(fn) !=0 { + logger.Logger("[LOG] loading file: " + fn) } - os.Exit(0) }, } + func init() { rootCmd.AddCommand(loadCmd) @@ -61,3 +99,183 @@ func init() { // is called directly, e.g.: // billyCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } + + + + + +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 { + logger.Logger("[LOG] leaving pgm") + return "leaving pgm..." + + } + if !m.chosen { + s = choicesView(m) + } else { + m.Quitting = true + logger.Logger("[LOG] leaving pgm") + return "leaving pgm..." + } + 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{} + }) +} + +// 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 { + 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 +} + + +// 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 +} diff --git a/cmd/root.go b/cmd/root.go index 03fb3b2..d616422 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -66,7 +66,7 @@ func init() { // Cobra supports persistent flags, which, if defined here, // will be global for your application. - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.pgm.yaml)") + rootCmd.PersistentFlags().StringVar(&cfgFile, "hostCfgFile", "", "hostCfgFile file (default is $HOME/.pgm.yaml)") // Cobra also supports local flags, which will only run // when this action is called directly. @@ -76,7 +76,7 @@ func init() { viper.SetDefault("homeDir", homeDirectory) err = viper.ReadInConfig() if err != nil { - logger.Logger("[ERROR] viper config issue" + err.Error()) + logger.Logger("[ERROR] viper hostCfgFile issue" + err.Error()) } pgmDir := viper.GetString("pgm_dir") logsDir := viper.GetString("logs_dir") @@ -89,10 +89,10 @@ func init() { } -// initConfig reads in config file and ENV variables if set. +// initConfig reads in hostCfgFile file and ENV variables if set. func InitConfig() { if cfgFile != "" { - // Use config file from the flag. + // Use hostCfgFile file from the flag. viper.SetConfigFile(cfgFile) } else { // Find home directory. @@ -101,7 +101,7 @@ func InitConfig() { fmt.Println(err) os.Exit(1) } - // Search config in home directory with name ".pgm" (without extension). + // Search hostCfgFile in home directory with name ".pgm" (without extension). viper.AddConfigPath(home) viper.SetConfigName(".pgm") @@ -120,8 +120,8 @@ func InitConfig() { viper.AutomaticEnv() // read in environment variables that match - // If a config file is found, read it in. + // If a hostCfgFile file is found, read it in. if err := viper.ReadInConfig(); err == nil { - logger.Logger("[LOG] Using config file:"+ viper.ConfigFileUsed()) + logger.Logger("[LOG] Using hostCfgFile file:"+ viper.ConfigFileUsed()) } } diff --git a/loader/loader.go b/loader/loader.go deleted file mode 100644 index a63350c..0000000 --- a/loader/loader.go +++ /dev/null @@ -1,264 +0,0 @@ -package loader - -import ( - "encoding/json" - "fmt" - tea "github.com/charmbracelet/bubbletea" - "pgm/ui" - - "github.com/muesli/termenv" - "github.com/spf13/viper" - "io/ioutil" - "os" - "pgm/config" - "pgm/data" - "pgm/logger" - "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) - 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 { - if s == "test" { - logger.Logger("[LOG] is test: true") - return true - } - logger.Logger("[LOG] is test: false") - 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] - g := LoadUi(m) - ui.GraphicUi(g) - return m, tea.Quit - } - - 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 { - var s string - if m.Quitting { - logger.Logger("[LOG] leaving pgm") - return "leaving pgm..." - - } - if !m.chosen { - s = choicesView(m) - } else { - m.Quitting = true - logger.Logger("[LOG] leaving pgm") - return "leaving pgm..." - } - return s -} - -// Passes our config to the screen file. -// screen is tasked with rendering more in depth views. -func LoadUi(m host) data.HostDetails { - 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) - return 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 { - 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 -} - - -// 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 -} diff --git a/ui/ui.go b/ui/ui.go index 2b4748a..e69de29 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -1,120 +0,0 @@ -package ui - -import ( - "fmt" - "os" - "pgm/data" - - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/glamour" - "github.com/muesli/termenv" -) - -const content = ` -Today’s Menu - -| Name | Price | Notes | -| --- | --- | --- | -| Tsukemono | $2 | Just an appetizer | -| Tomato Soup | $4 | Made with San Marzano tomatoes | -| Okonomiyaki | $4 | Takes a few minutes to make | -| Curry | $3 | We can add squash if you’d like | - -Seasonal Dishes - -| Name | Price | Notes | -| --- | --- | --- | -| Steamed bitter melon | $2 | Not so bitter | -| Takoyaki | $3 | Fun to eat | -| Winter squash | $3 | Today it's pumpkin | - -Desserts - -| Name | Price | Notes | -| --- | --- | --- | -| Dorayaki | $4 | Looks good on rabbits | -| Banana Split | $5 | A classic | -| Cream Puff | $3 | Pretty creamy! | - -All our dishes are made in-house by Karen, our chef. Most of our ingredients -are from are our garden, or the fish market down the street. - -Some famous people that have eaten here lately: - -* [x] René Redzepi -* [x] David Chang -* [ ] Jiro Ono (maybe some day) - -Bon appétit! -` - -var term = termenv.ColorProfile() - -type example struct { - viewport viewport.Model -} - -func newExample() (*example, error) { - vp := viewport.Model{Width: 78, Height: 20} - - renderer, err := glamour.NewTermRenderer(glamour.WithStylePath("notty")) - if err != nil { - return nil, err - } - - str, err := renderer.Render(content) - if err != nil { - return nil, err - } - - vp.SetContent(str) - - return &example{ - viewport: vp, - }, nil -} - -func (e example) Init() tea.Cmd { - return nil -} - -func (e example) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "q", "ctrl+c": - return e, tea.Quit - case "a": - default: - vp, _ := viewport.Update(msg, e.viewport) - e.viewport = vp - } - case tea.WindowSizeMsg: - e.viewport.Width = msg.Width - return e, nil - } - - return e, nil -} - -func (e example) View() string { - return viewport.View(e.viewport) + e.helpView() -} - -func (e example) helpView() string { - return termenv.String("\n ↑/↓: Navigate • q: Quit\n").Foreground(term.Color("241")).String() -} - -func GraphicUi(g data.HostDetails) { - model, err := newExample() - if err != nil { - fmt.Println("Could not intialize Bubble Tea model:", err) - os.Exit(1) - } - - if err := tea.NewProgram(model).Start(); err != nil { - fmt.Println("Bummer, there's been an error:", err) - os.Exit(1) - } -}