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 "github.com/stormlightlabs/noteleaf/tools"
18)
19
20var (
21 newTaskHandler = handlers.NewTaskHandler
22 newMovieHandler = handlers.NewMovieHandler
23 newTVHandler = handlers.NewTVHandler
24 newNoteHandler = handlers.NewNoteHandler
25 newBookHandler = handlers.NewBookHandler
26 newArticleHandler = handlers.NewArticleHandler
27 newPublicationHandler = handlers.NewPublicationHandler
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 root := rootCmd()
218
219 coreGroups := []CommandGroup{
220 NewTaskCommand(taskHandler),
221 NewNoteCommand(noteHandler),
222 NewPublicationCommand(publicationHandler),
223 NewArticleCommand(articleHandler),
224 }
225
226 for _, group := range coreGroups {
227 cmd := group.Create()
228 cmd.GroupID = "core"
229 root.AddCommand(cmd)
230 }
231
232 mediaCmd := &cobra.Command{
233 Use: "media",
234 Short: "Manage media queues (books, movies, TV shows)",
235 Long: `Track and manage reading lists and watch queues.
236
237Organize books, movies, and TV shows you want to consume. Search external
238databases to add items, track reading/watching progress, and maintain a
239history of completed media.`,
240 }
241 mediaCmd.GroupID = "core"
242 mediaCmd.AddCommand(NewMovieCommand(movieHandler).Create())
243 mediaCmd.AddCommand(NewTVCommand(tvHandler).Create())
244 mediaCmd.AddCommand(NewBookCommand(bookHandler).Create())
245 root.AddCommand(mediaCmd)
246
247 mgmt := []func() *cobra.Command{statusCmd, confCmd, setupCmd, resetCmd}
248 for _, cmdFunc := range mgmt {
249 cmd := cmdFunc()
250 cmd.GroupID = "management"
251 root.AddCommand(cmd)
252 }
253
254 root.AddCommand(tools.NewDocGenCommand(root))
255
256 opts := []fang.Option{
257 fang.WithVersion(version.String()),
258 fang.WithoutCompletions(),
259 fang.WithColorSchemeFunc(ui.NoteleafColorScheme),
260 }
261
262 if err := exc(context.Background(), root, opts...); err != nil {
263 return 1
264 }
265 return 0
266}
267
268func main() {
269 os.Exit(run())
270}