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/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}