Split up tui code into packages (views/*, shared, util)
This commit is contained in:
258
pkg/tui/views/conversations/conversations.go
Normal file
258
pkg/tui/views/conversations/conversations.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package conversations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
models "git.mlow.ca/mlow/lmcli/pkg/lmcli/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"
|
||||
"git.mlow.ca/mlow/lmcli/pkg/util"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type loadedConversation struct {
|
||||
conv models.Conversation
|
||||
lastReply models.Message
|
||||
}
|
||||
|
||||
type (
|
||||
// sent when conversation list is loaded
|
||||
msgConversationsLoaded ([]loadedConversation)
|
||||
// sent when a conversation is selected
|
||||
MsgConversationSelected models.Conversation
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
shared.State
|
||||
|
||||
conversations []loadedConversation
|
||||
cursor int // index of the currently selected conversation
|
||||
itemOffsets []int // keeps track of the viewport y offset of each rendered item
|
||||
|
||||
content viewport.Model
|
||||
}
|
||||
|
||||
func Conversations(state shared.State) Model {
|
||||
m := Model{
|
||||
State: state,
|
||||
content: viewport.New(0, 0),
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
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) {
|
||||
return true, func() tea.Msg {
|
||||
return MsgConversationSelected(m.conversations[m.cursor].conv)
|
||||
}
|
||||
}
|
||||
case "j", "down":
|
||||
if m.cursor < len(m.conversations)-1 {
|
||||
m.cursor++
|
||||
if m.cursor == len(m.conversations)-1 {
|
||||
// if last conversation, simply scroll to the bottom
|
||||
m.content.GotoBottom()
|
||||
} else {
|
||||
// this hack positions the *next* conversatoin slightly
|
||||
// *off* the screen, ensuring the entire m.cursor is shown,
|
||||
// even if its height may not be constant due to wrapping.
|
||||
tuiutil.ScrollIntoView(&m.content, m.itemOffsets[m.cursor+1], -1)
|
||||
}
|
||||
m.content.SetContent(m.renderConversationList())
|
||||
} else {
|
||||
m.cursor = len(m.conversations) - 1
|
||||
m.content.GotoBottom()
|
||||
}
|
||||
return true, nil
|
||||
case "k", "up":
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
if m.cursor == 0 {
|
||||
m.content.GotoTop()
|
||||
} else {
|
||||
tuiutil.ScrollIntoView(&m.content, m.itemOffsets[m.cursor], 1)
|
||||
}
|
||||
m.content.SetContent(m.renderConversationList())
|
||||
} else {
|
||||
m.cursor = 0
|
||||
m.content.GotoTop()
|
||||
}
|
||||
return true, nil
|
||||
case "n":
|
||||
// new conversation
|
||||
case "d":
|
||||
// show prompt to delete conversation
|
||||
case "c":
|
||||
// copy/clone conversation
|
||||
case "r":
|
||||
// show prompt to rename conversation
|
||||
case "shift+r":
|
||||
// show prompt to generate name for conversation
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
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) {
|
||||
var cmds []tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case shared.MsgViewEnter:
|
||||
cmds = append(cmds, m.loadConversations())
|
||||
m.content.SetContent(m.renderConversationList())
|
||||
case tea.WindowSizeMsg:
|
||||
m.HandleResize(msg.Width, msg.Height)
|
||||
m.content.SetContent(m.renderConversationList())
|
||||
case msgConversationsLoaded:
|
||||
m.conversations = msg
|
||||
m.content.SetContent(m.renderConversationList())
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.content, cmd = m.content.Update(msg)
|
||||
if cmd != nil {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
if m.Width > 0 {
|
||||
m.Views.Header = m.headerView()
|
||||
m.Views.Footer = "" // TODO: show /something/
|
||||
m.Views.Error = tuiutil.ErrorBanner(m.Err, m.Width)
|
||||
fixedHeight := tuiutil.Height(m.Views.Header) + tuiutil.Height(m.Views.Error) + tuiutil.Height(m.Views.Footer)
|
||||
m.content.Height = m.Height - fixedHeight
|
||||
m.Views.Content = m.content.View()
|
||||
}
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *Model) loadConversations() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
messages, err := m.Ctx.Store.LatestConversationMessages()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) headerView() string {
|
||||
titleStyle := lipgloss.NewStyle().Bold(true)
|
||||
header := titleStyle.Render("Conversations")
|
||||
return styles.Header.Width(m.Width).Render(header)
|
||||
}
|
||||
|
||||
func (m *Model) renderConversationList() string {
|
||||
now := time.Now()
|
||||
midnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
dayOfWeek := int(now.Weekday())
|
||||
categories := []struct {
|
||||
name string
|
||||
cutoff time.Duration
|
||||
}{
|
||||
{"Today", now.Sub(midnight)},
|
||||
{"Yesterday", now.Sub(midnight.AddDate(0, 0, -1))},
|
||||
{"This week", now.Sub(midnight.AddDate(0, 0, -dayOfWeek))},
|
||||
{"Last week", now.Sub(midnight.AddDate(0, 0, -(dayOfWeek + 7)))},
|
||||
{"This month", now.Sub(monthStart)},
|
||||
{"Last month", now.Sub(monthStart.AddDate(0, -1, 0))},
|
||||
{"2 Months ago", now.Sub(monthStart.AddDate(0, -2, 0))},
|
||||
{"3 Months ago", now.Sub(monthStart.AddDate(0, -3, 0))},
|
||||
{"4 Months ago", now.Sub(monthStart.AddDate(0, -4, 0))},
|
||||
{"5 Months ago", now.Sub(monthStart.AddDate(0, -5, 0))},
|
||||
{"6 Months ago", now.Sub(monthStart.AddDate(0, -6, 0))},
|
||||
{"Older", now.Sub(time.Time{})},
|
||||
}
|
||||
|
||||
categoryStyle := lipgloss.NewStyle().
|
||||
MarginBottom(1).
|
||||
Foreground(lipgloss.Color("12")).
|
||||
PaddingLeft(1).
|
||||
Bold(true)
|
||||
|
||||
itemStyle := lipgloss.NewStyle().
|
||||
MarginBottom(1)
|
||||
|
||||
ageStyle := lipgloss.NewStyle().Faint(true).SetString()
|
||||
titleStyle := lipgloss.NewStyle().Bold(true)
|
||||
untitledStyle := lipgloss.NewStyle().Faint(true).Italic(true)
|
||||
selectedStyle := lipgloss.NewStyle().Faint(true).Foreground(lipgloss.Color("6"))
|
||||
|
||||
var (
|
||||
currentOffset int
|
||||
currentCategory string
|
||||
sb strings.Builder
|
||||
)
|
||||
|
||||
m.itemOffsets = make([]int, len(m.conversations))
|
||||
sb.WriteRune('\n')
|
||||
currentOffset += 1
|
||||
|
||||
for i, c := range m.conversations {
|
||||
lastReplyAge := now.Sub(c.lastReply.CreatedAt)
|
||||
|
||||
var category string
|
||||
for _, g := range categories {
|
||||
if lastReplyAge < g.cutoff {
|
||||
category = g.name
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// print the category
|
||||
if category != currentCategory {
|
||||
currentCategory = category
|
||||
heading := categoryStyle.Render(currentCategory)
|
||||
sb.WriteString(heading)
|
||||
currentOffset += tuiutil.Height(heading)
|
||||
sb.WriteRune('\n')
|
||||
}
|
||||
|
||||
tStyle := titleStyle.Copy()
|
||||
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)
|
||||
if i == m.cursor {
|
||||
title = ">" + title[1:]
|
||||
}
|
||||
|
||||
m.itemOffsets[i] = currentOffset
|
||||
item := itemStyle.Render(fmt.Sprintf(
|
||||
"%s\n %s",
|
||||
title,
|
||||
ageStyle.Render(util.HumanTimeElapsedSince(lastReplyAge)),
|
||||
))
|
||||
sb.WriteString(item)
|
||||
currentOffset += tuiutil.Height(item)
|
||||
if i < len(m.conversations)-1 {
|
||||
sb.WriteRune('\n')
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
Reference in New Issue
Block a user