Author Archives: AKIYAMA

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年あっという間に過ぎてしまうのでホント嫌になってしまいます。

2024-12-12

which-key.elとメニューの定義

Emacs 30からwhich-key-modeが追加されます。which-key-mode自体は何年も前からある(github.com/justbur/emacs-which-key)ようですが、それがEmacsに組み込まれたということのようです。私はこれまで使ったことはありませんでしたが、手元のEmacs 30.0.92に入っていたので使ってみました。

そもそもwhich-key-modeは何かというと、次に押すべきキーを教えてくれる、というと抽象的で分かりづらいでしょうか。一言で言うと、自動的にキーメニューを表示してくれる(グローバルマイナー)モードです。例えばC-xと入力して少し待つ(次に打つべきキーを迷っている)とC-xで始まるキー割り当ての一覧が表示されます。もちろんC-xだけでなく複数のキーストロークが必要な場面では自動的にメニューが出ます。

which-key-modeを有効にしてC-xを押したところ
which-key-modeを有効にしてC-xを押したところ

キーメニューというとHydraTransientを思い出しますが、これらは基本的には明示的にメニューを定義した上で使うものだと思います。自分で使いやすいようにメニューを設計できますが、逆に言えばそのような手間をかけなければなりません。一方which-key-modeは現在のキーマップから自動的にメニューを作成してくれます。

which-key-modeは事前の定義が不要な分手軽で広範囲で使用できますが、表示されるのは無味乾燥なコマンド名の羅列……と言いたいところですが、そこにはちゃんと対策が用意されています。

次の関数を使うと特定のキー割り当てに対して表示される説明を好きなように変更(置き換え)できます。

後者二つはwhich-key-replacement-alist等のwhich-key専用の変数に置き換えルールを記録しますが、興味深いのは一つ目の特定のKEYMAPに対する説明の置き換えです。which-key-add-keymap-based-replacementsの実装を見れば分かりますが、その情報は指定されたKEYMAPそれ自体に保存されます。説明(replacement)に文字列が指定された場合を追ってみると、最終的には (define-key keymap (kbd key) (cons replacement 元の割り当て)) が実行されていることが分かります。これはいったいどういうことでしょうか?

元々Emacsのキーマップというのはメニューを記述する役割を兼ねています。そのためキーマップにはメニュー項目用の文字列を埋め込めるようになっています。キーマップの書式(Format of Keymaps (GNU Emacs Lisp Reference Manual))にある item-name というがそれです。つまりwhich-key-add-keymap-based-replacementsがやっていることは実質的にはメニューを構築しているようなものです。そしてwhich-key-modeはそのメニューの項目用の文字列をコマンド名の代わりに表示してくれるというわけです。

ということはつまり、わざわざwhich-key-add-keymap-based-replacementsを使わずともキーマップに項目文字列を最初から設定しておけばwhich-key-modeのキーメニューをよりわかりやすく出来るということです。

キーマップを作成するには例えば次のようなコードがよく使われてきました(<Emacs29)。

(let ((km (make-sparse-keymap)))
  (define-key km (kbd "C-c h c") 'hello-cat)
  (define-key km (kbd "C-c h d") 'hello-dog)
  (define-key km (kbd "C-c h f") 'hello-flog)
  km)

これを次のようにするだけでwhich-key-modeのメニューをよりわかりやすくすることが出来ます。

(let ((km (make-sparse-keymap)))
  (define-key km (kbd "C-c h") (cons "Hello Animals" (make-sparse-keymap)))
  (define-key km (kbd "C-c h c") '("Cat" . hello-cat))
  (define-key km (kbd "C-c h d") '("Dog" . hello-dog))
  (define-key km (kbd "C-c h f") '("Flog" . hello-flog))
  km)

(注: 最近(>=Emacs29)ではdefine-keyはレガシー扱いとなりkeymap-setやdefine-keymapが追加されていますが、いずれにせよ項目文字列を指定する方法は用意されています)

マイナーモードならこんな感じでしょうか。

(define-minor-mode hello-animals-mode
  "Hello Animals"
  :keymap
  `((,(kbd "C-c h") . ("Hello Animals" . ,(make-sparse-keymap)))
    (,(kbd "C-c h c") . ("Cat" . hello-cat))
    (,(kbd "C-c h d") . ("Dog" . hello-dog))
    (,(kbd "C-c h f") . ("Flog" . hello-flog))))

実際に有効にして C-c h を押してみたところ、次のように表示されました。

マイナーモードのキーマップに項目名を入れて使ってみた所
マイナーモードのキーマップに項目名を入れて使ってみた所

複雑なキーマップを分かりやすくしたい、でもHydraやTransientを使うのは面倒という場合はこのような工夫をしてみてはどうでしょうか。

余談:

ちなみにキーマップをキーボードで操作できるメニューにしたいのであれば tmm-prompt を使うという方法もあります。

(defun hello-cat () (interactive) (message "Nya"))
(defun hello-dog () (interactive) (message "Wan"))
(defun hello-flog () (interactive) (message "Geko"))

(let ((km (make-sparse-keymap)))
  (define-key km (kbd "C-c") (cons "C-c" (make-sparse-keymap)))
  (define-key km (kbd "C-c h") (cons "Hello Animals" (make-sparse-keymap)))
  (define-key km (kbd "C-c h c") '("Cat" . hello-cat))
  (define-key km (kbd "C-c h d") '("Dog" . hello-dog))
  (define-key km (kbd "C-c h f") '("Flog" . hello-flog))
  (tmm-prompt km)) ;; (x-popup-menu t km))だとマウスで操作するメニューになる

HydraTransientは良くも悪くも独特の世界観を作ってしまっているところが欠点ではありますよね。で、作り込んでみてもキー操作を覚えてしまえば見なくなるわけですし。それに一つのメニューの中に沢山のコマンドを表示してしまうと探すのが大変でむしろM-xでミニバッファから補完した方が探しやすいということにもなりかねません。Magitでたまにしか使わないコマンドのキーがメニューから見つけられないことが私は良くあります。いや、MagitはそもそもGitのコマンド体系自体が(以下略

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