From 89fbd724e5fc50e0c0895ba7c2513fd5e02d63c2 Mon Sep 17 00:00:00 2001 From: Md Yunus Date: Thu, 19 Mar 2026 20:05:35 +0530 Subject: [PATCH 1/3] Fix table text copy not matching selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When selecting text in tables, the copied content did not match the visual selection because border characters (│, ─, etc.) were not properly accounted for when mapping visual column positions to text content. The fix maps visual column positions from the rendered line (with borders) to the stripped line (without borders) by tracking which runes correspond to which visual columns, ensuring the copied text matches what the user selected. Fixes: #2167 Signed-off-by: Md Yunus --- pkg/tui/components/messages/clipboard.go | 64 ++++++++++++++++++------ 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/pkg/tui/components/messages/clipboard.go b/pkg/tui/components/messages/clipboard.go index 58c0b1d7a..2cad1aca4 100644 --- a/pkg/tui/components/messages/clipboard.go +++ b/pkg/tui/components/messages/clipboard.go @@ -112,33 +112,43 @@ func (m *model) extractSelectedText() string { line := stripBorderChars(plainLine) runes := []rune(line) - // Calculate how many display columns were removed by stripping border chars - borderOffset := runewidth.StringWidth(plainLine) - runewidth.StringWidth(line) + // Map visual column positions from the plain line (with borders) to the + // stripped line (without borders) by tracking which runes correspond to + // which visual columns + visualToRune := make(map[int]int) + plainRunes := []rune(plainLine) + visualCol := 0 + lineRuneIdx := 0 + + for _, r := range plainRunes { + if !boxDrawingChars[r] { + // This rune is kept in the stripped line + visualToRune[visualCol] = lineRuneIdx + lineRuneIdx++ + } + visualCol += runewidth.RuneWidth(r) + } - // Adjust column positions by subtracting the border offset - adjustedStartCol := max(0, startCol-borderOffset) - adjustedEndCol := max(0, endCol-borderOffset) + // Find the closest rune index for the start and end columns + startRuneIdx := findClosestRuneIndex(visualToRune, startCol, len(runes)) + endRuneIdx := findClosestRuneIndex(visualToRune, endCol, len(runes)) var lineText string switch i { case startLine: if startLine == endLine { - sIdx := displayWidthToRuneIndex(line, adjustedStartCol) - eIdx := min(displayWidthToRuneIndex(line, adjustedEndCol), len(runes)) - if sIdx < len(runes) && sIdx < eIdx { - lineText = strings.TrimSpace(string(runes[sIdx:eIdx])) + if startRuneIdx < len(runes) && startRuneIdx < endRuneIdx { + lineText = strings.TrimSpace(string(runes[startRuneIdx:endRuneIdx])) } break } // First line: from startCol to end - sIdx := displayWidthToRuneIndex(line, adjustedStartCol) - if sIdx < len(runes) { - lineText = strings.TrimSpace(string(runes[sIdx:])) + if startRuneIdx < len(runes) { + lineText = strings.TrimSpace(string(runes[startRuneIdx:])) } case endLine: // Last line: from start to endCol - eIdx := min(displayWidthToRuneIndex(line, adjustedEndCol), len(runes)) - lineText = strings.TrimSpace(string(runes[:eIdx])) + lineText = strings.TrimSpace(string(runes[:endRuneIdx])) default: // Middle lines: entire line lineText = strings.TrimSpace(line) @@ -153,6 +163,32 @@ func (m *model) extractSelectedText() string { return result.String() } +// findClosestRuneIndex finds the rune index for a given visual column, +// or the closest next rune if the exact column doesn't exist +func findClosestRuneIndex(visualToRune map[int]int, visualCol int, maxRunes int) int { + // Try exact match first + if runeIdx, ok := visualToRune[visualCol]; ok { + return runeIdx + } + + // Find the next available rune index after the visual column + for col := visualCol + 1; col <= visualCol+10; col++ { + if runeIdx, ok := visualToRune[col]; ok { + return runeIdx + } + } + + // Find the previous available rune index + for col := visualCol - 1; col >= 0; col-- { + if runeIdx, ok := visualToRune[col]; ok { + return runeIdx + } + } + + // Fallback: return the last rune index + return maxRunes +} + // copySelectionToClipboard copies the currently selected text to clipboard func (m *model) copySelectionToClipboard() tea.Cmd { if !m.selection.active { From 82a733127151c4008b3f3b06c45efc915e9813ee Mon Sep 17 00:00:00 2001 From: Md Yunus Date: Thu, 19 Mar 2026 23:13:12 +0530 Subject: [PATCH 2/3] fix: simplify function parameter declarations for linting --- pkg/tui/components/messages/clipboard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tui/components/messages/clipboard.go b/pkg/tui/components/messages/clipboard.go index 2cad1aca4..83a85d66b 100644 --- a/pkg/tui/components/messages/clipboard.go +++ b/pkg/tui/components/messages/clipboard.go @@ -165,7 +165,7 @@ func (m *model) extractSelectedText() string { // findClosestRuneIndex finds the rune index for a given visual column, // or the closest next rune if the exact column doesn't exist -func findClosestRuneIndex(visualToRune map[int]int, visualCol int, maxRunes int) int { +func findClosestRuneIndex(visualToRune map[int]int, visualCol, maxRunes int) int { // Try exact match first if runeIdx, ok := visualToRune[visualCol]; ok { return runeIdx From 792e6ebe9485aa58e44aa1faf2c5c6a02155f174 Mon Sep 17 00:00:00 2001 From: Md Yunus Date: Thu, 19 Mar 2026 23:21:24 +0530 Subject: [PATCH 3/3] fix: address golangci-lint warning S1029 --- pkg/tui/components/messages/clipboard.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/tui/components/messages/clipboard.go b/pkg/tui/components/messages/clipboard.go index 83a85d66b..06f163d40 100644 --- a/pkg/tui/components/messages/clipboard.go +++ b/pkg/tui/components/messages/clipboard.go @@ -116,11 +116,9 @@ func (m *model) extractSelectedText() string { // stripped line (without borders) by tracking which runes correspond to // which visual columns visualToRune := make(map[int]int) - plainRunes := []rune(plainLine) visualCol := 0 lineRuneIdx := 0 - - for _, r := range plainRunes { + for _, r := range plainLine { if !boxDrawingChars[r] { // This rune is kept in the stripped line visualToRune[visualCol] = lineRuneIdx