Documentation
¶
Index ¶
- Constants
- Variables
- func DirPath(root, date string) string
- func ExtractHashtags(body []byte) []string
- func Filename(date string, id int, slug, noteType string) string
- func FormatNote(f frontmatter, body []byte) ([]byte, error)
- func FormatTodoContent(tasks []Task) string
- func HasSpecialBehavior(s string) bool
- func IsDigits(s string) bool
- func IsTagRune(r rune) bool
- func NextID(root string) (int, error)
- func ParseFilename(baseName string) (ref, error)
- func ParseNote(data []byte) (frontmatter, []byte, error)
- func ReplaceBodyHashtags(body []byte, lowerMatch string, replace func(token []byte) []byte) (out []byte, n int)
- func SpecialBehaviorTypes() []string
- func StoreDirMode(root string) os.FileMode
- func StripFrontmatter(data []byte) []byte
- func ValidateSlug(slug string) error
- func ValidateTag(s string) error
- func WriteAtomic(path string, data []byte) error
- type Diff
- type Entry
- type Event
- type EventType
- type MemStore
- func (s *MemStore) All(opts ...QueryOpt) ([]Entry, error)
- func (s *MemStore) Delete(id int) error
- func (s *MemStore) Find(opts ...QueryOpt) (Entry, error)
- func (s *MemStore) Get(id int) (Entry, error)
- func (s *MemStore) IDs() ([]int, error)
- func (s *MemStore) Put(entry Entry) (Entry, error)
- func (s *MemStore) Reconcile(known map[int]time.Time) (Diff, error)
- type Meta
- type OSStore
- func (s *OSStore) AbsPath(entry Entry) string
- func (s *OSStore) All(opts ...QueryOpt) ([]Entry, error)
- func (s *OSStore) Delete(id int) error
- func (s *OSStore) Find(opts ...QueryOpt) (Entry, error)
- func (s *OSStore) Get(id int) (Entry, error)
- func (s *OSStore) IDs() ([]int, error)
- func (s *OSStore) Put(entry Entry) (Entry, error)
- func (s *OSStore) Reconcile(known map[int]time.Time) (Diff, error)
- func (s *OSStore) Root() string
- func (s *OSStore) Watch(ctx context.Context, opts ...WatchOpt) (Watcher, error)
- type QueryOpt
- type RemoveOpts
- type RemoveResult
- type RenameOpts
- type RenameResult
- type RolloverResult
- type Store
- type Task
- type WatchOpt
- type Watcher
Constants ¶
const DateFormat = "20060102"
DateFormat is the canonical YYYYMMDD layout for UID-derived and CLI-facing dates. Use with time.Parse / time.Format (and ParseInLocation for UTC).
Variables ¶
var ErrNotFound = errors.New("entry not found")
ErrNotFound is the package-wide "entry not found" sentinel. It is returned (wrapped) by Store.Get, Store.Find, and Store.Delete when no entry matches. Callers match with errors.Is:
if errors.Is(err, note.ErrNotFound) { … }
Functions ¶
func DirPath ¶
DirPath returns the year/month directory path for a given date string (Y...YMMDD), where MM and DD are zero-padded.
func ExtractHashtags ¶
ExtractHashtags scans body text and returns hashtag tokens (without the leading '#'), preserving source order and including duplicates. Rules:
- Lines whose first non-whitespace content is a run of '#' followed by whitespace or end-of-line are Markdown headings and are skipped entirely.
- Fenced code blocks (``` on a line, optionally indented, with optional info string) are skipped until the next fence line. Tilde fences (~~~) are not recognised.
- Inline backtick spans on a single line are skipped. An unclosed backtick suppresses hashtags for the remainder of its line.
- A '#' preceded by a word rune (any unicode letter/digit/'_') or a URL-path byte (`/`, `:`, `.`, `?`, `=`, `&`, `~`, `#`) is not a tag. This prevents matches inside URLs (`example.com/#anchor`), inline chains (`#one#two`), and adjacency to prose in any script (`café#bar`, `работа#tag`).
- Tag characters are any unicode letter, any unicode digit, '_', or '-'; other runes terminate a tag. A bare '#' with no following tag rune produces no output. A tag immediately followed by another '#' (e.g. `#one#two`) is rejected.
func Filename ¶
Filename generates a note filename from date, id, optional slug, and optional type. Type is encoded as a secondary file extension (e.g. ".todo.md") only when it's safe to round-trip through ParseFilename; values with '.' or path separators are omitted from the filename, with frontmatter remaining canonical.
func FormatNote ¶
FormatNote serialises frontmatter followed by body. Omits the frontmatter block entirely when f.IsZero(). Marshal errors surface to the caller; f.Extra values sourced from arbitrary YAML input can in principle fail to re-encode, so callers should handle the error rather than assume success.
func FormatTodoContent ¶
FormatTodoContent formats carried tasks into the new todo file content.
func HasSpecialBehavior ¶
HasSpecialBehavior reports whether s is a type with special notes behavior.
func IsDigits ¶
IsDigits reports whether s is non-empty and every rune is an ASCII digit. Use it to test numeric-ID references (wikilinks, CLI query arguments) or digit-shaped path segments (YYYY/MM directories) without re-implementing the predicate.
func IsTagRune ¶ added in v0.4.0
IsTagRune reports whether r is allowed inside a tag token: any unicode letter, any unicode digit, '_', or '-'.
func NextID ¶
NextID reads id.json, increments last_id, writes it back, and returns the new ID. The read-modify-write is serialized across processes via an exclusive flock on the store root directory, so concurrent callers cannot duplicate IDs.
func ParseFilename ¶
ParseFilename parses a note base filename (without .md extension) into its components. Expected format: Y...YMMDD_ID[_slug][.TYPE], where MM and DD are zero-padded. The dot-suffix is extracted as the filename-reported Type only when it round- trips cleanly (see filenameRoundtripSafeType). frontmatter `type` is canonical.
func ParseNote ¶
ParseNote splits a note file into its frontmatter and body. If no frontmatter block is present, the zero frontmatter is returned along with the full input as body and a nil error. If the frontmatter block is present but malformed, a non-nil error is returned along with the zero frontmatter; the body is still returned as a sub-slice so bulk readers can fall back to body-only processing. The returned body is always a sub-slice of the input — no allocation.
func ReplaceBodyHashtags ¶ added in v0.4.0
func ReplaceBodyHashtags(body []byte, lowerMatch string, replace func(token []byte) []byte) (out []byte, n int)
ReplaceBodyHashtags walks body using the same rules as ExtractHashtags. For every hashtag token whose lowercased value equals lowerMatch, the "#token" span (the leading '#' plus the token bytes) is replaced with the bytes returned by replace(token); token is the original byte slice of the hashtag without '#'. Returns the rewritten body and the number of replacements. body is returned unchanged (and n=0) if no replacements occur.
func SpecialBehaviorTypes ¶
func SpecialBehaviorTypes() []string
SpecialBehaviorTypes returns a fresh copy of the soft registry of note types with notes-specific handling. The returned slice may be freely mutated without affecting the package-internal list.
func StoreDirMode ¶
StoreDirMode returns the permissions to use when creating subdirectories under root. It inherits root's permissions so MkdirAll doesn't widen a restrictive root (e.g. 0o700), defaulting to 0o700 if root cannot be stat'd.
func StripFrontmatter ¶
StripFrontmatter returns data with any leading frontmatter block removed. If no valid frontmatter block is present, data is returned unchanged. Convenience for callers that want the body without parsing (e.g. `notes read --no-frontmatter`).
func ValidateSlug ¶
ValidateSlug returns an error if the slug cannot safely appear in a note filename. Empty slugs are accepted (they just omit the slug segment). All-digit slugs are rejected because they conflict with numeric ID lookup. Anything outside [A-Za-z0-9_-] is rejected to keep filenames portable and to avoid confusing the filename cache suffix.
func ValidateTag ¶ added in v0.4.0
ValidateTag reports whether s is a non-empty string of tag runes. Returns nil on valid input; an error describing the offending rune otherwise.
func WriteAtomic ¶
WriteAtomic writes data to path via a tmp+rename so partial writes don't leave a corrupted file behind.
Types ¶
type Diff ¶
type Diff struct {
// Added contains entries whose IDs were not present in the caller's known map.
Added []Entry
// Updated contains entries whose IDs were present in known with a different
// modification time. Stores compare mtimes for equality, not freshness, so
// callers also detect files whose mtimes moved backwards.
Updated []Entry
// Removed contains IDs from known that no longer exist in the store.
Removed []int
}
Diff is the result of a Reconcile call: what changed since the caller's last known view of the store.
type MemStore ¶
type MemStore struct {
// contains filtered or unexported fields
}
MemStore is an in-memory Store backed by map[int]Entry with an RWMutex. It is never user-facing — it exists to validate the Store interface shape and to serve as the test double for command tests.
MemStore skips the YAML and body-hashtag machinery that OSStore performs on read: Meta.Tags is exactly whatever the caller stored. Tag matching is case-insensitive.
func (*MemStore) All ¶
All returns every entry matching opts, newest-first by Meta.CreatedAt. See Store.All for the error contract: zero matches yield an empty slice and a nil error.
func (*MemStore) Find ¶
Find returns the newest entry matching opts, or ErrNotFound when no entry matches.
func (*MemStore) IDs ¶
IDs returns every stored ID newest-first by Meta.CreatedAt. Ties within the same timestamp break by higher ID first so the order is total and deterministic.
type Meta ¶
type Meta struct {
Title string
Slug string
Type string
CreatedAt time.Time
DateExplicit bool
UpdatedAt time.Time
Tags []string
Aliases []string
Description string
Public bool
Extra map[string]any
}
Meta holds the user-domain metadata for a note. YAML serialisation details live inside OSStore.
CreatedAt is the canonical authored timestamp. It always carries a value in memory: read from the frontmatter "date" field when present, otherwise derived from the filename's UID date prefix. It only round-trips back to the YAML "date" field when DateExplicit is true; otherwise the field is omitted on write and consumers fall back to the filename per SCHEMA.md.
DateExplicit reports that CreatedAt came from a user-supplied frontmatter "date" (on read) or a user-driven CLI flag like `update --date` (on write). Auto-defaulted dates (e.g. `notes new` stamping time.Now into a fresh entry) leave it false so the redundant frontmatter line is suppressed.
UpdatedAt is derived from the file's ModTime on read and is never written to YAML. OSStore.Put sets it to time.Now on every write — no file re-read needed because the OS updates ModTime when the file is rewritten.
Tags is the merged set of frontmatter "tags" and body "#hashtag" tokens; OSStore performs the merge on read and consumers never distinguish between the two sources.
Extra carries unknown frontmatter keys as map[string]any; OSStore handles conversion to/from yaml.Node at the serialisation boundary.
type OSStore ¶
type OSStore struct {
// contains filtered or unexported fields
}
OSStore is the filesystem-backed Store. It wraps the existing root/YYYY/MM/YYYYMMDD_id_slug.md layout and reuses the package's existing filename, frontmatter, and atomic-write helpers.
The Store interface never exposes filesystem paths. Callers that need the absolute path of an entry (e.g. the resolve command) type-assert to *OSStore and call AbsPath.
func NewOSStore ¶
NewOSStore returns an OSStore rooted at root. The directory must already exist; use os.Stat at the caller if validation is required.
func (*OSStore) AbsPath ¶
AbsPath returns the absolute path the store would use for entry given its current Meta.CreatedAt, ID, and Meta.Slug. It derives the path purely from the entry's fields — no I/O.
func (*OSStore) All ¶
All returns every entry matching opts, newest-first. Type/slug/date filters are evaluated from filenames; tag filters require reading file bodies.
func (*OSStore) IDs ¶
IDs returns every stored ID newest-first by (date DESC, id DESC) using only the filename layout — no file reads.
func (*OSStore) Put ¶
Put writes entry. See Store.Put for the full contract. When entry.ID is zero a new ID is allocated via NextID (id.json + flock) and CreatedAt is defaulted to time.Now if zero. On updates with a changed slug or date the file is renamed atomically.
func (*OSStore) Reconcile ¶
Reconcile returns the delta between known and the current on-disk state. known maps note ID to the file mtime the caller last observed, usually from Entry.Meta.UpdatedAt returned by All, Get, Find, or a previous Reconcile. Files whose mtimes match known are skipped entirely: no file read and no YAML parse. Files whose mtimes differ are read and parsed, even when the on-disk mtime moved backwards; mtime equality is the cache key. Do not seed known from the Entry returned by Put: Put avoids an extra stat and its UpdatedAt is the write time, not necessarily the exact filesystem mtime.
Caveats: filesystem mtime resolution can coalesce rapid writes on some filesystems, so a rewrite inside that resolution may be missed; tools such as rsync or touch can set mtimes backwards, which is why Reconcile compares equality rather than newer-than; a rename that changes the ID-bearing filename prefix appears as Removed+Added.
func (*OSStore) Watch ¶
Watch returns a Watcher that emits ID-keyed events for note files under the store root. Events are going-forward only: Watch does not replay a snapshot. Long-running consumers should subscribe first, then call Store.All, so file changes that happen during the initial list are queued on the watcher.
Implementations debounce internally. The current debounce window is fixed at 100 ms and coalesces by ID: create+delete in one window emits nothing, create+update emits create, and otherwise the last event in the window wins. Watch performs an initial filename scan before arming filesystem watches.
type QueryOpt ¶
type QueryOpt func(*query)
QueryOpt configures Store.All and Store.Find. Opts are combinable; multiple WithTag opts are AND-combined.
func WithBeforeDate ¶
WithBeforeDate matches entries whose Meta.CreatedAt falls on a calendar day strictly before d (day precision, in d's location).
func WithExactDate ¶
WithExactDate matches entries whose Meta.CreatedAt falls on the same calendar day as d (comparison is at day precision, in d's location).
func WithPublic ¶
WithPublic matches entries whose Meta.Public equals v. A note with no frontmatter "public" key reads as Public: false, so WithPublic(false) matches both explicit "public: false" and missing-key notes.
func WithSlug ¶
WithSlug matches entries whose Meta.Slug equals s. When multiple entries share a slug the newest match is returned first.
type RemoveOpts ¶ added in v0.4.0
type RemoveOpts struct {
// DryRun reports the modified paths without writing.
DryRun bool
}
RemoveOpts configures RemoveTag.
type RemoveResult ¶ added in v0.4.0
type RemoveResult struct {
// ModifiedPaths lists the absolute path of every note that was (or
// would be, in dry-run mode) modified, in newest-first order.
ModifiedPaths []string
}
RemoveResult is the outcome of a RemoveTag call.
func RemoveTag ¶ added in v0.4.0
func RemoveTag(store *OSStore, name string, opts RemoveOpts) (RemoveResult, error)
RemoveTag deletes the tag (matched case-insensitively) across the store. Frontmatter entries equal to name (case-insensitively) are dropped. Inline body "#name" tokens have their leading '#' stripped, leaving the bare word as prose. The store root is locked for the duration — including dry-run, so the previewed path list reflects a consistent snapshot. On mid-run failure, RemoveTag returns the error together with the partial path list of notes already written.
type RenameOpts ¶ added in v0.4.0
type RenameOpts struct {
// DryRun reports the modified paths without writing.
DryRun bool
}
RenameOpts configures RenameTag.
type RenameResult ¶ added in v0.4.0
type RenameResult struct {
// ModifiedPaths lists the absolute path of every note that was (or
// would be, in dry-run mode) modified, in newest-first order.
ModifiedPaths []string
}
RenameResult is the outcome of a RenameTag call.
func RenameTag ¶ added in v0.4.0
func RenameTag(store *OSStore, oldTag, newTag string, opts RenameOpts) (RenameResult, error)
RenameTag rewrites every occurrence of oldTag (matched case-insensitively) across the store, both in frontmatter "tags:" lists and in body "#hashtag" tokens, replacing it with newTag written literally. The store root is locked for the duration — including dry-run, so the previewed path list reflects a consistent snapshot. On mid-run failure, RenameTag returns the error together with the partial path list of notes already written.
type RolloverResult ¶
type RolloverResult struct {
CarriedTasks []Task // tasks to include in the new todo
UpdatedLines []string // modified lines of the previous todo (with (moved) tag added)
}
RolloverResult holds the output of a todo rollover operation.
func RolloverTasks ¶
func RolloverTasks(prevLines []string) RolloverResult
RolloverTasks determines which tasks to carry over and produces the modified previous todo.
type Store ¶
type Store interface {
// IDs returns the IDs of every entry newest-first by Meta.CreatedAt.
// Backends that can answer from a directory scan must not read file
// contents. Returns an empty slice (nil error) when the store is empty.
IDs() ([]int, error)
// All returns every entry matching opts, newest-first by Meta.CreatedAt.
// Returned entries are fully populated, including Meta.Tags merged from
// frontmatter tags and body hashtags. Zero matches returns an empty
// slice with a nil error.
All(opts ...QueryOpt) ([]Entry, error)
// Find returns the newest entry matching opts. Returns ErrNotFound when
// no entry matches. Backends may terminate the scan after the first
// match.
Find(opts ...QueryOpt) (Entry, error)
// Get returns the entry with the given ID, or ErrNotFound if no entry
// has that ID.
Get(id int) (Entry, error)
// Put writes entry. When entry.ID is zero the store assigns a fresh ID
// and defaults Meta.CreatedAt to time.Now if zero; otherwise Put performs
// a full replace of the existing entry and requires Meta.CreatedAt to be
// non-zero (returning an error otherwise). Meta.UpdatedAt is always set
// to time.Now on write. Returns the stored entry with all store-assigned
// fields populated.
Put(entry Entry) (Entry, error)
// Delete removes the entry with the given ID. Returns ErrNotFound when
// no entry has that ID.
Delete(id int) error
// Reconcile returns the delta between known and the current store state.
// known maps entry ID to the mtime the caller last observed for that ID.
// Entries with matching mtimes are skipped; changed entries are returned
// fully populated. Use this for cheap periodic resync; use All for an
// initial load.
Reconcile(known map[int]time.Time) (Diff, error)
}
Store is the backend abstraction the note package exposes. Implementations encapsulate the storage substrate (filesystem, in-memory, future cloud/DB) so CLI commands can target a single interface.
Error contract for lookups:
- Get, Find, and Delete return a wrapped ErrNotFound when no entry matches. Callers check with errors.Is(err, note.ErrNotFound).
- All returns an empty slice with a nil error when no entry matches; zero results are not considered an error.
type Task ¶
type Task struct {
Line string // original full line
Text string // trimmed task text, e.g. "Buy milk #daily"
Done bool // true when the marker is "x"
IsDaily bool // whether line contains #daily
IsMoved bool // whether line contains (moved)
LineNumber int // 0-based index in the source file lines
// contains filtered or unexported fields
}
Task represents a parsed task line from a todo note.
func ExtractTasks ¶
ExtractTasks parses all task lines from a todo file's content lines.
func (*Task) Reassembled ¶
Reassembled returns the task line with a new marker.