1package modes
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/patriceckhart/zot/internal/tui"
8)
9
10// changelogDialog is the one-shot release-notes overlay shown the
11// first time a user launches a new zot version. The body is the
12// markdown from the GitHub release page, rendered through the same
13// pipeline used for assistant messages so code fences + bold +
14// links look right.
15//
16// Any key dismisses; the parent Interactive then persists the
17// version-shown marker so the dialog never reappears for that
18// version.
19type changelogDialog struct {
20 active bool
21 version string
22 url string
23 body string
24 scroll int
25}
26
27func newChangelogDialog() *changelogDialog { return &changelogDialog{} }
28
29// Open populates and shows the dialog.
30func (d *changelogDialog) Open(version, url, body string) {
31 d.active = true
32 d.version = version
33 d.url = url
34 d.body = strings.TrimSpace(body)
35 d.scroll = 0
36}
37
38// Close hides the dialog.
39func (d *changelogDialog) Close() { d.active = false }
40
41// Active reports whether the overlay is visible and consuming keys.
42func (d *changelogDialog) Active() bool { return d != nil && d.active }
43
44// HandleKey: any key (other than scroll) closes the dialog. Returns
45// closed=true when the user dismissed; the parent uses that as the
46// signal to persist LastChangelogShown.
47func (d *changelogDialog) HandleKey(k tui.Key) (closed bool) {
48 if !d.Active() {
49 return false
50 }
51 switch k.Kind {
52 case tui.KeyUp:
53 if d.scroll > 0 {
54 d.scroll--
55 }
56 return false
57 case tui.KeyDown:
58 d.scroll++
59 return false
60 case tui.KeyPageUp:
61 d.scroll -= 8
62 if d.scroll < 0 {
63 d.scroll = 0
64 }
65 return false
66 case tui.KeyPageDown:
67 d.scroll += 8
68 return false
69 }
70 d.Close()
71 return true
72}
73
74// Render returns the dialog lines.
75func (d *changelogDialog) Render(th tui.Theme, width int) []string {
76 if !d.Active() {
77 return nil
78 }
79 title := fmt.Sprintf("zot %s \u2014 release notes (any key to dismiss)", d.version)
80 out := []string{frameHeaderColor(th, title, width, th.Accent)}
81 if d.url != "" {
82 out = append(out, " "+th.FG256(th.Muted, d.url))
83 out = append(out, "")
84 }
85
86 var bodyLines []string
87 for _, l := range strings.Split(d.body, "\n") {
88 if strings.HasPrefix(l, "\x00H:") {
89 // Heading: render in accent color, bold.
90 heading := strings.TrimPrefix(l, "\x00H:")
91 bodyLines = append(bodyLines, th.FG256(th.Accent, tui.Bold(heading)))
92 } else {
93 // Regular line: render through markdown for bullet points etc.
94 rendered := tui.RenderMarkdown(l, th, width-4)
95 for _, rl := range strings.Split(rendered, "\n") {
96 if len(rl) > 0 && rl[0] == tui.FlushLeftSentinel {
97 rl = rl[1:]
98 }
99 bodyLines = append(bodyLines, rl)
100 }
101 }
102 }
103
104 const maxRows = 18
105 if d.scroll > len(bodyLines)-1 {
106 d.scroll = len(bodyLines) - 1
107 }
108 if d.scroll < 0 {
109 d.scroll = 0
110 }
111 end := d.scroll + maxRows
112 if end > len(bodyLines) {
113 end = len(bodyLines)
114 }
115 for _, line := range bodyLines[d.scroll:end] {
116 out = append(out, " "+line)
117 }
118 if end < len(bodyLines) {
119 out = append(out, " "+th.FG256(th.Muted, fmt.Sprintf("\u2193 %d more lines (down/pgdn)", len(bodyLines)-end)))
120 }
121 out = append(out, frameRuleColor(th, width, th.Accent))
122 return out
123}