From dc856677cc482b7ad3e0038c2aa23b67df26e48e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Sun, 11 Oct 2026 11:23:20 +0200 Subject: [PATCH 1/2] feat: the changelog in the status pane, built in Co-Authored-By: Claude Opus 5.5 (1M context) --- changelog.go | 10 ++++++++++ cliff.toml | 2 ++ cmd/lazysecrets/main.go | 2 ++ docs/keys.md | 4 +++- internal/ui/app/app.go | 5 ++++- internal/ui/app/status.go | 30 +++++++++++++++++++++++++++++- internal/ui/app/status_test.go | 30 ++++++++++++++++++++++++++++++ internal/update/update.go | 5 ++++- 8 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 changelog.go create mode 100644 internal/ui/app/status_test.go diff --git a/changelog.go b/changelog.go new file mode 100644 index 0000000..915cd66 --- /dev/null +++ b/changelog.go @@ -0,0 +1,10 @@ +// Package lazysecrets holds what the module's root has for the app: the +// changelog, embedded so the status pane can show it offline. +package lazysecrets + +import _ "embed" + +// Changelog is CHANGELOG.md as of the build: every release up to it. +// +//go:embed CHANGELOG.md +var Changelog string diff --git a/cliff.toml b/cliff.toml index 1978ee8..e82f71c 100644 --- a/cliff.toml +++ b/cliff.toml @@ -49,6 +49,8 @@ commit_preprocessors = [ # A merge commit (not a squash) says "Merge pull request 'feat: …' (#4) # from branch into main": keep the title and number. { pattern = "^Merge pull request '(.+)' \\((#[0-9]+)\\) from .*", replace = "${1} (${2})" }, + # #11 was squash-merged with #10 (the status pane) in it: name both. + { pattern = "^feat: link each changelog entry to its PR and commit \\(#11\\)", replace = "feat: a status pane (0) with version, updates, accounts, files and links; changelog entries link to their PR and commit (#11)" }, # Titles may be written "[feat] x", "[feat]: x" or "feat:x" (see # pr-title.yml); all become "feat: x". { pattern = "^\\[(feat|fix|perf|refactor|docs|test|ci|chore|build|style|revert)(\\([^)]*\\))?(!?)\\](!?):?\\s*", replace = "${1}${2}${3}${4}: " }, diff --git a/cmd/lazysecrets/main.go b/cmd/lazysecrets/main.go index 3142ea5..571de9e 100644 --- a/cmd/lazysecrets/main.go +++ b/cmd/lazysecrets/main.go @@ -16,6 +16,7 @@ import ( tea "charm.land/bubbletea/v2" + "git.kebcloud.no/keb/lazysecrets" "git.kebcloud.no/keb/lazysecrets/internal/commands" "git.kebcloud.no/keb/lazysecrets/internal/config" "git.kebcloud.no/keb/lazysecrets/internal/instance" @@ -371,6 +372,7 @@ Flags: CacheFile: cacheFile, ConfigFiles: configFiles(*configPath), StatePath: statePath, + Changelog: lazysecrets.Changelog, Dir: placeDir, Open: open, Styles: &theme, diff --git a/docs/keys.md b/docs/keys.md index 00554e5..4f0508c 100644 --- a/docs/keys.md +++ b/docs/keys.md @@ -116,7 +116,9 @@ On top, the status pane (`0`) shows the version, a newer release and how your accounts are doing. Focused, the right pane lists more: the version and updates (`U`), each account (`L` on one that needs it signs in), the config, state and cache files (`e` opens one in your editor), and links -to the docs, source and issues (`o` opens one). `y` copies any of them. +to the docs, source and issues (`o` opens one), then the changelog of +every release up to yours, built in so it needs no network: `/` filters +it (`/fixed`, `/search`). `y` copies any row. | Key | Action | |---|---| diff --git a/internal/ui/app/app.go b/internal/ui/app/app.go index 8efcdf6..7b0880c 100644 --- a/internal/ui/app/app.go +++ b/internal/ui/app/app.go @@ -75,6 +75,8 @@ type Options struct { // and keeps its state, for the status pane. ConfigFiles []string StatePath string + // Changelog is CHANGELOG.md, for the status pane. + Changelog string // CacheFile keeps what was loaded between runs; "" keeps it for this // run only. CacheFile string @@ -185,6 +187,7 @@ type Model struct { configFiles []string statePath string statusW int // the status pane's width: the left column's, zoomed or not + changelog []panes.Field cacheDirty bool sourcesAt map[string]time.Time placeDir string @@ -448,7 +451,7 @@ func New(providers []provider.Provider, opts Options) Model { m.spinning = true m.cachePath, m.placeDir, m.sourcesAt = opts.CacheFile, opts.Dir, map[string]time.Time{} - m.configFiles, m.statePath = opts.ConfigFiles, opts.StatePath + m.configFiles, m.statePath, m.changelog = opts.ConfigFiles, opts.StatePath, changelogFields(opts.Changelog) if m.cachePath != "" { m.loadCacheFile() if len(m.sourceResults) > 0 { diff --git a/internal/ui/app/status.go b/internal/ui/app/status.go index e1427f7..893b830 100644 --- a/internal/ui/app/status.go +++ b/internal/ui/app/status.go @@ -94,7 +94,7 @@ func (m Model) statusFields() []panes.Field { add(panes.Field{Label: "docs", Value: repo + "/…/docs", URL: update.DefaultRepo + "/src/branch/main/docs"}) add(panes.Field{Label: "source", Value: repo, URL: update.DefaultRepo}) add(panes.Field{Label: "issues", Value: repo + "/issues", URL: update.DefaultRepo + "/issues"}) - return out + return append(out, m.changelog...) } // updateStatus says whether a newer release is out, and when it was @@ -141,3 +141,31 @@ func ago(d time.Duration) string { } return fmt.Sprintf("%dd ago", int(d.Hours()/24)) } + +// changelogFields are CHANGELOG.md as rows: each release's version and +// date, then its entries, marked by what kind they are. +func changelogFields(md string) []panes.Field { + var out []panes.Field + kind := "" + for _, line := range strings.Split(md, "\n") { + line = strings.TrimSpace(line) + switch { + case strings.HasPrefix(line, "## "): + version, date, _ := strings.Cut(strings.TrimPrefix(line, "## "), " – ") + out = append(out, panes.Field{Label: version, Value: date, Info: true}) + kind = "" + case strings.HasPrefix(line, "### "): + kind = strings.ToLower(strings.TrimPrefix(line, "### ")) + case strings.HasPrefix(line, "- ") && len(out) > 0: + text := update.PlainLinks(strings.TrimPrefix(line, "- ")) + if kind != "" { + text = kind + ": " + text + } + out = append(out, panes.Field{Value: text}) + case line != "" && len(out) > 0 && !strings.HasPrefix(line, "\n" + got := changelogFields(md) + want := []panes.Field{ + {Label: "v0.2.0", Value: "2026-10-12", Info: true}, + {Value: "added: Search (#11) (65dc2d7)"}, + {Value: "fixed: A fix (#3)"}, + {Label: "v0.1.0", Value: "2026-10-10", Info: true}, + {Value: "Hello world 👋"}, + } + if len(got) != len(want) { + t.Fatalf("got %+v", got) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("row %d: %+v, want %+v", i, got[i], want[i]) + } + } +} diff --git a/internal/update/update.go b/internal/update/update.go index d0e251e..4d1a9e3 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -50,9 +50,12 @@ type Release struct { // every release ends with. func (r Release) Notes() string { notes, _, _ := strings.Cut(r.Body, "## Install") - return mdLink.ReplaceAllString(strings.TrimSpace(notes), "$1") + return PlainLinks(strings.TrimSpace(notes)) } +// PlainLinks shows Markdown links as their text, for a terminal. +func PlainLinks(s string) string { return mdLink.ReplaceAllString(s, "$1") } + // mdLink is a Markdown link, [text](url): the notes link PRs and commits, // and a terminal shows them as their text. var mdLink = regexp.MustCompile(`\[([^\]]+)\]\((https?://[^)\s]+)\)`) -- 2.54.0 From c129f22f5b5045d6d9e46012376ab291a66b61e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Sun, 11 Oct 2026 13:09:49 +0200 Subject: [PATCH 2/2] feat: releases and changelog links in the status pane; the version opens its release Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/keys.md | 3 ++- internal/ui/app/status.go | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/keys.md b/docs/keys.md index 4f0508c..893d179 100644 --- a/docs/keys.md +++ b/docs/keys.md @@ -116,7 +116,8 @@ On top, the status pane (`0`) shows the version, a newer release and how your accounts are doing. Focused, the right pane lists more: the version and updates (`U`), each account (`L` on one that needs it signs in), the config, state and cache files (`e` opens one in your editor), and links -to the docs, source and issues (`o` opens one), then the changelog of +to the docs, source, issues, releases and changelog (`o` opens one; on +the version or update, its release), then the changelog of every release up to yours, built in so it needs no network: `/` filters it (`/fixed`, `/search`). `y` copies any row. diff --git a/internal/ui/app/status.go b/internal/ui/app/status.go index 893b830..02628fa 100644 --- a/internal/ui/app/status.go +++ b/internal/ui/app/status.go @@ -65,8 +65,17 @@ func (m Model) statusFields() []panes.Field { out = append(out, f) } } - add(panes.Field{Label: "version", Value: cmpOr(m.versionLabel(), "dev")}) - add(panes.Field{Label: "update", Value: m.updateStatus(), Info: m.newer == nil}) + // A release's rows open its release page. + version := panes.Field{Label: "version", Value: cmpOr(m.versionLabel(), "dev")} + if strings.HasPrefix(version.Value, "v") { + version.URL = update.DefaultRepo + "/releases/tag/" + version.Value + } + add(version) + upd := panes.Field{Label: "update", Value: m.updateStatus(), Info: m.newer == nil} + if m.newer != nil { + upd.URL = update.DefaultRepo + "/releases/tag/" + m.newer.Tag + } + add(upd) add(panes.Field{Label: "license", Value: "GPL-3.0-or-later · © 2026 Karl-Einar Bergstrøm", Info: true}) for i, p := range m.providers { @@ -94,6 +103,8 @@ func (m Model) statusFields() []panes.Field { add(panes.Field{Label: "docs", Value: repo + "/…/docs", URL: update.DefaultRepo + "/src/branch/main/docs"}) add(panes.Field{Label: "source", Value: repo, URL: update.DefaultRepo}) add(panes.Field{Label: "issues", Value: repo + "/issues", URL: update.DefaultRepo + "/issues"}) + add(panes.Field{Label: "releases", Value: repo + "/releases", URL: update.DefaultRepo + "/releases"}) + add(panes.Field{Label: "changelog", Value: repo + "/…/CHANGELOG.md", URL: update.DefaultRepo + "/src/branch/main/CHANGELOG.md"}) return append(out, m.changelog...) } -- 2.54.0