Notes and todos, moored to your code.
To moor a boat is to tie it to a fixed point so it stays where you left it. moor.nvim does that with your thoughts: capture them without leaving the buffer, tie them to a
file:line, and pull yourself back to that exact spot later.
moor is not an Obsidian or ZenNotes clone — it's meant to be used with
those apps, not instead of them. moor is the quick-capture lane while you
code; your notes app stays the place for reading, organizing, and everything
else. It shares exactly one concept with them — [[backlinks]] — and writes
plain, portable markdown. Point notes_dir at a vault that syncs (iCloud,
git, Syncthing) and your captures and todos show up on your phone; edits made
there flow back the next time moor scans.
More screenshots
The dashboard — every open todo in the vault, wikilinks bare, due dates highlighted:
s flips it into a flat soonest-first deadline list:
The project todo view keeps done and cancelled tasks visible:
Moored todos anchor a sign to their line in the code:
- Capture float — jot a note or todo over your code;
:wsaves and closes,<C-p>promotes to a split,<C-c>abandons - Per-project todos —
todo/<project>.md, keyed by your git root - Moorings — todos tied to a
file:line; the code line shows a⚓sign, and jumps work in both directions - Due dates —
due:tomorrow,due:fri,due:3dexpand to ZenNotes-parseable dates; the dashboard reads them back asdue in 3d/overdue 3d - Dashboard — every open todo in the vault, toggle in place, sort by deadline
- Backlinks — follow
[[links]], list backlinks, complete titles as you type[[, insert links via your picker - Synced-vault safe — no caching, tolerant of iCloud eviction, never rewrites a line that changed underneath it
- Zero dependencies — list UIs go through
vim.ui.select, so your picker (telescope ui-select, snacks, fzf-lua, mini.pick) is used automatically
- Neovim 0.11+
- Nothing else.
With lazy.nvim:
{
"54l1m/moor.nvim",
opts = {
notes_dir = "~/notes", -- your vault, e.g. an iCloud-synced ZenNotes folder
},
}require("moor").setup({
notes_dir = "~/notes",
ignore = { ".git", ".obsidian", "trash", "archive" }, -- dir names skipped by every scan
capture = {
-- Destination for note captures, relative to notes_dir. os.date() tokens
-- are expanded: "inbox/%Y-%m-%d.md" turns captures into daily notes.
note_file = "Captures.md",
-- Heading above each capture, as an os.date() format. false = raw append.
timestamp = "## %Y-%m-%d %H:%M",
window = { width = 0.5, height = 0.3, border = "rounded", title = " moor " },
maps = { promote = "<C-p>", abort = "<C-c>" }, -- inside the float only
},
todo = {
dir = "todo", -- todos live in <notes_dir>/todo/<project>.md
toggle_states = { " ", "x" }, -- add "-" for a cancelled state in the cycle
},
dashboard = {
window = { width = 0.7, height = 0.7, border = "rounded", title = " open todos " },
-- View-only; files keep "- [ ]" markdown. icons = false shows raw brackets.
-- cancelled styles the "-" state (add it to todo.toggle_states to use it).
icons = { open = "○", done = "✓", cancelled = "✗" },
-- Show "due tomorrow" / "due in 3d" / "overdue 3d" instead of the raw
-- due:YYYY-MM-DD token. View-only; files keep the absolute date.
friendly_dates = true,
maps = { toggle = "t", jump = "<CR>", jump_context = "gd", sort = "s", refresh = "r", close = "q" },
},
links = {
new_note_dir = "", -- where notes created from [[missing links]] land
completion = true, -- complete note titles when typing [[ (false disables)
},
-- Signs on code lines that have an open todo moored to them.
-- Set moorings = false to disable.
moorings = {
sign = "⚓",
},
-- Global keymaps, applied by setup(). Set keymaps = false to define none
-- (and bind the API yourself), set one entry to false to skip just it, or
-- give an entry a different lhs to rebind.
keymaps = {
capture_note = "<leader>nn",
capture_todo = "<leader>nt",
capture_todo_context = "<leader>nT",
add_todo = "<leader>na", -- prompt, moored to the cursor position
add_todo_plain = "<leader>nA", -- prompt, no file:line reference
dashboard = "<leader>nd",
toggle = "<leader>nx",
follow_link = "<leader>nf",
backlinks = "<leader>nb",
open_todo = "<leader>no",
mooring = "<leader>nm", -- jump from a moored code line to its todo
find_note = "<leader>ns", -- pick any note and open it
insert_link = "<leader>ni", -- pick a note, insert [[link]] at the cursor
},
})A thought hits while you're mid-function. Press your capture key: a small float
opens over the code, you type, :w — the float closes and you're back where
you were. That's the whole gesture.
-
note mode appends to
capture.note_fileunder a dated heading (## 2026-08-21 12:32by default —capture.timestamptakes anyos.date()format, orfalse), creating the file with a# Titleheader so it renders properly in ZenNotes/Obsidian. -
todo mode turns each line into
- [ ] …in your project's todo file. -
context (
capture({ mode = "todo", context = true })) moors the todo to where your cursor was:- [ ] handle EOF case · `internal/server/conn.go:142`
On your phone that's just readable monospace. In nvim, it's a jump target. Works whether you type plain prose or a full
- [ ] …line yourself — a hand-typed task without a mooring still gets one. -
promote (
<C-p>inside the float) reopens the same buffer in a split when the thought turns out to be bigger than the float.:wstill saves to the vault.<C-c>abandons.
:Moor capture, :Moor capture todo, :Moor capture todo context, or the
one-liner prompt :Moor todo (<leader>na) — moored by default; :Moor todo plain skips the file:line.
-
One file per project:
<notes_dir>/todo/<project>.md, where the project is your git root's name (cwd basename outside a repo).:Moor openshows the project todo view — the dashboard scoped to this project's file, with done tasks kept visible, struck through.<CR>opens the underlying file when you want to edit freely. -
:Moor toggleflips the checkbox on the current line — in any buffer, including inside the dashboard. -
Due dates are inline
due:YYYY-MM-DDtokens — the syntax ZenNotes already parses, so they work on your phone too. When capturing you can type relative shortcuts and moor expands them to the absolute date on save:due:today due:tomorrow due:fri due:monday due:3d due:2wThe dashboard shows the date as friendly relative text —
due tomorrow,due in 3d,overdue 3d— with overdue and due-today accented in the warning color (friendly_dates = falsekeeps the raw ISO token). The files always store the absolute date. -
:Moor dashboardopens a float aggregating every open- [ ]across the vault, grouped by note:todo/pf4.md (2) ○ fix race in loader · `lua/moor/init.lua:33` ○ ship the release due tomorrow inbox/Ideas.md (1) ○ try the capture float on the phone<CR>jumps to the note,gdjumps to the moored code location,ttoggles (the item disappears — that's the feedback),sswitches to a flat soonest-first deadline list (undated items last, source note dimmed at the end of each row) and back,rrescans,qcloses. The active keys are pinned in the float's bottom border, and your global toggle binding works here too.The rendering is view-only sugar — wikilinks show without their
[[brackets]], checkboxes use the configureddashboard.icons— while the files on disk always keep plain markdown.
| Group | Styles | Default |
|---|---|---|
MoorLink |
wikilink text in the views | Underlined |
MoorDone |
done task text | strikethrough, Comment fg |
MoorDoneMark |
done checkbox icon | Comment |
MoorTodoMark |
open checkbox icon | unstyled |
MoorDue |
future due dates | Special |
MoorOverdue |
overdue / due-today dates | DiagnosticError |
MoorAnchor |
mooring sign in the code | Special |
Change any of them in your config or colorscheme:
vim.api.nvim_set_hl(0, "MoorLink", { fg = "#7aa2f7" })The anchor works in both directions. Lines of your code that have an open todo
moored to them show a ⚓ sign in the sign column, read fresh from the project
todo file whenever you enter the buffer (and after every capture or toggle).
Complete the todo and the sign disappears.
<leader>nm / :Moor mooring jumps from the code to the todo: on a moored
line it goes straight there; elsewhere in a file with moorings it offers a
pick. moorings = false turns the whole thing off.
:Moor followopens the[[Note Title]]under the cursor, offering to create the note when it doesn't exist yet.:Moor backlinkslists every note linking to the current one viavim.ui.select—[[Title]],[[Title|alias]], and[[Title#heading]]all count.- Typing
[[in a vault note (or a capture float) pops note-title completion, matched case-insensitively anywhere in the title; accepting a title closes the]]for you. It's a plaincompletefunc, so<C-x><C-u>also summons it and<C-n>/<C-p>navigate.links.completion = falseturns it off. :Moor find(<leader>ns) opens any note via your picker;:Moor link(<leader>ni) picks a note and inserts a[[link]]to it at the cursor.
:checkhealth moor verifies the nvim version, that notes_dir exists and is
writable, times a vault scan, and shows where the current project's todos go.
moor never caches your notes. Every scan re-reads the directory, unreadable files (like evicted iCloud placeholders) are skipped silently, and toggling a todo from the dashboard re-checks the source line first — if your phone edited the note in the meantime, moor rescans instead of rewriting the wrong line.
moor.nvim/
├── lua/moor/
│ ├── init.lua -- public API: defaults, options, setup()
│ ├── cmd.lua -- :Moor subcommand dispatcher
│ ├── project.lua -- project identity (git root / cwd)
│ ├── vault.lua -- all notes_dir filesystem access
│ ├── tasks.lua -- pure checkbox-line parse/toggle/format
│ ├── due.lua -- due-date expansion, parsing, friendly display
│ ├── capture.lua -- the capture float
│ ├── todo.lua -- per-project todo operations
│ ├── dashboard.lua -- the todo dashboard (vault + project scopes)
│ ├── moorings.lua -- code-side anchor signs + jump to todo
│ ├── links.lua -- wikilinks: follow, backlinks, find, insert
│ ├── completion.lua -- [[title]] completefunc
│ ├── context.lua -- file:line references
│ ├── health.lua -- :checkhealth moor
│ └── picker.lua -- vim.ui.select seam (native adapters land here)
├── plugin/moor.lua -- :Moor command (lazy, zero startup cost)
├── scripts/demo.lua -- stages a fake vault for screenshots
└── tests/ -- plenary busted specs, one per module
Contributions are welcome and encouraged — especially picker adapters:
every list UI goes through lua/moor/picker.lua (a thin vim.ui.select
wrapper), so a native telescope/fzf-lua/snacks/mini.pick adapter with previews
only needs to touch that one file.
make test # run the suite (fetches plenary on first run)
make format # stylua
make format-checkConventional commits, tests for behavior changes, and make format before
pushing.
MIT




