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