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

initial commit

+970
+32
.gitignore
··· 1 + # If you prefer the allow list template instead of the deny list, see community template: 2 + # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 + # 4 + # Binaries for programs and plugins 5 + *.exe 6 + *.exe~ 7 + *.dll 8 + *.so 9 + *.dylib 10 + 11 + # Test binary, built with `go test -c` 12 + *.test 13 + 14 + # Code coverage profiles and other test artifacts 15 + *.out 16 + coverage.* 17 + *.coverprofile 18 + profile.cov 19 + 20 + # Dependency directories (remove the comment below to include it) 21 + # vendor/ 22 + 23 + # Go workspace file 24 + go.work 25 + go.work.sum 26 + 27 + # env file 28 + .env 29 + 30 + # Editor/IDE 31 + # .idea/ 32 + # .vscode/
+5
README.md
··· 1 + # Noteleaf 2 + 3 + A task & time management CLI built with Golang & Charm.sh libs. 4 + 5 + ## Development
+200
cmd/cli/main.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "log" 7 + "os" 8 + 9 + "github.com/charmbracelet/fang" 10 + "github.com/spf13/cobra" 11 + "stormlightlabs.org/noteleaf/internal/store" 12 + ) 13 + 14 + // App represents the main CLI application 15 + type App struct { 16 + db *store.Database 17 + config *store.Config 18 + } 19 + 20 + // NewApp creates a new CLI application instance 21 + func NewApp() (*App, error) { 22 + db, err := store.NewDatabase() 23 + if err != nil { 24 + return nil, fmt.Errorf("failed to initialize database: %w", err) 25 + } 26 + 27 + config, err := store.LoadConfig() 28 + if err != nil { 29 + return nil, fmt.Errorf("failed to load configuration: %w", err) 30 + } 31 + 32 + return &App{ 33 + db: db, 34 + config: config, 35 + }, nil 36 + } 37 + 38 + // Close cleans up application resources 39 + func (app *App) Close() error { 40 + if app.db != nil { 41 + return app.db.Close() 42 + } 43 + return nil 44 + } 45 + 46 + func main() { 47 + app, err := NewApp() 48 + if err != nil { 49 + log.Fatalf("Failed to initialize application: %v", err) 50 + } 51 + defer app.Close() 52 + 53 + rootCmd := &cobra.Command{ 54 + Use: "noteleaf", 55 + Short: "A TaskWarrior-inspired CLI with media queues and reading lists", 56 + } 57 + 58 + // Task management commands 59 + rootCmd.AddCommand(&cobra.Command{ 60 + Use: "add [description]", 61 + Short: "Add a new task", 62 + Args: cobra.MinimumNArgs(1), 63 + RunE: func(cmd *cobra.Command, args []string) error { 64 + description := args[0] 65 + fmt.Printf("Adding task: %s\n", description) 66 + // TODO: Implement task creation 67 + return nil 68 + }, 69 + }) 70 + 71 + rootCmd.AddCommand(&cobra.Command{ 72 + Use: "list", 73 + Short: "List tasks", 74 + Aliases: []string{"ls"}, 75 + RunE: func(cmd *cobra.Command, args []string) error { 76 + fmt.Println("Listing tasks...") 77 + // TODO: Implement task listing 78 + return nil 79 + }, 80 + }) 81 + 82 + rootCmd.AddCommand(&cobra.Command{ 83 + Use: "done [task-id]", 84 + Short: "Mark task as completed", 85 + Args: cobra.ExactArgs(1), 86 + RunE: func(cmd *cobra.Command, args []string) error { 87 + taskID := args[0] 88 + fmt.Printf("Marking task %s as done\n", taskID) 89 + // TODO: Implement task completion 90 + return nil 91 + }, 92 + }) 93 + 94 + // Movie queue commands 95 + movieCmd := &cobra.Command{ 96 + Use: "movie", 97 + Short: "Manage movie watch queue", 98 + } 99 + 100 + movieCmd.AddCommand(&cobra.Command{ 101 + Use: "add [title]", 102 + Short: "Add movie to watch queue", 103 + Args: cobra.MinimumNArgs(1), 104 + RunE: func(cmd *cobra.Command, args []string) error { 105 + title := args[0] 106 + fmt.Printf("Adding movie: %s\n", title) 107 + // TODO: Implement movie addition 108 + return nil 109 + }, 110 + }) 111 + 112 + movieCmd.AddCommand(&cobra.Command{ 113 + Use: "list", 114 + Short: "List movies in queue", 115 + RunE: func(cmd *cobra.Command, args []string) error { 116 + fmt.Println("Listing movies...") 117 + // TODO: Implement movie listing 118 + return nil 119 + }, 120 + }) 121 + 122 + rootCmd.AddCommand(movieCmd) 123 + 124 + // TV show commands 125 + tvCmd := &cobra.Command{ 126 + Use: "tv", 127 + Short: "Manage TV show watch queue", 128 + } 129 + 130 + tvCmd.AddCommand(&cobra.Command{ 131 + Use: "add [title]", 132 + Short: "Add TV show to watch queue", 133 + Args: cobra.MinimumNArgs(1), 134 + RunE: func(cmd *cobra.Command, args []string) error { 135 + title := args[0] 136 + fmt.Printf("Adding TV show: %s\n", title) 137 + // TODO: Implement TV show addition 138 + return nil 139 + }, 140 + }) 141 + 142 + tvCmd.AddCommand(&cobra.Command{ 143 + Use: "list", 144 + Short: "List TV shows in queue", 145 + RunE: func(cmd *cobra.Command, args []string) error { 146 + fmt.Println("Listing TV shows...") 147 + // TODO: Implement TV show listing 148 + return nil 149 + }, 150 + }) 151 + 152 + rootCmd.AddCommand(tvCmd) 153 + 154 + // Book commands 155 + bookCmd := &cobra.Command{ 156 + Use: "book", 157 + Short: "Manage reading list", 158 + } 159 + 160 + bookCmd.AddCommand(&cobra.Command{ 161 + Use: "add [title]", 162 + Short: "Add book to reading list", 163 + Args: cobra.MinimumNArgs(1), 164 + RunE: func(cmd *cobra.Command, args []string) error { 165 + title := args[0] 166 + fmt.Printf("Adding book: %s\n", title) 167 + // TODO: Implement book addition 168 + return nil 169 + }, 170 + }) 171 + 172 + bookCmd.AddCommand(&cobra.Command{ 173 + Use: "list", 174 + Short: "List books in reading list", 175 + RunE: func(cmd *cobra.Command, args []string) error { 176 + fmt.Println("Listing books...") 177 + // TODO: Implement book listing 178 + return nil 179 + }, 180 + }) 181 + 182 + rootCmd.AddCommand(bookCmd) 183 + 184 + // Configuration commands 185 + rootCmd.AddCommand(&cobra.Command{ 186 + Use: "config [key] [value]", 187 + Short: "Manage configuration", 188 + Args: cobra.ExactArgs(2), 189 + RunE: func(cmd *cobra.Command, args []string) error { 190 + key, value := args[0], args[1] 191 + fmt.Printf("Setting config %s = %s\n", key, value) 192 + // TODO: Implement config management 193 + return nil 194 + }, 195 + }) 196 + 197 + if err := fang.Execute(context.Background(), rootCmd, fang.WithVersion("0.1.0")); err != nil { 198 + os.Exit(1) 199 + } 200 + }
+1
cmd/handlers/handlers.go
··· 1 + package handlers
+56
go.mod
··· 1 + module stormlightlabs.org/noteleaf 2 + 3 + go 1.24.5 4 + 5 + require ( 6 + github.com/BurntSushi/toml v1.5.0 // indirect 7 + github.com/alecthomas/chroma/v2 v2.14.0 // indirect 8 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 9 + github.com/aymanbagabas/go-udiff v0.2.0 // indirect 10 + github.com/aymerick/douceur v0.2.0 // indirect 11 + github.com/charmbracelet/bubbles v0.21.0 // indirect 12 + github.com/charmbracelet/bubbletea v1.3.6 // indirect 13 + github.com/charmbracelet/colorprofile v0.3.1 // indirect 14 + github.com/charmbracelet/fang v0.3.0 // indirect 15 + github.com/charmbracelet/glamour v0.10.0 // indirect 16 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect 17 + github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 // indirect 18 + github.com/charmbracelet/log v0.4.2 // indirect 19 + github.com/charmbracelet/x/ansi v0.9.3 // indirect 20 + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 21 + github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect 22 + github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 // indirect 23 + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect 24 + github.com/charmbracelet/x/term v0.2.1 // indirect 25 + github.com/dlclark/regexp2 v1.11.0 // indirect 26 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 27 + github.com/go-logfmt/logfmt v0.6.0 // indirect 28 + github.com/gorilla/css v1.0.1 // indirect 29 + github.com/inconshreveable/mousetrap v1.1.0 // indirect 30 + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 31 + github.com/mattn/go-isatty v0.0.20 // indirect 32 + github.com/mattn/go-localereader v0.0.1 // indirect 33 + github.com/mattn/go-runewidth v0.0.16 // indirect 34 + github.com/mattn/go-sqlite3 v1.14.32 // indirect 35 + github.com/microcosm-cc/bluemonday v1.0.27 // indirect 36 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 37 + github.com/muesli/cancelreader v0.2.2 // indirect 38 + github.com/muesli/mango v0.1.0 // indirect 39 + github.com/muesli/mango-cobra v1.2.0 // indirect 40 + github.com/muesli/mango-pflag v0.1.0 // indirect 41 + github.com/muesli/reflow v0.3.0 // indirect 42 + github.com/muesli/roff v0.1.0 // indirect 43 + github.com/muesli/termenv v0.16.0 // indirect 44 + github.com/rivo/uniseg v0.4.7 // indirect 45 + github.com/spf13/cobra v1.9.1 // indirect 46 + github.com/spf13/pflag v1.0.6 // indirect 47 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 48 + github.com/yuin/goldmark v1.7.8 // indirect 49 + github.com/yuin/goldmark-emoji v1.0.5 // indirect 50 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect 51 + golang.org/x/net v0.33.0 // indirect 52 + golang.org/x/sync v0.15.0 // indirect 53 + golang.org/x/sys v0.33.0 // indirect 54 + golang.org/x/term v0.31.0 // indirect 55 + golang.org/x/text v0.24.0 // indirect 56 + )
+124
go.sum
··· 1 + github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 2 + github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 + github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= 4 + github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= 5 + github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 6 + github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 7 + github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 8 + github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 9 + github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 10 + github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 11 + github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= 12 + github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= 13 + github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= 14 + github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= 15 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= 16 + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 17 + github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= 18 + github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= 19 + github.com/charmbracelet/fang v0.3.0 h1:Be6TB+ExS8VWizTQRJgjqbJBudKrmVUet65xmFPGhaA= 20 + github.com/charmbracelet/fang v0.3.0/go.mod h1:b0ZfEXZeBds0I27/wnTfnv2UVigFDXHhrFNwQztfA0M= 21 + github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= 22 + github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= 23 + github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 24 + github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 25 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= 26 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= 27 + github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2 h1:vq2enzx1Hr3UenVefpPEf+E2xMmqtZoSHhx8IE+V8ug= 28 + github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2/go.mod h1:EJWvaCrhOhNGVZMvcjc0yVryl4qqpMs8tz0r9WyEkdQ= 29 + github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 h1:SOylT6+BQzPHEjn15TIzawBPVD0QmhKXbcb3jY0ZIKU= 30 + github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc= 31 + github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= 32 + github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= 33 + github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 34 + github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 35 + github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= 36 + github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 37 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= 38 + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 39 + github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= 40 + github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 41 + github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= 42 + github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= 43 + github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= 44 + github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 45 + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= 46 + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= 47 + github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 48 + github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 49 + github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 50 + github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= 51 + github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 52 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 53 + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 54 + github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= 55 + github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 56 + github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 57 + github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 58 + github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 59 + github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 60 + github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 61 + github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 62 + github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 63 + github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 64 + github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 65 + github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 66 + github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 67 + github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 68 + github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 69 + github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= 70 + github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 71 + github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 72 + github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 73 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 74 + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 75 + github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 76 + github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 77 + github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= 78 + github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= 79 + github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= 80 + github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= 81 + github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= 82 + github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= 83 + github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 84 + github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 85 + github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= 86 + github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= 87 + github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 88 + github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 89 + github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 90 + github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 91 + github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 92 + github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 93 + github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 94 + github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 95 + github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 96 + github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 97 + github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 98 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 99 + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 100 + github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 101 + github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= 102 + github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 103 + github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= 104 + github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= 105 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= 106 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= 107 + golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 108 + golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 109 + golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= 110 + golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 111 + golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 112 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 113 + golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 114 + golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 115 + golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 116 + golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 117 + golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 118 + golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 119 + golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 120 + golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 121 + golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 122 + golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 123 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 124 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+164
internal/models/task.go
··· 1 + package models 2 + 3 + import ( 4 + "encoding/json" 5 + "time" 6 + ) 7 + 8 + // Task represents a task item with TaskWarrior-inspired fields 9 + type Task struct { 10 + ID int64 `json:"id"` 11 + UUID string `json:"uuid"` 12 + Description string `json:"description"` 13 + Status string `json:"status"` // pending, completed, deleted 14 + Priority string `json:"priority,omitempty"` // A-Z or empty 15 + Project string `json:"project,omitempty"` 16 + Tags []string `json:"tags,omitempty"` 17 + Due *time.Time `json:"due,omitempty"` 18 + Entry time.Time `json:"entry"` 19 + Modified time.Time `json:"modified"` 20 + End *time.Time `json:"end,omitempty"` // completion time 21 + Start *time.Time `json:"start,omitempty"` // when task was started 22 + Annotations []string `json:"annotations,omitempty"` 23 + } 24 + 25 + // Movie represents a movie in the watch queue 26 + type Movie struct { 27 + ID int64 `json:"id"` 28 + Title string `json:"title"` 29 + Year int `json:"year,omitempty"` 30 + Status string `json:"status"` // queued, watched, removed 31 + Rating float64 `json:"rating,omitempty"` 32 + Notes string `json:"notes,omitempty"` 33 + Added time.Time `json:"added"` 34 + Watched *time.Time `json:"watched,omitempty"` 35 + } 36 + 37 + // TVShow represents a TV show in the watch queue 38 + type TVShow struct { 39 + ID int64 `json:"id"` 40 + Title string `json:"title"` 41 + Season int `json:"season,omitempty"` 42 + Episode int `json:"episode,omitempty"` 43 + Status string `json:"status"` // queued, watching, watched, removed 44 + Rating float64 `json:"rating,omitempty"` 45 + Notes string `json:"notes,omitempty"` 46 + Added time.Time `json:"added"` 47 + LastWatched *time.Time `json:"last_watched,omitempty"` 48 + } 49 + 50 + // Book represents a book in the reading list 51 + type Book struct { 52 + ID int64 `json:"id"` 53 + Title string `json:"title"` 54 + Author string `json:"author,omitempty"` 55 + Status string `json:"status"` // queued, reading, finished, removed 56 + Progress int `json:"progress"` // percentage 0-100 57 + Pages int `json:"pages,omitempty"` 58 + Rating float64 `json:"rating,omitempty"` 59 + Notes string `json:"notes,omitempty"` 60 + Added time.Time `json:"added"` 61 + Started *time.Time `json:"started,omitempty"` 62 + Finished *time.Time `json:"finished,omitempty"` 63 + } 64 + 65 + // MarshalTags converts tags slice to JSON string for database storage 66 + func (t *Task) MarshalTags() (string, error) { 67 + if len(t.Tags) == 0 { 68 + return "", nil 69 + } 70 + data, err := json.Marshal(t.Tags) 71 + return string(data), err 72 + } 73 + 74 + // UnmarshalTags converts JSON string from database to tags slice 75 + func (t *Task) UnmarshalTags(data string) error { 76 + if data == "" { 77 + t.Tags = nil 78 + return nil 79 + } 80 + return json.Unmarshal([]byte(data), &t.Tags) 81 + } 82 + 83 + // MarshalAnnotations converts annotations slice to JSON string for database storage 84 + func (t *Task) MarshalAnnotations() (string, error) { 85 + if len(t.Annotations) == 0 { 86 + return "", nil 87 + } 88 + data, err := json.Marshal(t.Annotations) 89 + return string(data), err 90 + } 91 + 92 + // UnmarshalAnnotations converts JSON string from database to annotations slice 93 + func (t *Task) UnmarshalAnnotations(data string) error { 94 + if data == "" { 95 + t.Annotations = nil 96 + return nil 97 + } 98 + return json.Unmarshal([]byte(data), &t.Annotations) 99 + } 100 + 101 + // IsCompleted returns true if the task is marked as completed 102 + func (t *Task) IsCompleted() bool { 103 + return t.Status == "completed" 104 + } 105 + 106 + // IsPending returns true if the task is pending 107 + func (t *Task) IsPending() bool { 108 + return t.Status == "pending" 109 + } 110 + 111 + // IsDeleted returns true if the task is deleted 112 + func (t *Task) IsDeleted() bool { 113 + return t.Status == "deleted" 114 + } 115 + 116 + // HasPriority returns true if the task has a priority set 117 + func (t *Task) HasPriority() bool { 118 + return t.Priority != "" 119 + } 120 + 121 + // IsWatched returns true if the movie has been watched 122 + func (m *Movie) IsWatched() bool { 123 + return m.Status == "watched" 124 + } 125 + 126 + // IsQueued returns true if the movie is in the queue 127 + func (m *Movie) IsQueued() bool { 128 + return m.Status == "queued" 129 + } 130 + 131 + // IsWatching returns true if the TV show is currently being watched 132 + func (tv *TVShow) IsWatching() bool { 133 + return tv.Status == "watching" 134 + } 135 + 136 + // IsWatched returns true if the TV show has been watched 137 + func (tv *TVShow) IsWatched() bool { 138 + return tv.Status == "watched" 139 + } 140 + 141 + // IsQueued returns true if the TV show is in the queue 142 + func (tv *TVShow) IsQueued() bool { 143 + return tv.Status == "queued" 144 + } 145 + 146 + // IsReading returns true if the book is currently being read 147 + func (b *Book) IsReading() bool { 148 + return b.Status == "reading" 149 + } 150 + 151 + // IsFinished returns true if the book has been finished 152 + func (b *Book) IsFinished() bool { 153 + return b.Status == "finished" 154 + } 155 + 156 + // IsQueued returns true if the book is in the queue 157 + func (b *Book) IsQueued() bool { 158 + return b.Status == "queued" 159 + } 160 + 161 + // ProgressPercent returns the reading progress as a percentage 162 + func (b *Book) ProgressPercent() int { 163 + return b.Progress 164 + }
+109
internal/store/config.go
··· 1 + package store 2 + 3 + import ( 4 + "fmt" 5 + "os" 6 + "path/filepath" 7 + 8 + "github.com/BurntSushi/toml" 9 + ) 10 + 11 + // Config holds application configuration 12 + type Config struct { 13 + // Database settings 14 + DatabasePath string `toml:"database_path,omitempty"` 15 + 16 + // Display settings 17 + DateFormat string `toml:"date_format"` 18 + ColorScheme string `toml:"color_scheme"` 19 + DefaultView string `toml:"default_view"` 20 + 21 + // Task settings 22 + DefaultPriority string `toml:"default_priority,omitempty"` 23 + AutoArchive bool `toml:"auto_archive"` 24 + 25 + // Sync settings 26 + SyncEnabled bool `toml:"sync_enabled"` 27 + SyncEndpoint string `toml:"sync_endpoint,omitempty"` 28 + SyncToken string `toml:"sync_token,omitempty"` 29 + 30 + // Export settings 31 + ExportFormat string `toml:"export_format"` 32 + 33 + // Media settings 34 + MovieAPIKey string `toml:"movie_api_key,omitempty"` 35 + BookAPIKey string `toml:"book_api_key,omitempty"` 36 + } 37 + 38 + // DefaultConfig returns a configuration with sensible defaults 39 + func DefaultConfig() *Config { 40 + return &Config{ 41 + DateFormat: "2006-01-02", 42 + ColorScheme: "default", 43 + DefaultView: "list", 44 + AutoArchive: false, 45 + SyncEnabled: false, 46 + ExportFormat: "json", 47 + } 48 + } 49 + 50 + // LoadConfig loads configuration from the config directory 51 + func LoadConfig() (*Config, error) { 52 + configDir, err := GetConfigDir() 53 + if err != nil { 54 + return nil, fmt.Errorf("failed to get config directory: %w", err) 55 + } 56 + 57 + configPath := filepath.Join(configDir, ".noteleaf.conf.toml") 58 + 59 + // If config file doesn't exist, create it with defaults 60 + if _, err := os.Stat(configPath); os.IsNotExist(err) { 61 + config := DefaultConfig() 62 + if err := SaveConfig(config); err != nil { 63 + return nil, fmt.Errorf("failed to create default config: %w", err) 64 + } 65 + return config, nil 66 + } 67 + 68 + data, err := os.ReadFile(configPath) 69 + if err != nil { 70 + return nil, fmt.Errorf("failed to read config file: %w", err) 71 + } 72 + 73 + config := DefaultConfig() 74 + if err := toml.Unmarshal(data, config); err != nil { 75 + return nil, fmt.Errorf("failed to parse config file: %w", err) 76 + } 77 + 78 + return config, nil 79 + } 80 + 81 + // SaveConfig saves the configuration to the config directory 82 + func SaveConfig(config *Config) error { 83 + configDir, err := GetConfigDir() 84 + if err != nil { 85 + return fmt.Errorf("failed to get config directory: %w", err) 86 + } 87 + 88 + configPath := filepath.Join(configDir, ".noteleaf.conf.toml") 89 + 90 + data, err := toml.Marshal(config) 91 + if err != nil { 92 + return fmt.Errorf("failed to marshal config: %w", err) 93 + } 94 + 95 + if err := os.WriteFile(configPath, data, 0644); err != nil { 96 + return fmt.Errorf("failed to write config file: %w", err) 97 + } 98 + 99 + return nil 100 + } 101 + 102 + // GetConfigPath returns the path to the configuration file 103 + func GetConfigPath() (string, error) { 104 + configDir, err := GetConfigDir() 105 + if err != nil { 106 + return "", err 107 + } 108 + return filepath.Join(configDir, ".noteleaf.conf.toml"), nil 109 + }
+178
internal/store/database.go
··· 1 + package store 2 + 3 + import ( 4 + "database/sql" 5 + "embed" 6 + "fmt" 7 + "os" 8 + "path/filepath" 9 + "runtime" 10 + "sort" 11 + "strings" 12 + 13 + _ "github.com/mattn/go-sqlite3" 14 + ) 15 + 16 + //go:embed sql/migrations 17 + var migrationFiles embed.FS 18 + 19 + // Database wraps sql.DB with application-specific methods 20 + type Database struct { 21 + *sql.DB 22 + path string 23 + } 24 + 25 + // GetConfigDir returns the appropriate configuration directory based on the OS 26 + func GetConfigDir() (string, error) { 27 + var configDir string 28 + 29 + switch runtime.GOOS { 30 + case "windows": 31 + appData := os.Getenv("APPDATA") 32 + if appData == "" { 33 + return "", fmt.Errorf("APPDATA environment variable not set") 34 + } 35 + configDir = filepath.Join(appData, "noteleaf") 36 + default: // Unix-like systems (Linux, macOS, BSD, etc.) 37 + xdgConfigHome := os.Getenv("XDG_CONFIG_HOME") 38 + if xdgConfigHome == "" { 39 + homeDir, err := os.UserHomeDir() 40 + if err != nil { 41 + return "", fmt.Errorf("failed to get user home directory: %w", err) 42 + } 43 + xdgConfigHome = filepath.Join(homeDir, ".config") 44 + } 45 + configDir = filepath.Join(xdgConfigHome, "noteleaf") 46 + } 47 + 48 + // Create the directory if it doesn't exist 49 + if err := os.MkdirAll(configDir, 0755); err != nil { 50 + return "", fmt.Errorf("failed to create config directory: %w", err) 51 + } 52 + 53 + return configDir, nil 54 + } 55 + 56 + // NewDatabase creates and initializes a new database connection 57 + func NewDatabase() (*Database, error) { 58 + configDir, err := GetConfigDir() 59 + if err != nil { 60 + return nil, fmt.Errorf("failed to get config directory: %w", err) 61 + } 62 + 63 + dbPath := filepath.Join(configDir, "noteleaf.db") 64 + 65 + db, err := sql.Open("sqlite3", dbPath) 66 + if err != nil { 67 + return nil, fmt.Errorf("failed to open database: %w", err) 68 + } 69 + 70 + // Enable foreign keys and WAL mode for better performance 71 + if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil { 72 + db.Close() 73 + return nil, fmt.Errorf("failed to enable foreign keys: %w", err) 74 + } 75 + 76 + if _, err := db.Exec("PRAGMA journal_mode = WAL"); err != nil { 77 + db.Close() 78 + return nil, fmt.Errorf("failed to enable WAL mode: %w", err) 79 + } 80 + 81 + database := &Database{ 82 + DB: db, 83 + path: dbPath, 84 + } 85 + 86 + // Run migrations 87 + if err := database.runMigrations(); err != nil { 88 + db.Close() 89 + return nil, fmt.Errorf("failed to run migrations: %w", err) 90 + } 91 + 92 + return database, nil 93 + } 94 + 95 + // runMigrations applies all pending migrations 96 + func (db *Database) runMigrations() error { 97 + // Get all migration files 98 + entries, err := migrationFiles.ReadDir("../sql/migrations") 99 + if err != nil { 100 + return fmt.Errorf("failed to read migrations directory: %w", err) 101 + } 102 + 103 + // Filter and sort up migrations 104 + var upMigrations []string 105 + for _, entry := range entries { 106 + if strings.HasSuffix(entry.Name(), "_up.sql") { 107 + upMigrations = append(upMigrations, entry.Name()) 108 + } 109 + } 110 + sort.Strings(upMigrations) 111 + 112 + // Apply migrations in order 113 + for _, migrationFile := range upMigrations { 114 + version := extractVersionFromFilename(migrationFile) 115 + 116 + // Check if migration already applied 117 + var count int 118 + err := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='migrations'").Scan(&count) 119 + if err != nil { 120 + return fmt.Errorf("failed to check migrations table: %w", err) 121 + } 122 + 123 + // If migrations table doesn't exist and this isn't migration 0000, skip 124 + if count == 0 && version != "0000" { 125 + continue 126 + } 127 + 128 + // Check if this specific migration was applied (only if migrations table exists) 129 + if count > 0 { 130 + var applied int 131 + err = db.QueryRow("SELECT COUNT(*) FROM migrations WHERE version = ?", version).Scan(&applied) 132 + if err != nil { 133 + return fmt.Errorf("failed to check migration %s: %w", version, err) 134 + } 135 + if applied > 0 { 136 + continue // Skip already applied migration 137 + } 138 + } 139 + 140 + // Read and execute migration 141 + content, err := migrationFiles.ReadFile("../sql/migrations/" + migrationFile) 142 + if err != nil { 143 + return fmt.Errorf("failed to read migration %s: %w", migrationFile, err) 144 + } 145 + 146 + if _, err := db.Exec(string(content)); err != nil { 147 + return fmt.Errorf("failed to execute migration %s: %w", migrationFile, err) 148 + } 149 + 150 + // Record migration as applied (only if migrations table exists) 151 + if count > 0 || version == "0000" { 152 + if _, err := db.Exec("INSERT INTO migrations (version) VALUES (?)", version); err != nil { 153 + return fmt.Errorf("failed to record migration %s: %w", version, err) 154 + } 155 + } 156 + } 157 + 158 + return nil 159 + } 160 + 161 + // extractVersionFromFilename extracts the 4-digit version from a migration filename 162 + func extractVersionFromFilename(filename string) string { 163 + parts := strings.Split(filename, "_") 164 + if len(parts) > 0 { 165 + return parts[0] 166 + } 167 + return "" 168 + } 169 + 170 + // GetPath returns the database file path 171 + func (db *Database) GetPath() string { 172 + return db.path 173 + } 174 + 175 + // Close closes the database connection 176 + func (db *Database) Close() error { 177 + return db.DB.Close() 178 + }
+1
internal/store/sql/migrations/0000_create_migrations_table_down.sql
··· 1 + DROP TABLE IF EXISTS migrations;
+5
internal/store/sql/migrations/0000_create_migrations_table_up.sql
··· 1 + CREATE TABLE IF NOT EXISTS migrations ( 2 + id INTEGER PRIMARY KEY, 3 + version TEXT UNIQUE NOT NULL, 4 + applied_at DATETIME DEFAULT CURRENT_TIMESTAMP 5 + );
+23
internal/store/sql/migrations/0001_create_all_tables_down.sql
··· 1 + -- Drop books table and indexes 2 + DROP INDEX IF EXISTS idx_books_title; 3 + DROP INDEX IF EXISTS idx_books_author; 4 + DROP INDEX IF EXISTS idx_books_status; 5 + DROP TABLE IF EXISTS books; 6 + 7 + -- Drop TV shows table and indexes 8 + DROP INDEX IF EXISTS idx_tv_shows_season_episode; 9 + DROP INDEX IF EXISTS idx_tv_shows_title; 10 + DROP INDEX IF EXISTS idx_tv_shows_status; 11 + DROP TABLE IF EXISTS tv_shows; 12 + 13 + -- Drop movies table and indexes 14 + DROP INDEX IF EXISTS idx_movies_title; 15 + DROP INDEX IF EXISTS idx_movies_status; 16 + DROP TABLE IF EXISTS movies; 17 + 18 + -- Drop tasks table and indexes 19 + DROP INDEX IF EXISTS idx_tasks_uuid; 20 + DROP INDEX IF EXISTS idx_tasks_due; 21 + DROP INDEX IF EXISTS idx_tasks_project; 22 + DROP INDEX IF EXISTS idx_tasks_status; 23 + DROP TABLE IF EXISTS tasks;
+72
internal/store/sql/migrations/0001_create_all_tables_up.sql
··· 1 + -- Tasks table 2 + CREATE TABLE IF NOT EXISTS tasks ( 3 + id INTEGER PRIMARY KEY AUTOINCREMENT, 4 + uuid TEXT UNIQUE NOT NULL, 5 + description TEXT NOT NULL, 6 + status TEXT DEFAULT 'pending', 7 + priority TEXT, 8 + project TEXT, 9 + tags TEXT, -- JSON array 10 + due DATETIME, 11 + entry DATETIME DEFAULT CURRENT_TIMESTAMP, 12 + modified DATETIME DEFAULT CURRENT_TIMESTAMP, 13 + end DATETIME, 14 + start DATETIME, 15 + annotations TEXT -- JSON array 16 + ); 17 + 18 + CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); 19 + CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project); 20 + CREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks(due); 21 + CREATE INDEX IF NOT EXISTS idx_tasks_uuid ON tasks(uuid); 22 + 23 + -- Movies table 24 + CREATE TABLE IF NOT EXISTS movies ( 25 + id INTEGER PRIMARY KEY AUTOINCREMENT, 26 + title TEXT NOT NULL, 27 + year INTEGER, 28 + status TEXT DEFAULT 'queued', 29 + rating REAL, 30 + notes TEXT, 31 + added DATETIME DEFAULT CURRENT_TIMESTAMP, 32 + watched DATETIME 33 + ); 34 + 35 + CREATE INDEX IF NOT EXISTS idx_movies_status ON movies(status); 36 + CREATE INDEX IF NOT EXISTS idx_movies_title ON movies(title); 37 + 38 + -- TV Shows table 39 + CREATE TABLE IF NOT EXISTS tv_shows ( 40 + id INTEGER PRIMARY KEY AUTOINCREMENT, 41 + title TEXT NOT NULL, 42 + season INTEGER, 43 + episode INTEGER, 44 + status TEXT DEFAULT 'queued', 45 + rating REAL, 46 + notes TEXT, 47 + added DATETIME DEFAULT CURRENT_TIMESTAMP, 48 + last_watched DATETIME 49 + ); 50 + 51 + CREATE INDEX IF NOT EXISTS idx_tv_shows_status ON tv_shows(status); 52 + CREATE INDEX IF NOT EXISTS idx_tv_shows_title ON tv_shows(title); 53 + CREATE INDEX IF NOT EXISTS idx_tv_shows_season_episode ON tv_shows(title, season, episode); 54 + 55 + -- Books table 56 + CREATE TABLE IF NOT EXISTS books ( 57 + id INTEGER PRIMARY KEY AUTOINCREMENT, 58 + title TEXT NOT NULL, 59 + author TEXT, 60 + status TEXT DEFAULT 'queued', 61 + progress INTEGER DEFAULT 0, 62 + pages INTEGER, 63 + rating REAL, 64 + notes TEXT, 65 + added DATETIME DEFAULT CURRENT_TIMESTAMP, 66 + started DATETIME, 67 + finished DATETIME 68 + ); 69 + 70 + CREATE INDEX IF NOT EXISTS idx_books_status ON books(status); 71 + CREATE INDEX IF NOT EXISTS idx_books_author ON books(author); 72 + CREATE INDEX IF NOT EXISTS idx_books_title ON books(title);