summaryrefslogtreecommitdiff
path: root/internal/ui/help/help.go
blob: 0d332792f9ce5072f5dbfb6dd644be861e5a37ee (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package help

import (
	"fmt"
	"regexp"
	"strings"

	"charm.land/lipgloss/v2"
)

// Item is a single key binding and description in a help section.
type Item struct {
	Key  string
	Desc string
}

// Section groups related help items under a title.
type Section struct {
	Title string
	Items []Item
}

// Palette contains the colors needed to render help content.
type Palette struct {
	HeaderFG string
	HeaderBG string
	KeyFG    string
	DescFG   string
	SearchFG string
	SearchBG string
}

// Render converts help sections into styled terminal content.
func Render(sections []Section, palette Palette, search *regexp.Regexp) string {
	headerStyle, keyStyle, descStyle := styles(palette)
	lines := make([]string, 0, len(sections)*4)
	for i, section := range sections {
		lines = append(lines, headerStyle.Render(section.Title))
		for _, item := range section.Items {
			lines = append(lines, formatLine(item.Key, item.Desc, keyStyle, descStyle))
		}
		if i < len(sections)-1 {
			lines = append(lines, "")
		}
	}

	if search != nil {
		for i, line := range lines {
			if search.MatchString(line) {
				lines[i] = highlightLine(line, palette, search)
			}
		}
	}

	return strings.Join(lines, "\n")
}

// Lines returns plain text lines for searchable help content.
func Lines(sections []Section) []string {
	lines := make([]string, 0, len(sections)*4)
	for i, section := range sections {
		lines = append(lines, section.Title)
		for _, item := range section.Items {
			lines = append(lines, fmt.Sprintf("%s: %s", item.Key, item.Desc))
		}
		if i < len(sections)-1 {
			lines = append(lines, "")
		}
	}
	return lines
}

func styles(palette Palette) (lipgloss.Style, lipgloss.Style, lipgloss.Style) {
	headerStyle := lipgloss.NewStyle().
		Bold(true).
		Foreground(lipgloss.Color(palette.HeaderFG)).
		Background(lipgloss.Color(palette.HeaderBG)).
		Padding(0, 1)

	keyStyle := lipgloss.NewStyle().
		Bold(true).
		Foreground(lipgloss.Color(palette.KeyFG))

	descStyle := lipgloss.NewStyle().
		Foreground(lipgloss.Color(palette.DescFG))

	return headerStyle, keyStyle, descStyle
}

func formatLine(key, desc string, keyStyle, descStyle lipgloss.Style) string {
	paddedKey := fmt.Sprintf("%-12s", key)
	return keyStyle.Render(paddedKey) + " " + descStyle.Render(desc)
}

func highlightLine(line string, palette Palette, search *regexp.Regexp) string {
	matches := search.FindAllStringIndex(line, -1)
	if len(matches) == 0 {
		return line
	}

	highlightStyle := lipgloss.NewStyle().
		Background(lipgloss.Color(palette.SearchBG)).
		Foreground(lipgloss.Color(palette.SearchFG))

	highlighted := line
	offset := 0
	for _, match := range matches {
		start := match[0] + offset
		end := match[1] + offset
		rendered := highlightStyle.Render(highlighted[start:end])
		highlighted = highlighted[:start] + rendered + highlighted[end:]
		offset += len(rendered) - (end - start)
	}

	return highlighted
}