cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 273 lines 6.7 kB view raw
1// TODO: Use glamour to render the markdown produced by [formatTaskForView] 2// TODO: remove the ID from the table 3package ui 4 5import ( 6 "context" 7 "fmt" 8 "io" 9 "strings" 10 "time" 11 12 tea "github.com/charmbracelet/bubbletea" 13 "github.com/stormlightlabs/noteleaf/internal/models" 14 "github.com/stormlightlabs/noteleaf/internal/repo" 15 "github.com/stormlightlabs/noteleaf/internal/utils" 16) 17 18// TaskRecord adapts models.Task to work with DataTable 19type TaskRecord struct { 20 *models.Task 21} 22 23func (t *TaskRecord) GetField(name string) any { 24 switch name { 25 case "id": 26 return t.ID 27 case "uuid": 28 return t.UUID 29 case "description": 30 return t.Description 31 case "status": 32 return t.Status 33 case "priority": 34 return t.Priority 35 case "project": 36 return t.Project 37 case "tags": 38 return t.Tags 39 case "due": 40 return t.Due 41 case "entry": 42 return t.Entry 43 case "start": 44 return t.Start 45 case "end": 46 return t.End 47 case "modified": 48 return t.Modified 49 case "annotations": 50 return t.Annotations 51 default: 52 return "" 53 } 54} 55 56// TaskDataSource adapts TaskRepository to work with DataTable 57type TaskDataSource struct { 58 repo utils.TestTaskRepository 59 showAll bool 60 status string 61 priority string 62 project string 63} 64 65func (t *TaskDataSource) Load(ctx context.Context, opts DataOptions) ([]DataRecord, error) { 66 repoOpts := repo.TaskListOptions{ 67 SortBy: "modified", 68 SortOrder: "DESC", 69 Limit: 50, 70 } 71 72 if !t.showAll && t.status == "" { 73 repoOpts.Status = "pending" 74 } 75 if t.status != "" { 76 repoOpts.Status = t.status 77 } 78 if t.priority != "" { 79 repoOpts.Priority = t.priority 80 } 81 if t.project != "" { 82 repoOpts.Project = t.project 83 } 84 85 tasks, err := t.repo.List(ctx, repoOpts) 86 if err != nil { 87 return nil, err 88 } 89 90 records := make([]DataRecord, len(tasks)) 91 for i, task := range tasks { 92 records[i] = &TaskRecord{Task: task} 93 } 94 95 return records, nil 96} 97 98func (t *TaskDataSource) Count(ctx context.Context, opts DataOptions) (int, error) { 99 records, err := t.Load(ctx, opts) 100 if err != nil { 101 return 0, err 102 } 103 return len(records), nil 104} 105 106func formatTaskForView(task *models.Task) string { 107 var content strings.Builder 108 content.WriteString(fmt.Sprintf("# Task %d\n\n", task.ID)) 109 content.WriteString(fmt.Sprintf("**UUID:** %s\n", task.UUID)) 110 content.WriteString(fmt.Sprintf("**Description:** %s\n", task.Description)) 111 content.WriteString(fmt.Sprintf("**Status:** %s\n", task.Status)) 112 113 if task.Priority != "" { 114 content.WriteString(fmt.Sprintf("**Priority:** %s\n", task.Priority)) 115 } 116 117 if task.Project != "" { 118 content.WriteString(fmt.Sprintf("**Project:** %s\n", task.Project)) 119 } 120 121 if len(task.Tags) > 0 { 122 content.WriteString(fmt.Sprintf("**Tags:** %s\n", strings.Join(task.Tags, ", "))) 123 } 124 125 if task.Due != nil { 126 content.WriteString(fmt.Sprintf("**Due:** %s\n", task.Due.Format("2006-01-02 15:04"))) 127 } 128 129 content.WriteString(fmt.Sprintf("**Created:** %s\n", task.Entry.Format("2006-01-02 15:04"))) 130 content.WriteString(fmt.Sprintf("**Modified:** %s\n", task.Modified.Format("2006-01-02 15:04"))) 131 132 if task.Start != nil { 133 content.WriteString(fmt.Sprintf("**Started:** %s\n", task.Start.Format("2006-01-02 15:04"))) 134 } 135 136 if task.End != nil { 137 content.WriteString(fmt.Sprintf("**Completed:** %s\n", task.End.Format("2006-01-02 15:04"))) 138 } 139 140 if len(task.Annotations) > 0 { 141 content.WriteString("\n**Annotations:**\n") 142 for _, annotation := range task.Annotations { 143 content.WriteString(fmt.Sprintf("- %s\n", annotation)) 144 } 145 } 146 147 return content.String() 148} 149 150func formatPriorityField(priority string) string { 151 if priority == "" { 152 return "-" 153 } 154 155 titlecase := utils.Titlecase(priority) 156 padded := fmt.Sprintf("%-10s", titlecase) 157 158 switch strings.ToLower(priority) { 159 case "high", "urgent": 160 return PriorityHigh.Render(padded) 161 case "medium": 162 return PriorityMedium.Render(padded) 163 case "low": 164 return PriorityLow.Render(padded) 165 default: 166 return padded 167 } 168} 169 170// NewTaskDataTable creates a new DataTable for browsing tasks 171func NewTaskDataTable(repo utils.TestTaskRepository, opts DataTableOptions, showAll bool, status, priority, project string) *DataTable { 172 if opts.Title == "" { 173 title := "Tasks" 174 if showAll { 175 title += " (showing all)" 176 } else { 177 title += " (pending only)" 178 } 179 opts.Title = title 180 } 181 182 opts.Fields = []Field{ 183 {Name: "id", Title: "ID", Width: 4}, 184 {Name: "description", Title: "Description", Width: 40, 185 Formatter: func(v any) string { 186 desc := fmt.Sprintf("%v", v) 187 if len(desc) > 38 { 188 return desc[:35] + "..." 189 } 190 return desc 191 }}, 192 {Name: "status", Title: "Status", Width: 10, 193 Formatter: func(v any) string { 194 status := fmt.Sprintf("%v", v) 195 if len(status) > 8 { 196 return status[:8] 197 } 198 return status 199 }}, 200 {Name: "priority", Title: "Priority", Width: 10, 201 Formatter: func(v any) string { 202 priority := fmt.Sprintf("%v", v) 203 return formatPriorityField(priority) 204 }}, 205 {Name: "project", Title: "Project", Width: 15, 206 Formatter: func(v any) string { 207 project := fmt.Sprintf("%v", v) 208 if project == "" { 209 return "-" 210 } 211 if len(project) > 13 { 212 return project[:10] + "..." 213 } 214 return project 215 }}, 216 } 217 218 if opts.ViewHandler == nil { 219 opts.ViewHandler = func(record DataRecord) string { 220 if taskRecord, ok := record.(*TaskRecord); ok { 221 return formatTaskForView(taskRecord.Task) 222 } 223 return "Unable to display task" 224 } 225 } 226 227 if len(opts.Actions) == 0 { 228 opts.Actions = []Action{ 229 { 230 Key: "d", 231 Description: "mark done", 232 Handler: func(record DataRecord) tea.Cmd { 233 return func() tea.Msg { 234 if taskRecord, ok := record.(*TaskRecord); ok { 235 if taskRecord.Status == "completed" { 236 return dataErrorMsg(fmt.Errorf("task already completed")) 237 } 238 taskRecord.Status = "completed" 239 taskRecord.End = &time.Time{} 240 *taskRecord.End = time.Now() 241 err := repo.Update(context.Background(), taskRecord.Task) 242 if err != nil { 243 return dataErrorMsg(err) 244 } 245 return dataLoadedMsg([]DataRecord{}) 246 } 247 return dataErrorMsg(fmt.Errorf("invalid task record")) 248 } 249 }, 250 }, 251 } 252 } 253 254 source := &TaskDataSource{ 255 repo: repo, 256 showAll: showAll, 257 status: status, 258 priority: priority, 259 project: project, 260 } 261 262 return NewDataTable(source, opts) 263} 264 265// NewTaskListFromTable creates a TaskList-compatible interface using DataTable 266func NewTaskListFromTable(repo utils.TestTaskRepository, output io.Writer, input io.Reader, static bool, showAll bool, status, priority, project string) *DataTable { 267 opts := DataTableOptions{ 268 Output: output, 269 Input: input, 270 Static: static, 271 } 272 return NewTaskDataTable(repo, opts, showAll, status, priority, project) 273}