cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 🍃
charm
leaflet
readability
golang
1package ui
2
3import (
4 "fmt"
5 "slices"
6 "strings"
7
8 "github.com/charmbracelet/lipgloss"
9 "github.com/stormlightlabs/noteleaf/internal/models"
10)
11
12const (
13 // U+25CF Black Circle
14 StatusTodoSymbol = "●"
15 // U+25D0 Circle with Left Half Black
16 StatusInProgressSymbol = "◐"
17 // U+25A0 Black Square
18 StatusBlockedSymbol = "■"
19 // U+2713 Check Mark
20 StatusDoneSymbol = "✓"
21 // U+26AB Medium Black Circle
22 StatusAbandonedSymbol = "⚫"
23 // U+25CB White Circle
24 StatusPendingSymbol = "○"
25 // U+2713 Check Mark
26 StatusCompletedSymbol = "✓"
27 // U+2717 Ballot X
28 StatusDeletedSymbol = "✗"
29 // U+2605 Black Star
30 PriorityHighSymbol = "★"
31 // U+2606 White Star
32 PriorityMediumSymbol = "☆"
33 // U+25E6 White Bullet
34 PriorityLowSymbol = "◦"
35 // U+25CB White Circle
36 PriorityNoneSymbol = "○"
37 // Three stars
38 PriorityHighPattern = "★★★"
39 // Two stars, one outline
40 PriorityMediumPattern = "★★☆"
41 // One star, two outline
42 PriorityLowPattern = "★☆☆"
43 // Three outline stars
44 PriorityNonePattern = "☆☆☆"
45)
46
47// Type aliases for status and priority styles (now defined in palette.go)
48var (
49 TodoStyle = StatusTodo
50 InProgressStyle = StatusInProgress
51 BlockedStyle = StatusBlocked
52 DoneStyle = StatusDone
53 AbandonedStyle = StatusAbandoned
54 PendingStyle = StatusPending
55 CompletedStyle = StatusCompleted
56 DeletedStyle = StatusDeleted
57 PriorityHighStyle = PriorityHigh
58 PriorityMediumStyle = PriorityMedium
59 PriorityLowStyle = PriorityLow
60 PriorityNoneStyle = PriorityNone
61 PriorityLegacyStyle = PriorityLegacy
62)
63
64// GetStatusSymbol returns the unicode symbol for a given status
65//
66// Default to pending
67func GetStatusSymbol(status string) string {
68 switch status {
69 case models.StatusTodo:
70 return StatusTodoSymbol
71 case models.StatusInProgress:
72 return StatusInProgressSymbol
73 case models.StatusBlocked:
74 return StatusBlockedSymbol
75 case models.StatusDone:
76 return StatusDoneSymbol
77 case models.StatusAbandoned:
78 return StatusAbandonedSymbol
79 case models.StatusPending:
80 return StatusPendingSymbol
81 case models.StatusCompleted:
82 return StatusCompletedSymbol
83 case models.StatusDeleted:
84 return StatusDeletedSymbol
85 default:
86 return StatusPendingSymbol
87 }
88}
89
90// GetStatusStyle returns the color style for a given status
91//
92// Defaults to pending style
93func GetStatusStyle(status string) lipgloss.Style {
94 switch status {
95 case models.StatusTodo:
96 return TodoStyle
97 case models.StatusInProgress:
98 return InProgressStyle
99 case models.StatusBlocked:
100 return BlockedStyle
101 case models.StatusDone:
102 return DoneStyle
103 case models.StatusAbandoned:
104 return AbandonedStyle
105 case models.StatusPending:
106 return PendingStyle
107 case models.StatusCompleted:
108 return CompletedStyle
109 case models.StatusDeleted:
110 return DeletedStyle
111 default:
112 return PendingStyle
113 }
114}
115
116// FormatStatusIndicator returns a styled status symbol and text
117func FormatStatusIndicator(status string) string {
118 symbol := GetStatusSymbol(status)
119 style := GetStatusStyle(status)
120 return style.Render(symbol)
121}
122
123// FormatStatusWithText returns a styled status symbol with status text
124func FormatStatusWithText(status string) string {
125 symbol := GetStatusSymbol(status)
126 style := GetStatusStyle(status)
127 return style.Render(fmt.Sprintf("%s %s", symbol, status))
128}
129
130// GetStatusDescription returns a human-friendly description of the status
131func GetStatusDescription(status string) string {
132 switch status {
133 case models.StatusTodo:
134 return "Ready to start"
135 case models.StatusInProgress:
136 return "Currently working"
137 case models.StatusBlocked:
138 return "Waiting on dependency"
139 case models.StatusDone:
140 return "Completed successfully"
141 case models.StatusAbandoned:
142 return "No longer relevant"
143 case models.StatusPending:
144 return "Pending (legacy)"
145 case models.StatusCompleted:
146 return "Completed (legacy)"
147 case models.StatusDeleted:
148 return "Deleted (legacy)"
149 default:
150 return "Unknown status"
151 }
152}
153
154// FormatTaskStatus returns a complete status display with symbol, status, and description
155func FormatTaskStatus(task *models.Task) string {
156 if task == nil {
157 return ""
158 }
159
160 symbol := GetStatusSymbol(task.Status)
161 style := GetStatusStyle(task.Status)
162 description := GetStatusDescription(task.Status)
163
164 return fmt.Sprintf("%s %s - %s", style.Render(symbol), style.Render(task.Status), description)
165}
166
167// StatusLegend returns a formatted legend showing all status symbols
168func StatusLegend() string {
169 var parts []string
170
171 statuses := []string{
172 models.StatusTodo,
173 models.StatusInProgress,
174 models.StatusBlocked,
175 models.StatusDone,
176 models.StatusAbandoned,
177 }
178
179 for _, status := range statuses {
180 parts = append(parts, FormatStatusWithText(status))
181 }
182
183 return strings.Join(parts, " ")
184}
185
186// GetAllStatusSymbols returns a map of all status symbols for reference
187func GetAllStatusSymbols() map[string]string {
188 return map[string]string{
189 models.StatusTodo: StatusTodoSymbol,
190 models.StatusInProgress: StatusInProgressSymbol,
191 models.StatusBlocked: StatusBlockedSymbol,
192 models.StatusDone: StatusDoneSymbol,
193 models.StatusAbandoned: StatusAbandonedSymbol,
194 models.StatusPending: StatusPendingSymbol,
195 models.StatusCompleted: StatusCompletedSymbol,
196 models.StatusDeleted: StatusDeletedSymbol,
197 }
198}
199
200// IsValidStatusTransition checks if a status transition is logically valid
201//
202// From todo, can go to in-progress, blocked, done, or abandoned
203// From in-progress, can go to blocked, done, abandoned, or back to todo
204// From blocked, can go to todo, in-progress, done, or abandoned
205// From done, can only be reopened to todo or in-progress
206// From abandoned, can be reopened to todo or in-progress
207func IsValidStatusTransition(from, to string) bool {
208 if from == models.StatusTodo {
209 validTo := []string{models.StatusInProgress, models.StatusBlocked, models.StatusDone, models.StatusAbandoned}
210 return slices.Contains(validTo, to)
211 }
212
213 if from == models.StatusInProgress {
214 validTo := []string{models.StatusTodo, models.StatusBlocked, models.StatusDone, models.StatusAbandoned}
215 return slices.Contains(validTo, to)
216 }
217
218 if from == models.StatusBlocked {
219 validTo := []string{models.StatusTodo, models.StatusInProgress, models.StatusDone, models.StatusAbandoned}
220 return slices.Contains(validTo, to)
221 }
222
223 if from == models.StatusDone {
224 validTo := []string{models.StatusTodo, models.StatusInProgress}
225 return slices.Contains(validTo, to)
226 }
227
228 if from == models.StatusAbandoned {
229 validTo := []string{models.StatusTodo, models.StatusInProgress}
230 return slices.Contains(validTo, to)
231 }
232
233 if from == models.StatusPending {
234 return to == models.StatusTodo || to == models.StatusInProgress
235 }
236
237 if from == models.StatusCompleted {
238 return to == models.StatusDone
239 }
240
241 return false
242}
243
244// GetPrioritySymbol returns the unicode symbol for a given priority
245func GetPrioritySymbol(priority string) string {
246 switch priority {
247 case models.PriorityHigh:
248 return PriorityHighSymbol
249 case models.PriorityMedium:
250 return PriorityMediumSymbol
251 case models.PriorityLow:
252 return PriorityLowSymbol
253 case "":
254 return PriorityNoneSymbol
255 default:
256 if len(priority) == 1 && priority >= "A" && priority <= "Z" {
257 return PriorityHighSymbol
258 }
259 switch priority {
260 case "1":
261 return PriorityLowSymbol
262 case "2", "3":
263 return PriorityMediumSymbol
264 case "4", "5":
265 return PriorityHighSymbol
266 default:
267 return PriorityNoneSymbol
268 }
269 }
270}
271
272// GetPriorityPattern returns the star pattern for a given priority
273func GetPriorityPattern(priority string) string {
274 switch priority {
275 case models.PriorityHigh:
276 return PriorityHighPattern
277 case models.PriorityMedium:
278 return PriorityMediumPattern
279 case models.PriorityLow:
280 return PriorityLowPattern
281 case "":
282 return PriorityNonePattern
283 default:
284 if len(priority) == 1 && priority >= "A" && priority <= "Z" {
285 if priority <= "C" {
286 return PriorityHighPattern
287 } else if priority <= "M" {
288 return PriorityMediumPattern
289 } else {
290 return PriorityLowPattern
291 }
292 }
293 switch priority {
294 case "1":
295 return PriorityLowPattern
296 case "2", "3":
297 return PriorityMediumPattern
298 case "4", "5":
299 return PriorityHighPattern
300 default:
301 return PriorityNonePattern
302 }
303 }
304}
305
306// GetPriorityStyle returns the color style for a given priority
307func GetPriorityStyle(priority string) lipgloss.Style {
308 switch priority {
309 case models.PriorityHigh:
310 return PriorityHighStyle
311 case models.PriorityMedium:
312 return PriorityMediumStyle
313 case models.PriorityLow:
314 return PriorityLowStyle
315 case "":
316 return PriorityNoneStyle
317 default:
318 if len(priority) == 1 && priority >= "A" && priority <= "Z" {
319 return PriorityLegacyStyle
320 }
321 switch priority {
322 case "1", "2", "3", "4", "5":
323 return PriorityLegacyStyle
324 default:
325 return PriorityNoneStyle
326 }
327 }
328}
329
330// FormatPriorityIndicator returns a styled priority pattern
331func FormatPriorityIndicator(priority string) string {
332 pattern := GetPriorityPattern(priority)
333 style := GetPriorityStyle(priority)
334 return style.Render(pattern)
335}
336
337// FormatPriorityWithText returns a styled priority with text description
338func FormatPriorityWithText(priority string) string {
339 pattern := GetPriorityPattern(priority)
340 style := GetPriorityStyle(priority)
341
342 if priority == "" {
343 return style.Render(fmt.Sprintf("%s None", pattern))
344 }
345
346 return style.Render(fmt.Sprintf("%s %s", pattern, priority))
347}
348
349// GetPriorityDescription returns a human-friendly description of the priority
350func GetPriorityDescription(priority string) string {
351 switch priority {
352 case models.PriorityHigh:
353 return "Urgent - do first"
354 case models.PriorityMedium:
355 return "Important - schedule soon"
356 case models.PriorityLow:
357 return "Nice to have - when time permits"
358 case "":
359 return "No priority set"
360 default:
361 if len(priority) == 1 && priority >= "A" && priority <= "Z" {
362 return fmt.Sprintf("Priority %s (legacy)", priority)
363 }
364 switch priority {
365 case "1":
366 return "Priority 1 (lowest)"
367 case "2":
368 return "Priority 2 (low)"
369 case "3":
370 return "Priority 3 (medium)"
371 case "4":
372 return "Priority 4 (high)"
373 case "5":
374 return "Priority 5 (highest)"
375 default:
376 return "Unknown priority"
377 }
378 }
379}
380
381// FormatTaskPriority returns a complete priority display with pattern, priority, and description
382func FormatTaskPriority(task *models.Task) string {
383 if task == nil {
384 return ""
385 }
386
387 pattern := GetPriorityPattern(task.Priority)
388 style := GetPriorityStyle(task.Priority)
389 description := GetPriorityDescription(task.Priority)
390
391 if task.Priority == "" {
392 return fmt.Sprintf("%s %s", style.Render(pattern), description)
393 }
394
395 return fmt.Sprintf("%s %s - %s", style.Render(pattern), style.Render(task.Priority), description)
396}
397
398// PriorityLegend returns a formatted legend showing all priority patterns
399func PriorityLegend() string {
400 var parts []string
401
402 priorities := []string{
403 models.PriorityHigh, models.PriorityMedium, models.PriorityLow, "",
404 }
405
406 for _, priority := range priorities {
407 parts = append(parts, FormatPriorityWithText(priority))
408 }
409
410 return strings.Join(parts, " ")
411}
412
413// GetAllPrioritySymbols returns a map of all priority symbols for reference
414func GetAllPrioritySymbols() map[string]string {
415 return map[string]string{
416 models.PriorityHigh: PriorityHighSymbol,
417 models.PriorityMedium: PriorityMediumSymbol,
418 models.PriorityLow: PriorityLowSymbol,
419 "": PriorityNoneSymbol,
420 }
421}
422
423// GetAllPriorityPatterns returns a map of all priority patterns for reference
424func GetAllPriorityPatterns() map[string]string {
425 return map[string]string{
426 models.PriorityHigh: PriorityHighPattern,
427 models.PriorityMedium: PriorityMediumPattern,
428 models.PriorityLow: PriorityLowPattern,
429 "": PriorityNonePattern,
430 }
431}
432
433// GetPriorityDisplayType returns the display type for a priority (text, numeric, or legacy)
434func GetPriorityDisplayType(priority string) string {
435 switch priority {
436 case models.PriorityHigh, models.PriorityMedium, models.PriorityLow:
437 return "text"
438 case "1", "2", "3", "4", "5":
439 return "numeric"
440 case "":
441 return "none"
442 default:
443 if len(priority) == 1 && priority >= "A" && priority <= "Z" {
444 return "legacy"
445 }
446 return "unknown"
447 }
448}