Author Archives: AKIYAMA

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が埋め込まれるので読めるようにしておきました。何なんですかねこれ? 自由にボックスタイプを追加できるからって好き勝手しすぎ。これはそう……まるで闇鍋じゃないか!

2024-12-06 ,

Dired内にファイルのメタデータを表示する

というわけで、el-xmpを使えばDiredの中にファイルのメタデータを表示できるようになりました。

2024-12-06-xmp-dired-filter-and-add-columns.png

もちろんソートやフィルタ、マークも可能です。

ようやくWindowsのエクスプローラでも出来るようなことがEmacsでもできるようになりました。良かった良かった。

とは言えサポートしているファイル形式がまだ限られているのでもう少し増やしたいところですね。

とりあえずID3に対応したので、MP3のアーティスト名やアルバム名なんかも表示できるようになりました。

2024-11-30

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

昨日の続き。

前回はファイルのスコアが書かれているCSVファイルを元にDired内のファイルをソートしてみましたが、これだけだとパッと見何の順番で並んでいるのかよく分かりませんよね。やはりファイルの一覧の中にスコアの数字そのものも表示されていないとピンときません。なので今回はDiredのファイル行の中に、ファイルに関連した任意の情報を追加する方法について考えてみようと思います。

勝手にファイル情報を追加しても大丈夫なのか

前回も書いたとおりDiredは単なるテキストバッファですから、勝手に書き替えてしまえば済む話です。本当に? 結論としては、たぶん、概ね問題ないと思います。

Diredはファイルに関する情報を変数の形では保持しておらず、全てバッファ内のテキストとして保持しています。なのでファイル名を始め全ての情報は何かコマンドを実行するたびにバッファ内のテキストから毎回抽出しなければなりません。デフォルトの -al オプションでファイルリストを作成すると、ファイル行には左から順にファイルの種類(d)、モードビット、ハードリンク数、所有者名、グループ名、サイズ、タイムスタンプ、そしてファイル名が並びます(シンボリックリンクの場合はその後に -> で始まる部分が追加されます)。この中で一番重要なのは当然一番右のファイル名ですが、その位置を正確に割り出すのは案外面倒な問題が伴います。そんな中で、ファイル名の左側に好きな情報を勝手に追加してしまっても大丈夫なのでしょうか。

大丈夫なのです。ファイル名の左にどんなテキストを追加してもDiredは正確にファイル名の位置を割り出すことが出来ます。

insert-directoryの秘密

その秘密はinsert-directoryという関数にあります。この関数はlsの出力をバッファに挿入する関数です(lsが無い環境ではls-lisp.elでエミュレートします)。lsと同じようにファイルパスとスイッチを引数に取ります。今回注目すべきは、スイッチとして --dired を指定したときの挙動です。GNUのlsには --dired (または-D)というオプションがあり、これが指定されているとlsは出力の最後にファイル名部分の範囲を先頭からの文字数で列挙してくれます。例えば次のように。

> ls -al --dired 
  total 684
  drwxr-xr-x 1 user user     0 11月 27 18:36 ./
  drwxr-xr-x 1 user user     0 11月 30 15:31 ../
  -rw-r--r-- 1 user user  1948  9月 25 16:55 compface.el
  -rw-r--r-- 1 user user   906 10月 26 18:46 compface.elc
  -rw-r--r-- 1 user user 12701  9月 25 16:55 exif.el
  -rw-r--r-- 1 user user  7640 10月 26 18:46 exif.elc
  -rw-r--r-- 1 user user 13135  9月 25 16:55 gravatar.el
  -rw-r--r-- 1 user user  9698 10月 26 18:46 gravatar.elc
  -rw-r--r-- 1 user user 14269  9月 25 16:55 image-converter.el
  -rw-r--r-- 1 user user 10276 10月 26 18:46 image-converter.elc
  -rw-r--r-- 1 user user 17854  9月 25 16:55 image-crop.el
  -rw-r--r-- 1 user user 13474 10月 26 18:46 image-crop.elc
  -rw-r--r-- 1 user user 84378 10月 16 04:24 image-dired.el
  -rw-r--r-- 1 user user 83477 10月 26 18:46 image-dired.elc
  -rw-r--r-- 1 user user 17044  9月 25 16:55 image-dired-dired.el
  -rw-r--r-- 1 user user 18328 10月 26 18:46 image-dired-dired.elc
  -rw-r--r-- 1 user user 29509  9月 25 16:55 image-dired-external.el
  -rw-r--r-- 1 user user 21633 10月 26 18:46 image-dired-external.elc
  -rw-r--r-- 1 user user 13209  9月 25 16:55 image-dired-tags.el
  -rw-r--r-- 1 user user  9952 10月 26 18:46 image-dired-tags.elc
  -rw-r--r-- 1 user user  8924  9月 25 16:55 image-dired-util.el
  -rw-r--r-- 1 user user  7195 10月 26 18:46 image-dired-util.elc
  -rw-r--r-- 1 user user 23171  9月 25 16:55 wallpaper.el
  -rw-r--r-- 1 user user 24016 10月 26 18:46 wallpaper.elc
//DIRED// 59 60 109 111 160 171 219 231 279 286 334 342 390 401 449 461 509 527 575 594 642 655 703 717 765 779 827 842 890 910 958 979 1027 1050 1098 1122 1170 1189 1237 1257 1305 1324 1372 1392 1440 1452 1500 1513
//DIRED-OPTIONS// --quoting-style=literal

末尾の //DIRED// で始まる行の後に並ぶ数字はファイル名部分の範囲を示しています。最初は59ですが、これは最初のファイル名の先頭が出力の先頭から数えて59文字目にあることを意味しています。次の60はファイル名の末尾の位置を同様に表します。つまり、一番最初の . を指しています。以下同様に続きます。

そして重要なのは、insert-directoryはこの情報を読み取ってバッファ内のファイル名が書かれている範囲にテキストプロパティ dired-filename=t を設定するということです。

なので行の中にあるファイル名を表す範囲を特定するにはテキストプロパティが設定されている範囲を探すだけで良く、その前にどんなテキストがあってもそのテキストプロパティが存在する限り(少なくともファイル名を特定するには)問題ありません。そのテキストプロパティが設定されている場所を探す関数がdired-move-to-filenamedired-move-to-end-of-filenameです。その中を見れば行末までの間にある dired-filename という名前のテキストプロパティを探していることが分かります。(注:これらの関数を見るとテキストプロパティが検出できなかったときの処理も書かれていて、正規表現検索によってファイル位置の範囲を特定しようとしています。なので100%問題ないとは言い切れない部分がありますが、今回はその辺りは無視することにします)

実際に追加してみる

前回書いたコードの中から流用できるものを引っ張ってきましょう。

サンプルの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 ((file-score-alist
              (my-dired-sort-read-file-score (dired-current-directory))))
    (goto-char beg)
    (while (< (point) end)
      ;; 各行について
      (when (dired-move-to-filename) ;; ファイル名の先頭へ移動
        ;; ファイル名とスコアを取得
        (let* ((file (dired-get-filename nil t))
               (score (alist-get file file-score-alist nil nil #'string=)))
          ;; 1文字左へ移動
          (backward-char)
          ;; スコアを挿入
          (insert (if score (format " %3d" score) "    "))))
      ;; 次の行へ
      (forward-line))))

バッファ内にある全ディレクトリに対してこれを適用する関数を作成します。前回作った my-dired-sort-by-file-score 関数とほとんど同じですが、今回は挿入すると末尾の位置がずれていくためマーカーを使用しています。そして my-dired-sort-region-by-file-score の代わりに今作った my-dired-add-file-score-to-region を呼び出すようにすればOKです。

(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)))))

前回作ったソートコマンドと一緒に使ってみるとDiredバッファは次のような見た目になりました。

c:/home/user/tmp/my-dired:
drwxr-xr-x 1 user user  0 11月 30 23:14     .
drwxr-xr-x 1 user user  0 11月 30 23:13     ..
-rw-r--r-- 1 user user 87 11月 30 23:14     .file-score.csv
-rw-r--r-- 1 user user  7 11月 30 23:13 140 melon.html
-rw-r--r-- 1 user user 12 11月 30 23:14 153 strawberry.html
-rw-r--r-- 1 user user  7 11月 30 23:13 185 grape.html
-rw-r--r-- 1 user user  8 11月 30 23:13 210 cherry.html
-rw-r--r-- 1 user user  8 11月 30 23:13 230 muscat.html

スコアが表示され、その順番でファイルが並んでいることが分かります。