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