Author Archives: misohena

2021-09-21 ,

Emacsの中で動く作図ツールを作る

先日も書きましたが最近はEmacsの中で動く作図ツールを作っています。

ソース: misohena/el-easydraw: Embedded drawing tool for Emacs (github.com)

org-modeの中で思いついた時に図を描きエクスポートするまでの様子
図1: org-modeの中で思いついた時に図を描きエクスポートするまでの様子

以前囲碁の棋譜編集ツールを作ってその時にも書きましたが、Emacsの中でこのくらいのことは出来ても罰は当たらないと思うんですよね(このくらい出来て当然だろ!の意)。

org-modeは素晴らしいツールでいろんな事が出来ますが、文書の中に別の要素を埋め込んで統一的に編集する機能はまだまだ改善の余地が沢山あると思います。(ソースコードブロックのようなテキストベースでプログラマーが誰でも喜ぶような物は充実していますけど) 特にGUI要素が全然足りません。例えば図を描くならditaaやPlantUMLなんかもありますが、やっぱりGUIで描きたくないですか? 20年以上前のWordに出来たようなことが現代の編集環境で出来ないというのはとても残念な事だと思います。(Xwidgetsが使えれば色々出来るのかもしれませんがWindowsなので未だに使ったことがありません。Cygwinで環境を整えれば使えるのかもしれませんが……)

ということでEmacsの中でシームレスに作図が出来るようにと作りました。まだまだ改善するところが沢山あって思っていた以上に難航していますが、日々テストと称していろんな図を作成して遊んでいます。

2021-09-21-edraw1.png
2021-09-21-itsumodori.png
2021-09-21-karasu1.png
2021-09-21-karasu2.png
2021-09-21-increase-issues.png
2021-09-21-diary.png
2021-09-21-edraw2.png
2021-09-21-edraw-svg-path-d-structure1.png
2021-09-21-edraw-svg-path-d-structure2.png
2021-09-21-edraw-self-dev.png
2021-09-21-edraw-propedit.png
2021-09-21-edraw-takao.png
2021-09-21-edraw-copy-paste-test.png

実装

画像表示(ビュー)とマウス操作(コントロール)はこれまで培ってきたEmacsでのSVGやオーバーレイ、マウスイベント処理の延長線上にあります。

その上で一番最初に手を付けたのは当たり判定でした。SVGで図形を表示するのは簡単ですがマウスでクリックした点と図形との当たり判定は自分で行わなければなりません。残念ながらEmacsはそこまで面倒を見てくれません。ベジェ曲線を含んだパスの判定をするにはそれなりに手間がかかりますが、これが出来なければ話になりません。幸いこの手の当たり判定やベジェ曲線については多少扱ったことがあったのですぐに実装出来ました(完全かはともかく)。それでもこういう当たり判定処理はちゃんと動くと嬉しいものですね。

編集対象となる図形データ(モデル)は、基本的にはSVGのDOMツリーです。edraw-bodyというidを持つg要素の下が編集エリアで、それ以外の所にUIに必要なもの(グリッドやアンカー点等)を配置します(もちろん出力時にはUI要素は削除します)。できるだけDOMツリーを尊重して編集対象となるデータは常にDOMツリーに持たせてそれを書き替える形にしようと思いました。しかし今となってはちょっと怪しくなっています。shapeクラスを作ってそれ経由でDOM要素を操作する形になっていますが、shapeオブジェクトが編集中のデータを一部持ってしまっています。毎回属性をparseして編集して文字列に戻すのも大変ですし(特にpathデータ)、ドラッグ移動中や選択中のポイントが必ずしも属性と一対一で対応するわけではないので困るということもありました。とはいえそういったものは一部の例外で、基本的にはshapeオブジェクトはDOMノードをラップする存在です。あ、ちなみに今回初めてeieioを使っています(これもまた色々暗中模索でした)。

そういった所を実装して割とすぐに簡単な図が描けるようになりましたが、その後の細々とした改良に沢山の時間を費やしています。

矢印テスト中の様子
図2: 矢印テスト中の様子

例えば矢印は手間がかかりました。SVGにはマーカーという機能があってあらかじめ定義しておいた図形(マーカー)をパスの頂点にくっつけることが出来るのですが、あらかじめ定義しておく必要がある時点で少し面倒ですし、色も含めて定義しておく必要があるので線の色が変わると定義も更新しなければなりません。重複する定義をまとめる仕組みや変更を検知して更新する仕組みが必要でした。なので中身は見た目よりもずっと面倒くさいことになっています。でもこの手のソフトを作るなら矢印は絶対に欲しいと思っていたので頑張りました。是非矢印を有効(パスを右クリックしてSet→End Marker→Arrow)にした上でstroke色を変えてみてください。矢印の色も一緒に変わるのは決して当たり前なことでは無いんです。それが証拠に線を半透明にするとボロが出ます(笑)。(SVG2ではfill=”context-stroke”という指定が出来るようになって多少やりやすくなりますがlibrsvgでは最近対応したばかりでまだ手元のEmacsでは使えません。librsvgでの対応が待たれる事項は他にも沢山あります)

パスの操作はいちいち場合分けが大変で苦労しました。SVGのパスデータ(path要素のd=属性)にはやっかいなところがいくつかあって(本文末尾参照)、アンカーポイントの削除、追加、パスの分割、連結、ハンドルのあれやこれやを実装する際に悩みの種となりました。SVG仕様の細かいところまで対応する必要は無かったのかもしれませんが、将来的にどんなデータを扱うのか分かりませんので。例えば今のところ複数のサブパス(一つのd=の中に複数のパスが存在するケース)は扱えないのですが、将来的には対応したいところです。でないとドーナツ型が作れませんし(ここが抜けない)。いや、まぁ、やってやれないことも無いんですけどね……(←U字になってる)。

その後も先日紹介したカラーピッカー、プロパティエディタ、コピー&ペースト、複数選択、UNDO/REDO等々少しずつ実装していきました。前回の囲碁エディタのようにもう少し短期間で切りの良いところまでいけると思ったのですが、一つ一つの改良とテストに思っていたよりも時間がかかってしまいました。

プロパティエディタの改良でマウス移動イベントが文字単位でしか発生しない事を知る
図3: プロパティエディタの改良でマウス移動イベントが文字単位でしか発生しない事を知る

org-modeとの連携とリンク形式、インライン画像表示、編集、エクスポート

一通り作図が出来るエディタが出来たらorg-modeとの連携部分も作らねばなりません。前回の囲碁エディタは #+begin_igo#+end_igo というスペシャルブロックを使いましたが、今回は [​[edraw:]] という独自のリンクタイプを追加することにしました。ブロックだと行の中に図を挿入できないからですこんな風に(SVG)。もちろんorg-modeで画像を挿入する通常の方法がリンクなのでそれにならったというのもあります。

現在サポートしているリンクの形式は次の通りです。

[​[edraw:file=./example.edraw.svg]​]

[​[edraw:data=<base64data>​]

[​[*Example][edraw:file=./example.edraw.svg]​]

[​[*Example][edraw:data=<base64data>]​]

ファイルへ(.edraw.svg)へのリンクの他、base64によるデータ埋め込みにも対応しています。外部ファイルが必須となると途端にお手軽さが減ってしまいます。一つの文書ファイルで完結していた方が取り扱いが楽なのは間違いありません。幸いSVGはXMLなのでラスター画像よりは大きくありませんし、それをさらにgzip圧縮してからbase64エンコードしています。

外部ファイルの場合、拡張子は.edraw.svgを推奨しています。Emacs Easy Drawが扱えるのは独自のルールに従ったSVGのサブセットのみです。他のソフトが出力したSVGを編集できるわけではありませんが、Emacs Easy Drawが出力したSVGをブラウザなど他のソフトで表示することは可能です。

通常のリンク形式(file, http, https)を拡張することも考えたのですがData URI対応の経験から既存の処理と混ざると非常に面倒だと思ったのでひとまず完全に独自のリンクタイプとしました。gzipで圧縮したSVGはブラウザで直接表示できないので、どのみちエクスポート時には独自の変換処理が必要になります。将来的には.edarw.svgファイルへの通常リンク(例: [​[file:./example.edraw.svg]] )を直接編集できるようにすることも考えています。ただ、データをorgファイル内に埋め込みたいと思うならやはり独自形式の方が都合が良いと思います。

これらのリンクは edraw-org-link-image-mode というマイナーモードによってバッファ内にインライン表示できます。org-modeの org-toggle-inline-images に相当しますが、こちらはマッチする形式は即画像で表示します(私は通常の画像リンクも即更新するように修正して使っているのでそのやり方を踏襲しました)。

リンクまたはインライン表示された画像上で C-c C-o を押すとエディタが開きます。編集の後 C-c C-c (またはメニューからFinish Edit)で編集したSVGデータをバッファ内(ファイルリンクの場合は指しているファイル)に書き戻します。

もちろんEmacsの中で表示・編集できるだけでなく、HTMLエクスポートの際にはimgまたはsvgタグとして出力できます。以下は実際にEmacs Easy Drawで描いた図です。このブログはOrg2blogで書かれているので、手元のorg文書に埋め込まれている図がそのまま皆様の目の前に現れています。

MLLZLLZLLMx1,y1 Lx2,y2 Lx3,y3 Z Lx4,y4 Lx5,y5 Z Lx6,y6 Lx7,y7Subpath1Subpath2Subpath3Subpath1(Closed Path)Subpath2(Closed Path)Subpath3(Open Path)
図4: Emacs Easy Drawで描いた図をSVGとしてエクスポートした例(ブラウザで文字が選択出来る)

Pathツールの使い方

PhotoshopやIllustrator等でおなじみのPathツール(ペンツール)ですが、知らない人は最初戸惑うかもしれません。なので使い方を示すアニメーションを作ってみました。

パスツールの使用例
図5: パスツールの使用例

単にクリックするとその場所に点(アンカーポイント)を追加します。次々にクリックするとアンカーポイント間が直線で結ばれます。

マウスボタンを押し下げてからそのままドラッグすると曲線の制御点(ハンドル)を動かすモードになります。押した点から伸ばした方向に向かう滑らかな曲線が出来ます。

一つのアンカーポイントからは二つのハンドルが伸びています。それぞれ前の区間と後ろの区間に対する制御点なのですが、この二つがアンカーポイントを挟んで互いに180度反対側にあると(要するに三つの点が一つの直線上にあると)、そのアンカーポイントを通る線は尖った部分が無く完全に滑らかになります(その直線を接線とした曲線になります)。

アンカーポイントハンドル
図6: アンカーポイントとハンドル

もしアンカーポイントを選択してもハンドルが二つ現れない場合は、アンカーポイントを右クリックして「Make Smooth」を選んでください。ちょうど良さそうな点を計算してそこに二つのハンドルを置きます。

逆にハンドルを消したい場合は「Make Corner」を選んでください。滑らかさが消えて完全に尖った形(折れ線)になります。

片側のハンドルだけを単独で動かしたい場合は、一つのハンドルをクリックして選択状態にしてください。そのハンドルだけ単独で動くようになります。(PhotoshopやIllustratorの「切り替えツール」は今のところありません)

ちなみに選択中のハンドルやアンカー(もちろんシェイプも)は矢印キーで移動できます。S-矢印キーで10ピクセル単位、M-矢印キーで数値入力で移動します。細かい調整の際には重宝します。

現在のパスを終了して新しいパスを開始したいときは再度Pathツールを選択してください。ツールバーのボタンを押すかaキーを押すとPathツールが初期状態から始まり、次のクリックでは新しいパスシェイプと最初のアンカーポイントが作成されます。

このとき(Pathツールを選択した直後)、現在選択中のパスの端点(一番端っこのアンカーポイント)をクリックすると、そのアンカーポイントからパスを伸ばす(再開する)ことが出来ます。

パスを伸ばしているときに既存のパスの端点をクリックすると、現在のパスをその端点を持つパスと連結します。

クリックで既存のアンカーと繋げたくない場合は、 C-u + クリック で確実に新しいアンカーポイントを追加できます。

操作方法の問題点について

他にも操作には色々注意点がありますが、とりあえず運用でカバーしつつ実用的な図がかけるところまでは出来たのではないかと思います。

完全に私の好みに合わせて作っているので他の方には使いづらい所もあるかとは思いますが、その辺りはご了承ください。

うまく自分の好みに合わせられず使いづらいところもありますが、その辺りは自分の実力が足りないのが悪いのだと諦められるので納得しやすいところではあります(笑)。

細かいところの改善は限りが無く時間は有限なので手を付けていないところも沢山あります。

無限の改善点の狭間で

まだまだ改善点は尽きません。思いついた物はtodo.orgに書き留めています。

一時期は一つ直していくそばから沢山の修正点が見つかる状況でしたが、それも少し落ち着いてきました。

修正点修正済み修正した一つ取り出す修正点を見つける修正作業
図7: 一つ修正すると修正点が増えている図

この手のツールは作っていけば行くほど次第に労多くして功少なしな事項ばかりが残っていくのが常です。

本当に切りがないので、このあたりでひとまず開発のペースを緩めようと思います。元々UNDOを実装するところまでは一気に作ろうと思っていてそれが出来たので。それに秋山の紅葉が私を呼んでいますので。

SVGは本当に表現力があって色々出来るので皆さんもEmacsに足りない要素をどんどん追加していきましょう。

おまけ:SVGパスデータの構造についての図

以下はSVG path要素のd属性について説明するためにEmacs Easy Drawで描いた図です。

pathは <path d="M10,10L30,10C40,20 40,80 30,100" stroke="red" /> のような記述で自由に線を引くための要素ですが、そのd属性(パスデータ)の編集にはいくつか注意点があるのでそれについて描いたものです。

基本的な構造(M, L, Cコマンド):

MLC(previous anchor’s)forward handlebackward handleanchoranchoranchorline segmentcurve segmentM commandL commandC command

Zコマンドでパスを閉じると始点と終点が切れ目のない繋がった形状になる(単に始点へ線を引くのとは幅の広い線において色々違いが出てくる)。最後の点と始点の間は直線で結ばれる。

MLLL commandL commandZ command(Line to previous M)* Z commands automatically create a closing segment but cannot represent a curve

曲線で閉じるにはMと同一点までCで閉じ線を引かないといけない(その上でZが必要)。その場合MとCの点は一つのアンカーポイントとして同一視して処理しなければならない(Mを動かしたらCも一緒に動かしたり、Mの前の点を求めるときはCをスキップする等)。

Same coordinatesMCCZClosing Segmentbackward handle of C and M!forward handle of M and C!Length=0

一つのパスデータには複数(0以上)のサブパスを含めることができる。

MLLMLLZClosed SubpathOpen SubpathMLOpen SubpathM L L Z M L L M L

直後にMコマンドが無いZコマンドはMの位置を開始点にした新しいサブパスを作る。

MLLZLM L L Z LSubpath1(Closed Path)Subpath2(Open Path)Subpath12

従って、一つのMコマンドが表す点は複数のサブパスで共有される場合がある。このMの点を削除したり分割したりする場合は注意が必要になる。

MLLZLLZLLMx1,y1 Lx2,y2 Lx3,y3 Z Lx4,y4 Lx5,y5 Z Lx6,y6 Lx7,y7Subpath1Subpath2Subpath3Subpath1(Closed Path)Subpath2(Closed Path)Subpath3(Open Path)

最後のアンカーポイントの前方ハンドルを記録するため、 -forward-handle-point という独自の拡張コマンドを追加している。当然これはSVG出力時には削除される。

C?M or L or CC command-forward-handle-point commandLast anchor point

(あー、矢印の色が……。複数の図を一つのHTMLに出力した(埋め込んだ)ときにマーカーIDが重複する問題に気がついてしまった……。修正するならエクスポート時にマーカーIDに図のIDを付け加えるとかかなぁ)

2021-09-11

The Emacs Widget Libraryのプッシュボタンを連打できない問題を解決する

プッシュボタンが連打できなくて修正しようとしたらドハマリして時間を大量に無駄にしたのでメモ。

Widgetライブラリのpush-buttonを連打しようとするとダブルクリックやトリプルクリックと判定されてボタンが押せなくなります。そしてなぜか範囲選択が始まったりします。「+」「-」とか「Previous」「Next」等をプッシュボタンにすると連打できないのはとても気になるので何とかしました。

次のコードは何とかしたカウンターアプリケーションの例です。 M-x my-count で単純なカウンターが動きます。

(require 'widget)

;; ダブルクリック、トリプルクリックに対応した `widget-button-click' の代わりです。
(defun my-widget-button-click (event)
  (interactive "e")
  ;; Add double and triple click support to widget-button-release-event-p
  (cl-letf (((symbol-function 'widget-button-release-event-p)
             'my-widget-button-release-event-p))
    (widget-button-click event)))

;; ダブルクリック、トリプルクリックに対応した `my-widget-button-release-event-p' の代わりです。
(defun my-widget-button-release-event-p (event)
  (and (eventp event)
       (memq (event-basic-type event) '(mouse-1 mouse-2 mouse-3))
       (or (and (or (memq 'double (event-modifiers event)) ;;double click
                    (memq 'triple (event-modifiers event))) ;;triple click
                (null (memq 'down (event-modifiers event))))
           (memq 'click (event-modifiers event))
           (memq 'drag (event-modifiers event)))))

(defvar my-push-button-map
  (let ((km (make-sparse-keymap)))
    (define-key km [drag-mouse-1] 'ignore)
    (define-key km [double-down-mouse-1] 'my-widget-button-click)
    (define-key km [triple-down-mouse-1] 'my-widget-button-click)
    km))


(defvar-local my-number-widget nil)
(defun my-increase (delta)
  (widget-value-set
   my-number-widget
   (+ (widget-value my-number-widget) delta)))
(defun my-inc (&rest _) (my-increase 1))
(defun my-dec (&rest _) (my-increase -1))

(defun my-counter ()
  (interactive)
  (pop-to-buffer "*my counter*")
  (kill-all-local-variables)
  (let ((inhibit-read-only t))
    (erase-buffer))
  (remove-overlays)
  (setq-local my-number-widget
              (widget-create 'number
                             :size 10
                             :value 0))
  (widget-insert " ")
  (widget-create 'push-button
                 :notify 'my-dec
                 :keymap my-push-button-map ;;support double and triple down
                 "Decrement")
  (widget-insert " ")
  (widget-create 'push-button
                 :notify 'my-inc
                 :keymap my-push-button-map ;;support double and triple down
                 "Increment")
  (widget-insert "\n")
  (use-local-map widget-keymap)
  (widget-setup)
  (widget-forward 1))

問題の原因は次の二点にありました。

  • ダブル、トリプルクリックのキーマップが割り当てられていないこと
  • マウスのボタンが離されたことを判定する関数がダブル、トリプルクリックに対応していないこと

プッシュボタンの処理はローカルマップのdown-mouse-1に割り当てられたwidget-button-click関数によって行われています。その中では押された位置のボタンを割り出して、ボタンが離されるまで追跡し、ボタンに関連付けられたアクションを実行します。途中でキャンセルされたらグローバルマップのマウスイベントに割り当てられたコマンドを実行します。

ローカルマップのdouble-down-mouse-1とtriple-down-mouse-1にwidget-button-clickが割り当てられていないのが問題なのだと思い割り当てたのですが変わりませんでした。

widget-button-click関数の中ではwidget-button-release-event-pという関数を呼んでいてイベントがマウスボタンを離したものかを判定しています。そこがclickとdragのみを考慮していてdoubleやtripleを考慮していません。なのでボタンを離しても離されていないものとしてずっと追跡を続けてしまっていました。

上の例ではcl-letfを使用してダブルクリック、トリプルクリックが発生したときだけwidget-button-release-event-pの中身を入れ替えることで対処しています。従ってwidgetライブラリ内の構造が変わると動かなくなるかもしれません。

以上ですが、誰得情報なんだこれ。

2021-09-06

Emacs用のSVG実装のカラーピッカー

先日からEmacsの中で動く作図ツールを作っています。

https://github.com/misohena/el-easydraw

その一環として今日はカラーピッカーを作りました。この手のソフトには必ずあるアレです。

Emacs上での先行事例はいくつかあるようでしたがSVGでの実装は見当たりませんでしたし、まぁ、自分で作りたいじゃないですか。こういうの作るの楽しいですし。

というわけで出来たのがこちら。

https://github.com/misohena/el-easydraw/blob/master/edraw-color-picker.el

一応ライブラリとして他で使い回すことを考えています。

応用としてとりあえず作ったコマンドがいくつか。

edraw-color-picker-read-colorread-colorの代わりを意識して作った色入力コマンドです。ミニバッファ内にカラーピッカーを配置してみました。文字からでもカラーピッカーからでも色を選択できます。

ミニバッファ内に表示されるカラーピッカー
図1: ミニバッファ内に表示されるカラーピッカー

edraw-color-picker-insert-colorは選択した色をそのままバッファに挿入するコマンドです。またedraw-color-picker-replace-color-at-pointは現在のポイントの位置にある色表記を読み取ってカラーピッカーで選択した色に置き換えるコマンドです。これらはバッファ内にインラインでカラーピッカーが表示されます。

インラインで表示されるカラーピッカー。
図2: インラインで表示されるカラーピッカー。

SVGは本当に簡単でこういう絵を出すのはサクッとすぐに出来ます。いや、本当は少し調査が必要だったのが二点。meshGradientの対応状況と色相バーの仕様です。こういう二次元のグラデーションで連想するのがメッシュ状の図形(いわゆるポリゴンの組み合わせ)で頂点に色を設定する仕組みです。3Dグラフィックスではおなじみですよね。SVGにも似たような仕組みがあったようなと調べてみたのですが、残念ながらEmacs(というかlibrsvg)では対応していないようですね(そもそもmeshGradient自体がSVG2から先送りされてたんですね)。幸い二枚のグラデーションを重ねれば良いとすぐに気がつけました。もう一点の色相バーですが、これ6色(赤、黄、緑、水、青、紫)を均等に並べて線形補間すればいいだけみたいですね。というわけでそれさえ分かればこういうカラーピッカーの絵自体はすぐに作れました。

画像内の各パーツは「領域」というオブジェクトで構成されています。指定に基づいて「領域」をレイアウト処理することで、いくらか柔軟に構成要素を増やしたり減らしたりできるようになっています。

領域にマウスイベントを配信(ディスパッチ)する仕組みを作り、それに反応して各部が動くようにしていきます。マウスイベントを受けて関連付けられた値が変わり、値の変更が通知されて表示が変わります。

各領域で選択した場所から最終的な色を求める方法ですが、色相バー(1次元)→彩度明度領域(2次元)→不透明度バー(1次元)の順に処理していきます。色相バーで選んだ色が彩度明度領域の右上の色になります。彩度明度領域は水平のグラデーション(左:白から右:色相選択色)の上に垂直グラデーション(上:透明黒から下:不透明黒)が重なっています。選んだ位置によって線形補間で水平、垂直の順で色を求めていきます。その色が今度は不透明度バーの上の色になって、最後に不透明度バーで選択した不透明度をその色に適用して完了です。仕組み自体はグラデーションの内容に依存していないので、色相、彩度、明度の軸を入れ替えても機能すると思いますし、1次元バーを緑、2次元領域を青と赤、などとしても大丈夫だと思います。(そのための切り替えUIを作るのが面倒くさかったのでやってませんが)

こうして出来たSVG画像はテキストプロパティまたはオーバーレイを使ってEmacs内に表示されます。というかEmacsではそれしか画像を表示する方法はありません(環境依存性の強い方法を除けば)。テキストプロパティでも良いのですが、オーバーレイの方がこういう用途では使いやすいでしょう。

オーバーレイにも画像を表示できる場所が三つあります。before-stringとdisplayとafter-stringです。displayはいいとして、before-stringとafter-stringですが、これはオーバーレイの前後に文字列を挿入するためのプロパティです。一見画像とは関係なさそうですが、この文字列にpropertize関数でdisplayテキストプロパティを適用するとオーバーレイの前後にさらに画像を表示することが可能になります。

これら色々ある表示方法を都合に合わせて選択できるように構造には柔軟性を持たせてあります。

先行事例を見てみると表示にフレームを使っているものがありました。普通のフレームを使っているものやchild frameという比較的最近のEmacsで使えるようになった仕組みを使っているものもありました。child frameを使うとフレーム内の好きな位置に重ねることが出来るのでポップアップで情報を出すパッケージで使われているようです。

とはいえフレームは扱いが難しく勉強が必要なので、とりあえずオーバーレイだけで出来る範囲で実装しています。

カラーピッカーはどこに表示すれば良いのでしょうか。だいたいポイント(やマウスカーソル)の近くですよね。ただ、ポイントの左や右にカラーピッカーのオーバーレイを挿入すると行が大きく膨らんで見づらくなってしまいますし、ウィンドウの左右からはみ出して見えなくなってしまうこともあります。となれば上の行か下の行に表示してはどうでしょう。上の行や下の行に適切な表示カラムが無いならどうすればいいでしょうか。幸いオーバーレイは行を新たに作ることが出来ます。before-string、display、after-stringの各プロパティにはそれぞれ文字列が指定できますが、いずれでも”\n”を入れることで改行が出来ます。例えば現在ポイントが20カラム目にあるとして、その真下付近に画像を表示するには、行末の改行文字にオーバーレイをかけて、before-stringを "\n " (←空白20個 – 画像の幅/2くらい)、displayに画像、after-stringに "\n" を指定してやればOKです。水平スクロールしている可能性があるのでwindow-hscrollの値を考慮するのを忘れずに。

また、ミニバッファに色を入力するようなシチュエーションでは画面の下の方にカラーピッカーを表示して欲しいです。いや、いっそのことミニバッファ内にカラーピッカーを入れたらどうでしょう。ここでもオーバーレイに改行させれば上をカラーピッカー、下を入力欄とすることが出来ます。ただし一つ注意点が。バッファの先頭にオーバーレイを入れたいわけですが、先頭の1文字にかけるとbefore-stringに画像を設定しなければならずその後に改行が入れられません。displayで改行すると1文字目が(改行に置き換わって)表示されなくなってしまいますし、after-stringで改行すると1文字目の後で改行されてしまいます。なので先頭の空の範囲((point-min)から(point-min)まで)にオーバーレイを設定します。注意点が二つ。evaporateプロパティがtだと空の範囲になった段階でオーバーレイが消えてしまいます。なのでevaporateは使わずちゃんとオーバーレイを管理する必要があります。もう一点は空の範囲のオーバーレイではdisplayプロパティが機能しないということ。before-stringとafter-stringは表示されるのですが、なぜかdisplayプロパティは空の範囲だと表示されません。なのでbefore-stringに画像を、after-stringに”\n”を指定することになります。(2021-09-07追記:よく考えたらbefore-stringの中で文字列の一部にだけdisplayプロパティをかけて、残りで改行すれば済むような気もします。未確認ですが、多分動くのではないかと)

他にもEmacsの色名とWeb(HTMLやCSS)の色名に対応するとか、色の表現形式とか、画像のスケーリングについてとか(上のスクリーンショット、ピッカーの大きさが違うのに気がつきましたか?)、色々書きたいことは山ほどありますが、この辺にしておきます。

2021-08-29 ,

org-modeのアジェンダで土曜日と日本の祝日を色づけする

土曜日は青で、日曜と祝日は 'org-agenda-date-weekend のfaceで表示するには次のように設定します。

(setq org-agenda-day-face-function
      (lambda (date)
        (let ((face (cond
                     ;; 土曜日
                     ((= (calendar-day-of-week date) 6)
                      '(:inherit org-agenda-date :foreground "#0df"))
                     ;; 日曜日か日本の祝日
                     ((or (= (calendar-day-of-week date) 0)
                          (let ((calendar-holidays japanese-holidays))
                            (calendar-check-holidays date)))
                      'org-agenda-date-weekend)
                     ;; 普通の日
                     (t 'org-agenda-date))))
          ;; 今日は色を反転
          (if (org-agenda-today-p date) (list :inherit face :inverse-video t) face))))
土曜日が青、日曜日と祝日が赤で色づけされたorg-agenda
図1: 土曜日が青、日曜日と祝日が赤で色づけされたorg-agenda

素のcalendarパッケージでもそうなのですが土曜日が青色で表示されないんですよね。日本のカレンダーでは当たり前で、もはや青くないと違和感を感じるレベルなのですが、きっと海外では違うのでしょうね? 今ちょっと検索してみましたがいろんな文化があって面白そうです。

日付の色づけは org-agenda-day-face-function 変数で変更できます。

引数の date には (month day year) という形式のリストが渡されてきます。これはcalendarパッケージが日付を扱うときの形式に合わせているようです。

土曜日用に新しいfaceを定義するのも面倒なのでanonymousフェイスで返しています。フェイス属性:inherit は先頭にないとダメなようです。

祝日の判定は calendar-check-holidays 関数で行っています。ただ、私は calendar-holidays 変数に日本の祝日以外も入れてしまっているので、一時的に calendar-holidays 変数シンボルを japanese-holidays の示すリストに束縛してからチェックしています。

「今日」のfaceを変えたいのですが土、日祝、普通の三種類があるので今日か否かで変えるとなると組み合わせで六種類必要になってしまいます。なのでここでもanonymousフェイスを生成して :inherit が指す先を日によって変えることにしました。 :inherit にはシンボルだけでなくanonymousフェイスも指定できるようです。foregroundはそのままにbackgroundだけちょっと色を付けるといったくらいならこの方法が楽でしょう。全ての組み合わせを細かく調整したいのであればいっそ次のような感じの方が清々しいかもしれません。

(setq org-agenda-day-face-function
      (lambda (date)
        (pcase (+ (if (org-agenda-today-p date) 3 0)
                  (cond
                   ((= (calendar-day-of-week date) 6)
                    0)
                   ((or (= (calendar-day-of-week date) 0)
                        (let ((calendar-holidays japanese-holidays))
                          (calendar-check-holidays date)))
                    1)
                   (t
                    2)))
          (0 (list :foreground "#08a"))
          (1 (list :foreground "#a00"))
          (2 (list :foreground "#aaa"))
          (3 (list :foreground "#0cf"))
          (4 (list :foreground "#f00"))
          (5 (list :foreground "#fff")))))
2021-08-08 , , ,

Windowsのコマンドラインからスクリーンショットを撮る(PowerShell)

ちょっと欲しくなったので調べてみました。

How can I do a screen capture in Windows PowerShell? – Stack Overflow の Jacob Colvin さんの回答が一番簡潔だったのでそれを元にいくつか用意してみました。

まずはマルチモニターを含めた全領域。

# screenshot-all.ps1
# From https://stackoverflow.com/questions/2969321/how-can-i-do-a-screen-capture-in-windows-powershell
Add-Type -AssemblyName System.Windows.Forms,System.Drawing

$screens = [Windows.Forms.Screen]::AllScreens

$top    = ($screens.Bounds.Top    | Measure-Object -Minimum).Minimum
$left   = ($screens.Bounds.Left   | Measure-Object -Minimum).Minimum
$width  = ($screens.Bounds.Right  | Measure-Object -Maximum).Maximum
$height = ($screens.Bounds.Bottom | Measure-Object -Maximum).Maximum

$bounds   = [Drawing.Rectangle]::FromLTRB($left, $top, $width, $height)
$bmp      = New-Object System.Drawing.Bitmap ([int]$bounds.width), ([int]$bounds.height)
$graphics = [Drawing.Graphics]::FromImage($bmp)

$graphics.CopyFromScreen($bounds.Location, [Drawing.Point]::Empty, $bounds.size)

$bmp.Save($Args[0])

$graphics.Dispose()
$bmp.Dispose()

次にプライマリースクリーンのみ。

# screenshot-primary.ps1
# From https://stackoverflow.com/questions/2969321/how-can-i-do-a-screen-capture-in-windows-powershell
Add-Type -AssemblyName System.Windows.Forms,System.Drawing

$screen = [Windows.Forms.Screen]::PrimaryScreen

$top    = $screen.Bounds.Top
$left   = $screen.Bounds.Left
$right  = $screen.Bounds.Right
$bottom = $screen.Bounds.Bottom

$bounds   = [Drawing.Rectangle]::FromLTRB($left, $top, $right, $bottom)
$bmp      = New-Object System.Drawing.Bitmap ([int]$bounds.width), ([int]$bounds.height)
$graphics = [Drawing.Graphics]::FromImage($bmp)

$graphics.CopyFromScreen($bounds.Location, [Drawing.Point]::Empty, $bounds.size)

$bmp.Save($Args[0])

$graphics.Dispose()
$bmp.Dispose()

最後にアクティブウィンドウのみ。

# screenshot-activewin.ps1
# Get Foreground Window's Rect
Add-Type -Type @'
using System;
using System.Runtime.InteropServices;
namespace Win32Util {
    public struct RECT {
        public int Left, Top, Right, Bottom;
    }
    public class Utils {
        [DllImport("user32.dll")]
        public static extern IntPtr GetForegroundWindow();
        // [DllImport("user32.dll")]
        // public static extern int GetWindowRect(IntPtr hWnd, out RECT lpRect);
        [DllImport("dwmapi.dll")]
        public static extern int DwmGetWindowAttribute(IntPtr hWnd, uint dwAttribute, out RECT lpRect, int cbAttribute);

        public static uint DWMWA_EXTENDED_FRAME_BOUNDS = 0x09;

        public static RECT GetForegroundWindowRect(){
            IntPtr hwnd = GetForegroundWindow();
            // RECT rect = new RECT();
            // GetWindowRect(hwnd, out rect);
            RECT rect = new RECT();
            DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, out rect, Marshal.SizeOf(typeof(RECT)));

            return rect;
        }
    }
}
'@
$rect = [Win32Util.Utils]::GetForegroundWindowRect()

# https://stackoverflow.com/questions/2969321/how-can-i-do-a-screen-capture-in-windows-powershell
Add-Type -AssemblyName System.Windows.Forms,System.Drawing

$top    = $rect.Top
$left   = $rect.Left
$right  = $rect.Right
$bottom = $rect.Bottom

$bounds   = [Drawing.Rectangle]::FromLTRB($left, $top, $right, $bottom)
$bmp      = New-Object System.Drawing.Bitmap ([int]$bounds.width), ([int]$bounds.height)
$graphics = [Drawing.Graphics]::FromImage($bmp)

$graphics.CopyFromScreen($bounds.Location, [Drawing.Point]::Empty, $bounds.size)

$bmp.Save($Args[0])

$graphics.Dispose()
$bmp.Dispose()

実行は powershell screenshot-activewin.ps1 test.png のようにします。

それにしても久しぶりのWin32がまさかこんな形だとは。Windows10でGetWindowRectだと左右下に7ピクセルずつ余分に広い矩形を返してしまうだとか。影とかの効果の分でしょうか? なので今はDwmGetWindowAttributeを使うんだそうです。

あ、ちなみにEmacsのorg-downloadからを使うには次のように設定すればいいんです。使用頻度が少なそうなのでHydraが無いと覚えられる気がしませんね。まぁ、別に全部クリップボードからでもいいじゃんって思ったりもしますが。

(when (eq system-type 'windows-nt)
  (defun my-org-download-screenshot (script-name)
    (let ((org-download-screenshot-method
           (format
            "powershell %s %%s"
            (expand-file-name
             (format "<path-to-script-dir>/%s" script-name)))))
      (org-download-screenshot)))
  (defun my-org-download-screenshot-all ()
    (interactive)
    (my-org-download-screenshot "screenshot-all.ps1"))
  (defun my-org-download-screenshot-primary ()
    (interactive)
    (my-org-download-screenshot "screenshot-primary.ps1"))
  (defun my-org-download-screenshot-active-window ()
    (interactive)
    (my-org-download-screenshot "screenshot-activewin.ps1"))
  ;; Key bind help
  (when (featurep 'hydra)
    (define-key org-mode-map (kbd "C-c d")
      (defhydra my-org-download-hydra
        (:color red :exit t :hint nil)
        "
org-download copy from:
_c_: Clipboard
_y_: Full-path or URL on kill-ring
_a_: All monitors
_p_: Primary monitor
_f_: Foreground window
or drop from a local image file.
"
        ("c" org-download-clipboard)
        ("y" org-download-yank)
        ("a" my-org-download-screenshot-all)
        ("p" my-org-download-screenshot-primary)
        ("f" my-org-download-screenshot-active-window)))))
2021-08-08

Companyのお節介な補完を抑制する

Emacsのバッファ内での入力補完を行うcompany-modeですが、使っていると困ることも色々ありました。それで少し設定を変えて使っていたのですが、まだ不満が残っていたのでここらでゆっくり検討してみました。

一番困るのが誤入力を助長してしまうという点です。テキスト入力中に勝手に候補を出すまでは良いのですが、RETを押すと勝手に出してきた候補を選択してしまい不要な文字列を付け足してしまいます。

例えば 100 と入力して RET を押したら 100 の後に改行が入ることを期待するわけですが、改行を打つ直前に勝手に 100 で始まる文字列を候補に出してきて、それが入力されてしまうわけです。例えばバッファ内に他に 10000 と書いてある部分があると、 100 ではなく 10000 が入力されてしまうわけです。入力時に気がつけば良いのですが、気がつかずに後から計算結果が合わなくて発覚したこともありました。

自動起動禁止!

慌てて行った設定が次。

;; case1
;; 自動起動を禁止する。(self-insert系を除外する)
(setq company-begin-commands
      '(c-scope-operator c-electric-colon c-electric-lt-gt c-electric-slash))

もう補完候補を自動で出すのは止めなさい、と。self-insert系コマンドを company-begin-commands から外せば少なくとも通常の文字を入力しているときに突然候補が出るのを抑制できます。最も安全です。

他の解決策としては、company-dabbrev-char-regexp から数字や日本語を除外して穏当なものだけ候補に出すという方法もよく見かけました1が、文字種によって判別するというのはなんだか違う気がしました。数字だからダメ、アルファベットだから良いという訳では無いのです。 abcdefghi とどこかに書いてあるバッファで abc RETと打っても同じ問題が起きます。

company-backends から company-dabbrev を外すという方法もよく見ましたが2、私は前からdabbrev自体は使っていましたから、companyを使う代わりにdabbrevが使えなくなるのは困ります。

手動と自動でバックエンドを切り替える(それとモードによって自動起動禁止)

そこで思いついたのが、手動で起動したときだけ company-dabbrev を使い、自動で起動したときは使わないという方法です。それなら M-/ でdabbrevを使い続けることが出来ます。数字であっても M-/ で候補を出してくれて構わないんです。

;; case2
;; dabbrevは手動で起動したときだけ有効にする。
;; アイドルタイムから始まった場合は、一時的に company-backends から
;; company-dabbrev を取り除く。
(defun my-company-idle-begin (oldfun &rest args)
  (let ((company-backends (remq 'company-dabbrev company-backends)))
    (apply oldfun args)))
(advice-add 'company-idle-begin :around 'my-company-idle-begin)

ついでにプログラミング系のモード以外では自動起動を禁止してしまいましょう。 org-modeやtext-modeで自動的に候補が出たからと言って何だというのでしょう!

;; いくつかのモードで自動的に候補を出すのを禁止する。
;; プログラミング系のモードでは比較的大丈夫な場合が多い。
;; 文法上入力できるものが限られており、補完が正しい可能性が高いから。
(defun my-company-inhibit-idle-begin ()
  (setq-local company-begin-commands nil))
(add-hook 'org-mode-hook #'my-company-inhibit-idle-begin)
(add-hook 'text-mode-hook #'my-company-inhibit-idle-begin)

思うにプログラミング言語系のモードは候補が自動的に出ても問題が少ないような気がします。プログラミング言語では場所ごとに書けるものが文法的に限定されています。例えば行末には大抵 ; を入れる言語では改行の前(RETを押したくなる直前)に候補が出ること自体がほとんどありません。出てくる候補の正答率も高いでしょう。

というわけでこのくらいでしばらく使っていたのですが、org-modeで自動的に候補が出てこないというのは少し寂しい気もします。 M-/ で候補を出せると言っても候補があること自体に気がつけませんからね。

自動起動や一部のモードは無選択状態で開始

別に候補を出してくれるのは構わないんです。問題なのは勝手にRETやTABを奪ってしまうことなのです。

候補を出しつつ、例えば選択は↓キー(C-n)を押してからでないと出来ないようにすれば良いのです。 何か良い方法は無いか……とcompany.elを眺めていたら次のような文字が目に飛び込んできました。

When `company-selection-default' is nil, add a special pseudo candidates
meant for no selection."

なんと company-selection-default を nil にするだけで無選択状態から開始できるのです。

;; case3
;; 基本的に候補は無選択状態から始める。
;; 誤って確定してしまうのを防ぐ。
(setq-default company-selection-default nil)
(setq-default company-selection nil)

この変数は defvar でなぜか defcustom ではありません。 しかし試してみたところきちんと動いているようです。ザッとコードを確認しても問題は無さそうに見えます。 (2021-08-09追記:細かい問題が見つかりました。修正方法は末尾に追記してあります)

これで常に無選択状態から始まりますが、手動で開始したときは選択状態から始まっていた方が良いでしょう。入力がまだ不完全で補って欲しくて手動で起動(M-/)しているわけですから、候補を選択した状態から始まっていても問題は無いでしょう。嫌ならC-gを押せば良いだけです。また、やはりプログラミング系のモードではこれまで選択状態から始まっていて違和感が少なかったのでとりあえず同じようにしましょう。

;; 手動起動したときには選択状態から始める。何か選びたいはずなので。
;; 自動起動したときでもモードによっては選択状態から始める。
;; 文法的に正しい候補が出せる可能性が高いとき。
(defun my-company-should-select-first-p ()
  (or
   company--manual-action ;;手動で起動したとき。
   (and (boundp 'lsp-mode) lsp-mode))) ;;LSPが使えるモードは補完の精度が高いはずなので。 (memq major-mode '(c-mode c++-mode))とかでも可。
(defun my-company-auto-begin (oldfun)
  (let ((company-selection-default
         (if (my-company-should-select-first-p) 0 nil)))
    (funcall oldfun)))
(advice-add 'company-auto-begin :around 'my-company-auto-begin)

無選択状態の時であればRETやTABがそのまま入力できるかと思いきやそうなっていません。無選択状態なので確定(誤入力)はしませんが、かといってそのままRETやTABがバッファに入ったりはしません。単純に無視されます。なぜならcompany-active-mapにRETやTABが登録されているので、候補が出ている間はそれが実行されてしまうからです。(ちなみに他の通常の文字(company-active-mapに登録されていない)は無選択状態の時(正確にはcompany-require-matchではないとき)にはそのままバッファに入力できます。手動で起動したり一度でも選択操作すると候補とマッチする文字しか入力できなくなります)

なので無選択状態の時はRETやTABをバッファ本来のキーバインドで実行するようにしました(補完を中断してキーを読み取り前に戻す)。

;; 無選択状態の時にTABやRETが入力されたら
;; そのバッファのモード本来のTABやRETを実行する。
(defun my-company-complete-respecting-user-input (&rest args)
  "ユーザー入力を尊重した補完を行う。"
  (interactive)
  (if (null company-selection)
      ;; モード本来の割り当てを実行する。
      (progn
        (company-abort)
        (company--unread-this-command-keys))
    ;; companyの(リマップ元の)コマンドを実行する。
    (apply this-original-command args)))
(define-key company-active-map [remap company-complete-selection]
  ;;RETに割り当てられているコマンドをリマップ
  'my-company-complete-respecting-user-input)
(define-key company-active-map [remap company-complete-common]
  ;;TABに割り当てられているコマンドをリマップ
  'my-company-complete-respecting-user-input)

というわけで、今はこのくらいで使っています。

100 RET と押しても10000が入ったりはしませんし、 org-modeのテーブルセル内で 100 TAB 等と押しても大丈夫です。100とRETの間で一瞬候補は出ますが無視してRETやTABがそのまま入ります。いつも「10000ポイント」とか「10000円」とか候補に出てきてちょっと気が散りますがw どこから候補を持ってきてるんだ。

dabbrevで日本語が入りすぎるのは気になりますが、そのあたりはきっと company-dabbrev-char-regexp を調整すれば良いのでしょう。

またしばらくこれで使ってみようと思います。

P.S. helmやivyでも感じたのですが、RETが候補選択に奪われがちなのは近年の補完インタフェースを見ていて気になるところです。

2021-08-09追記: 候補が一つになったときに候補が表示されなくなる問題の修正

company-selection-default を nil にした時の問題ですが、候補が一つに絞り込まれたときに候補が表示されなくなる現象を見つけました。表示されないだけで補完自体は続いているらしく C-n を押すと候補がポイントの位置に表示されます。

company-selection-default を nil にしたときには「無選択」という仮想的な候補が追加されるわけですが、それを考慮していない場所があるようです。

companyでは候補が一つだけの時のfrontendと候補が二つ以上の時のfrontendが分かれています。一つだけの時はポイントの位置に表示して、二つ以上の時はツールチップ的なオーバーレイで表示します。この「候補が一つだけ」の判定が「無選択」という候補を考慮していませんでした。

次のように修正すれば直ります。

  (defun company--show-inline-p ()
    (and (not (cdr company-candidates))
+        (or company-selection-default (null company-candidates)) ;;追加
         company-common
         (not (eq t (compare-strings company-prefix nil nil
                                     (car company-candidates) nil nil
                                     t)))
         (or (eq (company-call-backend 'ignore-case) 'keep-prefix)
             (string-prefix-p company-prefix company-common))))

adviceで書くなら次のようにします。

(defun my-company--show-inline-p (old-fun)
  (and
   ;; Include "no selection" as candidates
   (or company-selection-default (null company-candidates))
   (funcall old-fun)))
(advice-add 'company--show-inline-p :around 'my-company--show-inline-p)

2021-08-09追記: 選択操作をした後に文字で絞り込むと無選択に戻される問題を修正

同じく company-selection-default を nil にしたときの問題です。

候補の選択操作(C-n等)をした後に現在選択しているのとは違う後続の文字を入力して他の候補を選択しようとすると無選択状態に戻されてしまいます。

例えばemacs-lispにおいて、 def で default と defun が候補に出たとして、 C-n で default を選択してから次に u を押すと無選択状態になってしまいます。ここは defun が選択されていて欲しいところです。このままTABやRETを押すと無選択状態ですから当然defunは挿入されません。

現在選択している候補が消えたのだからデフォルトの選択である「無選択」に戻しただけのつもりなのかもしれません。

しかし選択操作をした段階でユーザーの意識はポイントのカーソルからツールチップの選択状態に移っていますから、そのタイミングで入力した文字は候補の選択を変える動作に使って欲しいのです。companyでもcompany-explicit-action-pという関数があって、ユーザーが明示的に行動を起こしたかによって挙動を変更する仕組みがあります。その思想と整合性がとれていないとも言えます。

調べてみたところ、候補の更新処理である company-update-candidates に問題を見つけました。

(defun company-update-candidates (candidates)
  (setq company-candidates-length (length candidates))
  (if company-selection-changed
      ;; Try to restore the selection
      (let ((selected (and company-selection
                           (nth company-selection company-candidates))))
        (setq company-candidates candidates)
        (when selected
          (setq company-selection 0)
          (catch 'found
            (while candidates
              (let ((candidate (pop candidates)))
                (when (and (string= candidate selected)
                           (equal (company-call-backend 'annotation candidate)
                                  (company-call-backend 'annotation selected)))
                  (throw 'found t)))
              (cl-incf company-selection))
            ;; ★★★ここを直したい!★★★
            (setq company-selection company-selection-default
                  company-selection-changed nil))))
    (setq company-selection company-selection-default
          company-candidates candidates))
  ;; Calculate common.
  (let ((completion-ignore-case (company-call-backend 'ignore-case)))
    ;; We want to support non-prefix completion, so filtering is the
    ;; responsibility of each respective backend, not ours.
    ;; On the other hand, we don't want to replace non-prefix input in
    ;; `company-complete-common', unless there's only one candidate.
    (setq company-common
          (if (cdr company-candidates)
              (let ((common (try-completion "" company-candidates)))
                (when (string-prefix-p company-prefix common
                                       completion-ignore-case)
                  common))
            (car company-candidates)))))

直す場所が深いのですが、adviceで次のようにすれば直ります。

(defun my-company-update-candidates (old-fun candidates)
  (let ((old-selection-changed company-selection-changed)
        (old-selection company-selection)
        ;; call the original function
        (result (funcall old-fun candidates)))
    ;; keep company-selection-changed
    (setq company-selection-changed old-selection-changed)
    ;; keep company-selection that is not nil
    (when (and old-selection (null company-selection) candidates)
      (setq company-selection 0))
    result))
(advice-add 'company-update-candidates :around 'my-company-update-candidates)

呼び出しの前後で「ユーザーが一度でも選択を変更したかフラグ(company-selection-changed)」と「現在の選択肢番号(company-selection)」を適切に維持します。company-selection-changedをnilにしてしまうとユーザーが明示的に選択を変更したという意思がなかったことになってしまうのが違和感の原因だと思います。

2021-08-09追記: 選択状態のfaceを目立つものにする

company-selection-default を nil にする場合は、現在選択している候補がはっきり分かるようにした方が良いと思います。特に上では現在選択しているかどうかでRETやTABの挙動を変えてしまっていますからね。

色のセンスはありませんが、例えば:

(set-face-background 'company-tooltip-selection "#a62")

(実際にはcustomize-faceで変更しています)

1~2日使ってみての感想ですが、自動で候補が出たときに未選択状態から始まるのは慣れるまで時間が必要ですね。下を押してRETを押す習慣を付けなければなりません。あとモードによって選択状態から始めているのは統一感が無くてあまり良くなかったかもしれませんね。

2021-08-05 ,

org-modeをData URI Schemeに対応させる

とりあえず対応させてみました。

↓はData URIなんですけど表示されてますか?

赤い丸(SVG)
図1: 赤い丸(SVG)
富士山!(JPEG)
図2: 富士山!(JPEG)

↑の書き方(org-modeのソース)は次の通りです。

#+CAPTION: 赤い丸(SVG)
[​[data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIj4NCiAgPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iNDAiIHN0cm9rZT0iYmxhY2siIHN0cm9rZS13aWR0aD0iMyIgZmlsbD0icmVkIiAvPg0KPC9zdmc+DQo=]]

#+CAPTION: 富士山!(JPEG)
[​[data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAeAAD/7gAOQWRvYmUAZMAAAAAB/9sAhAAQCwsLDAsQDAwQFw8NDxcbFBAQFBsfFxcXFxcfHhcaGhoaFx4eIyUnJSMeLy8zMy8vQEBAQEBAQEBAQEBAQEBAAREPDxETERUSEhUUERQRFBoUFhYUGiYaGhwaGiYwIx4eHh4jMCsuJycnLis1NTAwNTVAQD9AQEBAQEBAQEBAQED/wAARCAAyAGQDASIAAhEBAxEB/8QAgAAAAgMBAAAAAAAAAAAAAAAAAAMBAgQFAQADAQEBAAAAAAAAAAAAAAAAAQIDBAUQAAEDAgQDBQYFBQAAAAAAAAEAAgMRBCExQRJRYQVxgcEiE/CRoTJCI9FighQGsVJy0hURAAICAQUBAQAAAAAAAAAAAAABEQIhMUFxEgNhIv/aAAwDAQACEQMRAD8A3UUUTdqjavTk4RdEbUyiCESAvaooupadIkkHq3NYos9uTz/qtEsdrb27nmNvosxNBUuP0tbriVm/aqcL9cGi821LwcOmuiKLbYRsuZWzOFHuJLmfTUHIjgFlvz+1vZWBpNuTVnFoOnv0VL0Tt10ZLpCkXRFFdhZI3cw7hy8VO1XJIuiKJm1RtSkYuiEzahEgP2oppqcgujH0xxP3JABwYKn4pzH9Ptdwic3e2geR55MchgsLeyWn6LXm3rgxQdMuZaFwETDjV3zU/wAfxXStrG3tjuY3fIM5X+A0WC6/kNnECIj6zxlo2vMrmTfyK4kG07RGHCtBSrRiQeTslm36X+I0SrX6zvz3QeKM8zjXZ/aKYbnDXksPUqMsXNc5xeW9ri0GrqcOSLa7bJbC8lpFGRvcTjQDCpp8Fnml/emR0cgMM3kYW4kNaKf1xRWsPgbeOSttIYLtkjDSCaIFjnUFZCK4/mcnXVsJ9gFXCRo81RUO4HvzWQPt/wB62IncyGNkcUTfMXStzw4N1KnqN3Pbwhsbmm4jf6jY8Hbg75wOzNVuo1J2yZ32jo/uxbo5AS1zW4iozpXPsVhO5jWm4ZRrhhKzFv6hm1DOrCS29a5YXMeNskrRSrqV2kYeamoTY7m1lkMDHje5m9ocKB7TjjzotFd7ol1WxZu17dzCHNOoxCNqqLSMOJjJilpVpYaB1eNcCUmPqMBO18jX/mb5T3tPgq7Jk9WP2oR69tt3eo2iESEHMuuuTTuqS5wGFK+mwgalrfMa8ysU3ULqYU3emzSOIbG/DE96zjHPAozyxUqqWxTbJLzqUBzndgUGmqN3AYJiN111B/8Az4bGJ3kA3SvIoXGtdvYCskE8kIcYnFsjsNwz20xAPNUNCa1FSpa0AgjHgUsIeWaIb2W39F8IAfG1zXOI3Fwca0x0WaaaWaUzSuL5XUJec/grUaRiFDgKcxqhQGSwvJjEYH/ciOJa7Q8WnQpkVw5txFdOJl9LbRoIaQW5B3LsWanvUhrgQ4dyMCya7vqdzckZQxjFsTK0B1OKyGutVYgUrka5ckbTSoIPfihQMrsbtrTmhFDWuqEAW+p2eff3oOXt8EIQIjUeKDl3IQgZQ5J306+KEJMELf8AKc/1I4exQhADOHjn3KW/j2IQkUU+kZ69in2xQhMRX3IQhAj/2Q==]]
Emacs上での見た目
図3: Emacs上での見た目

Data URIを使うとorg-mode文書のサイズが肥大化するのと引き換えに外部ファイルを管理する手間が省けたりします(こうやってエクスポートしてWeb上に上げた場合はHTTPリクエストが減るとか、一方で画像キャッシュが効かないとか色々あると思います)。

Web上を検索したらorg-modeとData URIに関する話題はチラホラ見かけたのですがそのものズバリというものは見つからなかったんですよね。同じ事を考える人は沢山いそうなのですが……。

ソースは次の場所に置きました。

misohena/org-inline-image-fix: A collection of fixes related to the image display feature in org-mode

が、かなりヤバイクソコードなのでご了承ください。

インライン画像表示まわりは例によって org-display-inline-images 関数にadviceをかけて実現しているのですが、この関数は元々結構長くて改造が容易ではありません。以前は丸丸上書きするような修正の仕方もしたのですが、org-mode側に修正が入ると直さなければならなくなるので面倒です(ちょっと前にリモート画像対応が少し入りました)。なので今回は極力元の関数を活かす方向で修正したのですがそれが悪夢の始まりでした。

長ったらしい関数の中の挙動を修正するにはそこから呼び出す関数を書き替えるしか手が無く、 cl-letf で沢山の関数を上書きして無理矢理実現しています。まるで針の穴を通すようなプログラミングにゾクゾクしてしまいました。まるでパズルゲームです。何度もこれはもうダメか、書き直すしか無いかと諦めかけました。 org-element-property が実は defsubst でバイトコンパイルがかかると呼び出されないということに気がついたときにはどうしようかと思いました。追い詰められて plist-get とか format とか基本的な関数を書き替えているのでどこでおかしくなるか分かりません。 org-display-inline-images を書き替える他のプログラムを使ったときの動作は保証いたしかねます。正直全部上書きした方がまだマシだったかもしれませんね。どうせorg-mode側で修正がかかったら全部おじゃんでしょう。

:after adviceをかけて、本家の処理が終わった後にもう一度バッファを走査するのが一番安全かもしれません。大きな文書では効率が落ちるかもしれないので今回は回避しましたが(画像を扱っている時点で今更?)。

YouTube動画へのリンクを画像に置き換えるコード(TobiasZawada/org-yt: Youtube links in org-mode)を見つけたのですが、それは:afterアドバイスで再走査していました。皆さん苦労しているようです。

私も今 org-display-inline-images には三つもアドバイスがかかっているので苦労しています(上のリポジトリにはこれまでのものをまとめてあります: URLリンク対応, 画像リンク即時表示, サイズ制限)。

元々のコードがもう少し改造しやすくなっていると良いのですが……。

あ、あとエクスポートはHTMLだけ対応ですのであしからず。こちらも面倒でした。

2021-08-04

ivy-switch-bufferでブックマーク内のファイルをブックマーク名で検索できるようにする

Ivyでは ivy-use-virtual-bufferst にすると ivy-switch-buffer (C-x b) で(recentfや)ブックマーク内のファイルが選べるようになりますが、候補として登録されるのはあくまでファイル名(ivy-virtual-abbreviate を設定するとディレクトリパスも含めることは出来る)だけでブックマーク名は登録されません。なのでいくら分かりやすいブックマーク名を付けていても ivy-switch-buffer でそれを元にファイルを検索することは出来ません。なのでそれを出来るようにしてみました。

(require 'ivy)

(defun my-ivy-bookmark-name-filename-list ()
  "Return bookmark (name . filename) list as virtual buffer format."
  (delq nil
        (mapcar (lambda (record)
                  (when record
                    (let ((name (bookmark-name-from-full-record record))
                          (filename (bookmark-get-filename record)))
                      (when (and name filename)
                        (cons
                         (propertize
                          (format "*bookmark:%s" name)
                          'face 'ivy-virtual)
                         filename)))))
                bookmark-alist)))

(defun my-ivy-bookmark-append-advice (orig-fun)
  (let ((result-orig (funcall orig-fun))
        (my-virtual-list (my-ivy-bookmark-name-filename-list)))
    (setq ivy--virtual-buffers (append ivy--virtual-buffers my-virtual-list))
    (append result-orig (mapcar #'car my-virtual-list))))

(advice-add 'ivy--virtual-buffers :around 'my-ivy-bookmark-append-advice)
;;(advice-remove 'ivy--virtual-buffers 'my-ivy-bookmark-append-advice)

ivy-switch-buffer が扱うのはあくまでバッファ名です。現実にあるバッファの名前を選んでそこへ切り替えます。なのでrecentfやブックマークのファイル名はバッファ名ではないので「仮想バッファ」の名前として扱うことで無理矢理処理しています。仮想バッファ名リストを生成する関数が ivy--virtual-buffers です。その関数は、 ivy--virtual-buffers という変数(関数と同じ名前でややこしいですが)に仮想バッファ名とファイル名の対応表を構築した上で仮想バッファ名のリストを返します。

なので ivy--virtual-buffers 関数にadviceをかけて仮想バッファを付け足すのが上のコードです。

ivy-switch-bufferで選べる候補が気に入らないという方はこのようなやり方で候補を追加してみてはいかがでしょうか。

ちなみに、 *scratch* が無くても常に *scratch* を選べるようにするには my-ivy-bookmark-append-advice が返すリストに一つ付け加えるだけです。

(defun my-ivy-bookmark-append-advice (orig-fun)
  (let ((result-orig (funcall orig-fun))
        (my-virtual-list (my-ivy-bookmark-name-filename-list)))
    (setq ivy--virtual-buffers (append ivy--virtual-buffers my-virtual-list))
    (append result-orig (mapcar #'car my-virtual-list) '("*scratch*"))))

こうしておくと、もし知らないうちに *scratch* を閉じてしまっていても *scratch* というバッファ名を選べて新しくバッファを作成することが出来ます。

2021-08-04

WindowsでflycheckがNULというファイルを作ってしまう問題

以前flycheckを試したときにNULというファイルが出来て消すのが大変だったのでflymakeでいいやーと思ったことがあったのですが、最近また調べたところ対処法を書いているサイトを見つけました。

Emacs Flycheck

下記の記述だと、Windowsで、gcc によっては、同じディレクトリに “NUL” というファイルが生成されることがある。

:command ("gcc" "-c" "-I../../inc" "-I." "-O1" "-Wall" source "-o" null-device)

その場合、下記のように temporary-file-name を指定すると回避できる

:command ("gcc" "-c" "-I../../inc" "-I." "-O1" "-Wall" source "-o" temporary-file-name)

checker定義内の ‘null-device は同名の変数null-deviceの値で置き換えられて、Windowsだと “NUL” になります。これをCygwinやMSYS2/MinGWのgccが受け取るとコンパイルが成功したときにそのままNULというファイルを出力してしまうのが原因のようです。

同じようにcheckerを再定義すれば良いのかなと思ったのですが、flycheck.elを見てみると沢山のcheckerが定義されていてその中でnull-deviceも沢山使われていました。

そもそもcheckerを再定義するのも面倒ですし、かといってnull-deviceの値を書き替えるのも(他にどこで使われているのか分からないので)怖いですし、手っ取り早くadviceでnull-deviceをtemporary-file-nameに置き換えてしまうことにしました。

;;; Windowsでnull-deviceを使わないようにする。
;; null-deviceはNULなので、CygwinやMinGWだとNULというファイルを作ってしまう。
;; flycheck-command-wrapper-function でNULを置き換えても良いのだが
;; flycheck-substitute-argument でやった方が確実だし簡単。
(when (locate-library "flycheck")
  (with-eval-after-load 'flycheck
    (defun my-flycheck-substitute-argument (old-func arg checker &optional rest)
      (when (eq arg 'null-device)
        (setq arg 'temporary-file-name))

      (apply old-func arg checker rest))
    (advice-add 'flycheck-substitute-argument :around 'my-flycheck-substitute-argument)))

最近はLSPの方でエラー箇所を検出してしまうので必要ないのかもしれませんけどまたNULファイルが出来ると面倒なので念のため。

2021-08-03

2021年夏の新番組

最近は1話が月末まで遅れることが多くなってきましたね。

初評 テレビ開始 テレビCh 作品名 ネット配信
06/29(火) 25:34 日テレ ワンダーエッグ・プライオリティ 特別編  
07/01(木) 22:00 MX ピーチボーイリバーサイド dアニメ
07/01(木) 22:30 MX SCARLET NEXUS dアニメ
07/01(木) 23:30 MX ひぐらしのなく頃に 卒  
07/02(金) 24:00 MX ヴァニタスの手記 第1クール Amazon
07/02(金) 24:30 MX 100万の命の上に俺は立っている 第2シーズン  
07/02(金) 25:05 MX ぶらどらぶ(VLADLOVE) ※ネットでは既に配信済 dアニメ
07/02(金) 25:25 TBS系 乙女ゲームの破滅フラグしかない悪役令嬢に転生してしまった…X (第2期) dアニメ
07/02(金) 25:50 TBS系 俺、つしま Youtube
× 07/02(金) 26:25 TBS カノジョも彼女 ABEMA
× 07/03(土) 22:00 MX ぼくたちのリメイク ABEMA
07/03(土) 23:30 MX 魔法科高校の優等生 dアニメ
× 07/03(土) 25:00 MX かげきしょうじょ!! dアニメ
07/03(土) 25:30 MX 現実主義勇者の王国再建記 FOD
07/03(土) 25:30 テレ朝 RE-MAIN dアニメ
07/04(日) 21:30 MX 天官賜福 -吹替版- dアニメ
07/04(日) 22:00 MX 死神坊ちゃんと黒メイド dアニメ
07/04(日) 22:30 MX アイドリッシュセブン Third BEAT! (第3期) 第1クール  
07/04(日) 23:00 MX ゲッターロボ アーク dアニメ
× 07/04(日) 24:00 MX 探偵はもう、死んでいる。 dアニメ
07/04(日) 25:00 MX 指先から本気の熱情2-恋人は消防士- dアニメ
× 07/05(月) 25:30 テレ東 うらみちお兄さん dアニメ
07/05(月) 26:00 テレ東 精霊幻想記 ABEMA
07/06(火) 23:00 MX 転生したらスライムだった件 第2期 第2部 dアニメ
07/07(水) 07:05 テレ東 KICK&SLIDE  
07/07(水) 22:00 MX チート薬師のスローライフ~異世界に作ろうドラッグストア~ dアニメ
07/07(水) 22:30 MX TSUKIPRO THE ANIMATION 2  
07/07(水) 23:00 MX 月が導く異世界道中 dアニメ
07/07(水) 24:00 MX 小林さんちのメイドラゴンS (第2期) dアニメ
07/08(木) 24:00 MX 白い砂のアクアトープ ABEMA
07/08(木) ~ Netflix BIOHAZARD(バイオハザード):Infinite Darkness Netflix
07/09(金) 22:30 MX 迷宮ブラックカンパニー dアニメ
× 07/10(土) 22:30 MX D_CIDE TRAUMEREI dアニメ
07/11(日) 19:00 Eテレ ラブライブ!スーパースター!!  
07/11(日) テレ東 闇芝居 第9期 Paravi
× 07/12(月) 24:00 MX 出会って5秒でバトル dアニメ
× 07/14(水) 25:05 MX 女神寮の寮母くん。 dアニメ
07/14(水) 24:55 フジテレ NIGHT HEAD 2041 FOD
× 07/15(木) 24:30 MX Sonny Boy -サニーボーイ- dアニメ
07/18(日) 24:30 BS日テレ 戦乙女の食卓Ⅱ YouTube
07/20(火) ~ WEB配信 アサルトリリィふるーつ  
07/22(木) 24:55 フジテレ 平穏世代の韋駄天達 Amazon
07/31(土) 24:00 MX マギアレコード 魔法少女まどか☆マギカ外伝 2nd Season  
07/31(土) 26:00 テレ朝日 ジャヒー様はくじけない! dアニメ

今期は不作……凶作ですかね?