0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI時代のEmacs活用術: DenoteとOrg-modeで構築する、枯れない『個人知識データベース』

0
Last updated at Posted at 2026-05-02

はじめに

この記事は、文字情報のインプットとアウトプットを生活の糧とする「知的生産者」に向け、Emacsの設定およびその活用法を提示することを目的としています。
同時に、散逸しがちな自身の知見を体系化するための備忘録でもあります。

以前の記事ではEmacs全般の設定を俯瞰しましたが、本稿ではプレインな文字情報の蒐集・管理、そしてアウトプットへの昇華に焦点を絞ります。

その基軸となるのはOrg-modeであり、知識データベース構築の鍵となるリンクの構造化高度な検索機能です。

基本方針

EmacsとWebブラウザの機能分離

Lispマシンとしての側面を持つEmacsは、原理的にはあらゆるタスクをその内部で完結させるポテンシャルを有しています。
しかし、JavaScriptやTypeScriptを基盤とする現代的なWebサービスの動的な振る舞いへの対応には、一定の限界が存在します。

そこで、リッチなUI表示や音声・動画ストリーミングといった領域は、デスクトップブラウザ(Linux環境下でのbrave-origin-nightly使用を想定)に委ねるという設計思想を採用しています。
必要に応じてEmacsからこれらの外部サービスをシームレスに呼び出すことで、Emacs、ブラウザ、ターミナルの三位一体による完結したデスクトップ環境を目指します。

プレインテキスト環境の基盤としてのOrg-mode

AIエージェントの普及に伴い、計算機との対話の最小単位であるCUI(Command Line Interface)環境の価値が再評価されています。
そこでの標準言語はMarkdownですが、Emacsを知識環境の中核に据える場合、Markdownを凌駕するOrg-modeの諸機能(強力なハイパーリンク、アジェンダ管理、文芸的プログラミング等)が圧倒的な優位性を持ちます。

さらに、denoteパッケージを導入することで、Markdownのみならず画像や音声を含むあらゆるファイル形式をOrg-modeの管理体系下に統合し、一貫性のあるナレッジベースを構築することが可能になります。

AIとの連携

論文執筆や記事構成、翻訳といったフェーズにおいて、AIとの協調はもはや不可避です。
ただし、一次資料の厳密な引用や、言語学的・語源的な正確性が求められる場面では、伝統的な電子辞書や電子書籍(PDF/EPUB)との連携が依然として重要です。
本稿では、これら古典的なツールと、最新のAI駆動型環境を統合し、相乗効果を生み出すための設定について詳述します。

Googleエコシステムへの依拠

わたくしの日常的なワークフローは、Google WorkspaceをはじめとするGoogleのインフラに深く依存しています。
AI活用においても、特にコーディング以外の領域ではGeminiを多用しています(Google Searchとの強力な連携機能は、他のLLMにはない実用的な魅力です)。

したがって、以降の解説ではGoogleが提供する各種サービスのAPIを用いた設定が頻繁に登場することを、あらかじめご了承ください。

1. org-modeの設定

全般設定

org-mode本体に関連した設定です。
わたくしのemacs環境やキー設定の方針については以前の記事をご参照ください。

init.el
;;; === ORG-MODE ===
;; org-modeで行を折り返さない
(setq org-startup-truncated nil)

;; 階層に合わせて左にインデント
(setq org-startup-indented t)

;; 見出しをインデントした時にアスタリスクが減るのを防ぐ
(setq org-indent-mode-turns-on-hiding-stars nil)

;; インデントの幅を設定
(setq org-indent-indentation-per-level 2)

;; ファイルを開いたときの見出しの初期状態(#+STARTUP:で明示していない場合に適用される。見出しは折りたたみ、画像は表示) 
(setq org-startup-folded t)
(setq org-startup-with-inline-images t)

;; 見出しや本文の表示設定(フォントに関わるもの)
(add-hook 'org-mode-hook (lambda () (setq line-spacing 0.1)))

(with-eval-after-load 'org
  ;; 見出しの大きさと色(可変幅のフォントを使用)
  (set-face-attribute 'org-level-1 nil :inherit 'variable-pitch :height 1.1 :weight 'bold :foreground "#62b2ff")
  (set-face-attribute 'org-level-2 nil :inherit 'variable-pitch :height 1.1 :weight 'bold :foreground "#19e37d")
  (set-face-attribute 'org-level-3 nil :inherit 'variable-pitch :height 1.0 :weight 'bold :foreground "#f0ce43")
  (set-face-attribute 'org-level-4 nil :inherit 'variable-pitch :height 1.0 :weight 'bold :foreground "#ff79c6")
  (set-face-attribute 'org-level-5 nil :inherit 'variable-pitch :height 1.0 :weight 'bold :foreground "#bd93f9")
  (set-face-attribute 'org-level-6 nil :inherit 'variable-pitch :height 1.0 :weight 'bold :foreground "#46d9ff")

  ;; 固定幅のフォントが使われる構成要素を指定
  (set-face-attribute 'org-block nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-code nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-table nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-verbatim nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-formula nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-special-keyword nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-meta-line nil :inherit 'fixed-pitch)
  (set-face-attribute 'org-checkbox nil :inherit 'fixed-pitch)

  ;; 引用部分のスタイルの指定
  (set-face-attribute 'org-quote nil :inherit 'variable-pitch :slant 'italic))

;; org-modeで使われるファイルの親ディレクトリとデフォルトの親ノートを指定
(setq org-directory "~/Documents/MyOrg")
(setq org-default-notes-file "20250401T010000--mynotes__memo_private.org")

;; 表示される画像の幅の指定
(setq org-image-actual-width 300)

;; org-store-link(リンク取得)のキーシーケンス(F8も割り当てる)
(global-set-key (kbd "C-c l") 'org-store-link)
(global-set-key (kbd "<f8>") 'org-store-link)

;; org-insert-link(リンク挿入)のキーシーケンス(F9も割り当てる)
(global-set-key (kbd "C-c C-l") 'org-insert-link)
(global-set-key (kbd "<f9>") 'org-insert-link)

;; org-capture(新たな項目の取り込み)のキーシーケンス
(global-set-key (kbd "C-c c") 'org-capture)

;; org-captureのテンプレート(メニュー)の設定
;; 日々の情報蒐集において、重要な情報やメモ、リンク等は一旦これらの親ノートに記録
;; 親ノートのタイトルは必ずmyで始まるよう統一
;; org-modeで管理するファイルの命名法は、後述するdenoteでの記法で統一する
(setq org-capture-templates
      '(
        ("n" "Note" entry (file+headline "~/Documents/MyOrg/notes/20250401T010000--mynotes__memo_private.org" "Notes") "* %?\nEntered on %U\n %i\n %a")
        ("t" "Todo" entry (file+headline "~/Documents/MyOrg/notes/20250401T030000--mytask__private.org" "Capture") "** TODO %? \n   SCHEDULED: %^t \n")
        ("w" "Work" entry (file+headline "~/Documents/MyOrg/notes/20250401T040000--mywork__work.org" "Work") "* %?\nEntered on %U")
        ("v" "Voca" entry (file+headline "~/Documents/MyOrg/notes/20250325T000000--english-glossary__database.org" "2026 (voca)")"* %?")
        ("a" "AI" entry (file+headline "~/Documents/MyOrg/notes/20250401T050000--myai__ai_llm.org" "Q_board") "* %?\nEntered on %U\n %i\n %a")
        ("b" "Bulk" entry (file+headline "~/Documents/MyOrg/notes/20250825T184025--mybulk-news-link__actuality_portal.org" "misc") "* %?\nEntered on %U\n %i\n %a")
        ))

;; markdown変換のパッケージ呼び出し
(use-package ox-md
  :ensure nil
  :after org)

;; mynotes.org(親ノート)をC-c nですぐに開けるようにする
(defun show-org-buffer (file)
  "Show my org-file FILE on the current buffer."
  (interactive)
  (if (get-buffer file)
      (let ((buffer (get-buffer file)))
        (switch-to-buffer buffer)
        (message "%s" file))
    (find-file (concat "~/Documents/MyOrg/notes/" file))))

(global-set-key (kbd "C-c n") #'(lambda () (interactive)
                                  (show-org-buffer "20250401T010000--mynotes__memo_private.org")))

;; org-tempoを有効化(< に続けて1文字を入力することで構造テンプレートが挿入される)
(use-package org-tempo
  :ensure nil
  :after org)

;; Org-download(WEBページの画像を貼り付ける)
(use-package org-download
  :ensure t
  :demand t)
(setq-default org-download-image-dir "~/Documents/MyOrg/metadata")
(add-hook 'dired-mode-hook 'org-download-enable) ; Drag-and-drop to `dired`

(define-key org-mode-map (kbd "C-c L") 'org-download-yank)

;; Speed-commands(見出し行の先頭で1文字を入力することで、ツリーの展開や移動といった操作が可能)
(setq org-use-speed-commands t)

;; 画像表示の有無を切り替え)
(defun org-toggle-inline-images-in-subtree ()
  "Show images in the subtree only."
  (interactive)
  (save-excursion
    (save-restriction
      (org-narrow-to-subtree)
      (org-toggle-inline-images)
      (widen))))

(global-set-key (kbd "C-c v") 'org-toggle-inline-images-in-subtree)

;; org-sparse-treeを別バッファで表示(キーワード検索などで抽出した結果を新たなバッファで開く)
(defun org-sparse-tree-indirect-buffer (arg)
  (interactive "P")
  (let ((ibuf (switch-to-buffer (org-get-indirect-buffer))))
    (condition-case _
        (org-sparse-tree arg)
      (quit (kill-buffer ibuf)))))

(define-key org-mode-map (kbd "C-c /") 'org-sparse-tree-indirect-buffer)

;; 正規表現検索に一致した見出しのアウトラインを表示/隠すキー設定
(define-key org-mode-map (kbd "C-c s") 'outline-hide-by-heading-regexp)
(define-key org-mode-map (kbd "C-c S") 'outline-show-by-heading-regexp)

;; 選択したMarkdownの領域をpandoc経由でorg-modeに変換
(defun my-md-to-org-region (start end)
  "Convert region from markdown to org, replacing selection"
  (interactive "r")
  (shell-command-on-region start end "pandoc -f markdown -t org" t t))

(global-set-key (kbd "C-c m") 'my-md-to-org-region)

;; コードブロックの実行に必要なファイルをロード
(org-babel-do-load-languages
 'org-babel-load-languages
 '((C . t)
   (emacs-lisp . t)
   (python . t)
   (shell . t)))

;; リンク表示の切り替え
(global-set-key (kbd "C-c f") 'org-toggle-link-display)

;; 選択範囲をMarkdown形式に変換し、クリップボードに保存
(use-package org-to-markdown-and-kill
  :ensure nil
  :bind ("C-c p" . org-to-markdown-and-kill))

;; カーソル位置のリンクの内容をミニバッファに表示
(with-eval-after-load 'org
   (defun my-org-display-raw-link-at-point ()
    "Display the raw link when the cursor is on an Org mode link."
    (interactive)
    ;; org-agenda-mode で org-element-context を使用すると警告が出ることがあるため抑制
    (let ((element (let ((warning-minimum-level :error))
                     (org-element-context))))
      ;; カーソル位置の要素がリンクであるかを確認
      (if (and element (eq (car element) 'link))
          ;; リンクの場合、*Messages* バッファに記録せずにミニバッファ(echo area)に表示
          (let ((message-log-max nil))
            (message "%s" (propertize (org-element-property :raw-link element) 'face 'org-link)))
        ;; リンクでない場合、ユーザーに通知
        (message "No link at point"))))
  ;; org-mode で "C-z t" にコマンドを割り当て
  (define-key org-mode-map (kbd "C-t v") #'my-org-display-raw-link-at-point)
  ;; 元のコードと同様に org-agenda-mode でも利用可能にする
  (define-key org-agenda-mode-map (kbd "C-t v") #'my-org-display-raw-link-at-point))

;; 見出しのビュレットを8レベルまで指定
(use-package org-bullets
  :ensure t
  :config (setq org-bullets-bullet-list '("∑" "⨕" "∫" "Ω" "☿" "∅" "∢" "⚕"))
  :hook (org-mode . org-bullets-mode))

;; 次/前の見出しへ移動するキー割当
(with-eval-after-load 'org
  (define-key org-mode-map (kbd "M-n") #'org-next-visible-heading)
  (define-key org-mode-map (kbd "M-p") #'org-previous-visible-heading)
  ;; Alt+Shift+N/P も念のためバックアップとして設定
  (define-key org-mode-map (kbd "M-N") #'org-next-visible-heading)
  (define-key org-mode-map (kbd "M-P") #'org-previous-visible-heading))

;; カーソルが見出し行のアステリスクにあるとき、自動で日本語入力をオフ
(defun my-org-disable-ime-on-heading ()
  "When the cursor is on a heading's asterisk in Org mode, disable the input method."
  (when (and (eq major-mode 'org-mode)
             (bolp)
             (looking-at "\\*+"))
    (when current-input-method
      (deactivate-input-method))))
(add-hook 'post-command-hook #'my-org-disable-ime-on-heading)

;; 強調マーカー(*、/、=など)を非表示
(setq org-hide-emphasis-markers t)

;; pandocを使って他の文書形式に変換するパッケージの読み込み
(use-package ox-pandoc
  :ensure t
  :after org
  :config
  ;;  docx 出力時に特定のテンプレートを使う場合など
  ;; (setq org-pandoc-options-for-docx '((reference-doc . "~/.emacs.d/template.docx")))
  )

;; 選択範囲からverbatim (=text=) の装飾を取り除く
(defun my-remove-org-verbatim-in-region (start end)
  "Remove Org-mode verbatim markers (=text=) in the region and restore to plain text."
  (interactive "r")
  (save-excursion
    (save-restriction
      (narrow-to-region start end)
      (goto-char (point-min))
      (while (re-search-forward "=\\([^= \t\n][^=]*?[^= \t\n]\\|[^= \t\n]\\)=" nil t)
        (replace-match "\\1" t))))
  (message "Removed verbatim markers in region."))

(global-set-key (kbd "C-c V") 'my-remove-org-verbatim-in-region)

;; クリップボードの内容をMarkdownからorg-mode形式に変換して貼り付け
(defun my-paste-markdown-as-org ()
  "Convert Markdown text from the clipboard to Org-mode format and insert it."
  (interactive)
  (let ((md-text (current-kill 0)))
    (if (and md-text (stringp md-text))
        (let ((org-text (with-temp-buffer
                          (insert md-text)
                          (shell-command-on-region (point-min) (point-max) "pandoc -f markdown -t org" nil t)
                          (buffer-string))))
          (insert org-text)
          (message "Markdown converted to Org and pasted.")))))

(global-set-key (kbd "C-c M") 'my-paste-markdown-as-org)

;; リファイル(現エントリーや選択領域を別の場所に移動)対象ファイル
(setq org-rf-files (mapcar 'expand-file-name
                           '(
                             "~/Documents/MyOrg/notes/20250402T000000--rf-ai-and-philosoprf__ai_philosoprf.org"
                             "~/Documents/MyOrg/notes/20250402T000000--rf-linux-command__command_linux.org"
                             "~/Documents/MyOrg/notes/20250403T000000--rf-org-mode-tips__org_tutorial.org"
                             "~/Documents/MyOrg/notes/20250410T000000--rf-emacs-tips__cheat_emacs_tutorial.org"
                             "~/Documents/MyOrg/notes/20250419T185520--rf-denote__denote_tutorial.org"
                             "~/Documents/MyOrg/notes/20250503T112437--rf-playlist__movie_music_playlist.org"
                             "~/Documents/MyOrg/notes/20250503T123815--rf-linux-general-information__linux_tutorial.org"
                             "~/Documents/MyOrg/notes/20250503T125130--rf-lifehack__life_personal.org"
                             "~/Documents/MyOrg/notes/20250714T135605--rf-actuality-memo__actuality.org"
                             "~/Documents/MyOrg/notes/20250714T144010--rf-tech-news__technews.org"
                             "~/Documents/MyOrg/notes/20250803T120453--rf-miscellaneous-knowledge__km_life.org"
                             "~/Documents/MyOrg/notes/20251212T225642--rf-other-platform__android_macos_windows.org"
                             "~/Documents/MyOrg/notes/20260206T164215--rf-line-log-mirai__sns.org"
                             "~/Documents/MyOrg/notes/20260213T113959--rf-sociology__management_sociology.org"
                             "~/Documents/MyOrg/notes/20260213T121953--rf-mathematics__math.org"
                             "~/Documents/MyOrg/notes/20260303T094352--rf-knowledge-management__km.org"
                             )))

(setq org-refile-targets '((org-rf-files :maxlevel . 2))) ; 対象となる見出しの最大レベル
(setq org-refile-use-cache t) ; キャッシュを使用(リファイル時に上で指定した全ファイルが読み込まれる)
(setq org-outline-path-complete-in-steps t) ; 段階的に補完
(setq org-refile-use-outline-path 'file)  ; ファイル名を含めてパスを表示

denote

denoteは、哲学者であり著名なEmacsコントリビューターでもあるProtesilaos Stavrou(通称Prot)氏によって開発された、非常に論理的かつ「UNIX哲学」に忠実なメモ作成システムです。
日本国内での紹介事例はまだ少ないため、まずはその本質的な特徴から紐解いていきます。

denoteの論理的特徴

denoteが他のメモシステム(org-roam等)と一線を画すのは、その徹底したシンプルさと透明性にあります。

  • ファイル名が唯一の真実(Source of Truth):
    メタデータを外部データベース(SQLite等)やファイル内部の複雑なヘッダーに依存させず、ファイル名そのものに構造化して記述します。
    • 命名規則: IDENTIFIER--TITLE__KEYWORDS.EXTENSION
    • : 20240429T170000--epistemology-of-denote__philosophy_emacs.org
  • 技術的中立性(Agnosticism):
    ファイル名に情報が完結しているため、Emacs環境外であっても標準的なUNIXツール(find, grep, awk等)を用いた高度な検索・抽出が容易です。
  • 原子性(Atomicity):
    ひとつのファイルにはひとつの概念を割り当てる「Zettelkasten」的な運用を推奨しつつも、特定のメソッドを強要しない柔軟性を備えています。
主な情報源

最も信頼に値する詳細なリソースは、開発者本人による公式マニュアルです。

  • Denote Official Manual (Protesilaos Stavrou)
    • 特徴: 網羅的であり、設計思想から微細な設定までが緻密に記述されています。論理的な構成により、英語圏のドキュメントの中でも特筆すべき読みやすさを誇ります。
  • Taking Notes With the Emacs Denote Package (Lucid Manager)
    • 特徴: 実践的なチュートリアルです。org-modeとの相乗効果や、具体的なワークフローの構築方法が解説されています。

基本的な使い方とワークフロー

コマンド 説明
M-x denote 新規ノート作成。タイトルとキーワードを入力すると、規則に基づいたファイル名が動的に生成されます。
M-x denote-link 他のdenoteファイルへのリンクを挿入。ID(タイムスタンプ)ベースのため、ディレクトリ間のファイル移動にも耐えうる堅牢なリンクを保持します。
M-x denote-backlinks 現在のノートを参照している「バックリンク」を一覧表示し、知識のネットワークを可視化します。
M-x denote-rename-file 既存ファイルをdenoteの命名規則に沿って一括リネームします。PDFや画像資産の管理にも極めて有効です。

なぜdenoteなのか?

UNIX的な計算機環境に習熟した方であれば、「データはプレーンテキストに、意味は構造の中に」という原則がいかに強力であるかをご存知でしょう。
denoteは、複雑な抽象化レイヤー(データベース管理システム)を排し、ファイルシステムという計算機における最も堅牢な層に情報を定着させます。

これは、情報の長期的な可搬性と検索における「確実性」を最優先する、きわめてエピステモロジカル(認識論的)な選択であるといえます。

denoteパッケージの設定

まず、denoteの資産を格納するディレクトリを定義します。
ここでは、org-modeが管理するディレクトリ構造の内部に配置する(すなわち denoteorg-mode とする)構成を推奨します。

init.el
;;; === DENOTE ===
(use-package denote
  :ensure t
  :hook ((dired-mode . denote-dired-mode))
  :config
  ;; denoteが管理するファイルを置くディレクトリを指定
  (setq denote-directory "~/Documents/MyOrg/notes")
  (setq denote-file-type 'org)
  (setq denote-save-buffers nil)
  (setq denote-known-keywords '("coding" "work" "private"))
  (setq denote-infer-keywords t)
  (setq denote-sort-keywords t)
  (setq denote-prompts '(title keywords))
  (setq denote-excluded-directories-regexp nil)
  (setq denote-excluded-keywords-regexp nil)
  (setq denote-rename-confirmations '(rewrite-front-matter modify-file-name))
  (denote-rename-buffer-mode 1)
  :bind (("C-t n" . denote)
         ("C-t l" . denote-link)
         ("C-t L" . denote-add-links)
         ("C-t b" . denote-backlinks)
         ("C-t r" . denote-rename-file)
         ("C-t R" . denote-rename-file-using-front-matter)
         ("C-t d" . denote-dired)
         ))

;; context-menuをフック
(add-hook 'context-menu-functions #'denote-context-menu)


;; == consult-denote ==
;; consultを使った補完検索を行うためのパッケージ
;; ファイル検索にはfd、本文検索にはrgを使う
(use-package consult-denote
  :ensure t
  :bind
  (("C-t f" . consult-denote-find)
   ("C-t g" . consult-denote-grep))
  :config
  (setq consult-denote-find-command #'consult-fd)
  (setq consult-denote-grep-command #'consult-ripgrep)
  (consult-denote-mode 1))


;; == denote-org ==
(use-package denote-org
  :ensure t
  :after denote)


;; == denote-journal ==
;; denote形式で日記を作成するパッケージ(org-modeのカレンダーに反映される)
(use-package denote-journal
  :ensure t
  :after denote
  :config
  (setq denote-journal-title-format "Journal %Y-%m-%d")
  (setq denote-journal-keyword "journal"))

(global-set-key (kbd "C-t j") 'denote-journal-new-or-existing-entry)


;; 選択範囲を内容として新たなdenoteノートを作成(元ファイルへのリンクも自動的に挿入)
(defun my-denote-create-from-agent-log (start end)
  "選択範囲を内容として、新しいDenoteノートを作成し、ログへのリンクを付与する。"
  (interactive "r")
  (let ((content (buffer-substring-no-properties start end))
        (source-file (buffer-file-name)))
    (denote (read-string "Title: ") '("ailog" "fragment"))
    (insert content)
    (insert (format "\n\nSource: [[file:%s][Original Log]]" source-file))))

(global-set-key (kbd "C-t C-l") 'my-denote-create-from-agent-log)

TODOとスケジュールの管理

タスクおよびスケジュールの管理はOrg-modeが真価を発揮する領域ですが、モバイルデバイスとの親和性には依然として課題が残ります。
ここでは、デスクトップ(Emacs)とモバイル(Googleカレンダー)の長所を融合させるため、以下のワークフローを構築します:

  1. TODO管理:Org-modeをマスターとする
    1. ICSファイルを生成し、Googleカレンダーへエクスポート
  2. スケジュール管理:Googleカレンダーをマスターとする
    1. ical2org.awk を用いて、Org-modeへインポート

この双方向のデータ連携により、org-agenda (C-c a a) 上で、Googleカレンダーの予定とOrg-mode内のTODOを統合して俯瞰することが可能になります。

init.el
;; === TODO/AGENDA ===
;; TODO状態
(setq org-todo-keywords
      '((sequence "TODO(t)" "WAIT(w)" "|" "DONE(d)" "SOMEDAY(s)")))

;; DONEの時刻を記録
(setq org-log-done 'time)

;; org-agendaのキー設定
(global-set-key (kbd "C-c a") 'org-agenda)

;; org-agendaに含めるファイルを指定
(setq org-agenda-files (mapcar 'expand-file-name
                               '(
                                 "~/Documents/MyOrg/notes/google_calendar_readonly.org"
                                 "~/Documents/MyOrg/notes/20250401T030000--mytask__private.org"
                                 "~/Documents/MyOrg/notes/20250401T010000--mynotes__memo_private.org"
                                 "~/Documents/MyOrg/notes/20250401T040000--mywork__work.org"
                                 "~/Documents/MyOrg/notes/20250825T184025--mybulk-news-link__actuality_portal.org"
                                 "~/Documents/MyOrg/notes/20250401T050000--myai__ai_llm.org"
                                 )))

;; org-agendaをより見やすくするパッケージ
(use-package org-super-agenda
  :ensure t
  :after org-agenda
  :init
  (setq org-super-agenda-groups
        '(
          (:name "󰭦 Google Calendar"
                 :file-path "google_calendar_readonly"
                 :order 1)
          ;; 1. 最優先・期限切れ
          (:name "󰥔 Overdue & Urgent"
                 :deadline past
                 :priority "A"
                 :face (:foreground "#ff5f5f" :weight bold))
          ;; 2. 今日の予定(時間指定あり)
          (:name "󰭦 Today's Schedule"
                 :time-grid t
                 :date today)
          ;; 3. 仕事関連 (mywork__work.org)
          (:name "󰠚 Work Projects"
                 :file-path "mywork__work"
                 :order 1)
          ;; 4. AI・技術調査 (myai__ai_llm.org)
          (:name "󰚩 AI & Research"
                 :file-path "myai__ai"
                 :order 2)
          ;; 5. 個人・プライベート (mytask__private.org)
          (:name "󰈄 Private / Life"
                 :file-path "mytask__private"
                 :order 3)
          ;; 6. 読書・ニュース (mybulk-news-link)
          (:name "󰃭 Reading / Inbox"
                 :file-path "mybulk-news"
                 :order 10)
          ;; 7. 進行中のTODO
          (:name "󰄱 In Progress"
                 :todo ("WAIT" "TODO")
                 :order 5)
          ;; その他
          (:discard (:tag ("News" "BBS"))) ; 特定のタグを除外する場合
          ))
  :config
  ;; ヘッダーのセパレータ設定
  (setq org-super-agenda-header-separator " "
        org-super-agenda-header-prefix " ")

  ;; Agenda表示の微調整
  (setq org-agenda-prefix-format
        '((agenda . " %i %-12:c%?-12t% s")
          (todo . " %i %-12:c")
          (tags . " %i %-12:c")
          (search . " %i %-12:c")))

  (org-super-agenda-mode 1))

;; denoteの作者による推奨設定
(setq org-M-RET-may-split-line '((default . nil)))
(setq org-insert-heading-respect-content t)
(setq org-log-into-drawer t)



;; === EXPORT ===
;; 
(use-package ox-icalendar
  :ensure nil
  :config
  ;; エクスポート先のファイルを指定
  (setq org-icalendar-combined-agenda-file "~/Documents/DB/cloud_contents/my_ics/org-todo.ics")
  ;; 1. TODOキーワードを持つ項目をエクスポートに含める
  (setq org-icalendar-include-todo t)
  ;; 2. TODO状態のもの「だけ」をイベントとして扱う
  (setq org-icalendar-use-deadline '(event-if-todo))
  (setq org-icalendar-use-scheduled '(event-if-todo))
  ;; 3. タイムゾーンの設定
  (setq org-icalendar-timezone "Europe/Rome")
  )

;; エクスポートを実行する関数
(defun my-org-export-to-ics ()
  "Agendaファイルを1つのICSファイルにまとめて書き出す。"
  (interactive)
  (org-icalendar-combine-agenda-files)
  (message "ICSエクスポート完了: %s" org-icalendar-combined-agenda-file))

(global-set-key (kbd "C-t x") 'my-org-export-to-ics)



;; === IMPORT ===
;; ical2org.awkに手を加えたものを使用
(defun my-sync-google-calendar ()
  "Google Calendarの同期スクリプトを実行し、アジェンダを更新します。"
  (interactive)
  (message "Google Calendarを同期中...")
  (shell-command "/home/hoge_fuga/.local/bin/mygcalsync.sh")
  (org-agenda-redo-all)
  (message "Google Calendarの同期が完了しました。"))

(global-set-key (kbd "C-t s") 'my-sync-google-calendar)

Google Driveを経由したGoogleカレンダーへのエクスポート

ローカルで生成された org-todo.ics ファイルをGoogleカレンダーに読み込ませるには、一度Google Driveを経由させるのが効率的です。
ただし、Google Driveの通常の共有リンクでは、Googleカレンダーがファイルの内容(Rawデータ)を正しく解析できません。
そのため、以下の手順でURLを「直接ダウンロード用」に加工する必要があります。

  1. Google Driveでの共有設定
    org-schedule.ics を右クリック > 「共有」 > 一般的なアクセスを「リンクを知っている全員」に変更し、閲覧権限を付与します。
  2. ファイルIDの抽出
    共有URLから ファイルIDd//view の間に挟まれた文字列)をコピーします。
    • 例:https://drive.google.com/file/d/1A2B3C4D5E6F7G8H/view?usp=sharing
    • この場合、IDは 1A2B3C4D5E6F7G8H となります。
  3. URLの再構築
    以下のフォーマットの末尾に、先ほど抽出したファイルIDを結合します。
    • https://drive.google.com/uc?export=download&id=ここにファイルIDを貼る
  4. Googleカレンダーへの登録
    1. Googleカレンダーの左メニュー「他のカレンダー」横の「+」 > 「URL で追加」を選択。
    2. ステップ3で作成した「直接ダウンロード用URL」を貼り付けます。

反映の遅延について
Googleの公式ドキュメントには、「URLで追加したカレンダーの更新には最大24時間かかる場合がある」と明記されています。
即時反映が必要な場合は、設定の「インポート」から手動でファイルをアップロードすることを推奨します。

mygcalsync.shによるGoogleカレンダーのインポート

mygcalsync.sh は、Googleカレンダーの情報をOrg-modeへ取り込むためのシェルスクリプトです。

  1. 一方向のデータ連携
    本スクリプトは同期(Sync)ではなく、読み込み専用(Import)の処理を行います。そのため、スケジュール管理のマスター(権限)はGoogleカレンダー側に置く運用となります。
  2. 処理のフロー
    2-1. wget で取得したGoogleカレンダーのICSデータを一旦 /tmp/tempsync.ics にキャッシュ。
    2-2. ical2org.awk を介してOrg-mode形式へ変換。
    2-3. その後、指定のディレクトリへ保存。
  3. 事前準備:非公開URLの取得
    Emacs側からカレンダーにアクセスするため、専用の非公開URL(iCal形式)を取得する必要があります。
    1. ブラウザで Googleカレンダー を開く。
    2. 対象カレンダーの「︙(オーバーフローメニュー)」 > 「設定と共有」をクリック。
    3. 「カレンダーの統合」セクションへ移動し、「iCal 形式の秘密のアドレス」 という項目を探します。
    4. 表示されているURLをコピーし、スクリプトの設定変数に記述します。
mygcalsync.sh
#!/usr/bin/bash
export LC_ALL=C
# AWKスクリプトに渡すメタデータを環境変数で指定
export CALENDAR="google"
export TITLE="Google Calendar"
export AUTHOR="hoge_fuga" # YOUR NAME
export EMAIL="hoge_fuga@gmail.com" # YOUR EMAIL
export FILETAGS="gcal"

/usr/bin/wget -O /tmp/tempsync.ics https://calendar.google.com/... # 取得したical形式の秘密のアドレス
/home/hoge_fuga/.local/bin/ical2org.awk < /tmp/tempsync.ics > /home/hoge_fuga/Documents/MyOrg/notes/google_calendar_readonly.org

ical2org.awkファイルはここからダウンロードしてください。
そのままではインポートしたカレンダーがorg-agendaに反映されなかったので、次の2箇所を書き換えました。

ical2org.awk
# 160行目付近の変数の初期化に date1 date2 を追加
/^BEGIN:VEVENT/ {
   # ...既存の初期化...
   date1 = ""
   date2 = ""
   # ...

# 339行目付近から406行目付近の END:VEVENT セクションを次のように書き換え
/^END:VEVENT/ {
    is_duplicate = (id in UIDS)
    if (is_duplicate == 0 && (max_age < 0 || intfreq != "" || (lasttimestamp > 0 && systime() < lasttimestamp + max_age_seconds)))
{
        if (attending != attending_types["NOT_ATTENDING"]) {
            # 1. 見出しとタイムスタンプの出力
            if (condense) {
                print "* <" date "> " (gensub("^[ ]+", "", "", unescape(summary, 0)))
            } else {
                print "* " (gensub("^[ ]+", "", "g", unescape(summary, 0)))
                print "  <" date ">"  # 見出し直下にインデントして出力
            }

            # 2. プロパティドロワー
            print "  :PROPERTIES:"
            print "  :ID:        " id
            if (length(location)) print "  :LOCATION:  " location
            if (length(status))   print "  :STATUS:    " status

            attending_string = attending_types[attending]
            if (attending_string == "UNSET") attending_string = "ATTENDING"

            print "  :ATTENDING: " attending_string
            print "  :ATTENDEES: " join_keys(people_attending)
            print "  :END:"

            # 3. ログブック
            if (date2 != "") {
                print "  :LOGBOOK:"
                print "  CLOCK: [" date1 "]--[" date2 "] =>  " "0:00"
                print "  :END:"
            }

            print ""
            # 4. 説明文
            if (length(entry) > 1) {
                print gensub("^[ ]+", "", "g", unescape(entry, 1))
            }

            # 5. オリジナルのコメント
            if (original) {
                print "** COMMENT original iCal entry\n", gensub("\r", "", "g", icalentry)
            }
        }
        UIDS[id] = 1
    }
}

2. テキストの読み書きのための環境構築

本稿で詳述する各設定では、操作の迅速化と機能の系統立てを行うため、独自のプレフィックスキー(Prefix Key)を定義しています。

基本方針として、C-t (Ctrl-t) を denote 関連の関数群に、C-z (Ctrl-z) をそれ以外の独自拡張関数や外部パッケージのプレフィックスとして割り当てています。

init.el
;; == Ctrl-z prefix keybindings ==
(defvar my-keys-prefix-map (make-sparse-keymap)
  "My personal keybindings prefix map.")

(define-key global-map (kbd "C-z") my-keys-prefix-map)

;; == Ctrl-t prefix keybindings ==
(defvar my-keys-prefix-map-bis (make-sparse-keymap)
  "My personal keybindings prefix map bis.")

(define-key global-map (kbd "C-t") my-keys-prefix-map-bis)

電子辞書

コマンドライン・ベースの辞書検索ツールである sdcv (StarDict Console Version) は、オーソドックスながらも現代の知的生産において極めて有効なツールです。
電子書籍やPDFを閲覧している際、未知の語彙に遭遇しても、思考を中断することなく即座に検索を実行し、独立したバッファに展開された結果をノートへとシームレスに引用・貼付することが可能です。

設定にあたっては、あらかじめターミナル上で sdcv が正常に動作し、必要な辞書データが正しくロードされていることを確認してください。

init.el
;; == quick sdcv ==
(use-package quick-sdcv
  :ensure t
  :custom
  (quick-sdcv-dictionary-prefix-symbol "►")
  (quick-sdcv-ellipsis " ▼ ")
  (sdcv-word-pronounce nil)
  :config
  ;; 検索履歴の保存数
  (setq quick-sdcv-hist-size 10000)
  ;; q で検索結果が表示されたバッファを閉じる
  (define-key quick-sdcv-mode-map (kbd "q") 'quit-window))

;; 検索する辞書のリスト
(setq quick-sdcv-dictionary-complete-list
      '("EIJIRO"
        "新明解国語辞典"
        "広辞苑 第六版"
        "American Heritage Dictionary 4th Ed. (En-En)"
        "Oxford English Dictionary 2nd Ed. P1"
        "Oxford English Dictionary 2nd Ed. P2"
        ))

;; Ctrl-z s でカーソル位置あるいは選択領域の単語を検索
(define-key my-keys-prefix-map (kbd "s") 'quick-sdcv-search-at-point)
;; Ctrl-z S でキーボードから入力した単語を検索
(define-key my-keys-prefix-map (kbd "S") 'quick-sdcv-search-input)

電子書籍

本環境では、Emacs内部で完結する nov-mode と、外部アプリケーションである Calibre の電子書籍リーダーを、ドキュメントの性質や用途に応じて使い分けています。

org-mode へのリンク埋め込みや本文の引用といった「情報の再構成」に関しては、Emacsと密に統合された nov-mode が圧倒的に簡便です。一方で、図版を多用する文献や、日本語特有の「縦書き」ドキュメントの可読性といった、純粋な「読書体験」の質においては、Calibre に一日の長があります。

そこで、Calibre のブックリーダー側からも本文へのディープリンクを取得可能にし、org-mode 上のリンクから該当箇所へ直接ジャンプできる連携設定を導入しました。これにより、外部ビューアの優れたUIと、Emacsによる構造的な知識管理をシームレスに統合しています。

nov-mode

まずはnov-mode本体の設定を行います。

init.el
(use-package nov
  :ensure t
  :mode ("\\.epub\\'" . nov-mode)
  :bind (:map nov-mode-map
              ("t" . nov-goto-toc)
              ("n" . nov-next-document)
              ("p" . nov-previous-document)
              ("SPC" . nov-scroll-up)
              ("S-SPC" . nov-scroll-down))
  :hook ((nov-mode . (lambda ()
                       (display-line-numbers-mode -1)
                       (visual-line-mode 1)
                       (hl-line-mode -1)
                       )))
  :config
  (setq nov-text-width 96)
  (setq nov-header-line-format "%t: %c")
  (defun my-nov-font-maru ()
    "ゴシック体フォントの指定"
    (interactive)
    (when (display-graphic-p)
      (face-remap-add-relative 'variable-pitch
                               :family "Noto Sans CJK JP Medium"
                               :height 1.0)))
  (defun my-nov-font-serif ()
    "明朝体フォントの指定"
    (interactive)
    (when (display-graphic-p)
      (face-remap-add-relative 'variable-pitch
                               :family "Noto Serif CJK JP Medium"
                               :height 1.0)))
  (setq nov-variable-pitch-use-github-style t))

次に、nov-modeorg-mode のシームレスな相互運用を実現するための設定を記述します。

これによって、Org-modeにおける標準的なリンク開操作(C-c C-oorg-open-at-point)を通じて、対象となるEPUBファイルが適切なコンテキストで nov-mode によって自動的に展開されるようになります。

init.el
(with-eval-after-load 'org
  ;; nov: リンクタイプの定義
  (org-link-set-parameters "nov"
                           :follow #'my-nov-follow
                           :store  #'my-nov-store))

(defun my-nov-follow (path)
  "nov:リンクをクリックした時に、EPUBを開き指定されたインデックス(章)へ移動する"
  ;; 1. セパレータを # または :: の両方に対応させる
  (let* ((parts (split-string path "\\(?:#\\|::\\)"))
         (raw-path (if (fboundp 'org-link-unescape)
                       (org-link-unescape (nth 0 parts))
                     (nth 0 parts)))
         (file (expand-file-name raw-path))
         ;; 2. インデックスを取得(::25:34409 のような形式から '25' を取り出す)
         (index (if (nth 1 parts) (string-to-number (nth 1 parts)) 0)))

    (if (not (file-exists-p file))
        (message "Error: EPUB file not found: %s" file)

      (find-file file)

      (unless (derived-mode-p 'nov-mode)
        (nov-mode))

      ;; 3. nov-modeの初期化が完了してからジャンプする
      (if (and (derived-mode-p 'nov-mode) (bound-and-true-p nov-documents))
          (nov-goto-part index)
        (run-with-timer 0.2 nil (lambda (idx)
                                  (with-current-buffer (get-file-buffer file)
                                    (when (derived-mode-p 'nov-mode)
                                      (nov-goto-part idx))))
                        index)))))

(defun my-nov-store ()
  "nov-mode で C-c l (org-store-link) を押した際にリンクを生成"
  (when (derived-mode-p 'nov-mode)
    (let* ((file (buffer-file-name))
           (index nov-documents-index) ;; 現在表示中のドキュメント番号
           (title (nov-content-filter (format "%s" (plist-get nov-metadata :title))))
           (link (format "nov:%s#%d" file index))
           (desc (format "EPUB: %s (Part %d)" title index)))
      (org-link-store-props :type "nov" :link link :description desc)
      t)))

Calibre

まず、デスクトップ環境(ホストOS)上で Calibre が正常に稼働していることを前提条件とします。
ここでは、Emacs インターフェースを通じて Calibre ライブラリを統合管理するためのパッケージ calibredbに関連した設定を記述します。

init.el
(use-package calibredb
  :ensure t
  :commands calibredb
  :config
  (setq calibredb-root-dir "~/Commune/Calibre_2")
  (setq calibredb-db-dir (expand-file-name "metadata.db" calibredb-root-dir))
  (setq calibredb-library-alist '(("~/Commune/Calibre_2" (name . "Calibre"))))
  (setq calibredb-id-width 6)
  (setq calibredb-format-width 4)
  (setq calibredb-title-width 80)
  (setq calibredb-comment-width 0)
  (setq calibredb-date-width 0)
  (setq calibredb-format-nerd-icons t))

次に、Calibreの ebook-viewer から特定の箇所へのディープリンク(URI)を取得し、Org-modeへ統合するための設定を行います。

リンク取得および貼り付けの具体的なワークフローは以下の通りです:

  1. 対象箇所の表示: ebook-viewer で引用または参照したい箇所を表示します。
  2. 位置情報の取得: 右クリックメニュー、または上部ツールバーから「場所をコピー (Copy location)」を選択します。
  3. URI形式の確認: クリップボードには calibre://view-book/... という形式のURIが格納されます。
  4. Org-modeへの統合: これをOrg-mode上で [[calibre://view-book/...][タイトル]] の形式に整形して貼り付けます。

URLサニタイズに関する注意点
ebook-viewer が生成するURLには、しばしば [] といった文字が含まれます。
これらはOrg-modeのリンク構文と衝突し、リンクが正しく機能しない原因となります。
そのため、Emacs側でこれらの文字をエスケープ、あるいは置換して適切に処理する関数が必要となります。

init.el
(org-link-set-parameters "calibre"
                         :follow 'my-calibre-follow)

(defun my-calibre-follow (path)
  (call-process "xdg-open" nil 0 nil (concat "calibre:" path)))

(defun my-paste-calibre-link-as-org ()
  "クリップボードのCalibreリンクから [ ] をエスケープし、Orgリンク形式で挿入"
  (interactive)
  (let ((raw-link (current-kill 0)))
    ;; クリップボードの中身が calibre:// で始まっているか確認
    (if (and (stringp raw-link) (string-match-p "^calibre://" raw-link))
        (let* ((link (replace-regexp-in-string "\\[" "%5B" raw-link))
               (link (replace-regexp-in-string "\\]" "%5D" link))
               ;; リンクの説明文を入力(空欄の場合は "Calibre Link" にする)
               (desc (read-string "Link description: ")))
          (insert (format "[[%s][%s]]"
                          link
                          (if (string-empty-p desc) "Calibre Link" desc)))
          (message "Calibre link pasted as Org format."))
      (message "Clipboard does not contain a valid Calibre link."))))

(global-set-key (kbd "C-t c") 'my-paste-calibre-link-as-org)

これら一連の設定により、Org-modeのノート上で C-t c を実行するだけで、クリップボード内の位置情報を元にリンクが自動生成されます。

一度リンクが作成されれば、他の標準的なリンクと同様に C-c C-o (org-open-at-point) を実行するだけで ebook-viewer が自動起動し、指定されたページや章へ即座に復帰(ジャンプ)することが可能になります。

PDFファイル

最初にpdf-toolsパッケージの設定を記述します。

init.el
;; == pdf-tools ==
(use-package pdf-tools
  :ensure t
  :mode ("\\.pdf\\'" . pdf-view-mode)
  :config
  (pdf-tools-install :no-query)
  (add-hook 'pdf-view-mode-hook (lambda() (display-line-numbers-mode -1)))
  (setq pdf-annot-activate-created-annotations t)
  (define-key pdf-view-mode-map (kbd "C-s") 'isearch-forward)
  (define-key pdf-view-mode-map (kbd "C-c C-a C-a") 'pdf-annot-minor-mode)
  (define-key pdf-view-mode-map (kbd "C-c C-a C-o") 'pdf-outline)
  (setq pdf-view-resize-factor 1.1)
  (setq pdf-cache-size 500))

次に、リンクに関する設定を行います。

init.el
(with-eval-after-load 'org
  (org-link-set-parameters "pdf"
                           :follow #'my-pdf-follow
                           :store  #'my-pdf-store))

(defun my-pdf-follow (path)
  "pdf:リンクをクリックした時に、指定されたページでPDFを開く"
  (let* ((parts (split-string path "::"))
         (file (nth 0 parts))
         (page (if (nth 1 parts) (string-to-number (nth 1 parts)) 1)))
    (find-file file)
    (pdf-view-goto-page page)))

(defun my-pdf-store ()
  "pdf-view-mode で C-c l (org-store-link) を押した際にリンクを生成"
  (when (derived-mode-p 'pdf-view-mode)
    (let* ((file (buffer-file-name))
           (page (pdf-view-current-page))
           (title (file-name-nondirectory file))
           (link (format "pdf:%s::%d" file page))
           (desc (format "%s (p.%d)" title page)))
      (org-link-store-props :type "pdf" :link link :description desc))))

LaTex編集環境

Emacsには数多のLaTeX編集環境が存在しますが、本環境ではYaTeX(野鳥)を選定しています。

詳細な環境構築の手順や、各フォーマットへのエクスポート手法については、TeXWikiを参照されることをお勧めします。

init.el
;; == YaTeX ==
(use-package yatex
  :ensure nil
  :commands yatex-mode
  :mode (("\\.tex$" . yatex-mode)
         ("\\.cls$" . yatex-mode)
         ("\\.sty$" . yatex-mode)
         ("\\.clo$" . yatex-mode)
         ("\\.bbl$" . yatex-mode))
  :init
  ;; 基本的なコマンド設定
  (setq tex-command "lualatex")
  (setq dvi2-command "zathura")
  (setq YaTeX-pdf-view-command "zathura")

  ;; 日本語・エンコーディング設定
  (setq YaTeX-kanji-code 4)       ; UTF-8を強制
  (setq YaTeX-use-LaTeX2e t)      ; LaTeX2eを使用
  (setq YaTeX-use-AMS-LaTeX t)    ; 数式環境の強化

  :config
  ;; RefTeXとの連携
  (with-eval-after-load 'reftex
    (setq reftex-plug-into-AUCTeX nil) ; YaTeXで使う場合はnil
    (add-hook 'yatex-mode-hook '((lambda () (reftex-mode 1)))))

  ;; その他使い勝手の向上
  (setq YaTeX-fill-column 0)      ; 自動改行を無効化
  (setq YaTeX-help-file (expand-file-name "help/YATEXHLP.jp" user-emacs-directory))
  )

3. Webコンテンツとのリンク

Webページのリンク取得

WebコンテンツをOrg-modeへリンクするためのワークフローは以下の通りです。

ブラウザ拡張機能(Copycat)の活用

Copycatをブラウザにインストールして利用します。

  1. キャプチャ: 閲覧中のページで右クリックメニュー(または設定したショートカット)を開き、Tab LinkOrg-Mode を選択します。
  2. ペースト: Emacsのバッファ上で C-y (yank) を実行し、リンクを貼り付けます。
  3. 自動構成: リンクの記述部(Description)には、閲覧ページのタイトルが自動的に挿入されます。

コンテンツのローカル・アーカイブ(永続化)

Web上の情報は消失や改変のリスクが常にあるため、参照頻度の高い重要なコンテンツはローカルディレクトリに保存して「永続化」を図ります。

ここでは monolith を使用し、CSSや画像を含むページ全体を単一のHTMLファイルとして「魚拓(スナップショット)」として保存します。

  • インストール: Rust環境を構築後、以下のコマンドでインストール可能です。
    cargo install monolith
  • 運用: 自作のシェルスクリプト gyotaku.sh を介して実行します。monolith で取得したデータを denote の命名規則に準拠したファイル名で保存することで、他のノート資産と一貫性のある管理を可能にしています。
gyotaku.sh
#!/bin/bash

# 入力したURLを変数に代入
read -p "URL to save: " target_url

# 入力が空でないか確認
if [ -z "$target_url" ]; then
  echo "エラー: URLが入力されていません。"
  exit 1
fi
# タイトルを変数に代入
read -p "Title to save: " target_title

title_no_space=${target_title// /-}

# 保存先ディレクトリを作成(存在しない場合)
# ~ではなく $HOME を使う方が確実
output_dir="$HOME/Documents/MyOrg/gyotaku"
mkdir -p "$output_dir"

# 現在の日時を取得(denoteファイル形式)
current_datetime=$(date '+%Y%m%dT%H%M%S')

# 出力ファイルパスを構築(denoteファイル形式)
output_file="${output_dir}/${current_datetime}--${title_no_space}__gyotaku.html"

# ターゲットを保存
echo "URLを保存中: $target_url"
echo "保存先: $output_file"

# monolith コマンドを実行
# 変数をダブルクォートで囲み、スペース等を含むURLに対応
monolith "$target_url" -o "$output_file"

# 実行結果を確認
if [ $? -eq 0 ]; then
  echo "保存が完了しました。"
else
  echo "エラー: URLの保存に失敗しました。"
  exit 1
fi

exit 0

保存場所に関する設計上の注意点
monolith は、画像やCSSなどのメディアファイルを含め、ターゲットページを単一の HTML ファイルとしてカプセル化(パッキング)して保存します。その特性上、Org-mode が得意とする「プレインテキストベースでの透過的な参照や全文検索」には適していません。

したがって、Org-mode や denote がナレッジベースとして直接スキャン・管理するディレクトリへの混入は避け、外部のアーカイブ用ディレクトリ等に隔離して保存した上で、通常のファイルリンク(file: リンク)を介して参照する運用を推奨します。

Emailのリンク

Emacsにおける電子メール環境の構築・維持は、現代においては極めて高コストかつストレスフルな作業と言わざるを得ません。

Gnus、Mu4e、Notmuch、Wanderlustといった優れたクライアントは存在するものの、近年の複雑な認証プロトコル(OAuth2等)への対応や、マルチメディアを多用するHTMLメールのレンダリング、外部ツールとの連携の煩雑さを考慮すると、あえてEmacs内部に閉じた環境を構築するメリットは、保守コストに対して見合わないと感じています(わたくしの環境でもGnusによる送受信設定は行っていますが、実際に起動することは稀です)。

そのため、電子メールについては「ブラウザ(Gmail)の利便性」と「Org-modeによる構造化管理」を分離し、「実利優先」の方針で運用しています。

  1. 情報の集約(Hub化)
    プライベート・ビジネスを問わず、すべてのメールをメインのGmailアドレスに集約し、強力なフィルタリングとタグ(ラベル)によって一元管理します。
  2. Webコンテキストの取得
    Org-modeから参照したいメールについては、ブラウザで当該メールを開き、前述の拡張機能「Copycat」を用いてURIリンクを生成・ペーストします。
  3. アーカイブの永続化とリンク
    証跡としての保存や精緻な引用が必要なメールについては、.eml 形式でローカルディレクトリに保存し、Org-modeから直接リンクを張ります。
    • 保存された .eml ファイルへのリンク上で C-c C-o (org-open-at-point) を実行すると、ブラウザ(または適切なビューア)で即座に内容が表示されるよう環境を定義します。

以下に、.eml ファイルへのリンクを適切に処理するための設定を記述します。

init.el
;; eml形式のファイルを閲覧・リンク保存するための設定
(add-to-list 'auto-mode-alist '("\\.eml\\'" . message-mode))

(with-eval-after-load 'org
  ;; リンクタイプの定義(保存関数 :store もここで指定)
  (org-link-set-parameters "eml"
                           :follow #'my-eml-follow
                           :store  #'my-eml-store)

  ;; brave-origin-nightlyでファイルを開くための関数(ブラウザのファイル名は各自の環境にあわせる)
  (defun my-eml-follow (path)
    (let ((full-path (expand-file-name path)))
      (if (file-exists-p full-path)
          (start-process "brave-eml" nil "brave-origin-nightly" full-path)
        (user-error "File not found: %s" full-path))))

  ;; .emlファイル用のリンク保存関数
  (defun my-eml-store ()
    (let ((file (buffer-file-name)))
      (when (and file (string-match-p "\\.eml\\'" file))
        (let* ((title (file-name-nondirectory file))
               (link (concat "eml:" file))
               (desc (format "Email: %s" title)))
          (org-link-store-props :type "eml" :link link :description desc)
          t))))

;; 既存のfile:リンク用設定
(setq org-file-apps
      (append '(("\\.eml\\'" . "brave-origin-nihtly %s"))
              org-file-apps))

RSSリーダーとブラウザの動的切り替え

情報の定期購読(RSS)には elfeedを採用しています。
一次情報の迅速なスクリーニングを優先するため、elfeed 内でリンクを開く際の標準ブラウザには、Emacs 内蔵のテキストベース・ブラウザである eww を割り当てています。

また、org-mode 上の Web リンクを処理する際、情報の性質に応じて「eww(内部ブラウザ)」と「デスクトップブラウザ(外部ブラウザ)」をシームレスに選択できる設定を追加しました。

これにより、以下のようなコンテキストに応じた使い分けが可能になります:

  • eww を選択する場合: 純粋なテキスト情報の抽出や、ノートへのクイックな引用が主目的である場合(JS等のレンダリングを排した高速なブラウジング)。
  • デスクトップブラウザを選択する場合: 動画・音声コンテンツの再生、複雑なレイアウトの確認、あるいは Web サービスとの動的なインタラクションが必要な場合。

以下に、この「ハイブリッドなブラウジング環境」を実現するための設定を記述します。

elfeedの設定

init.el
;; == elfeed ==
;; 購読するサイトのリスト
(setq elfeed-feeds
      '(("https://www.nhk.or.jp/rss/news/cat0.xml" ja news)
        ("https://gigazine.net/index.php?/news/rss_2.0/" ja tech)
        ("https://www.ansa.it/lombardia/notizie/lombardia_rss.xml" it news)
        ("https://www.france24.com/fr/rss" fr news)
        ("https://www.repubblica.it/rss/homepage/rss2.0.xml?ref=RHFT" it news)
        ("https://www.nytimes.com/services/xml/rss/nyt/HomePage.xml" en news)
        ("https://feeds.feedburner.com/d0od" en tech)
        ("https://feeds.feedburner.com/Techcrunch" en tech)
        ("https://feeds.feedburner.com/ItsFoss" en tech)
        ("https://www.wired.com/feed/rss" en tech)
        ))
;; 1週間以内の未読記事を初期表示
(setq-default elfeed-search-filter "@1-week-ago +unread ")
(global-set-key (kbd "C-z w") 'elfeed)
;; defaut in-line browser
(setq browse-url-browser-function 'browse-url-default-browser)

;; messageの文字色の変更
(with-eval-after-load 'message
  (set-face-attribute 'message-header-subject nil
                      :foreground "white"
                      :weight 'bold
                      :inherit nil))

(add-hook 'modus-themes-after-load-theme-hook
          (lambda ()
            ;; 選択範囲(region)をより目立たせる
            (set-face-attribute 'region nil :background "#324a6f" :foreground "white")
            (when (facep 'message-header-subject)
              (set-face-attribute 'message-header-subject nil :foreground "white" :weight 'bold))))

ewwの設定

init.el
;; == eww browser ==
(setq eww-search-prefix "https://duckduckgo.com/?q=%s")
(setq url-user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:126.0) Gecko/20100101 Firefox/126.0")
(setq shr-width 80)
(setq eww-image-max-width 600)

;; GoogleおよびYoutube関連のリンクはデスクトップブラウザで開き、それ以外はブラウザを選択できるようにする
(setq browse-url-handlers
      '(("https?://\\([^/.]+\\.\\)?google\\." . browse-url-default-browser)
        ("https?://\\([^/.]+\\.\\)?youtube\\." . browse-url-default-browser)
        ("." . my-browse-url-interactive)))

;; URLを開くブラウザをインタラクティブに選択
(defun my-browse-url-interactive (url &rest _args)
  "ブラウザを選択(elfeedでは自動的にewwを使用)"
  (if (derived-mode-p 'elfeed-search-mode 'elfeed-show-mode)
      ;; Elfeedバッファにいる場合は直接ewwで開く
      (eww-browse-url url)
    ;; それ以外のバッファ(org-modeなど)では1文字入力を求める
    (let ((choice (read-char-choice (format "Open with: (e)ww or (d)efault browser? ") '(?e ?d))))
      (if (eq choice ?e)
          (eww-browse-url url)
        (browse-url-default-browser url)))))

4. AIとの連携

基本方針で述べた通り、本節ではGeminiとEmacsの連携メカニズムについて詳述します。
現在、わたくしは以下の4つのアプローチを、利用頻度の高い順に使い分けています:

  1. tmux上のチャットモード: gemini-cli による対話型インターフェース
  2. Emacsプロンプトモード: コンテキストに応じたGeminiのオンデマンド呼び出し
  3. agent-shell経由: エージェント機能を用いたタスク実行
  4. Emacs vterm上: エミュレータ内での gemini-cli 運用

これらすべてのチャット出力(アウトプット)を同一のディレクトリに集約・永続化することで、後述する consult-ripgrep を用いた「知識アーカイブ」としての横断的検索が可能になります。

また、Emacsバッファ上で推敲中の外国語文書に対し、Geminiを用いた高度なスペルチェックや文体校正も実施しています。

tmux上での gemini-cli 活用

ターミナル環境でAIエージェントを運用する際のデファクトスタンダードと言える構成ですが、その安定性は群を抜いています。
詳細な設定は本稿の範疇を超えますが、tpm(Tmux Package Manager)を介して tmux-logging を導入し、チャットの全プロセスを自動保存しています。

gemini-cli の回答を直接Org-modeのノートへ取り込む際は、CLI側で /copy コマンドを実行した後、Emacs側で以下のカスタム関数を呼び出し、Org-mode形式に変換して貼り付けます。

init.el
  ;; == Gemini CLI to Org ==
(defun my-paste-gemini-chat-as-org ()
  "Get Gemini chat log from clipboard (Markdown), convert it to Org format, and insert."
  (interactive)
  (let* ((md-text (current-kill 0))
         (pandoc-command "pandoc")
         (org-text (shell-command-to-string
                    (format "echo %s | %s -f markdown -t org"
                            (shell-quote-argument md-text)
                            pandoc-command))))
    (with-temp-buffer
      (insert org-text)
      (goto-char (point-min))
      (while (re-search-forward "^#\\+BEGIN_QUOTE\n\\(.*?\\)\n#\\+END_QUOTE" nil t)
        (replace-match "* \\1" t t))

      (goto-char (point-min))
      (while (re-search-forward "^----*$" nil t)
        (replace-match "" t t))

      (goto-char (point-min))
      (while (re-search-forward "\n\n\n+" nil t)
        (replace-match "\n\n" t t))
      (kill-ring-save (point-min) (point-max)))
    (yank)
    (message "Gemini chat log converted to Org format and pasted.")))

(global-set-key (kbd "C-z C-y") 'my-paste-gemini-chat-as-org)

プロンプトモードでの Gemini 呼び出し

バッファ内の選択領域を直接 gemini-cli へ送出するモードです。実行後、即座に生成される新規バッファで回答を確認できます。
得られた回答は kill-ring-save (M-w) でコピーし、前述の my-paste-markdown-as-org (C-c M) を介して、構造を維持したままノートへ統合します。

init.el
;; == Gemini CLI ==
(defun my-gemini-cli-chat-region (start end)
  "リージョンをGeminiに質問し、結果をMarkdown形式で表示する"
  (interactive "r")
  (let* ((question (buffer-substring-no-properties start end))
         (command (concat "gemini -p " (shell-quote-argument question)))
         (output-buffer-name "*Gemini Chat Output*")
         (buf (get-buffer-create output-buffer-name)))
    (message "Geminiに問い合わせ中...")

    (with-current-buffer buf
      (setq buffer-read-only nil)
      (erase-buffer)
      ;; Markdownモードを適用
      (when (fboundp 'markdown-mode)
        (markdown-mode))
      ;; ANSIカラーを有効にする設定
      (setq-local ansi-color-for-comint-mode t))

    (let ((proc (start-process-shell-command "gemini-process" buf command)))
      (set-process-sentinel proc
                            (lambda (p e)
                              (when (string= e "finished\n")
                                (message "Geminiからの回答が完了しました。")
                                ))))
    (display-buffer buf)))

(global-set-key (kbd "C-z g") 'my-gemini-cli-chat-region)

agent-shell の利用

AIエージェントを介したコーディング支援には有用ですが、論文構成や翻訳、記事執筆といった「言語的精緻さ」が求められる用途には、現状では最適化の余地があると感じています。
具体的には、レスポンスの遅延や、ログの増大に伴うEmacs全体のパフォーマンス低下が顕著になる傾向があるため、利用は限定的です。
なお、agent-shell終了時には、生成されたバッファの内容を自動的にログファイルへと記録するフックを記述しています。

init.el
;; == agent-shell ==
(use-package agent-shell
  :ensure t
  :vc (:url "https://github.com/google/gemini-cli"
            :lisp-dir "emacs/agent-shell")
  :bind (("C-z G" . agent-shell-google-start-gemini))
  :config
  (setq agent-shell-transcript-file-path-function
        (lambda ()
          (let* ((log-dir "~/Documents/MyAgentLogs/")
                 (filename (format-time-string "%Y%m%dT%H%M%S--agent-log__llm.md"))
                 (filepath (expand-file-name filename log-dir)))
            filepath))))

vterm 上での gemini-cli

標準的なターミナルでの運用と同様ですが、Emacsのバッファ管理下に置ける点がメリットです。
exit による vterm 終了時、表示内容をログファイルとして自動保存する設定を施しています。

init.el
;; vtermの終了時にmd形式のログファイルを自動保存する
(add-hook 'vterm-exit-functions
          (lambda (buffer _event)
            (with-current-buffer buffer
              (let* ((dir (expand-file-name "~/Documents/MyAgentLogs/"))
                     (filename (format "%svterm-%s.md" dir (format-time-string "%Y%m%d-%H%M%S"))))
                (unless (file-directory-p dir)
                  (make-directory dir t))
                (write-region (point-min) (point-max) filename)
                (message "Vterm log saved to %s" filename)))))

チャットログの横断検索

ここが「Emacsによる知識管理」の真骨頂です。
特定ディレクトリ(~/Documents/MyAgentLogs/)に蓄積された膨大なチャットログに対し、モダンな補完・検索スタック(Vertico + Consult + Corfu + Marginalia + Orderless + Embark + recentf + cape)をフル活用した高速なキーワード検索を実現しています。

過去のAIとの対話そのものを「検索可能な外部メモリ」として再定義することで、記憶の断片を即座に現在の作業バッファへと召喚することが可能になります。

init.el
;; == LLM Agentのログを検索 ==
(defun my-consult-ripgrep-agent-logs ()
  "Search in MyAgentLogs directory using `consult-ripgrep' with Japanese support."
  (interactive)
  (let* ((consult-ripgrep-args (concat consult-ripgrep-args " -a --encoding utf-8"))
         (coding-system-for-read 'utf-8)
         (coding-system-for-write 'utf-8))
    (consult-ripgrep (expand-file-name "~/Documents/MyAgentLogs/"))))

(global-set-key (kbd "C-t a") 'my-consult-ripgrep-agent-logs)

5. 翻訳と外国語のライティング

多言語(日本語・英語・イタリア語・フランス語)を横断するワークフローにおいて、以下の3段階の使い分けを行っています。

  1. ラフな翻訳: DeepLおよびGoogle翻訳(大意把握と迅速な下訳)
  2. 標準的なスペルチェック: Emacs標準の ispell(バックエンドに hunspell を採用)
  3. 精緻な校正・推敲: Geminiによる文体(Style)および構文の最適化

なお、1と2の詳細な設定については以前の記事に譲り、本稿ではGeminiを活用した高度な校正環境に絞って記述します。

Geminiによる校正

校正を必要とする領域を選択し、C-z j (my-gemini-grammar-check-region) を実行します。
Geminiに送出するシステムプロンプトは、学術的、あるいはビジネス的な文脈など、ご自身の用途に合わせて柔軟にカスタマイズしてください。

init.el
;; == 選択範囲の文法とスペルをGeminiでチェック ==
(defun my-gemini-grammar-check-region (start end)
  "選択範囲の文法とスペルをGeminiでチェックし、修正案と解説を表示する。"
  (interactive "r")
  (let* ((text (buffer-substring-no-properties start end))
         ;; プロンプトの定義:ここでは英語とイタリア語の両方に対応できるよう指示
         (system-prompt "Please act as a professional proofreader.
Check the following text for grammar, spelling, and natural phrasing.
If the text is in English, provide the explanation in English.
If it is in Italian, provide it in Italian.
Output the corrected version first, followed by a brief list of changes.

Text to check:
")
         (full-query (concat system-prompt "\n" text))
         (command (concat "gemini -p " (shell-quote-argument full-query)))
         (output-buffer-name "*Gemini Grammar Check*")
         (buf (get-buffer-create output-buffer-name)))

    (message "Geminiで文法チェック中...")

    (with-current-buffer buf
      (setq buffer-read-only nil)
      (erase-buffer)
      (when (fboundp 'markdown-mode) (markdown-mode))
      (setq-local ansi-color-for-comint-mode t))

    (let ((proc (start-process-shell-command "gemini-grammar-process" buf command)))
      (set-process-sentinel proc
                            (lambda (p e)
                              (when (string= e "finished\n")
                                (message "チェックが完了しました。")))))
    (display-buffer buf)))

(global-set-key (kbd "C-z j") 'my-gemini-grammar-check-region)

バッファの直接置換

本関数は、Geminiが生成した校正案を選択領域に直接上書きします。
情報の「破壊的な変更」を伴うため、実行には注意を要しますが、推敲のテンポを損なわない利点があります。
Markdownの装飾やGeminiによる冗長な解説を排除し、純粋に「修正後のテキスト」のみを返却させるようプロンプトを設計しています。

init.el
;; == 選択範囲の文章をGeminiで校正、置換 ==
(defun my-gemini-grammar-replace-region (start end)
  "選択範囲の文章をGeminiで校正し、元の文章を修正後のものに直接置き換える。"
  (interactive "r")
  (let* ((text (buffer-substring-no-properties start end))
         ;; 置換専用の厳格なプロンプト
         (system-prompt "Please correct the following text. 
Provide ONLY the corrected text. 
Do not include any explanations, preamble, or markdown code blocks (backticks). 
Preserve the original language (English or Italian).

Text to correct:
")
         (full-query (concat system-prompt "\n" text))
         ;; geminiコマンドの構築
         (command (concat "gemini -p " (shell-quote-argument full-query))))

    (message "Geminiで置換校正中...")
    ;; 第5引数を t にすることで、リージョンを出力結果で置換する
    (shell-command-on-region start end command nil t)
    (message "置換が完了しました。")))

(global-set-key (kbd "C-z J") 'my-gemini-grammar-replace-region)

最後に

長文となりましたが、以上が「文字情報のインプットとアウトプットを生活の糧とする」ためのEmacs構成案です。

UNIX的なシンプルさと、現代的なAIの強力な推論能力をEmacsという単一のコンテキストで統合することは、単なる効率化を超え、思考の明晰さを保つための強力な一助となります。
本稿が、同じ道を歩むEmacsユーザーの皆様の知的生産活動に、少しでも寄与できれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?