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 ¶
- Variables
- func NewID() string
- func ParseDue(s string) (time.Time, bool)
- type BackendSpec
- type BuildOptions
- type Draft
- type Filter
- type Invalidator
- type Item
- type ObsidianOption
- type ObsidianStore
- func (s *ObsidianStore) Add(_ context.Context, sc Scope, draft Draft) (Item, error)
- func (s *ObsidianStore) Complete(_ context.Context, sc Scope, id string) (Item, error)
- func (s *ObsidianStore) List(ctx context.Context, q Query) ([]Item, error)
- func (*ObsidianStore) Name() string
- func (s *ObsidianStore) Update(_ context.Context, sc Scope, id string, patch Patch) (Item, error)
- type Patch
- type Query
- type RESTConfig
- type RESTStore
- func (s *RESTStore) Add(ctx context.Context, sc Scope, draft Draft) (Item, error)
- func (s *RESTStore) Complete(ctx context.Context, sc Scope, id string) (Item, error)
- func (s *RESTStore) List(ctx context.Context, q Query) ([]Item, error)
- func (s *RESTStore) Name() string
- func (s *RESTStore) Update(ctx context.Context, sc Scope, id string, patch Patch) (Item, error)
- type Router
- func (r *Router) Add(ctx context.Context, frameName string, draft Draft) (Item, error)
- func (r *Router) Complete(ctx context.Context, frameName, id string) (Item, error)
- func (r *Router) Frame(ctx context.Context, name string, filter Filter) ([]Item, error)
- func (r *Router) Master(ctx context.Context, filter Filter) ([]Item, error)
- func (r *Router) Update(ctx context.Context, frameName, id string, patch Patch) (Item, error)
- type Scope
- type Store
Constants ¶
This section is empty.
Variables ¶
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.
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 ¶
Draft is the input to Add: everything a new item needs before the backend assigns it an ID.
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 ¶
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) Complete ¶
Complete flips the task with id to done, preserving the line's exact formatting (only the checkbox character changes).
type Patch ¶
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 ¶
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) List ¶
List queries the service once per scope (each scope's params become query parameters) and labels the returned items with the scope frame + backend.
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 ¶
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) Frame ¶
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 ¶
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).
type Scope ¶
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.