AT Protocol Terminal Interface Explorer
1package ui
2
3import (
4 "fmt"
5 "strings"
6
7 comatproto "github.com/bluesky-social/indigo/api/atproto"
8 "github.com/bluesky-social/indigo/atproto/identity"
9 "github.com/charmbracelet/bubbles/list"
10 tea "github.com/charmbracelet/bubbletea"
11 "github.com/charmbracelet/lipgloss"
12 "github.com/treethought/attie/at"
13)
14
15var (
16 headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205"))
17 labelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
18 valueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("255"))
19 collectionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
20 dimStyle = lipgloss.NewStyle().Faint(true)
21)
22
23type CollectionList struct {
24 list list.Model
25}
26
27type CollectionListItem struct {
28 Name string
29}
30
31func (c CollectionListItem) FilterValue() string {
32 return c.Name
33}
34func (c CollectionListItem) Title() string {
35 return c.Name
36 // return collectionStyle.Render(c.Name)
37}
38func (c CollectionListItem) Description() string {
39 return ""
40}
41
42func NewCollectionList(collections []string) *CollectionList {
43 items := make([]list.Item, len(collections))
44 for i, col := range collections {
45 ci := CollectionListItem{Name: col}
46 items[i] = list.Item(ci)
47 }
48 del := list.DefaultDelegate{
49 ShowDescription: false,
50 Styles: list.NewDefaultItemStyles(),
51 }
52 del.SetHeight(1)
53
54 l := list.New(items, del, 80, 20)
55 l.SetShowTitle(false)
56 l.SetShowStatusBar(false)
57 l.SetFilteringEnabled(true)
58 return &CollectionList{
59 list: l,
60 }
61}
62
63func (cl *CollectionList) Init() tea.Cmd {
64 return nil
65}
66
67func (cl *CollectionList) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
68
69 if !cl.list.SettingFilter() {
70 switch msg := msg.(type) {
71 case tea.KeyMsg:
72 switch msg.String() {
73 case "enter":
74 if item, ok := cl.list.SelectedItem().(CollectionListItem); ok {
75 return cl, func() tea.Msg {
76 return selectCollectionMsg{collection: item.Name}
77 }
78 }
79 }
80 }
81 }
82
83 var cmd tea.Cmd
84 cl.list, cmd = cl.list.Update(msg)
85 return cl, cmd
86}
87
88func (cl *CollectionList) View() string {
89 if len(cl.list.Items()) == 0 {
90 return dimStyle.Render("No collections found")
91 }
92 return cl.list.View()
93}
94
95type RepoView struct {
96 identity *identity.Identity
97 repo *comatproto.RepoDescribeRepo_Output
98 clist *CollectionList
99 header string
100 width int
101 height int
102}
103
104func NewRepoView() *RepoView {
105 return &RepoView{
106 clist: NewCollectionList([]string{}),
107 width: 80,
108 height: 24,
109 }
110}
111
112func (r *RepoView) buildHeader() string {
113 if r.repo == nil {
114 return ""
115 }
116 var s strings.Builder
117
118 s.WriteString(headerStyle.Render("📦 Repository"))
119 s.WriteString("\n\n")
120
121 s.WriteString(labelStyle.Render("Handle: "))
122 s.WriteString(valueStyle.Render(r.repo.Handle))
123 s.WriteString(" ")
124 s.WriteString(labelStyle.Render("Valid: "))
125 if r.repo.HandleIsCorrect {
126 s.WriteString(valueStyle.Render("✓"))
127 } else {
128 s.WriteString(dimStyle.Render("✗"))
129 }
130 s.WriteString("\n")
131
132 s.WriteString(labelStyle.Render("DID: "))
133 s.WriteString(dimStyle.Render(r.repo.Did))
134 s.WriteString("\n\n")
135
136 s.WriteString(labelStyle.Render("PDS: "))
137 s.WriteString(valueStyle.Render(r.identity.PDSEndpoint()))
138 s.WriteString("\n")
139
140 // Collections section header
141 s.WriteString(headerStyle.Render("Collections "))
142 s.WriteString(dimStyle.Render(fmt.Sprintf("(%d)", len(r.repo.Collections))))
143 s.WriteString("\n")
144
145 // add bottom border
146 return lipgloss.NewStyle().BorderBottom(true).Render(s.String())
147
148}
149
150func (r *RepoView) SetRepo(repo *at.RepoWithIdentity) tea.Cmd {
151 r.identity = repo.Identity
152 r.repo = repo.Repo
153 r.header = r.buildHeader()
154 r.clist = NewCollectionList(repo.Repo.Collections)
155 return r.clist.Init()
156}
157
158func (r *RepoView) Init() tea.Cmd {
159 return nil
160}
161
162func (r *RepoView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
163 clist, cmd := r.clist.Update(msg)
164 r.clist = clist.(*CollectionList)
165 return r, cmd
166}
167
168// updateListSize calculates and sets the list size to fill remaining space
169func (r *RepoView) SetSize(w, h int) {
170 r.width = w
171 r.height = h
172 if r.clist == nil {
173 return
174 }
175 headerHeight := lipgloss.Height(r.header)
176
177 // List gets all remaining space
178 listHeight := r.height - headerHeight
179 if listHeight < 5 {
180 listHeight = 5
181 }
182
183 r.clist.list.SetSize(r.width, listHeight)
184}
185
186func (r *RepoView) View() string {
187 if r.repo == nil {
188 return "No repository loaded"
189 }
190 return lipgloss.JoinVertical(lipgloss.Left, r.header, r.clist.View())
191}