cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 277 lines 7.5 kB view raw
1package main 2 3import ( 4 "context" 5 "fmt" 6 "os" 7 "strings" 8 9 "github.com/charmbracelet/fang" 10 "github.com/charmbracelet/log" 11 "github.com/spf13/cobra" 12 "github.com/stormlightlabs/noteleaf/internal/handlers" 13 "github.com/stormlightlabs/noteleaf/internal/store" 14 "github.com/stormlightlabs/noteleaf/internal/ui" 15 "github.com/stormlightlabs/noteleaf/internal/utils" 16 "github.com/stormlightlabs/noteleaf/internal/version" 17) 18 19var ( 20 newTaskHandler = handlers.NewTaskHandler 21 newMovieHandler = handlers.NewMovieHandler 22 newTVHandler = handlers.NewTVHandler 23 newNoteHandler = handlers.NewNoteHandler 24 newBookHandler = handlers.NewBookHandler 25 newArticleHandler = handlers.NewArticleHandler 26 newPublicationHandler = handlers.NewPublicationHandler 27 newDocumentHandler = handlers.NewDocumentHandler 28 exc = fang.Execute 29) 30 31// App represents the main CLI application 32type App struct { 33 db *store.Database 34 config *store.Config 35} 36 37// NewApp creates a new CLI application instance ([App]) 38func NewApp() (*App, error) { 39 db, err := store.NewDatabase() 40 if err != nil { 41 return nil, fmt.Errorf("failed to initialize database: %w", err) 42 } 43 44 config, err := store.LoadConfig() 45 if err != nil { 46 return nil, fmt.Errorf("failed to load configuration: %w", err) 47 } 48 49 return &App{db, config}, nil 50} 51 52// Close cleans up application resources 53func (app *App) Close() error { 54 if app.db != nil { 55 return app.db.Close() 56 } 57 return nil 58} 59 60func statusCmd() *cobra.Command { 61 return &cobra.Command{ 62 Use: "status", 63 Short: "Show application status and configuration", 64 Long: `Display comprehensive application status information. 65 66Shows database location, configuration file path, data directories, and current 67settings. Use this command to verify your noteleaf installation and diagnose 68configuration issues.`, 69 RunE: func(cmd *cobra.Command, args []string) error { 70 return handlers.Status(cmd.Context(), args, cmd.OutOrStdout()) 71 }, 72 } 73} 74 75func resetCmd() *cobra.Command { 76 return &cobra.Command{ 77 Use: "reset", 78 Short: "Reset the application (removes all data)", 79 Long: `Remove all application data and return to initial state. 80 81This command deletes the database, all media files, notes, and articles. The 82configuration file is preserved. Use with caution as this operation cannot be 83undone. You will be prompted for confirmation before deletion proceeds.`, 84 RunE: func(cmd *cobra.Command, args []string) error { 85 return handlers.Reset(cmd.Context(), args) 86 }, 87 } 88} 89 90func rootCmd() *cobra.Command { 91 root := &cobra.Command{ 92 Use: "noteleaf", 93 Short: "A TaskWarrior-inspired CLI with notes, media queues and reading lists", 94 Long: `noteleaf - personal information manager for the command line 95 96A comprehensive CLI tool for managing tasks, notes, articles, and media queues. 97Inspired by TaskWarrior, noteleaf combines todo management with reading lists, 98watch queues, and a personal knowledge base. 99 100Core features include hierarchical tasks with dependencies, recurring tasks, 101time tracking, markdown notes with tags, article archiving, and media queue 102management for books, movies, and TV shows.`, 103 RunE: func(c *cobra.Command, args []string) error { 104 if len(args) == 0 { 105 return c.Help() 106 } 107 108 output := strings.Join(args, " ") 109 fmt.Fprintln(c.OutOrStdout(), output) 110 return nil 111 }, 112 } 113 114 root.SetHelpCommand(&cobra.Command{Hidden: true}) 115 cobra.EnableCommandSorting = false 116 117 root.AddGroup(&cobra.Group{ID: "core", Title: "Core:"}) 118 root.AddGroup(&cobra.Group{ID: "management", Title: "Manage:"}) 119 return root 120} 121 122func setupCmd() *cobra.Command { 123 handler, err := handlers.NewSeedHandler() 124 if err != nil { 125 log.Fatalf("failed to instantiate seed handler: %v", err) 126 } 127 128 root := &cobra.Command{ 129 Use: "setup", 130 Short: "Initialize and manage application setup", 131 Long: `Initialize noteleaf for first use. 132 133Creates the database, configuration file, and required data directories. Run 134this command after installing noteleaf or when setting up a new environment. 135Safe to run multiple times as it will skip existing resources.`, 136 RunE: func(c *cobra.Command, args []string) error { 137 return handlers.Setup(c.Context(), args) 138 }, 139 } 140 141 seedCmd := &cobra.Command{ 142 Use: "seed", 143 Short: "Populate database with test data", 144 Long: "Add sample tasks, books, and notes to the database for testing and demonstration purposes", 145 RunE: func(c *cobra.Command, args []string) error { 146 force, _ := c.Flags().GetBool("force") 147 return handler.Seed(c.Context(), force) 148 }, 149 } 150 seedCmd.Flags().BoolP("force", "f", false, "Clear existing data and re-seed") 151 152 root.AddCommand(seedCmd) 153 return root 154} 155 156func confCmd() *cobra.Command { 157 handler, err := handlers.NewConfigHandler() 158 if err != nil { 159 log.Fatalf("failed to create config handler: %v", err) 160 } 161 return NewConfigCommand(handler).Create() 162} 163 164func run() int { 165 logger := utils.NewLogger("info", "text") 166 utils.Logger = logger 167 168 app, err := NewApp() 169 if err != nil { 170 logger.Error("Failed to initialize application", "error", err) 171 return 1 172 } 173 defer app.Close() 174 175 taskHandler, err := newTaskHandler() 176 if err != nil { 177 log.Error("failed to create task handler", "err", err) 178 return 1 179 } 180 181 movieHandler, err := newMovieHandler() 182 if err != nil { 183 log.Error("failed to create movie handler", "err", err) 184 return 1 185 } 186 187 tvHandler, err := newTVHandler() 188 if err != nil { 189 log.Error("failed to create TV handler", "err", err) 190 return 1 191 } 192 193 noteHandler, err := newNoteHandler() 194 if err != nil { 195 log.Error("failed to create note handler", "err", err) 196 return 1 197 } 198 199 bookHandler, err := newBookHandler() 200 if err != nil { 201 log.Error("failed to create book handler", "err", err) 202 return 1 203 } 204 205 articleHandler, err := newArticleHandler() 206 if err != nil { 207 log.Error("failed to create article handler", "err", err) 208 return 1 209 } 210 211 publicationHandler, err := newPublicationHandler() 212 if err != nil { 213 log.Error("failed to create publication handler", "err", err) 214 return 1 215 } 216 217 documentHandler, err := newDocumentHandler() 218 if err != nil { 219 log.Error("failed to create document handler", "err", err) 220 return 1 221 } 222 223 root := rootCmd() 224 225 coreGroups := []CommandGroup{ 226 NewTaskCommand(taskHandler), 227 NewNoteCommand(noteHandler), 228 NewPublicationCommand(publicationHandler), 229 NewArticleCommand(articleHandler), 230 NewSearchCommand(documentHandler), 231 } 232 233 for _, group := range coreGroups { 234 cmd := group.Create() 235 cmd.GroupID = "core" 236 root.AddCommand(cmd) 237 } 238 239 mediaCmd := &cobra.Command{ 240 Use: "media", 241 Short: "Manage media queues (books, movies, TV shows)", 242 Long: `Track and manage reading lists and watch queues. 243 244Organize books, movies, and TV shows you want to consume. Search external 245databases to add items, track reading/watching progress, and maintain a 246history of completed media.`, 247 } 248 mediaCmd.GroupID = "core" 249 mediaCmd.AddCommand(NewMovieCommand(movieHandler).Create()) 250 mediaCmd.AddCommand(NewTVCommand(tvHandler).Create()) 251 mediaCmd.AddCommand(NewBookCommand(bookHandler).Create()) 252 root.AddCommand(mediaCmd) 253 254 mgmt := []func() *cobra.Command{statusCmd, confCmd, setupCmd, resetCmd} 255 for _, cmdFunc := range mgmt { 256 cmd := cmdFunc() 257 cmd.GroupID = "management" 258 root.AddCommand(cmd) 259 } 260 261 registerTools(root) 262 263 opts := []fang.Option{ 264 fang.WithVersion(version.String()), 265 fang.WithoutCompletions(), 266 fang.WithColorSchemeFunc(ui.NoteleafColorScheme), 267 } 268 269 if err := exc(context.Background(), root, opts...); err != nil { 270 return 1 271 } 272 return 0 273} 274 275func main() { 276 os.Exit(run()) 277}