package modes import ( "fmt" "strings" "github.com/bnema/zut/packages/tui" ) // changelogDialog is the one-shot release-notes overlay shown the // first time a user launches a new zut version. The body is the // markdown from the GitHub release page, rendered through the same // pipeline used for assistant messages so code fences + bold + // links look right. // // Any key dismisses; the parent Interactive then persists the // version-shown marker so the dialog never reappears for that // version. type changelogDialog struct { active bool version string url string body string scroll int } func newChangelogDialog() *changelogDialog { return &changelogDialog{} } // Open populates and shows the dialog. func (d *changelogDialog) Open(version, url, body string) { d.active = true d.version = version d.url = url d.body = strings.TrimSpace(body) d.scroll = 0 } // Close hides the dialog. func (d *changelogDialog) Close() { d.active = false } // Active reports whether the overlay is visible and consuming keys. func (d *changelogDialog) Active() bool { return d != nil && d.active } // HandleKey: any key (other than scroll) closes the dialog. Returns // closed=true when the user dismissed; the parent uses that as the // signal to persist LastChangelogShown. func (d *changelogDialog) HandleKey(k tui.Key) (closed bool) { if !d.Active() { return false } switch k.Kind { case tui.KeyUp: if d.scroll > 0 { d.scroll-- } return false case tui.KeyDown: d.scroll++ return false case tui.KeyPageUp: d.scroll -= 8 if d.scroll < 0 { d.scroll = 0 } return false case tui.KeyPageDown: d.scroll += 8 return false } d.Close() return true } // Render returns the dialog lines. func (d *changelogDialog) Render(th tui.Theme, width int) []string { if !d.Active() { return nil } title := fmt.Sprintf("zut %s \u2014 release notes (any key to dismiss)", d.version) out := []string{frameHeaderColor(th, title, width, th.Accent)} if d.url != "" { out = append(out, " "+th.FGColor(th.Muted, d.url)) out = append(out, "") } var bodyLines []string for _, l := range strings.Split(d.body, "\n") { if strings.HasPrefix(l, "\x00H:") { // Heading: render in accent color, bold. heading := strings.TrimPrefix(l, "\x00H:") styled := th.FGColor(th.Accent, tui.Bold(heading)) bodyLines = append(bodyLines, wrapDialogTextRows(styled, width)...) } else { // Regular line: render and wrap through the shared dialog // markdown path so ANSI styling does not affect display width. bodyLines = append(bodyLines, renderDialogMarkdownRows(l, th, width)...) } } const maxRows = 18 if d.scroll > len(bodyLines)-1 { d.scroll = len(bodyLines) - 1 } if d.scroll < 0 { d.scroll = 0 } end := d.scroll + maxRows if end > len(bodyLines) { end = len(bodyLines) } out = append(out, bodyLines[d.scroll:end]...) if end < len(bodyLines) { out = append(out, " "+th.FGColor(th.Muted, fmt.Sprintf("\u2193 %d more lines (down/pgdn)", len(bodyLines)-end))) } out = append(out, frameRuleColor(th, width, th.Accent)) return out }