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.

feat: note model

+215 -5
+7
ROADMAP.md
··· 72 72 - `read|view` - Displays formatted note content with syntax highlighting 73 73 - `edit|update` - Opens configured editor OR Replaces note content with new markdown file 74 74 - `remove|rm|delete|del` - Permanently removes the note file and metadata 75 + 76 + - `search` - Search notes by content, title, or tags 77 + - `tag` - Add/remove tags from notes 78 + - `recent` - Show recently created/modified notes 79 + - `templates` - Create notes from predefined templates 80 + - `archive` - Archive old notes 81 + - `export` - Export notes to various formats
+43
internal/models/models.go
··· 94 94 Finished *time.Time `json:"finished,omitempty"` 95 95 } 96 96 97 + // Note represents a markdown note 98 + type Note struct { 99 + ID int64 `json:"id"` 100 + Title string `json:"title"` 101 + Content string `json:"content"` 102 + Tags []string `json:"tags,omitempty"` 103 + Archived bool `json:"archived"` 104 + Created time.Time `json:"created"` 105 + Modified time.Time `json:"modified"` 106 + FilePath string `json:"file_path,omitempty"` 107 + } 108 + 97 109 // MarshalTags converts tags slice to JSON string for database storage 98 110 func (t *Task) MarshalTags() (string, error) { 99 111 if len(t.Tags) == 0 { ··· 226 238 func (b *Book) SetCreatedAt(time time.Time) { b.Added = time } 227 239 func (b *Book) GetUpdatedAt() time.Time { return b.Added } 228 240 func (b *Book) SetUpdatedAt(time time.Time) { b.Added = time } 241 + 242 + // MarshalTags converts tags slice to JSON string for database storage 243 + func (n *Note) MarshalTags() (string, error) { 244 + if len(n.Tags) == 0 { 245 + return "", nil 246 + } 247 + data, err := json.Marshal(n.Tags) 248 + return string(data), err 249 + } 250 + 251 + // UnmarshalTags converts JSON string from database to tags slice 252 + func (n *Note) UnmarshalTags(data string) error { 253 + if data == "" { 254 + n.Tags = nil 255 + return nil 256 + } 257 + return json.Unmarshal([]byte(data), &n.Tags) 258 + } 259 + 260 + // IsArchived returns true if the note is archived 261 + func (n *Note) IsArchived() bool { 262 + return n.Archived 263 + } 264 + 265 + func (n *Note) GetID() int64 { return n.ID } 266 + func (n *Note) SetID(id int64) { n.ID = id } 267 + func (n *Note) GetTableName() string { return "notes" } 268 + func (n *Note) GetCreatedAt() time.Time { return n.Created } 269 + func (n *Note) SetCreatedAt(time time.Time) { n.Created = time } 270 + func (n *Note) GetUpdatedAt() time.Time { return n.Modified } 271 + func (n *Note) SetUpdatedAt(time time.Time) { n.Modified = time }
+145 -3
internal/models/models_test.go
··· 533 533 }) 534 534 }) 535 535 536 + t.Run("Note Model", func(t *testing.T) { 537 + t.Run("Model Interface Implementation", func(t *testing.T) { 538 + note := &Note{ 539 + ID: 1, 540 + Title: "Test Note", 541 + Content: "This is test content", 542 + Created: time.Now(), 543 + } 544 + 545 + if note.GetID() != 1 { 546 + t.Errorf("Expected ID 1, got %d", note.GetID()) 547 + } 548 + 549 + note.SetID(2) 550 + if note.GetID() != 2 { 551 + t.Errorf("Expected ID 2 after SetID, got %d", note.GetID()) 552 + } 553 + 554 + if note.GetTableName() != "notes" { 555 + t.Errorf("Expected table name 'notes', got '%s'", note.GetTableName()) 556 + } 557 + 558 + createdAt := time.Now() 559 + note.SetCreatedAt(createdAt) 560 + if !note.GetCreatedAt().Equal(createdAt) { 561 + t.Errorf("Expected created at %v, got %v", createdAt, note.GetCreatedAt()) 562 + } 563 + 564 + updatedAt := time.Now().Add(time.Hour) 565 + note.SetUpdatedAt(updatedAt) 566 + if !note.GetUpdatedAt().Equal(updatedAt) { 567 + t.Errorf("Expected updated at %v, got %v", updatedAt, note.GetUpdatedAt()) 568 + } 569 + }) 570 + 571 + t.Run("Archive Methods", func(t *testing.T) { 572 + note := &Note{Archived: false} 573 + 574 + if note.IsArchived() { 575 + t.Error("Note should not be archived") 576 + } 577 + 578 + note.Archived = true 579 + if !note.IsArchived() { 580 + t.Error("Note should be archived") 581 + } 582 + }) 583 + 584 + t.Run("Tags Marshaling", func(t *testing.T) { 585 + note := &Note{} 586 + 587 + result, err := note.MarshalTags() 588 + if err != nil { 589 + t.Fatalf("MarshalTags failed: %v", err) 590 + } 591 + if result != "" { 592 + t.Errorf("Expected empty string for empty tags, got '%s'", result) 593 + } 594 + 595 + note.Tags = []string{"personal", "work", "idea"} 596 + result, err = note.MarshalTags() 597 + if err != nil { 598 + t.Fatalf("MarshalTags failed: %v", err) 599 + } 600 + 601 + expected := `["personal","work","idea"]` 602 + if result != expected { 603 + t.Errorf("Expected %s, got %s", expected, result) 604 + } 605 + 606 + newNote := &Note{} 607 + err = newNote.UnmarshalTags(result) 608 + if err != nil { 609 + t.Fatalf("UnmarshalTags failed: %v", err) 610 + } 611 + 612 + if len(newNote.Tags) != 3 { 613 + t.Errorf("Expected 3 tags, got %d", len(newNote.Tags)) 614 + } 615 + if newNote.Tags[0] != "personal" || newNote.Tags[1] != "work" || newNote.Tags[2] != "idea" { 616 + t.Errorf("Tags not unmarshaled correctly: %v", newNote.Tags) 617 + } 618 + 619 + emptyNote := &Note{} 620 + err = emptyNote.UnmarshalTags("") 621 + if err != nil { 622 + t.Fatalf("UnmarshalTags with empty string failed: %v", err) 623 + } 624 + if emptyNote.Tags != nil { 625 + t.Error("Expected nil tags for empty string") 626 + } 627 + }) 628 + 629 + t.Run("JSON Marshaling", func(t *testing.T) { 630 + now := time.Now() 631 + modified := now.Add(time.Hour) 632 + note := &Note{ 633 + ID: 1, 634 + Title: "Test Note", 635 + Content: "This is test content with **markdown**", 636 + Tags: []string{"personal", "markdown"}, 637 + Archived: false, 638 + Created: now, 639 + Modified: modified, 640 + FilePath: "/path/to/note.md", 641 + } 642 + 643 + data, err := json.Marshal(note) 644 + if err != nil { 645 + t.Fatalf("JSON marshal failed: %v", err) 646 + } 647 + 648 + var unmarshaled Note 649 + err = json.Unmarshal(data, &unmarshaled) 650 + if err != nil { 651 + t.Fatalf("JSON unmarshal failed: %v", err) 652 + } 653 + 654 + if unmarshaled.ID != note.ID { 655 + t.Errorf("Expected ID %d, got %d", note.ID, unmarshaled.ID) 656 + } 657 + if unmarshaled.Title != note.Title { 658 + t.Errorf("Expected title %s, got %s", note.Title, unmarshaled.Title) 659 + } 660 + if unmarshaled.Content != note.Content { 661 + t.Errorf("Expected content %s, got %s", note.Content, unmarshaled.Content) 662 + } 663 + if unmarshaled.Archived != note.Archived { 664 + t.Errorf("Expected archived %v, got %v", note.Archived, unmarshaled.Archived) 665 + } 666 + if unmarshaled.FilePath != note.FilePath { 667 + t.Errorf("Expected file path %s, got %s", note.FilePath, unmarshaled.FilePath) 668 + } 669 + }) 670 + }) 671 + 536 672 t.Run("Interface Implementations", func(t *testing.T) { 537 673 t.Run("All models implement Model interface", func(t *testing.T) { 538 674 var models []Model ··· 541 677 movie := &Movie{} 542 678 tvShow := &TVShow{} 543 679 book := &Book{} 680 + note := &Note{} 544 681 545 - models = append(models, task, movie, tvShow, book) 682 + models = append(models, task, movie, tvShow, book, note) 546 683 547 - if len(models) != 4 { 548 - t.Errorf("Expected 4 models, got %d", len(models)) 684 + if len(models) != 5 { 685 + t.Errorf("Expected 5 models, got %d", len(models)) 549 686 } 550 687 551 688 // Test that all models have the required methods ··· 627 764 movie := &Movie{} 628 765 tvShow := &TVShow{} 629 766 book := &Book{} 767 + note := &Note{} 630 768 631 769 // Test that zero values don't cause panics 632 770 if task.IsCompleted() || task.IsPending() || task.IsDeleted() { ··· 647 785 648 786 if book.ProgressPercent() != 0 { 649 787 t.Errorf("Zero value book should have 0%% progress, got %d%%", book.ProgressPercent()) 788 + } 789 + 790 + if note.IsArchived() { 791 + t.Error("Zero value note should not be archived") 650 792 } 651 793 }) 652 794 })
+2 -2
internal/store/migration_test.go
··· 112 112 t.Fatalf("RunMigrations failed: %v", err) 113 113 } 114 114 115 - expectedTables := []string{"migrations", "tasks", "movies", "tv_shows", "books"} 115 + expectedTables := []string{"migrations", "tasks", "movies", "tv_shows", "books", "notes"} 116 116 117 117 for _, tableName := range expectedTables { 118 118 var count int ··· 354 354 t.Error("No migrations were applied") 355 355 } 356 356 357 - tables := []string{"tasks", "movies", "tv_shows", "books"} 357 + tables := []string{"tasks", "movies", "tv_shows", "books", "notes"} 358 358 for _, table := range tables { 359 359 var count int 360 360 err = db.QueryRow("SELECT COUNT(*) FROM " + table).Scan(&count)
+2
internal/store/sql/migrations/0002_create_notes_table_down.sql
··· 1 + -- Drop notes table 2 + DROP TABLE IF EXISTS notes;
+16
internal/store/sql/migrations/0002_create_notes_table_up.sql
··· 1 + -- Notes table 2 + CREATE TABLE IF NOT EXISTS notes ( 3 + id INTEGER PRIMARY KEY AUTOINCREMENT, 4 + title TEXT NOT NULL, 5 + content TEXT NOT NULL, 6 + tags TEXT, -- JSON array 7 + archived BOOLEAN DEFAULT FALSE, 8 + created DATETIME DEFAULT CURRENT_TIMESTAMP, 9 + modified DATETIME DEFAULT CURRENT_TIMESTAMP, 10 + file_path TEXT -- optional path to source markdown file 11 + ); 12 + 13 + CREATE INDEX IF NOT EXISTS idx_notes_title ON notes(title); 14 + CREATE INDEX IF NOT EXISTS idx_notes_archived ON notes(archived); 15 + CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(created); 16 + CREATE INDEX IF NOT EXISTS idx_notes_modified ON notes(modified);