Author Archives: AKIYAMA

2023-08-18

before-stringに別のオーバーレイのfaceが適用されない

before-stringプロパティを持つオーバーレイ(ov1)があったとします。そのオーバーレイ(ov1)を囲むように別のオーバーレイ(ov2)もあったとします。その別のオーバーレイ(ov2)がfaceプロパティを持っていた場合、そのfaceはov1のbefore-stringに影響するでしょうか。

[OV1BEFORE][OV1TEXT] OV2TEXT][OV2TEXT ov1の範囲ov2の範囲ov1のbefore-stringによって生成された部分青くハイライトするfaceが設定されている
図1: 二つのオーバーレイが重なる様子

色々試してみたのですが、なかなか影響させる方法が見つかりませんでした。

ov1のbefore-stringはov1が囲んでいるテキストの最初の文字のfaceテキストプロパティのみに影響を受けるようです(もちろんbefore-string自体にテキストプロパティが付いている場合は別です)。

これはtransient-mark-modeやhl-line-modeを使ってbefore-stringを持つオーバーレイを囲ってみればよく分かります。before-stringの部分だけハイライトされません。

Emacs Lispで再現するコードは次のようになります。

(let ((beg (point))
      (_ (insert "[OV2TEXT [OV1TEXT] OV2TEXT]"))
      (end (point)))
  (let ((ov1 (make-overlay (+ beg 9) (- end 9))))
    (overlay-put ov1 'evaporate t)
    (overlay-put ov1 'before-string "[OV1BEFORE]"))
  (let ((ov2 (make-overlay beg end)))
    (overlay-put ov2 'evaporate t)
    (overlay-put ov2 'face '(:background "#4080c0"))))

結果は次のようになります。

before-stringに他のオーバーレイのfaceプロパティが適用されない様子
図2: before-stringに他のオーバーレイのfaceプロパティが適用されない様子

色々変えて試してみました。

  • priorityプロパティを色々指定してみても変わりません。
  • ov2の範囲を色々変えても変わりません。
  • after-stringもbefore-stringと同様に影響を受けません。
  • [OV1TEXT]の先頭文字([)にfaceテキストプロパティ(実際にはfont-lock-face)を付けると、before-stringにはそのfaceが適用されます。つまり (put-text-property (+ beg 9) (+ beg 10) 'font-lock-face '(:background "red")) のように。これは回避策には利用できそうですがov2のfaceが適用されているわけではありません。ちなみにafter-stringは[OV1TEXT]の最後の文字……ではなく、その次の文字に設定したfaceが適用されます。
  • displayプロパティで表示した文字列([OV1TEXT]を置き換える)は影響を受けます。

displayプロパティは影響を受ける。その事実を知ったとき、私にはあるアイデアが浮かびました。before-stringにdisplayテキストプロパティを付けたらどうなるんだろう。つまり次のようにするわけです。

(let ((beg (point))
      (_ (insert "[OV2TEXT [OV1TEXT] OV2TEXT]"))
      (end (point)))
  (let ((ov1 (make-overlay (+ beg 9) (- end 9))))
    (overlay-put ov1 'evaporate t)
    (overlay-put ov1 'before-string
                 ;; ↓★displayテキストプロパティを設定する。
                 (propertize "_" 'display "[OV1BEFORE-DISPLAY]")))
  (let ((ov2 (make-overlay beg end)))
    (overlay-put ov2 'evaporate t)
    (overlay-put ov2 'face '(:background "#4080c0"))))

結果は何と……

before-stringに他のオーバーレイのfaceプロパティが適用されている様子
図3: before-stringに他のオーバーレイのfaceプロパティが適用されている様子

ちゃんと適用されました!

これらは一体どう解釈すれば良いのでしょうか。

まずbefore-stringに他のオーバーレイのfaceが適用されないのはバグでしょうか、意図した仕様でしょうか、それとも単に未定義動作(どうなっても文句は言えない)なだけでしょうか。前述したとおりtransient-mark-modeで範囲選択すればすぐに分かるので誰も気が付かないと言うことは無いと思うんですよね。さりとてこの挙動に何かメリットがあるのかと問われればあまり思いつきません。

一方before-stringのdisplayには効くというのはどうなのでしょうか。この挙動に頼って良いものなのでしょうか。

GNUのサイトに行ってバグトラッカーとメールのアーカイブを何度か行ったり来たりした後、嫌になって探すのを諦めました。

個人的にはどちらにも適用されるのが自然な挙動のように感じます。

今回の問題が気になったきっかけは、dired-details-rでhl-line-modeが正しく機能しなかったことです。dired-details-rでは、行末の"\n"部分にオーバーレイをかけてbefore-stringでファイルの詳細情報を表示しています1。なので、ファイルの詳細情報の部分は一切ハイライトされません。これでは現在の行をハイライトする意味がありません。

dired-details-rでhl-line-modeが正しく機能しない様子
図4: dired-details-rでhl-line-modeが正しく機能しない様子

すでに色々回避策を適用してしまったのですが、もしbefore-stringのdisplayに頼って良いのならもっとシンプルで安定したコードに出来そうです。

いやはや、Emacsのテキスト&オーバーレイプロパティまわりは何とも複雑ですね。

(追記:dired-details-rでhl-line-modeが正しく機能しない件は解決しました! ちなみにこのテクニックはall-the-icons-dired(私は色々独自に手を入れて使っています)でも有効です。あれはafter-stringでアイコンを挿入するので、そのままではhl-line-modeでアイコン部分がハイライトされません)

hl-line-modeが完全に機能するようになった様子
図5: hl-line-modeが完全に機能するようになった様子

脚注:

1

その辺りの経緯については以前に書いたと思います。たぶんEmacsでdisplayプロパティを使って改行を置き換えると非常に遅くなる件のあたり

2023-08-17

image-diredにサムネイル画像関連フックを追加する

もっと……もっとフックを……、フックが欲しいのじゃ!

私はdired-details-rを使ってファイルの詳細情報をファイル名の右側に表示していますが、image-diredでファイル名の前にサムネイルを追加すると表示がずれることがあります。

少し前の改良でファイル名の前にあるアイコンやらサムネイルやらの幅を考慮することは出来たのですが、それはdiredがディレクトリを読み込んだ直後の話。その後でサムネイルの状態に何か変化があると、やはりずれてしまいます。

典型的なのは、サムネイルの表示・非表示を切り替える操作をした場合。 image-dired-dired-toggle-marked-thumbs コマンド(C-t C-tで選択した画像のサムネイルをトグル)や私の改造だと my-image-dired-dired-show-all-thumbs コマンド(C-t C-aで全画像のサムネイルを表示)あたりの操作です。表示すれば詳細が右にずれますし、整えてから消せば左にずれます。

もう一つはサムネイル画像を生成し終わったとき。新しくサムネイルを生成するときは、ひとまず無効な画像(30ピクセル四方の正方形です)を表示させておいて、生成が終わったら更新するようにimage-diredは出来ています。問題はこれが非同期であるという点です(もちろん長時間待たされないという意味では良い点です)。サムネイルを生成するための外部プロセス(ImageMagickやらGraphicsMagickやら)が終了したらclear-image-cacheを実行することで画像を表示するオーバーレイが更新されます。その時に詳細情報が右にずれます。非同期なのでそのタイミングは予測できません。

というわけで次図のような画面が出来上がるわけです。

詳細情報が右にずれたdiredバッファ
図1: 詳細情報が右にずれたdiredバッファ

もちろんgを押せばすぐに揃うのですが、面倒くさいせいかなぜか押さないことも多いです。するとずれた部分が時々目の中にチラチラ入ってきて何だか気分が悪いです。

それに何だかみっともないじゃないですか。こうやってずれたスクリーンショットを気が付かずに出してしまうこともあります(実際に最近の記事の中にあります!)。こんなに好き勝手に改造して自己満足に浸っているくせにズレてるのかよ! みたいな。

というわけで直すことにしたのでした。

これを直すには、diredバッファへサムネイルのオーバーレイを挿入した後とサムネイル画像の生成が終わった後に、見た目を更新する処理を挟む必要があります。しかしそのようなタイミングで呼び出してくれる便利なフックはもちろん存在しません。フック……もっとフックを! と思うことはEmacsではいつものことです。というわけで、そのフック(それらのタイミングで任意の関数を呼び出す仕組み)をimage-diredに追加してみます。

まずはサムネイルを表示・消去した直後に呼び出すフック。diredバッファにサムネイルを表示するオーバーレイを挿入するのはimage-dired-dired.el内のimage-dired-dired-toggle-marked-thumbsコマンドです(Emacs 29.1時点)。その中身は次の通り。

;; Emacs 29.1付属のimage-dired-dired.elより
;;;###autoload
(defun image-dired-dired-toggle-marked-thumbs (&optional arg)
  "Toggle thumbnails in front of marked file names in the Dired buffer.
If no file is marked, toggle display of thumbnail on the current file's line.
ARG, if non-nil (interactively, the prefix argument), specifies the files
whose thumbnail display to toggle instead of the marked files: if ARG is an
integer, use the next ARG (or previous -ARG, if ARG<0) files; any other
value of ARG means toggle thumbnail display of the current line's file."
  (interactive "P" dired-mode)
  (setq image-dired--generate-thumbs-start  (current-time))
  (dired-map-over-marks
   (let ((image-pos  (dired-move-to-filename))
         (image-file (dired-get-filename nil t))
         thumb-file
         overlay)
     (when (and image-file
                (string-match-p (image-dired--file-name-regexp) image-file))
       (setq thumb-file (create-image
                         (image-dired--get-create-thumbnail-file image-file)))
       ;; If image is not already added, then add it.
       (let ((thumb-ov (cl-loop for ov in (overlays-in (point) (1+ (point)))
                                if (overlay-get ov 'thumb-file) return ov)))
         (if thumb-ov
             (delete-overlay thumb-ov)
           (put-image thumb-file image-pos)
           (setq overlay
                 (cl-loop for ov in (overlays-in (point) (1+ (point)))
                          if (overlay-get ov 'put-image) return ov))
           (overlay-put overlay 'image-file image-file)
           (overlay-put overlay 'thumb-file thumb-file))
         ;; ★ここに追加したい
         )))
   ;; Show or hide thumbnail on ARG next files.
   arg)
  (add-hook 'dired-after-readin-hook
            'image-dired-dired-after-readin-hook nil t))

この関数の (if thumb-ov (overlay-put overlay 'thumb-file thumb-file)) の後くらいに処理を挟みたいわけです。この関数はトグル動作なので、表示するときと消去するときの両方で通るところに挟むのが良いでしょう。しかしそこに処理を挟むのは困難です。

それ以前に、私はこの前の改造でこの関数をバラしてサムネイルの前後の余白を調整したところでした。

なので、その改造後の関数に手を入れてフック機能を追加してしまいましょう。以下★印の所三箇所を追加しました。

(defun my-image-dired-dired-toggle-marked-thumbs (&optional arg)
  ;; Derived from `image-dired-dired-toggle-marked-thumbs'
  "Toggle thumbnails in front of file names in the Dired buffer.
If no marked file could be found, insert or hide thumbnails on the
current line.  ARG, if non-nil, specifies the files to use instead
of the marked files.  If ARG is an integer, use the next ARG (or
previous -ARG, if ARG<0) files."
  (interactive "P")
  (dired-map-over-marks
   (my-image-dired-dired-set-thumb-visibility 'toggle) ;;ファイル毎の処理を分離
   arg             ; Show or hide image on ARG next files.
   'show-progress) ; Update dired display after each image is updated.
  (add-hook 'dired-after-readin-hook
            'image-dired-dired-after-readin-hook nil t))

;; ★2023-08-17追加
(defvar my-image-dired-dired-change-thumb-hook nil
  "diredバッファ内のサムネイルの表示状態が変化したら呼び出されるフッ
クです。

呼ばれるときの引数は(THUMBNAIL-OVERLAY ORIGINAL-IMAGE-FILENAME
IMAGE-POSITION)です。現在のバッファは変化したdiredバッファです。")

(defun my-image-dired-dired-set-thumb-visibility (visibility)
  ;; Derived from `image-dired-dired-toggle-marked-thumbs'
  (let ((image-pos  (dired-move-to-filename))
        (image-file (dired-get-filename nil t)))
    (when (and image-file
               (string-match-p (image-file-name-regexp) image-file))
      (let* ((thumb-file
              ;; Emacs 28まで
              ;;(image-dired-get-thumbnail-image image-file)
              ;; Emacs 29から
              (create-image
               (image-dired--get-create-thumbnail-file image-file)))
             (thumb-ov (cl-loop for ov in (overlays-in (point) (1+ (point)))
                                if (overlay-get ov 'thumb-file) return ov)))
        ;; 他から使うためにトグル以外もできるようにした
        (if thumb-ov
            (when (memq visibility '(nil toggle))
              (delete-overlay thumb-ov)
              ;; ★2023-08-17追加 : 表示→非表示
              (run-hook-with-args 'my-image-dired-dired-change-thumb-hook
                                  nil image-file image-pos))
          (when (memq visibility '(t toggle))
            ;; ★2023-08-17修正 : 非表示→表示
            (let ((new-thumb-ov
                   ;; 独自の関数を呼ぶ
                   (my-image-dired-dired-create-thumbnail-overlay
                    image-pos image-file thumb-file)))
              (run-hook-with-args 'my-image-dired-dired-change-thumb-hook
                                  new-thumb-ov image-file image-pos))))))))

こうしておくことで、一つのサムネイルが表示・非表示されるたびに任意の処理を挟むことが出来ます。そこでレイアウトを整えてやろうというわけです。

次にサムネイル画像の生成が終わったとき。画像の生成はimage-dired-external.el内のimage-dired-create-thumb-1関数で行っています。内容を見てみましょう。

;; Emacs 29.1付属のimage-dired-external.elより

(defun image-dired-create-thumb-1 (original-file thumbnail-file)
  "For ORIGINAL-FILE, create thumbnail image named THUMBNAIL-FILE."
  (image-dired--check-executable-exists
   'image-dired-cmd-create-thumbnail-program)
  (let* ((size (number-to-string (image-dired--thumb-size)))
         (modif-time (format-time-string
                      "%s" (file-attribute-modification-time
                            (file-attributes original-file))))
         (thumbnail-nq8-file (replace-regexp-in-string ".png\\'" "-nq8.png"
                                                       thumbnail-file))
         (spec `((?s . ,size) (?w . ,size) (?h . ,size)
                 (?m . ,modif-time)
                 (?f . ,original-file)
                 (?q . ,thumbnail-nq8-file)
                 (?t . ,thumbnail-file)))
         (thumbnail-dir (file-name-directory thumbnail-file))
         process)
    (when (not (file-exists-p thumbnail-dir))
      (with-file-modes #o700
        (make-directory thumbnail-dir t))
      (message "Thumbnail directory created: %s" thumbnail-dir))

    ;; Thumbnail file creation processes begin here and are marshaled
    ;; in a queue by `image-dired-create-thumb'.
    (let ((cmd image-dired-cmd-create-thumbnail-program)
          (args (mapcar
                 (lambda (arg) (format-spec arg spec))
                 (if (memq image-dired-thumbnail-storage
                           image-dired--thumbnail-standard-sizes)
                     image-dired-cmd-create-standard-thumbnail-options
                   image-dired-cmd-create-thumbnail-options))))
      (image-dired-debug "Running %s %s" cmd (string-join args " "))
      (setq process
            (apply #'start-process "image-dired-create-thumbnail" nil
                   cmd args)))

    (setf (process-sentinel process)
          (lambda (process status)
            ;; Trigger next in queue once a thumbnail has been created
            (cl-decf image-dired-queue-active-jobs)
            (image-dired-thumb-queue-run)
            (when (= image-dired-queue-active-jobs 0)
              (image-dired-debug
               (format-time-string
                "Generated thumbnails in %s.%3N seconds"
                (time-subtract nil
                               image-dired--generate-thumbs-start))))
            (if (not (and (eq (process-status process) 'exit)
                          (zerop (process-exit-status process))))
                (message "Thumb could not be created for %s: %s"
                         (abbreviate-file-name original-file)
                         (string-replace "\n" "" status))
              (set-file-modes thumbnail-file #o600)
              (clear-image-cache thumbnail-file)
              ;; PNG thumbnail has been created since we are
              ;; following the XDG thumbnail spec, so try to optimize
              (when (memq image-dired-thumbnail-storage
                          image-dired--thumbnail-standard-sizes)
                (cond
                 ((and image-dired-cmd-pngnq-program
                       (executable-find image-dired-cmd-pngnq-program))
                  (image-dired-pngnq-thumb spec))
                 ((and image-dired-cmd-pngcrush-program
                       (executable-find image-dired-cmd-pngcrush-program))
                  (image-dired-pngcrush-thumb spec))
                 ((and image-dired-cmd-optipng-program
                       (executable-find image-dired-cmd-optipng-program))
                  (image-dired-optipng-thumb spec)))))))
    process))

この関数は引数にoriginal-fileとthumbnail-fileを取ります。分かりやすいですね。

start-processで外部プロセスを起動しています。

その直後にプロセスオブジェクトのsentinelとしてlambda関数を設定することでプロセスの終了を検出しています。sentinelはプロセスの状態が変わったときに呼び出されます。

問題なのはこれがlambda、つまり匿名の関数だということ。この関数にadviceを追加して処理を追加するようなマネはできません。

しかしその後を見ると、このimage-dired-create-thumb-1はプロセスオブジェクトを返却しています。やった! ラッキー!! プロセスオブジェクトが得られるなら、そのsentinelを書き替えることが出来ます。次のように。

(defvar my-image-dired-create-thumb-hook nil)

(defun my-image-dired-create-thumb-1-around-for-call-hook
    (orig-fun original-file thumbnail-file &rest args)
  "`image-dired-create-thumb-1'に対するaround adviceです。"
  (let* (;; 元のimage-dired-create-thumb-1を呼び出す。
         (process (apply orig-fun original-file thumbnail-file args))
         ;; 返ってきたprocessオブジェクトの元のsentinelをとっておく。
         (orig-sentinel (process-sentinel process)))
    ;; sentinelを横取りする。
    (set-process-sentinel
     process
     (lambda (process status)
       (prog1
           ;; 元のsentinelを呼ぶ。
           (funcall orig-sentinel process status)
         ;; 成功だったら、フックを呼び出す。
         (when (and (eq (process-status process) 'exit)
                    (zerop (process-exit-status process)))
           (run-hook-with-args 'my-image-dired-create-thumb-hook
                               original-file thumbnail-file)))))
    process))

(advice-add 'image-dired-create-thumb-1 :around
            #'my-image-dired-create-thumb-1-around-for-call-hook)

つまり、image-dired-create-thumb-1が設定した元のsentinelをとっておいて、独自の関数にsentinelを書き替えてしまい、その独自の関数は元のsentinelを呼び出してから追加の処理をするわけです。

こういう横取りの仕方を見ると私はよく割り込みベクタのフックを思い出します。MS-DOSで常駐プログラム書いたり、システムコールをフックして処理を挟んでみたり。やってることは昔と変わらないなー。

というわけでこれで処理を挟む準備は整いました。後は次のようにすればdired-details-rがレイアウトを整えてくれます。

;; サムネイル画像をdiredバッファに挿入した後、または削除した後の処理。
;; 呼び出されるときのcurrent-bufferはdiredバッファです。

(defun my-image-dired-dired-update-on-change-thumb (_ov _original-file pos)
  (save-excursion
    (goto-char pos)
    (when (fboundp 'dired-details-r-update-current-line)
      (dired-details-r-update-current-line))))

(add-hook 'my-image-dired-dired-change-thumb-hook
          #'my-image-dired-dired-update-on-change-thumb)

;; サムネイル画像を生成し終わったときの処理。
;; 呼び出されるときのcurrent-bufferがどこかは分からないので注意すること。

(defun my-image-dired-dired-update-on-create-thumb (original-path thumb-path)
  ;; 念のため確実にフルパスにする。
  (setq original-path (expand-file-name original-path))
  (setq thumb-path (expand-file-name thumb-path))

  (let ((dired-buffers
         ;; 元画像があるディレクトリを表示するdiredバッファを列挙する。
         ;; サブディレクトリを挿入していると複数あり得る。
         (let ((original-dir (file-name-directory original-path)))
           (cl-loop for buf in (buffer-list)
                    when (and
                          (eq (buffer-local-value 'major-mode buf) 'dired-mode)
                          (assoc original-dir
                                 (buffer-local-value 'dired-subdir-alist buf)))
                    collect buf))))

    ;; 各diredバッファについて:
    (dolist (buf dired-buffers)
      (with-current-buffer buf
        ;; @todo サムネイル左右の余白も更新したい。
        ;; dired-details-rでoriginal-pathの行だけ更新する。
        (when (fboundp 'dired-details-r-update-file)
          (dired-details-r-update-file original-path))))))

(add-hook 'my-image-dired-create-thumb-hook
          'my-image-dired-dired-update-on-create-thumb)

画像生成後の方は、いつ呼び出されるのか予測が出来ないので少し込み入っています。

2023-08-17

image-diredで不要になったサムネイルを削除する

前回の続き。image-diredが生成するサムネイルを元の画像の場所が分かるような形式にしたのでした(image-dired-thumbnail-storage'image-dired-dir のときの話)。

image-diredのサムネイルを元の画像の場所が分かる形式にする(2023-08-17修正あり)

今回はこの仕組みを使って、不要になったサムネイルを削除するコマンドを作ります。

まずサムネイルのパスから元の画像の場所を求める関数を作成します。

;;;; サムネイルファイル名のデコード

(defun my-image-dired--decode-thumb-name (str)
  "STR内の%xx表記を元の文字に戻します。"
  (replace-regexp-in-string
   "%[0-9A-Fa-f][0-9A-Fa-f]"
   (lambda (entity) (char-to-string (string-to-number (substring entity 1) 16)))
   str t t))

(defun my-image-dired-thumb-original-file-name (thumb-path)
  "サムネイル画像THUMB-PATHの元画像ファイルのファイル名(ディレクトリ
部分を含まない)を返します。"
  (my-image-dired--decode-thumb-name (file-name-sans-extension ;;jpgを取り除く
                                      (file-name-nondirectory thumb-path))))

(defun my-image-dired-thumb-original-dir (thumb-path)
  "サムネイル画像THUMB-PATHの元画像ファイルがあるディレクトリを返します。"
  (setq thumb-path (expand-file-name thumb-path))
  (unless (string-prefix-p (expand-file-name (image-dired-dir))
                           thumb-path)
    (error "THUMB-PATH is not in the `image-dired-dir'"))
  (my-image-dired--decode-thumb-name
   (file-name-nondirectory
    (directory-file-name
     (file-name-directory thumb-path)))))

(defun my-image-dired-thumb-original-path (thumb-path)
  "サムネイル画像THUMB-PATHの元画像ファイルへのパスを返します。
元画像ファイルは存在しているとは限りません。"
  (file-name-concat (my-image-dired-thumb-original-dir thumb-path)
                    (my-image-dired-thumb-original-file-name thumb-path)))

そしてこれらを使って元の画像が既に無くなっているサムネイルを削除するコマンドを作りました。

;;;; サムネイルの掃除
(defvar my-image-dired-clean-verbose nil)

(defun my-image-dired-clean-thumbs-dir (thumbs-dir &optional forced-delete-p)
  "サムネイルが格納されているディレクトリTHUMBS-DIR内を掃除します。

THUMBS-DIRは`my-image-dired--thumb-dir'で作成されたような、元のディ
レクトリが%エンコードされているパスです。その下にあるファイルは
`my-image-dired--thumb-file-name'でエンコードされている必要があり
ます。

FORCED-DELETE-Pがnon-nilの時は、元画像がまだあるサムネイルも含め
て全てのサムネイルが削除されます。"
  (let ((original-dir (my-image-dired-thumb-original-dir
                       (file-name-as-directory thumbs-dir)))
        (valid-count 0)
        (deleted-count 0)
        (error-count 0))
    (dolist (thumb-path (directory-files thumbs-dir t "\\.jpg\\'"))
      (let* ((original-filename (my-image-dired-thumb-original-file-name
                                 thumb-path))
             (original-path (file-name-concat original-dir original-filename)))
        (if (and (not forced-delete-p)
                 (file-exists-p original-path))
            ;; 有効なサムネイルが存在した。
            (cl-incf valid-count)
          ;; 無効なサムネイルを消す。
          (when my-image-dired-clean-verbose
            (message "Delete thumb %s (Original:%s)" thumb-path original-path))
          (condition-case err
              (progn
                (delete-file thumb-path)
                (cl-incf deleted-count))
            (error
             (when my-image-dired-clean-verbose
               (message "Failed to delete thumb %s (Original:%s) err=%s" thumb-path original-path err))
             (cl-incf error-count))))))

    ;; 有効なサムネイルが存在しないならディレクトリの削除を試みる。
    (when (= valid-count 0)
      (when my-image-dired-clean-verbose
        (message "Delete thumbs dir %s" thumbs-dir))
      (ignore-errors (delete-directory thumbs-dir)))

    ;; 結果を返す。
    (my-image-dired-clean-count-make deleted-count error-count valid-count)))

;; 処理結果の意味

(defun my-image-dired-clean-count-make (&optional deleted error valid)
  (list (or deleted 0) (or error 0) (or valid 0)))
(defun my-image-dired-clean-count-inc (dst src)
  (cl-loop for dst-cell on dst
           for src-value in src
           do (setcar dst-cell (+ (car dst-cell) src-value))))
(defun my-image-dired-clean-count-deleted (count) (nth 0 count))
(defun my-image-dired-clean-count-error (count) (nth 1 count))
(defun my-image-dired-clean-count-valid (count) (nth 2 count))

(defun my-image-dired-clean-report (count show-msg)
  (when show-msg
    (message "Deleted %s, Error %s, Valid %s"
             (my-image-dired-clean-count-deleted count)
             (my-image-dired-clean-count-error count)
             (my-image-dired-clean-count-valid count)))
  count)

(defun my-image-dired-clean-thumbs (&optional forced-delete-p)
  "既に存在しない画像に対するサムネイルを削除します。"
  (interactive)
  ;; `image-dired-dir'下の全てのディレクトリについて
  ;; `my-image-dired-clean-thumbs-dir'を呼び出す。
  (let ((thumbs-root-dir (image-dired-dir))
        (count (my-image-dired-clean-count-make)))
    (dolist (file (directory-files thumbs-root-dir))
      (when (not (member file '("." "..")))
        (let ((thumbs-dir (expand-file-name file thumbs-root-dir)))
          (when (file-directory-p thumbs-dir)
            (my-image-dired-clean-count-inc
             count
             (my-image-dired-clean-thumbs-dir thumbs-dir forced-delete-p))))))
    (my-image-dired-clean-report count (called-interactively-p 'interactive))
    count))

ディレクトリを限定して、お掃除をするコマンドも追加します。

(defun my-image-dired-clean-thumbs-under (original-dir &optional subdirs-p
                                                       forced-delete-p)
  "ORIGINAL-DIR下の画像に対するサムネイルのうち不要になったものを削除します。

SUBDIRS-Pがnon-nilの場合、ORIGINAL-DIR以下にあった子孫ディレクト
リも処理に含めます。

インタラクティブに実行した場合は、現在のディレクトリが
original-dirになり、コマンドのプレフィックス引数がsubdirs-pになり
ます。"
  (interactive
   (list
    (if (derived-mode-p 'dired-mode 'wdired-mode)
        (dired-current-directory) ;;複数のディレクトリを表示している場合に備えて
      default-directory)
    current-prefix-arg))

  (let ((thumbs-dir (my-image-dired--thumb-dir
                     (file-name-as-directory original-dir)))
        (count (my-image-dired-clean-count-make)))
    (if subdirs-p
        ;; 子孫ディレクトリを含める。
        ;; エンコードしたディレクトリパスと先頭がマッチする
        ;; サムネイルディレクトリを全て対象にする。
        ;; 既に削除されているディレクトリも列挙できる。
        (dolist (thumbs-dir (directory-files
                             (image-dired-dir)
                             t
                             (concat "\\`"
                                     (regexp-quote
                                      (file-name-nondirectory thumbs-dir)))))
          (my-image-dired-clean-count-inc
           count
           (my-image-dired-clean-thumbs-dir thumbs-dir forced-delete-p)))
      ;; 指定ディレクトリ下のみ。
      (when (file-directory-p thumbs-dir)
        (setq count
              (my-image-dired-clean-thumbs-dir thumbs-dir forced-delete-p))))
    (my-image-dired-clean-report count (called-interactively-p 'interactive))
    count))

ついでに無条件でサムネイルを削除するコマンドも追加しておきます。

(defun my-image-dired-delete-thumbs ()
  "全てのサムネイルを削除します。"
  (interactive
   (unless (yes-or-no-p "Delete all thumbnails? ")
     (keyboard-quit)))
  (my-image-dired-clean-report
   (my-image-dired-clean-thumbs t)
   (called-interactively-p 'interactive)))


(defun my-image-dired-delete-thumbs-under (original-dir &optional subdirs-p)
  "ORIGINAL-DIR下の画像に対するサムネイルを全て削除します。

SUBDIRS-Pがnon-nilの場合、ORIGINAL-DIR以下にあった子孫ディレクト
リも処理に含めます。

インタラクティブに実行した場合は、現在のディレクトリが
original-dirになり、コマンドのプレフィックス引数がsubdirs-pになり
ます。"
  (interactive
   (list
    (if (derived-mode-p 'dired-mode 'wdired-mode)
        (dired-current-directory) ;;複数のディレクトリを表示している場合に備えて
      default-directory)
    current-prefix-arg))

  (my-image-dired-clean-report
   (my-image-dired-clean-thumbs-under original-dir subdirs-p t)
   (called-interactively-p 'interactive)))

これで次の4つのコマンドが出来ました。

  • my-image-dired-clean-thumbs : 元の画像が無くなったサムネイルを削除
  • my-image-dired-clean-thumbs-under : 同ディレクトリ指定バージョン
  • my-image-dired-delete-thumbs : 無条件でサムネイルを削除
  • my-image-dired-delete-thumbs-under : 同ディレクトリ指定バージョン

無条件削除はdiredで~/.emacs.d/image-diredを開いて手動で削除すればいい気もするんですけどね。一応。

cleanやclean-underをいつ呼び出すかは残された問題です。一括サムネイル表示コマンド(image-dired-show-all-from-dirや私の改造版だとmy-image-dired-dired-show-all-thumbsとか)に仕込んでおくくらいでしょうか? いや、ファイル数が多いディレクトリではあまり良くない? 最悪手動で呼び出すしかないですね。そのあたりは好き好きで。

後は許容容量に応じて古くなったサムネイルを削除する機能とか。「古くなった」と一口に言ってもそれはどういう意味なのか。最終アクセス日時情報が取れないと難しいところはあります。まぁ、その辺はもういいかなーという気もします。結局気が向いたときにdiredで~/.emacs.d/image-diredを開いて消せば良いのでしょう。MD5ハッシュだとそれすら簡単には出来ないので前回・今回の改造は意義があることでしょう。

それにしても本家はこの辺りどうするつもりなんでしょうね。タグやコメントでは~/.emacs.d/image-dired/.image-dired_dbというファイルを作って記録するので、結局その延長で対応表ファイルを作るくらいなのかなーと予想しますが。