Emacs
org-mode
org-clock
EmacsDay 1

org-clock-in を意識しないで作業時間を記録する

はじめに

本記事では,org-clock-inを自分で呼び出さずに,タスクの作業時間を半自動で記録していく方法を紹介します.

対象者:
- Org Modeのユーザで,org-clock を使って作業時間を記録している人.

Org Mode で作業時間を記録する

通常のフローでは,作業を開始する時に org-clock-in(C-c C-x TAB) を呼び出し,作業完了時に,タスクのステータスをDONEにするか,org-clock-out(C-c C-x C-o)を呼び出して計測を終えます.すると,ツリーのLOGBOOKに次のような情報が記録され,作業時間を把握可能になります.

Screen Shot 2017-12-01 at 2.12.47.png

いくつかの作業を終えたら,org-clock-report(C-c C-x C-r)を使って作業時間を集計できます.きちんと使えれば,作業記録や見積もりに使えて非常に便利です.

ところが,作業開始時にorg-clock-inを呼び忘れると,当然ながら何も記録されません.

呼び忘れを避けるために,org-clock-inの実行を促すことを目的としたパッケージ(clocker.el)もありますが,何とか自動化したいところです.

さらに,すでに org-cloci-in で記録し始めているのに,org-clock-outせずに Emacsを終了するなどのイレギュラーをすると,次に org-clock-in するときに,以下のような厄介なダイアログが表示されてしまいます.org-clockに詳しいのなら問題ないですが,できればこの状況は避けたいところ.

Screen Shot 2017-12-01 at 1.06.13.png

ではどうするか?

手動では忘れがちな org-clock-in の呼び出しを「ナローイング」に紐付けることで問題を回避します.

基本概念としては,あるタスクに集中する = 時間計測中 になります.

Org Mode のナローイング(org-narrow-to-subtree)をそのまま使うのは少々煩雑なので,これを簡単に行うためのツール(org-tree-slide.el)を導入して,拡張した org-clock-inorg-clock-outを各種hookぶら下げることで計測を自動化します.

org-tree-slide.el のインストール

MELPA(MELPA)で配布しているので,package.elからインストールできます.

もしくは,Github から入手して,所定のパスに配置しても動きます.

org-tree-slide.el の基本設定

org-tree-slide.elは,本来ライブ編集可能なプレゼンテーションツールですが,各ツリーを簡単にナローイングするためのツールでもあります.スライドのエフェクトとヘッダを隠すモードを元に,表示関連の変数を少し変えればサクサクと心地よく使えます.

(with-eval-after-load "org-tree-slide"
  (org-tree-slide-narrowing-control-profile) ;; ナローイング用基本設定の適用
  (setq org-tree-slide-modeline-display 'outside) ;; 高速動作用(推奨)
  (setq org-tree-slide-skip-done nil)) ;; DONEなタスクも表示する
(global-set-key (kbd "<f8>") 'org-tree-slide-mode))

計測したいタスク内でM-x org-tree-slide-modeすれば(上記設定なら<f8>押下)ナローイングが実施され,マイナーモードを終了(もう一度<f8>押下)すればナローイングが解除されます.要はナローイングされている時だけ時間が計測されています.

なお,タスクの切り替えは,デフォルトでC-<C->に割当てられていますが,自由に変更できます.私はタスクの切り替えをマイナーモードが有効な場合に限り =F9= と =F10= に割り当てています.単一キー押下で制御できるので,慣れるととても快適です.

(with-eval-after-load "org-tree-slide"
  (define-key org-tree-slide-mode-map (kbd "<f9>")
    'org-tree-slide-move-previous-tree)
  (define-key org-tree-slide-mode-map (kbd "<f10>")
    'org-tree-slide-move-next-tree))

時間計測を自動化するための設定

(with-eval-after-load "org-tree-slide"
  (when (require 'org-clock nil t)

    ;; org-clock-in を拡張
    ;; 発動条件1)タスクが DONE になっていないこと(変更可)
    ;; 発動条件2)アウトラインレベルが4まで.それ以上に深いレベルでは計測しない(変更可)
    (defun my:org-clock-in ()
      (setq vc-display-status nil) ;; モードライン節約
      (when (and (looking-at (concat "^\\*+ " org-not-done-regexp))
                 (memq (org-outline-level) '(1 2 3 4)))
        (org-clock-in)))

    ;; org-clock-out を拡張
    (defun my:org-clock-out ()
      (setq vc-display-status t) ;; モードライン節約解除
      (when (org-clocking-p)
        (org-clock-out)))

    ;; org-clock-in をナローイング時に呼び出す.
    (add-hook 'org-tree-slide-after-narrow-hook #'my:org-clock-in)

    ;; org-clock-out を適切なタイミングで呼び出す.
    (add-hook 'org-tree-slide-before-move-next-hook #'my:org-clock-out)
    (add-hook 'org-tree-slide-before-move-previous-hook #'my:org-clock-out)
    (add-hook 'org-tree-slide-mode-stop-hook #'my:org-clock-out)

    ;; 一時的にナローイングを解く時にも計測を止めたい人向け
    (add-hook 'org-tree-slide-before-content-view-hook #'my:org-clock-out)))

org-clock 用の追加設定(推奨)

  • 計測時間の最小単位を指定(1分未満を記録しない)
(setq org-clock-out-remove-zero-time-clocks t)
  • Emacs終了時に,org-clock-outし忘れのタスクの時計を止めます.
(defun my:org-clock-out-and-save-when-exit ()
      "Save buffers and stop clocking when kill emacs."
      (when (org-clocking-p)
        (org-clock-out)
        (save-some-buffers t)))
(add-hook 'kill-emacs-hook #'my:org-clock-out-and-save-when-exit)
  • 記録中のタスク名を表示する場所をモードラインからフレームタイトルに変更.
(setq org-clock-clocked-in-display 'frame-title)

モードラインは常に混雑しているので,計測中のタスク名と経過時間をフレームタイトルに表示させます.

使ってみる

記録の開始と終了

ちょっと伝わりにくいかもしれませんが,アニメーションにしました.マイナーモードが有効な間は,表示中のタスクは自動的に org-clock が有効化され時間が計測されます.タイトルバーに表示されるタスク名と経過時間の切り替わりに着目してください.

output.gif

なお,入力キーが下に出ていますが,次の対応関係にあります.

呼出関数 機能
[F8] org-tree-slide-mode マイナーモードのON/OFF
[F9] org-tree-slide-move-previous-tree 前のタスクに移動(マイナーモードONが継続)
[F10] org-tree-slide-move-next-tree 次のタスクに移動(マイナーモードONが継続)

計測結果の確認

計測結果の集計範囲をサブツリーに限定した場合の例は次のようになります.

Screen Shot 2017-12-01 at 3.24.37.png

サブツリーに含まれるタスクのうち3項目しか計上されていないのは,上記のアニメーションでは,3項目以外のタスクをマイナーモードが有効な状態で表示していないためです.当該タスクにナローイングしていないのだから,計測されていないのは正しい結果です.

なお org-clock-reportを実行すれば,カーソル以下にデフォルトのclocktableが挿入されますが,私は次のようなブロックを愛用しています.Orgバッファ内に記述して,ブロック内で C-c C-c すれば,ファイル内に記録された年間エフォートを勝手に集計してくれます.

#+BEGIN: clocktable :maxlevel 3 :scope file :tstart "<2017-01-01 Sun>" :tend "<2017-12-31 Sun>"
#+END:

幾つかオプションを指定していますが,詳しくはマニュアルを参考にしてください.

時間を計測するファイルを限定する

実際の利用では,作業時間の記録は一部のファイルだけで十分というケースが多いと思います.そこで,作業時間を自動計測するファイルを限定する設定を追加します.Orgバッファのヘッダに独自変数を追加し,上記のmy:org-clock-inを少し改良すればOKです.

  • 対象のOrgバッファのヘッダに独自変数と値を追加する
#+TREE_SLIDE: autoclockin

#+STARTUP: の下辺りに置いておけばOKです.次の判定関数を使えば,置き場所を気にする必要はありません.

  • 判定関数を追加する
(defun my:tree-slide-autoclockin-p ()
  (save-excursion
    (save-restriction
      (widen)
      (goto-char 1)
      (let ((keyword "TREE_SLIDE:") ;; 上の設定で宣言した変数
            (value "autoclockin") ;; 上の変数で定義した値
            (result nil))
        (while
            (and (re-search-forward (concat "^#\\+" keyword "[ \t]*") nil t)
                 (re-search-forward value (point-at-eol) t))
          (setq result t))
        result))))
  • 前述の my:org-clock-in を修正
;; org-clock-in を拡張
;; 発動条件1)タスクがDONEになっていないこと
;; 発動条件2)アウトラインレベルが4まで.それ以上に深いレベルでは計測しない
;; 発動条件3)バッファごとの自動計測用フラグが有効なこと
(defun my:org-clock-in ()
  (setq vc-display-status nil)
  (when (and (memq (org-outline-level) '(1 2 3 4))
             (looking-at (concat "^\\*+ " org-not-done-regexp))
             (my:tree-slide-autoclockin-p)) ;; バッファごとのフラグ確認
    (org-clock-in)))

このように判定条件の一部に,追加した独自変数#+TREE_SLIDE:値の確認を追加します.

おわりに

org-tree-slide.elを利用した org-clock-inorg-clock-outの半自動化について解説しました.作業時間を計測するための専用Orgバッファを準備している人にはピッタリのアプローチだと思いますので,ぜひお試しください.

(余談)実は「今日」の作業量をモードラインに表示するオススメパッケージ(org-clock-today.el)があり,これとの組み合わせを実現したいのですが,残念ながらナローイングした範囲しか計測してくれないので,ちょっと工夫が必要そうです.

(2017-12-06)
次の設定で org-clock-today.elをハックして所望の動作になりました.ナローイングしていてもファイル全体を集計対象にできます.また,オリジナルコードにorg-clock-sum-todayから値をもらって期間設定する箇所がありますが,そのままだとタイムゾーンが考慮されないパスを通るので,org-clock-sum-todayも advice で上書きしています.

(with-eval-after-load "org-clock-today"
  (defun advice:org-clock-today-update-mode-line ()
    "Calculate the total clocked time of today and update the mode line."
    (setq org-clock-today-string
          (if (org-clock-is-active)
              (save-excursion
                (save-restriction
                  (with-current-buffer (org-clock-is-active)
                    (widen)
                    (let* ((current-sum (org-clock-sum-today))
                           (open-time-difference (time-subtract
                                                  (float-time)
                                                  (float-time org-clock-start-time)))
                           (open-seconds (time-to-seconds open-time-difference))
                           (open-minutes (/ open-seconds 60))
                           (total-minutes (+ current-sum
                                             open-minutes)))
                      (concat " " (org-minutes-to-clocksum-string total-minutes))))))
            ""))
    (force-mode-line-update))
  (advice-add 'org-clock-today-update-mode-line :override
              #'advice:org-clock-today-update-mode-line)

  (defun advice:org-clock-sum-today (&optional headline-filter)
    "Sum the times for each subtree for today."
    (let ((range (org-clock-special-range 'today nil t)))
      (org-clock-sum (car range) (cadr range)
                         headline-filter :org-clock-minutes-today)))
  (advice-add 'org-clock-sum-today :override #'advice:org-clock-sum-today))

Screen_Shot_2017-12-06_at_4_19_55.png


本記事は,Emacs Advent Calendar 2017 の初日です.
二日目の記事は,@Mwroteさんの要約! spacemacs 上で TidalCycles を導入し音楽制作するです.