tangled
alpha
login
or
join now
treethought.xyz
/
attie
5
fork
atom
AT Protocol Terminal Interface Explorer
5
fork
atom
overview
issues
pulls
pipelines
use list bubbl for collections
Cam Sweeney
3 weeks ago
bd3efd39
6e6f65e8
+153
-49
6 changed files
expand all
collapse all
unified
split
at
client.go
go.mod
go.sum
main.go
ui
app.go
repo.go
+1
-1
at/client.go
···
57
return nil, fmt.Errorf("failed to get client with identifier: %w", err)
58
}
59
60
-
// TODO: downlaod repo as car
61
// https://github.com/bluesky-social/cookbook/blob/main/go-repo-export/main.go#L46
62
resp, err := comatproto.RepoDescribeRepo(ctx, client, repo)
63
if err != nil {
···
57
return nil, fmt.Errorf("failed to get client with identifier: %w", err)
58
}
59
60
+
// TODO: download repo as car
61
// https://github.com/bluesky-social/cookbook/blob/main/go-repo-export/main.go#L46
62
resp, err := comatproto.RepoDescribeRepo(ctx, client, repo)
63
if err != nil {
+1
go.mod
···
48
github.com/prometheus/common v0.45.0 // indirect
49
github.com/prometheus/procfs v0.12.0 // indirect
50
github.com/rivo/uniseg v0.4.7 // indirect
0
51
github.com/spaolacci/murmur3 v1.1.0 // indirect
52
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect
53
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
···
48
github.com/prometheus/common v0.45.0 // indirect
49
github.com/prometheus/procfs v0.12.0 // indirect
50
github.com/rivo/uniseg v0.4.7 // indirect
51
+
github.com/sahilm/fuzzy v0.1.1 // indirect
52
github.com/spaolacci/murmur3 v1.1.0 // indirect
53
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect
54
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+8
go.sum
···
2
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
3
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
4
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
0
0
5
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
6
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
7
github.com/bluesky-social/indigo v0.0.0-20260213232405-1286ca7a7cb2 h1:q/dijVJ+cA17e2qmJZPNuB7anByq1W6+uYJr1D9gfto=
···
20
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
21
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
22
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
0
0
23
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
24
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
25
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
···
42
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
43
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
44
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
0
0
45
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
46
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
47
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
···
84
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
85
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
86
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
0
0
87
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
88
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
89
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
···
2
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
3
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
4
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
5
+
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
6
+
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
7
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
8
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
9
github.com/bluesky-social/indigo v0.0.0-20260213232405-1286ca7a7cb2 h1:q/dijVJ+cA17e2qmJZPNuB7anByq1W6+uYJr1D9gfto=
···
22
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
23
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
24
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
25
+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
26
+
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
27
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
28
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
29
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
···
46
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
47
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
48
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
49
+
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
50
+
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
51
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
52
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
53
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
···
90
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
91
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
92
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
93
+
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
94
+
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
95
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
96
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
97
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
+2
-2
main.go
···
16
17
func main() {
18
log.SetFormatter(&log.TextFormatter{
19
-
FullTimestamp: false,
20
DisableTimestamp: true,
21
})
22
f, err := os.Create("debug.log")
···
29
30
app := ui.NewApp()
31
32
-
p := tea.NewProgram(app)
33
if _, err := p.Run(); err != nil {
34
log.Fatal(err)
35
}
···
16
17
func main() {
18
log.SetFormatter(&log.TextFormatter{
19
+
FullTimestamp: false,
20
DisableTimestamp: true,
21
})
22
f, err := os.Create("debug.log")
···
29
30
app := ui.NewApp()
31
32
+
p := tea.NewProgram(app, tea.WithAltScreen())
33
if _, err := p.Run(); err != nil {
34
log.Fatal(err)
35
}
+6
-3
ui/app.go
···
24
25
func NewApp() *App {
26
search := &CommandPallete{}
27
-
repoView := &RepoView{}
28
return &App{
29
client: at.NewClient(""),
30
search: search,
···
40
func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
41
switch msg := msg.(type) {
42
// top level always handle ctrl-c
0
0
0
43
case tea.KeyMsg:
44
switch msg.String() {
45
case "ctrl+c", "q":
···
66
}
67
68
case repoLoadedMsg:
69
-
a.repoView.SetRepo(msg.repo)
70
a.active = a.repoView
71
a.search.loading = false
72
-
return a, nil
73
74
case repoErrorMsg:
75
a.search.err = msg.err.Error()
···
24
25
func NewApp() *App {
26
search := &CommandPallete{}
27
+
repoView := NewRepoView()
28
return &App{
29
client: at.NewClient(""),
30
search: search,
···
40
func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
41
switch msg := msg.(type) {
42
// top level always handle ctrl-c
43
+
case tea.WindowSizeMsg:
44
+
a.active, _ = a.active.Update(msg)
45
+
return a, nil
46
case tea.KeyMsg:
47
switch msg.String() {
48
case "ctrl+c", "q":
···
69
}
70
71
case repoLoadedMsg:
72
+
cmd := a.repoView.SetRepo(msg.repo)
73
a.active = a.repoView
74
a.search.loading = false
75
+
return a, cmd
76
77
case repoErrorMsg:
78
a.search.err = msg.err.Error()
+135
-43
ui/repo.go
···
2
3
import (
4
"fmt"
0
0
5
comatproto "github.com/bluesky-social/indigo/api/atproto"
0
6
tea "github.com/charmbracelet/bubbletea"
7
"github.com/charmbracelet/lipgloss"
8
-
"strings"
9
)
10
11
-
// =============================================================================
12
-
// RepoView - Displays repository information and collections
13
-
// =============================================================================
14
-
15
var (
16
headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205"))
17
labelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
···
20
dimStyle = lipgloss.NewStyle().Faint(true)
21
)
22
23
-
type RepoView struct {
24
-
repo *comatproto.RepoDescribeRepo_Output
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
25
}
26
27
-
func (r *RepoView) Init() tea.Cmd {
28
return nil
29
}
30
31
-
func (r *RepoView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
32
-
// RepoView doesn't handle messages directly
33
-
// Parent App handles navigation
34
-
return r, nil
35
}
36
37
-
func (r *RepoView) View() string {
38
-
if r.repo == nil {
39
-
return "No repository loaded"
40
}
0
0
41
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
42
var s strings.Builder
43
44
-
// Header
45
-
s.WriteString(headerStyle.Render("📦 Repository Information"))
46
s.WriteString("\n\n")
47
48
-
// Repository details
49
s.WriteString(labelStyle.Render("Handle: "))
50
s.WriteString(valueStyle.Render(r.repo.Handle))
0
0
0
0
0
0
0
51
s.WriteString("\n")
52
53
s.WriteString(labelStyle.Render("DID: "))
54
-
s.WriteString(valueStyle.Render(r.repo.Did))
0
0
0
0
0
55
s.WriteString("\n")
56
57
-
if r.repo.DidDoc != nil {
58
-
s.WriteString(labelStyle.Render("DID Document: "))
59
-
s.WriteString(dimStyle.Render("Available"))
60
-
s.WriteString("\n")
61
-
}
62
63
-
s.WriteString("\n")
0
0
0
0
0
0
64
65
-
// Collections section
66
-
s.WriteString(headerStyle.Render("Collections"))
67
-
s.WriteString(fmt.Sprintf(" (%d total)", len(r.repo.Collections)))
68
-
s.WriteString("\n\n")
69
70
-
if len(r.repo.Collections) == 0 {
71
-
s.WriteString(dimStyle.Render("No collections found"))
72
-
} else {
73
-
for _, collection := range r.repo.Collections {
74
-
s.WriteString(" • ")
75
-
s.WriteString(collectionStyle.Render(collection))
76
-
s.WriteString("\n")
77
-
}
78
}
0
0
0
0
79
80
-
s.WriteString("\n\n")
0
0
0
0
0
0
81
82
-
// Footer with help
83
-
s.WriteString(dimStyle.Render("Press Esc to go back • Ctrl+C to quit"))
0
0
0
84
85
-
return s.String()
86
}
87
88
-
func (r *RepoView) SetRepo(repo *comatproto.RepoDescribeRepo_Output) {
89
-
r.repo = repo
0
0
0
0
0
0
0
0
0
0
0
0
0
90
}
···
2
3
import (
4
"fmt"
5
+
"strings"
6
+
7
comatproto "github.com/bluesky-social/indigo/api/atproto"
8
+
"github.com/charmbracelet/bubbles/list"
9
tea "github.com/charmbracelet/bubbletea"
10
"github.com/charmbracelet/lipgloss"
0
11
)
12
0
0
0
0
13
var (
14
headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205"))
15
labelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
···
18
dimStyle = lipgloss.NewStyle().Faint(true)
19
)
20
21
+
type CollectionList struct {
22
+
list list.Model
23
+
}
24
+
25
+
type CollectionListItem struct {
26
+
Name string
27
+
}
28
+
29
+
func (c CollectionListItem) FilterValue() string {
30
+
return c.Name
31
+
}
32
+
func (c CollectionListItem) Title() string {
33
+
return collectionStyle.Render(c.Name)
34
+
}
35
+
func (c CollectionListItem) Description() string {
36
+
return ""
37
+
}
38
+
39
+
func NewCollectionList(collections []string) *CollectionList {
40
+
items := make([]list.Item, len(collections))
41
+
for i, col := range collections {
42
+
ci := CollectionListItem{Name: col}
43
+
items[i] = list.Item(ci)
44
+
}
45
+
del := list.DefaultDelegate{
46
+
ShowDescription: false,
47
+
Styles: list.NewDefaultItemStyles(),
48
+
}
49
+
del.SetHeight(1)
50
+
51
+
l := list.New(items, del, 80, 20)
52
+
l.SetShowTitle(false)
53
+
l.SetShowStatusBar(false)
54
+
l.SetFilteringEnabled(false)
55
+
return &CollectionList{
56
+
list: l,
57
+
}
58
}
59
60
+
func (cl *CollectionList) Init() tea.Cmd {
61
return nil
62
}
63
64
+
func (cl *CollectionList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
65
+
var cmd tea.Cmd
66
+
cl.list, cmd = cl.list.Update(msg)
67
+
return cl, cmd
68
}
69
70
+
func (cl *CollectionList) View() string {
71
+
if len(cl.list.Items()) == 0 {
72
+
return dimStyle.Render("No collections found")
73
}
74
+
return cl.list.View()
75
+
}
76
77
+
type RepoView struct {
78
+
repo *comatproto.RepoDescribeRepo_Output
79
+
clist *CollectionList
80
+
header string
81
+
width int
82
+
height int
83
+
}
84
+
85
+
func NewRepoView() *RepoView {
86
+
return &RepoView{
87
+
clist: NewCollectionList([]string{}),
88
+
width: 80,
89
+
height: 24,
90
+
}
91
+
}
92
+
93
+
func (r *RepoView) buildHeader() string {
94
+
if r.repo == nil {
95
+
return ""
96
+
}
97
var s strings.Builder
98
99
+
// Title
100
+
s.WriteString(headerStyle.Render("📦 Repository"))
101
s.WriteString("\n\n")
102
0
103
s.WriteString(labelStyle.Render("Handle: "))
104
s.WriteString(valueStyle.Render(r.repo.Handle))
105
+
s.WriteString(" ")
106
+
s.WriteString(labelStyle.Render("Valid: "))
107
+
if r.repo.HandleIsCorrect {
108
+
s.WriteString(valueStyle.Render("✓"))
109
+
} else {
110
+
s.WriteString(dimStyle.Render("✗"))
111
+
}
112
s.WriteString("\n")
113
114
s.WriteString(labelStyle.Render("DID: "))
115
+
s.WriteString(dimStyle.Render(r.repo.Did))
116
+
s.WriteString("\n\n")
117
+
118
+
// Collections section header
119
+
s.WriteString(headerStyle.Render("Collections "))
120
+
s.WriteString(dimStyle.Render(fmt.Sprintf("(%d)", len(r.repo.Collections))))
121
s.WriteString("\n")
122
123
+
return s.String()
124
+
}
0
0
0
125
126
+
func (r *RepoView) SetRepo(repo *comatproto.RepoDescribeRepo_Output) tea.Cmd {
127
+
r.repo = repo
128
+
r.header = r.buildHeader()
129
+
r.clist = NewCollectionList(repo.Collections)
130
+
r.updateListSize()
131
+
return r.clist.Init()
132
+
}
133
134
+
func (r *RepoView) Init() tea.Cmd {
135
+
return nil
136
+
}
0
137
138
+
func (r *RepoView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
139
+
switch msg := msg.(type) {
140
+
case tea.WindowSizeMsg:
141
+
r.width = msg.Width
142
+
r.height = msg.Height
143
+
r.updateListSize()
0
0
144
}
145
+
clist, cmd := r.clist.Update(msg)
146
+
r.clist = clist.(*CollectionList)
147
+
return r, cmd
148
+
}
149
150
+
// updateListSize calculates and sets the list size to fill remaining space
151
+
func (r *RepoView) updateListSize() {
152
+
if r.clist == nil {
153
+
return
154
+
}
155
+
headerHeight := lipgloss.Height(r.header)
156
+
footerHeight := 2 // "\n" + help text line
157
158
+
// List gets all remaining space
159
+
listHeight := r.height - headerHeight - footerHeight
160
+
if listHeight < 5 {
161
+
listHeight = 5
162
+
}
163
164
+
r.clist.list.SetSize(r.width, listHeight)
165
}
166
167
+
func (r *RepoView) View() string {
168
+
if r.repo == nil {
169
+
return "No repository loaded"
170
+
}
171
+
172
+
// Footer help text
173
+
footer := dimStyle.Render("Press Esc to go back • ↑/↓ or j/k to navigate • Ctrl+C to quit")
174
+
175
+
// Join header (fixed), list (scrollable), and footer
176
+
return lipgloss.JoinVertical(
177
+
lipgloss.Left,
178
+
r.header,
179
+
r.clist.View(),
180
+
"\n"+footer,
181
+
)
182
}