todo

package
v0.8.25 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 30, 2026 License: GPL-3.0 Imports: 20 Imported by: 0

Documentation

Overview

Package todo is carlos's pluggable task/reminder layer.

The model the rest of carlos sees is small and backend-agnostic:

  • Item - one task, with a stable backend-owned ID, optional due date and tags, labelled with the frame it belongs to.
  • Store - the four operations a backend must implement: List / Add / Complete / Update. Obsidian is the out-of-the-box backend; a generic REST backend ships as the reference external one.
  • Scope - "where" a query or mutation lands: a frame name plus the Obsidian vault_subtree (so descendant folders nest naturally) plus opaque per-backend params (e.g. a REST project id).
  • Router - resolves each frame to its backend + scope, and answers the two lenses: Master (union across every frame) and Frame (one frame and the folders beneath it).

The package deliberately depends only on the standard library plus the frame config type, so it stays unit-testable without a vault, a daemon, or a network. Backends pull in their own dependencies (the Obsidian backend touches the filesystem; the REST backend an *http.Client).

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("todo: item not found")

ErrNotFound is returned by Complete/Update when no item with the given ID exists in the resolved scope.

Functions

func NewID

func NewID() string

NewID mints a fresh Obsidian block-ref slug (without the caret), e.g. "todo-ab12cd". It draws 5 random bytes → 8 base32 chars. A crypto/rand failure (vanishingly rare) falls back to a fixed marker so Add never panics; the caller's per-file collision check still guards uniqueness.

func ParseDue

func ParseDue(s string) (time.Time, bool)

ParseDue parses a YYYY-MM-DD due string into a UTC midnight time. ok is false for the empty string or an unparseable value, so callers can treat "no due date" and "garbage" alike (both simply never fire a reminder).

Types

type BackendSpec

type BackendSpec struct {
	Name       string
	Type       string
	BaseURL    string
	AuthHeader string
	AuthValue  string
}

BackendSpec describes one external backend to construct. Type selects the transport; the remaining fields are transport-specific (currently REST).

type BuildOptions

type BuildOptions struct {
	// DefaultBackend is the fallback backend name (empty → "obsidian").
	DefaultBackend string
	// Inbox is the Obsidian inbox filename (empty → "todos.md").
	Inbox string
	// Invalidator, when set, is wired into the Obsidian store so notes-cache
	// entries are dropped after writes.
	Invalidator Invalidator
	// Backends are the external backends to register alongside Obsidian.
	Backends []BackendSpec
}

BuildOptions parameterise BuildRouter.

type Draft

type Draft struct {
	Text string
	Due  string
	Tags []string
}

Draft is the input to Add: everything a new item needs before the backend assigns it an ID.

type Filter

type Filter string

Filter selects which items List returns by completion state.

const (
	// FilterOpen returns only not-done items. The default.
	FilterOpen Filter = "open"
	// FilterDone returns only completed items.
	FilterDone Filter = "done"
	// FilterAll returns every item regardless of state.
	FilterAll Filter = "all"
)

type Invalidator

type Invalidator interface {
	ResetPath(path string)
}

Invalidator is the subset of *notes.Cache the store needs: drop the cached index for a vault path so the next read re-walks. Declared here so the todo package does not import the notes package.

type Item

type Item struct {
	// ID is the backend-stable handle Complete/Update target. For Obsidian
	// it is the block-ref slug WITHOUT the leading caret (e.g. "todo-ab12cd").
	ID string `json:"id"`
	// Text is the human description with the due marker, block-ref, and
	// standalone #tags stripped out into their structured fields.
	Text string `json:"text"`
	// Done is the checkbox state.
	Done bool `json:"done"`
	// Frame labels which frame the item belongs to (the master view sets it
	// per source frame; a single-frame query echoes that frame).
	Frame string `json:"frame,omitempty"`
	// Backend names the Store that produced the item ("obsidian", "rest", …).
	Backend string `json:"backend,omitempty"`
	// Source is a backend-specific locator for display: for Obsidian the
	// "relpath:line" of the checkbox; for REST the endpoint path.
	Source string `json:"source,omitempty"`
	// Due is the due date in YYYY-MM-DD form, or "" when none. Kept as a
	// string so JSON stays timezone-free and round-trips Obsidian's
	// `📅 YYYY-MM-DD` marker exactly.
	Due string `json:"due,omitempty"`
	// Tags are the item's #tags, without the leading '#'.
	Tags []string `json:"tags,omitempty"`
}

Item is one task as carlos sees it, independent of which backend stores it.

Slice fields are normalised to non-nil so JSON encodes `[]` rather than `null`. ID is stable for the lifetime of the item within its backend: the Obsidian backend mints an Obsidian block reference (`^todo-xxxx`); a REST backend echoes whatever id its service assigns.

func (Item) DueOnOrBefore

func (it Item) DueOnOrBefore(day time.Time) bool

DueOnOrBefore reports whether the item has a due date on or before the given day, compared at date granularity in day's own location (so callers pass a local "now" to get reminders keyed to the user's wall-clock date). Used by the reminder scanner to find overdue + due-today items.

type ObsidianOption

type ObsidianOption func(*ObsidianStore)

ObsidianOption configures an ObsidianStore.

func WithIDFunc

func WithIDFunc(fn func() string) ObsidianOption

WithIDFunc overrides the block-ref id generator (tests inject a counter).

func WithInbox

func WithInbox(name string) ObsidianOption

WithInbox overrides the default "todos.md" inbox filename.

func WithInvalidator

func WithInvalidator(inv Invalidator) ObsidianOption

WithInvalidator wires a notes-cache invalidator called after each write.

type ObsidianStore

type ObsidianStore struct {
	// contains filtered or unexported fields
}

ObsidianStore is the out-of-the-box backend: todos live as standard `- [ ] task` checkbox lines inside the user's Obsidian vault, scoped to a frame's vault_subtree. Because a subtree is a folder prefix, a frame transparently "sees" the todos in every descendant note, which is how the targeted lens shows a frame and its children.

Reads walk the subtree and parse checkbox lines; writes mutate the single owning file and rewrite it atomically (temp + fsync + rename), mirroring the notes_write recipe so a crash mid-write never corrupts a note. After every write the optional Invalidator drops the notes cache entry so notes_search / notes_get reflect the change immediately.

func NewObsidianStore

func NewObsidianStore(vaultPath string, opts ...ObsidianOption) *ObsidianStore

NewObsidianStore builds a store over the vault rooted at vaultPath.

func (*ObsidianStore) Add

func (s *ObsidianStore) Add(_ context.Context, sc Scope, draft Draft) (Item, error)

Add appends a new task to the scope's inbox file and returns it.

func (*ObsidianStore) Complete

func (s *ObsidianStore) Complete(_ context.Context, sc Scope, id string) (Item, error)

Complete flips the task with id to done, preserving the line's exact formatting (only the checkbox character changes).

func (*ObsidianStore) List

func (s *ObsidianStore) List(ctx context.Context, q Query) ([]Item, error)

List walks each scope's subtree, parses checkbox lines, and returns the items matching the filter, ordered by file path then line. An empty Scopes slice scans the whole vault under a blank frame label.

func (*ObsidianStore) Name

func (*ObsidianStore) Name() string

Name identifies this backend.

func (*ObsidianStore) Update

func (s *ObsidianStore) Update(_ context.Context, sc Scope, id string, patch Patch) (Item, error)

Update applies a partial patch to the task with id. Because the content can change, the line is re-rendered canonically (id and list marker preserved).

type Patch

type Patch struct {
	Text *string
	Done *bool
	Due  *string
	Tags *[]string
}

Patch is a partial update for Update. Nil fields are left untouched so a caller can flip Done without disturbing Text, retag without re-dating, etc.

type Query

type Query struct {
	Scopes []Scope
	Filter Filter
}

Query is a List request. Scopes lists every (frame, subtree) the query should sweep; the master view passes one per frame, a targeted lens passes one. An empty Scopes slice means "the backend's whole space" (legacy single-shelf mode).

type RESTConfig

type RESTConfig struct {
	Name       string
	BaseURL    string
	AuthHeader string
	AuthValue  string
	// Client is optional; a 15s-timeout *http.Client is used when nil.
	Client httpDoer
}

RESTConfig configures a RESTStore. AuthHeader/AuthValue are sent verbatim on every request when both are non-empty (e.g. "Authorization" / "Bearer xyz").

type RESTStore

type RESTStore struct {
	// contains filtered or unexported fields
}

RESTStore is the reference external backend: a Store backed by any HTTP service that speaks the small "carlos todo" contract below. It is the proof that the Store seam supports out-of-vault systems over a real transport without baking in a specific vendor SDK. Pointing it at Todoist, Google Tasks, or a self-hosted service is a thin server-side adapter that maps the vendor's API onto this contract.

Contract (all bodies are the JSON Item shape; auth via a configurable header):

GET    {base}/todos?filter=open[&<param>=<value>...]   → [Item, ...]
POST   {base}/todos        {text,due,tags,<params>}    → Item
POST   {base}/todos/{id}/complete                      → Item
PATCH  {base}/todos/{id}    {text?,done?,due?,tags?}   → Item

A 404 on complete/update maps to ErrNotFound; any other non-2xx surfaces as an error carrying the status and a truncated body.

func NewRESTStore

func NewRESTStore(cfg RESTConfig) *RESTStore

NewRESTStore builds a REST-backed store from cfg.

func (*RESTStore) Add

func (s *RESTStore) Add(ctx context.Context, sc Scope, draft Draft) (Item, error)

Add posts a new task, merging the scope params into the body.

func (*RESTStore) Complete

func (s *RESTStore) Complete(ctx context.Context, sc Scope, id string) (Item, error)

Complete posts to the item's complete route.

func (*RESTStore) List

func (s *RESTStore) List(ctx context.Context, q Query) ([]Item, error)

List queries the service once per scope (each scope's params become query parameters) and labels the returned items with the scope frame + backend.

func (*RESTStore) Name

func (s *RESTStore) Name() string

Name identifies this backend (configurable so multiple REST services can coexist under distinct names).

func (*RESTStore) Update

func (s *RESTStore) Update(ctx context.Context, sc Scope, id string, patch Patch) (Item, error)

Update patches the item.

type Router

type Router struct {
	// contains filtered or unexported fields
}

Router is the aggregation layer. It maps each frame to a backend Store and a Scope (via the frame's Capabilities["todos"] block, defaulting to the Obsidian backend + the frame's vault_subtree), and answers the two lenses the user asked for:

  • Master: the union of every frame's todos, each item labelled with its source frame. The high-level overview.
  • Frame: one frame and the folders nested beneath its subtree. The targeted lens.

Mutations (Add / Complete / Update) route to the single frame's backend.

func BuildRouter

func BuildRouter(vaultPath string, frames frame.Config, active string, opts BuildOptions) (*Router, error)

BuildRouter assembles a Router over the vault plus any declared external backends. The Obsidian backend is always registered under "obsidian". active overrides frames.Active for session-scoped frame focus (empty keeps the on-disk active frame).

func NewRouter

func NewRouter(frames frame.Config, def string, stores map[string]Store) *Router

NewRouter wires the backend stores and the frame configuration. def is the fallback backend name for frames that do not pin one (typically "obsidian"); an empty def defaults to "obsidian". stores must contain def.

func (*Router) Add

func (r *Router) Add(ctx context.Context, frameName string, draft Draft) (Item, error)

Add routes a new task to the named frame's backend (active frame if empty).

func (*Router) Complete

func (r *Router) Complete(ctx context.Context, frameName, id string) (Item, error)

Complete marks the task with id done in the named frame's backend.

func (*Router) Frame

func (r *Router) Frame(ctx context.Context, name string, filter Filter) ([]Item, error)

Frame returns the todos for a single frame (and the folders nested beneath its subtree) under the given filter. An empty name targets the active frame.

func (*Router) Master

func (r *Router) Master(ctx context.Context, filter Filter) ([]Item, error)

Master returns the union of every frame's todos under the given filter, each item labelled with its source frame. Frames are grouped by backend so each store is queried once with all its scopes. Items are ordered by frame name, then by their backend order (file path / line for Obsidian).

func (*Router) Update

func (r *Router) Update(ctx context.Context, frameName, id string, patch Patch) (Item, error)

Update applies patch to the task with id in the named frame's backend.

type Scope

type Scope struct {
	Frame   string
	Subtree string
	Params  map[string]string
}

Scope is "where" a query or mutation applies. For Obsidian, Subtree is the cleaned vault-relative folder prefix (descendant folders are included, which is how a frame "sees its children's todos"). Params carries opaque per-backend routing (e.g. {"project": "Ludus"} for a REST backend).

type Store

type Store interface {
	// Name is the backend identifier used in config and on Item.Backend.
	Name() string
	// List returns items matching q, ordered deterministically (the Obsidian
	// backend orders by file path then line).
	List(ctx context.Context, q Query) ([]Item, error)
	// Add appends a new item in sc and returns it with its minted ID.
	Add(ctx context.Context, sc Scope, draft Draft) (Item, error)
	// Complete marks the item with id done within sc and returns it.
	Complete(ctx context.Context, sc Scope, id string) (Item, error)
	// Update applies patch to the item with id within sc and returns it.
	Update(ctx context.Context, sc Scope, id string, patch Patch) (Item, error)
}

Store is the contract every todo backend implements. Implementations must be safe for concurrent use by multiple goroutines.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL