cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐Ÿƒ
charm leaflet readability golang

feat(wip): update push command to support file input and dry-run option

+256 -80
+24 -9
cmd/publication_commands.go
··· 296 296 root.AddCommand(patchCmd) 297 297 298 298 pushCmd := &cobra.Command{ 299 - Use: "push [note-ids...]", 299 + Use: "push [note-ids...] [--file files...]", 300 300 Short: "Create or update multiple documents on leaflet", 301 301 Long: `Batch publish or update multiple local notes to leaflet.pub. 302 302 ··· 307 307 This is useful for bulk operations and continuous publishing workflows. 308 308 309 309 Examples: 310 - noteleaf pub push 1 2 3 # Publish/update notes 1, 2, and 3 311 - noteleaf pub push 42 99 --draft # Create/update as drafts`, 312 - Args: cobra.MinimumNArgs(1), 310 + noteleaf pub push 1 2 3 # Publish/update notes 1, 2, and 3 311 + noteleaf pub push 42 99 --draft # Create/update as drafts 312 + noteleaf pub push --file article.md # Create note from file and push 313 + noteleaf pub push --file a.md b.md --draft # Create notes from multiple files 314 + noteleaf pub push 1 2 --dry-run # Validate without pushing 315 + noteleaf pub push --file article.md --dry-run # Create note but don't push`, 313 316 RunE: func(cmd *cobra.Command, args []string) error { 317 + isDraft, _ := cmd.Flags().GetBool("draft") 318 + dryRun, _ := cmd.Flags().GetBool("dry-run") 319 + files, _ := cmd.Flags().GetStringSlice("file") 320 + 321 + defer c.handler.Close() 322 + 323 + if len(files) > 0 { 324 + return c.handler.PushFromFiles(cmd.Context(), files, isDraft, dryRun) 325 + } 326 + 327 + if len(args) == 0 { 328 + return fmt.Errorf("no note IDs or files provided") 329 + } 330 + 314 331 noteIDs := make([]int64, len(args)) 315 332 for i, arg := range args { 316 333 id, err := parseNoteID(arg) ··· 320 337 noteIDs[i] = id 321 338 } 322 339 323 - isDraft, _ := cmd.Flags().GetBool("draft") 324 - 325 - defer c.handler.Close() 326 - return c.handler.Push(cmd.Context(), noteIDs, isDraft) 340 + return c.handler.Push(cmd.Context(), noteIDs, isDraft, dryRun) 327 341 }, 328 342 } 329 343 pushCmd.Flags().Bool("draft", false, "Create/update as drafts instead of publishing") 344 + pushCmd.Flags().Bool("dry-run", false, "Create note records but skip leaflet push") 345 + pushCmd.Flags().StringSliceP("file", "f", []string{}, "Create notes from markdown files before pushing") 330 346 root.AddCommand(pushCmd) 331 - 332 347 return root 333 348 } 334 349
+33 -2
internal/docs/ROADMAP.md
··· 4 4 5 5 ## Core Usability 6 6 7 - The foundation across all domains is implemented. Tasks support CRUD operations, projects, tags, contexts, and time tracking. Notes have create, list, read, edit, and remove commands with interactive and static modes. Media queues exist for books, movies, and TV with progress and status management. SQLite persistence is in place with setup, seed, and reset commands. TUIs and colorized output are available. 7 + The foundation across all domains is implemented. Tasks support CRUD operations, projects, tags, contexts, and time tracking. 8 + Notes have create, list, read, edit, and remove commands with interactive and static modes. Media queues exist for books, movies, and TV with progress and status management. SQLite persistence is in place with setup, seed, and reset commands. TUIs and colorized output are available. 8 9 9 10 ## RC 10 11 ··· 43 44 #### Publication 44 45 45 46 - [x] Implement authentication with BlueSky/leaflet (AT Protocol). 46 - - [ ] Add OAuth2 47 + - [ ] Add [OAuth2](#publications--authentication) 47 48 - [x] Verify `pub pull` fetches and syncs documents from leaflet. 48 49 - [x] Confirm `pub list` with status filtering (`all`, `published`, `draft`). 49 50 - [ ] Test `pub post` creates new documents with draft/preview/validate modes. ··· 205 206 - [ ] Enhanced parsing coverage 206 207 - [ ] Export to multiple formats 207 208 - [ ] Linking with tasks and notes 209 + 210 + ### Publications & Authentication 211 + 212 + - [ ] OAuth2 authentication for AT Protocol 213 + - [ ] Client metadata server for publishing application details 214 + - [ ] DPoP (Demonstrating Proof of Possession) implementation 215 + - [ ] ES256 JWT generation with unique JTI nonces 216 + - [ ] Server-issued nonce management with 5-minute rotation 217 + - [ ] Separate nonce tracking for authorization and resource servers 218 + - [ ] PAR (Pushed Authorization Requests) flow 219 + - [ ] PKCE code challenge generation 220 + - [ ] State token management 221 + - [ ] Request URI handling 222 + - [ ] Identity resolution and verification 223 + - [ ] Bidirectional handle verification 224 + - [ ] DID resolution from handles 225 + - [ ] Authorization server discovery via .well-known endpoints 226 + - [ ] Token lifecycle management 227 + - [ ] Access token refresh (5-15 min lifetime recommended) 228 + - [ ] Refresh token rotation (180 day max for confidential clients) 229 + - [ ] Concurrent request handling to prevent duplicate refreshes 230 + - [ ] Secure token storage (encrypted at rest) 231 + - [ ] Local callback server for OAuth redirects 232 + - [ ] Ephemeral HTTP server on localhost 233 + - [ ] Browser launch integration 234 + - [ ] Timeout handling for abandoned flows 235 + - [ ] Migration path from app passwords to OAuth 236 + - [ ] Detect existing app password sessions 237 + - [ ] Prompt users to upgrade authentication 238 + - [ ] Maintain backward compatibility 208 239 209 240 ### User Experience 210 241
+152 -14
internal/handlers/publication.go
··· 7 7 "fmt" 8 8 "os" 9 9 "path/filepath" 10 + "strings" 10 11 "time" 11 12 12 13 "github.com/stormlightlabs/noteleaf/internal/models" ··· 384 385 return nil 385 386 } 386 387 388 + // createNoteFromFile creates a note from a markdown file and returns its ID 389 + func (h *PublicationHandler) createNoteFromFile(ctx context.Context, filePath string) (int64, error) { 390 + if _, err := os.Stat(filePath); os.IsNotExist(err) { 391 + return 0, fmt.Errorf("file does not exist: %s", filePath) 392 + } 393 + 394 + content, err := os.ReadFile(filePath) 395 + if err != nil { 396 + return 0, fmt.Errorf("failed to read file: %w", err) 397 + } 398 + 399 + contentStr := string(content) 400 + if strings.TrimSpace(contentStr) == "" { 401 + return 0, fmt.Errorf("file is empty: %s", filePath) 402 + } 403 + 404 + title, noteContent, tags := parseNoteContent(contentStr) 405 + if title == "" { 406 + title = strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) 407 + } 408 + 409 + note := &models.Note{ 410 + Title: title, 411 + Content: noteContent, 412 + Tags: tags, 413 + FilePath: filePath, 414 + } 415 + 416 + noteID, err := h.repos.Notes.Create(ctx, note) 417 + if err != nil { 418 + return 0, fmt.Errorf("failed to create note: %w", err) 419 + } 420 + 421 + ui.Infoln("Created note from file: %s", filePath) 422 + ui.Infoln(" Note: %s (ID: %d)", title, noteID) 423 + if len(tags) > 0 { 424 + ui.Infoln(" Tags: %s", strings.Join(tags, ", ")) 425 + } 426 + 427 + return noteID, nil 428 + } 429 + 430 + // parseNoteContent extracts title, content, and tags from markdown content 431 + func parseNoteContent(content string) (title, noteContent string, tags []string) { 432 + lines := strings.Split(content, "\n") 433 + 434 + for _, line := range lines { 435 + line = strings.TrimSpace(line) 436 + if after, ok := strings.CutPrefix(line, "# "); ok { 437 + title = after 438 + break 439 + } 440 + } 441 + 442 + for _, line := range lines { 443 + line = strings.TrimSpace(line) 444 + if strings.HasPrefix(line, "<!-- Tags:") && strings.HasSuffix(line, "-->") { 445 + tagStr := strings.TrimPrefix(line, "<!-- Tags:") 446 + tagStr = strings.TrimSuffix(tagStr, "-->") 447 + tagStr = strings.TrimSpace(tagStr) 448 + 449 + if tagStr != "" { 450 + for tag := range strings.SplitSeq(tagStr, ",") { 451 + tag = strings.TrimSpace(tag) 452 + if tag != "" { 453 + tags = append(tags, tag) 454 + } 455 + } 456 + } 457 + } 458 + } 459 + 460 + noteContent = content 461 + 462 + return title, noteContent, tags 463 + } 464 + 465 + // PushFromFiles creates notes from files and pushes them to leaflet 466 + func (h *PublicationHandler) PushFromFiles(ctx context.Context, filePaths []string, isDraft bool, dryRun bool) error { 467 + if len(filePaths) == 0 { 468 + return fmt.Errorf("no file paths provided") 469 + } 470 + 471 + ui.Infoln("Creating notes from %d file(s)...\n", len(filePaths)) 472 + 473 + noteIDs := make([]int64, 0, len(filePaths)) 474 + var failed int 475 + 476 + for _, filePath := range filePaths { 477 + noteID, err := h.createNoteFromFile(ctx, filePath) 478 + if err != nil { 479 + ui.Warningln("Failed to create note from %s: %v", filePath, err) 480 + failed++ 481 + continue 482 + } 483 + noteIDs = append(noteIDs, noteID) 484 + } 485 + 486 + if len(noteIDs) == 0 { 487 + return fmt.Errorf("failed to create any notes from files") 488 + } 489 + 490 + ui.Newline() 491 + if dryRun { 492 + ui.Successln("Created %d note(s) from files. Skipping leaflet push (dry run).", len(noteIDs)) 493 + ui.Infoln("Note IDs: %v", noteIDs) 494 + return nil 495 + } 496 + 497 + return h.Push(ctx, noteIDs, isDraft, dryRun) 498 + } 499 + 387 500 // Push creates or updates multiple documents on leaflet from local notes 388 - func (h *PublicationHandler) Push(ctx context.Context, noteIDs []int64, isDraft bool) error { 389 - if !h.atproto.IsAuthenticated() { 501 + func (h *PublicationHandler) Push(ctx context.Context, noteIDs []int64, isDraft bool, dryRun bool) error { 502 + if !dryRun && !h.atproto.IsAuthenticated() { 390 503 return fmt.Errorf("not authenticated - run 'noteleaf pub auth' first") 391 504 } 392 505 ··· 394 507 return fmt.Errorf("no note IDs provided") 395 508 } 396 509 397 - ui.Infoln("Processing %d note(s)...\n", len(noteIDs)) 510 + if dryRun { 511 + ui.Infoln("Dry run: validating %d note(s)...\n", len(noteIDs)) 512 + } else { 513 + ui.Infoln("Processing %d note(s)...\n", len(noteIDs)) 514 + } 398 515 399 516 var created, updated, failed int 400 517 var errors []string ··· 408 525 continue 409 526 } 410 527 411 - if note.HasLeafletAssociation() { 412 - err = h.Patch(ctx, noteID) 528 + if dryRun { 529 + _, _, err := h.prepareDocumentForPublish(ctx, noteID, isDraft, note.HasLeafletAssociation()) 413 530 if err != nil { 414 - ui.Warningln(" [%d] Failed to update '%s': %v", noteID, note.Title, err) 531 + ui.Warningln(" [%d] Validation failed for '%s': %v", noteID, note.Title, err) 415 532 errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err)) 416 533 failed++ 417 534 } else { 418 - updated++ 535 + ui.Infoln(" [%d] '%s' - validation passed", noteID, note.Title) 536 + if note.HasLeafletAssociation() { 537 + updated++ 538 + } else { 539 + created++ 540 + } 419 541 } 420 542 } else { 421 - err = h.Post(ctx, noteID, isDraft) 422 - if err != nil { 423 - ui.Warningln(" [%d] Failed to create '%s': %v", noteID, note.Title, err) 424 - errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err)) 425 - failed++ 543 + if note.HasLeafletAssociation() { 544 + err = h.Patch(ctx, noteID) 545 + if err != nil { 546 + ui.Warningln(" [%d] Failed to update '%s': %v", noteID, note.Title, err) 547 + errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err)) 548 + failed++ 549 + } else { 550 + updated++ 551 + } 426 552 } else { 427 - created++ 553 + err = h.Post(ctx, noteID, isDraft) 554 + if err != nil { 555 + ui.Warningln(" [%d] Failed to create '%s': %v", noteID, note.Title, err) 556 + errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err)) 557 + failed++ 558 + } else { 559 + created++ 560 + } 428 561 } 429 562 } 430 563 } 431 564 432 565 ui.Newline() 433 - ui.Successln("Push complete: %d created, %d updated, %d failed", created, updated, failed) 566 + if dryRun { 567 + ui.Successln("Dry run complete: %d would be created, %d would be updated, %d failed validation", created, updated, failed) 568 + ui.Infoln("No changes made to leaflet") 569 + } else { 570 + ui.Successln("Push complete: %d created, %d updated, %d failed", created, updated, failed) 571 + } 434 572 435 573 if len(errors) > 0 { 436 574 return fmt.Errorf("push completed with %d error(s)", failed)
+8 -8
internal/handlers/publication_test.go
··· 1662 1662 handler := CreateHandler(t, NewPublicationHandler) 1663 1663 ctx := context.Background() 1664 1664 1665 - err := handler.Push(ctx, []int64{1, 2, 3}, false) 1665 + err := handler.Push(ctx, []int64{1, 2, 3}, false, false) 1666 1666 if err == nil { 1667 1667 t.Error("Expected error when not authenticated") 1668 1668 } ··· 1694 1694 t.Fatalf("Failed to restore session: %v", err) 1695 1695 } 1696 1696 1697 - err = handler.Push(ctx, []int64{}, false) 1697 + err = handler.Push(ctx, []int64{}, false, false) 1698 1698 if err == nil { 1699 1699 t.Error("Expected error when no note IDs provided") 1700 1700 } ··· 1726 1726 t.Fatalf("Failed to restore session: %v", err) 1727 1727 } 1728 1728 1729 - err = handler.Push(ctx, []int64{999}, false) 1729 + err = handler.Push(ctx, []int64{999}, false, false) 1730 1730 if err == nil { 1731 1731 t.Error("Expected error when note not found") 1732 1732 } ··· 1773 1773 t.Fatalf("Failed to restore session: %v", err) 1774 1774 } 1775 1775 1776 - err = handler.Push(ctx, []int64{id1, id2}, false) 1776 + err = handler.Push(ctx, []int64{id1, id2}, false, false) 1777 1777 1778 1778 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1779 1779 t.Logf("Got error during push (expected for external service call): %v", err) ··· 1830 1830 t.Fatalf("Failed to restore session: %v", err) 1831 1831 } 1832 1832 1833 - err = handler.Push(ctx, []int64{id1, id2}, false) 1833 + err = handler.Push(ctx, []int64{id1, id2}, false, false) 1834 1834 1835 1835 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1836 1836 t.Logf("Got error during push (expected for external service call): %v", err) ··· 1882 1882 t.Fatalf("Failed to restore session: %v", err) 1883 1883 } 1884 1884 1885 - err = handler.Push(ctx, []int64{newID, existingID}, false) 1885 + err = handler.Push(ctx, []int64{newID, existingID}, false, false) 1886 1886 1887 1887 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1888 1888 t.Logf("Got error during push (expected for external service call): %v", err) ··· 1920 1920 } 1921 1921 1922 1922 invalidID := int64(999) 1923 - err = handler.Push(ctx, []int64{id1, invalidID}, false) 1923 + err = handler.Push(ctx, []int64{id1, invalidID}, false, false) 1924 1924 1925 1925 if err == nil { 1926 1926 t.Error("Expected error due to invalid note ID") ··· 1961 1961 t.Fatalf("Failed to restore session: %v", err) 1962 1962 } 1963 1963 1964 - err = handler.Push(ctx, []int64{id}, true) 1964 + err = handler.Push(ctx, []int64{id}, true, false) 1965 1965 1966 1966 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1967 1967 t.Logf("Got error during push (expected for external service call): %v", err)
+39 -47
internal/ui/palette.go
··· 19 19 20 20 var NoteleafColorScheme fang.ColorSchemeFunc = noteleafColorScheme 21 21 22 + // noteleafColorScheme provides Iceberg-inspired colors for CLI help/documentation 23 + // 24 + // Philosophy: Cool blues as primary, warm accents for emphasis, hierarchical text colors 25 + // See: https://github.com/cocopon/iceberg.vim for more information 22 26 func noteleafColorScheme(c lipglossv2.LightDarkFunc) fang.ColorScheme { 23 27 return fang.ColorScheme{ 24 - Base: c(Salt, Pepper), // Light/Dark base text 25 - Title: c(Guac, Julep), // Green primary for titles 26 - Description: c(Squid, Smoke), // Muted gray for descriptions 27 - Codeblock: c(Butter, BBQ), // Light/Dark background for code 28 - Program: c(Malibu, Sardine), // Blue for program names 29 - DimmedArgument: c(Oyster, Ash), // Subtle gray for dimmed text 30 - Comment: c(Pickle, NeueGuac), // Green for comments 31 - Flag: c(Violet, Mauve), // Purple for flags 32 - FlagDefault: c(Lichen, Turtle), // Teal for flag defaults 33 - Command: c(Julep, Guac), // Bright green for commands 34 - QuotedString: c(Citron, Mustard), // Yellow for quoted strings 35 - Argument: c(Sapphire, Guppy), // Blue for arguments 36 - Help: c(Smoke, Iron), // Gray for help text 37 - Dash: c(Iron, Oyster), // Medium gray for dashes 38 - ErrorHeader: [2]color.Color{Cherry, Sriracha}, // Red for error headers (fg, bg) 39 - ErrorDetails: c(Coral, Salmon), // Red/pink for error details 28 + Base: c(Salt, Pepper), // Primary text on dark background 29 + Title: c(Malibu, Malibu), // Blue primary for titles (Iceberg primary) 30 + Description: c(Smoke, Smoke), // Secondary text for descriptions 31 + Codeblock: c(Butter, BBQ), // Light/Dark background for code blocks 32 + Program: c(Malibu, Sardine), // Blue for program names (primary accent) 33 + DimmedArgument: c(Oyster, Ash), // Dimmed text for optional arguments 34 + Comment: c(Squid, Squid), // Muted gray for comments (Iceberg comment) 35 + Flag: c(Hazy, Jelly), // Purple for flags (Iceberg special) 36 + FlagDefault: c(Lichen, Turtle), // Teal for flag defaults (secondary accent) 37 + Command: c(Julep, Julep), // Green for commands (success/positive) 38 + QuotedString: c(Tang, Tang), // Orange for quoted strings (warning/warm) 39 + Argument: c(Lichen, Lichen), // Teal for arguments (secondary accent) 40 + Help: c(Squid, Squid), // Muted gray for help text 41 + Dash: c(Oyster, Oyster), // Dimmed gray for dashes/separators 42 + ErrorHeader: [2]color.Color{Sriracha, Sriracha}, // Red for error headers (Iceberg error) 43 + ErrorDetails: c(Coral, Salmon), // Pink/coral for error details 40 44 } 41 45 } 42 46 ··· 53 57 } 54 58 55 59 var ( 56 - // Background colors (dark mode, Iceberg-inspired) 57 60 ColorBGBase = Pepper.Hex() // #201F26 - Darkest base 58 61 ColorBGSecondary = BBQ.Hex() // #2d2c35 - Secondary background 59 62 ColorBGTertiary = Charcoal.Hex() // #3A3943 - Tertiary/elevated 60 63 ColorBGInput = Iron.Hex() // #4D4C57 - Input fields/focus 61 64 62 - // Text colors (light to dark hierarchy) 63 65 ColorTextPrimary = Salt.Hex() // #F1EFEF - Primary text (brightest) 64 66 ColorTextSecondary = Smoke.Hex() // #BFBCC8 - Secondary text 65 67 ColorTextMuted = Squid.Hex() // #858392 - Muted/comments 66 68 ColorTextDimmed = Oyster.Hex() // #605F6B - Dimmed text 67 69 68 - // Semantic colors (Iceberg-inspired: cool blues/purples with warm accents) 69 70 ColorPrimary = Malibu.Hex() // #00A4FF - Blue (primary accent) 70 71 ColorSuccess = Julep.Hex() // #00FFB2 - Green (success/positive) 71 72 ColorError = Sriracha.Hex() // #EB4268 - Red (errors) ··· 73 74 ColorInfo = Violet.Hex() // #C259FF - Purple (info) 74 75 ColorAccent = Lichen.Hex() // #5CDFEA - Teal (secondary accent) 75 76 76 - // Base styles 77 77 PrimaryStyle = newStyle().Foreground(lipgloss.Color(ColorPrimary)) 78 78 SuccessStyle = newBoldStyle().Foreground(lipgloss.Color(ColorSuccess)) 79 79 ErrorStyle = newBoldStyle().Foreground(lipgloss.Color(ColorError)) ··· 85 85 TitleStyle = newPBoldStyle(0, 1).Foreground(lipgloss.Color(ColorPrimary)) 86 86 SubtitleStyle = newEmStyle().Foreground(lipgloss.Color(ColorAccent)) 87 87 88 - // Layout styles 89 88 BoxStyle = newPStyle(1, 2).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorPrimary)) 90 89 ErrorBoxStyle = newPStyle(1, 2).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorError)) 91 90 HeaderStyle = newPBoldStyle(0, 1).Foreground(lipgloss.Color(ColorPrimary)) 92 91 CellStyle = newPStyle(0, 1).Foreground(lipgloss.Color(ColorTextPrimary)) 93 92 94 - // List styles 95 93 ListItemStyle = newStyle().Foreground(lipgloss.Color(ColorTextPrimary)).PaddingLeft(2) 96 94 SelectedItemStyle = newBoldStyle().Foreground(lipgloss.Color(ColorPrimary)).PaddingLeft(2) 97 95 98 - // Table/data view styles (replacing ANSI code references) 99 96 TableStyle = newStyle().BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color(ColorTextMuted)) 100 97 TableHeaderStyle = newBoldStyle().Foreground(lipgloss.Color(ColorAccent)) 101 98 TableTitleStyle = newBoldStyle().Foreground(lipgloss.Color(ColorPrimary)) 102 99 TableSelectedStyle = newBoldStyle().Foreground(lipgloss.Color(ColorTextPrimary)).Background(lipgloss.Color(ColorBGInput)) 103 100 104 - // Task-specific styles 105 101 TaskTitleStyle = newBoldStyle().Foreground(lipgloss.Color(ColorTextPrimary)) 106 102 TaskIDStyle = newStyle().Foreground(lipgloss.Color(ColorTextMuted)).Width(8) 107 103 108 - // Status styles (Iceberg-inspired: muted โ†’ blue โ†’ red โ†’ green) 109 - StatusTodo = newStyle().Foreground(lipgloss.Color(ColorTextMuted)) // Gray (muted) 110 - StatusInProgress = newStyle().Foreground(lipgloss.Color(ColorPrimary)) // Blue (active) 111 - StatusBlocked = newStyle().Foreground(lipgloss.Color(ColorError)) // Red (blocked) 112 - StatusDone = newStyle().Foreground(lipgloss.Color(ColorSuccess)) // Green (success) 113 - StatusPending = newStyle().Foreground(lipgloss.Color(ColorWarning)) // Orange (pending) 114 - StatusCompleted = newStyle().Foreground(lipgloss.Color(ColorSuccess)) // Green (completed) 115 - StatusAbandoned = newStyle().Foreground(lipgloss.Color(ColorTextDimmed)) // Dimmed gray (abandoned) 116 - StatusDeleted = newStyle().Foreground(lipgloss.Color(Cherry.Hex())) // Dark red (deleted) 104 + StatusTodo = newStyle().Foreground(lipgloss.Color(ColorTextMuted)) // Gray (muted) 105 + StatusInProgress = newStyle().Foreground(lipgloss.Color(ColorPrimary)) // Blue (active) 106 + StatusBlocked = newStyle().Foreground(lipgloss.Color(ColorError)) // Red (blocked) 107 + StatusDone = newStyle().Foreground(lipgloss.Color(ColorSuccess)) // Green (success) 108 + StatusPending = newStyle().Foreground(lipgloss.Color(ColorWarning)) // Orange (pending) 109 + StatusCompleted = newStyle().Foreground(lipgloss.Color(ColorSuccess)) // Green (completed) 110 + StatusAbandoned = newStyle().Foreground(lipgloss.Color(ColorTextDimmed)) // Dimmed gray (abandoned) 111 + StatusDeleted = newStyle().Foreground(lipgloss.Color(Pom.Hex())) // Dark red (deleted) 117 112 118 - // Priority styles (Iceberg-inspired: red โ†’ orange โ†’ gray) 119 - PriorityHigh = newBoldStyle().Foreground(lipgloss.Color(Cherry.Hex())) // #FF388B - Bright red 120 - PriorityMedium = newStyle().Foreground(lipgloss.Color(Tang.Hex())) // #FF985A - Orange 121 - PriorityLow = newStyle().Foreground(lipgloss.Color(ColorAccent)) // Teal (low) 122 - PriorityNone = newStyle().Foreground(lipgloss.Color(ColorTextMuted)) // Gray (no priority) 123 - PriorityLegacy = newStyle().Foreground(lipgloss.Color(Urchin.Hex())) // #C337E0 - Magenta (legacy) 113 + PriorityHigh = newBoldStyle().Foreground(lipgloss.Color(Pom.Hex())) // #FF388B - Bright red 114 + PriorityMedium = newStyle().Foreground(lipgloss.Color(Tang.Hex())) // #FF985A - Orange 115 + PriorityLow = newStyle().Foreground(lipgloss.Color(ColorAccent)) // #5CDFEA - Teal (low) 116 + PriorityNone = newStyle().Foreground(lipgloss.Color(ColorTextMuted)) // #858392 - Gray (no priority) 117 + PriorityLegacy = newStyle().Foreground(lipgloss.Color(Urchin.Hex())) // #C337E0 - Magenta (legacy) 124 118 125 - // Content type styles (distinctive colors for different media) 126 - MovieStyle = newBoldStyle().Foreground(lipgloss.Color(Coral.Hex())) // #FF577D - Pink/coral 127 - TVStyle = newBoldStyle().Foreground(lipgloss.Color(Violet.Hex())) // #C259FF - Purple 128 - BookStyle = newBoldStyle().Foreground(lipgloss.Color(Guac.Hex())) // #12C78F - Green 129 - MusicStyle = newBoldStyle().Foreground(lipgloss.Color(Lichen.Hex())) // #5CDFEA - Teal 119 + MovieStyle = newBoldStyle().Foreground(lipgloss.Color(Coral.Hex())) // #FF577D - Pink/coral 120 + TVStyle = newBoldStyle().Foreground(lipgloss.Color(Violet.Hex())) // #C259FF - Purple 121 + BookStyle = newBoldStyle().Foreground(lipgloss.Color(Guac.Hex())) // #12C78F - Green 122 + MusicStyle = newBoldStyle().Foreground(lipgloss.Color(Lichen.Hex())) // #5CDFEA - Teal 130 123 131 - // Diff styles 132 - AdditionStyle = newStyle().Foreground(lipgloss.Color(Pickle.Hex())) // #00A475 - Green 133 - DeletionStyle = newStyle().Foreground(lipgloss.Color(Pom.Hex())) // #AB2454 - Red 124 + AdditionStyle = newStyle().Foreground(lipgloss.Color(Pickle.Hex())) // #00A475 - Green 125 + DeletionStyle = newStyle().Foreground(lipgloss.Color(Pom.Hex())) // #AB2454 - Red 134 126 )