fix(tui): wrap long artifact lines in viewer instead of truncating
Artifact content wider than the modal was silently cut off. Lines are now hard-wrapped at rune boundaries to the modal content width, and scroll clamping uses the wrapped line count so the last page stays reachable.
This commit is contained in:
@@ -440,8 +440,9 @@ func (m Model) artifactContentBodyH() int {
|
||||
return h
|
||||
}
|
||||
|
||||
// artifactContentLines splits the selected artifact's content into display lines.
|
||||
func (m Model) artifactContentLines() []string {
|
||||
// artifactContentLines splits the selected artifact's content into display lines,
|
||||
// wrapping each line to the specified width.
|
||||
func (m Model) artifactContentLines(w int) []string {
|
||||
if m.artifactsIndex < 0 || m.artifactsIndex >= len(m.artifacts) {
|
||||
return nil
|
||||
}
|
||||
@@ -449,12 +450,13 @@ func (m Model) artifactContentLines() []string {
|
||||
if c == nil {
|
||||
return []string{"(no content stored)"}
|
||||
}
|
||||
return strings.Split(strings.ReplaceAll(*c, "\r\n", "\n"), "\n")
|
||||
lines := strings.Split(strings.ReplaceAll(*c, "\r\n", "\n"), "\n")
|
||||
return wrapLines(lines, w)
|
||||
}
|
||||
|
||||
// artifactContentMaxScroll keeps the last page of content anchored to the body bottom.
|
||||
func (m Model) artifactContentMaxScroll() int {
|
||||
max := len(m.artifactContentLines()) - m.artifactContentBodyH()
|
||||
func (m Model) artifactContentMaxScroll(w int) int {
|
||||
max := len(m.artifactContentLines(w)) - m.artifactContentBodyH()
|
||||
if max < 0 {
|
||||
max = 0
|
||||
}
|
||||
@@ -515,11 +517,13 @@ func (m Model) artifactsModal() string {
|
||||
}
|
||||
|
||||
// Selected artifact content, scrollable with PgUp/PgDn.
|
||||
lines := m.artifactContentLines()
|
||||
// Width available for content: w - 2 (left padding) - 2 (box borders) - 2 (right padding) = w - 6
|
||||
contentW := w - 6
|
||||
lines := m.artifactContentLines(contentW)
|
||||
bodyH := m.artifactContentBodyH()
|
||||
coff := m.artifactScroll
|
||||
if coff > m.artifactContentMaxScroll() {
|
||||
coff = m.artifactContentMaxScroll()
|
||||
if coff > m.artifactContentMaxScroll(contentW) {
|
||||
coff = m.artifactContentMaxScroll(contentW)
|
||||
}
|
||||
if coff < 0 {
|
||||
coff = 0
|
||||
@@ -534,7 +538,7 @@ func (m Model) artifactsModal() string {
|
||||
cend = len(lines)
|
||||
}
|
||||
for i := coff; i < cend; i++ {
|
||||
b.WriteString(mbg(t, " "+truncate(lines[i], w-6), t.P.Fg) + "\n")
|
||||
b.WriteString(mbg(t, " "+lines[i], t.P.Fg) + "\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "select"}, {"PgUp/Dn", "scroll"}, {"v/esc", "close"}}))
|
||||
@@ -582,3 +586,37 @@ func truncate(s string, w int) string {
|
||||
}
|
||||
return string(r[:w-1]) + "…"
|
||||
}
|
||||
|
||||
// wrapLines wraps each line in the input slice to the specified width using hard rune-boundary wrapping.
|
||||
func wrapLines(lines []string, w int) []string {
|
||||
if w < 1 {
|
||||
w = 1
|
||||
}
|
||||
var result []string
|
||||
for _, line := range lines {
|
||||
wrapped := wrapLine(line, w)
|
||||
result = append(result, wrapped...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// wrapLine wraps a single line to the specified width using hard rune-boundary wrapping.
|
||||
func wrapLine(s string, w int) []string {
|
||||
if w < 1 {
|
||||
w = 1
|
||||
}
|
||||
runes := []rune(s)
|
||||
if len(runes) <= w {
|
||||
return []string{s}
|
||||
}
|
||||
var lines []string
|
||||
for len(runes) > 0 {
|
||||
if len(runes) <= w {
|
||||
lines = append(lines, string(runes))
|
||||
break
|
||||
}
|
||||
lines = append(lines, string(runes[:w]))
|
||||
runes = runes[w:]
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user