tui: fleshed out converation selection

This commit is contained in:
Matt Low 2024-04-02 06:53:29 +00:00
parent 7463b7502c
commit 69d3265b64
3 changed files with 46 additions and 14 deletions

View File

@ -253,6 +253,7 @@ func (m chatModel) Update(msg tea.Msg) (chatModel, tea.Cmd) {
m.conversation = (*models.Conversation)(msg) m.conversation = (*models.Conversation)(msg)
cmds = append(cmds, m.loadMessages(m.conversation)) cmds = append(cmds, m.loadMessages(m.conversation))
case msgMessagesLoaded: case msgMessagesLoaded:
m.selectedMessage = len(msg) - 1
m.setMessages(msg) m.setMessages(msg)
m.updateContent() m.updateContent()
m.content.GotoBottom() m.content.GotoBottom()
@ -407,7 +408,7 @@ func (m *chatModel) handleMessagesKey(msg tea.KeyMsg) (bool, tea.Cmd) {
m.selectedMessage-- m.selectedMessage--
m.updateContent() m.updateContent()
offset := m.messageOffsets[m.selectedMessage] offset := m.messageOffsets[m.selectedMessage]
scrollIntoView(&m.content, offset, 0.1) scrollIntoView(&m.content, offset, m.content.Height/2)
} }
return true, nil return true, nil
case "ctrl+j": case "ctrl+j":
@ -415,7 +416,7 @@ func (m *chatModel) handleMessagesKey(msg tea.KeyMsg) (bool, tea.Cmd) {
m.selectedMessage++ m.selectedMessage++
m.updateContent() m.updateContent()
offset := m.messageOffsets[m.selectedMessage] offset := m.messageOffsets[m.selectedMessage]
scrollIntoView(&m.content, offset, 0.1) scrollIntoView(&m.content, offset, m.content.Height/2)
} }
return true, nil return true, nil
case "ctrl+r": case "ctrl+r":
@ -441,7 +442,7 @@ func (m *chatModel) handleInputKey(msg tea.KeyMsg) (bool, tea.Cmd) {
m.selectedMessage = len(m.messages) - 1 m.selectedMessage = len(m.messages) - 1
} }
offset := m.messageOffsets[m.selectedMessage] offset := m.messageOffsets[m.selectedMessage]
scrollIntoView(&m.content, offset, 0.1) scrollIntoView(&m.content, offset, m.content.Height/2)
} }
m.updateContent() m.updateContent()
m.input.Blur() m.input.Blur()

View File

@ -29,7 +29,8 @@ type conversationsModel struct {
basemodel basemodel
conversations []loadedConversation conversations []loadedConversation
cursor int // index of the currently selected message message cursor int // index of the currently selected conversation
itemOffsets []int // keeps track of the viewport y offset of each rendered item
content viewport.Model content viewport.Model
} }
@ -59,13 +60,33 @@ func (m *conversationsModel) handleInput(msg tea.KeyMsg) (bool, tea.Cmd) {
case "j", "down": case "j", "down":
if m.cursor < len(m.conversations)-1 { if m.cursor < len(m.conversations)-1 {
m.cursor++ 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.
scrollIntoView(&m.content, m.itemOffsets[m.cursor+1], -1)
}
m.content.SetContent(m.renderConversationList()) m.content.SetContent(m.renderConversationList())
} else {
m.cursor = len(m.conversations) - 1
m.content.GotoBottom()
} }
return true, nil return true, nil
case "k", "up": case "k", "up":
if m.cursor > 0 { if m.cursor > 0 {
m.cursor-- m.cursor--
if m.cursor == 0 {
m.content.GotoTop()
} else {
scrollIntoView(&m.content, m.itemOffsets[m.cursor], 1)
}
m.content.SetContent(m.renderConversationList()) m.content.SetContent(m.renderConversationList())
} else {
m.cursor = 0
m.content.GotoTop()
} }
return true, nil return true, nil
case "n": case "n":
@ -186,7 +207,6 @@ func (m *conversationsModel) renderConversationList() string {
{"Older", now.Sub(time.Time{})}, {"Older", now.Sub(time.Time{})},
} }
// TODO: pick nice color
categoryStyle := lipgloss.NewStyle(). categoryStyle := lipgloss.NewStyle().
MarginBottom(1). MarginBottom(1).
Foreground(lipgloss.Color("170")). Foreground(lipgloss.Color("170")).
@ -201,8 +221,12 @@ func (m *conversationsModel) renderConversationList() string {
untitledStyle := lipgloss.NewStyle().Faint(true).Italic(true) untitledStyle := lipgloss.NewStyle().Faint(true).Italic(true)
selectedStyle := lipgloss.NewStyle().Faint(true).Foreground(lipgloss.Color("6")) selectedStyle := lipgloss.NewStyle().Faint(true).Foreground(lipgloss.Color("6"))
var currentOffset int
var currentCategory string var currentCategory string
m.itemOffsets = make([]int, len(m.conversations))
sb := &strings.Builder{} sb := &strings.Builder{}
sb.WriteRune('\n')
currentOffset += 1
for i, c := range m.conversations { for i, c := range m.conversations {
lastReplyAge := now.Sub(c.lastReply.CreatedAt) lastReplyAge := now.Sub(c.lastReply.CreatedAt)
@ -217,7 +241,10 @@ func (m *conversationsModel) renderConversationList() string {
// print the category // print the category
if category != currentCategory { if category != currentCategory {
currentCategory = category currentCategory = category
fmt.Fprintf(sb, "%s\n", categoryStyle.Render(currentCategory)) heading := categoryStyle.Render(currentCategory)
sb.WriteString(heading)
currentOffset += height(heading)
sb.WriteRune('\n')
} }
tStyle := titleStyle.Copy() tStyle := titleStyle.Copy()
@ -234,13 +261,17 @@ func (m *conversationsModel) renderConversationList() string {
title = ">" + title[1:] title = ">" + title[1:]
} }
heading := fmt.Sprintf( m.itemOffsets[i] = currentOffset
item := itemStyle.Render(fmt.Sprintf(
"%s\n%s", "%s\n%s",
title, title,
padding+ageStyle.Render(util.HumanTimeElapsedSince(lastReplyAge)), padding+ageStyle.Render(util.HumanTimeElapsedSince(lastReplyAge)),
) ))
sb.WriteString(itemStyle.Render(heading)) sb.WriteString(item)
currentOffset += height(item)
if i < len(m.conversations)-1 {
sb.WriteRune('\n') sb.WriteRune('\n')
} }
}
return sb.String() return sb.String()
} }

View File

@ -73,7 +73,7 @@ func truncateToCellWidth(str string, width int, tail string) string {
// fraction is the fraction of the total screen height into view the offset // fraction is the fraction of the total screen height into view the offset
// should be scrolled into view. 0.5 = items will be snapped to middle of // should be scrolled into view. 0.5 = items will be snapped to middle of
// view // view
func scrollIntoView(vp *viewport.Model, offset int, fraction float32) { func scrollIntoView(vp *viewport.Model, offset int, edge int) {
currentOffset := vp.YOffset currentOffset := vp.YOffset
if offset >= currentOffset && offset < currentOffset+vp.Height { if offset >= currentOffset && offset < currentOffset+vp.Height {
return return
@ -81,9 +81,9 @@ func scrollIntoView(vp *viewport.Model, offset int, fraction float32) {
distance := currentOffset - offset distance := currentOffset - offset
if distance < 0 { if distance < 0 {
// we should scroll down until it just comes into view // we should scroll down until it just comes into view
vp.SetYOffset(currentOffset - (distance + (vp.Height - int(float32(vp.Height)*fraction))) + 1) vp.SetYOffset(currentOffset - (distance + (vp.Height - edge)) + 1)
} else { } else {
// we should scroll up // we should scroll up
vp.SetYOffset(currentOffset - distance - int(float32(vp.Height)*fraction)) vp.SetYOffset(currentOffset - distance - edge)
} }
} }