2024-12-11 ,

Diredでファイルの右側に好きな情報を追加する

いやぁ、やっぱりSVGっていいですよね。先日から作っているel-xmpですが、レーティングの表示が単なる数字なのがつまらないのでSVGで描いてみました。ひょっとしたらUnicodeやアイコンフォントで作れるのかなとも思いましたが、よく分かりませんし出来たとしてもフォントへの依存は避けられないと思ったのでSVGでいいや。それで色々調整してこんな感じで生成できました。

2024-12-11-svg-rating-text.png

何がいいって、こういう風にエディタの中に直接結果が挿入されるところですよね。私は80年代のBASICからプログラムを始めた人間ですが、あれもこんな風にコードを書いて実行したらその画面の中の好きな位置に結果が出力されるような環境でした(末尾では無く好きな位置にというのが結構重要だと思っています)。グラフィックスは別プレーンに描いてから無限ループで止めておかなければならなかったりもしましたが、時代が進むとテキストの後ろに合成表示されるようになったりもしました。私はEmacs Lispをいじっているとよくあの頃の感覚を思い出します。

それで気を良くして実際にdiredの中にレーティングを表示させてみたところ……

2024-12-11-dired-with-rating-1.png

あれ、数字のままだ。何でだ???

……ああ、私はファイルの詳細情報を右側に表示するためにdired-details-rを使用していたのでした。こいつはオーバーレイ(またはファイル数が多いときはテキストプロパティ)を使用して詳細情報をファイル名の右側に表示します。そしてオーバーレイ(またはテキストプロパティ)のdisplayプロパティを使用してファイル名前後の文字を詳細情報込みの文字列に置き換えることで無理矢理詳細情報を好きな位置に表示させているのでした。

で、SVG画像もまたdisplayプロパティを必要とします。もちろん画像を含めて全てのテキストプロパティをコピーして右側に持ってきているのですが、displayプロパティの中のdisplayプロパティはEmacsの仕様では無視されるため、SVG画像としてでは無く元の数字文字列として表示されてしまうわけです。

なのでいったんdired-details-rを切れば表示されます。

2024-12-11-dired-with-rating-2.png

この通り。

うーん、どうしよう。オーバーレイプロパティのbefore-stringやafter-stringを使えば回避できなくもないけれど、ファイル数が多いときにはテキストプロパティを使いたいので却下。

と、ここで思い出すのは前々回書いたDiredに好きなファイル情報を追加する話。

そこではファイル名の左側に情報を挿入しましたが、ファイルの右側にも情報を挿入できるのでしょうか。

そこでも触れましたが、Diredバッファ内のファイル名が書かれている範囲には dired-filename というテキストプロパティが設定されています。そのためファイル名の前後に何か余計なテキストが挿入されてもファイル名を見失うことはありません。なのでその点だけ見れば大丈夫なはずです。……本当にそうでしょうか?

ファイル名の右側に情報を追加してみる

前と同じサンプルを使って試してみましょう。

ファイルのスコアを書いた.file-score.csvファイル(前と同じ):

muscat.html,230
melon.html,140
grape.html,185
cherry.html,210
strawberry.html,153

これを読む関数(前と同じ):

(defun my-dired-sort-read-file-score (dir)
  (ignore-errors
    (with-temp-buffer
      (insert-file-contents (file-name-concat dir ".file-score.csv"))
      (goto-char (point-min))
      (cl-loop until (eobp)
               for (file score) = (split-string (buffer-substring (point) (line-end-position)) ",")
               collect (cons (expand-file-name file dir)
                             (string-to-number score))
               do (forward-line)))))

一つのディレクトリの中のファイルリストの範囲を特定する関数(前と同じ):

(defun my-dired-next-dir-files-region ()
  ;; ファイル名が無いところをスキップ
  (while (and (null (dired-move-to-filename))
              (= (forward-line) 0)))
  (unless (eobp)
    (let ((beg (line-beginning-position)))
      ;; ファイル名があるところをスキップ
      (while (and (dired-move-to-filename)
                  (= (forward-line) 0)))
      (cons beg (line-beginning-position)))))

で、肝心の追加するところは関数名は同じままで少し変更してみます。

(defun my-dired-add-file-score-to-region (beg end)
  ;; (↓when-let/if-let廃止するとか言ってるの超うざくね? アホか)
  (when-let* ((file-score-alist
               (my-dired-sort-read-file-score (dired-current-directory))))
    (goto-char beg)
    (while (< (point) end)
      ;; 各行について (★この辺から変更)
      (when-let* ((file (dired-get-filename nil t)) ;; ファイル名の取得
                  (score (alist-get file file-score-alist nil nil #'string=))) ;; スコアの取得
        ;; 両者取得できたら
        ;; 行末へ(つまりファイル名の右へ)
        (end-of-line)
        ;; スコアを挿入
        (insert (format "  %4d" score)))
      ;; 次の行へ
      (forward-line))))

最後のコマンドの部分(前と同じ):

(defun my-dired-add-file-score ()
  (interactive nil dired-mode)
  (widen)
  (goto-char (point-min))
  (let ((inhibit-read-only t))
    (while-let ((files-region (my-dired-next-dir-files-region)))
      (let ((beg (car files-region))
            (end (copy-marker (cdr files-region))))
        (my-dired-add-file-score-to-region beg end)
        (goto-char end)
        (set-marker end nil)))))

実際に試してみる

サンプルファイルがあるディレクトリで M-x my-dired-add-file-score したところ、次のような見た目が得られました。

c:/home/user/tmp/my-dired:
drwxrwxrwx  1 user user 4096 24-12-09 21:44 .
drwxrwxrwx  1 user user 4096 24-12-11 17:38 ..
-rw-rw-rw-  1 user user   87 24-12-09 18:45 .file-score.csv
-rw-rw-rw-  1 user user    8 24-12-09 18:46 cherry.html   210
-rw-rw-rw-  1 user user    7 24-12-09 18:46 grape.html   185
-rw-rw-rw-  1 user user    7 24-12-09 18:46 melon.html   140
-rw-rw-rw-  1 user user    8 24-12-09 18:46 muscat.html   230
-rw-rw-rw-  1 user user   14 24-12-09 18:47 strawberry.html   153

数字の位置がずれているのはご愛敬。揃える処理を入れていないので。問題はDiredとしてちゃんと動作するのかどうか。

少し試したくらいだと問題ないように見えます。wでコピーできるファイル名は正しいものですし、ファイルを開いたりも出来ます。

問題点と原因

しかししばらくいじっているとおかしな所も見つけました。

  • ~ (dired-flag-backup-files) や # (dired-flag-auto-save-files) によるマークができない(バックアップファイルや自動保存ファイルが検出されない)
  • 色付け(fontify、font-lock)がおかしい
    • バックアップファイルなどに色が付かない
    • マークやフラグが付いた行はファイル名だけでなくその右側のスコア部分まで色が付く

調べてみるとやはり行の末尾がファイル名の末尾であることを前提にしたコードが存在していることが分かりました。まぁ、そういうのが嫌なのでdired-details-rはオーバーレイやテキストプロパティを使っていた訳なので、案の定と言ったところです。

マークができない原因は、マークする関数が各ファイル行においてend-of-lineで行末へ飛んでからpreceding-charでその前の文字を取得・チェックしている(~や#であるかどうかをチェックしている)からでした。付近のコメントも読んでみた限り、どうも高速化のためにあえてそのようにしているフシがあります。

色付けについては、font-lockの色付けルール(dired-font-lock-keywords)が全般的に行の末尾がファイル名の末尾であることを前提にして書かれてしまっていることが原因です。例えば一番分かりやすいのが正規表現 $ をファイル名の末尾として使ってしまっている所。 $ の前には拡張子とマッチする正規表現が先行します。なので、ファイル名の後に何か情報を追加すると(その追加した情報の末尾がたまたまマッチしない限り)マッチしなくなってしまいます。

dired-details-rの改善

これらの調査を踏まえた上で、右側に詳細情報を表示したときにSVG画像が反映されない問題は結局はdired-details-rの問題なので、そちらを修正することにしました。

まずは詳細情報の表示を(オーバーレイやテキストプロパティでは無く)テキストの挿入によって行うオプションを追加。これまでにも表示方式を指定するカスタマイズ変数はあったので、そこにテキスト挿入によって表示を行う指定値(text)を追加。そしてオーバーレイやテキストを更新する所では、設定値によって代わりにテキスト挿入や削除を行うように変更。

そして今回見つけたいくつかの弊害に対処。マークの問題は関数を丸丸置き換えるしか無さそう。色付けは問題があるfont-lock-keywordsルールを置き換える関数を作成し、dired-mode-hookで実行。どちらもかなり無理矢理。もちろんまだ見つけていない問題がどこかにあるかもしれませんが、それは見つけたらその都度直しましょう。

というわけで、これによって次のように詳細情報をファイル名の右に表示しつつSVGで描かれたレーティングの星マークも表示できました。

2024-12-11-dired-with-rating-3.png

el-xmpの進捗

el-xmpの方はとりあえず、すぐにやりたいことは一通りやったので一段落といった所でしょうか。まだまだやれることはいくらでもありますがキリがないので。

そうそう、ISO base media file formatというのを解析してメタデータを抽出できるようにしたんですよ(規格書が日本円で3~4万円くらいしててドン引き)。QuickTimeから始まってMPEG4(mp4、m4a)とかJPEG2000なんかもだいたい同じ形式みたいです。基本的な構造はボックスと呼ばれるサイズとタイプのヘッダーから始まるデータブロックの羅列です。ボックスの中にボックスが入れ子になる事が良くあるのでツリーのような構造とも言えますが、それほど自己記述性は無くボックスタイプ毎に定義される内部形式(syntax)が分からなければ中に何が入っているかは分かりません(当然入れ子になっているボックスがあるのかも分かりません)。ボックスタイプは典型的な4文字コードに加えてUUIDでも表現できるようになっていて誰でも他人とぶつからないボックスタイプを追加できます。それでメタデータなのですが、案の定あちこちに散らばっている感じですね。そのファイルの素性毎にどこにあるのかまちまちです。もちろんあらゆる方式に対応することは出来ませんが、とりあえず手元にあるm4aファイルくらいは読めるようにしておきました。それと写真をPhotoshopでJPEG2000で保存し直したものなんかもXMPとEXIFが埋め込まれるので読めるようにしておきました。何なんですかねこれ? 自由にボックスタイプを追加できるからって好き勝手しすぎ。これはそう……まるで闇鍋じゃないか!