From 81cd1d097162b2ca1363879285a12aecf2e335bc Mon Sep 17 00:00:00 2001 From: Leon Mika Date: Sun, 27 Mar 2022 11:40:32 +1100 Subject: [PATCH] Added status and prompt --- cmd/dynamo-browse/main.go | 15 ++- .../dynamo-browse/ui/teamodels/frame/frame.go | 26 ++++- .../ui/teamodels/statusandprompt/events.go | 25 +++++ .../ui/teamodels/statusandprompt/model.go | 94 +++++++++++++++++++ .../dynamo-browse/ui/teamodels/utils/utils.go | 6 ++ 5 files changed, 157 insertions(+), 9 deletions(-) create mode 100644 internal/dynamo-browse/ui/teamodels/statusandprompt/events.go create mode 100644 internal/dynamo-browse/ui/teamodels/statusandprompt/model.go diff --git a/cmd/dynamo-browse/main.go b/cmd/dynamo-browse/main.go index 3a6b258..c2b2385 100644 --- a/cmd/dynamo-browse/main.go +++ b/cmd/dynamo-browse/main.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/dynamodb" - "github.com/brianvoe/gofakeit/v6" tea "github.com/charmbracelet/bubbletea" "github.com/lmika/awstools/internal/common/ui/commandctrl" "github.com/lmika/awstools/internal/common/ui/dispatcher" @@ -19,6 +18,7 @@ import ( "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/frame" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout" "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/modal" + "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/statusandprompt" "github.com/lmika/gopkgs/cli" "log" "os" @@ -65,9 +65,12 @@ func main() { _ = uiModel // END TEMP - model := layout.FullScreen(layout.NewVBox( - frame.NewFrame("This is the header", layout.Model(newTestModel("this is the top"))), - frame.NewFrame("This is another header", layout.Model(newTestModel("this is the bottom"))), + model := layout.FullScreen(statusandprompt.New( + layout.NewVBox( + frame.NewFrame("This is the header", true, layout.Model(newTestModel("this is the top"))), + frame.NewFrame("This is another header", false, layout.Model(newTestModel("this is the bottom"))), + ), + "Hello world", )) //frameSet := frameset.New([]frameset.Frame{ @@ -126,7 +129,9 @@ func newTestModel(descr string) tea.Model { OnKeyPressed: func(k string) tea.Cmd { log.Println("got key press: " + k) if k == "enter" { - return modal.PushMode(newTestModel("this is mode " + gofakeit.CarModel() + " (press k to end)")) + return statusandprompt.Prompt("What is your car? ", func(val string) tea.Cmd { + return statusandprompt.SetStatus("Your car is = " + val) + }) } else if k == "k" { return modal.PopMode } diff --git a/internal/dynamo-browse/ui/teamodels/frame/frame.go b/internal/dynamo-browse/ui/teamodels/frame/frame.go index c51928a..eaf7089 100644 --- a/internal/dynamo-browse/ui/teamodels/frame/frame.go +++ b/internal/dynamo-browse/ui/teamodels/frame/frame.go @@ -13,17 +13,22 @@ var ( Bold(true). Foreground(lipgloss.Color("#ffffff")). Background(lipgloss.Color("#4479ff")) + + inactiveHeaderStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#000000")). + Background(lipgloss.Color("#d1d1d1")) ) // Frame is a frame that appears in the type Frame struct { header string + active bool model layout.ResizingModel width int } -func NewFrame(header string, model layout.ResizingModel) Frame { - return Frame{header, model, 0} +func NewFrame(header string, active bool, model layout.ResizingModel) Frame { + return Frame{header, active, model, 0} } func (f Frame) Init() tea.Cmd { @@ -31,6 +36,14 @@ func (f Frame) Init() tea.Cmd { } func (f Frame) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg.(type) { + case tea.KeyMsg: + // If frame is not active, do not receive key messages + if !f.active { + return f, nil + } + } + newModel, cmd := f.model.Update(msg) f.model = newModel.(layout.ResizingModel) return f, cmd @@ -48,8 +61,13 @@ func (f Frame) Resize(w, h int) layout.ResizingModel { } func (f Frame) headerView() string { + style := inactiveHeaderStyle + if f.active { + style = activeHeaderStyle + } + titleText := f.header - title := activeHeaderStyle.Render(titleText) - line := activeHeaderStyle.Render(strings.Repeat(" ", utils.Max(0, f.width-lipgloss.Width(title)))) + title := style.Render(titleText) + line := style.Render(strings.Repeat(" ", utils.Max(0, f.width-lipgloss.Width(title)))) return lipgloss.JoinHorizontal(lipgloss.Left, title, line) } diff --git a/internal/dynamo-browse/ui/teamodels/statusandprompt/events.go b/internal/dynamo-browse/ui/teamodels/statusandprompt/events.go new file mode 100644 index 0000000..b1739a2 --- /dev/null +++ b/internal/dynamo-browse/ui/teamodels/statusandprompt/events.go @@ -0,0 +1,25 @@ +package statusandprompt + +import tea "github.com/charmbracelet/bubbletea" + +type setStatusMsg string + +type startPromptMsg struct { + prompt string + onDone func(val string) tea.Cmd +} + +func SetStatus(newStatus string) tea.Cmd { + return func() tea.Msg { + return setStatusMsg(newStatus) + } +} + +func Prompt(prompt string, onDone func(val string) tea.Cmd) tea.Cmd { + return func() tea.Msg { + return startPromptMsg{ + prompt: prompt, + onDone: onDone, + } + } +} diff --git a/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go new file mode 100644 index 0000000..df3e5a7 --- /dev/null +++ b/internal/dynamo-browse/ui/teamodels/statusandprompt/model.go @@ -0,0 +1,94 @@ +package statusandprompt + +import ( + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/layout" + "github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/utils" +) + +// StatusAndPrompt is a resizing model which displays a submodel and a status bar. When the start prompt +// event is received, focus will be torn away and the user will be given a prompt the enter text. +type StatusAndPrompt struct { + model layout.ResizingModel + statusMessage string + pendingInput *startPromptMsg + textInput textinput.Model + width int +} + +func New(model layout.ResizingModel, initialMsg string) StatusAndPrompt { + textInput := textinput.New() + return StatusAndPrompt{model: model, statusMessage: initialMsg, textInput: textInput} +} + +func (s StatusAndPrompt) Init() tea.Cmd { + return s.model.Init() +} + +func (s StatusAndPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case setStatusMsg: + s.statusMessage = string(msg) + case startPromptMsg: + if s.pendingInput != nil { + // ignore, already in an input + return s, nil + } + + s.textInput.Prompt = msg.prompt + s.textInput.Focus() + s.textInput.SetValue("") + s.pendingInput = &msg + return s, nil + case tea.KeyMsg: + if s.pendingInput != nil { + switch msg.String() { + case "ctrl+c", "esc": + s.pendingInput = nil + case "enter": + pendingInput := s.pendingInput + s.pendingInput = nil + + return s, pendingInput.onDone(s.textInput.Value()) + } + } + } + + if s.pendingInput != nil { + var cc utils.CmdCollector + + newTextInput, cmd := s.textInput.Update(msg) + cc.Add(cmd) + s.textInput = newTextInput + + if _, isKey := msg.(tea.Key); !isKey { + s.model = cc.Collect(s.model.Update(msg)).(layout.ResizingModel) + } + + return s, cc.Cmd() + } + + newModel, cmd := s.model.Update(msg) + s.model = newModel.(layout.ResizingModel) + return s, cmd +} + +func (s StatusAndPrompt) View() string { + return lipgloss.JoinVertical(lipgloss.Top, s.model.View(), s.viewStatus()) +} + +func (s StatusAndPrompt) Resize(w, h int) layout.ResizingModel { + s.width = w + submodelHeight := h - lipgloss.Height(s.viewStatus()) + s.model = s.model.Resize(w, submodelHeight) + return s +} + +func (s StatusAndPrompt) viewStatus() string { + if s.pendingInput != nil { + return s.textInput.View() + } + return s.statusMessage +} diff --git a/internal/dynamo-browse/ui/teamodels/utils/utils.go b/internal/dynamo-browse/ui/teamodels/utils/utils.go index 91b0f06..8f8a0f6 100644 --- a/internal/dynamo-browse/ui/teamodels/utils/utils.go +++ b/internal/dynamo-browse/ui/teamodels/utils/utils.go @@ -6,6 +6,12 @@ type CmdCollector struct { cmds []tea.Cmd } +func (c *CmdCollector) Add(cmd tea.Cmd) { + if cmd != nil { + c.cmds = append(c.cmds, cmd) + } +} + func (c *CmdCollector) Collect(m tea.Model, cmd tea.Cmd) tea.Model { if cmd != nil { c.cmds = append(c.cmds, cmd)