Rebuilding the UI models
Rebuilding the UI model with brand new utility models for layout and dealing with model stuff.
This commit is contained in:
parent
6ac22aad1f
commit
b0909ffe4e
|
@ -6,6 +6,7 @@ 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"
|
||||
|
@ -14,6 +15,10 @@ import (
|
|||
"github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels"
|
||||
"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/gopkgs/cli"
|
||||
"log"
|
||||
"os"
|
||||
|
@ -55,8 +60,31 @@ func main() {
|
|||
})
|
||||
|
||||
uiModel := ui.NewModel(uiDispatcher, commandController, tableReadController, tableWriteController)
|
||||
p := tea.NewProgram(uiModel, tea.WithAltScreen())
|
||||
loopback.program = p
|
||||
|
||||
// TEMP
|
||||
_ = 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"))),
|
||||
))
|
||||
|
||||
//frameSet := frameset.New([]frameset.Frame{
|
||||
// {
|
||||
// Header: "Frame 1",
|
||||
// Model: newTestModel("this is model 1"),
|
||||
// },
|
||||
// {
|
||||
// Header: "Frame 2",
|
||||
// Model: newTestModel("this is model 2"),
|
||||
// },
|
||||
//})
|
||||
//
|
||||
//modal := modal.New(frameSet)
|
||||
|
||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
||||
//loopback.program = p
|
||||
|
||||
// TEMP -- profiling
|
||||
//cf, err := os.Create("trace.out")
|
||||
|
@ -91,3 +119,18 @@ type msgLoopback struct {
|
|||
func (m *msgLoopback) Send(msg tea.Msg) {
|
||||
m.program.Send(msg)
|
||||
}
|
||||
|
||||
func newTestModel(descr string) tea.Model {
|
||||
return teamodels.TestModel{
|
||||
Message: descr,
|
||||
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)"))
|
||||
} else if k == "k" {
|
||||
return modal.PopMode
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
55
internal/dynamo-browse/ui/teamodels/frame/frame.go
Normal file
55
internal/dynamo-browse/ui/teamodels/frame/frame.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package frame
|
||||
|
||||
import (
|
||||
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"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
activeHeaderStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#4479ff"))
|
||||
)
|
||||
|
||||
// Frame is a frame that appears in the
|
||||
type Frame struct {
|
||||
header string
|
||||
model layout.ResizingModel
|
||||
width int
|
||||
}
|
||||
|
||||
func NewFrame(header string, model layout.ResizingModel) Frame {
|
||||
return Frame{header, model, 0}
|
||||
}
|
||||
|
||||
func (f Frame) Init() tea.Cmd {
|
||||
return f.model.Init()
|
||||
}
|
||||
|
||||
func (f Frame) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
newModel, cmd := f.model.Update(msg)
|
||||
f.model = newModel.(layout.ResizingModel)
|
||||
return f, cmd
|
||||
}
|
||||
|
||||
func (f Frame) View() string {
|
||||
return lipgloss.JoinVertical(lipgloss.Top, f.headerView(), f.model.View())
|
||||
}
|
||||
|
||||
func (f Frame) Resize(w, h int) layout.ResizingModel {
|
||||
f.width = w
|
||||
headerHeight := lipgloss.Height(f.headerView())
|
||||
f.model = f.model.Resize(w, h-headerHeight)
|
||||
return f
|
||||
}
|
||||
|
||||
func (f Frame) headerView() string {
|
||||
titleText := f.header
|
||||
title := activeHeaderStyle.Render(titleText)
|
||||
line := activeHeaderStyle.Render(strings.Repeat(" ", utils.Max(0, f.width-lipgloss.Width(title))))
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
||||
}
|
38
internal/dynamo-browse/ui/teamodels/layout/fullscreen.go
Normal file
38
internal/dynamo-browse/ui/teamodels/layout/fullscreen.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package layout
|
||||
|
||||
import tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
// FullScreen returns a model which will allocate the resizing model the entire height and width of the screen.
|
||||
func FullScreen(rm ResizingModel) tea.Model {
|
||||
return fullScreenModel{submodel: rm}
|
||||
}
|
||||
|
||||
type fullScreenModel struct {
|
||||
w, h int
|
||||
submodel ResizingModel
|
||||
ready bool
|
||||
}
|
||||
|
||||
func (f fullScreenModel) Init() tea.Cmd {
|
||||
return f.submodel.Init()
|
||||
}
|
||||
|
||||
func (f fullScreenModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
f.ready = true
|
||||
f.submodel = f.submodel.Resize(msg.Width, msg.Height)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
newSubModel, cmd := f.submodel.Update(msg)
|
||||
f.submodel = newSubModel.(ResizingModel)
|
||||
return f, cmd
|
||||
}
|
||||
|
||||
func (f fullScreenModel) View() string {
|
||||
if !f.ready {
|
||||
return ""
|
||||
}
|
||||
return f.submodel.View()
|
||||
}
|
48
internal/dynamo-browse/ui/teamodels/layout/model.go
Normal file
48
internal/dynamo-browse/ui/teamodels/layout/model.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package layout
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ResizingModel is a model that handles resizing events. The submodel will not get WindowSizeMessages but will
|
||||
// guarantee to receive at least one resize event before the initial view.
|
||||
type ResizingModel interface {
|
||||
tea.Model
|
||||
Resize(w, h int) ResizingModel
|
||||
}
|
||||
|
||||
// Model takes a tea-model and displays it as a resizing model. The model will be
|
||||
// displayed with all the available space provided
|
||||
func Model(m tea.Model) ResizingModel {
|
||||
return &teaModel{submodel: m}
|
||||
}
|
||||
|
||||
type teaModel struct {
|
||||
submodel tea.Model
|
||||
w, h int
|
||||
}
|
||||
|
||||
func (t teaModel) Init() tea.Cmd {
|
||||
return t.submodel.Init()
|
||||
}
|
||||
|
||||
func (t teaModel) Update(msg tea.Msg) (m tea.Model, cmd tea.Cmd) {
|
||||
t.submodel, cmd = t.submodel.Update(msg)
|
||||
return t, cmd
|
||||
}
|
||||
|
||||
func (t teaModel) View() string {
|
||||
subview := t.submodel.View() + " (h: " + strconv.Itoa(t.h) + "\n"
|
||||
subviewHeight := lipgloss.Height(subview)
|
||||
subviewVPad := strings.Repeat("\n", utils.Max(t.h-subviewHeight-1, 0))
|
||||
return lipgloss.JoinVertical(lipgloss.Top, subview, subviewVPad)
|
||||
}
|
||||
|
||||
func (t teaModel) Resize(w, h int) ResizingModel {
|
||||
t.w, t.h = w, h
|
||||
return t
|
||||
}
|
56
internal/dynamo-browse/ui/teamodels/layout/vbox.go
Normal file
56
internal/dynamo-browse/ui/teamodels/layout/vbox.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package layout
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// VBox is a model which will display its children vertically.
|
||||
type VBox struct {
|
||||
children []ResizingModel
|
||||
}
|
||||
|
||||
func NewVBox(children ...ResizingModel) VBox {
|
||||
return VBox{children: children}
|
||||
}
|
||||
|
||||
func (vb VBox) Init() tea.Cmd {
|
||||
var cc utils.CmdCollector
|
||||
for _, c := range vb.children {
|
||||
cc.Collect(c, c.Init())
|
||||
}
|
||||
return cc.Cmd()
|
||||
}
|
||||
|
||||
func (vb VBox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cc utils.CmdCollector
|
||||
for i, c := range vb.children {
|
||||
vb.children[i] = cc.Collect(c.Update(msg)).(ResizingModel)
|
||||
}
|
||||
return vb, cc.Cmd()
|
||||
}
|
||||
|
||||
func (vb VBox) View() string {
|
||||
sb := new(strings.Builder)
|
||||
for i, c := range vb.children {
|
||||
if i > 0 {
|
||||
sb.WriteRune('\n')
|
||||
}
|
||||
sb.WriteString(c.View())
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (vb VBox) Resize(w, h int) ResizingModel {
|
||||
childrenHeight := h / len(vb.children)
|
||||
lastChildRem := h % len(vb.children)
|
||||
for i, c := range vb.children {
|
||||
if i == len(vb.children)-1 {
|
||||
vb.children[i] = c.Resize(w, childrenHeight+lastChildRem)
|
||||
} else {
|
||||
vb.children[i] = c.Resize(w, childrenHeight)
|
||||
}
|
||||
}
|
||||
return vb
|
||||
}
|
19
internal/dynamo-browse/ui/teamodels/modal/events.go
Normal file
19
internal/dynamo-browse/ui/teamodels/modal/events.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package modal
|
||||
|
||||
import tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
type newModePushed tea.Model
|
||||
|
||||
type modePopped struct{}
|
||||
|
||||
// PushMode pushes a new mode on the modal stack. The new mode will receive keyboard events.
|
||||
func PushMode(newMode tea.Model) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
return newModePushed(newMode)
|
||||
}
|
||||
}
|
||||
|
||||
// PopMode pops the top-level mode from the modal stack. If there's no modes on the stack, this method does nothing.
|
||||
func PopMode() tea.Msg {
|
||||
return modePopped{}
|
||||
}
|
71
internal/dynamo-browse/ui/teamodels/modal/model.go
Normal file
71
internal/dynamo-browse/ui/teamodels/modal/model.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package modal
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/ui/teamodels/utils"
|
||||
"log"
|
||||
)
|
||||
|
||||
// Modal is a tea model which displays modes on a stack. Only the top-level model is display and will receive
|
||||
// keyboard and mouse events.
|
||||
type Modal struct {
|
||||
baseMode tea.Model
|
||||
modeStack []tea.Model
|
||||
}
|
||||
|
||||
func New(baseMode tea.Model) Modal {
|
||||
return Modal{baseMode: baseMode}
|
||||
}
|
||||
|
||||
func (m Modal) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Modal) pushMode(model tea.Model) {
|
||||
m.modeStack = append(m.modeStack, model)
|
||||
log.Printf("pusing new mode: len = %v", len(m.modeStack))
|
||||
}
|
||||
|
||||
func (m *Modal) popMode() {
|
||||
if len(m.modeStack) > 0 {
|
||||
m.modeStack = m.modeStack[:len(m.modeStack)-1]
|
||||
}
|
||||
}
|
||||
|
||||
func (m Modal) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cc utils.CmdCollector
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case newModePushed:
|
||||
m.pushMode(msg)
|
||||
return m, nil
|
||||
case modePopped:
|
||||
m.popMode()
|
||||
return m, nil
|
||||
case tea.KeyMsg, tea.MouseMsg:
|
||||
// only notify top level stack
|
||||
if len(m.modeStack) > 0 {
|
||||
m.modeStack[len(m.modeStack)-1] = cc.Collect(m.modeStack[len(m.modeStack)-1].Update(msg))
|
||||
} else {
|
||||
m.baseMode = cc.Collect(m.baseMode.Update(msg))
|
||||
}
|
||||
default:
|
||||
// notify all modes of other events
|
||||
// TODO: is this right?
|
||||
m.baseMode = cc.Collect(m.baseMode.Update(msg))
|
||||
for i, s := range m.modeStack {
|
||||
m.modeStack[i] = cc.Collect(s.Update(msg))
|
||||
}
|
||||
}
|
||||
|
||||
return m, cc.Cmd()
|
||||
}
|
||||
|
||||
func (m Modal) View() string {
|
||||
// only show top level mode
|
||||
if len(m.modeStack) > 0 {
|
||||
log.Printf("viewing mode stack: len = %v", len(m.modeStack))
|
||||
return m.modeStack[len(m.modeStack)-1].View()
|
||||
}
|
||||
return m.baseMode.View()
|
||||
}
|
9
internal/dynamo-browse/ui/teamodels/modalevents.go
Normal file
9
internal/dynamo-browse/ui/teamodels/modalevents.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package teamodels
|
||||
|
||||
import tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
// NewModePushed pushes a new mode on the modal stack
|
||||
type NewModePushed tea.Model
|
||||
|
||||
// ModePopped pops a mode from the modal stack
|
||||
type ModePopped struct{}
|
33
internal/dynamo-browse/ui/teamodels/testmodel.go
Normal file
33
internal/dynamo-browse/ui/teamodels/testmodel.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package teamodels
|
||||
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// TestModel is a model used for testing
|
||||
type TestModel struct {
|
||||
Message string
|
||||
OnKeyPressed func(k string) tea.Cmd
|
||||
}
|
||||
|
||||
func (t TestModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t TestModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc":
|
||||
return t, tea.Quit
|
||||
default:
|
||||
return t, t.OnKeyPressed(msg.String())
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t TestModel) View() string {
|
||||
return t.Message
|
||||
}
|
8
internal/dynamo-browse/ui/teamodels/utils/minmax.go
Normal file
8
internal/dynamo-browse/ui/teamodels/utils/minmax.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package utils
|
||||
|
||||
func Max(x, y int) int {
|
||||
if x > y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
25
internal/dynamo-browse/ui/teamodels/utils/utils.go
Normal file
25
internal/dynamo-browse/ui/teamodels/utils/utils.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package utils
|
||||
|
||||
import tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
type CmdCollector struct {
|
||||
cmds []tea.Cmd
|
||||
}
|
||||
|
||||
func (c *CmdCollector) Collect(m tea.Model, cmd tea.Cmd) tea.Model {
|
||||
if cmd != nil {
|
||||
c.cmds = append(c.cmds, cmd)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (c CmdCollector) Cmd() tea.Cmd {
|
||||
switch len(c.cmds) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return c.cmds[0]
|
||||
default:
|
||||
return tea.Batch(c.cmds...)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue