From b4b65b9b608cd10e168d173448788f186b715c3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:59:27 +0000 Subject: [PATCH] Implement scrolling with J/K keys and fix image positioning Co-authored-by: foglar <82380203+foglar@users.noreply.github.com> --- internal/tui/chat.go | 73 ++++++++++++++++++++++++++++++++++--- internal/tui/keybindings.go | 12 ++++++ internal/tui/layout.go | 2 +- internal/tui/sidebar.go | 6 +++ internal/tui/tui.go | 1 + 5 files changed, 87 insertions(+), 7 deletions(-) diff --git a/internal/tui/chat.go b/internal/tui/chat.go index 1aebcdb..8c6a3fb 100644 --- a/internal/tui/chat.go +++ b/internal/tui/chat.go @@ -28,6 +28,21 @@ func updateChatView(v *gocui.View) { clear := cleanimage.NewKittyImageCleaner() 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) + + // Get view dimensions to check if images are visible + _, viewHeight := v.Size() + for i, msg := range chatData.Messages { decoded, err := base64.StdEncoding.DecodeString(msg.Content) if err != nil { @@ -60,13 +75,27 @@ func updateChatView(v *gocui.View) { continue } - if !strings.EqualFold(msg.Sender, users[selectedUserIdx]) { - fmt.Fprintf(v, "%s", "\t\t\t\t\t"+Colors.Text(Colors.Base02)+"You ("+formattedTime+"):"+Colors.Reset+"\n\t\t\t\t\t"+string(decoded)+"\n\n") - render_image.RenderImage(userIconPath, i*messageRowIncrement+messageRowOffset, chatViewColumn, w, h, false) + // Calculate row position accounting for scroll offset + // Each message takes 3 lines (messageRowIncrement) + rowPosition := i*messageRowIncrement + messageRowOffset - chatScrollOffset + + // Only render images that are visible in the view + if rowPosition >= 0 && rowPosition < viewHeight { + if !strings.EqualFold(msg.Sender, users[selectedUserIdx]) { + fmt.Fprintf(v, "%s", "\t\t\t\t\t"+Colors.Text(Colors.Base02)+"You ("+formattedTime+"):"+Colors.Reset+"\n\t\t\t\t\t"+string(decoded)+"\n\n") + render_image.RenderImage(userIconPath, rowPosition, chatViewColumn, w, h, false) + } else { + fmt.Fprintf(v, "%s", "\t\t\t\t\t"+Colors.Text(Colors.Base05)+msg.Sender+" ("+formattedTime+"):"+Colors.Reset+"\n\t\t\t\t\t"+string(decoded)+"\n\n") + iconPath := fmt.Sprintf(contactIconPathFmt, strings.ToLower(msg.Sender)) + render_image.RenderImage(iconPath, rowPosition, chatViewColumn, w, h, false) + } } else { - fmt.Fprintf(v, "%s", "\t\t\t\t\t"+Colors.Text(Colors.Base05)+msg.Sender+" ("+formattedTime+"):"+Colors.Reset+"\n\t\t\t\t\t"+string(decoded)+"\n\n") - iconPath := fmt.Sprintf(contactIconPathFmt, strings.ToLower(msg.Sender)) - render_image.RenderImage(iconPath, i*messageRowIncrement+messageRowOffset, chatViewColumn, w, h, false) + // Still print the text even if image is not visible + if !strings.EqualFold(msg.Sender, users[selectedUserIdx]) { + fmt.Fprintf(v, "%s", "\t\t\t\t\t"+Colors.Text(Colors.Base02)+"You ("+formattedTime+"):"+Colors.Reset+"\n\t\t\t\t\t"+string(decoded)+"\n\n") + } else { + fmt.Fprintf(v, "%s", "\t\t\t\t\t"+Colors.Text(Colors.Base05)+msg.Sender+" ("+formattedTime+"):"+Colors.Reset+"\n\t\t\t\t\t"+string(decoded)+"\n\n") + } } } } @@ -92,6 +121,38 @@ func sendMessage(g *gocui.Gui, v *gocui.View) error { log.Printf("Error getting chat view: %v", err) return err } + + // Reset scroll to bottom when sending a new message + chatScrollOffset = 0 updateChatView(chatView) return nil } + +func scrollChatUp(g *gocui.Gui, v *gocui.View) error { + chatView, err := g.View("chat") + if err != nil { + return err + } + + // Scroll up by 1 line + if chatScrollOffset > 0 { + chatScrollOffset-- + updateChatView(chatView) + } + return nil +} + +func scrollChatDown(g *gocui.Gui, v *gocui.View) error { + chatView, err := g.View("chat") + if err != nil { + return err + } + + // Scroll down by 1 line + maxScroll := len(chatData.Messages) * messageRowIncrement + if chatScrollOffset < maxScroll { + chatScrollOffset++ + updateChatView(chatView) + } + return nil +} diff --git a/internal/tui/keybindings.go b/internal/tui/keybindings.go index 31686ed..5c3704f 100644 --- a/internal/tui/keybindings.go +++ b/internal/tui/keybindings.go @@ -14,6 +14,18 @@ func keybindings(g *gocui.Gui) error { if err := g.SetKeybinding("", gocui.KeyCtrlK, gocui.ModNone, prevContact); err != nil { return err } + if err := g.SetKeybinding("", 'j', gocui.ModNone, scrollChatDown); err != nil { + return err + } + if err := g.SetKeybinding("", 'J', gocui.ModNone, scrollChatDown); err != nil { + return err + } + if err := g.SetKeybinding("", 'k', gocui.ModNone, scrollChatUp); err != nil { + return err + } + if err := g.SetKeybinding("", 'K', gocui.ModNone, scrollChatUp); err != nil { + return err + } if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, toggleProfileView); err != nil { return err } diff --git a/internal/tui/layout.go b/internal/tui/layout.go index 5b15f2d..d902a75 100644 --- a/internal/tui/layout.go +++ b/internal/tui/layout.go @@ -44,7 +44,7 @@ func layoutChat(g *gocui.Gui, maxX, maxY int) error { } v.Title = " Chat " v.Wrap = true - v.Autoscroll = true + v.Autoscroll = false updateChatView(v) } return nil diff --git a/internal/tui/sidebar.go b/internal/tui/sidebar.go index 94cb58a..aa8614f 100644 --- a/internal/tui/sidebar.go +++ b/internal/tui/sidebar.go @@ -74,6 +74,9 @@ func nextContact(g *gocui.Gui, v *gocui.View) error { selectedUserIdx = 0 } + // Reset scroll offset when changing contacts + chatScrollOffset = 0 + if err := updateContactsView(g); err != nil { return err } @@ -96,6 +99,9 @@ func prevContact(g *gocui.Gui, v *gocui.View) error { selectedUserIdx = len(users) - 1 } + // Reset scroll offset when changing contacts + chatScrollOffset = 0 + if err := updateContactsView(g); err != nil { return err } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 388c220..3871920 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -10,6 +10,7 @@ var users []string var prevWidth, prevHeight int var chatData ChatData var selectedUserIdx int = 0 +var chatScrollOffset int = 0 func Run() { LoadContacts(defaultServerPath)