Implement message selection-based scrolling with j/k keys

Co-authored-by: foglar <82380203+foglar@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-10-09 13:40:31 +00:00
parent 113fe93f58
commit c2607b2860
3 changed files with 85 additions and 33 deletions

View File

@ -28,26 +28,40 @@ func updateChatView(v *gocui.View) {
clear := cleanimage.NewKittyImageCleaner() clear := cleanimage.NewKittyImageCleaner()
fmt.Print(clear.DeleteByColumn(chatViewColumn, false)) fmt.Print(clear.DeleteByColumn(chatViewColumn, false))
// Reset scroll offset when switching contacts or if out of bounds
if chatScrollOffset < 0 {
chatScrollOffset = 0
}
if chatScrollOffset > len(chatData.Messages) {
chatScrollOffset = len(chatData.Messages)
}
// Set view origin based on scroll offset
ox, _ := v.Origin()
v.SetOrigin(ox, chatScrollOffset)
// Check if we have valid users // Check if we have valid users
if len(users) == 0 || selectedUserIdx >= len(users) { if len(users) == 0 || selectedUserIdx >= len(users) {
return return
} }
// Get view dimensions to check if images are visible // Ensure selectedMessageIdx is within bounds
if len(chatData.Messages) == 0 {
return
}
if selectedMessageIdx < 0 {
selectedMessageIdx = 0
}
if selectedMessageIdx >= len(chatData.Messages) {
selectedMessageIdx = len(chatData.Messages) - 1
}
// Get view dimensions
_, viewHeight := v.Size() _, viewHeight := v.Size()
// Calculate which messages should be visible
// We want to show the selected message in the middle of the view when possible
messagesPerView := viewHeight / messageRowIncrement
if messagesPerView <= 0 {
messagesPerView = 1
}
// Calculate start index - show newest messages at bottom
startIdx := max(0, min(selectedMessageIdx-(messagesPerView/2), len(chatData.Messages)-messagesPerView))
// Calculate scroll offset to position view correctly
scrollOffset := startIdx * messageRowIncrement
ox, _ := v.Origin()
v.SetOrigin(ox, scrollOffset)
// Get terminal cell size once for all messages // Get terminal cell size once for all messages
w, h, err := cell_size.GetTerminalCellSizePixels() w, h, err := cell_size.GetTerminalCellSizePixels()
if err != nil { if err != nil {
@ -62,7 +76,10 @@ func updateChatView(v *gocui.View) {
h = 0 h = 0
} }
for i, msg := range chatData.Messages { // Render only visible messages
endIdx := min(startIdx+messagesPerView+1, len(chatData.Messages))
for i := startIdx; i < endIdx; i++ {
msg := chatData.Messages[i]
decoded, err := base64.StdEncoding.DecodeString(msg.Content) decoded, err := base64.StdEncoding.DecodeString(msg.Content)
if err != nil { if err != nil {
log.Printf("Error decoding message: %v", err) log.Printf("Error decoding message: %v", err)
@ -90,12 +107,20 @@ func updateChatView(v *gocui.View) {
iconPath = fmt.Sprintf(contactIconPathFmt, strings.ToLower(msg.Sender)) iconPath = fmt.Sprintf(contactIconPathFmt, strings.ToLower(msg.Sender))
} }
// Print message text // Highlight selected message
fmt.Fprintf(v, "%s", "\t\t\t\t\t"+senderLabel+"\n\t\t\t\t\t"+string(decoded)+"\n\n") prefix := "\t\t\t\t\t"
if i == selectedMessageIdx {
prefix = "\x1b[7m\t\t\t\t\t"
senderLabel = senderLabel + "\x1b[0m\x1b[7m"
decoded = append([]byte("\x1b[0m\x1b[7m"), decoded...)
decoded = append(decoded, []byte("\x1b[0m")...)
}
// Calculate row position accounting for scroll offset // Print message text
// Each message takes 3 lines (messageRowIncrement) fmt.Fprintf(v, "%s", prefix+senderLabel+"\n"+prefix+string(decoded)+"\n\n")
rowPosition := i*messageRowIncrement + messageRowOffset - chatScrollOffset
// Calculate row position for image rendering
rowPosition := i*messageRowIncrement + messageRowOffset - scrollOffset
// Only render images that are visible in the view // Only render images that are visible in the view
if rowPosition >= 0 && rowPosition < viewHeight { if rowPosition >= 0 && rowPosition < viewHeight {
@ -126,36 +151,45 @@ func sendMessage(g *gocui.Gui, v *gocui.View) error {
return err return err
} }
// Reset scroll to bottom when sending a new message // Select newest message (at the end)
chatScrollOffset = 0 if len(chatData.Messages) > 0 {
selectedMessageIdx = len(chatData.Messages) - 1
}
updateChatView(chatView) updateChatView(chatView)
return nil return nil
} }
func scrollChatUp(g *gocui.Gui, v *gocui.View) error { func scrollChatUp(g *gocui.Gui, v *gocui.View) error {
if len(chatData.Messages) == 0 {
return nil
}
chatView, err := g.View("chat") chatView, err := g.View("chat")
if err != nil { if err != nil {
return err return err
} }
// Scroll up by 1 line // Move selection up (to older messages)
if chatScrollOffset > 0 { if selectedMessageIdx > 0 {
chatScrollOffset-- selectedMessageIdx--
updateChatView(chatView) updateChatView(chatView)
} }
return nil return nil
} }
func scrollChatDown(g *gocui.Gui, v *gocui.View) error { func scrollChatDown(g *gocui.Gui, v *gocui.View) error {
if len(chatData.Messages) == 0 {
return nil
}
chatView, err := g.View("chat") chatView, err := g.View("chat")
if err != nil { if err != nil {
return err return err
} }
// Scroll down by 1 line // Move selection down (to newer messages)
maxScroll := len(chatData.Messages) * messageRowIncrement if selectedMessageIdx < len(chatData.Messages)-1 {
if chatScrollOffset < maxScroll { selectedMessageIdx++
chatScrollOffset++
updateChatView(chatView) updateChatView(chatView)
} }
return nil return nil

View File

@ -74,8 +74,15 @@ func nextContact(g *gocui.Gui, v *gocui.View) error {
selectedUserIdx = 0 selectedUserIdx = 0
} }
// Reset scroll offset when changing contacts // Load messages for new contact
chatScrollOffset = 0 LoadMessages(users[selectedUserIdx])
// Select newest message (scroll to bottom)
if len(chatData.Messages) > 0 {
selectedMessageIdx = len(chatData.Messages) - 1
} else {
selectedMessageIdx = 0
}
if err := updateContactsView(g); err != nil { if err := updateContactsView(g); err != nil {
return err return err
@ -99,8 +106,15 @@ func prevContact(g *gocui.Gui, v *gocui.View) error {
selectedUserIdx = len(users) - 1 selectedUserIdx = len(users) - 1
} }
// Reset scroll offset when changing contacts // Load messages for new contact
chatScrollOffset = 0 LoadMessages(users[selectedUserIdx])
// Select newest message (scroll to bottom)
if len(chatData.Messages) > 0 {
selectedMessageIdx = len(chatData.Messages) - 1
} else {
selectedMessageIdx = 0
}
if err := updateContactsView(g); err != nil { if err := updateContactsView(g); err != nil {
return err return err

View File

@ -10,7 +10,7 @@ var users []string
var prevWidth, prevHeight int var prevWidth, prevHeight int
var chatData ChatData var chatData ChatData
var selectedUserIdx int = 0 var selectedUserIdx int = 0
var chatScrollOffset int = 0 var selectedMessageIdx int = 0
func Run() { func Run() {
LoadContacts(defaultServerPath) LoadContacts(defaultServerPath)
@ -18,6 +18,10 @@ func Run() {
// Load initial messages if there are any contacts // Load initial messages if there are any contacts
if len(users) > 0 && selectedUserIdx < len(users) { if len(users) > 0 && selectedUserIdx < len(users) {
LoadMessages(users[selectedUserIdx]) LoadMessages(users[selectedUserIdx])
// Initialize to newest message (bottom)
if len(chatData.Messages) > 0 {
selectedMessageIdx = len(chatData.Messages) - 1
}
} }
g, err := gocui.NewGui(gocui.OutputNormal) g, err := gocui.NewGui(gocui.OutputNormal)