cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 🍃
charm leaflet readability golang
at main 199 lines 4.6 kB view raw
1package ui 2 3import ( 4 "context" 5 "io" 6 "strings" 7 8 "github.com/charmbracelet/glamour" 9 "github.com/stormlightlabs/noteleaf/internal/models" 10 "github.com/stormlightlabs/noteleaf/internal/repo" 11 "github.com/stormlightlabs/noteleaf/internal/utils" 12) 13 14// NoteRecord adapts models.Note to work with DataList (since notes work better as a list than table) 15type NoteRecord struct { 16 *models.Note 17} 18 19func (n *NoteRecord) GetField(name string) any { 20 switch name { 21 case "id": 22 return n.ID 23 case "title": 24 return n.Title 25 case "content": 26 return n.Content 27 case "tags": 28 return n.Tags 29 case "archived": 30 return n.Archived 31 case "created": 32 return n.Created 33 case "modified": 34 return n.Modified 35 case "file_path": 36 return n.FilePath 37 default: 38 return "" 39 } 40} 41 42func (n *NoteRecord) GetTitle() string { 43 return n.Title 44} 45 46func (n *NoteRecord) GetDescription() string { 47 var parts []string 48 49 if len(n.Tags) > 0 { 50 parts = append(parts, strings.Join(n.Tags, ", ")) 51 } 52 53 parts = append(parts, "Modified: "+n.Modified.Format("2006-01-02 15:04")) 54 55 return strings.Join(parts, " • ") 56} 57 58func (n *NoteRecord) GetFilterValue() string { 59 // Make notes searchable by title, content, and tags 60 searchable := []string{n.Title, n.Content} 61 searchable = append(searchable, n.Tags...) 62 return strings.Join(searchable, " ") 63} 64 65// NoteDataSource adapts NoteRepository to work with DataList 66type NoteDataSource struct { 67 repo utils.TestNoteRepository 68 showArchived bool 69 tags []string 70} 71 72func (n *NoteDataSource) Load(ctx context.Context, opts ListOptions) ([]ListItem, error) { 73 repoOpts := repo.NoteListOptions{ 74 Tags: n.tags, 75 } 76 77 if !n.showArchived { 78 archived := false 79 repoOpts.Archived = &archived 80 } 81 82 if opts.Search != "" { 83 repoOpts.Content = opts.Search 84 } 85 86 if opts.Limit > 0 { 87 repoOpts.Limit = opts.Limit 88 } 89 90 notes, err := n.repo.List(ctx, repoOpts) 91 if err != nil { 92 return nil, err 93 } 94 95 items := make([]ListItem, len(notes)) 96 for i, note := range notes { 97 items[i] = &NoteRecord{Note: note} 98 } 99 100 return items, nil 101} 102 103func (n *NoteDataSource) Count(ctx context.Context, opts ListOptions) (int, error) { 104 items, err := n.Load(ctx, opts) 105 if err != nil { 106 return 0, err 107 } 108 return len(items), nil 109} 110 111func (n *NoteDataSource) Search(ctx context.Context, query string, opts ListOptions) ([]ListItem, error) { 112 opts.Search = query 113 return n.Load(ctx, opts) 114} 115 116// NewNoteDataList creates a new DataList for browsing notes 117func NewNoteDataList(repo utils.TestNoteRepository, opts DataListOptions, showArchived bool, tags []string) *DataList { 118 if opts.Title == "" { 119 opts.Title = "Notes" 120 } 121 122 opts.ShowSearch = true 123 opts.Searchable = true 124 125 if opts.ViewHandler == nil { 126 opts.ViewHandler = func(item ListItem) string { 127 if noteRecord, ok := item.(*NoteRecord); ok { 128 return formatNoteForView(noteRecord.Note) 129 } 130 return "Unable to display note" 131 } 132 } 133 134 source := &NoteDataSource{ 135 repo: repo, 136 showArchived: showArchived, 137 tags: tags, 138 } 139 140 return NewDataList(source, opts) 141} 142 143// NewNoteListFromList creates a NoteList-compatible interface using DataList 144func NewNoteListFromList(repo utils.TestNoteRepository, output io.Writer, input io.Reader, static bool, showArchived bool, tags []string) *DataList { 145 opts := DataListOptions{ 146 Output: output, 147 Input: input, 148 Static: static, 149 Title: "Notes", 150 } 151 return NewNoteDataList(repo, opts, showArchived, tags) 152} 153 154// formatNoteForView formats a note for display (similar to original implementation) 155func formatNoteForView(note *models.Note) string { 156 var content strings.Builder 157 158 content.WriteString("# " + note.Title + "\n\n") 159 160 if len(note.Tags) > 0 { 161 content.WriteString("**Tags:** ") 162 for i, tag := range note.Tags { 163 if i > 0 { 164 content.WriteString(", ") 165 } 166 content.WriteString("`" + tag + "`") 167 } 168 content.WriteString("\n\n") 169 } 170 171 content.WriteString("**Created:** " + note.Created.Format("2006-01-02 15:04") + "\n") 172 content.WriteString("**Modified:** " + note.Modified.Format("2006-01-02 15:04") + "\n\n") 173 content.WriteString("---\n\n") 174 175 noteContent := strings.TrimSpace(note.Content) 176 if !strings.HasPrefix(noteContent, "# ") { 177 content.WriteString(noteContent) 178 } else { 179 lines := strings.Split(noteContent, "\n") 180 if len(lines) > 1 { 181 content.WriteString(strings.Join(lines[1:], "\n")) 182 } 183 } 184 185 renderer, err := glamour.NewTermRenderer( 186 glamour.WithAutoStyle(), 187 glamour.WithWordWrap(80), 188 ) 189 if err != nil { 190 return content.String() 191 } 192 193 rendered, err := renderer.Render(content.String()) 194 if err != nil { 195 return content.String() 196 } 197 198 return rendered 199}