org-mode をもう一度 (→ 全てを管理しない管理策)

このスクラップは
単なるマークアップ言語を超えた org-mode
の姿を目にするために、様々な org-*
を試していくノートです。すべてがリンクした世界まで行くぞ!!

Emacs 29: プロファイリングの切り替え
まずは足元を固めていきます。以前はプロファイル (init.el
のディレクトリ) の切り替えのため chemacs2
を使用していました。 Emacs 29 には emacs --init-directory <path>
で切り替えができるようです。 restart-emacs
も生えたとか。
年越しに Emacs 29 に乗り換えていきます:
$ sudo nix-channel update
$ sudo nixos-rebuild switch
$ emacs --version
GNU Emacs 29.1
Copyright (C) 2023 Free Software Foundation, Inc.
GNU Emacs comes with ABSOLUTELY NO WARRANTY.
You may redistribute copies of GNU Emacs
under the terms of the GNU General Public License.
For more information about these matters, see the file named COPYING.
楽ちん! Head build もできるのでしょうか。
アップデート後は以下のエラーにあたりました。ひとまず以下を追加しましたが、ちゃんと issue を読まないと……
(defvar native-comp-deferred-compilation-deny-list nil)

起動用スクリプト
猛烈な反対に遭いそうですが、僕は Emacs をターミナルで起動します:
#!/usr/bin/env bash
dir="$HOME/dotfiles/editor/emacs-leaf"
CARGO_TARGET_DIR=target/ra emacs -nw --init-directory "$dir" "$@"
起動スクリプトに
bash
を挟まないようにした方が良いのかもしれません。

org-mode
で書くには
Zenn Book を まだまだ足元を固めていきます。 conao3/ox-zenn.el を使えば、単ファイルの変換 (.org
-> .md
) はできそうです。 Book 全体を変換するコマンド (interactive な関数) を作ってみます。
インストール
まずは leaf.el
を使って ox-zenn
をインストールします。こんな形で org-export-with-toc
をセットして良いのでしょうか:
(leaf ox-zenn
:url "https://github.com/conao3/ox-zenn.el"
:custom ((org-zenn-with-last-modified . nil)
(org-export-with-toc . nil)))
既存のコマンド
ox-zenn
の interactive
コマンドは以下の 3 つです:
-
org-zenn-export-to-markdown
: バッファに出力 -
org-zenn-export-as-markdown
: ファイルに出力 (パスはファイル中のプロパティで指定) -
org-zenn-convert-region-to-md
: 選択範囲の org 構文を markdown に変換する。 Zenn スクラップでこれを使いたい気がしますが……?
コマンド作成
単ファイル変換 (出力先を指定)
この部分を抜き出せば良さそうです:
(let ((outfile (org-export-output-file-name ".md" subtreep)))
(org-export-to-file 'zennmd outfile async subtreep visible-only))
Book 全体の変換コマンドの作成
止めた方法 (自作関数)
以下の org-zenn-export-book
に ~/zenn/books-org/my-book
のようなパスを渡すと、 ~/zenn/books/my-book
以下に .md
ファイルを出力してくれます:
(leaf ox-zenn
:url "https://github.com/conao3/ox-zenn.el"
:custom ((org-zenn-with-last-modified . nil)
(org-export-with-toc . nil))
:config
(defun org-zenn-export-to-markdown-as (outfile &optional async subtreep visible-only)
(interactive "sOutfile: ")
(org-export-to-file 'zennmd outfile async subtreep visible-only))
(defun org-zenn-export-book (&optional org-dir)
"`$org-dir' -> `$md-dir': `$zenn/books-org/$book' -> `$zenn/books/$book'"
(interactive "sBook directory: ")
(setq org-dir (expand-file-name (or org-dir ".")))
(let* ((book (file-name-nondirectory org-dir))
(zenn (file-name-directory (directory-file-name (file-name-directory org-dir))))
(md-dir (concat zenn "books/" book "/")))
(if (not (and (file-directory-p org-dir) (file-directory-p md-dir)))
(message "Not an org book directroy?")
(dolist (src-file-name (seq-filter (lambda (s) (string-suffix-p ".org" s)) (directory-files org-dir)))
(let* ((src-file (concat org-dir "/" src-file-name))
(dst-file-name (concat (file-name-base src-file-name) ".md"))
(dst-file (concat md-dir dst-file-name)))
(with-temp-buffer
(insert-file-contents src-file)
(org-zenn-export-to-markdown-as dst-file nil nil nil))))))))
ox-zenn
の機能を使うのが良さそうです
-
publish サポート
1 つの.org
ファイル = 1 つの.md
ファイル -
subtree スタイル
1 つの.org
ファイルの中に複数の記事
conao3 さんのこういう発想は最高ですね。いずれ僕も 1 book = 1 org ファイル 程度の集約は試してみたいです。
追記: 元ネタは ox-hugo だったとか。

1 つの org ファイルを 1 つの book として export する
予定するソースファイル
各章をレベル 1 の見出しとして書きます。出力ファイル名を :EXPORT_FILE_NAME:
プロパティで設定し、出力ディレクトリは #+BOOK_DIR
で決めます。 #+LINK
は全章で共通の値を使います。
#+TITLE: Learn Git via Magit
#+BOOK_DIR: ../books/learn-git-via-magit
#+LINK: magit https://github.com/magit/magit
#+CAPTION: config.yaml
#+BEGIN_SRC yaml
#+END_SRC
* 環境構築
:PROPERTIES:
:EXPORT_FILE_NAME: ../books/learn-git-via-magit/setup
:END:
* コンフリクトの解消
:PROPERTIES:
:EXPORT_FILE_NAME: ../books/learn-git-via-magit/conflicts
:END:
Level 1 の見出しを export する
#+BOOK_DIR
を反映するため、手動で cd
します。 org-map-entries
によって、 Level 1 の見出しのすべてに org-export-to-markdown
を走らせることができました。
(defun org-zenn-export-buffer-to-book (&optional org-dir)
"Runs subtree export to each level 1 headings. Respects `#+BOOK_DIR'."
(interactive)
(let* ((dir default-directory)
(pub-dir (car (cdr (car (org-collect-keywords '("BOOK_DIR" "XYZ")))))))
;; cd into the target directory, but manually:
(when pub-dir (cd pub-dir))
;; export all the top level headings and come back:
(unwind-protect
(org-map-entries (lambda () (org-zenn-export-to-markdown nil t)) "LEVEL=1")
(when pub-dir (cd dir)))))

config.yaml
を自動生成する
config.yaml
を tangle する
* =config.yaml=
#+BEGIN_SRC yaml :tangle ../books/kyopro-bonsai-hs/config.yaml
title: "競プロ盆栽.hs"
summary: "Haskell の AtCoder 用自作ライブラリ解説"
topics: ["haskell", "atcoder"]
published: false
price: 0
chapters:
- intro
#+END_SRC
チャプター一覧の生成を自動化する
no-web syntax にてコードブロックの評価結果を tangle できます
* =config.yaml=
#+BEGIN_SRC yaml :tangle ../books/kyopro-bonsai-hs/config.yaml
title: "競プロ盆栽.hs"
summary: "Haskell の AtCoder 用自作ライブラリ解説"
topics: ["haskell", "atcoder"]
published: false
price: 0
#+END_SRC
#+NAME: zenn-headings
#+BEGIN_SRC elisp
(princ "chapters:\n")
(org-map-entries
(lambda ()
(let ((title (org-entry-get nil "EXPORT_FILE_NAME")))
(when title (princ (concat "- " title "\n")))))
"LEVEL=1")
#+END_SRC
#+BEGIN_SRC yaml :noweb yes :tangle ../books/kyopro-bonsai-hs/config.yaml
<<zenn-headings()>>
#+END_SRC
:DRAFT:
判定付きのチャプター生成
#+NAME: zenn-headings
#+BEGIN_SRC elisp
(princ "chapters:\n")
(org-map-entries
(lambda ()
(let* ((title (org-entry-get nil "EXPORT_FILE_NAME"))
(is-draft (org-entry-get nil "DRAFT")))
(when (and title (not is-draft))
(princ (concat "- " title "\n")))))
"LEVEL=1")
#+END_SRC
#+TITLE:
を使う
TODO

org -> markdown の変換がうまく行かないケース
\\
)
改行 (org-mode では行末の \\
が line break として扱われます。しかし markdown に変換する時に \\
が改行として解釈されないようです。
直接 markdown として書いてしまえばなんとかなります。
#+BEGIN_EXPORT md
1. Buffer
aa
2. BinaryHeap
bb
#+END_EXPORT

org-mode
は手順書の夢を見るか
org-mode
を作業記録に使った際の追跡性の高さに注目したいと思います。
リンク機能
行き過ぎた org-mode
は、何でもリンクしてしまうのが強みです。カレンダーと日報や TODO リストがリンクされていたり、知識の断片である org-roam
ファイルに (?) 飛んだり、おおよそブラウザや SNS アプリ以外のほぼすべてが Emacs の中で繋がるとか。
GitHub のソースにも簡単にリンクできるようです。コードリーディングの際は org-mode
でノートを取っておくだけで、誰かとコードの追跡を共有できます。将来的に自分が見返すこともあるでしょうから、この追跡性を標準にしない手は無いです。
さて試しにソースファイルへのリンクを org-store-link
で取得し、 org-insert-link
(C-c l
) から挿入してみました:
[[file:~/dev/hs/toy-lib/src/Data/Vector/IxVector.hs::generateIV :: (Unindex i, U.Unbox a) => (i, i) -> (i -> a) -> IxUVector i a][generateIV]]
行指定には、番号ではなくテキストの内容を使う (こともできる) ようです。また org-insert-link
の際には org-store-link
の履歴が参照されるため、 org-store-link
を専用コマンドにする意義があります。
TODO: ローカルのファイルではなく upstream リポジトリへのリンクを取得し保存したい
org-babel
)
コードブロックの評価 (言語紹介で REPL を叩いてみせることがありますが、実際に .org
ファイルに書いたコードブロックを評価してその結果を吐かせることで、結果の正しさが保証されます。あるいは作業ログとして org-mode
が使えることになります。『ガチ言語 Haskell』を書くときも org-babel
を使っていれば良かったですね。
#+BEGIN_SRC haskell :results output
import Data.List
scanl' (+) (0 :: Int) [1, 2, 3, 4, 5]
#+END_SRC
#+RESULTS:
: [0,1,3,6,10,15]
あるいはファイル全体で :results output
を指定すると:
#+PROPERTY: header-args :results output
#+BEGIN_SRC haskell
import Data.List
scanl' (+) (0 :: Int) [1, 2, 3, 4, 5]
#+END_SRC
#+RESULTS:
: [0,1,3,6,10,15]
- TODO:
import
などを外だしする - TODO: literate haskell

org-capture
機能
規定ファイルに追記するための API のようです。たとえば task.org
にささっと TODO
を追加する際に使えます。
主な設定
Setting up capture の通り、規定ファイル名を変更できます:
(setq org-default-notes-file (concat org-directory "/notes.org"))
Capture templates で task 以外の挿入もできるようになりそうです。
ショートカット
org-capture
のメリットとしては、 .org
ファイル以外の編集中にも気軽に使用できます。そのため The Org Manual においても グローバルなショートカットキーの設定が提案されています:
(global-set-key (kbd "C-c l") #'org-store-link)
(global-set-key (kbd "C-c a") #'org-agenda)
(global-set-key (kbd "C-c c") #'org-capture)
-
org-store-link
はカーソル下へのリンクを (どこへ?) 保存します。後からorg-insert-link
で貼り付けできるようになります。 -
org-agenda
は調査中です。 -
org-capture
は冒頭の通りです。
evil-collection には org-capture
のショートカット設定がありませんでした。やはりグローバルなキーバインドを設定するのが良さそうです。
Journal ファイルへの追記
Doom Emacs の j
TODO

見出しの畳み込み
org-cycle
による畳み込み状態の変更
見出しの上にカーソルがある場合、 org-cycle
によって畳み込み・展開をトグルできます。
org-cycle-emulate-tab
を nil
に設定しておくと、カーソルが本文中にある場合も外側の見出しをトグルできるようになります。
;; fold
(evil-define-key 'normal org-mode-map
"za" #'org-cycle
"zR" #'org-fold-show-all
;; close/open
"zC" #'org-fold-hide-sublevels
"zO" #'org-fold-show-subtree
)
すべての見出しの状態を変える
S-TAB でサイクルします。
一定レベルまでの畳込みに切り替える
Vim では z1
, z2
.. のようなキーバインドで畳み込みのレベルを一括変更する方法が一般的だったと思います。たぶん。
このようなキーバインドを作るためには、 outline-hide-sublevels
関数を呼び出せば良いです。 TODO: 本文を展開しつつ subheading は展開しない? ← 別に本文を展開しなくて良い気がしてきた
親見出しを畳む
C-c C-u で親見出しまで移動して畳めば良さそうです。

org-agenda
TODO リストと 大体いつもこの辺りで飽きます。今度こそ予定管理を org-mode に移行できるのか……?
基本的な機能
これを見るとイメージが掴みやすいと思います。以外とできることは少ない気も。
TODO リスト、タイムスタンプなど
見出しの挿入
M-RET
C-RET
見出しの移動
- M-十字キー: 見出しの移動
- M-S-十字キー: 見出しの移動 (小見出し付き)
見出しへのカーソル移動
-
C-c C-n
:outline-next-visible-heading
-
C-c C-n
:outline-previous-visible-heading
TODO の挿入
M-S-RET
タスクの状態
アイテムの状態は手書きできるほか、 org-todo
(C-c C-t
) や org-shiftright
(S-<right>
) などから変更できます:
;; `"|"' 以降は done 状態。 KILL は取り消し
;; https://www.karelvo.com/blog/2023-02-17-orgmode/
(setq org-todo-keywords '((sequence "TODO(t)" "WAIT(w)" "|" "DONE(d)" "KILL(k)")))
すべての子タスクが DONE になったら親タスクを DONE にする
Priorities
タスクが山積みになってくると、優先度の高いタスクを agenda の上の方に表示してほしくなります。タスクの優先度は A, B, C で、無表記のタスクは優先度 B です。
-
S-<up>
(org-shiftup
): 優先度を上げます -
S-<down>
:org-shift-down
: 優先度を下げます
タイムスタンプ
タスクの予定はタイムスタンプとして入力します。タイムスタンプを入力するのは org-time-stamp
(C-c .
) です。 Evil においては insert モードで呼び出しすると、挿入位置がズレずに済みます:
Shift + 上下左右でカーソル移動
**** TODO 歯医者に行く <2024-01-20 Sat> :life:
DEADLINE: <2024-01-24 Wed>
カレンダーを月曜日から始める:
(setq-default calendar-week-start-day 1)
C-c .
の 2 連続実行で時間範囲を指定できます: <2024-01-24 Wed>--<2024-01-31 Tue>
agenda に表示されないタイムスタンプは inactive と称され C-c !
で挿入できます (org-time-stamp-inactive
) 。 Inactive なタイムスタンプは [2024-01-24 Wed]
のように角カッコで囲われます。
タグ
タスクに対してはタグ (:life
など) の設定ができます。タグは C-c C-q
(org-set-tags-command
) から付与できます。また 親見出しのタグは小見出しに引き継がれます 。
deadline, schecule
タスクに対しては schedule (C-c s
) や deadline (C-c d
) が設定できます。 Schedule は開始日、 deadline は完了日で、いずれも agenda に出てくるようです。
log
タスクの状態を DONE
に切り替えた際に、自動的に完了時間を挿入することができます。必要なら有効化します:
(setq org-log-done 'time)
org-agenda
org-agenda
を使えば、所定の .org
ファイルから集めた予定データの一覧が見られます。
(setq org-agenda-files
(mapcar (lambda (path) (concat org-directory path))
'("/agenda.org"
"/journal.org")))
agenda がめちゃめちゃ見づらい。と思いきや、予定が増えてくるとタイムラインが格好いいですね。
- TODO: タイムラインを出す?
- TODO: 次の周へ移動、月単位表示、日単位表示など
- TODO: タスクのソート
org-super-agenda
タグや日付で agenda のグループ分けが成されるようになります。たとえばこんな感じ:
(leaf org-super-agenda
:url "https://github.com/alphapapa/org-super-agenda"
:config
(setq org-super-agenda-groups
'((:name "Next to do"
:todo "next"
:order 1)
(:name "Due Today"
:deadline today
:order 2)
(:name "Devlog"
:tag "devlog"
:order 50)
(:name "Book"
:tag "book"
:order 100)))
(org-super-agenda-mode 1))
TODO: イカしたすくりーんしょっt
org-agenda
に表示する
月単位の目標管理を TODO
org-agenda
に表示する
年単位の目標管理を TODO
(複数) タグ持ちの agenda 全部見る
たとえば :output
タグ付きのタスクを、さらに :devlog:
などタグ分けする。タスクを保存するファイルで分けても良いが、あえてタグで分けたい。
TODO

カレンダー表示
(leaf calfw)
(leaf calfw-org
:commands cfw:open-org-calendar)
TODO: サイズ設定

暗号化とアップロード
TODO

elfeed, elfeed-org
TODO

clock, estimates
TODO

babel, tangle
シェルを実行してみる
まずは実行可能な言語の設定を行い:
(org-babel-do-load-languages
'org-babel-load-languages '((ditaa . t)
(dot . t)
(shell . t)))
適当な source block を作って org-ctrl-c-ctrl-c
(C-c C-c
) で実行すると、 RESULTS
ブロックが出力されます:
#+BEGIN_SRC shell
echo test
#+END_SRC
#+RESULTS:
: test
RESULTS:
ブロックは org-export
では出力されないようです。コードブロックとして表示するには、 #:BEGIN_SRC
にて :exports both
を指定します:
-#+BEGIN_SRC shell :results output
+#+BEGIN_SRC shell :results both
プロジェクト作成時のシェルの実行履歴を残してみる
TODO: cwd
org-tangle
)
ファイルを出力する (単ファイルの出力
次のようなコードブロックの中身をファイルへ出力できます。複数のファイルを 1 つのドキュメントに収めることができ、順を追った解説に適しているかもしれません。
#+BEGIN_SRC sh :tangle ./example.sh
echo 'Hello, world!'
#+END_SRC
-
C-c C-t v
:org-babel-tangle
バッファの全ブロックの `:tangle: を実行
1 つのファイルへ延々と追記する
Nix と組み合わせてみる?

シェルの実行履歴を残す
そもそも org-mode
無しでもログ記録の開始・終了を可能にしたいが……。
TODO. cwd
のセットなど?

Evil との衝突
タブキーが効かない (ターミナルのみ)
TAB キーと C-i がターミナル内では衝突するとか。 za
を org-cycle
に割り当てて満足することにします。

#+BEGIN_YARUO
#+BEGIN_AA
にしておけば良かったです。

満足
そんなに新機能が欲しいことも無いので閉じます。