note

package
v0.4.2 Latest Latest
Warning

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

Go to latest
Published: May 14, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Index

Constants

View Source
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

View Source
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

func DirPath(root, date string) string

DirPath returns the year/month directory path for a given date string (Y...YMMDD), where MM and DD are zero-padded.

func ExtractHashtags

func ExtractHashtags(body []byte) []string

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

func Filename(date string, id int, slug, noteType string) string

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

func FormatNote(f frontmatter, body []byte) ([]byte, error)

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

func FormatTodoContent(tasks []Task) string

FormatTodoContent formats carried tasks into the new todo file content.

func HasSpecialBehavior

func HasSpecialBehavior(s string) bool

HasSpecialBehavior reports whether s is a type with special notes behavior.

func IsDigits

func IsDigits(s string) bool

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

func IsTagRune(r rune) bool

IsTagRune reports whether r is allowed inside a tag token: any unicode letter, any unicode digit, '_', or '-'.

func NextID

func NextID(root string) (int, error)

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

func ParseFilename(baseName string) (ref, error)

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

func ParseNote(data []byte) (frontmatter, []byte, error)

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

func StoreDirMode(root string) os.FileMode

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

func StripFrontmatter(data []byte) []byte

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

func ValidateSlug(slug string) error

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

func ValidateTag(s string) error

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

func WriteAtomic(path string, data []byte) error

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 Entry

type Entry struct {
	ID   int
	Meta Meta
	Body string
}

Entry is the single domain object that Store implementations work with.

type Event

type Event struct {
	Type EventType
	ID   int
}

Event is an ID-keyed Store change notification.

type EventType

type EventType int

EventType describes the kind of note change a Watcher observed.

const (
	EventCreated EventType = iota + 1
	EventUpdated
	EventDeleted
)

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 NewMemStore

func NewMemStore() *MemStore

NewMemStore returns an empty MemStore.

func (*MemStore) All

func (s *MemStore) All(opts ...QueryOpt) ([]Entry, error)

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) Delete

func (s *MemStore) Delete(id int) error

Delete removes the entry with the given ID, or returns ErrNotFound.

func (*MemStore) Find

func (s *MemStore) Find(opts ...QueryOpt) (Entry, error)

Find returns the newest entry matching opts, or ErrNotFound when no entry matches.

func (*MemStore) Get

func (s *MemStore) Get(id int) (Entry, error)

Get returns the entry with the given ID, or ErrNotFound.

func (*MemStore) IDs

func (s *MemStore) IDs() ([]int, error)

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.

func (*MemStore) Put

func (s *MemStore) Put(entry Entry) (Entry, error)

Put stores entry. When entry.ID is zero a new ID is assigned as max(existing IDs) + 1 (1 for an empty store) and Meta.CreatedAt is defaulted to time.Now if zero. Updates (entry.ID != 0) must carry a non-zero Meta.CreatedAt. Meta.UpdatedAt is always set to time.Now.

func (*MemStore) Reconcile

func (s *MemStore) Reconcile(known map[int]time.Time) (Diff, error)

Reconcile returns the delta between known and the current in-memory state. known maps entry ID to the UpdatedAt timestamp the caller last observed.

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

func NewOSStore(root string) *OSStore

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

func (s *OSStore) AbsPath(entry Entry) string

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

func (s *OSStore) All(opts ...QueryOpt) ([]Entry, error)

All returns every entry matching opts, newest-first. Type/slug/date filters are evaluated from filenames; tag filters require reading file bodies.

func (*OSStore) Delete

func (s *OSStore) Delete(id int) error

Delete removes the file for the given ID.

func (*OSStore) Find

func (s *OSStore) Find(opts ...QueryOpt) (Entry, error)

Find returns the newest entry matching opts, or ErrNotFound.

func (*OSStore) Get

func (s *OSStore) Get(id int) (Entry, error)

Get returns the entry with the given ID.

func (*OSStore) IDs

func (s *OSStore) IDs() ([]int, error)

IDs returns every stored ID newest-first by (date DESC, id DESC) using only the filename layout — no file reads.

func (*OSStore) Put

func (s *OSStore) Put(entry Entry) (Entry, error)

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

func (s *OSStore) Reconcile(known map[int]time.Time) (Diff, error)

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) Root

func (s *OSStore) Root() string

Root returns the absolute path the store is rooted at.

func (*OSStore) Watch

func (s *OSStore) Watch(ctx context.Context, opts ...WatchOpt) (Watcher, error)

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

func WithBeforeDate(d time.Time) QueryOpt

WithBeforeDate matches entries whose Meta.CreatedAt falls on a calendar day strictly before d (day precision, in d's location).

func WithExactDate

func WithExactDate(d time.Time) QueryOpt

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

func WithPublic(v bool) QueryOpt

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

func WithSlug(s string) QueryOpt

WithSlug matches entries whose Meta.Slug equals s. When multiple entries share a slug the newest match is returned first.

func WithTag

func WithTag(t string) QueryOpt

WithTag matches entries whose Meta.Tags contains t (case-insensitive). Multiple WithTag opts combine with AND semantics.

func WithType

func WithType(t string) QueryOpt

WithType matches entries whose Meta.Type equals t.

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

func ExtractTasks(lines []string) []Task

ExtractTasks parses all task lines from a todo file's content lines.

func ParseTask

func ParseTask(line string, lineNumber int) *Task

ParseTask attempts to parse a line as a task. Returns nil if not a task line.

func (*Task) Reassembled

func (t *Task) Reassembled(marker string) string

Reassembled returns the task line with a new marker.

func (*Task) WithTag

func (t *Task) WithTag(tag string) string

WithTag returns the task line with a tag inserted after the marker bracket. E.g. "- [ ] Do thing" with tag "moved" becomes "- [ ] (moved) Do thing". Returns the line unchanged if the tag is already present.

type WatchOpt

type WatchOpt func(*watchConfig)

WatchOpt configures OSStore.Watch. No options are currently exposed; the parameter is reserved so options such as WithDebounce can be added without changing the Watch signature.

type Watcher

type Watcher interface {
	Events() <-chan Event
	Close() error
}

Watcher emits Store change events until its context is cancelled or Close is called.

Jump to

Keyboard shortcuts

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