WhspBrd/internal/tui/chat.go
copilot-swe-agent[bot] c2607b2860 Implement message selection-based scrolling with j/k keys
Co-authored-by: foglar <82380203+foglar@users.noreply.github.com>
2025-10-09 13:40:31 +00:00

197 lines
4.8 KiB
Go

package tui
import (
"encoding/base64"
"fmt"
"log"
"strings"
"time"
"whspbrd/pkg/cell_size"
"whspbrd/pkg/clean_image"
"whspbrd/pkg/render_image"
"github.com/jroimartin/gocui"
)
const (
chatViewColumn = 23
userIconPath = "./configs/icon.png"
contactIconPathFmt = "./configs/servers/default/users/%s/icon.png"
messageRowOffset = 2
messageRowIncrement = 3
)
func updateChatView(v *gocui.View) {
v.Clear()
clear := cleanimage.NewKittyImageCleaner()
fmt.Print(clear.DeleteByColumn(chatViewColumn, false))
// Check if we have valid users
if len(users) == 0 || selectedUserIdx >= len(users) {
return
}
// 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()
// 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
w, h, err := cell_size.GetTerminalCellSizePixels()
if err != nil {
log.Println("Error getting terminal cell size:", err)
w, h = 10, 20 // fallback values
}
if h > w {
h = h * 2
w = 0
} else {
w = w*3 - (w / 10)
h = 0
}
// 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)
if err != nil {
log.Printf("Error decoding message: %v", err)
continue
}
decoded = []byte(strings.TrimSuffix(string(decoded), "\n"))
t, err := time.Parse(time.RFC3339, msg.Timestamp)
if err != nil {
log.Printf("Error parsing timestamp: %v", err)
t = time.Now() // fallback to current time
}
formattedTime := t.Format("2006-01-02 15:04")
// Determine if this is a message from the user or the contact
isUserMessage := !strings.EqualFold(msg.Sender, users[selectedUserIdx])
var senderLabel string
var iconPath string
if isUserMessage {
senderLabel = Colors.Text(Colors.Base02) + "You (" + formattedTime + "):" + Colors.Reset
iconPath = userIconPath
} else {
senderLabel = Colors.Text(Colors.Base05) + msg.Sender + " (" + formattedTime + "):" + Colors.Reset
iconPath = fmt.Sprintf(contactIconPathFmt, strings.ToLower(msg.Sender))
}
// Highlight selected message
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")...)
}
// Print message text
fmt.Fprintf(v, "%s", prefix+senderLabel+"\n"+prefix+string(decoded)+"\n\n")
// Calculate row position for image rendering
rowPosition := i*messageRowIncrement + messageRowOffset - scrollOffset
// Only render images that are visible in the view
if rowPosition >= 0 && rowPosition < viewHeight {
render_image.RenderImage(iconPath, rowPosition, chatViewColumn, w, h, false)
}
}
}
func sendMessage(g *gocui.Gui, v *gocui.View) error {
if len(users) == 0 || selectedUserIdx >= len(users) {
return nil
}
input := v.Buffer()
v.Clear()
v.SetCursor(0, 0)
v.SetOrigin(0, 0)
if err := WriteMessage(users[selectedUserIdx], "You", users[selectedUserIdx], input); err != nil {
log.Printf("Error writing message: %v", err)
return err
}
LoadMessages(users[selectedUserIdx])
chatView, err := g.View("chat")
if err != nil {
log.Printf("Error getting chat view: %v", err)
return err
}
// Select newest message (at the end)
if len(chatData.Messages) > 0 {
selectedMessageIdx = len(chatData.Messages) - 1
}
updateChatView(chatView)
return nil
}
func scrollChatUp(g *gocui.Gui, v *gocui.View) error {
if len(chatData.Messages) == 0 {
return nil
}
chatView, err := g.View("chat")
if err != nil {
return err
}
// Move selection up (to older messages)
if selectedMessageIdx > 0 {
selectedMessageIdx--
updateChatView(chatView)
}
return nil
}
func scrollChatDown(g *gocui.Gui, v *gocui.View) error {
if len(chatData.Messages) == 0 {
return nil
}
chatView, err := g.View("chat")
if err != nil {
return err
}
// Move selection down (to newer messages)
if selectedMessageIdx < len(chatData.Messages)-1 {
selectedMessageIdx++
updateChatView(chatView)
}
return nil
}