Refactor TUI rendering handling and general cleanup
Improves render handling by moving the responsibility of laying out the whole UI from each view and into the main `tui` model. Our `ViewModel` interface has now diverged from bubbletea's `Model` and introduces individual `Header`, `Content`, and `Footer` methods for rendering those UI elements. Also moved away from using value receivers on our Update and View functions (as is common across Bubbletea) to pointer receivers, which cleaned up some of the weirder aspects of the code (e.g. before we essentially had no choice but to do our rendering in `Update` in order to calculate and update the final height of the main content's `viewport`).
This commit is contained in:
@@ -7,9 +7,11 @@ import (
|
||||
"git.mlow.ca/mlow/lmcli/pkg/lmcli"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/tui/model"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
|
||||
tuiutil "git.mlow.ca/mlow/lmcli/pkg/tui/util"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/tui/views/chat"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/tui/views/conversations"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type LaunchOptions struct {
|
||||
@@ -20,13 +22,18 @@ type LaunchOptions struct {
|
||||
type Model struct {
|
||||
App *model.AppModel
|
||||
|
||||
// window size
|
||||
width int
|
||||
height int
|
||||
|
||||
// errors we will display to the user and allow them to dismiss
|
||||
errs []error
|
||||
|
||||
activeView shared.View
|
||||
views map[shared.View]shared.ViewModel
|
||||
}
|
||||
|
||||
func initialModel(ctx *lmcli.Context, opts LaunchOptions) Model {
|
||||
sharedData := shared.ViewState{}
|
||||
|
||||
func initialModel(ctx *lmcli.Context, opts LaunchOptions) *Model {
|
||||
app := &model.AppModel{
|
||||
Ctx: ctx,
|
||||
Conversation: opts.InitialConversation,
|
||||
@@ -36,20 +43,24 @@ func initialModel(ctx *lmcli.Context, opts LaunchOptions) Model {
|
||||
App: app,
|
||||
activeView: opts.InitialView,
|
||||
views: map[shared.View]shared.ViewModel{
|
||||
shared.ViewChat: chat.Chat(app, sharedData),
|
||||
shared.ViewConversations: conversations.Conversations(app, sharedData),
|
||||
shared.ViewChat: chat.Chat(app),
|
||||
shared.ViewConversations: conversations.Conversations(app),
|
||||
},
|
||||
}
|
||||
|
||||
return m
|
||||
return &m
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
func (m *Model) Init() tea.Cmd {
|
||||
cmds := []tea.Cmd{
|
||||
func() tea.Msg {
|
||||
return shared.MsgViewChange(m.activeView)
|
||||
},
|
||||
)
|
||||
}
|
||||
for _, v := range m.views {
|
||||
cmds = append(cmds, v.Init())
|
||||
}
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *Model) handleGlobalInput(msg tea.KeyMsg) tea.Cmd {
|
||||
@@ -66,8 +77,10 @@ func (m *Model) handleGlobalInput(msg tea.KeyMsg) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width, m.height = msg.Width, msg.Height
|
||||
case tea.KeyMsg:
|
||||
cmd := m.handleGlobalInput(msg)
|
||||
if cmd != nil {
|
||||
@@ -75,15 +88,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
case shared.MsgViewChange:
|
||||
m.activeView = shared.View(msg)
|
||||
view := m.views[m.activeView]
|
||||
|
||||
var cmds []tea.Cmd
|
||||
if !view.Initialized() {
|
||||
cmds = append(cmds, view.Init())
|
||||
}
|
||||
cmds = append(cmds, tea.WindowSize(), shared.ViewEnter())
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
return m, tea.Batch(tea.WindowSize(), shared.ViewEnter())
|
||||
case shared.MsgError:
|
||||
m.errs = append(m.errs, msg)
|
||||
}
|
||||
|
||||
view, cmd := m.views[m.activeView].Update(msg)
|
||||
@@ -91,8 +98,38 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
return m.views[m.activeView].View()
|
||||
func (m *Model) View() string {
|
||||
if m.width == 0 || m.height == 0 {
|
||||
// we're dimensionless!
|
||||
return ""
|
||||
}
|
||||
|
||||
header := m.views[m.activeView].Header(m.width)
|
||||
footer := m.views[m.activeView].Footer(m.width)
|
||||
fixedUIHeight := tuiutil.Height(header) + tuiutil.Height(footer)
|
||||
|
||||
errBanners := make([]string, len(m.errs))
|
||||
for idx, err := range m.errs {
|
||||
errBanners[idx] = tuiutil.ErrorBanner(err, m.width)
|
||||
fixedUIHeight += tuiutil.Height(errBanners[idx])
|
||||
}
|
||||
|
||||
content := m.views[m.activeView].Content(m.width, m.height-fixedUIHeight)
|
||||
|
||||
sections := make([]string, 0, 4)
|
||||
if header != "" {
|
||||
sections = append(sections, header)
|
||||
}
|
||||
if content != "" {
|
||||
sections = append(sections, content)
|
||||
}
|
||||
if len(errBanners) > 0 {
|
||||
sections = append(sections, lipgloss.JoinVertical(lipgloss.Left, errBanners...))
|
||||
}
|
||||
if footer != "" {
|
||||
sections = append(sections, footer)
|
||||
}
|
||||
return lipgloss.JoinVertical(lipgloss.Left, sections...)
|
||||
}
|
||||
|
||||
type LaunchOption func(*LaunchOptions)
|
||||
|
||||
Reference in New Issue
Block a user