TUI refactor
- Clean up, improved startup logic, initial conversation load - Moved converation/message business logic (mostly) into `model/tui`
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"git.mlow.ca/mlow/lmcli/pkg/api"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/tui/bubbles"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/tui/model"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/tui/shared"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/tui/styles"
|
||||
tuiutil "git.mlow.ca/mlow/lmcli/pkg/tui/util"
|
||||
@@ -16,40 +17,30 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type loadedConversation struct {
|
||||
conv api.Conversation
|
||||
lastReply api.Message
|
||||
}
|
||||
|
||||
type (
|
||||
// sent when conversation list is loaded
|
||||
msgConversationsLoaded ([]loadedConversation)
|
||||
msgConversationsLoaded ([]model.LoadedConversation)
|
||||
// sent when a conversation is selected
|
||||
msgConversationSelected api.Conversation
|
||||
// sent when a conversation is deleted
|
||||
msgConversationDeleted struct{}
|
||||
)
|
||||
|
||||
// Prompt payloads
|
||||
type (
|
||||
deleteConversationPayload api.Conversation
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
shared.Shared
|
||||
shared.Sections
|
||||
|
||||
conversations []loadedConversation
|
||||
cursor int // index of the currently selected conversation
|
||||
itemOffsets []int // keeps track of the viewport y offset of each rendered item
|
||||
App *model.AppModel
|
||||
cursor int
|
||||
|
||||
content viewport.Model
|
||||
itemOffsets []int // conversation y offsets
|
||||
|
||||
content viewport.Model
|
||||
confirmPrompt bubbles.ConfirmPrompt
|
||||
}
|
||||
|
||||
func Conversations(shared shared.Shared) Model {
|
||||
func Conversations(app *model.AppModel, shared shared.Shared) Model {
|
||||
m := Model{
|
||||
App: app,
|
||||
Shared: shared,
|
||||
content: viewport.New(0, 0),
|
||||
}
|
||||
@@ -67,16 +58,17 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
if len(m.conversations) > 0 && m.cursor < len(m.conversations) {
|
||||
if len(m.App.Conversations) > 0 && m.cursor < len(m.App.Conversations) {
|
||||
m.App.Conversation = &m.App.Conversations[m.cursor].Conv
|
||||
m.App.Messages = []api.Message{}
|
||||
return true, func() tea.Msg {
|
||||
return msgConversationSelected(m.conversations[m.cursor].conv)
|
||||
return shared.MsgViewChange(shared.StateChat)
|
||||
}
|
||||
}
|
||||
case "j", "down":
|
||||
if m.cursor < len(m.conversations)-1 {
|
||||
if m.cursor < len(m.App.Conversations)-1 {
|
||||
m.cursor++
|
||||
if m.cursor == len(m.conversations)-1 {
|
||||
// if last conversation, simply scroll to the bottom
|
||||
if m.cursor == len(m.App.Conversations)-1 {
|
||||
m.content.GotoBottom()
|
||||
} else {
|
||||
// this hack positions the *next* conversatoin slightly
|
||||
@@ -86,7 +78,7 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||
}
|
||||
m.content.SetContent(m.renderConversationList())
|
||||
} else {
|
||||
m.cursor = len(m.conversations) - 1
|
||||
m.cursor = len(m.App.Conversations) - 1
|
||||
m.content.GotoBottom()
|
||||
}
|
||||
return true, nil
|
||||
@@ -107,14 +99,14 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||
case "n":
|
||||
// new conversation
|
||||
case "d":
|
||||
if !m.confirmPrompt.Focused() && len(m.conversations) > 0 && m.cursor < len(m.conversations) {
|
||||
title := m.conversations[m.cursor].conv.Title
|
||||
if !m.confirmPrompt.Focused() && len(m.App.Conversations) > 0 && m.cursor < len(m.App.Conversations) {
|
||||
title := m.App.Conversations[m.cursor].Conv.Title
|
||||
if title == "" {
|
||||
title = "(untitled)"
|
||||
}
|
||||
m.confirmPrompt = bubbles.NewConfirmPrompt(
|
||||
fmt.Sprintf("Delete '%s'?", title),
|
||||
deleteConversationPayload(m.conversations[m.cursor].conv),
|
||||
m.App.Conversations[m.cursor].Conv,
|
||||
)
|
||||
m.confirmPrompt.Style = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
@@ -132,7 +124,7 @@ func (m *Model) HandleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return nil
|
||||
return m.loadConversations()
|
||||
}
|
||||
|
||||
func (m *Model) HandleResize(width, height int) {
|
||||
@@ -150,20 +142,15 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
m.HandleResize(msg.Width, msg.Height)
|
||||
m.content.SetContent(m.renderConversationList())
|
||||
case msgConversationsLoaded:
|
||||
m.conversations = msg
|
||||
m.cursor = max(0, min(len(m.conversations), m.cursor))
|
||||
m.App.Conversations = msg
|
||||
m.cursor = max(0, min(len(m.App.Conversations), m.cursor))
|
||||
m.content.SetContent(m.renderConversationList())
|
||||
case msgConversationSelected:
|
||||
m.Values.ConvShortname = msg.ShortName.String
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
return shared.MsgViewChange(shared.StateChat)
|
||||
})
|
||||
case bubbles.MsgConfirmPromptAnswered:
|
||||
m.confirmPrompt.Blur()
|
||||
if msg.Value {
|
||||
switch payload := msg.Payload.(type) {
|
||||
case deleteConversationPayload:
|
||||
cmds = append(cmds, m.deleteConversation(api.Conversation(payload)))
|
||||
conv, ok := msg.Payload.(api.Conversation)
|
||||
if ok {
|
||||
cmds = append(cmds, m.deleteConversation(conv))
|
||||
}
|
||||
}
|
||||
case msgConversationDeleted:
|
||||
@@ -193,24 +180,17 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
|
||||
func (m *Model) loadConversations() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
messages, err := m.Ctx.Store.LatestConversationMessages()
|
||||
err, conversations := m.App.LoadConversations()
|
||||
if err != nil {
|
||||
return shared.MsgError(fmt.Errorf("Could not load conversations: %v", err))
|
||||
}
|
||||
|
||||
loaded := make([]loadedConversation, len(messages))
|
||||
for i, m := range messages {
|
||||
loaded[i].lastReply = m
|
||||
loaded[i].conv = *m.Conversation
|
||||
}
|
||||
|
||||
return msgConversationsLoaded(loaded)
|
||||
return msgConversationsLoaded(conversations)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) deleteConversation(conv api.Conversation) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := m.Ctx.Store.DeleteConversation(&conv)
|
||||
err := m.App.Ctx.Store.DeleteConversation(&conv)
|
||||
if err != nil {
|
||||
return shared.MsgError(fmt.Errorf("Could not delete conversation: %v", err))
|
||||
}
|
||||
@@ -289,12 +269,12 @@ func (m *Model) renderConversationList() string {
|
||||
sb strings.Builder
|
||||
)
|
||||
|
||||
m.itemOffsets = make([]int, len(m.conversations))
|
||||
m.itemOffsets = make([]int, len(m.App.Conversations))
|
||||
sb.WriteRune('\n')
|
||||
currentOffset += 1
|
||||
|
||||
for i, c := range m.conversations {
|
||||
lastReplyAge := now.Sub(c.lastReply.CreatedAt)
|
||||
for i, c := range m.App.Conversations {
|
||||
lastReplyAge := now.Sub(c.LastReply.CreatedAt)
|
||||
|
||||
var category string
|
||||
for _, g := range categories {
|
||||
@@ -314,14 +294,14 @@ func (m *Model) renderConversationList() string {
|
||||
}
|
||||
|
||||
tStyle := titleStyle.Copy()
|
||||
if c.conv.Title == "" {
|
||||
if c.Conv.Title == "" {
|
||||
tStyle = tStyle.Inherit(untitledStyle).SetString("(untitled)")
|
||||
}
|
||||
if i == m.cursor {
|
||||
tStyle = tStyle.Inherit(selectedStyle)
|
||||
}
|
||||
|
||||
title := tStyle.Width(m.Width - 3).PaddingLeft(2).Render(c.conv.Title)
|
||||
title := tStyle.Width(m.Width - 3).PaddingLeft(2).Render(c.Conv.Title)
|
||||
if i == m.cursor {
|
||||
title = ">" + title[1:]
|
||||
}
|
||||
@@ -334,7 +314,7 @@ func (m *Model) renderConversationList() string {
|
||||
))
|
||||
sb.WriteString(item)
|
||||
currentOffset += tuiutil.Height(item)
|
||||
if i < len(m.conversations)-1 {
|
||||
if i < len(m.App.Conversations)-1 {
|
||||
sb.WriteRune('\n')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user