More monior TUI refactor/cleanup

`tui/tui.go` is no longer responsible for passing window resize updates
to all views, instead we request a new window size message to be sent at
the same time we enter the view, allowing the view to catch and handle
it.

Add `Initialized` to `tui/shared/View` model, now we only call
`Init` on a view before entering it for the first time, rather than
calling `Init` on all views when the application starts.

Renames file, small cleanups
This commit is contained in:
Matt Low 2024-09-16 14:04:08 +00:00
parent 7c0bfefc65
commit 24b5cdbbf6
6 changed files with 68 additions and 66 deletions

View File

@ -16,7 +16,6 @@ type LoadedConversation struct {
LastReply api.Message LastReply api.Message
} }
// AppModel represents the application data model
type AppModel struct { type AppModel struct {
Ctx *lmcli.Context Ctx *lmcli.Context
Conversations []LoadedConversation Conversations []LoadedConversation
@ -193,7 +192,6 @@ func (a *AppModel) PromptLLM(messages []api.Message, chatReplyChunks chan api.Ch
) )
} }
// Helper function
func cycleSelectedMessage(selected *api.Message, choices []api.Message, dir MessageCycleDirection) (*api.Message, error) { func cycleSelectedMessage(selected *api.Message, choices []api.Message, dir MessageCycleDirection) (*api.Message, error) {
currentIndex := -1 currentIndex := -1
for i, reply := range choices { for i, reply := range choices {

View File

@ -5,6 +5,7 @@ import (
) )
type Shared struct { type Shared struct {
Initialized bool
Width int Width int
Height int Height int
Err error Err error
@ -29,6 +30,12 @@ type (
MsgError error MsgError error
) )
func ViewEnter() tea.Cmd {
return func() tea.Msg {
return MsgViewEnter{}
}
}
func WrapError(err error) tea.Cmd { func WrapError(err error) tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
return MsgError(err) return MsgError(err)

View File

@ -26,10 +26,10 @@ type LaunchOptions struct {
type Model struct { type Model struct {
App *model.AppModel App *model.AppModel
view shared.View view shared.View
// views
chat chat.Model chat chat.Model
conversations conversations.Model conversations conversations.Model
Width int
Height int
} }
func initialModel(ctx *lmcli.Context, opts LaunchOptions) Model { func initialModel(ctx *lmcli.Context, opts LaunchOptions) Model {
@ -50,8 +50,6 @@ func initialModel(ctx *lmcli.Context, opts LaunchOptions) Model {
func (m Model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {
return tea.Batch( return tea.Batch(
m.conversations.Init(),
m.chat.Init(),
func() tea.Msg { func() tea.Msg {
return shared.MsgViewChange(m.view) return shared.MsgViewChange(m.view)
}, },
@ -96,15 +94,23 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
case shared.MsgViewChange: case shared.MsgViewChange:
m.view = shared.View(msg) m.view = shared.View(msg)
var cmds []tea.Cmd
switch m.view { switch m.view {
case shared.StateChat:
m.chat.HandleResize(m.Width, m.Height)
case shared.StateConversations: case shared.StateConversations:
m.conversations.HandleResize(m.Width, m.Height) if !m.conversations.Initialized {
cmds = append(cmds, m.conversations.Init())
m.conversations.Initialized = true
} }
return m, func() tea.Msg { return shared.MsgViewEnter(struct{}{}) } case shared.StateChat:
case tea.WindowSizeMsg: if !m.chat.Initialized {
m.Width, m.Height = msg.Width, msg.Height cmds = append(cmds, m.chat.Init())
m.chat.Initialized = true
}
}
cmds = append(cmds, tea.WindowSize(), shared.ViewEnter())
return m, tea.Batch(cmds...)
} }
var cmd tea.Cmd var cmd tea.Cmd

View File

@ -9,39 +9,9 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
func (m *Model) setMessage(i int, msg api.Message) { func (m *Model) waitForResponseChunk() tea.Cmd {
if i >= len(m.App.Messages) { return func() tea.Msg {
panic("i out of range") return msgChatResponseChunk(<-m.chatReplyChunks)
}
m.App.Messages[i] = msg
m.messageCache[i] = m.renderMessage(i)
}
func (m *Model) addMessage(msg api.Message) {
m.App.Messages = append(m.App.Messages, msg)
m.messageCache = append(m.messageCache, m.renderMessage(len(m.App.Messages)-1))
}
func (m *Model) setMessageContents(i int, content string) {
if i >= len(m.App.Messages) {
panic("i out of range")
}
m.App.Messages[i].Content = content
m.messageCache[i] = m.renderMessage(i)
}
func (m *Model) rebuildMessageCache() {
m.messageCache = make([]string, len(m.App.Messages))
for i := range m.App.Messages {
m.messageCache[i] = m.renderMessage(i)
}
}
func (m *Model) updateContent() {
atBottom := m.content.AtBottom()
m.content.SetContent(m.conversationMessagesView())
if atBottom {
m.content.GotoBottom()
} }
} }
@ -97,6 +67,7 @@ func (m *Model) updateMessageContent(message *api.Message) tea.Cmd {
func (m *Model) cycleSelectedRoot(conv *api.Conversation, dir model.MessageCycleDirection) tea.Cmd { func (m *Model) cycleSelectedRoot(conv *api.Conversation, dir model.MessageCycleDirection) tea.Cmd {
if len(m.App.RootMessages) < 2 { if len(m.App.RootMessages) < 2 {
return nil return nil
} }
@ -153,11 +124,9 @@ func (m *Model) promptLLM() tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
resp, err := m.App.PromptLLM(m.App.Messages, m.chatReplyChunks, m.stopSignal) resp, err := m.App.PromptLLM(m.App.Messages, m.chatReplyChunks, m.stopSignal)
if err != nil { if err != nil {
return msgChatResponseError(err) return msgChatResponseError(err)
} }
return msgChatResponse(resp) return msgChatResponse(resp)
} }
} }

View File

@ -11,19 +11,39 @@ import (
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
func (m *Model) HandleResize(width, height int) { func (m *Model) setMessage(i int, msg api.Message) {
m.Width, m.Height = width, height if i >= len(m.App.Messages) {
m.content.Width = width panic("i out of range")
m.input.SetWidth(width - m.input.FocusedStyle.Base.GetHorizontalFrameSize()) }
if len(m.App.Messages) > 0 { m.App.Messages[i] = msg
m.rebuildMessageCache() m.messageCache[i] = m.renderMessage(i)
m.updateContent() }
func (m *Model) addMessage(msg api.Message) {
m.App.Messages = append(m.App.Messages, msg)
m.messageCache = append(m.messageCache, m.renderMessage(len(m.App.Messages)-1))
}
func (m *Model) setMessageContents(i int, content string) {
if i >= len(m.App.Messages) {
panic("i out of range")
}
m.App.Messages[i].Content = content
m.messageCache[i] = m.renderMessage(i)
}
func (m *Model) rebuildMessageCache() {
m.messageCache = make([]string, len(m.App.Messages))
for i := range m.App.Messages {
m.messageCache[i] = m.renderMessage(i)
} }
} }
func (m *Model) waitForResponseChunk() tea.Cmd { func (m *Model) updateContent() {
return func() tea.Msg { atBottom := m.content.AtBottom()
return msgChatResponseChunk(<-m.chatReplyChunks) m.content.SetContent(m.conversationMessagesView())
if atBottom {
m.content.GotoBottom()
} }
} }
@ -31,7 +51,13 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
var cmds []tea.Cmd var cmds []tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.HandleResize(msg.Width, msg.Height) m.Width, m.Height = msg.Width, msg.Height
m.content.Width = msg.Width
m.input.SetWidth(msg.Width - m.input.FocusedStyle.Base.GetHorizontalFrameSize())
if len(m.App.Messages) > 0 {
m.rebuildMessageCache()
m.updateContent()
}
case shared.MsgViewEnter: case shared.MsgViewEnter:
// wake up spinners and cursors // wake up spinners and cursors
cmds = append(cmds, cursor.Blink, m.spinner.Tick) cmds = append(cmds, cursor.Blink, m.spinner.Tick)

View File

@ -127,11 +127,6 @@ func (m Model) Init() tea.Cmd {
return m.loadConversations() return m.loadConversations()
} }
func (m *Model) HandleResize(width, height int) {
m.Width, m.Height = width, height
m.content.Width = width
}
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
var cmds []tea.Cmd var cmds []tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
@ -139,7 +134,8 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
cmds = append(cmds, m.loadConversations()) cmds = append(cmds, m.loadConversations())
m.content.SetContent(m.renderConversationList()) m.content.SetContent(m.renderConversationList())
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.HandleResize(msg.Width, msg.Height) m.Width, m.Height = msg.Width, msg.Height
m.content.Width = msg.Width
m.content.SetContent(m.renderConversationList()) m.content.SetContent(m.renderConversationList())
case msgConversationsLoaded: case msgConversationsLoaded:
m.App.Conversations = msg m.App.Conversations = msg