mirror of
https://github.com/chenasraf/watchr.git
synced 2026-05-17 17:28:06 +00:00
feat: preview window json syntax highlighting
This commit is contained in:
2
go.mod
2
go.mod
@@ -12,11 +12,13 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/alecthomas/chroma/v2 v2.23.1 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -1,3 +1,5 @@
|
|||||||
|
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
@@ -14,6 +16,8 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ
|
|||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
|||||||
97
internal/ui/highlight.go
Normal file
97
internal/ui/highlight.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/chroma/v2"
|
||||||
|
"github.com/alecthomas/chroma/v2/formatters"
|
||||||
|
"github.com/alecthomas/chroma/v2/lexers"
|
||||||
|
"github.com/alecthomas/chroma/v2/styles"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ansiEscPattern matches ANSI escape sequences (CSI and simple ESC sequences).
|
||||||
|
var ansiEscPattern = regexp.MustCompile(`\x1b\[[0-9;?]*[a-zA-Z]|\x1b[^\[]`)
|
||||||
|
|
||||||
|
// stripANSI removes all ANSI escape sequences from a string.
|
||||||
|
func stripANSI(s string) string {
|
||||||
|
return ansiEscPattern.ReplaceAllString(s, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractJSON finds the first JSON object or array in a string,
|
||||||
|
// skipping any leading non-JSON content (e.g. ANSI codes, log prefixes).
|
||||||
|
// Returns the JSON substring and any prefix before it.
|
||||||
|
func extractJSON(s string) (prefix, jsonStr string, ok bool) {
|
||||||
|
// Find first { or [
|
||||||
|
idx := strings.IndexAny(s, "{[")
|
||||||
|
if idx < 0 {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return s[:idx], s[idx:], true
|
||||||
|
}
|
||||||
|
|
||||||
|
// highlightJSON attempts to detect JSON content, pretty-print it, and apply
|
||||||
|
// syntax highlighting for terminal output. Returns the original string
|
||||||
|
// unchanged if the content is not valid JSON.
|
||||||
|
func highlightJSON(s string) string {
|
||||||
|
trimmed := strings.TrimSpace(s)
|
||||||
|
if len(trimmed) == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip ANSI codes for JSON detection and parsing
|
||||||
|
clean := stripANSI(trimmed)
|
||||||
|
|
||||||
|
// Extract JSON from the string (may have a non-JSON prefix)
|
||||||
|
prefix, jsonStr, ok := extractJSON(clean)
|
||||||
|
if !ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to pretty-print the JSON portion
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := json.Indent(&buf, []byte(jsonStr), "", " "); err != nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
pretty := buf.String()
|
||||||
|
|
||||||
|
// Highlight with chroma (only the JSON portion)
|
||||||
|
lexer := lexers.Get("json")
|
||||||
|
if lexer == nil {
|
||||||
|
return pretty
|
||||||
|
}
|
||||||
|
lexer = chroma.Coalesce(lexer)
|
||||||
|
|
||||||
|
style := styles.Get("monokai")
|
||||||
|
if style == nil {
|
||||||
|
style = styles.Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter := formatters.Get("terminal256")
|
||||||
|
if formatter == nil {
|
||||||
|
formatter = formatters.Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
iterator, err := lexer.Tokenise(nil, pretty)
|
||||||
|
if err != nil {
|
||||||
|
return pretty
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err := formatter.Format(&out, style, iterator); err != nil {
|
||||||
|
return pretty
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-attach any non-JSON prefix (stripped of ANSI)
|
||||||
|
result := out.String()
|
||||||
|
if prefix != "" {
|
||||||
|
prefix = strings.TrimSpace(prefix)
|
||||||
|
if prefix != "" {
|
||||||
|
result = prefix + "\n" + result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
164
internal/ui/highlight_test.go
Normal file
164
internal/ui/highlight_test.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWrapTextANSI(t *testing.T) {
|
||||||
|
t.Run("ANSI sequences are not split", func(t *testing.T) {
|
||||||
|
// Red "ab" then reset: \033[31mab\033[0m
|
||||||
|
input := "\033[31mabcdef\033[0m"
|
||||||
|
lines := wrapText(input, 3)
|
||||||
|
// Should wrap into "abc" and "def", each with proper ANSI
|
||||||
|
if len(lines) != 2 {
|
||||||
|
t.Fatalf("expected 2 lines, got %d: %q", len(lines), lines)
|
||||||
|
}
|
||||||
|
// First line should have the color and a reset
|
||||||
|
if !strings.Contains(lines[0], "\033[31m") {
|
||||||
|
t.Errorf("first line missing color code: %q", lines[0])
|
||||||
|
}
|
||||||
|
if !strings.Contains(lines[0], "abc") {
|
||||||
|
t.Errorf("first line missing 'abc': %q", lines[0])
|
||||||
|
}
|
||||||
|
// Second line should re-apply the color
|
||||||
|
if !strings.Contains(lines[1], "\033[31m") {
|
||||||
|
t.Errorf("second line missing re-applied color code: %q", lines[1])
|
||||||
|
}
|
||||||
|
if !strings.Contains(lines[1], "def") {
|
||||||
|
t.Errorf("second line missing 'def': %q", lines[1])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no ANSI still works", func(t *testing.T) {
|
||||||
|
lines := wrapText("abcdef", 3)
|
||||||
|
if len(lines) != 2 {
|
||||||
|
t.Fatalf("expected 2 lines, got %d", len(lines))
|
||||||
|
}
|
||||||
|
if lines[0] != "abc" {
|
||||||
|
t.Errorf("expected 'abc', got %q", lines[0])
|
||||||
|
}
|
||||||
|
if lines[1] != "def" {
|
||||||
|
t.Errorf("expected 'def', got %q", lines[1])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ANSI reset clears active state", func(t *testing.T) {
|
||||||
|
// Color "ab", reset, then "cd"
|
||||||
|
input := "\033[31mab\033[0mcd"
|
||||||
|
lines := wrapText(input, 2)
|
||||||
|
// "ab" on first line (with color), "cd" on second (no color re-applied)
|
||||||
|
if len(lines) != 2 {
|
||||||
|
t.Fatalf("expected 2 lines, got %d: %q", len(lines), lines)
|
||||||
|
}
|
||||||
|
// Second line should NOT have the red color re-applied
|
||||||
|
if strings.Contains(lines[1], "\033[31m") {
|
||||||
|
t.Errorf("second line should not have color after reset: %q", lines[1])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapPreviewContentANSI(t *testing.T) {
|
||||||
|
// Multi-line input with ANSI codes should survive split + wrap
|
||||||
|
input := "\033[31mhello\033[0m\n\033[32mworld\033[0m"
|
||||||
|
lines := wrapPreviewContent(input, 80)
|
||||||
|
if len(lines) != 2 {
|
||||||
|
t.Fatalf("expected 2 lines, got %d", len(lines))
|
||||||
|
}
|
||||||
|
if !strings.Contains(lines[0], "\033[31m") || !strings.Contains(lines[0], "hello") {
|
||||||
|
t.Errorf("first line missing color or content: %q", lines[0])
|
||||||
|
}
|
||||||
|
if !strings.Contains(lines[1], "\033[32m") || !strings.Contains(lines[1], "world") {
|
||||||
|
t.Errorf("second line missing color or content: %q", lines[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHighlightJSON(t *testing.T) {
|
||||||
|
t.Run("valid JSON object is pretty-printed and highlighted", func(t *testing.T) {
|
||||||
|
input := `{"name":"test","count":42}`
|
||||||
|
result := highlightJSON(input)
|
||||||
|
|
||||||
|
// Should be pretty-printed (multi-line)
|
||||||
|
if !strings.Contains(result, "\n") {
|
||||||
|
t.Error("expected multi-line pretty-printed output")
|
||||||
|
}
|
||||||
|
// Should contain the key and value
|
||||||
|
if !strings.Contains(result, "name") {
|
||||||
|
t.Error("expected output to contain 'name'")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, "test") {
|
||||||
|
t.Error("expected output to contain 'test'")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, "42") {
|
||||||
|
t.Error("expected output to contain '42'")
|
||||||
|
}
|
||||||
|
// Should contain ANSI escape codes (syntax highlighting)
|
||||||
|
if !strings.Contains(result, "\033[") {
|
||||||
|
t.Error("expected ANSI color codes in output")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid JSON array", func(t *testing.T) {
|
||||||
|
input := `[1, 2, 3]`
|
||||||
|
result := highlightJSON(input)
|
||||||
|
if !strings.Contains(result, "\033[") {
|
||||||
|
t.Error("expected ANSI color codes for JSON array")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-JSON returns unchanged", func(t *testing.T) {
|
||||||
|
input := "hello world"
|
||||||
|
result := highlightJSON(input)
|
||||||
|
if result != input {
|
||||||
|
t.Errorf("expected unchanged output for non-JSON, got %q", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid JSON returns unchanged", func(t *testing.T) {
|
||||||
|
input := `{"broken": }`
|
||||||
|
result := highlightJSON(input)
|
||||||
|
if result != input {
|
||||||
|
t.Errorf("expected unchanged output for invalid JSON, got %q", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty string returns unchanged", func(t *testing.T) {
|
||||||
|
result := highlightJSON("")
|
||||||
|
if result != "" {
|
||||||
|
t.Errorf("expected empty string, got %q", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("whitespace-only returns unchanged", func(t *testing.T) {
|
||||||
|
input := " "
|
||||||
|
result := highlightJSON(input)
|
||||||
|
if result != input {
|
||||||
|
t.Errorf("expected unchanged output, got %q", result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("JSON with leading ANSI escape sequences", func(t *testing.T) {
|
||||||
|
input := "\x1b[34h\x1b[?25h{\"key\":\"value\"}"
|
||||||
|
result := highlightJSON(input)
|
||||||
|
if !strings.Contains(result, "key") {
|
||||||
|
t.Error("expected output to contain 'key'")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, "value") {
|
||||||
|
t.Error("expected output to contain 'value'")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, "\033[") {
|
||||||
|
t.Error("expected ANSI color codes in output")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("JSON with non-JSON prefix text", func(t *testing.T) {
|
||||||
|
input := `some prefix {"key":"value"}`
|
||||||
|
result := highlightJSON(input)
|
||||||
|
if !strings.Contains(result, "key") {
|
||||||
|
t.Error("expected output to contain 'key'")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result, "some prefix") {
|
||||||
|
t.Error("expected output to preserve prefix")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -598,6 +598,9 @@ func truncateToWidth(s string, maxWidth int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wrapText wraps text to fit within the given width, returning multiple lines.
|
// wrapText wraps text to fit within the given width, returning multiple lines.
|
||||||
|
// It is ANSI-aware: escape sequences are preserved intact and don't count
|
||||||
|
// toward the visible width. When a line wraps, any active ANSI state is
|
||||||
|
// carried over so colours continue on the next line.
|
||||||
func wrapText(s string, width int) []string {
|
func wrapText(s string, width int) []string {
|
||||||
if width <= 0 {
|
if width <= 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -607,29 +610,84 @@ func wrapText(s string, width int) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var lines []string
|
var lines []string
|
||||||
currentLine := ""
|
var currentLine strings.Builder
|
||||||
currentWidth := 0
|
currentWidth := 0
|
||||||
|
// Track the last seen ANSI escape so we can re-apply it after a wrap
|
||||||
|
var activeANSI string
|
||||||
|
|
||||||
for _, r := range s {
|
i := 0
|
||||||
|
runes := []rune(s)
|
||||||
|
for i < len(runes) {
|
||||||
|
// Check for ANSI escape sequence: ESC [ ... final_byte
|
||||||
|
if runes[i] == '\033' && i+1 < len(runes) && runes[i+1] == '[' {
|
||||||
|
// Consume entire escape sequence
|
||||||
|
var seq strings.Builder
|
||||||
|
seq.WriteRune(runes[i]) // ESC
|
||||||
|
i++
|
||||||
|
seq.WriteRune(runes[i]) // [
|
||||||
|
i++
|
||||||
|
for i < len(runes) {
|
||||||
|
seq.WriteRune(runes[i])
|
||||||
|
// Final byte of CSI sequence is in range 0x40-0x7E
|
||||||
|
if runes[i] >= 0x40 && runes[i] <= 0x7E {
|
||||||
|
i++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
seqStr := seq.String()
|
||||||
|
currentLine.WriteString(seqStr)
|
||||||
|
// Track reset vs color sequences
|
||||||
|
if seqStr == "\033[0m" || seqStr == "\033[m" {
|
||||||
|
activeANSI = ""
|
||||||
|
} else {
|
||||||
|
activeANSI = seqStr
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r := runes[i]
|
||||||
runeWidth := lipgloss.Width(string(r))
|
runeWidth := lipgloss.Width(string(r))
|
||||||
if currentWidth+runeWidth > width {
|
if currentWidth+runeWidth > width {
|
||||||
// Start new line
|
// Close any active ANSI on this line before wrapping
|
||||||
lines = append(lines, currentLine)
|
if activeANSI != "" {
|
||||||
currentLine = string(r)
|
currentLine.WriteString("\033[0m")
|
||||||
currentWidth = runeWidth
|
}
|
||||||
} else {
|
lines = append(lines, currentLine.String())
|
||||||
currentLine += string(r)
|
currentLine.Reset()
|
||||||
currentWidth += runeWidth
|
currentWidth = 0
|
||||||
|
// Re-apply active ANSI on the new line
|
||||||
|
if activeANSI != "" {
|
||||||
|
currentLine.WriteString(activeANSI)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
currentLine.WriteRune(r)
|
||||||
|
currentWidth += runeWidth
|
||||||
|
i++
|
||||||
}
|
}
|
||||||
// Don't forget the last line
|
// Don't forget the last line
|
||||||
if currentLine != "" {
|
if currentLine.Len() > 0 {
|
||||||
lines = append(lines, currentLine)
|
lines = append(lines, currentLine.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wrapPreviewContent splits multi-line content (e.g. pretty-printed JSON) by
|
||||||
|
// newlines first, then wraps each line to fit within the given width.
|
||||||
|
func wrapPreviewContent(s string, width int) []string {
|
||||||
|
var result []string
|
||||||
|
for line := range strings.SplitSeq(s, "\n") {
|
||||||
|
if line == "" {
|
||||||
|
result = append(result, "")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wrapped := wrapText(line, width)
|
||||||
|
result = append(result, wrapped...)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (m *model) updateFiltered() {
|
func (m *model) updateFiltered() {
|
||||||
m.filtered = []int{}
|
m.filtered = []int{}
|
||||||
m.filterRegexErr = nil
|
m.filterRegexErr = nil
|
||||||
@@ -1178,7 +1236,7 @@ func (m model) renderMainView() string {
|
|||||||
if m.showPreview && len(m.filtered) > 0 && m.cursor >= 0 && m.cursor < len(m.filtered) {
|
if m.showPreview && len(m.filtered) > 0 && m.cursor >= 0 && m.cursor < len(m.filtered) {
|
||||||
idx := m.filtered[m.cursor]
|
idx := m.filtered[m.cursor]
|
||||||
if idx < len(m.lines) {
|
if idx < len(m.lines) {
|
||||||
previewContent = m.lines[idx].Content
|
previewContent = highlightJSON(m.lines[idx].Content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1231,7 +1289,7 @@ func (m model) renderMainView() string {
|
|||||||
var previewLines []string
|
var previewLines []string
|
||||||
// Wrap preview content to fit width
|
// Wrap preview content to fit width
|
||||||
if previewContent != "" {
|
if previewContent != "" {
|
||||||
previewLines = wrapText(previewContent, innerWidth)
|
previewLines = wrapPreviewContent(previewContent, innerWidth)
|
||||||
}
|
}
|
||||||
// Pad preview to height
|
// Pad preview to height
|
||||||
for len(previewLines) < previewH {
|
for len(previewLines) < previewH {
|
||||||
@@ -1285,12 +1343,11 @@ func (m model) renderMainView() string {
|
|||||||
// Prepare preview lines (wrap text instead of truncating)
|
// Prepare preview lines (wrap text instead of truncating)
|
||||||
var previewLines []string
|
var previewLines []string
|
||||||
if previewContent != "" {
|
if previewContent != "" {
|
||||||
// Determine preview width for wrapping
|
|
||||||
previewW := leftW
|
previewW := leftW
|
||||||
if m.config.PreviewPosition == PreviewRight {
|
if m.config.PreviewPosition == PreviewRight {
|
||||||
previewW = rightW
|
previewW = rightW
|
||||||
}
|
}
|
||||||
previewLines = wrapText(previewContent, previewW)
|
previewLines = wrapPreviewContent(previewContent, previewW)
|
||||||
}
|
}
|
||||||
for len(previewLines) < listHeight {
|
for len(previewLines) < listHeight {
|
||||||
previewLines = append(previewLines, "")
|
previewLines = append(previewLines, "")
|
||||||
|
|||||||
Reference in New Issue
Block a user