From 89f9cf27383a908ba616905076f01d0c508e3aa4 Mon Sep 17 00:00:00 2001 From: Evan Harris Date: Wed, 18 Sep 2024 16:03:33 -0400 Subject: [PATCH] Support displaying images data from `tui i` (#55) * Add lipgloss table Signed-off-by: Evan Harris * Add lipgloss table part 2 Signed-off-by: Evan Harris * Add stub for model Signed-off-by: Evan Harris * Make fmt Signed-off-by: Evan Harris * Add debug model stub Signed-off-by: Evan Harris * Rename welcome model -> home Signed-off-by: Evan Harris * Do not print to the console when in tui mode Signed-off-by: Evan Harris * Support tui -> i Signed-off-by: Evan Harris * Rm dead code Signed-off-by: Evan Harris * Reenable debug and dry up code Signed-off-by: Evan Harris * Run Fmt Signed-off-by: Evan Harris * Use a bubble tea specific name Signed-off-by: Evan Harris * Use bubble tea specific naming for image model Signed-off-by: Evan Harris * Delete duplicate images TUI model Signed-off-by: Evan Harris * Swap in bubble tea specific names for images model Signed-off-by: Evan Harris * Swap in bubble tea specific names for home model struct Signed-off-by: Evan Harris * Swap out Models for TUIs to improve naming for manging multiple tui models Signed-off-by: Evan Harris * Swap in bubble tea specific names for debug tui Signed-off-by: Evan Harris --------- Signed-off-by: Evan Harris --- pkg/app/master/command/images/cli.go | 22 +++--- pkg/app/master/command/images/handler.go | 25 +++---- .../master/{tui => command}/images/model.go | 69 ++++++++++++++----- pkg/app/master/command/tui/cli.go | 10 ++- pkg/app/master/tui/common/const.go | 10 ++- pkg/app/master/tui/common/event.go | 15 ++++ pkg/app/master/tui/debug/model.go | 67 ++++++++++++++++++ pkg/app/master/tui/home/model.go | 56 +++++++-------- pkg/app/master/tui/keys/keys.go | 4 +- pkg/app/master/tui/tui.go | 19 +---- 10 files changed, 208 insertions(+), 89 deletions(-) rename pkg/app/master/{tui => command}/images/model.go (69%) create mode 100644 pkg/app/master/tui/common/event.go create mode 100644 pkg/app/master/tui/debug/model.go diff --git a/pkg/app/master/command/images/cli.go b/pkg/app/master/command/images/cli.go index 3a19bc7c..d0bc0db3 100644 --- a/pkg/app/master/command/images/cli.go +++ b/pkg/app/master/command/images/cli.go @@ -15,9 +15,10 @@ const ( ) type CommandParams struct { - Runtime string `json:"runtime,omitempty"` - Filter string `json:"filter,omitempty"` - TUI bool `json:"tui"` + Runtime string `json:"runtime,omitempty"` + Filter string `json:"filter,omitempty"` + TUI bool `json:"tui,omitempty"` + GlobalTUI bool `json:"globalTui,omitempty"` } var ImagesFlags = []cli.Flag{ @@ -40,17 +41,20 @@ var CLI = &cli.Command{ return command.ErrNoGlobalParams } - xc := app.NewExecutionContext( - Name, - gcvalues.QuietCLIMode, - gcvalues.OutputFormat) - + tuiMode := ctx.Bool(FlagTUI) cparams := &CommandParams{ Runtime: ctx.String(command.FlagRuntime), Filter: ctx.String(FlagFilter), - TUI: ctx.Bool(FlagTUI), + TUI: tuiMode, } + quietLogs := tuiMode || gcvalues.QuietCLIMode + + xc := app.NewExecutionContext( + Name, + quietLogs, + gcvalues.OutputFormat) + OnCommand(xc, gcvalues, cparams) return nil }, diff --git a/pkg/app/master/command/images/handler.go b/pkg/app/master/command/images/handler.go index b0f3c500..144abb63 100644 --- a/pkg/app/master/command/images/handler.go +++ b/pkg/app/master/command/images/handler.go @@ -13,7 +13,6 @@ import ( "github.com/mintoolkit/mint/pkg/app" "github.com/mintoolkit/mint/pkg/app/master/command" "github.com/mintoolkit/mint/pkg/app/master/tui" - imagesModel "github.com/mintoolkit/mint/pkg/app/master/tui/images" "github.com/mintoolkit/mint/pkg/app/master/version" cmd "github.com/mintoolkit/mint/pkg/command" "github.com/mintoolkit/mint/pkg/crt" @@ -34,13 +33,14 @@ type ovars = app.OutVars func OnCommand( xc *app.ExecutionContext, gparams *command.GenericParams, - cparams *CommandParams) { + cparams *CommandParams) map[string]crt.BasicImageInfo { const cmdName = Name - logger := log.WithFields(log.Fields{"app": appName, "cmd": cmdName}) + logger := log.WithFields(log.Fields{"app": appName, "cmd": cmdName}) viChan := version.CheckAsync(gparams.CheckVersion, gparams.InContainer, gparams.IsDSImage) cmdReport := report.NewImagesCommand(gparams.ReportLocation, gparams.InContainer) + cmdReport.State = cmd.StateStarted xc.Out.State(cmd.StateStarted) @@ -128,20 +128,20 @@ func OnCommand( images, err := crtClient.ListImages(cparams.Filter) xc.FailOn(err) - if cparams.TUI { - standalone := true - model := imagesModel.InitialModel(images, standalone) - tui.RunTUI(model, standalone) - } - - if xc.Out.Quiet { + if cparams.TUI { // `images --tui` + initialTUI := InitialTUI(images, true) + tui.RunTUI(initialTUI, true) + return nil + } else if cparams.GlobalTUI { // `tui` -> `i` + return images + } else if xc.Out.Quiet { if xc.Out.OutputFormat == command.OutputFormatJSON { fmt.Printf("%s\n", jsonutil.ToPretty(images)) - return + return nil } printImagesTable(images) - return + return nil } else { xc.Out.Info("image.list", ovars{"count": len(images)}) for name, info := range images { @@ -170,6 +170,7 @@ func OnCommand( "file": cmdReport.ReportLocation(), }) } + return nil } func printImagesTable(images map[string]crt.BasicImageInfo) { diff --git a/pkg/app/master/tui/images/model.go b/pkg/app/master/command/images/model.go similarity index 69% rename from pkg/app/master/tui/images/model.go rename to pkg/app/master/command/images/model.go index 28d5c6a8..69b8725e 100644 --- a/pkg/app/master/tui/images/model.go +++ b/pkg/app/master/command/images/model.go @@ -7,8 +7,10 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss/table" "github.com/dustin/go-humanize" + "github.com/mintoolkit/mint/pkg/app" + + "github.com/mintoolkit/mint/pkg/app/master/command" "github.com/mintoolkit/mint/pkg/app/master/tui/common" - "github.com/mintoolkit/mint/pkg/app/master/tui/home" "github.com/mintoolkit/mint/pkg/app/master/tui/keys" "github.com/mintoolkit/mint/pkg/crt" "github.com/mintoolkit/mint/pkg/crt/docker/dockerutil" @@ -16,12 +18,13 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -// Model represents the state of the TUI. -type Model struct { +// TUI represents the internal state of the terminal user interface. +type TUI struct { table table.Table width int height int standalone bool + loading bool } // Styles - move to `common` @@ -46,13 +49,16 @@ var ( // End styles -// InitialModel returns the initial state of the model. -func InitialModel(images map[string]crt.BasicImageInfo, standalone bool) *Model { - m := &Model{ - width: 20, - height: 15, - standalone: standalone, +func LoadTUI() *TUI { + m := &TUI{ + width: 20, + height: 15, + loading: true, } + return m +} + +func generateTable(images map[string]crt.BasicImageInfo) table.Table { var rows [][]string for k, v := range images { imageRow := []string{k, dockerutil.CleanImageID(v.ID)[:12], humanize.Time(time.Unix(v.Created, 0)), humanize.Bytes(uint64(v.Size))} @@ -79,18 +85,49 @@ func InitialModel(images map[string]crt.BasicImageInfo, standalone bool) *Model Headers("Name", "ID", "Created", "Size"). Rows(rows...) - m.table = *t + return *t +} + +// InitialTUI returns the initial state of the TUI. +func InitialTUI(images map[string]crt.BasicImageInfo, standalone bool) *TUI { + m := &TUI{ + width: 20, + height: 15, + standalone: standalone, + } + m.table = generateTable(images) return m } -func (m Model) Init() tea.Cmd { +func (m TUI) Init() tea.Cmd { // Just return `nil`, which means "no I/O right now, please." return nil } -// Update is called to handle user input and update the model's state. -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +// Update is called to handle user input and update the TUI's state. +func (m TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case common.Event: + xc := app.NewExecutionContext( + "tui", + true, + "json", + ) + + cparams := &CommandParams{ + Runtime: crt.AutoRuntime, + GlobalTUI: true, + } + + gcValue, ok := msg.Data.(*command.GenericParams) + if !ok || gcValue == nil { + return nil, nil + } + + images := OnCommand(xc, gcValue, cparams) + m.table = generateTable(images) + return m, nil case tea.WindowSizeMsg: m.table.Width(msg.Width) m.table.Height(msg.Height) @@ -100,14 +137,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, keys.Global.Quit): return m, tea.Quit case key.Matches(msg, keys.Global.Back): - return home.InitialModel() + return common.TUIsInstance.Home, nil } } return m, nil } // View returns the view that should be displayed. -func (m Model) View() string { +func (m TUI) View() string { var components []string content := m.table.String() @@ -121,7 +158,7 @@ func (m Model) View() string { ) } -func (m Model) help() string { +func (m TUI) help() string { if m.standalone { return common.HelpStyle("• q: quit") } diff --git a/pkg/app/master/command/tui/cli.go b/pkg/app/master/command/tui/cli.go index 2ec5fde1..5bf477e2 100644 --- a/pkg/app/master/command/tui/cli.go +++ b/pkg/app/master/command/tui/cli.go @@ -1,6 +1,7 @@ package tui import ( + "github.com/mintoolkit/mint/pkg/app/master/command" tui "github.com/mintoolkit/mint/pkg/app/master/tui" "github.com/mintoolkit/mint/pkg/app/master/tui/home" cmd "github.com/mintoolkit/mint/pkg/command" @@ -18,8 +19,13 @@ var CLI = &cli.Command{ Aliases: []string{Alias}, Usage: Usage, Action: func(ctx *cli.Context) error { - m, _ := home.InitialModel() - tui.RunTUI(m, false) + gcvalues, ok := command.CLIContextGet(ctx.Context, command.GlobalParams).(*command.GenericParams) + if !ok || gcvalues == nil { + return command.ErrNoGlobalParams + } + + initialTUI, _ := home.InitialTUI(gcvalues) + tui.RunTUI(initialTUI, false) return nil }, } diff --git a/pkg/app/master/tui/common/const.go b/pkg/app/master/tui/common/const.go index c15eba15..d438f7e4 100644 --- a/pkg/app/master/tui/common/const.go +++ b/pkg/app/master/tui/common/const.go @@ -6,6 +6,12 @@ import ( var ( // P the current tea program - P *tea.Program - Models []tea.Model + P *tea.Program + TUIsInstance TUIs ) + +type TUIs struct { + Home tea.Model + Images tea.Model + Debug tea.Model +} diff --git a/pkg/app/master/tui/common/event.go b/pkg/app/master/tui/common/event.go new file mode 100644 index 00000000..4adcb8b9 --- /dev/null +++ b/pkg/app/master/tui/common/event.go @@ -0,0 +1,15 @@ +package common + +const ( + GetImagesEvent EventType = "getImages" +) + +type ( + // EventType identifies the type of event + EventType string + // Event represents an event in the lifecycle of a resource + Event struct { + Type EventType + Data interface{} + } +) diff --git a/pkg/app/master/tui/debug/model.go b/pkg/app/master/tui/debug/model.go new file mode 100644 index 00000000..a01c5425 --- /dev/null +++ b/pkg/app/master/tui/debug/model.go @@ -0,0 +1,67 @@ +package debug + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/lipgloss" + "github.com/mintoolkit/mint/pkg/app/master/tui/common" + "github.com/mintoolkit/mint/pkg/app/master/tui/keys" + + tea "github.com/charmbracelet/bubbletea" +) + +// TUI represents the state of the TUI. +type TUI struct { + standalone bool +} + +// InitialTUI returns the initial state of the model. +func InitialTUI(standalone bool) *TUI { + m := &TUI{ + standalone: standalone, + } + + return m +} + +func (m TUI) Init() tea.Cmd { + // Just return `nil`, which means "no I/O right now, please." + return nil +} + +// Update is called to handle user input and update the model's state. +func (m TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, keys.Global.Quit): + return m, tea.Quit + // NOTE -> We should only support this back navigation, + // if the images tui is not standalone + case key.Matches(msg, keys.Global.Back): + return common.TUIsInstance.Home, nil + } + } + return m, nil +} + +// View returns the view that should be displayed. +func (m TUI) View() string { + var components []string + + content := "Debug support coming soon" + + components = append(components, content) + + components = append(components, m.help()) + + return lipgloss.JoinVertical(lipgloss.Left, + components..., + ) +} + +func (m TUI) help() string { + if m.standalone { + return common.HelpStyle("• q: quit") + } + return common.HelpStyle("• esc: back • q: quit") +} diff --git a/pkg/app/master/tui/home/model.go b/pkg/app/master/tui/home/model.go index 0a84500d..29c54c61 100644 --- a/pkg/app/master/tui/home/model.go +++ b/pkg/app/master/tui/home/model.go @@ -1,63 +1,61 @@ package home import ( - "log" - "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/mintoolkit/mint/pkg/app/master/command" + "github.com/mintoolkit/mint/pkg/app/master/command/images" "github.com/mintoolkit/mint/pkg/app/master/tui/common" + "github.com/mintoolkit/mint/pkg/app/master/tui/debug" "github.com/mintoolkit/mint/pkg/app/master/tui/keys" ) type mode int -const ( - nav mode = iota - image - debug -) - -// Default Model -type Model struct { - mode mode +// Default TUI +type TUI struct { + Gcvalues *command.GenericParams } -func InitialModel() (tea.Model, tea.Cmd) { - m := &Model{mode: nav} - log.Printf("Welcome model initialized: %v", m) +func InitialTUI(gcvalues *command.GenericParams) (tea.Model, tea.Cmd) { + m := &TUI{Gcvalues: gcvalues} + return m, nil } -func (m Model) Init() tea.Cmd { +func (m TUI) Init() tea.Cmd { // Just return `nil`, which means "no I/O right now, please." return nil } // Update is called to handle user input and update the model's state. -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m TUI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch { case key.Matches(msg, keys.Global.Quit): return m, tea.Quit // Quit the program. - case key.Matches(msg, keys.Welcome.Images): - m.mode = image - // TODO - Unhardcode the model index - return common.Models[1].Update(nil) + case key.Matches(msg, keys.Home.Images): + getImagesEvent := common.Event{ + Type: common.GetImagesEvent, + Data: m.Gcvalues, + } - // TODO - support debug model - // case key.Matches(msg, keys.Welcome.Debug): - // m.mode = debug - // // TODO - Unhardcode the model index - // return common.Models[2].Update(nil) + LoadTUI := images.LoadTUI() + common.TUIsInstance.Images = LoadTUI + return LoadTUI.Update(getImagesEvent) + case key.Matches(msg, keys.Home.Debug): + debugModel := debug.InitialTUI(false) + common.TUIsInstance.Debug = debugModel + return debugModel.Update(nil) } } return m, nil } // View returns the view that should be displayed. -func (m Model) View() string { +func (m TUI) View() string { content := "Dashboard\n Select which view you would like to open\n" return lipgloss.JoinVertical(lipgloss.Left, @@ -65,8 +63,6 @@ func (m Model) View() string { m.help(), ) } -func (m Model) help() string { - return common.HelpStyle("• i: Open images view • q: quit") - // TODO - support debug view - // return common.HelpStyle("• i: Open images view • d: Open debug view • q: quit") +func (m TUI) help() string { + return common.HelpStyle("• i: Open images view • d: Open debug view • q: quit") } diff --git a/pkg/app/master/tui/keys/keys.go b/pkg/app/master/tui/keys/keys.go index 7e0cc4c6..1f578859 100644 --- a/pkg/app/master/tui/keys/keys.go +++ b/pkg/app/master/tui/keys/keys.go @@ -26,12 +26,12 @@ var Global = global{ ), } -type welcome struct { +type home struct { Images key.Binding Debug key.Binding } -var Welcome = welcome{ +var Home = home{ Images: key.NewBinding( key.WithKeys("i"), key.WithHelp("i", "Open images view"), diff --git a/pkg/app/master/tui/tui.go b/pkg/app/master/tui/tui.go index 363cf045..ab53a915 100644 --- a/pkg/app/master/tui/tui.go +++ b/pkg/app/master/tui/tui.go @@ -7,8 +7,6 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/mintoolkit/mint/pkg/app/master/tui/common" - "github.com/mintoolkit/mint/pkg/app/master/tui/images" - "github.com/mintoolkit/mint/pkg/crt" ) // RunTUI starts the TUI program. @@ -18,22 +16,11 @@ func RunTUI(model tea.Model, standalone bool) { log.Printf("RunTUI Logging - %v", err) os.Exit(1) } - defer f.Close() - // We are running the tui via `debug --tui` || `images --tui` - if standalone { - common.Models = []tea.Model{model} - } else { - // TODO - support debug - // debugModel, _ := debug.InitialModel() - // common.Models = []tea.Model{model, images.InitialModel(data), debugModel} - - // TODO - rather than using mockdata, determine the flow to hydrate the model - // with images data. - mockData := make(map[string]crt.BasicImageInfo) - common.Models = []tea.Model{model, images.InitialModel(mockData, false)} + // We are running the tui via `mint tui` + if !standalone { + common.TUIsInstance.Home = model } - // We are running the tui via `mint --tui` common.P = tea.NewProgram(model, tea.WithAltScreen()) if _, err := common.P.Run(); err != nil {