Skip to content

Commit f34b150

Browse files
arimxyerclaude
andcommitted
Add styled version list and interactive version picker
-list now shows a lipgloss table with version, date, and relative time (falls back to plain output when piped). New -pick flag opens a bubbletea interactive picker to browse and select a version to view. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 71b1edf commit f34b150

File tree

4 files changed

+157
-4
lines changed

4 files changed

+157
-4
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ Claude Code 2.0.73 (2025-12-19)
128128
| `-json` | Output as JSON |
129129
| `-md` | Output as markdown |
130130
| `-list` | List all available versions |
131+
| `-pick` | Interactive version picker |
131132
| `-version <ver>` | Fetch specific version |
132133
| `-web` | Open changelog source in browser |
133134
| `-v` | Show aic version |

main.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func main() {
8383
os.Exit(1)
8484
}
8585

86-
var jsonOutput, mdOutput, listVersions, webOpen bool
86+
var jsonOutput, mdOutput, listVersions, pickVersion, webOpen bool
8787
var targetVersion string
8888

8989
for i := 1; i < len(args); i++ {
@@ -94,6 +94,8 @@ func main() {
9494
mdOutput = true
9595
case "-list", "--list":
9696
listVersions = true
97+
case "-pick", "--pick":
98+
pickVersion = true
9799
case "-web", "--web":
98100
webOpen = true
99101
case "-version", "--version":
@@ -120,10 +122,13 @@ func main() {
120122
os.Exit(1)
121123
}
122124

125+
if pickVersion {
126+
runPickCommand(source.DisplayName, entries)
127+
os.Exit(0)
128+
}
129+
123130
if listVersions {
124-
for _, entry := range entries {
125-
fmt.Println(entry.Version)
126-
}
131+
outputVersionList(source.DisplayName, entries)
127132
os.Exit(0)
128133
}
129134

@@ -175,6 +180,7 @@ func printUsage() {
175180
fmt.Fprintf(os.Stderr, " -json Output as JSON\n")
176181
fmt.Fprintf(os.Stderr, " -md Output as markdown\n")
177182
fmt.Fprintf(os.Stderr, " -list List all versions\n")
183+
fmt.Fprintf(os.Stderr, " -pick Interactive version picker\n")
178184
fmt.Fprintf(os.Stderr, " -version <ver> Get specific version\n")
179185
fmt.Fprintf(os.Stderr, " -web Open changelog source in browser\n")
180186
fmt.Fprintf(os.Stderr, " -v, --version Show aic version\n")
@@ -183,6 +189,7 @@ func printUsage() {
183189
fmt.Fprintf(os.Stderr, " aic claude # Latest Claude Code entry\n")
184190
fmt.Fprintf(os.Stderr, " aic codex -json # Latest Codex entry as JSON\n")
185191
fmt.Fprintf(os.Stderr, " aic opencode -list # List OpenCode versions\n")
192+
fmt.Fprintf(os.Stderr, " aic claude -pick # Interactive version picker\n")
186193
fmt.Fprintf(os.Stderr, " aic gemini -version 0.21.0 # Specific Gemini version\n")
187194
fmt.Fprintf(os.Stderr, " aic latest # All releases in last 24h\n")
188195
fmt.Fprintf(os.Stderr, " aic status # Status table of all tools\n")

output.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
"strings"
88

99
"github.com/charmbracelet/glamour"
10+
"github.com/charmbracelet/lipgloss"
11+
"github.com/charmbracelet/lipgloss/table"
12+
"github.com/mattn/go-isatty"
1013
)
1114

1215
func outputJSON(entry *ChangelogEntry) {
@@ -40,6 +43,43 @@ func outputMarkdown(entry *ChangelogEntry) {
4043
}
4144
}
4245

46+
func outputVersionList(displayName string, entries []ChangelogEntry) {
47+
if !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) {
48+
for _, entry := range entries {
49+
fmt.Println(entry.Version)
50+
}
51+
return
52+
}
53+
54+
fmt.Printf("%s — %d releases\n\n", displayName, len(entries))
55+
56+
rows := make([][]string, len(entries))
57+
for i, entry := range entries {
58+
date := "-"
59+
ago := "-"
60+
if !entry.ReleasedAt.IsZero() {
61+
date = entry.ReleasedAt.Format("2006-01-02")
62+
ago = formatRelativeTime(entry.ReleasedAt)
63+
}
64+
rows[i] = []string{entry.Version, date, ago}
65+
}
66+
67+
headerStyle := lipgloss.NewStyle().Bold(true)
68+
69+
t := table.New().
70+
Border(lipgloss.NormalBorder()).
71+
Headers("Version", "Released", "Ago").
72+
Rows(rows...).
73+
StyleFunc(func(row, col int) lipgloss.Style {
74+
if row == table.HeaderRow {
75+
return headerStyle
76+
}
77+
return lipgloss.NewStyle()
78+
})
79+
80+
fmt.Println(t)
81+
}
82+
4383
func outputRendered(displayName string, entry *ChangelogEntry) {
4484
var dateStr string
4585
if !entry.ReleasedAt.IsZero() {

pick.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
tea "github.com/charmbracelet/bubbletea"
9+
"github.com/charmbracelet/lipgloss"
10+
)
11+
12+
type pickModel struct {
13+
entries []ChangelogEntry
14+
displayName string
15+
cursor int
16+
selected int
17+
quitting bool
18+
}
19+
20+
func (m pickModel) Init() tea.Cmd {
21+
return nil
22+
}
23+
24+
func (m pickModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
25+
switch msg := msg.(type) {
26+
case tea.KeyMsg:
27+
switch msg.String() {
28+
case "up", "k":
29+
if m.cursor > 0 {
30+
m.cursor--
31+
}
32+
case "down", "j":
33+
if m.cursor < len(m.entries)-1 {
34+
m.cursor++
35+
}
36+
case "enter":
37+
m.selected = m.cursor
38+
m.quitting = true
39+
return m, tea.Quit
40+
case "q", "esc", "ctrl+c":
41+
m.selected = -1
42+
m.quitting = true
43+
return m, tea.Quit
44+
}
45+
}
46+
return m, nil
47+
}
48+
49+
func (m pickModel) View() string {
50+
if m.quitting {
51+
return ""
52+
}
53+
54+
bold := lipgloss.NewStyle().Bold(true)
55+
dim := lipgloss.NewStyle().Faint(true)
56+
57+
var b strings.Builder
58+
59+
for i, entry := range m.entries {
60+
date := "-"
61+
ago := "-"
62+
if !entry.ReleasedAt.IsZero() {
63+
date = entry.ReleasedAt.Format("2006-01-02")
64+
ago = formatRelativeTime(entry.ReleasedAt)
65+
}
66+
67+
pointer := " "
68+
if i == m.cursor {
69+
pointer = "> "
70+
}
71+
72+
line := fmt.Sprintf("%s%-12s %-12s %s", pointer, entry.Version, date, ago)
73+
if i == m.cursor {
74+
line = bold.Render(line)
75+
}
76+
b.WriteString(line)
77+
b.WriteString("\n")
78+
}
79+
80+
b.WriteString("\n")
81+
b.WriteString(dim.Render(" ↑/↓ navigate • enter select • q cancel"))
82+
b.WriteString("\n")
83+
84+
return b.String()
85+
}
86+
87+
func runPickCommand(displayName string, entries []ChangelogEntry) {
88+
model := pickModel{
89+
entries: entries,
90+
displayName: displayName,
91+
selected: -1,
92+
}
93+
94+
p := tea.NewProgram(model)
95+
result, err := p.Run()
96+
if err != nil {
97+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
98+
os.Exit(1)
99+
}
100+
101+
final := result.(pickModel)
102+
if final.selected >= 0 && final.selected < len(entries) {
103+
outputRendered(displayName, &entries[final.selected])
104+
}
105+
}

0 commit comments

Comments
 (0)