Dotfiles notes
Expert help with the meganote system - cross-tool note capture, daily notes, and obsidian.nvim integration. Covers Hammerspoon, Shade, nvim, and the full capture → daily note linking pipeline.
git clone https://github.com/megalithic/dotfiles
T=$(mktemp -d) && git clone --depth=1 https://github.com/megalithic/dotfiles "$T" && mkdir -p ~/.claude/skills && cp -r "$T/docs/skills/notes" ~/.claude/skills/megalithic-dotfiles-notes && rm -rf "$T"
docs/skills/notes/SKILL.mdmeganote system expert
Prerequisites
Load the
skill first for Shade-specific details:shade
- Shade app internals (Swift, ContextGatherer, MLX inference)
- IPC notification protocol and debugging
- nvim RPC from Shade side (ShadeNvim.swift)
- Sidebar mode window management
This skill focuses on the nvim side of meganote: obsidian.nvim config, daily note linking, template substitutions, and task management.
Overview
The meganote system is a multi-tool note capture and organization system built across Hammerspoon, Shade, nvim (obsidian.nvim), and Obsidian. It enables quick capture of text and images with rich context, automatic linking to daily notes, and seamless integration with an Obsidian vault.
Architecture
┌─────────────────────────────────────────────────────────────────┐ │ User Hotkey Trigger │ │ Hyper+Shift+N (text) / Hyper+Shift+O (daily) │ └──────────────────────────┬──────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ Hammerspoon │ │ Posts DistributedNotification: io.shade.note.capture │ └──────────────────────────┬──────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ Shade.app │ │ 1. ContextGatherer: app type, URL, selection, language │ │ 2. Writes: ~/.local/state/shade/context.json │ │ 3. ShadeNvim RPC: :Obsidian new_from_template capture-text │ └──────────────────────────┬──────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ nvim (obsidian.nvim) │ │ 1. Reads context.json for template substitution │ │ 2. Creates: captures/YYYYMMDDHHMM-descriptor.md │ │ 3. User adds notes, saves file │ └──────────────────────────┬──────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────┐ │ nvim (autocmds.lua) │ │ BufWritePost autocmd "NotesCaptureLink": │ │ 1. Parse frontmatter, extract first content │ │ 2. Ensure daily note exists (create via :ObsidianToday) │ │ 3. Check same-day: only link if capture date == today │ │ 4. Append: - HH:MM [[filename|description]] to daily note │ └─────────────────────────────────────────────────────────────────┘
Key Directories & Files
| Path | Purpose |
|---|---|
| Main nvim notes plugin |
| obsidian.nvim config + template substitutions |
| Capture → daily note linking autocmd |
| Hammerspoon → Shade IPC |
| Path utilities for notes |
| Image capture workflow |
| Runtime capture context |
| Obsidian vault root (default: ) |
| Daily notes (year folders) |
| Capture notes |
| Image attachments |
| Obsidian templates (daily.md, capture-text.md, etc.) |
Capture Filename Format
YYYYMMDDHHMM-descriptor.md │ │ │ └─ Derived from context (window title, domain, app type) └─ Zettelkasten timestamp (creation time)
Examples:
202601141430-github-pr.md202601141432-stackoverflow-python.md
(fallback)202601141435-capture.md
Daily Note Linking
When Linking Occurs
The
NotesCaptureLink autocmd triggers on BufWritePost for */captures/*.md files.
Same-Day Check (CRITICAL)
Captures are only auto-linked if created on the same day as the daily note:
-- In autocmds.lua append_to_daily_note() local capture_date = extract_capture_date(filename) -- "20260114" from "202601141430-..." local today = os.date("%Y%m%d") if capture_date ~= today then -- Capture from a different day - don't link to today's daily return false, "not_same_day" end
Daily Note Auto-Creation
If the daily note doesn't exist when saving a capture, it's created automatically:
-- ensure_daily_note_exists() in autocmds.lua -- Uses obsidian.nvim's client:today() which applies the daily.md template local obsidian = require("obsidian") local client = obsidian.get_client() client:today() -- Creates with template substitutions
Link Format
Appended to
## Captures section in daily note:
## Captures - 14:30 [[202601141430-github-pr|Code review for auth changes]] - 14:45 [[202601141445-stackoverflow|Python async patterns]]
Template Substitutions
obsidian.nvim Template Variables
Defined in
lua/plugins/obsidian.lua:
| Variable | Value | Used In |
|---|---|---|
| | Daily note ID |
| | Frontmatter |
| Incomplete tasks from previous day | Daily template |
| Link to previous daily note | Daily template |
| Collapsible callout with app/URL/window | Capture template |
| Selected text as code block | Capture template |
| Image filename in assets/ | Image capture |
Context JSON Schema
Written to
~/.local/state/shade/context.json by Shade:
{ "appType": "browser", "appName": "Brave Browser Nightly", "windowTitle": "GitHub - Pull Request #123", "url": "https://github.com/owner/repo/pull/123", "selection": "const foo = 'bar';", "detectedLanguage": "javascript", "filePath": "/path/to/file.js", "filetype": "javascript", "timestamp": "2026-01-14T14:30:00" }
Key Functions Reference
autocmds.lua
| Function | Purpose |
|---|---|
| Returns |
| Parses YYYYMMDD from capture filename |
| Creates daily note via :ObsidianToday if missing |
| Links capture to daily note |
| Extracts YAML frontmatter as table |
| Gets first non-header content line |
| Creates link description |
notes.lua
| Function | Purpose |
|---|---|
| Cycle task checkbox status |
| Find most recent daily note before today |
| Check if capture has user content |
| Prompt to delete empty capture |
| Execute OCR on image |
| Sort task list by status |
obsidian.lua
| Function | Purpose |
|---|---|
| Find previous daily for task migration |
| Get unchecked tasks from daily note |
| Parse context.json |
| Create YYYYMMDDHHMM-descriptor ID |
| Clean string for filename use |
Hotkeys
| Hotkey | Action | Flow |
|---|---|---|
| Hyper+Shift+N | Text capture | HS → Shade → context.json → obsidian.nvim capture |
| Hyper+Ctrl+N | Capture in sidebar | Same, but enters sidebar-left mode first |
| Hyper+Shift+O | Open daily note | HS → Shade → :ObsidianToday |
| Hyper+N | Toggle Shade | HS → Shade toggle visibility |
Decision Trees
"Capture not linking to daily note"
Capture not linking? │ ├─▶ Check capture filename format │ └─▶ Must be: YYYYMMDDHHMM-*.md (12 digits then dash) │ ├─▶ Missing digits → obsidian.nvim note_id_func issue │ └─▶ Correct → Continue │ ├─▶ Check same-day │ └─▶ Compare capture date (first 8 digits) to today │ ├─▶ Different day → Expected behavior (no cross-day linking) │ └─▶ Same day → Continue │ ├─▶ Check daily note exists │ └─▶ ls $NOTES_HOME/daily/YYYY/YYYYMMDD.md │ ├─▶ Missing → Should auto-create on save │ │ └─▶ Check ensure_daily_note_exists() logs │ └─▶ Exists → Continue │ ├─▶ Check frontmatter │ └─▶ Capture must have valid YAML frontmatter │ └─▶ No frontmatter → Not a proper capture, skip linking │ └─▶ Check vim.b[buf].capture_linked └─▶ If true, already linked (or marked as processed)
"Daily note not created on capture save"
Daily not created? │ ├─▶ Check obsidian.nvim loaded │ └─▶ :Obsidian (should show commands) │ └─▶ Not loaded → Check lazy.nvim config │ ├─▶ Check client available │ └─▶ :lua print(require('obsidian').get_client()) │ └─▶ nil → Workspace not found │ ├─▶ Check directory exists │ └─▶ ls $NOTES_HOME/daily/YYYY/ │ └─▶ Missing → vim.fn.mkdir should create it │ └─▶ Check template └─▶ ls $NOTES_HOME/templates/daily.md └─▶ Missing → obsidian.nvim fails silently
"Context not captured in note"
No context in capture? │ ├─▶ Check context.json written │ └─▶ cat ~/.local/state/shade/context.json │ ├─▶ Empty/missing → Shade ContextGatherer issue │ └─▶ Has data → Continue │ ├─▶ Check read_shade_context() │ └─▶ :lua print(vim.inspect(require('plugins.obsidian').read_shade_context())) │ └─▶ nil → JSON parse error or file missing │ ├─▶ Check template uses variables │ └─▶ cat $NOTES_HOME/templates/capture-text.md │ └─▶ Should have {{capture_context}}, {{capture_selection}} │ └─▶ Check app has focus when capturing └─▶ Context gathered from frontmost app at capture time
Common Patterns
Adding a new template substitution
-- In lua/plugins/obsidian.lua, under templates.substitutions: my_variable = function() local ctx = read_shade_context() if ctx and ctx.someField then return ctx.someField end return "" end,
Modifying link description format
-- In lua/config/autocmds.lua, build_description(): -- Priority 1: First content line -- Priority 2: Source context (domain · language) -- Priority 3: "Text capture" fallback
Adding a new capture type
- Create template in
$NOTES_HOME/templates/capture-newtype.md - Add to obsidian.lua
:templates.template_customizations["capture-newtype"] = { notes_subdir = "captures", note_id_func = generate_capture_note_id, }, - Add Shade notification handler if needed
Task Management
Task Status Cycle
[ ] → [.] → [x] → [ ] │ │ │ │ │ └─ Done (completed) │ └─ In progress (started) └─ Todo (not started)
Task Sorting Order
In daily notes,
sort_tasks() orders by:
In progress (highest)[.]
Partially done[-]
Not started[ ]
Partially complete[/]- Other statuses
Completed (lowest)[x]
Task Migration
On new daily note creation:
- Extracts
tasks from previous day- [ ] - Replaces "tomorrow" with "today"
- Inserts into
{{migrated_tasks}}
Debugging
Check capture linking logs
:messages " Look for "Linked to daily:" or warning messages
Verify daily note path
:lua print(require('config.autocmds').get_daily_note_path())
Test context reading
:lua print(vim.inspect(require('plugins.obsidian').read_shade_context()))
Check capture date extraction
:lua print(require('config.autocmds').extract_capture_date("202601141430-test")) -- Should print: "20260114"
Manual daily note creation
:ObsidianToday
Force re-link capture
:lua vim.b.capture_linked = nil :w
Related Skills
- shade: Shade app IPC, context gathering, nvim RPC
- nvim: Neovim configuration, LSP, plugins
- hs: Hammerspoon configuration, hotkeys
Key Implementation Details
Why same-day linking?
Prevents accidental linking of old captures to today's daily note when re-saving files. Each capture should only link to the daily note for its creation date.
Why auto-create daily note?
Users often capture notes before opening their daily note. Auto-creation ensures the capture link isn't lost due to missing daily note.
Why context.json instead of direct RPC?
- Decouples Shade from obsidian.nvim internals
- Templates can use consistent substitution syntax
- Easier debugging (context is visible as file)
- obsidian.nvim reads context at template expansion time
Frontmatter preservation
obsidian.nvim's
frontmatter.func() preserves custom fields (source, source_url, etc.) from captures. Without this, obsidian.nvim would strip non-standard fields.