📝

ターミナルでメモ管理 (Neovim, nb, zeno.zsh)

に公開
1

なぜターミナルでメモを?

ターミナルでメモを取る最大のメリットは カスタマイズ性CLIコマンドでの操作 です。
自分好みのキーバインド、検索機能、ワークフローを自由に構築できます。

たとえば以下のようなことが可能です。

  • CLIコマンド一発でメモを作成・検索・編集
  • 正規表現でメモを検索し、fzfでプレビューしながら選択
  • URLを渡すだけでタイトルを自動取得してメモを作成
  • Neovimからメモの検索・作成・編集をすべて完結

また、最近ではNeovimで画像を表示できるようになり、CLIでのメモ管理がより実用的になりました。

メモに求めるもの

記録を残す上で最も大切なのは、以下のような「メモを取る動作」を高速に回すことだと思います。

メモを取る動作

  • ファイルを作成 → 編集 → 保存
  • 検索 → 追記
  • 別ファイルへの参照を追加

そしてこの動作を高速に回すためには、以下のような要件が必要だと考えました。

  • ディレクトリ管理を意識しない
  • どのパスからでもアクセスできる
  • ファイル名もタイトルも考えたくない
  • 自動保存してGitHubで管理

これらを満たすツールとして、私は nb を選びました。
Obsidian も検討したのですが、CLIコマンドの提供が無さそうだったので、今回は見送りました。

nb

nbはメモを管理するCLIツールです。

以下のように、コマンドでメモを追加/編集/検索などができます。

nbの基本操作コマンド
# メモを追加 (エイリアス: nb a)
nb add

# メモ一覧を表示 (エイリアス: nb ls)
nb list

# メモを編集 (エイリアス: nb e)
nb edit メモ番号

# メモを検索(タイトル・内容両方) (エイリアス: nb q)
nb search "キーワード"

# タイトル・ファイル名のみで検索
nb ls "キーワード"

# 内容のみで検索したい場合はgrep/rgを使用
rg "キーワード" "$(nb notebooks current --path)"

メモは自動保存されるため、保存をサボってしまう人でも安心ですね。
GitHubリポジトリと連携する設定をしておくと、自動でpushまでやってくれます。

nbのインストールはHomebrewで簡単に行えます。

# nbのインストール
brew install xwmx/taps/nb
# 最新版の場合
brew install xwmx/taps/nb --head

nbのnotebook管理

nbのメモはnotebookという単位で管理することができます。
ディレクトリ構造は以下のようになります。

nbのディレクトリ構成
~/.nb/
├── home/       # デフォルトのnotebook
│   ├── .git/
│   ├── 20251031152222.md
│   └── 20251101093045.md
└── work/       # 追加したnotebook
    ├── .git/
    ├── 20251105140030.md
    └── 20251106183012.md

例えば、普段使いのメモは home 、仕事用のメモは work のように notebook で分けて管理できます。
notebookごとにGitリポジトリを紐づける設定ができるので、普段使いのメモはプライベートリポジトリに、仕事用のメモはローカルのみで管理するといった使い方も可能です。

notebook のディレクトリはコマンドで簡単に作成できます。
コマンドで操作が可能なため、ディレクトリ構造を意識せずにメモを管理できます。

notebookの操作コマンド
# notebook一覧を表示
nb notebooks

# notebookを作成
nb notebooks add work

# notebookを切り替え
nb use work

# 現在のnotebookを確認
nb notebooks current

# 別のnotebookのメモを操作(notebook名:をつける)
nb ls work:
nb edit work:1

nbの設定

nbで使用するエディタの設定

設定ファイルは~/.nbrcを使用します。
このファイルはnbをインストールと同時に自動生成されます。
内容は以下のようになっています。

~/.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
###############################################################################

エディタを設定するには以下のコマンドを実行します。

# 使用するエディタを設定
nb set editor nvim

すると、~/.nbrc に以下の行が追加されます。

export EDITOR="nvim" # Set by `nb` • Thu Jan  9 12:52:05 JST 2025

このように、コマンドで設定を行うと自動的に設定ファイルに追記してくれます。
もちろん、手動で設定ファイルを編集しても問題ありません。

nbのファイルを管理するディレクトリの設定

各プロジェクトの移動はghqで行なっているため、nbのメモを保存するディレクトリもghqの管理下に置くことにしました。
ghqで使用しているディレクトリの指定は以下のように設定しています。

[ghq]
 root = ~/src

このディレクトリ配下であればghq listでnbのリポジトリも表示されるようになります。
ghq getでGitHubからリポジトリをクローンすると、ghqのroot + github.com/自分のユーザー名/ 配下に配置されるため github.com/mozumasu/nb をnb管理ディレクトリに設定します。

nb set nb_dir
# ~/src/github.com/mozumasu/nbを入力してEnter

上記のコマンドを実行すると、 ~/.nbrc に以下の行が追加されます。

export NB_DIR="${NB_DIR:-/Users/mozumasu/src/github.com/mozumasu/nb}" # Set by `nb` • Sat Jan 11 20:09:06 JST 2025

ノートを追加して、実際に ~/src/github.com/mozumasu/nb 配下にnbのディレクトリが追加されるか確認してみましょう。

# ノートを追加
nb notebooks add example
# ノートに対応するディレクトリが作成されているか確認
ls ~/src/github.com/mozumasu/nb
# example/ home/

GitHubリポジトリと連携する

nbのノートブックをGitHub管理して別端末でも利用できるように設定します。
どのノートブックのリポジトリかわかるように、nb-ノートブック名でリモートリポジトリを作成しましょう。

# デフォルトのnotebook (home) を管理するGitHubリポジトリを作成
gh repo create nb-home --private

用意したリモートリポジトリをnbのnotebookに紐づけます。

# 使用するノートブックを指定
nb use home
# 使用するリモートリポジトリの設定
nb remote set git@github.com:mozumasu/nb-home.git

これで、homeノートブックの変更が自動でGitHubリポジトリにpushされるようになります。

リストで表示される絵文字のカスタマイズ

nb ls でノート一覧を表示したときに、ノートの種類ごとに絵文字が表示されます。
この絵文字は設定ファイルでカスタマイズ可能です。

nbのリストの絵文字

~/.nbrc
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="📹"
最終的な~/.nbrcの例
~/.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="📹"

zeno.zshとnbを組みあわせて幸せに

いちいち nb ls でノート番号を確認して nb edit 番号 とするのは面倒です。
Tab補完で、fzfのようにプレビューが出せれば最高ですよね。
それ、zeno.zshでできちゃうんです。

alt nbでzeno.zshの補完を使う
nbのノート番号をzeno.zshでTab補完する

zeno.zsh は zsh/fishのプラグインで以下の機能があります。

  • スニペット設定
  • fzf補完
  • コマンド履歴検索

zeno.zshのインストール

シェルのプラグインマネージャーとして、動作が早い sheldon を使用してインストールします。

# 設定ファイルを生成
sheldon init --shell zsh

# Initialize new config file `~/.config/sheldon/plugins.toml`? [y/N] y
# Initialized ~/.config/sheldon/plugins.toml

実行すると以下のようなsheldonの設定ファイルが生成されます。

~/.config/sheldon/plugins.toml
# `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"

合わせて、以下のコマンドを zshの設定ファイル(~/.zshrc)に追記して、プラグインを読み込むようにします。

~/.zshrc
eval "$(sheldon source)"

zeno.zshをインストールしたい場合は、以下のようにpluginsセクションに追記します。

~/.config/sheldon/plugins.toml
[plugins]
+ [plugins.zeno]
+ github = "yuki-yano/zeno.zsh"
+ [plugins.fast-syntax-highlighting]
+ github = "zdharma-continuum/fast-syntax-highlighting"

zeno.zshを使用するためには以下のような設定をzshの設定ファイルに追記する必要があります。

~/.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の設定は、~/.config/zeno/config.yml に記述します。
nbのノート番号をTab補完したい場合はcompletionsセクションに以下のように追加します。

~/.config/zeno/config.yml
completions:
  - name: nb edit
    patterns:
      - "^nb e( .*)? $"
      - "^nb edit( .*)? $"
    sourceCommand: "nb ls --no-color | grep -E '^\\[[0-9]+\\]'"
    options:
      --ansi: true  # ← ANSIカラー有効
      --prompt: "'nb edit >'"
      --preview: "echo {} | sed -E 's/^\\[([0-9]+)\\].*/\\1/' | xargs nb show"
    callback: "sed -E 's/^\\[([0-9]+)\\].*/\\1/'"

合わせてサブコマンドのヘルプを確認するための補完を設定するのもおすすめです。

~/.config/zeno/config.yml
completions:
  - name: nb subcommands
    patterns:
      - ^\s*nb\s*$
      - ^\s*nb\s+help\s*$
    sourceCommand: nb subcommands
    options:
      --prompt: "'nb subcommand >'"

スニペットを登録する場合は以下のように追記してください。

~/.config/zeno/config.yml
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)"

スニペットは以下のように、space キーで展開できます。

zeno.zshのスニペットをnbで使う

nb用に設定したシェル関数

nba: URLから記事をメモに追加

URLを渡すと記事のタイトルを自動取得してnbにメモを追加する関数です。

nbにURLから記事を追加する

~/.zshrc
# 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}

参照: [${title}](${url})"

  nb add --filename "${title}.md" --content "$content"
  echo "Note created: [${title}](${url})"
}

nbq: 検索結果をfzfで選択して編集

nb searchの検索結果をfzfでプレビューしながら選択し、そのまま編集できる関数です。
nbの検索結果をfzfでプレビューして編集する

~/.zshrc
# 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
}

nbのリンク参照

別のページにリンクを貼る場合は、以下のように[[]]を使用します。

[[ページタイトル]]

nbで画像を管理する

画像のインポートは nb import 画像のパス で行います。
画像をターミナル上でペーストするとパスが入るため、nb import と入力したあとにペーストするだけで画像をnbで管理することができます。
私はRaycastのクリップボード履歴からペーストして使っています。

# 画像パスはRaycastのクリップボード履歴からペースト
nb import ../../../../../Documents/screenshot/スクリーンショット%202025-11-06%208.48.40.png

ファイル名を変更したい場合は nb rename ノート番号 新しいファイル名 を使用します。

# 番号を指定してファイル名を変更
$ nb rename 13 wezterm-doc.png
Moving:   [13] wezterm-doc
To:       wezterm-doc.png
Proceed?  [y/N] y
Moved to: [13] 🌄 wezterm-doc.png

以下のようにimport時にファイル名を指定することも可能です。

# 画像のインポート時にファイル名を指定する
nb import 画像のパス 画像ファイル名

ノートで画像のリンクを貼る場合は以下のように記述します。
現在のパスを示す ./ は不要なので注意しましょう 。

![画像のタイトル](画像のファイル名.png)

Todo機能

nb には 以下の2つのようなタスク管理の機能があります。

  • task: ノート機能はなく、markdownのチェックボックスのみ
  • todo: ノート機能にチェックボックスチェックボックスの機能が組みあわさったもの. タスクも追加できる

taskは本文中の - [ ]nb tasksコマンドで表示して管理することができます。
しかし、記事の下書きなどで - [ ] を使用するため私は task は使用せず、 todo のみ使用しています。

todoは nb todo addコマンドで追加でき、拡張子 .todo.md という拡張子でファイルが追加されます。

# todo
nb todo add "やること"
# 完了にする
nb todo do タスクID
# 未完了にする
nb todo undo タスクID

todoの一覧表示では以下のコマンドを使用します。

# すべてのタスク一覧
nb todos
# 完了タスク
nb todos closed
# 未完了タスク
nb todo open

特に、やること/やらないことの管理のために、通常のノートをタスクに切り替えるコマンドを使用しています。
やることは大抵ノートに概要を書くので、そのノートをtodoに変換して管理しています。

# ノート7 ("demo.md") をtodo "demo.todo.md" に変換
nb rename 7 --to-todo
# todoをノートに変換
nb rename 7 --to-note

Neovim

Neovimをメモ編集用のエディタとして使用している理由は以下の通りです。

  • 最も効率よくテキストするためのキーバインド
  • 見た目もキーバインドもカスタマイズ可能
  • 外部コマンドが実行できる
  • 普段使用しているプラグインをそのまま使える

nb用に設定したNeovimの設定を紹介していきます。

バッファタイトルの設定

nbのファイル名は自動でタイムスタンプになるため、ファイル名だけでは内容がわかりません。

nvimでnbのノートを表示

タブやバッファラインにファイル名ではなく1行目のタイトルを表示できます。

まず、nbのヘルパー関数を定義します。

~/.config/nvim/lua/config/nb.lua
local M = {}

-- nbコマンドのプレフィックス
local NB_CMD = "NB_EDITOR=: NO_COLOR=1 nb"

-- nbのノートディレクトリパスを取得
function M.get_nb_dir()
  -- nbのディレクトリパスに合わせて変更してください
  return vim.fn.expand("~/.nb")
end

-- nbコマンドを実行
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

-- リスト行をパースして構造化データを返す
-- 例: "[1] 🌄 image.png" -> { note_id = "1", name = "image.png", is_image = true }
-- 例: "[2] ノートタイトル" -> { note_id = "2", name = "ノートタイトル", 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

-- パース済みアイテム一覧を取得
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

-- nbノートのタイトルを取得する関数(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

-- ノート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

LazyVimではタブの表示に bufferline.nvim がデフォルトで使われているので、以下のように設定を拡張します。

~/.config/nvim/lua/plugins/bufferline.lua
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,
}

この設定により、nbのノートを開いたときに1行目のタイトルがバッファラインに表示されるようになります。

nbのタイトルをバッファラインに表示
bufferlineにnbのタイトルを表示

検索の設定

ノートのタイトルや、ノートの内容をgrep検索して開きたいときに便利なのがファジーファインダー系のプラグインです。
LazyVimでは snacks.nvim がデフォルトで使用されているので、これを活用します。

検索でもファイル名ではなく、メモのタイトルで検索できるようにしています。

snacks.nvimでnbのノートを検索する
snacks.nvimでnbのノートを検索する

以下のファイルを作成すると、snacks.nvimでnbのノートを検索できるようになります。

~/.config/nvim/lua/plugins/nb.lua
-- 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

-- snacks.nvimでノートの内容をgrep検索
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" },
  },
}

この設定で以下のキーマップが使えます:

  • <leader>np - ノートのタイトル一覧から検索して開く
  • <leader>ng - ノートの内容をgrep検索して開く

ノートタイトルで検索して開く
タイトル検索でノートを開く

grepしてノートを開く
grep検索でノートを開く

削除機能の追加

picker内でノートを削除できるようにします。
config/nb.lua に削除用の関数を追加します(return M の前に追加)。

~/.config/nvim/lua/config/nb.lua
+ -- ノートを削除
+ function M.delete_note(note_id)
+   local output = M.run_cmd("delete --force " .. note_id)
+   return output ~= nil
+ end

  return M

plugins/nb.luapick_notes にも削除アクションとキーバインドを追加します。

~/.config/nvim/lua/plugins/nb.lua
  Snacks.picker({
    title = "nb Notes",
    items = items,
    -- ... 省略 ...
    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" },
+       },
+     },
+   },
  })

これで picker 内で <C-d> を押すと、確認ダイアログが表示され、選択中のノートを削除できます。

ノートを追加する設定

Neovimからnbのノートを追加できるようにします。
config/nb.lua にノート追加用の関数を追加します(return M の前に追加)。

~/.config/nvim/lua/config/nb.lua
+ -- ノートを追加して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
+
+   -- 追加されたノートのIDを取得
+   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

plugins/nb.lua にもノートを追加する関数とキーマップを追加します。

~/.config/nvim/lua/plugins/nb.lua
+ -- ノートを追加して開く
+ 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 = {
+     { "<leader>na", add_note, desc = "nb add" },
      { "<leader>np", pick_notes, desc = "nb picker" },
      { "<leader>ng", grep_notes, desc = "nb grep" },
    },
  }

これで <leader>na でノートを追加して開けるようになります。

Neovimからnbのノートを追加する
NbのノートをNeovimから追加する

画像をインポートする設定

Neovimから画像をnbにインポートし、マークダウンリンクを挿入できるようにします。
config/nb.lua に画像インポート用の関数を追加します(return M の前に追加)。

~/.config/nvim/lua/config/nb.lua
+ -- 画像をnbにインポートする
+ function M.import_image(image_path, new_filename)
+   if not image_path or image_path == "" then
+     return nil, "No path provided"
+   end
+
+   -- 前後の空白とクォートを除去してパスを展開
+   local cleaned_path = image_path:gsub("^%s*['\"]?", ""):gsub("['\"]?%s*$", "")
+   local expanded_path = vim.fn.expand(cleaned_path)
+
+   -- ファイルが存在するか確認
+   if vim.fn.filereadable(expanded_path) == 0 then
+     return nil, "File not found: " .. expanded_path
+   end
+
+   -- 新しいファイル名が指定されていれば追加
+   local final_filename
+   if new_filename and new_filename ~= "" then
+     -- 拡張子がなければ元の拡張子を追加
+     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
+
+   -- コマンドを構築して実行
+   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
+
+   -- インポートされたファイルのIDを取得
+   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

plugins/nb.lua にも画像インポート関数とキーマップを追加します。

~/.config/nvim/lua/plugins/nb.lua
+ -- 画像をインポートしてマークダウンリンクを挿入
+ 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
+
+     -- 新しいファイル名を入力(空ならそのまま)
+     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("![%s](%s)", 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" },
    },
  }

これで <leader>ni で画像をインポートできるようになります。

  1. 画像パスを入力(ペースト)
  2. 新しいファイル名を入力(空Enterで元の名前を使用)

画像がnbにインポートされ、カーソル位置にマークダウンの画像リンクが挿入されます。

リンクを挿入する設定

snacks.nvimのpickerを使って、nbの画像やノートを選択してリンクを挿入できるようにします。

plugins/nb.lua にリンク挿入の関数とキーマップを追加します。

~/.config/nvim/lua/plugins/nb.lua
+ -- リンクを挿入
+ 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("![%s](%s)", 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" },
    },
  }

これで <leader>nl でプレビュー付きのpickerが表示されます。
画像(🌄マーク付き)を選択すると ![filename](filename) 形式、ノートを選択すると [[title]] 形式で挿入されます。

Claude Codeとの連携

nbのメモはローカルにマークダウンファイルとして保存されるため、Claude Codeとの相性も良いです。
ログ用のノートを作成し、スラッシュコマンドでセッションの内容を追記したり要約したりしています。

# ログ用ノートを作成
nb notebooks add log

合わせてHooksでフォーマットの設定を行うと、Claude Codeで編集した内容が自動で整形されるため便利です。
以下の例では、 rumdl を使用してマークダウンファイルをフォーマットしています。

{
  "hooks": {
    "PostToolUse": [{
      "matcher": "Write|Edit|MultiEdit",
      "hooks": [{
        "type": "command",
         "command": "jq -r '.tool_input.file_path | select(endswith(\".md\"))' | xargs -r rumdl fmt"
      }]
    }]
  }
}
rumdlの紹介

インストール

brew install rumdl

使い方

# 現在のディレクトリ内の Markdown ファイルを Lint する
rumdl check .

# ファイル形式をチェック(修正不可能な違反が残っていても成功時は終了コード 0 を返す)
rumdl fmt .

# 自動修正できない違反を報告する(違反が残っている場合は終了コード1を返す)
rumdl check --fix .

# デフォルトの設定ファイルを作成する
rumdl init

設定ファイルは次の場所に配置できます。

  • プロジェクトディレクトリまたは親ディレクトリ内に .rumdl.toml または rumdl.toml
  • .config/rumdl.toml (config-dir 規約に準拠)

既存の.markdownlint.json.markdownlint.yaml を使用することもできるため、rumdlを導入する際に既存の設定を活かすことも可能です。

~/.config/rumdl.toml
# rumdl 設定ファイル

# グローバル設定オプション
[global]
# 無効にするルールのリスト(必要に応じてコメント解除して修正)
# disable = ["MD013", "MD033"]

# 排他的に有効にするルールのリスト(指定した場合、これらのルールのみが実行されます)
# enable = ["MD001", "MD003", "MD004"]

# リント対象に含めるファイル/ディレクトリパターンのリスト(指定した場合、これらのみがリントされます)
# include = [
#    "docs/*.md",
#    "src/**/*.md",
#    "README.md"
# ]

# リント対象から除外するファイル/ディレクトリパターンのリスト
exclude = [
    # 除外する一般的なディレクトリ
    ".git",
    ".github",
    "node_modules",
    "vendor",
    "dist",
    "build",

    # 特定のファイルまたはパターン
    "CHANGELOG.md",
    "LICENSE.md",
]

# ディレクトリをスキャンする際に .gitignore ファイルを尊重する(デフォルト: true)
respect-gitignore = true

# Markdown フレーバー/方言(有効にするにはコメント解除)
# オプション: mkdocs, gfm, commonmark
# flavor = "mkdocs"

# ルール固有の設定(必要に応じてコメント解除して修正)

# [MD003]
# style = "atx"  # 見出しスタイル (atx, atx_closed, setext)

# [MD004]
# style = "asterisk"  # 順序なしリストスタイル (asterisk, plus, dash, consistent)

# [MD007]
# indent = 4  # 順序なしリストのインデント

# [MD013]
# line-length = 100  # 行の長さ
# code-blocks = false  # コードブロックを行の長さチェックから除外
# tables = false  # テーブルを行の長さチェックから除外
# headings = true  # 見出しを行の長さチェックに含める

# [MD044]
# names = ["rumdl", "Markdown", "GitHub"]  # 正しく大文字小文字を使用すべき固有名詞
# code-blocks = false  # コードブロックで固有名詞をチェックする(デフォルト: false、コードブロックをスキップ)

スラッシュコマンドは以下のものを使用しています。

Claude Codeがアクセスできるように /add-dir コマンドか、claude/setting.json でnbのノートディレクトリを追加しておきましょう。

~/.config/claude/settings.json
{
  "permissions": {
    "additionalDirectories": [
      "~/src/github.com/mozumasu/nb", # ここにnbのノートディレクトリを追加
      "~/src/github.com/mozumasu/zenn/articles"
    ]
  },
}

おわりに

この記事では、nb・Neovim・zeno.zshを組み合わせたターミナルでのメモ管理環境を紹介しました。

  • nb: CLIでメモの作成・検索・編集ができる
  • シェル関数: nbaでURL からタイトルを自動取得、nbqでfzfプレビュー付き検索
  • Neovim連携: snacks.nvimでメモの検索・追加をエディタ内で完結
  • zeno.zsh: fzf補完でノート選択を快適に

ターミナルでのメモ管理は、最初の設定に少し手間がかかりますが、一度構築してしまえば自分だけのワークフローを実現できます。

この記事が、みなさんのメモ環境構築の参考になれば幸いです。

GitHubで編集を提案

Discussion

EJEJ

On WSL2, this setup helps open images using msedge.exe instead of Linux browsers.

$ > cat ~/.local/bin/xdg-open
#!/bin/bash
file="$1"
if [[ ! -f "$file" ]]; then
  exit 1
fi
win_path="$(wslpath -w "$file" 2>/dev/null)" || {
  exit 1
}
cd /mnt/c        # change dir to avoid cmd path warning
cmd.exe /c "start msedge ${win_path}"
cd - > /dev/null # return to dir silently
exit 0