cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 🍃
charm leaflet readability golang
at main 448 lines 12 kB view raw
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}