AT Protocol Terminal Interface Explorer

initial work for full record view

+120 -64
+31 -16
ui/app.go
··· 16 16 ) 17 17 18 18 type App struct { 19 - client *at.Client 20 - search *CommandPallete 21 - repoView *RepoView 22 - rlist *RecordsList 23 - active tea.Model 24 - err string 25 - w, h int 19 + client *at.Client 20 + search *CommandPallete 21 + repoView *RepoView 22 + rlist *RecordsList 23 + recordView *RecordView 24 + active tea.Model 25 + err string 26 + w, h int 26 27 } 27 28 28 29 func NewApp() *App { 29 30 search := &CommandPallete{} 30 31 repoView := NewRepoView() 31 32 return &App{ 32 - client: at.NewClient(""), 33 - search: search, 34 - repoView: repoView, 35 - rlist: NewRecordsList(nil), 36 - active: search, 33 + client: at.NewClient(""), 34 + search: search, 35 + repoView: repoView, 36 + rlist: NewRecordsList(nil), 37 + recordView: NewRecordView(false), 38 + active: search, 37 39 } 38 40 } 39 41 ··· 45 47 cmds := []tea.Cmd{} 46 48 a.search.SetSize(a.w, a.h) 47 49 a.repoView.SetSize(a.w, a.h) 48 - if a.rlist != nil { 49 - a.rlist.SetSize(a.w, a.h) 50 - } 50 + a.rlist.SetSize(a.w, a.h) 51 + a.recordView.SetSize(a.w, a.h) 51 52 return tea.Batch(cmds...) 52 53 } 53 - 54 54 55 55 func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 56 56 switch msg := msg.(type) { ··· 72 72 case a.rlist: 73 73 a.active = a.repoView 74 74 return a, nil 75 + case a.recordView: 76 + a.active = a.rlist 77 + return a, nil 75 78 } 76 79 } 77 80 ··· 89 92 90 93 case repoLoadedMsg: 91 94 cmd := a.repoView.SetRepo(msg.repo) 95 + a.repoView.SetSize(a.w, a.h) // Set size before switching view 92 96 a.active = a.repoView 93 97 a.search.loading = false 94 98 return a, cmd ··· 99 103 100 104 case recordsLoadedMsg: 101 105 cmd := a.rlist.SetRecords(msg.records) 106 + a.rlist.SetSize(a.w, a.h) // Set size before switching view 102 107 a.active = a.rlist 103 108 a.search.loading = false 104 109 return a, cmd 110 + 111 + case recordSelectedMsg: 112 + a.recordView.SetRecord(msg.record) 113 + a.recordView.SetSize(a.w, a.h) // Set size before switching view 114 + a.active = a.recordView 115 + return a, nil 105 116 106 117 case repoErrorMsg: 107 118 a.search.err = msg.err.Error() ··· 163 174 164 175 type recordsLoadedMsg struct { 165 176 records []*agnostic.RepoListRecords_Record 177 + } 178 + 179 + type recordSelectedMsg struct { 180 + record *agnostic.RepoListRecords_Record 166 181 } 167 182 168 183 type repoErrorMsg struct {
+75
ui/record.go
··· 1 + package ui 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + 7 + "github.com/bluesky-social/indigo/api/agnostic" 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "github.com/charmbracelet/bubbles/viewport" 10 + tea "github.com/charmbracelet/bubbletea" 11 + "github.com/charmbracelet/lipgloss" 12 + ) 13 + 14 + type RecordView struct { 15 + record *agnostic.RepoListRecords_Record 16 + vp viewport.Model 17 + header string 18 + preview bool 19 + } 20 + 21 + func NewRecordView(preview bool) *RecordView { 22 + vp := viewport.New(80, 20) 23 + return &RecordView{ 24 + vp: vp, 25 + preview: preview, 26 + } 27 + } 28 + 29 + func (rv *RecordView) SetSize(w, h int) { 30 + rv.vp.Width = w 31 + rv.vp.Height = h - lipgloss.Height(rv.header) 32 + } 33 + 34 + func (rv *RecordView) buildHeader() string { 35 + if rv.record == nil { 36 + return "" 37 + } 38 + uri, err := syntax.ParseATURI(rv.record.Uri) 39 + if err != nil { 40 + return headerStyle.Render(rv.record.Uri) 41 + } 42 + header := rv.record.Uri 43 + if rv.preview { 44 + header = fmt.Sprintf("%s/%s", uri.Collection(), uri.RecordKey().String()) 45 + } 46 + return headerStyle.Render(header) 47 + } 48 + 49 + func (rv *RecordView) SetRecord(record *agnostic.RepoListRecords_Record) { 50 + rv.record = record 51 + if rv.record == nil || rv.record.Value == nil { 52 + rv.vp.SetContent("") 53 + return 54 + } 55 + data, err := json.MarshalIndent(rv.record.Value, "", " ") 56 + if err != nil { 57 + data = fmt.Appendf([]byte{}, "error marshaling record: %v", err) 58 + } 59 + rv.vp.SetContent(string(data)) 60 + rv.header = rv.buildHeader() 61 + } 62 + 63 + func (rv *RecordView) Init() tea.Cmd { 64 + return rv.vp.Init() 65 + } 66 + 67 + func (rv *RecordView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 68 + var cmd tea.Cmd 69 + rv.vp, cmd = rv.vp.Update(msg) 70 + return rv, cmd 71 + } 72 + 73 + func (rv *RecordView) View() string { 74 + return lipgloss.JoinVertical(lipgloss.Left, rv.header, rv.vp.View()) 75 + }
+14 -48
ui/records.go ui/collection.go
··· 1 1 package ui 2 2 3 3 import ( 4 - "encoding/json" 5 4 "fmt" 6 5 "strings" 7 6 8 7 "github.com/bluesky-social/indigo/api/agnostic" 9 8 "github.com/bluesky-social/indigo/atproto/syntax" 10 9 "github.com/charmbracelet/bubbles/list" 11 - "github.com/charmbracelet/bubbles/viewport" 12 10 tea "github.com/charmbracelet/bubbletea" 13 11 "github.com/charmbracelet/lipgloss" 14 12 ) 15 13 16 - type RecordView struct { 17 - record *agnostic.RepoListRecords_Record 18 - vp viewport.Model 19 - } 20 - 21 - func NewRecordView() *RecordView { 22 - vp := viewport.New(80, 20) 23 - return &RecordView{ 24 - vp: vp, 25 - } 26 - } 27 - 28 - func (rv *RecordView) SetSize(w, h int) { 29 - rv.vp.Width = w 30 - rv.vp.Height = h 31 - } 32 - 33 - func (rv *RecordView) SetRecord(record *agnostic.RepoListRecords_Record) { 34 - rv.record = record 35 - if rv.record == nil || rv.record.Value == nil { 36 - rv.vp.SetContent("") 37 - return 38 - } 39 - data, err := json.MarshalIndent(rv.record.Value, "", " ") 40 - if err != nil { 41 - data = fmt.Appendf([]byte{}, "error marshaling record: %v", err) 42 - } 43 - rv.vp.SetContent(string(data)) 44 - } 45 - 46 - func (rv *RecordView) Init() tea.Cmd { 47 - return nil 48 - } 49 - 50 - func (rv *RecordView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 51 - var cmd tea.Cmd 52 - rv.vp, cmd = rv.vp.Update(msg) 53 - return rv, cmd 54 - } 55 - 56 - func (rv *RecordView) View() string { 57 - return rv.vp.View() 58 - } 59 - 60 14 type RecordsList struct { 61 15 rlist list.Model 62 - preview RecordView 16 + preview *RecordView 63 17 header string 64 18 w, h int 65 19 } ··· 108 62 l.SetFilteringEnabled(true) 109 63 rl := &RecordsList{ 110 64 rlist: l, 111 - preview: RecordView{}, 65 + preview: NewRecordView(true), 112 66 } 113 67 rl.SetRecords(records) 114 68 return rl ··· 171 125 if item, ok := rl.rlist.SelectedItem().(RecordListItem); ok { 172 126 rl.preview.SetRecord(item.r) 173 127 } 128 + switch msg := msg.(type) { 129 + case tea.KeyMsg: 130 + switch msg.String() { 131 + case "enter": 132 + if item, ok := rl.rlist.SelectedItem().(RecordListItem); ok { 133 + return rl, func() tea.Msg { 134 + return recordSelectedMsg{record: item.r} 135 + } 136 + } 137 + } 138 + } 139 + 174 140 return rl, cmd 175 141 } 176 142