cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 🍃
charm leaflet readability golang
at main 232 lines 5.7 kB view raw
1package ui 2 3import ( 4 "context" 5 "fmt" 6 "io" 7 "strings" 8 9 "github.com/charmbracelet/glamour" 10 "github.com/stormlightlabs/noteleaf/internal/models" 11 "github.com/stormlightlabs/noteleaf/internal/utils" 12) 13 14// PublicationRecord adapts models.Note with leaflet metadata to work with DataList 15type PublicationRecord struct { 16 *models.Note 17} 18 19func (p *PublicationRecord) GetField(name string) any { 20 switch name { 21 case "id": 22 return p.ID 23 case "title": 24 return p.Title 25 case "status": 26 if p.IsDraft { 27 return "draft" 28 } 29 return "published" 30 case "published_at": 31 return p.PublishedAt 32 case "modified": 33 return p.Modified 34 case "leaflet_rkey": 35 return p.LeafletRKey 36 case "leaflet_cid": 37 return p.LeafletCID 38 default: 39 return "" 40 } 41} 42 43func (p *PublicationRecord) GetTitle() string { 44 status := "draft" 45 if !p.IsDraft { 46 status = "published" 47 } 48 return fmt.Sprintf("[%d] %s (%s)", p.ID, p.Title, status) 49} 50 51func (p *PublicationRecord) GetDescription() string { 52 var parts []string 53 54 if p.PublishedAt != nil { 55 parts = append(parts, "Published: "+p.PublishedAt.Format("2006-01-02 15:04")) 56 } 57 58 parts = append(parts, "Modified: "+p.Modified.Format("2006-01-02 15:04")) 59 60 if p.LeafletRKey != nil { 61 parts = append(parts, "rkey: "+*p.LeafletRKey) 62 } 63 64 return strings.Join(parts, " • ") 65} 66 67func (p *PublicationRecord) GetFilterValue() string { 68 searchable := []string{p.Title, p.Content} 69 if p.LeafletRKey != nil { 70 searchable = append(searchable, *p.LeafletRKey) 71 } 72 return strings.Join(searchable, " ") 73} 74 75// PublicationDataSource loads notes with leaflet metadata 76type PublicationDataSource struct { 77 repo utils.TestNoteRepository 78 filter string // "all", "published", or "draft" 79} 80 81func (p *PublicationDataSource) Load(ctx context.Context, opts ListOptions) ([]ListItem, error) { 82 var notes []*models.Note 83 var err error 84 85 switch p.filter { 86 case "published": 87 notes, err = p.repo.ListPublished(ctx) 88 case "draft": 89 notes, err = p.repo.ListDrafts(ctx) 90 default: 91 notes, err = p.repo.GetLeafletNotes(ctx) 92 } 93 94 if err != nil { 95 return nil, err 96 } 97 98 if opts.Search != "" { 99 var filtered []*models.Note 100 searchLower := strings.ToLower(opts.Search) 101 for _, note := range notes { 102 if strings.Contains(strings.ToLower(note.Title), searchLower) || 103 strings.Contains(strings.ToLower(note.Content), searchLower) || 104 (note.LeafletRKey != nil && strings.Contains(strings.ToLower(*note.LeafletRKey), searchLower)) { 105 filtered = append(filtered, note) 106 } 107 } 108 notes = filtered 109 } 110 111 if opts.Limit > 0 && opts.Limit < len(notes) { 112 notes = notes[:opts.Limit] 113 } 114 115 items := make([]ListItem, len(notes)) 116 for i, note := range notes { 117 items[i] = &PublicationRecord{Note: note} 118 } 119 120 return items, nil 121} 122 123func (p *PublicationDataSource) Count(ctx context.Context, opts ListOptions) (int, error) { 124 items, err := p.Load(ctx, opts) 125 if err != nil { 126 return 0, err 127 } 128 return len(items), nil 129} 130 131func (p *PublicationDataSource) Search(ctx context.Context, query string, opts ListOptions) ([]ListItem, error) { 132 opts.Search = query 133 return p.Load(ctx, opts) 134} 135 136// NewPublicationDataList creates a new DataList for browsing published/draft documents 137func NewPublicationDataList(repo utils.TestNoteRepository, opts DataListOptions, filter string) *DataList { 138 if opts.Title == "" { 139 opts.Title = "Publications" 140 } 141 142 opts.ShowSearch = true 143 opts.Searchable = true 144 145 if opts.ViewHandler == nil { 146 opts.ViewHandler = func(item ListItem) string { 147 if pubRecord, ok := item.(*PublicationRecord); ok { 148 return formatPublicationForView(pubRecord.Note) 149 } 150 return "Unable to display publication" 151 } 152 } 153 154 source := &PublicationDataSource{ 155 repo: repo, 156 filter: filter, 157 } 158 159 return NewDataList(source, opts) 160} 161 162// NewPublicationListFromList creates a publication list using DataList 163func NewPublicationListFromList(repo utils.TestNoteRepository, output io.Writer, input io.Reader, static bool, filter string) *DataList { 164 opts := DataListOptions{ 165 Output: output, 166 Input: input, 167 Static: static, 168 Title: "Publications", 169 } 170 return NewPublicationDataList(repo, opts, filter) 171} 172 173// buildPublicationMarkdown builds markdown content for a publication without rendering 174func buildPublicationMarkdown(note *models.Note) string { 175 var content strings.Builder 176 177 content.WriteString("# " + note.Title + "\n\n") 178 179 status := "published" 180 if note.IsDraft { 181 status = "draft" 182 } 183 content.WriteString("- **Status:** " + status + "\n") 184 185 if note.PublishedAt != nil { 186 content.WriteString("- **Published:** " + note.PublishedAt.Format("2006-01-02 15:04") + "\n") 187 } 188 189 content.WriteString("- **Modified:** " + note.Modified.Format("2006-01-02 15:04") + "\n") 190 191 if note.LeafletRKey != nil { 192 content.WriteString("- **RKey:** `" + ObfuscateMiddle(*note.LeafletRKey, 3, 3) + "`\n") 193 } 194 195 if note.LeafletCID != nil { 196 content.WriteString("- **CID:** `" + ObfuscateMiddle(*note.LeafletCID, 3, 3) + "`\n") 197 } 198 199 content.WriteString("\n---\n\n") 200 201 noteContent := strings.TrimSpace(note.Content) 202 if !strings.HasPrefix(noteContent, "# ") { 203 content.WriteString(noteContent) 204 } else { 205 lines := strings.Split(noteContent, "\n") 206 if len(lines) > 1 { 207 content.WriteString(strings.Join(lines[1:], "\n")) 208 } 209 } 210 211 return content.String() 212} 213 214// formatPublicationForView formats a publication for display with glamour 215func formatPublicationForView(note *models.Note) string { 216 markdown := buildPublicationMarkdown(note) 217 218 renderer, err := glamour.NewTermRenderer( 219 glamour.WithStandardStyle("tokyo-night"), 220 glamour.WithWordWrap(80), 221 ) 222 if err != nil { 223 return markdown 224 } 225 226 rendered, err := renderer.Render(markdown) 227 if err != nil { 228 return markdown 229 } 230 231 return rendered 232}