Plugin Installation
The zk NeoVim plugin provides seamless integration for creating notes directly from your editor.
Prerequisites
Section titled “Prerequisites”- NeoVim 0.9+
- plenary.nvim (required dependency)
- snacks.nvim (optional, for picker UI)
zkbinary installed and in your$PATH
Installation
Section titled “Installation”Using lazy.nvim
Section titled “Using lazy.nvim”{ "infrashift/zettelkasten", dependencies = { "nvim-lua/plenary.nvim", "folke/snacks.nvim", -- Optional, for picker UI }, config = function() require("zk").setup({ bin = "zk", -- Path to zk binary }) end,}Using packer.nvim
Section titled “Using packer.nvim”use { "infrashift/zettelkasten", requires = { "nvim-lua/plenary.nvim" }, config = function() require("zk").setup({ bin = "zk", }) end,}Using vim-plug
Section titled “Using vim-plug”Plug 'nvim-lua/plenary.nvim'Plug 'infrashift/zettelkasten'
" In your init.lua or after/plugin:lua require("zk").setup({ bin = "zk" })Manual Installation
Section titled “Manual Installation”-
Clone the repository:
Terminal window git clone https://github.com/infrashift/zettelkasten.git ~/.local/share/nvim/site/pack/plugins/start/zettelkasten -
Ensure plenary.nvim is also installed
-
Add to your
init.lua:require("zk").setup({bin = "zk",})
Configuration
Section titled “Configuration”Basic Setup
Section titled “Basic Setup”require("zk").setup({ bin = "zk", -- Default: "zk"})Custom Binary Path
Section titled “Custom Binary Path”require("zk").setup({ bin = "/usr/local/bin/zk",})With Development Build
Section titled “With Development Build”require("zk").setup({ bin = vim.fn.expand("~/projects/zettelkasten/zk"),})Full Setup with Tag Completion
Section titled “Full Setup with Tag Completion”require("zk").setup({ bin = "zk",})
-- Enable tag completion in markdown filesrequire("zk").setup_tag_completion()
-- Register nvim-cmp source (optional, if using nvim-cmp)require("zk").setup_cmp()Filetype Settings
Section titled “Filetype Settings”When editing a zettel file (markdown with id: in frontmatter), the plugin
automatically provides buffer-local keymaps:
| Keymap | Description |
|---|---|
<C-x><C-t> | Tag completion (insert mode) |
<localleader>l | Insert link |
<localleader>L | Insert link with title |
<localleader>b | Toggle backlinks |
<localleader>p | Set project (note and todo types) |
<localleader>t | Tether / Untether (note and todo types) |
<localleader>a | Add tags (all zettel types) |
<localleader>v | Validate frontmatter (all zettel types) |
<localleader>s | Set todo status (todo-type only) |
Help Documentation
Section titled “Help Documentation”Full documentation is available via :help zk.
See User Commands for a complete reference of all :Zk commands.
Keybindings
Section titled “Keybindings”Add these to your NeoVim configuration:
-- Quick untethered note (no project)vim.keymap.set("n", "<leader>zf", function() require("zk").create_note("untethered")end, { desc = "Create untethered note" })
-- Quick tethered note (will use git project)vim.keymap.set("n", "<leader>zp", function() require("zk").create_note("tethered")end, { desc = "Create tethered note" })
-- Tether current notevim.keymap.set("n", "<leader>zP", function() require("zk").tether_note()end, { desc = "Tether note" })
-- Set project on current notevim.keymap.set("n", "<leader>zs", function() require("zk").set_project()end, { desc = "Set project" })
-- Search with pickervim.keymap.set("n", "<leader>zz", function() require("zk.picker").search()end, { desc = "Search zettels" })
-- Live searchvim.keymap.set("n", "<leader>z/", function() require("zk.picker").live_search()end, { desc = "Live search zettels" })
-- Index current directoryvim.keymap.set("n", "<leader>zi", function() require("zk").index()end, { desc = "Index zettels" })
-- Generate graph visualizationvim.keymap.set("n", "<leader>zg", function() require("zk").graph()end, { desc = "Generate graph" })
-- Preview current note in floating windowvim.keymap.set("n", "<leader>zv", function() require("zk").preview_note()end, { desc = "Preview note" })
-- Preview note by IDvim.keymap.set("n", "<leader>zV", function() require("zk").preview_by_id()end, { desc = "Preview by ID" })
-- Insert link (opens picker)vim.keymap.set("n", "<leader>zl", function() require("zk").link_picker()end, { desc = "Insert link" })
-- Insert link with titlevim.keymap.set("n", "<leader>zL", function() require("zk").link_picker({ include_title = true })end, { desc = "Insert link with title" })
-- Toggle backlinks panelvim.keymap.set("n", "<leader>zb", function() require("zk").toggle_backlinks()end, { desc = "Toggle backlinks" })
-- Open backlinks in splitvim.keymap.set("n", "<leader>zB", function() require("zk").backlinks_split()end, { desc = "Backlinks split" })
-- Create note from template (opens picker)vim.keymap.set("n", "<leader>zt", function() require("zk").template_picker()end, { desc = "Create from template" })
-- Quick meeting notesvim.keymap.set("n", "<leader>zm", function() require("zk").create_from_template("meeting")end, { desc = "Create meeting notes" })
-- Today's daily notevim.keymap.set("n", "<leader>zd", function() require("zk").daily()end, { desc = "Today's daily note" })
-- Yesterday's daily note (morning review)vim.keymap.set("n", "<leader>zD", function() require("zk").daily({ date = "yesterday" })end, { desc = "Yesterday's daily note" })
-- Browse daily notesvim.keymap.set("n", "<leader>zw", function() require("zk").daily_picker()end, { desc = "Browse daily notes" })With Which-Key
Section titled “With Which-Key”local wk = require("which-key")wk.register({ z = { name = "Zettelkasten", f = { function() require("zk").create_note("untethered") end, "Untethered note" }, p = { function() require("zk").create_note("tethered") end, "Tethered note" }, P = { function() require("zk").tether_note() end, "Tether note" }, s = { function() require("zk").set_project() end, "Set project" }, g = { function() require("zk").graph() end, "Generate graph" }, v = { function() require("zk").preview_note() end, "Preview note" }, V = { function() require("zk").preview_by_id() end, "Preview by ID" }, l = { function() require("zk").link_picker() end, "Insert link" }, L = { function() require("zk").link_picker({ include_title = true }) end, "Insert link with title" }, b = { function() require("zk").toggle_backlinks() end, "Toggle backlinks" }, B = { function() require("zk").backlinks_split() end, "Backlinks split" }, t = { function() require("zk").template_picker() end, "Create from template" }, m = { function() require("zk").create_from_template("meeting") end, "Meeting notes" }, d = { function() require("zk").daily() end, "Today's daily" }, D = { function() require("zk").daily({ date = "yesterday" }) end, "Yesterday's daily" }, w = { function() require("zk").daily_picker() end, "Browse daily notes" }, },}, { prefix = "<leader>" })How It Works
Section titled “How It Works”- When you call
create_note(), NeoVim prompts for a note title - The plugin invokes
zk create "title" --category <category> - The
zkbinary detects your current git project automatically - A confirmation message appears on success
Troubleshooting
Section titled “Troubleshooting””zk: command not found”
Section titled “”zk: command not found””Ensure the zk binary is in your $PATH:
# Check if zk is accessiblewhich zk
# If not, add to PATH or specify full path in setuprequire("zk").setup({ bin = "/full/path/to/zk",})“No module named ‘plenary’”
Section titled ““No module named ‘plenary’””Install plenary.nvim:
-- lazy.nvim{ "nvim-lua/plenary.nvim" }
-- packer.nvimuse "nvim-lua/plenary.nvim"Notes Not Created
Section titled “Notes Not Created”- Check that
zk createworks from terminal - Verify you’re in a git repository (for project detection)
- Check NeoVim messages with
:messages
API Reference
Section titled “API Reference”setup(opts)
Section titled “setup(opts)”Initialize the plugin with configuration options.
require("zk").setup({ bin = "zk", -- Path to zk binary (default: "zk")})create_note(note_category, project)
Section titled “create_note(note_category, project)”Create a new note with the specified category.
Parameters:
note_category(string): Either"untethered"or"tethered"project(string, optional): Project context. If nil, auto-detected from git.
Behavior:
- Prompts for note title via
vim.fn.input() - Executes
zk createasynchronously via plenary.job - Prints success/failure message
require("zk").create_note("untethered")require("zk").create_note("tethered", "my-project")tether_note(file_path, project)
Section titled “tether_note(file_path, project)”Tether an untethered note (promote to tethered).
Parameters:
file_path(string, optional): Path to the note. Defaults to current buffer.project(string, optional): Project context. If nil, auto-detected from git.
Behavior:
- Executes
zk tetherasynchronously - Reloads the buffer if the tethered file is currently open
- Prints success/failure message
require("zk").tether_note() -- Current file, auto-detect projectrequire("zk").tether_note(nil, "my-project") -- Current file, explicit projectuntether_note(file_path)
Section titled “untether_note(file_path)”Untether a tethered note (revert to untethered).
Parameters:
file_path(string, optional): Path to the note. Defaults to current buffer.
Behavior:
- Executes
zk untetherasynchronously - Reloads the buffer if the untethered file is currently open
- Prints success/failure message
require("zk").untether_note() -- Current filerequire("zk").untether_note("/path/to/note.md") -- Specific fileset_project(file_path, project)
Section titled “set_project(file_path, project)”Set or update the project for a zettel.
Parameters:
file_path(string, optional): Path to the note. Defaults to current buffer.project(string, optional): Project name. If nil, prompts for input.
Behavior:
- Prompts for project name if not provided
- Executes
zk set-projectasynchronously - Reloads the buffer if the modified file is currently open
- Prints success/failure message
require("zk").set_project() -- Current file, prompt for projectrequire("zk").set_project(nil, "my-project") -- Current file, explicit projectsearch(query, opts)
Section titled “search(query, opts)”Search zettels with optional filters.
Parameters:
query(string, optional): Full-text search query.opts(table, optional):project(string): Filter by projectcategory(string): Filter by categorytags(table): Filter by tags (AND logic)limit(number): Max resultson_results(function): Callback receiving results array
require("zk").search("authentication")require("zk").search("query", { project = "my-project", on_results = function(r) ... end })index(path)
Section titled “index(path)”Index zettels for searching.
Parameters:
path(string, optional): Path to index. Defaults to current directory.
require("zk").index() -- Index cwdrequire("zk").index("~/zk_vault/")graph(opts)
Section titled “graph(opts)”Generate an ASCII tree visualization of note relationships.
Parameters:
opts(table, optional):path(string): Path to scan. Defaults to current directory.limit(number): Maximum nodes to display. Defaults to 10.
Behavior:
- Executes
zk graphasynchronously - Opens the ASCII tree output in a vertical split
require("zk").graph() -- Graph cwd with defaultsrequire("zk").graph({ limit = 20, path = "~/zk_vault/" })preview_note(file_path)
Section titled “preview_note(file_path)”Preview a note in a floating window.
Parameters:
file_path(string, optional): Path to the note. Defaults to current buffer.
Behavior:
- Reads file content
- Opens a centered floating window with rounded border
- Sets up keymaps:
q/<Esc>to close,<CR>to open in buffer
Returns: Table with buf (buffer handle) and win (window handle)
require("zk").preview_note() -- Preview current filerequire("zk").preview_note("/path/to/note.md")preview_by_id(id)
Section titled “preview_by_id(id)”Preview a note by its ID (searches the index).
Parameters:
id(string, optional): 12-digit zettel ID. Prompts for input if nil.
Behavior:
- Searches for note with matching ID
- Opens floating preview if found
require("zk").preview_by_id("202602131045")require("zk").preview_by_id() -- Prompts for IDinsert_link(id, title, include_title)
Section titled “insert_link(id, title, include_title)”Insert a zettel link at the cursor position.
Parameters:
id(string): The 12-digit zettel IDtitle(string, optional): Note title for[[id|title]]formatinclude_title(boolean): If true and title provided, uses[[id|title]]format
Behavior:
- Formats link as
[[id]]or[[id|title]] - Inserts at cursor position
- Moves cursor to end of inserted link
require("zk").insert_link("202602131045") -- Inserts [[202602131045]]require("zk").insert_link("202602131045", "My Note", true) -- Inserts [[202602131045|My Note]]insert_link_prompt(include_title)
Section titled “insert_link_prompt(include_title)”Prompt for a zettel ID and insert a link.
Parameters:
include_title(boolean, optional): If true, searches for title and uses[[id|title]]format
require("zk").insert_link_prompt() -- Prompts for ID, inserts [[id]]require("zk").insert_link_prompt(true) -- Prompts for ID, inserts [[id|title]]link_picker(opts)
Section titled “link_picker(opts)”Open a picker to search and insert a link. Requires snacks.nvim.
Parameters:
opts(table, optional):include_title(boolean): Default format for<CR>actionquery(string): Initial search query
Picker keymaps:
<CR>- Insert link (format depends oninclude_titleoption)<C-t>- Insert link with title[[id|title]]
require("zk").link_picker() -- Opens picker, <CR> inserts [[id]]require("zk").link_picker({ include_title = true }) -- <CR> inserts [[id|title]]get_tags(callback)
Section titled “get_tags(callback)”Get all unique tags from indexed zettels (async).
Parameters:
callback(function): Called with sorted list of tags
require("zk").get_tags(function(tags) for _, tag in ipairs(tags) do print(tag) endend)get_tags_sync()
Section titled “get_tags_sync()”Get all tags synchronously. Uses a 60-second cache.
Returns: Sorted list of tag strings
local tags = require("zk").get_tags_sync()refresh_tags()
Section titled “refresh_tags()”Clear the tag cache and reload tags.
require("zk").refresh_tags()complete_tags()
Section titled “complete_tags()”Trigger manual tag completion at cursor using vim’s completion menu.
require("zk").complete_tags()setup_tag_completion()
Section titled “setup_tag_completion()”Set up automatic tag completion for markdown files.
Behavior:
- Sets
omnifuncfor markdown files - Adds
<C-x><C-t>keymap for tag completion - Only completes when cursor is in frontmatter
tags:section
require("zk").setup_tag_completion()setup_cmp()
Section titled “setup_cmp()”Register nvim-cmp source for tag completion.
Returns: true if nvim-cmp is available, false otherwise
if require("zk").setup_cmp() then print("nvim-cmp source registered")endget_backlinks(id_or_file, callback)
Section titled “get_backlinks(id_or_file, callback)”Get all notes that link to the specified zettel (async).
Parameters:
id_or_file(string): Zettel ID or file pathcallback(function): Called with list of backlink objects
Backlink object fields:
id: Zettel IDtitle: Note titleproject: Project namecategory: “untethered” or “tethered”file_path: Absolute path to file
require("zk").get_backlinks("202602131045", function(backlinks) print("Found " .. #backlinks .. " backlinks")end)get_backlinks_sync(id_or_file)
Section titled “get_backlinks_sync(id_or_file)”Get backlinks synchronously.
Returns: List of backlink objects
local backlinks = require("zk").get_backlinks_sync("202602131045")backlinks_panel(opts)
Section titled “backlinks_panel(opts)”Open a floating backlinks panel for the current note.
Parameters:
opts(table, optional):id(string): Zettel ID to show backlinks forfile(string): File path to show backlinks for
Panel keymaps:
<CR>/o- Open selected notep- Preview in floating windowq/<Esc>- Close panel
Returns: Table with buf, win, and backlinks
require("zk").backlinks_panel() -- Current noterequire("zk").backlinks_panel({ id = "202602131045" })backlinks_split(opts)
Section titled “backlinks_split(opts)”Open backlinks in a split window.
Parameters:
opts(table, optional):position(string): “right” (default), “left”, “bottom”, or “top”id(string): Zettel IDfile(string): File path
require("zk").backlinks_split()require("zk").backlinks_split({ position = "bottom" })toggle_backlinks(opts)
Section titled “toggle_backlinks(opts)”Toggle the floating backlinks panel.
require("zk").toggle_backlinks()templates
Section titled “templates”Table containing template metadata. Available templates: meeting, book-review, snippet, project-idea, user-story, feature.
for name, meta in pairs(require("zk").templates) do print(name .. ": " .. meta.description)endget_template(name)
Section titled “get_template(name)”Get template metadata by name.
Parameters:
name(string): Template name
Returns: Table with name, description, category, tags or nil if not found
local tmpl = require("zk").get_template("meeting")print(tmpl.description) -- "Meeting notes with attendees and action items"print(tmpl.category) -- "untethered"create_from_template(template_name, project)
Section titled “create_from_template(template_name, project)”Create a note from a template.
Parameters:
template_name(string): Template name (e.g.,"meeting","user-story")project(string, optional): Project context. If nil, auto-detected from git.
Behavior:
- Prompts for note title
- Executes
zk create --template <name>asynchronously - Prints success/failure message
require("zk").create_from_template("meeting")require("zk").create_from_template("feature", "my-project")template_picker(opts)
Section titled “template_picker(opts)”Open a picker to select a template and create a note. Requires snacks.nvim.
Parameters:
opts(table, optional):project(string): Project context for the new note
Behavior:
- Displays all available templates with descriptions
- On selection, prompts for note title
- Creates note using selected template
require("zk").template_picker()require("zk").template_picker({ project = "my-project" })daily(opts)
Section titled “daily(opts)”Create or open a daily note. Daily notes are idempotent - the same file is returned for the same date.
Parameters:
opts(table, optional):date(string): Date inYYYY-MM-DDformat, or"yesterday"for yesterday
Behavior:
- Determines target date (defaults to today)
- Creates daily note if it doesn’t exist
- Opens the daily note in the current buffer
require("zk").daily() -- Todayrequire("zk").daily({ date = "yesterday" }) -- Yesterdayrequire("zk").daily({ date = "2026-02-10" }) -- Specific datelist_daily(opts, callback)
Section titled “list_daily(opts, callback)”Get daily notes asynchronously.
Parameters:
opts(table, optional):week(boolean): Show only this week’s notesmonth(boolean): Show only this month’s notes
callback(function): Called with list of daily note objects
Daily note object fields:
date: Date string (YYYY-MM-DD)title: Note titlefile_path: Absolute path to file
require("zk").list_daily({ week = true }, function(notes) for _, note in ipairs(notes) do print(note.date .. ": " .. note.file_path) endend)list_daily_sync(opts)
Section titled “list_daily_sync(opts)”Get daily notes synchronously.
Parameters:
opts(table, optional): Same aslist_daily
Returns: List of daily note objects
local notes = require("zk").list_daily_sync()local this_week = require("zk").list_daily_sync({ week = true })daily_picker(opts)
Section titled “daily_picker(opts)”Open a picker to browse daily notes. Requires snacks.nvim.
Parameters:
opts(table, optional):week(boolean): Show only this week’s notesmonth(boolean): Show only this month’s notes
require("zk").daily_picker()require("zk").daily_picker({ week = true })require("zk").daily_picker({ month = true })Picker API
Section titled “Picker API”Requires snacks.nvim to be installed.
require("zk.picker").search(opts)
Section titled “require("zk.picker").search(opts)”Open picker with all indexed zettels.
require("zk.picker").live_search(opts)
Section titled “require("zk.picker").live_search(opts)”Open picker with live search (results update as you type).
require("zk.picker").untethered(opts)
Section titled “require("zk.picker").untethered(opts)”Browse only untethered notes.
require("zk.picker").tethered(opts)
Section titled “require("zk.picker").tethered(opts)”Browse only tethered notes.
Common opts:
project(string): Filter by projectcategory(string): Filter by categorytags(table): Filter by tagslimit(number): Max results
require("zk.picker").insert_link(opts)
Section titled “require("zk.picker").insert_link(opts)”Open picker specifically for inserting links. Same as require("zk").link_picker(opts).
Opts:
include_title(boolean): Use[[id|title]]format by default
Future Features
Section titled “Future Features”- Project completion