cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 🍃
charm
leaflet
readability
golang
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}