Author Archives: AKIYAMA

2025-02-21

Emacsでメールファイル(.eml等)をそのまま読む

昔から疑問に思っていて未だによく分かっていないことなのですが、Emacsから1件のメールのデータが入ったファイルを開いてその内容を読むにはどうしたら良いのでしょうか。

Web検索でちょっと調べてみても、既存のメールを取り扱うEmacs用のシステムに取りこんで読めというようなことばかりが引っかかり、単純に1件のメールだけが入ったファイルを普通のファイルを開くように読む方法がなかなか出てこないんですよね。

ここで言うメールが入ったファイルというのは沢山のメールが一つに詰め込まれたmbox形式とかではなくて、一般的なメールクライアントがメッセージを外部ファイルとして保存するときに.emlという拡張子を付けて出力するような先頭にメールヘッダーが入っているようなファイルのことです(ええと何形式というんだっけ)。内容はMIMEでエンコードされていて普通はそのままでは読めません(……ひょっとしてASCII言語圏ではそのまま読めるのでしょうか? だからあまり問題になっていない?)。

私は普段Emacs上ではWanderlustを使っているのですが、それが依存しているsemiというライブラリにMIMEエンコードされたバッファをプレビューするコマンドが含まれています。

その名もmime-view-buffer。メールファイルを開いて M-x mime-view-buffer を実行すれば、メールの内容が 別バッファに 人間が読める状態で表示されます。素晴らしい。

ちなみに検索して見つかった別の方法としては次のものがあります。これはGnusの関数(gnus-article-prepare-display)を使います。

Any way to just render an email file on disk? : r/emacs

(defun my/render-mime-message ()
  "Render the current buffer as a Gnus article."
  (interactive)
  (gnus-article-prepare-display))

これで読めると言えば読めるのですが、どうにも釈然としません。普通にEmacsからファイルを開く要領で読めるようにならないのでしょうか。要するにfind-fileで画像ファイルやpdfファイルを開いたら中身のバイナリではなく人間が読めるようなものが出るのと同じようにしたいわけです。

と、今更そのようなことを言い出したのは、先日いくつかのメールへのリンクをorg-mode文書に書きたくなって久しぶりにol-wl.elを使ったのがきっかけでした。メールへのリンクをorg-mode文書に書きたくなることはほとんどなく、近年はorg-contribが別パッケージに分かれたこともあってインストールすらされていませんでした。ol-wl.elを使うと wl: リンクタイプが追加されるのですが、そのパスにはWanderlustが管理するフォルダ表記とメッセージIDを指定します(例: wl:+inbox#somemessageid@example.jp)。そのリンクをC-c C-oで開くとWanderlustが起動してその中でメールの内容が表示されます。それを見て、単純に file: リンクタイプでメールが入っているファイルへリンクしたらダメなのかな? と思ったわけです(例: file:~/my-wl-mail-dir/inbox/123)。

画像やPDFを開くノリでできないかと思ったのですが、あれはバッファ全体をdisplayテキストプロパティ(またはオーバーレイプロパティ)で画像に置き換えることで実現しています。今回やりたいのは画像では無くテキストで置き換えること。displayプロパティで別のテキストに置き換えるとそのテキストの中にポイントを置けないのであまり望ましくないでしょう。なので、どちらかと言えばhexl-modeがファイルの中身をHEXダンプに置き換える時に使っている手法が近そうです。

ただ、これはちゃんと実装しないと置き換えた後のテキストで元のファイルを上書きしてしまうリスクがあります。

とりあえず今回必要だったのはorg-modeからリンクを張っていつでも中身を見られるようにすることだけなので、必ずしも律儀にそのファイルに関連付けられたバッファで開く必要はありません。org-modeではリンクを開くときの動作を条件毎にカスタマイズできるようになっているので、そこでmime-view-bufferで開くような関数を指定してしまえば良いのです。

(defun my-mime-view-file (file _original-link-path)
  "mime-view-bufferを使ってメールFILEのプレビューを開く。"
  (with-temp-buffer
    ;; PreviewバッファのカレントディレクトリはFILEがある場所にする。
    (setq default-directory
          (file-name-directory (expand-file-name file)))
    (insert-file-contents file)
    (mime-view-buffer
     nil
     ;; バッファ名にファイル名を入れる。
     (format "*MIME View: %s*" (file-name-nondirectory file)))))

;; Wanderlust用のメールが格納されているディレクトリにあるファイルを開
;; くときは my-mime-view-file を使う(例)。
(add-to-list 'org-file-apps '("c:/my-wl-mail-dir/" . my-mime-view-file)))

後は [[file:c:/my-wl-mail-dir/inbox/123]] のようなリンクを書けば、それをC-c C-oで開こうとすると上の関数の働きによって人間が読めるものがすぐに開くというわけです。

とりあえずこれでお茶を濁しておきますが、そのうちhexl-mode的な手法で内容を表示するmajor-modeを作りたい所。

メールが入ったファイルを読むだけのことがなんでこんなに面倒なんだろう。

2025-02-20

nerd-icons-diredへ移行

新PCへの移行に伴いこれまで使っていたall-the-icons-dired(を少し修正したもの)からnerd-icons-diredへ移行したのですが、いくつか気になった点があったので修正。

と、ソースコードを見たら、あれ、これall-the-icons-diredとほとんど同じですね。all-the-icons-diredを元にnerd-iconsを使うように修正した物っぽい? なのでこれまでall-the-icons-dired向けに修正したのがほとんどのそのまま適用出来ます。

まずはhl-line-mode(現在の行をハイライトしてくれるマイナーモード)でアイコン部分がハイライトされない問題。アイコンの背景が黒いままになってしまいます。これは以前「before-stringに別のオーバーレイのfaceが適用されない」で書いた現象が原因で、そこにも書きましたが回避策はbefore-stringやafter-stringに直接テキスト(アイコン)を書くのではなく、before-stringやafter-stringにdisplayプロパティを指定した文字列を指定して、そのdisplayプロパティでテキスト(アイコン)を表示すると、その部分にはオーバーレイのfaceが適用されるようになります。つまり、次のようにします。

 (defun nerd-icons-dired--add-overlay (pos string)
   "Add overlay to display STRING at POS."
   (let ((ov (make-overlay (1- pos) pos)))
     (overlay-put ov 'nerd-icons-dired-overlay t)
-    (overlay-put ov 'after-string string)))
+    (overlay-put ov 'after-string (propertize string 'display string))))
 

次に気がついたのはファイルが消えたのにアイコンが消えない場合があるということです。手っ取り早い対処法の一つはオーバーレイのevaporateプロパティをtにすることです。こうするとファイルの行が削除されたときに一緒にオーバーレイも消えてくれます。上で修正した場所のすぐ下にevaporateプロパティの設定を追加。

 (defun nerd-icons-dired--add-overlay (pos string)
   "Add overlay to display STRING at POS."
   (let ((ov (make-overlay (1- pos) pos)))
     (overlay-put ov 'nerd-icons-dired-overlay t)
-    (overlay-put ov 'after-string (propertize string 'display string))))
+    (overlay-put ov 'after-string (propertize string 'display string))
+    (overlay-put ov 'evaporate t)))
 

そもそもDiredバッファの変化を検出するために沢山のadviceを追加していますが、おそらくdired-after-readin-hookでやった方が簡単だと思います。多分。注意点としては、このフックは変更箇所をnarrowingしてから呼び出されるということがあります。なのでこれを使うなら不用意にwidenして全体を処理してはいけません。次のように修正してみました。

   "Get nerd-icons-dired overlays at POS."
   (apply #'nerd-icons-dired--overlays-in `(,pos ,pos)))
 
-(defun nerd-icons-dired--remove-all-overlays ()
-  "Remove all `nerd-icons-dired' overlays."
+(defun nerd-icons-dired--remove-all-overlays-from-whole-buffer ()
+  "Remove all `nerd-icons-dired' overlays from the whole buffer."
   (save-restriction
     (widen)
-    (mapc #'delete-overlay
-          (nerd-icons-dired--overlays-in (point-min) (point-max)))))
+    (nerd-icons-dired--remove-all-overlays)))
+
+(defun nerd-icons-dired--remove-all-overlays ()
+  "Remove all `nerd-icons-dired' overlays within the narrowed region."
+  (mapc #'delete-overlay
+        (nerd-icons-dired--overlays-in (point-min) (point-max))))
 
 (defun nerd-icons-dired--refresh ()
-  "Display the icons of files in a Dired buffer."
+  "Display the icons of files within the narrowed region of the Dired buffer."
   (nerd-icons-dired--remove-all-overlays)
   (save-excursion
     (goto-char (point-min))
@@ -110,40 +114,22 @@
       (nerd-icons-dired--refresh))
     result)) ;; Return the result
 
+(defun nerd-icons-dired--after-readin-hook ()
+  (when nerd-icons-dired-mode
+    (nerd-icons-dired--refresh)))
+
 (defun nerd-icons-dired--setup ()
   "Setup `nerd-icons-dired'."
   (when (derived-mode-p 'dired-mode)
     (setq-local tab-width 1)
-    (advice-add 'dired-readin :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-revert :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-internal-do-deletions :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-insert-subdir :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-create-directory :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-do-redisplay :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-kill-subdir :around #'nerd-icons-dired--refresh-advice)
-    (advice-add 'dired-do-kill-lines :around #'nerd-icons-dired--refresh-advice)
-    (with-eval-after-load 'dired-narrow
-      (advice-add 'dired-narrow--internal :around #'nerd-icons-dired--refresh-advice))
-    (with-eval-after-load 'dired-subtree
-      (advice-add 'dired-subtree-toggle :around #'nerd-icons-dired--refresh-advice))
-    (with-eval-after-load 'wdired
-      (advice-add 'wdired-abort-changes :around #'nerd-icons-dired--refresh-advice))
+    (add-hook 'dired-after-readin-hook #'nerd-icons-dired--after-readin-hook nil t)
     (nerd-icons-dired--refresh)))
 
 (defun nerd-icons-dired--teardown ()
   "Functions used as advice when redisplaying buffer."
-  (advice-remove 'dired-readin #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-revert #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-internal-do-deletions #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-narrow--internal #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-subtree-toggle #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-insert-subdir #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-do-kill-lines #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-create-directory #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-do-redisplay #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'dired-kill-subdir #'nerd-icons-dired--refresh-advice)
-  (advice-remove 'wdired-abort-changes #'nerd-icons-dired--refresh-advice)
-  (nerd-icons-dired--remove-all-overlays))
+  (kill-local-variable 'tab-width)
+  (remove-hook 'dired-after-readin-hook #'nerd-icons-dired--after-readin-hook t)
+  (nerd-icons-dired--remove-all-overlays-from-whole-buffer))
 
 ;;;###autoload
 (define-minor-mode nerd-icons-dired-mode

これで私の使い方では問題が無いのですが、使い方によってはおかしいこともあるかもしれません。

次にリモート(Tramp経由)のディレクトリで使ったときに遅かったので次のように修正(この辺りはall-the-icons-diredを使っているときに気がついて修正したのを移植したもので、nerd-iconsになってからはあまりテストしていません)。ファイル毎にリモートアクセスが必要な処理をしているのでそれを排除しました。

 (defun nerd-icons-dired--refresh ()
   "Display the icons of files within the narrowed region of the Dired buffer."
   (nerd-icons-dired--remove-all-overlays)
   (save-excursion
     (goto-char (point-min))
     (while (not (eobp))
       (when (dired-move-to-filename nil)
-        (let ((file (dired-get-filename 'relative 'noerror)))
+        (let ((file (dired-get-filename nil 'noerror))) ;; Full path
           (when file
-            (let ((icon (if (file-directory-p file)
-                            (nerd-icons-icon-for-dir file
-                                                     :face 'nerd-icons-dired-dir-face
-                                                     :v-adjust nerd-icons-dired-v-adjust)
+            (let ((icon (if ;; Avoid using `file-directory-p' as it will
+                            ;; cause remote access.
+                            (save-excursion (forward-line 0)
+                                            (looking-at-p dired-re-dir))
+                            (if (file-remote-p file)
+                                ;; Avoid file-*-p functions
+                                (nerd-icons-sucicon "nf-custom-folder_oct"
+                                                    :face 'nerd-icons-dired-dir-face
+                                                    :v-adjust nerd-icons-dired-v-adjust)
+                              (nerd-icons-icon-for-dir file
+                                                       :face 'nerd-icons-dired-dir-face
+                                                       :v-adjust nerd-icons-dired-v-adjust))
                           (nerd-icons-icon-for-file file :v-adjust nerd-icons-dired-v-adjust)))
                   (inhibit-read-only t))
-              (if (member file '("." ".."))
+              (if (string-match-p "\\(?:\\`\\|[/\\\\]\\)\\.\\.?\\'" file) ;; . or ..
                   (nerd-icons-dired--add-overlay (dired-move-to-filename) "  \t")
                 (nerd-icons-dired--add-overlay (dired-move-to-filename) (concat icon "\t")))))))
       (forward-line 1))))

最後にファイル数が多いときにアイコンを表示しないようにしてみました。

+(defcustom nerd-icons-dired-max-lines 1000
+  "The maximum number of lines in the buffer in which icons will be displayed.
+Performance can be improved by hiding icons when there are a large
+number of files."
+  :group 'nerd-icons
+  :type '(choice (integer)
+                 (const :tag "No limit" nil)))
+
 (defvar nerd-icons-dired-mode)
 
 (defun nerd-icons-dired--add-overlay (pos string)
@@ -88,6 +96,13 @@
         (nerd-icons-dired--overlays-in (point-min) (point-max))))
 
 (defun nerd-icons-dired--refresh ()
+  (if (and nerd-icons-dired-max-lines
+           (> (line-number-at-pos (point-max) t) nerd-icons-dired-max-lines))
+      ;; If there are many files, it will be very slow, so disable icons.
+      (nerd-icons-dired--remove-all-overlays-from-whole-buffer)
+    (nerd-icons-dired--refresh--internal)))
+
+(defun nerd-icons-dired--refresh--internal ()
   "Display the icons of files within the narrowed region of the Dired buffer."
   (nerd-icons-dired--remove-all-overlays)
   (save-excursion

これらを合わせてnerd-icons-dired/nerd-icons-dired.el at misohena · misohena/nerd-icons-diredに置いてあります。

ちなみにall-the-iconsからnerd-iconsへ移行した理由ですが、私はall-the-iconsが使用するフォントの幅をFont Forgeで無理矢理揃えたものを使用していて、Emacs以外の用途で使うときに困るよなぁ……と前々から心配していたからです。新PCに移行したときに、この書き替えたフォントをインストールするのに躊躇しました。フォント名もちゃんと変えてall-the-icons.elも変えれば良いのかもしれませんが、面倒なので今回はnerd-iconsを使ってみることにしたわけです。

2025-02-17 ,

Emacs用のカラーピッカーに対する最近の変更

最近はまた作図ツールのカラーピッカー部分を色々直していました。

misohena/el-easydraw: Embedded drawing tool for Emacs

先日久しぶりに使ったら変なところが見つかったので、この際溜まっていた改良点をいくつか潰そうと思ったわけです。

特に単独利用、つまり(作図エディタから使うのではなく)任意のバッファ内で色を表すテキストを置き換えたり挿入したりする使い方を中心に直しました。

カラーピッカーを使ってcss-mode内の色テキストを置き換えているところ
図1: カラーピッカーを使ってcss-mode内の色テキストを置き換えているところ

以下修正点:

一時キーマップの不具合を修正
まずはきっかけとなった不具合の修正。カスタマイズバッファ(Custom-mode)内で使ったらエラーが出たので何かなと思ったら、どうも色を決定した後でも一時キーマップが終了していないようでした。なので、C-c C-cやOKボタンで色を決定した後、その次のキー入力が一時キーマップに食われてしまいます。その時C-c C-cでカスタマイズの反映をしようとすると、既に閉じてしまったカラーピッカーのOKボタンが押されてカスタマイズバッファ内の色テキストを置き換えようとし、その場所がすでに編集可能な範囲を外れているとエラーが出るということでした。一時キーマップの使い方を色々見直しました。
子フレームが外に出て一部が見えなくなる問題を修正
表示する位置の計算を調整しました。
別ウィンドウをクリックしてカラーピッカーを出したときの問題を修正
これEmacsでマウスを使うコマンドを書くとよくやっちゃうんですよね。マウスだとカレントバッファや選択中ウィンドウ以外を操作対象に出来るので。
導入を簡単にするマイナーモードを作成
edraw-color-picker-modeedraw-color-picker-global-mode を追加しました。自分でフックか何かを書いてコマンドを好きなキーに割り当てる人には必要ないのですが、初期設定を簡単にするためのマイナーモードを作成しました。Emacs全体で使えるようにするにはグローバルマイナーモードである edraw-color-picker-global-mode を有効にするだけです。キー割り当てや有効にするメジャーモードは M-x customize-group edraw-color-picker-mode から設定できます。メジャーモード毎にキー割り当てを変えられるようにするため少々苦労しました(バッファーローカルマイナーモードキーマップ)。context-menu-modeが有効な場合はコンテキストメニューにも項目が追加されます。
CSS Color Module Level 4までの各種構文に対応
今のところcolor関数以外のhsl、hwb、lab、lch、oklab、oklchに対応しています。
元の書き方に合わせた置換
置き換える前の色テキストを解析して、使用している構文、単位、空白の入れ方等を置換後のテキストにも反映させました。
出力書式の設定の増強
出力する形式をカスタマイズするためのプロパティも沢山追加しました。が、この辺りはまだまだ整理が必要です。UIもありません。
メニューを追加

メニューから出力形式をある程度選べるようになりました。また、色成分の直接入力や矢印キーによる値の変更など、存在に気がつきにくいコマンドを載せてあります。

メニューでCSSの出力書式を選んでいるところ(日本語環境の場合)
図2: メニューでCSSの出力書式を選んでいるところ(日本語環境の場合)
バッファへの即時反映
デフォルトでは、カラーピッカー使用中に挿入・置換結果が逐一バッファへ反映されるようになりました。css-mode等でバッファ内のテキストに色を付けている場合にはそれも自動的に更新されるというわけです。……しかしテストしてみるとcss-modeやweb-modeだと対応している(色を付けてくれる)構文って結構限られているんですね。
固定パレットの追加
少々見た目が煩雑になってしまいますが、下部に順番固定のパレットを配置しました。エントリーを右クリックするとメニューが出るので、現在選択中の色をそこへ設定できます。パレット全体をファイルへ保存したり読み込んだりも出来ます。作図エディタから使うとパレットの状態は自動的に保存されるのですが、他から使う場合は明示的に保存する必要があります。
M-p/M-nで履歴選択
M-pやM-nで最近使った色を選べるようになりました。
最後に選択した色相を維持
これまでは初期色が無彩色の場合、赤(色相0度)がから始まっていましたが、最後に選択したときの色相から再開するようにしました。ちゃんとやるならもっと色々工夫をしなければならないのですが、とりあえず最低限。
現在の色の表示
エコーエリアに色の情報を表示するようにしました。
デフォルトの大きさを調整
小さいと使いづらいのでデフォルトの大きさ(edraw-color-picker-near-point-scale)を1.0にしました。この辺りはお好みで。

まだまだ直した方が良いところは尽きませんが、少しはマシになったかもしれません。

作図エディタの方も色々改良していますが、それはまたいずれ。

というわけでEmacsのcss-modeやcustomize-face等でカラーピッカーを使う設定の続きでした。その記事を書いてからもう大分経ちましたね。ボヤボヤしていると1年2年あっという間に過ぎてしまうのでホント嫌になってしまいます。