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
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// artifactContentLines splits the selected artifact's content into display lines.
|
// artifactContentLines splits the selected artifact's content into display lines,
|
||||||
func (m Model) artifactContentLines() []string {
|
// wrapping each line to the specified width.
|
||||||
|
func (m Model) artifactContentLines(w int) []string {
|
||||||
if m.artifactsIndex < 0 || m.artifactsIndex >= len(m.artifacts) {
|
if m.artifactsIndex < 0 || m.artifactsIndex >= len(m.artifacts) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -449,12 +450,13 @@ func (m Model) artifactContentLines() []string {
|
|||||||
if c == nil {
|
if c == nil {
|
||||||
return []string{"(no content stored)"}
|
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.
|
// artifactContentMaxScroll keeps the last page of content anchored to the body bottom.
|
||||||
func (m Model) artifactContentMaxScroll() int {
|
func (m Model) artifactContentMaxScroll(w int) int {
|
||||||
max := len(m.artifactContentLines()) - m.artifactContentBodyH()
|
max := len(m.artifactContentLines(w)) - m.artifactContentBodyH()
|
||||||
if max < 0 {
|
if max < 0 {
|
||||||
max = 0
|
max = 0
|
||||||
}
|
}
|
||||||
@@ -515,11 +517,13 @@ func (m Model) artifactsModal() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Selected artifact content, scrollable with PgUp/PgDn.
|
// 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()
|
bodyH := m.artifactContentBodyH()
|
||||||
coff := m.artifactScroll
|
coff := m.artifactScroll
|
||||||
if coff > m.artifactContentMaxScroll() {
|
if coff > m.artifactContentMaxScroll(contentW) {
|
||||||
coff = m.artifactContentMaxScroll()
|
coff = m.artifactContentMaxScroll(contentW)
|
||||||
}
|
}
|
||||||
if coff < 0 {
|
if coff < 0 {
|
||||||
coff = 0
|
coff = 0
|
||||||
@@ -534,7 +538,7 @@ func (m Model) artifactsModal() string {
|
|||||||
cend = len(lines)
|
cend = len(lines)
|
||||||
}
|
}
|
||||||
for i := coff; i < cend; i++ {
|
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"}}))
|
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]) + "…"
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -401,7 +401,8 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
m.artifactScroll--
|
m.artifactScroll--
|
||||||
}
|
}
|
||||||
case k.Type == tea.KeyPgDown:
|
case k.Type == tea.KeyPgDown:
|
||||||
if m.artifactScroll < m.artifactContentMaxScroll() {
|
contentW := m.modalWidth() - 6
|
||||||
|
if m.artifactScroll < m.artifactContentMaxScroll(contentW) {
|
||||||
m.artifactScroll++
|
m.artifactScroll++
|
||||||
}
|
}
|
||||||
case runeIs(k, "v"):
|
case runeIs(k, "v"):
|
||||||
|
|||||||
Reference in New Issue
Block a user