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 root.AddCommand(patchCmd) 297 298 pushCmd := &cobra.Command{ 299 - Use: "push [note-ids...]", 300 Short: "Create or update multiple documents on leaflet", 301 Long: `Batch publish or update multiple local notes to leaflet.pub. 302 ··· 307 This is useful for bulk operations and continuous publishing workflows. 308 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), 313 RunE: func(cmd *cobra.Command, args []string) error { 314 noteIDs := make([]int64, len(args)) 315 for i, arg := range args { 316 id, err := parseNoteID(arg) ··· 320 noteIDs[i] = id 321 } 322 323 - isDraft, _ := cmd.Flags().GetBool("draft") 324 - 325 - defer c.handler.Close() 326 - return c.handler.Push(cmd.Context(), noteIDs, isDraft) 327 }, 328 } 329 pushCmd.Flags().Bool("draft", false, "Create/update as drafts instead of publishing") 330 root.AddCommand(pushCmd) 331 - 332 return root 333 } 334
··· 296 root.AddCommand(patchCmd) 297 298 pushCmd := &cobra.Command{ 299 + Use: "push [note-ids...] [--file files...]", 300 Short: "Create or update multiple documents on leaflet", 301 Long: `Batch publish or update multiple local notes to leaflet.pub. 302 ··· 307 This is useful for bulk operations and continuous publishing workflows. 308 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 + 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`, 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 + 331 noteIDs := make([]int64, len(args)) 332 for i, arg := range args { 333 id, err := parseNoteID(arg) ··· 337 noteIDs[i] = id 338 } 339 340 + return c.handler.Push(cmd.Context(), noteIDs, isDraft, dryRun) 341 }, 342 } 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") 346 root.AddCommand(pushCmd) 347 return root 348 } 349
+33 -2
internal/docs/ROADMAP.md
··· 4 5 ## Core Usability 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. 8 9 ## RC 10 ··· 43 #### Publication 44 45 - [x] Implement authentication with BlueSky/leaflet (AT Protocol). 46 - - [ ] Add OAuth2 47 - [x] Verify `pub pull` fetches and syncs documents from leaflet. 48 - [x] Confirm `pub list` with status filtering (`all`, `published`, `draft`). 49 - [ ] Test `pub post` creates new documents with draft/preview/validate modes. ··· 205 - [ ] Enhanced parsing coverage 206 - [ ] Export to multiple formats 207 - [ ] Linking with tasks and notes 208 209 ### User Experience 210
··· 4 5 ## Core Usability 6 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. 9 10 ## RC 11 ··· 44 #### Publication 45 46 - [x] Implement authentication with BlueSky/leaflet (AT Protocol). 47 + - [ ] Add [OAuth2](#publications--authentication) 48 - [x] Verify `pub pull` fetches and syncs documents from leaflet. 49 - [x] Confirm `pub list` with status filtering (`all`, `published`, `draft`). 50 - [ ] Test `pub post` creates new documents with draft/preview/validate modes. ··· 206 - [ ] Enhanced parsing coverage 207 - [ ] Export to multiple formats 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 239 240 ### User Experience 241
+152 -14
internal/handlers/publication.go
··· 7 "fmt" 8 "os" 9 "path/filepath" 10 "time" 11 12 "github.com/stormlightlabs/noteleaf/internal/models" ··· 384 return nil 385 } 386 387 // 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() { 390 return fmt.Errorf("not authenticated - run 'noteleaf pub auth' first") 391 } 392 ··· 394 return fmt.Errorf("no note IDs provided") 395 } 396 397 - ui.Infoln("Processing %d note(s)...\n", len(noteIDs)) 398 399 var created, updated, failed int 400 var errors []string ··· 408 continue 409 } 410 411 - if note.HasLeafletAssociation() { 412 - err = h.Patch(ctx, noteID) 413 if err != nil { 414 - ui.Warningln(" [%d] Failed to update '%s': %v", noteID, note.Title, err) 415 errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err)) 416 failed++ 417 } else { 418 - updated++ 419 } 420 } 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++ 426 } else { 427 - created++ 428 } 429 } 430 } 431 432 ui.Newline() 433 - ui.Successln("Push complete: %d created, %d updated, %d failed", created, updated, failed) 434 435 if len(errors) > 0 { 436 return fmt.Errorf("push completed with %d error(s)", failed)
··· 7 "fmt" 8 "os" 9 "path/filepath" 10 + "strings" 11 "time" 12 13 "github.com/stormlightlabs/noteleaf/internal/models" ··· 385 return nil 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 + 500 // Push creates or updates multiple documents on leaflet from local notes 501 + func (h *PublicationHandler) Push(ctx context.Context, noteIDs []int64, isDraft bool, dryRun bool) error { 502 + if !dryRun && !h.atproto.IsAuthenticated() { 503 return fmt.Errorf("not authenticated - run 'noteleaf pub auth' first") 504 } 505 ··· 507 return fmt.Errorf("no note IDs provided") 508 } 509 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 + } 515 516 var created, updated, failed int 517 var errors []string ··· 525 continue 526 } 527 528 + if dryRun { 529 + _, _, err := h.prepareDocumentForPublish(ctx, noteID, isDraft, note.HasLeafletAssociation()) 530 if err != nil { 531 + ui.Warningln(" [%d] Validation failed for '%s': %v", noteID, note.Title, err) 532 errors = append(errors, fmt.Sprintf("note %d (%s): %v", noteID, note.Title, err)) 533 failed++ 534 } else { 535 + ui.Infoln(" [%d] '%s' - validation passed", noteID, note.Title) 536 + if note.HasLeafletAssociation() { 537 + updated++ 538 + } else { 539 + created++ 540 + } 541 } 542 } else { 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 + } 552 } else { 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 + } 561 } 562 } 563 } 564 565 ui.Newline() 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 + } 572 573 if len(errors) > 0 { 574 return fmt.Errorf("push completed with %d error(s)", failed)
+8 -8
internal/handlers/publication_test.go
··· 1662 handler := CreateHandler(t, NewPublicationHandler) 1663 ctx := context.Background() 1664 1665 - err := handler.Push(ctx, []int64{1, 2, 3}, false) 1666 if err == nil { 1667 t.Error("Expected error when not authenticated") 1668 } ··· 1694 t.Fatalf("Failed to restore session: %v", err) 1695 } 1696 1697 - err = handler.Push(ctx, []int64{}, false) 1698 if err == nil { 1699 t.Error("Expected error when no note IDs provided") 1700 } ··· 1726 t.Fatalf("Failed to restore session: %v", err) 1727 } 1728 1729 - err = handler.Push(ctx, []int64{999}, false) 1730 if err == nil { 1731 t.Error("Expected error when note not found") 1732 } ··· 1773 t.Fatalf("Failed to restore session: %v", err) 1774 } 1775 1776 - err = handler.Push(ctx, []int64{id1, id2}, false) 1777 1778 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1779 t.Logf("Got error during push (expected for external service call): %v", err) ··· 1830 t.Fatalf("Failed to restore session: %v", err) 1831 } 1832 1833 - err = handler.Push(ctx, []int64{id1, id2}, false) 1834 1835 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1836 t.Logf("Got error during push (expected for external service call): %v", err) ··· 1882 t.Fatalf("Failed to restore session: %v", err) 1883 } 1884 1885 - err = handler.Push(ctx, []int64{newID, existingID}, false) 1886 1887 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1888 t.Logf("Got error during push (expected for external service call): %v", err) ··· 1920 } 1921 1922 invalidID := int64(999) 1923 - err = handler.Push(ctx, []int64{id1, invalidID}, false) 1924 1925 if err == nil { 1926 t.Error("Expected error due to invalid note ID") ··· 1961 t.Fatalf("Failed to restore session: %v", err) 1962 } 1963 1964 - err = handler.Push(ctx, []int64{id}, true) 1965 1966 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1967 t.Logf("Got error during push (expected for external service call): %v", err)
··· 1662 handler := CreateHandler(t, NewPublicationHandler) 1663 ctx := context.Background() 1664 1665 + err := handler.Push(ctx, []int64{1, 2, 3}, false, false) 1666 if err == nil { 1667 t.Error("Expected error when not authenticated") 1668 } ··· 1694 t.Fatalf("Failed to restore session: %v", err) 1695 } 1696 1697 + err = handler.Push(ctx, []int64{}, false, false) 1698 if err == nil { 1699 t.Error("Expected error when no note IDs provided") 1700 } ··· 1726 t.Fatalf("Failed to restore session: %v", err) 1727 } 1728 1729 + err = handler.Push(ctx, []int64{999}, false, false) 1730 if err == nil { 1731 t.Error("Expected error when note not found") 1732 } ··· 1773 t.Fatalf("Failed to restore session: %v", err) 1774 } 1775 1776 + err = handler.Push(ctx, []int64{id1, id2}, false, false) 1777 1778 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1779 t.Logf("Got error during push (expected for external service call): %v", err) ··· 1830 t.Fatalf("Failed to restore session: %v", err) 1831 } 1832 1833 + err = handler.Push(ctx, []int64{id1, id2}, false, false) 1834 1835 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1836 t.Logf("Got error during push (expected for external service call): %v", err) ··· 1882 t.Fatalf("Failed to restore session: %v", err) 1883 } 1884 1885 + err = handler.Push(ctx, []int64{newID, existingID}, false, false) 1886 1887 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1888 t.Logf("Got error during push (expected for external service call): %v", err) ··· 1920 } 1921 1922 invalidID := int64(999) 1923 + err = handler.Push(ctx, []int64{id1, invalidID}, false, false) 1924 1925 if err == nil { 1926 t.Error("Expected error due to invalid note ID") ··· 1961 t.Fatalf("Failed to restore session: %v", err) 1962 } 1963 1964 + err = handler.Push(ctx, []int64{id}, true, false) 1965 1966 if err != nil && !strings.Contains(err.Error(), "not authenticated") { 1967 t.Logf("Got error during push (expected for external service call): %v", err)
+39 -47
internal/ui/palette.go
··· 19 20 var NoteleafColorScheme fang.ColorSchemeFunc = noteleafColorScheme 21 22 func noteleafColorScheme(c lipglossv2.LightDarkFunc) fang.ColorScheme { 23 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 40 } 41 } 42 ··· 53 } 54 55 var ( 56 - // Background colors (dark mode, Iceberg-inspired) 57 ColorBGBase = Pepper.Hex() // #201F26 - Darkest base 58 ColorBGSecondary = BBQ.Hex() // #2d2c35 - Secondary background 59 ColorBGTertiary = Charcoal.Hex() // #3A3943 - Tertiary/elevated 60 ColorBGInput = Iron.Hex() // #4D4C57 - Input fields/focus 61 62 - // Text colors (light to dark hierarchy) 63 ColorTextPrimary = Salt.Hex() // #F1EFEF - Primary text (brightest) 64 ColorTextSecondary = Smoke.Hex() // #BFBCC8 - Secondary text 65 ColorTextMuted = Squid.Hex() // #858392 - Muted/comments 66 ColorTextDimmed = Oyster.Hex() // #605F6B - Dimmed text 67 68 - // Semantic colors (Iceberg-inspired: cool blues/purples with warm accents) 69 ColorPrimary = Malibu.Hex() // #00A4FF - Blue (primary accent) 70 ColorSuccess = Julep.Hex() // #00FFB2 - Green (success/positive) 71 ColorError = Sriracha.Hex() // #EB4268 - Red (errors) ··· 73 ColorInfo = Violet.Hex() // #C259FF - Purple (info) 74 ColorAccent = Lichen.Hex() // #5CDFEA - Teal (secondary accent) 75 76 - // Base styles 77 PrimaryStyle = newStyle().Foreground(lipgloss.Color(ColorPrimary)) 78 SuccessStyle = newBoldStyle().Foreground(lipgloss.Color(ColorSuccess)) 79 ErrorStyle = newBoldStyle().Foreground(lipgloss.Color(ColorError)) ··· 85 TitleStyle = newPBoldStyle(0, 1).Foreground(lipgloss.Color(ColorPrimary)) 86 SubtitleStyle = newEmStyle().Foreground(lipgloss.Color(ColorAccent)) 87 88 - // Layout styles 89 BoxStyle = newPStyle(1, 2).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorPrimary)) 90 ErrorBoxStyle = newPStyle(1, 2).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorError)) 91 HeaderStyle = newPBoldStyle(0, 1).Foreground(lipgloss.Color(ColorPrimary)) 92 CellStyle = newPStyle(0, 1).Foreground(lipgloss.Color(ColorTextPrimary)) 93 94 - // List styles 95 ListItemStyle = newStyle().Foreground(lipgloss.Color(ColorTextPrimary)).PaddingLeft(2) 96 SelectedItemStyle = newBoldStyle().Foreground(lipgloss.Color(ColorPrimary)).PaddingLeft(2) 97 98 - // Table/data view styles (replacing ANSI code references) 99 TableStyle = newStyle().BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color(ColorTextMuted)) 100 TableHeaderStyle = newBoldStyle().Foreground(lipgloss.Color(ColorAccent)) 101 TableTitleStyle = newBoldStyle().Foreground(lipgloss.Color(ColorPrimary)) 102 TableSelectedStyle = newBoldStyle().Foreground(lipgloss.Color(ColorTextPrimary)).Background(lipgloss.Color(ColorBGInput)) 103 104 - // Task-specific styles 105 TaskTitleStyle = newBoldStyle().Foreground(lipgloss.Color(ColorTextPrimary)) 106 TaskIDStyle = newStyle().Foreground(lipgloss.Color(ColorTextMuted)).Width(8) 107 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) 117 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) 124 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 130 131 - // Diff styles 132 - AdditionStyle = newStyle().Foreground(lipgloss.Color(Pickle.Hex())) // #00A475 - Green 133 - DeletionStyle = newStyle().Foreground(lipgloss.Color(Pom.Hex())) // #AB2454 - Red 134 )
··· 19 20 var NoteleafColorScheme fang.ColorSchemeFunc = noteleafColorScheme 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 26 func noteleafColorScheme(c lipglossv2.LightDarkFunc) fang.ColorScheme { 27 return fang.ColorScheme{ 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 44 } 45 } 46 ··· 57 } 58 59 var ( 60 ColorBGBase = Pepper.Hex() // #201F26 - Darkest base 61 ColorBGSecondary = BBQ.Hex() // #2d2c35 - Secondary background 62 ColorBGTertiary = Charcoal.Hex() // #3A3943 - Tertiary/elevated 63 ColorBGInput = Iron.Hex() // #4D4C57 - Input fields/focus 64 65 ColorTextPrimary = Salt.Hex() // #F1EFEF - Primary text (brightest) 66 ColorTextSecondary = Smoke.Hex() // #BFBCC8 - Secondary text 67 ColorTextMuted = Squid.Hex() // #858392 - Muted/comments 68 ColorTextDimmed = Oyster.Hex() // #605F6B - Dimmed text 69 70 ColorPrimary = Malibu.Hex() // #00A4FF - Blue (primary accent) 71 ColorSuccess = Julep.Hex() // #00FFB2 - Green (success/positive) 72 ColorError = Sriracha.Hex() // #EB4268 - Red (errors) ··· 74 ColorInfo = Violet.Hex() // #C259FF - Purple (info) 75 ColorAccent = Lichen.Hex() // #5CDFEA - Teal (secondary accent) 76 77 PrimaryStyle = newStyle().Foreground(lipgloss.Color(ColorPrimary)) 78 SuccessStyle = newBoldStyle().Foreground(lipgloss.Color(ColorSuccess)) 79 ErrorStyle = newBoldStyle().Foreground(lipgloss.Color(ColorError)) ··· 85 TitleStyle = newPBoldStyle(0, 1).Foreground(lipgloss.Color(ColorPrimary)) 86 SubtitleStyle = newEmStyle().Foreground(lipgloss.Color(ColorAccent)) 87 88 BoxStyle = newPStyle(1, 2).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorPrimary)) 89 ErrorBoxStyle = newPStyle(1, 2).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color(ColorError)) 90 HeaderStyle = newPBoldStyle(0, 1).Foreground(lipgloss.Color(ColorPrimary)) 91 CellStyle = newPStyle(0, 1).Foreground(lipgloss.Color(ColorTextPrimary)) 92 93 ListItemStyle = newStyle().Foreground(lipgloss.Color(ColorTextPrimary)).PaddingLeft(2) 94 SelectedItemStyle = newBoldStyle().Foreground(lipgloss.Color(ColorPrimary)).PaddingLeft(2) 95 96 TableStyle = newStyle().BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color(ColorTextMuted)) 97 TableHeaderStyle = newBoldStyle().Foreground(lipgloss.Color(ColorAccent)) 98 TableTitleStyle = newBoldStyle().Foreground(lipgloss.Color(ColorPrimary)) 99 TableSelectedStyle = newBoldStyle().Foreground(lipgloss.Color(ColorTextPrimary)).Background(lipgloss.Color(ColorBGInput)) 100 101 TaskTitleStyle = newBoldStyle().Foreground(lipgloss.Color(ColorTextPrimary)) 102 TaskIDStyle = newStyle().Foreground(lipgloss.Color(ColorTextMuted)).Width(8) 103 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) 112 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) 118 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 123 124 + AdditionStyle = newStyle().Foreground(lipgloss.Color(Pickle.Hex())) // #00A475 - Green 125 + DeletionStyle = newStyle().Foreground(lipgloss.Color(Pom.Hex())) // #AB2454 - Red 126 )