···11+NOTELEAF(1) User Commands NOTELEAF(1)
22+33+NAME
44+ noteleaf - manage tasks, notes, books, movies, TV shows, and saved
55+ articles from the command line
66+77+SYNOPSIS
88+ noteleaf [--help] [--version] <command> [<args>]
99+1010+DESCRIPTION
1111+ noteleaf is a terminal-first productivity tool written in Go. It combines
1212+ task management (with time tracking), note-taking, and media queues
1313+ (books, movies, TV shows, and saved articles) into a single command-line interface.
1414+1515+ The design borrows from Taskwarrior and todo.txt but extends them with
1616+ features such as:
1717+ - Interactive TUIs for browsing notes and tasks
1818+ - Article parsing and storage
1919+ - Unified commands across different domains
2020+ - Time tracking with start/stop and timesheet summaries
2121+2222+ Subcommands are grouped by domain: task, note, book, movie, tv, and article.
2323+ Each group has its own subcommands and options.
2424+2525+OPTIONS
2626+ --help, -h
2727+ Show help for noteleaf or any subcommand.
2828+2929+ --version, -v
3030+ Print the current version and exit.
3131+3232+COMMANDS
3333+ General
3434+ help [command]
3535+ Show help for a command or subcommand.
3636+3737+ version
3838+ Print the program version.
3939+4040+ TASK COMMANDS
4141+ noteleaf task add [description]
4242+ Add a new task.
4343+ Flags:
4444+ -p, --priority <value> Set task priority
4545+ --project <name> Set project
4646+ -c, --context <name> Set context
4747+ -d, --due YYYY-MM-DD Set due date
4848+ -t, --tags tag1,tag2 Add tags
4949+5050+ noteleaf task list
5151+ List tasks.
5252+ Flags:
5353+ -i, --interactive Force interactive mode
5454+ --static Print static list
5555+ -a, --all Show all tasks
5656+ --status <status> Filter by status
5757+ --priority <value> Filter by priority
5858+ --project <name> Filter by project
5959+ --context <name> Filter by context
6060+6161+ noteleaf task view <id>
6262+ View task details.
6363+ Flags:
6464+ --format detailed|brief Output format
6565+ --json Print JSON
6666+ --no-metadata Hide metadata
6767+6868+ noteleaf task update <id>
6969+ Update task fields.
7070+ Flags:
7171+ --description <text>
7272+ --status <status>
7373+ -p, --priority <value>
7474+ --project <name>
7575+ -c, --context <name>
7676+ -d, --due YYYY-MM-DD
7777+ --add-tag tag
7878+ --remove-tag tag
7979+8080+ noteleaf task edit <id>
8181+ Interactive edit with status/priority toggles.
8282+ Alias: e
8383+8484+ noteleaf task delete <id>
8585+ Delete task permanently.
8686+8787+ noteleaf task projects
8888+ List projects.
8989+ Flags: --static, --todo-txt
9090+9191+ noteleaf task tags
9292+ List tags.
9393+ Flags: --static
9494+9595+ noteleaf task contexts
9696+ List contexts.
9797+ Flags: --static, --todo-txt
9898+9999+ noteleaf task done <id>
100100+ Mark task as completed.
101101+ Alias: complete
102102+103103+ noteleaf task start <id>
104104+ Start time tracking for a task.
105105+ Flags: -n, --note <text> Add note to entry
106106+107107+ noteleaf task stop <id>
108108+ Stop time tracking for a task.
109109+110110+ noteleaf task timesheet
111111+ Show time tracking summary.
112112+ Flags:
113113+ -d, --days <n> Number of days (default 7)
114114+ -t, --task <id> Timesheet for specific task
115115+116116+ MOVIE COMMANDS
117117+ noteleaf movie add [query...]
118118+ Search and add a movie to the watch queue.
119119+ Flags: -i, --interactive
120120+121121+ noteleaf movie list [--all|--watched|--queued]
122122+ List movies by status.
123123+124124+ noteleaf movie watched <id>
125125+ Mark movie as watched.
126126+ Alias: seen
127127+128128+ noteleaf movie remove <id>
129129+ Remove from queue.
130130+ Alias: rm
131131+132132+ TV COMMANDS
133133+ noteleaf tv add [query...]
134134+ Search and add a TV show to the watch queue.
135135+ Flags: -i, --interactive
136136+137137+ noteleaf tv list [--all|--queued|--watching|--watched]
138138+ List TV shows by status.
139139+140140+ noteleaf tv watching <id>
141141+ Mark as currently watching.
142142+143143+ noteleaf tv watched <id>
144144+ Mark as watched.
145145+ Alias: seen
146146+147147+ noteleaf tv remove <id>
148148+ Remove show from queue.
149149+ Alias: rm
150150+151151+ BOOK COMMANDS
152152+ noteleaf book add [query...]
153153+ Search and add a book to the reading list.
154154+ Flags: -i, --interactive
155155+156156+ noteleaf book list [--all|--reading|--finished|--queued]
157157+ Show reading list.
158158+159159+ noteleaf book reading <id>
160160+ Mark book as currently reading.
161161+162162+ noteleaf book finished <id>
163163+ Mark book as finished.
164164+ Alias: read
165165+166166+ noteleaf book remove <id>
167167+ Remove from reading list.
168168+ Alias: rm
169169+170170+ noteleaf book progress <id> <percent>
171171+ Update reading progress percentage (0-100).
172172+173173+ noteleaf book update <id> <status>
174174+ Update status (queued|reading|finished|removed).
175175+176176+ NOTE COMMANDS
177177+ noteleaf note create [title] [content...]
178178+ Create a note.
179179+ Aliases: new
180180+ Flags:
181181+ -i, --interactive Open interactive editor
182182+ -e, --editor Open note in editor
183183+ -f, --file <path> Create from markdown file
184184+185185+ noteleaf note list
186186+ List notes (interactive TUI or static).
187187+ Aliases: ls
188188+ Flags:
189189+ -a, --archived Include archived
190190+ -s, --static Static list
191191+ --tags tag1,tag2 Filter by tags
192192+193193+ noteleaf note read <id>
194194+ Display note content.
195195+ Alias: view
196196+197197+ noteleaf note edit <id>
198198+ Edit note in editor.
199199+200200+ noteleaf note remove <id>
201201+ Remove note permanently.
202202+ Aliases: rm, delete, del
203203+204204+ ARTICLE COMMANDS
205205+ noteleaf article add <url>
206206+ Parse and save article from URL.
207207+208208+ noteleaf article list [query]
209209+ List saved articles.
210210+ Aliases: ls
211211+ Flags:
212212+ --author <name>
213213+ -l, --limit <n>
214214+215215+ noteleaf article view <id>
216216+ Show article metadata and preview.
217217+ Alias: show
218218+219219+ noteleaf article read <id>
220220+ Display full content as Markdown.
221221+222222+ noteleaf article remove <id>
223223+ Remove article and associated files.
224224+ Aliases: rm, delete
225225+226226+EXIT STATUS
227227+ noteleaf returns 0 on success.
228228+ Non-zero exit status indicates an error.
229229+230230+EXAMPLES
231231+ Add and list tasks:
232232+ noteleaf task add "Write blog post" -p H --project blog --due 2025-10-15
233233+ noteleaf task list --project blog
234234+235235+ Mark complete:
236236+ noteleaf task done 42
237237+238238+ Track media:
239239+ noteleaf book add "The Name of the Wind"
240240+ noteleaf movie add "Blade Runner" -i
241241+ noteleaf tv list --watching
242242+243243+ Work with notes:
244244+ noteleaf note create "Ideas" "sketch out product roadmap"
245245+ noteleaf note list --tags=work
246246+247247+ Save an article:
248248+ noteleaf article add https://example.com/post
249249+ noteleaf article list --author "Ada Lovelace"
250250+251251+FILES
252252+ (TODO: configuration and data file paths once implemented)
253253+254254+SEE ALSO
255255+ Taskwarrior(1), todo.txt(5), git(1), neovim(1), rsync(1)
256256+257257+AUTHOR
258258+ Owais @ Stormlight Labs <https://github.com/stormlightlabs/noteleaf>
259259+260260+BUGS
261261+ Please report issues at: https://github.com/stormlightlabs/noteleaf/issues
262262+263263+COPYRIGHT
264264+ Copyright (c) 2025 Stormlight Labs, LLC.
265265+ Licensed under the MIT License.
266266+267267+NOTELEAF(1) User Commands NOTELEAF(1)
268268+
+178-19
internal/handlers/tasks.go
···5353}
54545555// Create creates a new task
5656-func (h *TaskHandler) Create(ctx context.Context, desc []string, priority, project, context, due string, tags []string) error {
5757- if len(desc) < 1 {
5656+func (h *TaskHandler) Create(ctx context.Context, description, priority, project, context, due, recur, until, parentUUID, dependsOn string, tags []string) error {
5757+ if description == "" {
5858 return fmt.Errorf("task description required")
5959 }
60606161- description := strings.Join(desc, " ")
6161+ parsed := parseDescription(description)
6262+6363+ if project != "" {
6464+ parsed.Project = project
6565+ }
6666+ if context != "" {
6767+ parsed.Context = context
6868+ }
6969+ if due != "" {
7070+ parsed.Due = due
7171+ }
7272+ if recur != "" {
7373+ parsed.Recur = recur
7474+ }
7575+ if until != "" {
7676+ parsed.Until = until
7777+ }
7878+ if parentUUID != "" {
7979+ parsed.ParentUUID = parentUUID
8080+ }
8181+ if dependsOn != "" {
8282+ parsed.DependsOn = strings.Split(dependsOn, ",")
8383+ }
8484+ if len(tags) > 0 {
8585+ parsed.Tags = append(parsed.Tags, tags...)
8686+ }
62876388 task := &models.Task{
6489 UUID: uuid.New().String(),
6565- Description: description,
9090+ Description: parsed.Description,
6691 Status: "pending",
6792 Priority: priority,
6868- Project: project,
6969- Context: context,
7070- Tags: tags,
9393+ Project: parsed.Project,
9494+ Context: parsed.Context,
9595+ Tags: parsed.Tags,
9696+ Recur: models.RRule(parsed.Recur),
9797+ DependsOn: parsed.DependsOn,
7198 }
72997373- if due != "" {
7474- if dueTime, err := time.Parse("2006-01-02", due); err == nil {
100100+ if parsed.Due != "" {
101101+ if dueTime, err := time.Parse("2006-01-02", parsed.Due); err == nil {
75102 task.Due = &dueTime
76103 } else {
77104 return fmt.Errorf("invalid due date format, use YYYY-MM-DD: %w", err)
78105 }
79106 }
80107108108+ if parsed.Until != "" {
109109+ if untilTime, err := time.Parse("2006-01-02", parsed.Until); err == nil {
110110+ task.Until = &untilTime
111111+ } else {
112112+ return fmt.Errorf("invalid until date format, use YYYY-MM-DD: %w", err)
113113+ }
114114+ }
115115+116116+ if parsed.ParentUUID != "" {
117117+ task.ParentUUID = &parsed.ParentUUID
118118+ }
119119+81120 id, err := h.repos.Tasks.Create(ctx, task)
82121 if err != nil {
83122 return fmt.Errorf("failed to create task: %w", err)
···88127 if priority != "" {
89128 fmt.Printf("Priority: %s\n", priority)
90129 }
9191- if project != "" {
9292- fmt.Printf("Project: %s\n", project)
130130+ if task.Project != "" {
131131+ fmt.Printf("Project: %s\n", task.Project)
93132 }
9494- if context != "" {
9595- fmt.Printf("Context: %s\n", context)
133133+ if task.Context != "" {
134134+ fmt.Printf("Context: %s\n", task.Context)
96135 }
9797- if len(tags) > 0 {
9898- fmt.Printf("Tags: %s\n", strings.Join(tags, ", "))
136136+ if len(task.Tags) > 0 {
137137+ fmt.Printf("Tags: %s\n", strings.Join(task.Tags, ", "))
99138 }
100139 if task.Due != nil {
101140 fmt.Printf("Due: %s\n", task.Due.Format("2006-01-02"))
102141 }
142142+ if task.Recur != "" {
143143+ fmt.Printf("Recur: %s\n", task.Recur)
144144+ }
145145+ if task.Until != nil {
146146+ fmt.Printf("Until: %s\n", task.Until.Format("2006-01-02"))
147147+ }
148148+ if task.ParentUUID != nil {
149149+ fmt.Printf("Parent: %s\n", *task.ParentUUID)
150150+ }
151151+ if len(task.DependsOn) > 0 {
152152+ fmt.Printf("Depends on: %s\n", strings.Join(task.DependsOn, ", "))
153153+ }
103154104155 return nil
105156}
···150201}
151202152203// Update updates a task using parsed flag values
153153-func (h *TaskHandler) Update(ctx context.Context, taskID, description, status, priority, project, context, due string, addTags, removeTags []string) error {
204204+func (h *TaskHandler) Update(ctx context.Context, taskID, description, status, priority, project, context, due, recur, until, parentUUID string, addTags, removeTags []string, addDeps, removeDeps string) error {
154205 var task *models.Task
155206 var err error
156207···186237 return fmt.Errorf("invalid due date format, use YYYY-MM-DD: %w", err)
187238 }
188239 }
240240+ if recur != "" {
241241+ task.Recur = models.RRule(recur)
242242+ }
243243+ if until != "" {
244244+ if untilTime, err := time.Parse("2006-01-02", until); err == nil {
245245+ task.Until = &untilTime
246246+ } else {
247247+ return fmt.Errorf("invalid until date format, use YYYY-MM-DD: %w", err)
248248+ }
249249+ }
250250+ if parentUUID != "" {
251251+ task.ParentUUID = &parentUUID
252252+ }
189253190254 for _, tag := range addTags {
191255 if !slices.Contains(task.Tags, tag) {
···195259196260 for _, tag := range removeTags {
197261 task.Tags = removeString(task.Tags, tag)
262262+ }
263263+264264+ // Handle dependency additions
265265+ if addDeps != "" {
266266+ deps := strings.Split(addDeps, ",")
267267+ for _, dep := range deps {
268268+ dep = strings.TrimSpace(dep)
269269+ if dep != "" && !slices.Contains(task.DependsOn, dep) {
270270+ task.DependsOn = append(task.DependsOn, dep)
271271+ }
272272+ }
273273+ }
274274+275275+ // Handle dependency removals
276276+ if removeDeps != "" {
277277+ deps := strings.Split(removeDeps, ",")
278278+ for _, dep := range deps {
279279+ dep = strings.TrimSpace(dep)
280280+ task.DependsOn = removeString(task.DependsOn, dep)
281281+ }
198282 }
199283200284 err = h.repos.Tasks.Update(ctx, task)
···681765 fmt.Printf(" (due: %s)", task.Due.Format("2006-01-02"))
682766 }
683767768768+ if task.Recur != "" {
769769+ fmt.Printf(" \u21bb")
770770+ }
771771+772772+ if len(task.DependsOn) > 0 {
773773+ fmt.Printf(" \u2937%d", len(task.DependsOn))
774774+ }
775775+684776 fmt.Println()
685777}
686778···710802 fmt.Printf("Due: %s\n", task.Due.Format("2006-01-02 15:04"))
711803 }
712804805805+ if task.Recur != "" {
806806+ fmt.Printf("Recurrence: %s\n", task.Recur)
807807+ }
808808+809809+ if task.Until != nil {
810810+ fmt.Printf("Recur Until: %s\n", task.Until.Format("2006-01-02"))
811811+ }
812812+813813+ if task.ParentUUID != nil {
814814+ fmt.Printf("Parent Task: %s\n", *task.ParentUUID)
815815+ }
816816+817817+ if len(task.DependsOn) > 0 {
818818+ fmt.Printf("Depends On:\n")
819819+ for _, dep := range task.DependsOn {
820820+ fmt.Printf(" - %s\n", dep)
821821+ }
822822+ }
823823+713824 if !noMetadata {
714825 fmt.Printf("Created: %s\n", task.Entry.Format("2006-01-02 15:04"))
715826 fmt.Printf("Modified: %s\n", task.Modified.Format("2006-01-02 15:04"))
···740851 return nil
741852}
742853854854+// ParsedTaskData holds extracted metadata from a task description
855855+type ParsedTaskData struct {
856856+ Description string
857857+ Project string
858858+ Context string
859859+ Tags []string
860860+ Due string
861861+ Recur string
862862+ Until string
863863+ ParentUUID string
864864+ DependsOn []string
865865+}
866866+867867+// parseDescription extracts inline metadata from description text
868868+// Supports: +project @context #tag due:YYYY-MM-DD recur:RULE until:DATE parent:UUID depends:UUID1,UUID2
869869+func parseDescription(text string) *ParsedTaskData {
870870+ parsed := &ParsedTaskData{Tags: []string{}, DependsOn: []string{}}
871871+ words := strings.Fields(text)
872872+873873+ var descWords []string
874874+ for _, word := range words {
875875+ switch {
876876+ case strings.HasPrefix(word, "+"):
877877+ parsed.Project = strings.TrimPrefix(word, "+")
878878+ case strings.HasPrefix(word, "@"):
879879+ parsed.Context = strings.TrimPrefix(word, "@")
880880+ case strings.HasPrefix(word, "#"):
881881+ parsed.Tags = append(parsed.Tags, strings.TrimPrefix(word, "#"))
882882+ case strings.HasPrefix(word, "due:"):
883883+ parsed.Due = strings.TrimPrefix(word, "due:")
884884+ case strings.HasPrefix(word, "recur:"):
885885+ parsed.Recur = strings.TrimPrefix(word, "recur:")
886886+ case strings.HasPrefix(word, "until:"):
887887+ parsed.Until = strings.TrimPrefix(word, "until:")
888888+ case strings.HasPrefix(word, "parent:"):
889889+ parsed.ParentUUID = strings.TrimPrefix(word, "parent:")
890890+ case strings.HasPrefix(word, "depends:"):
891891+ deps := strings.TrimPrefix(word, "depends:")
892892+ parsed.DependsOn = strings.Split(deps, ",")
893893+ default:
894894+ descWords = append(descWords, word)
895895+ }
896896+ }
897897+898898+ parsed.Description = strings.Join(descWords, " ")
899899+ return parsed
900900+}
901901+743902func removeString(slice []string, item string) []string {
744903 var result []string
745904 for _, s := range slice {
···772931 return fmt.Sprintf("%.1fh", hours)
773932 }
774933 days := int(hours / 24)
775775- remainingHours := hours - float64(days*24)
776776- if remainingHours == 0 {
934934+ if remainingHours := hours - float64(days*24); remainingHours == 0 {
777935 return fmt.Sprintf("%dd", days)
936936+ } else {
937937+ return fmt.Sprintf("%dd %.1fh", days, remainingHours)
778938 }
779779- return fmt.Sprintf("%dd %.1fh", days, remainingHours)
780939}
···11+-- Add recurrence fields directly to tasks
22+ALTER TABLE tasks ADD COLUMN recur TEXT; -- e.g. "daily", "weekly", ISO8601 rule
33+ALTER TABLE tasks ADD COLUMN until DATETIME; -- optional end date for recurrence
44+ALTER TABLE tasks ADD COLUMN parent_uuid TEXT; -- parent/template task UUID
55+66+-- Create dependencies table
77+CREATE TABLE IF NOT EXISTS task_dependencies (
88+ id INTEGER PRIMARY KEY AUTOINCREMENT,
99+ task_uuid TEXT NOT NULL, -- the dependent task
1010+ depends_on_uuid TEXT NOT NULL, -- the blocking task
1111+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1212+1313+ FOREIGN KEY(task_uuid) REFERENCES tasks(uuid) ON DELETE CASCADE,
1414+ FOREIGN KEY(depends_on_uuid) REFERENCES tasks(uuid) ON DELETE CASCADE
1515+);
1616+1717+-- Indexes for faster dependency lookups
1818+CREATE INDEX IF NOT EXISTS idx_task_dependencies_task_uuid ON task_dependencies(task_uuid);
1919+CREATE INDEX IF NOT EXISTS idx_task_dependencies_depends_on_uuid ON task_dependencies(depends_on_uuid);