iTranslated by AI
Note Management in the Terminal (Neovim, nb, zeno.zsh)
Why take notes in the terminal?
The biggest advantages of taking notes in the terminal are customizability and operation via CLI commands.
You can freely build your preferred keybindings, search functions, and workflows.
For example, the following is possible:
- Create, search, and edit notes with a single CLI command
- Search notes using regular expressions and select them while previewing with fzf
- Automatically fetch titles and create notes just by passing a URL
- Complete all searching, creating, and editing of notes from within Neovim
Furthermore, it has recently become possible to display images in Neovim, making note management in the CLI more practical.
What I look for in notes
When it comes to keeping records, I think the most important thing is to quickly cycle through "note-taking actions" like the following:
Note-taking actions
- Create file → Edit → Save
- Search → Append
- Add references to other files
In order to cycle through these actions quickly, I felt the following requirements were necessary:
- Don't want to think about directory management
- Accessible from any path
- Don't want to think about filenames or titles
- Auto-save and manage via GitHub
I chose nb as the tool to meet these needs.
I also considered Obsidian, but since it didn't seem to offer CLI commands, I decided against it this time.
nb
nb is a CLI tool for managing notes.
You can add, edit, and search for notes using commands as shown below.
# Add a note (Alias: nb a)
nb add
# List notes (Alias: nb ls)
nb list
# Edit a note (Alias: nb e)
nb edit note_number
# Search notes (both title and content) (Alias: nb q)
nb search "keyword"
# Search by title/filename only
nb ls "keyword"
# Use grep/rg if you want to search only by content
rg "keyword" "$(nb notebooks current --path)"
Notes are automatically saved, so it's safe even for people who tend to forget to save. If you set it up to link with a GitHub repository, it will even handle pushing automatically.
Installing nb is easy using Homebrew.
# Install nb
brew install xwmx/taps/nb
# For the latest version
brew install xwmx/taps/nb --head
nb's notebook management
nb notes can be managed in units called notebooks.
The directory structure is as follows:
~/.nb/
├── home/ # Default notebook
│ ├── .git/
│ ├── 20251031152222.md
│ └── 20251101093045.md
└── work/ # Additional notebook
├── .git/
├── 20251105140030.md
└── 20251106183012.md
For example, you can manage notes separately in notebooks, such as home for everyday notes and work for business notes.
Since you can link a Git repository to each notebook, you can use a private repository for your daily notes and manage work notes locally only.
Notebook directories can be easily created using commands.
Since operations are possible via commands, you can manage notes without worrying about the directory structure.
# List notebooks
nb notebooks
# Create a notebook
nb notebooks add work
# Switch notebooks
nb use work
# Check current notebook
nb notebooks current
# Operate notes in another notebook (add notebook_name:)
nb ls work:
nb edit work:1
nb Settings
Setting the editor used by nb
The configuration file is ~/.nbrc.
This file is automatically generated upon installing nb.
The content is as follows:
#!/usr/bin/env bash
###############################################################################
# .nbrc
#
# Configuration file for `nb`, a command line note-taking, bookmarking,
# and knowledge base application with encryption, search, Git-backed syncing,
# and more in a single portable script.
#
# Edit this file manually or manage settings using the `nb settings`
# subcommand. Configuration options are set as environment variables, eg:
# export NB_ENCRYPTION_TOOL=gpg
#
# https://github.com/xwmx/nb
###############################################################################
To set the editor, run the following command:
# Set the editor to use
nb set editor nvim
Then, the following line will be added to ~/.nbrc:
export EDITOR="nvim" # Set by `nb` • Thu Jan 9 12:52:05 JST 2025
In this way, when you configure settings via commands, it automatically appends them to the configuration file.
Of course, there is no problem with manually editing the configuration file.
Setting the directory for managing nb files
Since I move between projects using ghq, I decided to place the directory where nb notes are saved under ghq management as well.
The directory specification used in ghq is set as follows:
[ghq]
root = ~/src
If it's under this directory, the nb repository will also be displayed in ghq list.
When you clone a repository from GitHub with ghq get, it is placed under ghq root + github.com/your_username/, so I set github.com/mozumasu/nb as the nb management directory.
nb set nb_dir
# Enter ~/src/github.com/mozumasu/nb and press Enter
Executing the command above adds the following line to ~/.nbrc:
export NB_DIR="${NB_DIR:-/Users/mozumasu/src/github.com/mozumasu/nb}" # Set by `nb` • Sat Jan 11 20:09:06 JST 2025
Let's add a note and check if the nb directory is actually created under ~/src/github.com/mozumasu/nb.
# Add a note
nb notebooks add example
# Check if the directory corresponding to the notebook is created
ls ~/src/github.com/mozumasu/nb
# example/ home/
Linking with a GitHub repository
Configure nb notebooks to be managed on GitHub so they can be used on other devices.
To make it clear which notebook the repository belongs to, create a remote repository named nb-notebook_name.
# Create a GitHub repository to manage the default notebook (home)
gh repo create nb-home --private
Link the prepared remote repository to the nb notebook.
# Specify the notebook to use
nb use home
# Set the remote repository to use
nb remote set git@github.com:mozumasu/nb-home.git
Now, changes to the home notebook will be automatically pushed to the GitHub repository.
Customizing emojis displayed in the list
When you list notes with nb ls, emojis are displayed for each type of note.
These emojis can be customized in the configuration file.

export NB_INDICATOR_AUDIO="🔉"
export NB_INDICATOR_BOOKMARK="🔖"
export NB_INDICATOR_DOCUMENT="📄"
export NB_INDICATOR_EBOOK="📖"
export NB_INDICATOR_ENCRYPTED="🔒"
export NB_INDICATOR_FOLDER="📂"
export NB_INDICATOR_IMAGE="🌄"
export NB_INDICATOR_PINNED="📌"
export NB_INDICATOR_TODO="✔️ "
export NB_INDICATOR_TODO_DONE="✅"
export NB_INDICATOR_VIDEO="📹"
Final example of ~/.nbrc
#!/usr/bin/env bash
###############################################################################
# .nbrc
#
# Configuration file for `nb`, a command line note-taking, bookmarking,
# and knowledge base application with encryption, search, Git-backed syncing,
# and more in a single portable script.
#
# Edit this file manually or manage settings using the `nb settings`
# subcommand. Configuration options are set as environment variables, eg:
# export NB_ENCRYPTION_TOOL=gpg
#
# https://github.com/xwmx/nb
###############################################################################
export EDITOR="nvim" # Set by `nb` • Thu Jan 9 12:52:05 JST 2025
export NB_DIR="${NB_DIR:-/Users/mozumasu/src/github.com/mozumasu/nb}" # Set by `nb` • Sat Jan 11 20:09:06 JST 2025
export NB_INDICATOR_AUDIO="🔉"
export NB_INDICATOR_BOOKMARK="🔖"
export NB_INDICATOR_DOCUMENT="📄"
export NB_INDICATOR_EBOOK="📖"
export NB_INDICATOR_ENCRYPTED="🔒"
export NB_INDICATOR_FOLDER="📂"
export NB_INDICATOR_IMAGE="🌄"
export NB_INDICATOR_PINNED="📌"
export NB_INDICATOR_TODO="✔️ "
export NB_INDICATOR_TODO_DONE="✅"
export NB_INDICATOR_VIDEO="📹"
Be happy by combining zeno.zsh and nb
It's a hassle to check the note number with nb ls and run nb edit <number> every time. It would be ideal if you could get a preview like fzf with Tab completion. You can achieve exactly that with zeno.zsh.

Tab completing nb note numbers with zeno.zsh
zeno.zsh is a zsh/fish plugin with the following features:
- Snippet configuration
- fzf completion
- Command history search
Installing zeno.zsh
I will install it using sheldon, which is a fast shell plugin manager.
# Generate the configuration file
sheldon init --shell zsh
# Initialize new config file `~/.config/sheldon/plugins.toml`? [y/N] y
# Initialized ~/.config/sheldon/plugins.toml
Executing this generates a sheldon configuration file like the one below.
# `sheldon` configuration file
# ----------------------------
#
# You can modify this file directly or you can use one of the following
# `sheldon` commands which are provided to assist in editing the config file:
#
# - `sheldon add` to add a new plugin to the config file
# - `sheldon edit` to open up the config file in the default editor
# - `sheldon remove` to remove a plugin from the config file
#
# See the documentation for more https://github.com/rossmacarthur/sheldon#readme
shell = "zsh"
[plugins]
# For example:
#
# [plugins.base16]
# github = "chriskempson/base16-shell"
Additionally, append the following command to your zsh configuration file (~/.zshrc) to load the plugins.
eval "$(sheldon source)"
To install zeno.zsh, add it to the plugins section as follows:
[plugins]
+ [plugins.zeno]
+ github = "yuki-yano/zeno.zsh"
+ [plugins.fast-syntax-highlighting]
+ github = "zdharma-continuum/fast-syntax-highlighting"
To use zeno.zsh, you need to append the following settings to your zsh configuration file (~/.zshrc).
export ZENO_HOME=~/.config/zeno
# git file preview with color
export ZENO_GIT_CAT="bat --color=always"
# git folder preview with color
# export ZENO_GIT_TREE="eza --tree"
if [[ -n $ZENO_LOADED ]]; then
bindkey ' ' zeno-auto-snippet
# if you use zsh's incremental search
# bindkey -M isearch ' ' self-insert
bindkey '^m' zeno-auto-snippet-and-accept-line
bindkey '^i' zeno-completion
bindkey '^xx' zeno-insert-snippet # open snippet picker (fzf) and insert at cursor
bindkey '^x ' zeno-insert-space
bindkey '^x^m' accept-line
bindkey '^x^z' zeno-toggle-auto-snippet
# preprompt bindings
bindkey '^xp' zeno-preprompt
bindkey '^xs' zeno-preprompt-snippet
# Outside ZLE you can run `zeno-preprompt git {{cmd}}` or `zeno-preprompt-snippet foo`
# to set the next prompt prefix; invoking them with an empty argument resets the state.
bindkey '^r' zeno-smart-history-selection # smart history widget
# fallback if completion not matched
# (default: fzf-completion if exists; otherwise expand-or-complete)
# export ZENO_COMPLETION_FALLBACK=expand-or-complete
fi
zeno.zsh settings are written in ~/.config/zeno/config.yml.
If you want to use Tab completion for nb note numbers, add the following to the completions section.
completions:
- name: nb edit
patterns:
- "^nb e( .*)? $"
- "^nb edit( .*)? $"
sourceCommand: "nb ls --no-color | grep -E '^\\[[0-9]+\\]'"
options:
--ansi: true # Enable ANSI colors
--prompt: "'nb edit >'"
--preview: "echo {} | sed -E 's/^\\[([0-9]+)\\].*/\\1/' | xargs nb show"
callback: "sed -E 's/^\\[([0-9]+)\\].*/\\1/'"
It is also recommended to set up completion for checking subcommand help as well.
completions:
- name: nb subcommands
patterns:
- ^\s*nb\s*$
- ^\s*nb\s+help\s*$
sourceCommand: nb subcommands
options:
--prompt: "'nb subcommand >'"
To register snippets, add the following.
completions:
...
+ snippets:
+ - name: Edit Note
+ keyword: nbe
+ snippet: nb edit
+
+ - name: List Note
+ keyword: nbl
+ snippet: nb ls --limit 20
+
+ - name: List All Note
+ keyword: nbla
+ snippet: nb ls --all
+
+ - name: nb search
+ keyword: nbg
+ snippet: rg "{{keyword}}" "$(nb notebooks current --path)"
Snippets can be expanded with the space key as shown below.

Shell functions configured for nb
nba: Add an article from a URL to notes
This function automatically fetches the title of an article when given a URL and adds a note to nb.

# nb add article - Add a note with article title and URL
# Usage: nba <url> - Auto-fetch title from URL
# nba <title> <url> - Use specified title
function nba() {
if [ $# -lt 1 ]; then
echo "Usage: nba <url> # Auto-fetch title"
echo " nba <title> <url> # Manual title"
return 1
fi
local title=""
local url=""
if [ $# -eq 1 ]; then
url="$1"
echo "Fetching title from: $url"
title=$(curl -sL --max-redirs 3 --max-time 5 --compressed "$url" | head -c 512 | perl -0777 -ne 'print $1 if /<title[^>]*>([^<]+)<\/title>/i')
title=$(echo "$title" | perl -pe 's/^\s+|\s+$//g; s/\s+/ /g')
if [ -z "$title" ]; then
echo "Error: Could not fetch title from URL"
return 1
fi
echo "Title: $title"
else
title="$1"
url="$2"
fi
local content="# ${title}
Reference: [${title}](${url})"
nb add --filename "${title}.md" --content "$content"
echo "Note created: [${title}](${url})"
}
nbq: Search results selected and edited with fzf
This function allows you to select from nb search results while previewing with fzf and edit the note immediately.

# nb query - Search notes and select with fzf preview
# Usage: nbq <search query>
function nbq() {
if [ -z "$1" ]; then
echo "Usage: nbq <search query>"
return 1
fi
local query="$*"
local results=$(nb q "$query" --no-color 2>/dev/null | grep -E '^\[[0-9]+\]')
if [ -z "$results" ]; then
echo "No results found for: $query"
return 1
fi
export _NBQ_QUERY="$query"
local selected=$(echo "$results" | fzf --ansi \
--preview 'note_id=$(echo {} | sed -E "s/^\[([0-9]+)\].*/\1/")
echo "=== Note [$note_id] ==="
echo ""
nb show "$note_id" | head -5
echo ""
echo "=== Matching lines ==="
echo ""
nb show "$note_id" | grep -i --color=always -C 2 "$_NBQ_QUERY" | head -30' \
--preview-window=right:60%:wrap \
--header "Search: $query")
unset _NBQ_QUERY
if [ -n "$selected" ]; then
local note_id=$(echo "$selected" | sed -E 's/^\[([0-9]+)\].*/\1/')
nb edit "$note_id"
fi
}
Linking references in nb
To link to another page, use [[]] as follows:
[[Page Title]]
Managing images with nb
Images can be imported using nb import path_to_image.
When you paste an image into the terminal, its path is inserted, so you can manage images in nb simply by typing nb import and then pasting the image. I use Raycast's clipboard history for this.
# Paste the image path from Raycast clipboard history
nb import ../../../../../Documents/screenshot/Screenshot%202025-11-06%208.48.40.png
To rename a file, use nb rename note_number new_filename.
# Rename a file by specifying its number
$ nb rename 13 wezterm-doc.png
Moving: [13] wezterm-doc
To: wezterm-doc.png
Proceed? [y/N] y
Moved to: [13] 🌄 wezterm-doc.png
It is also possible to specify a filename during import as follows:
# Specify the filename when importing an image
nb import path_to_image image_filename
To link an image in a note, write it as follows.
Note that the ./ prefix for the current path is not required.

Todo functionality
nb has two types of task management features as follows:
- task: No note functionality, only markdown checkboxes.
- todo: A combination of note functionality and checkbox functionality. You can also add tasks.
task can be managed and displayed using the nb tasks command for - [ ] within the body.
However, since I use - [ ] for article drafts and such, I do not use task and only use todo.
todo can be added with the nb todo add command, which adds a file with the .todo.md extension.
# todo
nb todo add "task to do"
# Mark as done
nb todo do task_id
# Mark as undone
nb todo undo task_id
For listing todos, use the following commands:
# List all tasks
nb todos
# Closed tasks
nb todos closed
# Open tasks
nb todo open
In particular, I use commands to switch regular notes to tasks to manage things to do/not to do.
Since I usually write an overview of what needs to be done in a note, I convert that note into a todo for management.
# Convert note 7 ("demo.md") to todo "demo.todo.md"
nb rename 7 --to-todo
# Convert todo to note
nb rename 7 --to-note
Neovim
I use Neovim as the editor for editing notes for the following reasons:
- Keybindings for the most efficient text editing
- Both appearance and keybindings are customizable
- External commands can be executed
- Existing plugins can be used as they are
I will introduce the Neovim settings configured for nb.
Buffer Title Settings
Since nb's filenames are automatically generated as timestamps, it's impossible to tell what the content is from the filename alone.

You can display the title from the first line of the note in the tabs or buffer lines instead of the filename.
First, define the nb helper functions.
local M = {}
-- nb command prefix
local NB_CMD = "NB_EDITOR=: NO_COLOR=1 nb"
-- Get nb note directory path
function M.get_nb_dir()
-- Change this according to your nb directory path
return vim.fn.expand("~/.nb")
end
-- Execute nb command
function M.run_cmd(args)
local cmd = NB_CMD .. " " .. args
local output = vim.fn.systemlist(cmd)
if vim.v.shell_error ~= 0 then
return nil
end
return output
end
-- Parse list line and return structured data
-- Example: "[1] 🌄 image.png" -> { note_id = "1", name = "image.png", is_image = true }
-- Example: "[2] Note Title" -> { note_id = "2", name = "Note Title", is_image = false }
function M.parse_list_item(line)
local note_id = line:match("^%[(.-)%]")
if not note_id then
return nil
end
local is_image = line:match("🌄") ~= nil
local name
if is_image then
name = line:match("%%[%d+%]%%s*🌄%%s*(.+)$")
else
name = line:match("%%[%d+%]%%s*(.+)$")
end
if not name then
return nil
end
return {
note_id = note_id,
name = vim.trim(name),
is_image = is_image,
text = line,
}
end
-- Get list of parsed items
function M.list_items()
local output = M.run_cmd("list --no-color")
if not output then
return nil
end
local items = {}
for _, line in ipairs(output) do
local item = M.parse_list_item(line)
if item then
table.insert(items, item)
end
end
return items
end
-- Function to get the title of an nb note (for bufferline)
function M.get_title(filepath)
local nb_dir = M.get_nb_dir()
if not filepath:match("^" .. nb_dir) then
return nil
end
local file = io.open(filepath, "r")
if not file then
return nil
end
local first_line = file:read("*l")
file:close()
if first_line then
return first_line:match("^#%s+(.+)")
end
return nil
end
-- Get file path from note ID
function M.get_note_path(note_id)
local output = M.run_cmd("show --path " .. note_id)
if output and output[1] then
return vim.trim(output[1])
end
return ""
end
return M
In LazyVim, bufferline.nvim is used by default for tab display, so we extend the configuration as follows.
return {
"akinsho/bufferline.nvim",
opts = function(_, opts)
local nb = require("config.nb")
opts.options = opts.options or {}
opts.options.name_formatter = function(buf)
local title = nb.get_title(buf.path)
return title or buf.name
end
end,
}
With this setting, the title from the first line will be displayed in the buffer line when you open an nb note.

Displaying nb titles in the bufferline
Search Settings
Fuzzy finder plugins are useful when you want to search for and open notes by their titles or by grep-searching their content. In LazyVim, snacks.nvim is used by default, so we will utilize that.
I have configured it so that searching can be done by the title of the memo rather than the filename.

Searching for nb notes with snacks.nvim
Creating the following file allows you to search for nb notes with snacks.nvim.
-- Search and open notes from the title list using snacks.nvim
local function pick_notes()
local nb = require("config.nb")
local Snacks = require("snacks")
local items = nb.list_items()
if not items or #items == 0 then
vim.notify("No notes found", vim.log.levels.WARN)
return
end
Snacks.picker({
title = "nb Notes",
items = items,
format = function(item)
return { { item.text } }
end,
preview = function(ctx)
local item = ctx.item
if not item.file then
item.file = nb.get_note_path(item.note_id)
end
return Snacks.picker.preview.file(ctx)
end,
confirm = function(picker, item)
picker:close()
if item then
vim.cmd.edit(nb.get_note_path(item.note_id))
end
end,
})
end
-- Grep search note contents using snacks.nvim
local function grep_notes()
local nb = require("config.nb")
local Snacks = require("snacks")
Snacks.picker.grep({
dirs = { nb.get_nb_dir() },
})
end
return {
"folke/snacks.nvim",
keys = {
{ "<leader>np", pick_notes, desc = "nb picker" },
{ "<leader>ng", grep_notes, desc = "nb grep" },
},
}
With this configuration, you can use the following keymaps:
-
<leader>np- Search and open from the note title list -
<leader>ng- Grep search note content and open

Opening a note with title search

Opening a note with grep search
Adding Deletion Functionality
I'll add functionality to delete notes directly from within the picker. Add a deletion function to config/nb.lua (add it before return M).
+ -- Delete a note
+ function M.delete_note(note_id)
+ local output = M.run_cmd("delete --force " .. note_id)
+ return output ~= nil
+ end
return M
Also, add the deletion action and keybinding to pick_notes in plugins/nb.lua.
Snacks.picker({
title = "nb Notes",
items = items,
-- ... omitted ...
confirm = function(picker, item)
picker:close()
if item then
vim.cmd.edit(nb.get_note_path(item.note_id))
end
end,
+ actions = {
+ delete_note = function(picker)
+ local item = picker:current()
+ if item then
+ vim.ui.select({ "Yes", "No" }, {
+ prompt = "Delete: " .. item.name .. "?",
+ }, function(choice)
+ if choice == "Yes" then
+ if nb.delete_note(item.note_id) then
+ vim.notify("Deleted: " .. item.name, vim.log.levels.INFO)
+ picker:close()
+ pick_notes()
+ else
+ vim.notify("Failed to delete", vim.log.levels.ERROR)
+ end
+ end
+ end)
+ end
+ end,
+ },
+ win = {
+ input = {
+ keys = {
+ ["<C-d>"] = { "delete_note", mode = { "n", "i" }, desc = "Delete note" },
+ },
+ },
+ },
})
Now, when you press <C-d> within the picker, a confirmation dialog appears, allowing you to delete the selected note.
Settings to Add Notes
Enable adding nb notes from Neovim.
Add a function to config/nb.lua for adding notes (add it before return M).
+ -- Add a note and return the ID
+ function M.add_note(title)
+ local timestamp = os.date("%Y%m%d%H%M%S")
+ local note_title = title and title ~= "" and title or os.date("%Y-%m-%d %H:%M:%S")
+ local escaped_title = note_title:gsub('"', '\\"')
+ local args = string.format('add --no-color --filename "%s.md" --title "%s"', timestamp, escaped_title)
+
+ local output = M.run_cmd(args)
+ if not output then
+ return nil
+ end
+
+ -- Get the ID of the added note
+ for _, line in ipairs(output) do
+ local note_id = line:match("%[(%d+)%]")
+ if note_id then
+ return note_id
+ end
+ end
+ return nil
+ end
return M
Also, add a function and keymap for adding notes to plugins/nb.lua.
+ -- Add a note and open it
+ local function add_note()
+ local nb = require("config.nb")
+ vim.ui.input({ prompt = "Note title (empty for timestamp): " }, function(title)
+ local note_id = nb.add_note(title)
+ if note_id then
+ local path = nb.get_note_path(note_id)
+ if path and path ~= "" then
+ vim.cmd.edit(path)
+ end
+ else
+ vim.notify("Failed to add note", vim.log.levels.ERROR)
+ end
+ end)
+ end
return {
"folke/snacks.nvim",
keys = {
+ { "\u003cleader\u003ena", add_note, desc = "nb add" },
{ "\u003cleader\u003enp", pick_notes, desc = "nb picker" },
{ "\u003cleader\u003eng", grep_notes, desc = "nb grep" },
},
}
Now you can add and open notes with \u003cleader\u003ena.

Adding nb notes from Neovim
Settings to Import Images
Enable importing images into nb from Neovim and inserting Markdown links.
Add a function for importing images to config/nb.lua (add it before return M).
+ -- Import an image into nb
+ function M.import_image(image_path, new_filename)
+ if not image_path or image_path == "" then
+ return nil, "No path provided"
+ end
+
+ -- Remove leading/trailing whitespace and quotes, then expand the path
+ local cleaned_path = image_path:gsub("^%s*['\"]?", ""):gsub("['\"]?%s*$", "")
+ local expanded_path = vim.fn.expand(cleaned_path)
+
+ -- Check if file exists
+ if vim.fn.filereadable(expanded_path) == 0 then
+ return nil, "File not found: " .. expanded_path
+ end
+
+ -- Add new filename if specified
+ local final_filename
+ if new_filename and new_filename ~= "" then
+ -- Add original extension if missing
+ if not new_filename:match("%.%w+$") then
+ local ext = vim.fn.fnamemodify(expanded_path, ":e")
+ new_filename = new_filename .. "." .. ext
+ end
+ final_filename = new_filename
+ else
+ final_filename = vim.fn.fnamemodify(expanded_path, ":t")
+ end
+
+ -- Build and execute command
+ local escaped_path = vim.fn.shellescape(expanded_path)
+ local args = "import --no-color " .. escaped_path
+ if new_filename and new_filename ~= "" then
+ args = args .. " " .. vim.fn.shellescape(new_filename)
+ end
+
+ local output = M.run_cmd(args)
+ if not output then
+ return nil, "Import failed"
+ end
+
+ -- Get the ID of the imported file
+ for _, line in ipairs(output) do
+ local note_id = line:match("%[(%d+)%]")
+ if note_id then
+ return note_id, final_filename
+ end
+ end
+ return nil, "Could not parse import result"
+ end
return M
Also, add the image import function and keymap to plugins/nb.lua.
+ -- Import image and insert Markdown link
+ local function import_image()
+ local nb = require("config.nb")
+ vim.ui.input({ prompt = "Image path: ", completion = "file" }, function(image_path)
+ if not image_path or image_path == "" then
+ return
+ end
+
+ -- Input new filename (leave empty for original)
+ vim.ui.input({ prompt = "New filename (empty to keep original): " }, function(new_filename)
+ local note_id, result = nb.import_image(image_path, new_filename)
+ if note_id then
+ local filename = result
+ local link = string.format("", filename, filename)
+ vim.api.nvim_put({ link }, "c", true, true)
+ vim.notify("Imported: " .. filename, vim.log.levels.INFO)
+ else
+ vim.notify(result or "Failed to import image", vim.log.levels.ERROR)
+ end
+ end)
+ end)
+ end
return {
"folke/snacks.nvim",
keys = {
{ "<leader>na", add_note, desc = "nb add" },
+ { "<leader>ni", import_image, desc = "nb import image" },
{ "<leader>np", pick_notes, desc = "nb picker" },
{ "<leader>ng", grep_notes, desc = "nb grep" },
},
}
Now you can import images with <leader>ni.
- Enter (or paste) the image path.
- Enter a new filename (press Enter while empty to use the original name).
The image will be imported into nb, and a Markdown image link will be inserted at the cursor position.
Settings to Insert Links
Using the snacks.nvim picker, you can select an nb image or note and insert a link.
Add the link insertion function and keymap to plugins/nb.lua.
+ -- Insert link
+ local function link_item()
+ local nb = require("config.nb")
+ local Snacks = require("snacks")
+ local items = nb.list_items()
+
+ if not items or #items == 0 then
+ vim.notify("No items found", vim.log.levels.WARN)
+ return
+ end
+
+ Snacks.picker({
+ title = "nb Link",
+ items = items,
+ format = function(item)
+ return { { item.text } }
+ end,
+ preview = function(ctx)
+ local item = ctx.item
+ if not item.file then
+ item.file = nb.get_note_path(item.note_id)
+ end
+ return Snacks.picker.preview.file(ctx)
+ end,
+ confirm = function(picker, item)
+ picker:close()
+ if item then
+ local link
+ if item.is_image then
+ link = string.format("", item.name, item.name)
+ else
+ link = string.format("[[%s]]", item.name)
+ end
+ vim.api.nvim_put({ link }, "c", true, true)
+ end
+ end,
+ })
+ end
return {
"folke/snacks.nvim",
keys = {
{ "<leader>na", add_note, desc = "nb add" },
{ "<leader>ni", import_image, desc = "nb import image" },
+ { "<leader>nl", link_item, desc = "nb link" },
{ "<leader>np", pick_notes, desc = "nb picker" },
{ "<leader>ng", grep_notes, desc = "nb grep" },
},
}
Now, <leader>nl will display a picker with a preview.
If you select an image (marked with 🌄), it will be inserted in  format, and if you select a note, it will be inserted in [[title]] format.
Integration with Claude Code
Since nb notes are saved locally as Markdown files, they integrate well with Claude Code. I create notes for logs and use slash commands to append or summarize session contents.
# Create a log notebook
nb notebooks add log
It is also convenient to configure formatting using Hooks, as content edited by Claude Code will be automatically formatted. In the following example, rumdl is used to format Markdown files.
{
"hooks": {
"PostToolUse": [{
"matcher": "Write|Edit|MultiEdit",
"hooks": [{
"type": "command",
"command": "jq -r '.tool_input.file_path | select(endswith(\".md\"))' | xargs -r rumdl fmt"
}]
}]
}
}
Introduction to rumdl
Installation
brew install rumdl
Usage
# Lint Markdown files in the current directory
rumdl check .
# Format files (returns exit code 0 on success even if unfixable violations remain)
rumdl fmt .
# Report violations that cannot be automatically fixed (returns exit code 1 if violations remain)
rumdl check --fix .
# Create a default configuration file
rumdl init
Configuration files can be placed in the following locations:
-
.rumdl.tomlorrumdl.tomlwithin the project directory or parent directory -
.config/rumdl.toml(complies with the config-dir convention)
Since existing .markdownlint.json or .markdownlint.yaml can also be used, you can leverage your existing settings when introducing rumdl.
# rumdl configuration file
# Global configuration options
[global]
# List of rules to disable (uncomment and modify as needed)
# disable = ["MD013", "MD033"]
# List of rules to enable exclusively (if specified, only these rules will be executed)
# enable = ["MD001", "MD003", "MD004"]
# List of file/directory patterns to include in linting (if specified, only these will be linted)
# include = [
# "docs/*.md",
# "src/**/*.md",
# "README.md"
# ]
# List of file/directory patterns to exclude from linting
exclude = [
# Common directories to exclude
".git",
".github",
"node_modules",
"vendor",
"dist",
"build",
# Specific files or patterns
"CHANGELOG.md",
"LICENSE.md",
]
# Respect .gitignore files when scanning directories (default: true)
respect-gitignore = true
# Markdown flavor/dialect (uncomment to enable)
# Options: mkdocs, gfm, commonmark
# flavor = "mkdocs"
# Rule-specific settings (uncomment and modify as needed)
# [MD003]
# style = "atx" # Heading style (atx, atx_closed, setext)
# [MD004]
# style = "asterisk" # Unordered list style (asterisk, plus, dash, consistent)
# [MD007]
# indent = 4 # Unordered list indentation
# [MD013]
# line-length = 100 # Line length
# code-blocks = false # Exclude code blocks from line length checks
# tables = false # Exclude tables from line length checks
# headings = true # Include headings in line length checks
# [MD044]
# names = ["rumdl", "Markdown", "GitHub"] # Proper nouns that should use correct casing
# code-blocks = false # Check proper nouns in code blocks (default: false, skips code blocks)
I use the following slash commands:
- https://github.com/mozumasu/dotfiles/blob/main/.config/claude/commands/nb-log.md
- https://github.com/mozumasu/dotfiles/blob/main/.config/claude/commands/nb.md
Make sure to add the nb note directory using the /add-dir command or in claude/settings.json so that Claude Code can access it.
{
"permissions": {
"additionalDirectories": [
"~/src/github.com/mozumasu/nb", # Add the nb note directory here
"~/src/github.com/mozumasu/zenn/articles"
]
},
}
Conclusion
In this article, I introduced a terminal-based note management environment combining nb, Neovim, and zeno.zsh.
- nb: Create, search, and edit notes via the CLI.
-
Shell functions: Automatically fetch titles from URLs with
nba, and search with fzf previews withnbq. - Neovim integration: Complete note searching and adding within the editor using snacks.nvim.
- zeno.zsh: Comfortable note selection with fzf completion.
Terminal-based note management takes a little effort to set up initially, but once built, you can realize a workflow that is uniquely your own.
I hope this article provides a helpful reference for building your own note-taking environment.
Discussion
On WSL2, this setup helps open images using msedge.exe instead of Linux browsers.