🌊

Emacsで文章執筆記録をつける

2021/08/08に公開

動機

文章を書くために、定数倍の法則を利用せよとは、三中氏の言葉(https://amzn.to/2X2jFqd )だ。
この本自体、とてもおもしろくて、論文や本を書く人は一読されたい。

定数倍の法則とは、毎日少しずつでも執筆を続けていくことで執筆が着実に進むことをいう。

書く日と書かない日がきまぐれであってはいけないし、締切直前にがんばることもいけない。
毎日、着実に執筆を続ける。
そうすれば、気づいたときにはかなりの分量が書けている。

そのための後押しをするのが、記録だ。
何月何日、何文字書いたのかを記録して、例えば月ごと、週ごとの統計を出せば、自分の取り組みを振り返ることができる。
本を書いていると、「進まねー」となって投げ出したくなるものだが、記録を見れば、「あれ?意外と進んでるやん」となる。

僕も三中氏の本を読み、定数倍の法則を経験するべく記録に取り組むことにした。

ついでに、今回の開発過程を記しておく。
プログラミング過程の実況中継的なものだ。

今回は普段使っているエディタのEmacsを使用してプログラムを書いた。Emacsの使いかたがわからない人には、意味がないように見えるかもしれない。

プログラミングを学んでも、自分でオリジナルのものが作れないという人は多い。今回のような試行錯誤を含んだ実況解説的なものは、プログラムを作るときに思考過程、作業過程を説明している。だから、「知ってるけど作れない」人には、Emacsということは関係なく参考になると思う。

やったこと

Emacsで、文章執筆の開始と終了時の字数を計算し、その日の進捗を出すようにした。

作業開始時に、スタート用のキー(F6にしといた)を押してから、終了時キーを押す(S-F6)と、こんな感じで、ログファイルに取り組みが記録される。
記録さえできれば、集計スクリプトを使って、任意の集計を得ることもできる。

08/08/21 PROGRESS: [13:19:43]--[14:21:48] => 188 characters in 62 minutes.

開発過程

Emacs Lispはちゃんと学んでないので、なかなか苦労した。
Pythonとかで書いたほうがいいんじゃないのか?とも思ったのだが、せっかくなのでEmacs Lispを書くことに取り組んだ。

一つ一つググりながら作ったのだが、ほとんどはEmacs Lisp Refernce Manualの説明で十分だった。リファレンスマニュアルは、C-h iから辿れる。日本語版も出ているので、別途インストールを勧める。

ただ、Emacs Lisp勉強会(基礎編) — ありえるえりあはとても参考になった。

リファレンスマニュアルの日本語版はAyanokoji Takeshiさんの労作でとても読みやすくできている。余談だけれど、ここからcloneして自分でコンパイルしようと思ったら、texinfoがない。Windowsでtexinfoを入れるには。。。と考えるうちに頭が痛くなってしまったので、上記のコンパイル済みのものをもらってきた。

ダウンロードしてきた和訳は、Emacs/Infoファイルの追加 - Glamenv-Septzen.netを参考にして追加した。たぶん、これも.emacs.dなどに追加したほうが良いんじゃないかなと思うんだけれど、後回しにして、他のinfoファイルが入っているディレクトリに入れてしまった。ヴァージョンアップのときに困りそうだ。

設計

まずはやりたいことを考えて、言葉で書いてみる。
普段は、最初に図を書くことが多いのだが、今回はシンプルそうなので、フローを箇条書きにしてみた。

フローはこんな感じ

  • write-start
  1. 対象のファイル名をtaskのプロパティから得る
  2. バッファをアクティブにする(なければ開く)
  3. 時間とバッファの文字数を数える
  4. 開始情報をPROGRESS:に記録
  • 執筆
  • write-end clock-outの前に
  1. 対象のファイル名を得るをtaskのプロパティから得る
  2. バッファをアクティブにする(なければ開く)
  3. 時間とバッファの文字数を数える
  4. 開始情報を探す
  5. 終了情報を追記する

この時点では、org-modeのTaskで管理しようと思っていた。が、この記事執筆時点では、ログは別ファイルに記録している。(org-modeについては、こちらがおすすめ。「生活のすべてを管理できる超強力ツール Org-mode (フェンリル | デベロッパーズブログ)」「私の org-mode の使い方 2020」

次の段階で、clock-in, clock-outにフックをかけて、字数の記録ができるようになりたいところ。(clockの使い方、便利さについては、こちらかな。「Emacs org-agenda他を運用して1年経ったのでまとめる - メモ.org」

実装の実験

Emacs Lispを書いたことがほとんどないので、それぞれのコードをどう書くのかが想像すらつかない。
一つずつ実験しながらコードを組み上げていくことにした。

ファイル名を与えて、バッファの文字数を数える

ファイルの文字数を数えるためには、それをEmacsに読み込んで数えると良さそうだ。なお、ファイルを読み込んだ場所をバッファという。(ごめん、正確じゃない。正確にはマニュアルをどうぞ。)

とはいえ、バッファの文字数を数えることすら分からなかったので、いろいろ調べて以下を実験してみた。

(defun my-count-words ()
  (interactive)
    (setq mcw-size (count-words-region (point-min) (point-max)))
    (insert mcw-size)
  )

あれ?

Ǵ

こんな文字がカーソル位置に表示される。
こんなもの生成したつもりはないんだけど。

もしかしたら、字数がそのまま文字コード的なものとみなされているのかもしれない。

字数を整数から文字列に変換する関数は・・・number-to-stringか。試してみよう。

(defun my-count-words ()
  (interactive)
    (setq mcw-size (count-words-region (point-min) (point-max)))
    (insert (number-to-string  mcw-size))
  )

できた!

この関数をM-xで実行すると、今いるバッファの文字数を数えて、カーソル位置に挿入することができる。

次は、特定のバッファに移動して、そのバッファの文字数を取得する。
与えたファイルがまだ開いていなければ、開いてから移動する。よくやる方法のようで、マニュアルに書かれていた。

     (switch-to-buffer (find-file-noselect my-count-file ))

ファイルが存在しなければ、その名前のファイルを作って、字数を数える。当然字数はゼロだ。
エラー処理をした方が良いのだが、まずは先に進む。

(setq my-count-file "Your file path")
(defun my-count-words-begin ()
  "record number of characters before edit."
  (interactive)
  (setq curbuff (current-buffer))
  (save-excursion
      (switch-to-buffer (find-file-noselect my-count-file ))
	(setq mcw-begin-size (count-words-region (point-min) (point-max)))
	)        
      (switch-to-buffer curbuf)
   (insert (number-to-string  mcw-size))
   )

save-excursionは、今いるバッファの状態を保存する。例えば、関数中でカーソルを別の場所に移動しても、関数を出るときには、元の場所に戻る。

指定したバッファの文字数を記録するのは、思ったより簡単だった。

次は、作業終了時の処理を作成する。
終了時の処理を設計して気づいたのだが、バッファに移動して文字数を数える部分は同じ処理が使えそうなので、これを関数に切り出す。

(setq my-count-file "Your file path")

(defun my-count-words (mcfile)
   (curbuff (current-buffer)))
   (save-excursion
      (switch-to-buffer (find-file-noselect mcfile ))
	(setq my-count-buffer (count-words-region (point-min) (point-max)))
	)        
   (switch-to-buffer curbuf)
   my-count-buffer)
  )

curbuffとmy-count-bufferは関数の外では不要だ。

ちょっとリファクタリングして、ローカル変数にしておく。
ついでに、関数の説明も追加しておく。

(setq my-count-file "Your file path")

(defun my-count-words (mcfile)
  (let ((mycount-buffer)
	(curbuff (current-buffer)))
    (save-excursion
      (switch-to-buffer (find-file-noselect mcfile ))
	(setq my-count-buffer (count-words-region (point-min) (point-max)))
	)        
      (switch-to-buffer curbuf)
      my-count-buffer)
  )

呼び出し側はこうだ。

(defun my-count-words-begin ()
  "record number of characters before edit."
  (interactive)
    (setq mcw-begin-size (my-count-words my-count-file))
    (setq mcw-begin-time (current-time))
  )

あとは、my-count-words-endをbeginと同じように作成して、ログを記録する部分を書く。

作業時間の記録

ここでちょっと寄り道だ。
字数を数えられるようになったら、欲が出てきた。
作業時間も記録したくなってきた。

elispで時間をどう扱うのか、検討もつかない。

またか。。。

Reference Manualを見る。時間の扱いは難しくなさそうだ。

例えば、経過時間は、以下で200秒という結果を得る。

(setq ae (current-time))
(setq ab (time-add ae -200))
(time-subtract ae ab)
;=> (0 200 0 0)

この秒の部分だけを取り出したい。正規表現を使うか、carとかcdrとか使うのか。。。と悩んだのでまたもマニュアル参照。

(format-time-string "%T" (current-time))
   (setq ae (current-time))
   (setq ab (time-add ae -200))
   (insert (number-to-string (/ (time-convert (time-subtract ae ab) 'integer) 60)))

秒以下を切り捨てて、分に直したものを得る。ポイントは、time-convertだ。これに先ほどのオブジェクト(0 200 0 0)を与えると、秒数に直してくれる。これをさらに60で除すことで、分を得る。
ちなみに、lispで割り算は商(整数部)を返すので、四捨五入等は不要だ。

だいぶやりたいことのパーツが揃ってきた。

そうそう、行頭の日付は、普通のプログラミング言語と同様だ。
以下のようにすると、日付(週数)時間を出力する。

   (insert (format-time-string "%D(%W)%T" ab))
   ;=> 08/08/21(31)16:35:52

終了時の処理

今までのを参考にすればだいたい作れる。

いちおう、計測をスタートしていない=mcw-begin-sizeが定義されていないとエラーが出てしまうので、そのときは、「Sorry, you didn't record the begin-time.」とでも表示するようにしたい。

変数が定義されているかを調べるのは、if文使えばいいけど、boundpというそのものずばりの条件があるので、これをif文に組み合わせる。

が、if文の後が問題だった。変数がない方(いわゆるelse)は、メッセージを表示させるだけなので簡単。

変数があるときはログを記録する。これがちょっとめんどう。というか、(if (条件) 処理1 処理2)というのが書式なのだが、処理1と処理2はいずれも一つの文しか書けない。2つ目を書くとそれが処理2とみなされてしまう。

どうしたものか。。。

こういうときに処理をまとめる方法がprognを使う方法だ。他の言語だと、カッコでくくったりする。
elispでは、prognを使えばいい。
これは、設定ファイルを書いたりするなかで覚えたテクニックで、マニュアルからみたのかよく覚えていない。

でも、とりあえずこれでできた。

(defun my-count-words-end ()
    (if (boundp 'mcw-begin-size)
	(progn     (switch-to-buffer (find-file-noselect my-count-log ))
		   (goto-char (point-max))
		   (forward-line 1)
		   (insert "test"
			    ))
		   )
      (message "Sorry, you didn't record the begin-time."))
    )
  (switch-to-buffer curbuf)
)

確認

改めて、出力したいものを確認しておく。

08/08/21 PROGRESS: [13:19:43]--[14:21:48] => 188 characters in 62 minutes.

冒頭の日付、途中の時間、文字数、経過時間(分)はそれぞれ計算できた。
後は、これをまとめて一つの行に組み立てるだけだ。

printf的なものがあるのかもしれないが、ここはinsertという文を使って、一つずつ書き込むことにした。次のバージョンを作るときにでも修正しよう。

insertはここまでの実験の過程でだいぶ練習したので、今はこれが楽だ。

完成

さて、以上でパーツができたので、一つに組み上げる。
今日作ったものは、こんな構造になっている。

  • write-start
  1. 対象のファイル名をsetqで指定する
  2. バッファをアクティブにする(なければ開く)
  3. 時間とバッファの文字数を数える
  4. 開始情報を変数に保存
  • 執筆
  • write-end
  1. 対象のファイル名を指定したものを得る
  2. バッファをアクティブにする(なければ開く)
  3. 時間とバッファの文字数を数える
  4. 開始情報と終了情報から字数、経過時間を計算する
  5. 執筆情報を追記する

できたのが、成果物のファイルだ。

成果物

(setq my-count-file "Your file path")
(setq my-count-log "Your log file path")

(defun my-count-words (mcfile)
  "Get number of characters from mcfile."
  (let ((mycount-buffer)
	(curbuff (current-buffer)))
    (save-excursion
      (switch-to-buffer (find-file-noselect mcfile ))
	(setq my-count-buffer (count-words-region (point-min) (point-max)))
	)        
      (switch-to-buffer curbuf)
      my-count-buffer)
  )

(defun my-count-words-begin ()
  "record number of characters before edit."
  (interactive)
    (setq mcw-begin-size (my-count-words my-count-file))
    (setq mcw-begin-time (current-time))
  )

(defun my-count-words-end ()
  "get the number of characters edited in a day."
  (interactive)
  (setq mcw-end-size (my-count-words my-count-file))
  (setq mcw-end-time (current-time))
  (setq curbuf (current-buffer))
  (save-excursion
    (if (boundp 'mcw-begin-size)
	(progn     (switch-to-buffer (find-file-noselect my-count-log ))
		   (goto-char (point-max))
		   (forward-line 1)
		   (insert (concat
			    "\n"
			    (format-time-string "%D" mcw-begin-time)
			    " PROGRESS: [" 
			    (format-time-string "%T" mcw-begin-time)
			    "]--["
			    (format-time-string "%T" mcw-end-time)
			    "] => "
			    (number-to-string (- mcw-end-size mcw-begin-size))
			    " characters in "
			    (number-to-string (/ (time-convert (time-subtract mcw-end-time mcw-begin-time) 'integer) 60))
			    " minutes."
			    ))
		   )
      (message "Sorry, you didn't record the begin-time."))
    )
  (switch-to-buffer curbuf)
)

(global-set-key [f6] 'my-count-words-begin)
(global-set-key [\S-f6] 'my-count-words-end)
(global-set-key [\C-\S-f6] 'my-count-words-report)

今後

org-clockと同様、Task管理に組み込めるようにしたい。
TaskのLOGBOOKに入れられるといいけど、かなり大変そうだ。

[ ] 集計用のスクリプトを作る(Lispじゃなければすぐに作れる)
[ ] 複数ファイルの管理を一つのログファイルでできるように。(行頭にファイル名を入れるだけなので、簡単)
[ ] 別のコンピュータでの編集(同じファイルだけどパスが違う)もまとめて集計できる方法を考える。(ファイル名の入れ方を考える)

ファイル名の入れ方だが、例えば、README.mdなんかはプロジェクトごとにある。これらが混同されないように、かつ別のコンピュータ間では同じものとみなせるように、という工夫がほしい。
ファイル名ではなく、ファイルのタグ的なものを指定するようにすればよさそうだ。
どちらにしろ、コンピュータごとにパスが違えば、その指定部分はコンピュータごとに分岐させる必要がある。

ここまで書いて気づいたけれど、org-modeのタスクと統合せず別ファイルにログを記入するなら、「今編集しているバッファ」の文字数を数えればいいだけだったかもしれない。

そういう意味では、以下のようにしてしまって、毎回、Taskにコメントを追加する際に、記録を挿入するようにしても良いかもしれない。
[ ] 指定したバッファの編集記録をカーソル位置に挿入するように変更

まだ、Lisp的な書き方というのはよくわからない。
作成の過程でマニュアルを読んだ結果、テキスト処理はかなり楽そうな印象を受けた。

Discussion