tui: fleshed out converation selection
This commit is contained in:
parent
7463b7502c
commit
69d3265b64
@ -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()
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user