Monthly Archives: 7月 2021

2021-07-30 ,

EmacsのIndiumでJavaScript開発を試す

Emacs用のJavaScript開発環境であるIndiumを試してみました。

NicolasPetton/Indium: A JavaScript development environment for Emacs

一口に開発環境と言っても何が出来るのかよく分からないと思いますが、

  • Emacs側で入力したJavaScriptを実際の実行環境上で実行(評価)して結果を出力する
    • 入力毎に逐次実行するいわゆるREPL
    • 自由に書いて自由に実行できるscratchバッファ
    • JavaScriptソースコード内のコードを実行
  • デバッガー(ステップ実行や値の調査など)

といったことが出来るようです。

実行環境はChromeとNode.jsに対応していて、どちら用のJavaScriptでも実行したりデバッグしたり出来ます。Chromeで言えばデベロッパーツールとしてコンソールやデバッガーがありますが、あれがEmacsから操作できるといえば分かりやすいでしょうか。

私が試してみたきっかけはorg-modeのJavaScriptコードブロック(ob-js.el)がこのIndiumと連携可能だと書いてあったからなのですが、それはまた後ほど。

indiumでデバッグしているところ
図1: indiumでデバッグしているところ

手元の環境

  • Windows 10
  • Emacs 27.2
  • Google Chrome 92
  • Node.js 12

あたりが既に入っています。

インストール

  1. npm install -g indium

    Node.jsでIndiumサーバをインストールします。

  2. M-x package-install indium

    Emacsにindiumパッケージ(Indiumクライアント)をインストールします。

  3. indium-chrome.el の indium-chrome-data-dir部分を変更します。(2021-07-29時点)

    (defvar indium-chrome-data-dir
      (make-directory indium-chrome--default-data-dir t)
      "Chrome profile directory used by Indium.")
    

    ↓へ変更。

    (defvar indium-chrome-data-dir
      (progn
        (make-directory indium-chrome--default-data-dir t)
        indium-chrome--default-data-dir)
      "Chrome profile directory used by Indium.")
    

    必要に応じて.elcを削除したりコンパイルし直したりしてください。既にロードしてしまっている場合は変数の値を再設定してください。

    これをやらないとChromeがエラーを出します。

    Chromeの起動時に出たエラー。nilディレクトリ
    図2: Chromeの起動時に出たエラー。nilディレクトリ

    IndiumはデフォルトだとChromeを独立したプロファイルで起動します。そのプロファイルを置くディレクトリをmake-directoryで作るのは良いのですがmake-directoryの戻り値であるnilを変数に入れてしまっています。

プロジェクト設定を行う

  1. プロジェクトディレクトリを作る

    まずは何をするにもプロジェクト設定ファイルが必要です。その設定ファイルが置いてある場所を基準に色々な動作が決められているので、まずはそれを置くプロジェクトディレクトリを作りましょう。

  2. プロジェクト設定ファイルを作る

    プロジェクトディレクトリの下に .indium.json というファイルを作ります。

    内容は次の通り。

    {
        "configurations": [
            {
                "name": "Local Host 8080 Page",
                "type": "chrome",
                "url": "http://localhost:8080/index.html"
            }
        ]
    }
    

    nameは適当な名前を付けてください。

    typeは実行環境の種類です。chromeかnodeが指定出来るみたいです。今回はchrome。

    urlはtypeにchromeを指定した場合のオプションで、Chromeからどのページを開くか(デバッグ対象にするURL)を指定します。Chromeから開けるページならどこでもOKです。普通は自分の開発中のページを指定することになりますが、他人様のページを指定することも出来ますし(変なデータを送りつけるのは止めましょう)、ローカルファイル(file:///)を指定することも出来ます。

    例えば次のようにするとGitHubのIndiumのページがデバッグ対象になります。

    {
        "configurations": [
            {
                "name": "Indium Page",
                "type": "chrome",
                "url": "https://github.com/NicolasPetton/Indium"
            }
        ]
    }
    

    また次のようにするとローカルにあるファイルがデバッグ対象になります。

    {
        "configurations": [
            {
                "name": "My Local Directory",
                "type": "chrome",
                "url": "file:///home/hoge/indium-tutorial/index.html"
            }
        ]
    }
    

Windowsでローカルファイルを指定するときの注意点

Windowsでurlにローカルファイル(file:/// プロトコル)を指定した場合、デバッグ(ブレークポイントの設定)が正しく行えない問題に遭遇しました。

調べてみたところ、例えば file:///C:/home/hoge のようなURLが /C:/home/hoge のように頭にスラッシュが残されたパスに変換されてしまう箇所があるようです。例えばindiumサーバのソースコード server/helpers/workspace.js の resolveUrl 関数に fileプロトコルの時の処理があるのですが、ここで /C:/C:/ に変換していないためにWindowsではおかしなパスになってしまいます。他にも問題があるようで、そこを直しただけでは解決しませんでした(逆変換とかがどこかにある?)。その上のconvertRemoteRootを見るとremoteRootオプションで指定した文字列を空文字列で置き換えています。この動作を利用すれば問題を回避することが可能です。

回避方法ですが、remoteRootオプションにプロジェクトディレクトリへのフルパスを頭にスラッシュを付けて指定します。

{
    "configurations": [
        {
            "name": "My Local Directory(Windows)",
            "type": "chrome",
            "remoteRoot": "/C:/home/hoge/indium-tutorial/",
            "url": "file:///C:/home/hoge/indium-tutorial/index.html"
        }
    ]
}

remoteRootオプションがあると、その文字列を手元のプロジェクトディレクトリに置き換えてくれます。これでスラッシュから始まる不正なパスを正しいパスへ置き換えることが出来ます。

httpサーバの起動

urlに localhost:8080 などと指定した場合は自分で別途Webサーバを起動する必要があります。お手軽に起動できるサーバが色々あるみたいなので好きな物を使ってください。

Emacsで完結するならsimple-httpdがお手軽です。 M-x httpd-serve-directory だけで完了です。

色々遊ぶ

  1. まずプロジェクトディレクトリ下にindex.htmlを作ります。

    <!DOCTYPE html>
    <html>
      <head>
        <title>Test</title>
      </head>
      <body>
        <p id="hello">hello, world</p>
      </body>
    </html>
    
  2. プロジェクトディレクトリ下で M-x indium-launch を実行します。

    するとデバッグ用オプションの付いたChromeが起動してurlオプションで指定したページが開きます。(indium-chrome-data-dir変数の場所にChromeプロファイルが出来てその上で実行されるはず)

    Emacs側では *JS REPL* という名前のバッファが開きます(以下REPL)。

  3. REPLに document.getElementById("hello").innerText; (単に hello.innerText でも良い)と入れると "hello, world" が返ってきます。
  4. REPLに document.body.insertAdjacentHTML("beforeend", "Konnichiwa!"); と入れるとブラウザに Konnichiwa! と表示されます。
  5. プロジェクトディレクトリの下にexample.jsを作ります。

    var cv = document.createElement("canvas");
    cv.width = 640;
    cv.height = 480;
    var ctx = cv.getContext("2d");
    function draw(color){
        ctx.fillStyle = color || "blue";
        ctx.fillRect(0, 0, 640, 480);
        document.body.appendChild(cv);
    }
    draw();
    
  6. REPLに次のように入れるとexample.jsがロードされて画面に青い矩形が出ます。

    var script = document.createElement("script");
    script.src = "example.js";
    document.body.appendChild(script);
    
  7. REPLに次のように入れると赤い線が出ます。

    ctx.lineWidth = 2;
    ctx.strokeStyle = "red";
    ctx.moveTo(0, 0);
    ctx.lineTo(640, 480);
    ctx.stroke();
    
  8. Emacsでexample.jsを開いてdraw()関数内の一行目にブレークポイントを置きます。 M-x indium-add-breakpoint か、または M-x indium-interaction-mode の後に C-c b b でブレークポイントを置けます。
  9. REPLに draw("red"); を入れるとブレークポイントの位置にカーソルが飛びます。
  10. l を押すとローカル変数一覧が出ます。colorが"red"になっていることが分かります。 SPC でステップ実行したり c で実行を再開したりできます。
  11. M-x indium-quit で接続を切ります。専用プロファイルで起動しているChromeも閉じましょう。

Node.jsでも遊ぶ

Node.js用のJavaScriptをデバッグすることも出来ます。

新しくプロジェクトディレクトリを作り、設定ファイルを作り、適当なJavaScriptファイルを作ります。

まずは設定ファイル(.indium.json)を作ります。

{
    "configurations": [
        {
            "name": "Example",
            "type": "node",
            "program": "node",
            "args": "./example.js",
            "inspect-brk": true,
            "remoteRoot": "/c:/home/hoge/indium-node-example/"
        }
    ]
}

typeはnodeとします。

argsに実行するJavaScriptファイルを指定します。

inspect-brkがtrueだと最初でブレークします。

Windowsでは残念ながらNode.jsでも /c:/ で始まるパスの問題でうまくデバッグが出来ませんでした(M-x indium-list-script-sources で確認できます)。それを回避するためにremoteRootを指定しています。

適当なJavaScriptファイル(example.js)を作ります。

function concat(a, b){
    return a + b;
}
const hw = concat("hello, ", "world");
console.log(hw);

あとはこのディレクトリで M-x indium-launch すれば最初の所(上の例では const hw のところ)に飛びます。ステップインしてconcat関数の中に入り、 *JS REPL* バッファで a とか b とか打ってみてください。変数の値が確認できます。

org-modeのコードブロック(babel)から使う?

Org-babel-jsによればorg-modeのJavaScriptコードブロック(ob-js.el)はIndiumに対応しているようなこと書かれています。ヘッダー引数に :session "*JS REPL*" を指定するのだとか。しかし(プロジェクトファイルを作った上で)実際にやってみると org-babel-execute:js: Symbol’s function definition is void: indium-run-node というエラーが出ます。Indiumには現在 indium-run-node という関数は無いのでどうやら現在は動かなくなってしまっているようです。

IndiumにもIssueが上がっています。

Question: org-mode ob-js.el sessions · Issue #234 · NicolasPetton/Indium

org-modeから使うなら自分で改造する必要があると思います。

2021-07-29

新しい寝タブ用タブレットスタンドを購入

以前寝タブ用にタブレットスタンドを購入したのですが最近はほとんど使っておらず布団の脇に放置されていました。というのも

  1. アームが短く頭のすぐ近くに置かなければならないこと
  2. 頭の近くにあると使わないときに邪魔なので遠ざけたいがアームが曲げづらく土台ごと遠ざけなければならないこと
  3. 土台ごと遠ざけると次に使うのが億劫になること(土台は布団の下に挟み込まれているので簡単には動かない)

あたりが原因だと思います。他にもアームが曲げづらいので使っている間の調整が難しいこともあると思います。

使わないアームが布団の脇にずっと放置されているのも何だか不愉快になってきたので、代わりのものを探してみたところ良さそうな商品が見つかりました。

タブレットスタンド スマホ ホルダー 360度回転可能 38cm-140cm高さ調節可能 主体調節でき 折り畳み式 フレキシブルアーム 寝ながら 根元強化 下垂防止 3.5~10.6インチ 便利スタンド

これこれ、こういう昔ながらの電器スタンドのアームみたいなタイプが欲しかったんです。以前探したときにはなかなか見つからなかったのですが最近はタブレットスタンドのバリエーションもかなり増えてきたのかもしれませんね。

届いたので早速組み立てて使ってみました。

一番下の直線部分は2本のパイプをパイプに切られたネジで連結する仕組みになっています。1本だけ使って低くすることも可能。それを土台のネジに繋げます。

直線パイプの上、一番最初の可動部分はアームパーツを下のパイプ上部の穴に差し込むだけになっています。なのでこの部分が垂直軸まわりに360度自由に動きます。邪魔なときにアームをよけておくのに使えます。

アームはかなり広範囲に動きます。パイプ2本だと高いかなと思ったのですが、アームによってかなり低い位置まで下ろせます。もちろん高い位置にもできるので目からの距離を使用中に柔軟に調整できます。

唯一の欠点はタブレットを挟む部分がタイトなことでしょうか。バネによってタブレットを挟んで固定するのですが、10インチタブレットだとかなりギリギリです。挟む力がかなり強いので挟みっぱなしだとそのうち本体が変形しないか心配になります。まぁ、多分大丈夫だとは思いますが……。

というわけでかなり満足度の高いタブレットスタンドでした。

2021-07-28

Emacsでモードライン内の上下に空白を増やす

この間MeiryoKe_ConsoleとInconsolataを合成したフォントを作成してEmacsではそれを使うようにしたのですが、合成したフォントはMeiryoKe_Consoleの寸法がベースになっているのでグリフの上下に余白が少なくモードラインの上下も何だか詰まった感じになってしまいました。

上下が詰まったモードライン
図1: 上下が詰まったモードライン

以前はInconsolataに上下の余裕がたっぷりあったので気にならなかったのですが。

というわけでちょっと余白を増やしてみました。モードラインの内容は mode-line-format 変数で指定できますが、その中ではテキストプロパティを自由に設定できるようになっています。テキストプロパティを設定できると言うことは、そう、おなじみのdisplayプロパティが使えるというわけですね。

今回はモードラインの先頭にdisplayプロパティによって空白を作り出し、その空白の幅、高さ、アセントを設定することで上下の余白をコントロールすることにしました。

;; モードラインの上下内側に空白を設ける。
;; 変数をリスキーだと指定しないとテキストプロパティが反映されないので注意。
(defvar my-mode-line-height
  '((:propertize " " display (space . (:width (1) :height (22) :ascent (16)))))) ;;w=1px, h=20px, ascent=16px
(put 'my-mode-line-height 'risky-local-variable t)

;; mode-line-formatの先頭に'my-mode-line-heightを挿入する。
;; 末尾に挿入すると長いファイル名で右端がフレーム外に出ると空白も消えてしまう。
;; バッファローカル変数なのでsetq-defaultを使う。
(setq-default mode-line-format
              (append (list "" 'my-mode-line-height) ;;先頭は文字列じゃないとダメ
                      mode-line-format))

:height でモードライン全体の高さを指定します。 :ascent でベースラインより上の高さを指定します。 :ascent を増やすと上の余白が増えて下の余白が減ります。

上下に余白を増やしたモードライン
図2: 上下に余白を増やしたモードライン

他にもface(mode-line や mode-line-inactive)の:box属性を使って境界線の幅を増やし色もバックグラウンドと同じにする方法もあるようです。今時のフラットな見た目ならこっちの方が手軽かもしれません。左右に少し空白が空くとは思いますが数ピクセルくらいなら気にならないでしょう。

参考: faces - Padding around modeline text? - Emacs Stack Exchange

2021-07-27

dired-details-rを修正する

皆さんは dired-dwim-target 変数を使ってますか? diredでコピー先や移動先を良い感じに推測してくれる機能です。分割ウィンドウで他のディレクトリを開いているとコピー先や移動先をそのディレクトリにしてくれます。古からある二画面ファイラーなんかではおなじみの機能ですね。ついでにM-nで出る「未来の履歴」にもいくつか候補を設定しておいてくれます。(EmacsではM(Alt)-p(previousのp)で過去の履歴をたどれてM-n(next)で現在に戻ってこれますが、現在を越えてM-nを押すとこれから選びそうな候補が出てくるというUIの考え方が(一部で)あります)

それでコピーや移動のときには大変便利なのですが、ファイル名を変更するときにはよくミスを犯してしまうのです。なぜならファイル名の変更と移動は同じRキーに割り当てられているからです。名前の変更はファイルの移動と同じ、または逆にファイルの移動はディレクトリパスも含めた名前の変更と同じと考えられるからです。Unixではどちらもmvコマンドですし。それでファイル名を変更しようとRキーを押したとき、 dired-dwim-target が有効だとたまたま開いていた別のディレクトリが移動先として設定されてしまうわけです。それに気がつかずに新しいファイル名を入力してEnterを押すと、ファイル名も変わるのですが同時に別のディレクトリにファイルが飛んで行ってしまうわけです。ぎゃー!

というわけでどうにかならないのかなと。移動と名前の変更が同じキーになっているのが原因ですから、別のキーに割り当てて候補の出方を変えるというのも手です。小文字のrは使っていないのでここに「ファイル名変更(ディレクトリ変更不可)」を割り当てるとか。でも非標準の操作体系を増やすと別のマシンを使うときに戸惑うのであまりやりたくありません(いまさら!)。安全優先でRは名前変更のための候補を優先して出し、移動先の候補はM-nで出すだけで十分かもしれません。と、Twitterでつぶやいたのですが……

「名前変更には wdired 使ってます」との天の声が。

なんとそんな使い分けの方法があったとは!

wdiredとはファイル一覧のファイル名部分をテキストエディタの要領で編集するとファイル名が変わってくれるという便利機能です。

しかし私がwdiredを使っていないのには理由がありまして、以前wdiredを試したときにファイル名の入力がうまくいかなかったり、うまく行っても表示がおかしくなったりしたのです。

というのも私はdired-details-rというファイルサイズやタイムスタンプなどの詳細をファイル名の右に出す拡張を作っていて、どうもそれが悪さをしているようなのです。

しかしそれを直してしまえばwdiredを使ってみるのもやぶさかではありません。

というわけで重い腰を上げて dired-details-r を直すことにしました。他にも色々直したいところがあったのでついでに直すことにしました。

  • wdiredでファイル名を編集したときにファイル名の先頭でread-onlyエラーで書き替えられないことがあったり、書き替えられても書き替えた部分が表示されないことがある不具合を修正
  • ファイル名の末尾にカーソル(ポイント)を合わせられない不具合を修正(詳細情報の右に飛ぶ。wdiredでファイル名の末尾に文字を追加すると追加した部分だけ詳細情報の右に表示される!)
  • カスタマイズまわり(グループとか型とか)の修正
  • マイナーモード化(dired-details-r-mode)
  • 一部のファイルだけ更新がかかったときに列が乱れる不具合の修正
  • "(" キーでの表示切り替えで元の表示形式(詳細の左表示)に戻せる機能を追加(wdiredでパーミッションを編集したい場合に備えて)
  • 設定のデフォルト値の調整
  • 導入方法の修正(以前はrequireするだけでしたが、マイナーモード化したので (dired-details-r-setup) を呼び出すか自分でマイナーモードを起動するか選択可能に)

以上を修正。

dired-details-r + all-the-icons
図1: dired-details-r + all-the-icons

晴れてwdiredが正常に使えるようになりました。

wdiredでのファイル名変更手順は、C-x C-q で編集を開始し、ファイル名を書き替えてから C-c C-c または 再度C-x C-q。Rを押してファイル名を書くより面倒かなと思ったのですが案外違和感がありません。C-x C-qはリードオンリーモードの切り替えでよく使うキー操作です。通常diredはリードオンリーモードになっていてテキストを書き替えられませんが、リードオンリーモードを解除してファイル名部分を書き替えて確定、または再度リードオンリーモードを有効にする、というある意味自然な流れなのは知っていました。しかしそうは言ってもタイプ数が増えるのだから面倒だろうと思ってもいたのですが、想像していたよりも違和感なく素早く操作できます。しばらく名前の変更はwdired縛りで行こうと思います。一括変更はさすがに % m でマークして % R で正規表現置換した方が楽な気もしますがどうでしょうね。

色々ときっかけを頂いたiquiwさんありがとうございました。

2021-07-24 ,

月の満ち欠け画像をアジェンダに表示する

以前月の出、月の入り、月齢をorg-modeのアジェンダに表示する仕組みを作った(moonrise-el)のですが、その時に月の満ち欠けを画像で表示できたらなぁと思っていたのでした。

というのも Emacs で時の流れを感じる - Qiita という記事でモードラインに月の満ち欠けを表示しているのを見ていいなーと思っていたからです。

その記事で紹介しているコードではU+1F311からU+1F318にある絵文字を使用して月の満ち欠けを表現しているのですが、私の使っているEmacsではフォントの設定が十分ではなく絵文字が表示できないのでその方法は使えませんでした(Windowsではカラー絵文字が使えないというのもあります)。

最近all-the-iconsを導入したのですが、その中に入っていた Weather Icons というフォントに月の満ち欠けのグリフが入っているのを見つけました。

Weather Icons内のグリフ
図1: Weather Icons内のグリフ

これを使うと次のようなコードで月の満ち欠けを表現できます。

(insert (all-the-icons-wicon "moon-14")) ;;0から27まで指定可能

これは使えると思い次のコードを作成しました。

(require 'all-the-icons)
(setq moonrise-moon-age-format
      (lambda (age)
        (concat (format "(%.2f) " age)
                 (all-the-icons-wicon
                  (format "moon-%d"
                          (mod (round (* (/ age 29.53) 28)) 28))
                  :v-adjust -0.1))))

本当は太陽と月の黄経差から求めた方が良いのですがmoonrise-elにはその機能が無いのでとりあえず月齢を使って計算しています。(2021-07-25追記:moonrise-elに月相を計算する機能を追加しました。現在は何も設定しなくても画像が出ます)

早速実行してみたのですが、org-modeのアジェンダでは正しいグリフが表示されません。calendarからL m(calendar-moonrise-moonset)やL d(calendar-moonrise-moonset-month)したときは正しく表示されます。

org-agendaで正しく表示されないグリフ
図2: org-agendaで正しく表示されないグリフ
calendarで正しく表示されるグリフ
図3: calendarで正しく表示されるグリフ

調べてみたところorg-agenda.el内でテキストプロパティfaceを'org-agenda-calendar-sexpに設定してしまうのが原因でした。all-the-iconsはテキストプロパティのfont-lock-faceにフォントファミリーを指定することで狙ったフォントでグリフを表示させるの仕組みなので、faceが指定されると違うフォントが選択されて正しいグリフで表示されなくなってしまいます。

困りました。このためだけにorg-agenda.elに手を入れるのは気が進みません。

何か良い方法は無いものか。describe-charでテキストプロパティの状態を眺めているとall-the-iconsが出力したfont-lock-faceプロパティ自体はバッファに反映されている点に気がつきました(faceプロパティが追加されたのでそちらが優先されているわけです)。つまり上の設定のlambda関数の中で生成したテキストプロパティ自体は正しくアジェンダバッファに反映されているわけです。テキストプロパティが使えるのなら画像を表示するのは簡単です。displayプロパティを使えば良いのです。

まず月の満ち欠けを表す画像を用意します。たまたまPCに入っていたBlenderでサクッと作ってみました。球と平行光源とカメラを置いて光源の向きをグルッと回すアニメーションを設定してレンダリングするだけです。

Blenderで画像を作る
図4: Blenderで画像を作る
レンダリングされた画像
図5: レンダリングされた画像

次に変数moonrise-moon-age-formatにはその画像を使うdisplayプロパティ付きの文字列を生成する関数を指定します。

(setq moonrise-moon-age-format
      (lambda (age)
        (concat (format "(%.2f) " age)
                (propertize
                 "A" 'display
                 (create-image
                  (format "~/tmp/moonphases/moonphase-%02d.png"
                          (min 30 (round (* (/ age 29.53) 30))))
                  nil nil
                  :ascent 'center)))))

すると狙い通りagendaバッファ上に月の満ち欠けが画像で表示されました。

org-agendaで正しく表示された月画像
図6: org-agendaで正しく表示された月画像

無事目的は達成できたのですが画像ファイルを管理するのが面倒です。よく考えてみたらSVGが使えるのですからEmacs内で生成してしまえば良さそうなものです。球の光が当たる境目は三角関数で簡単に求められるでしょう。

(require 'svg)
(defun moonrise-create-moon-svg (age size)
  (let* ((svg (svg-create size size))
         ;; 円の中心と半径
         (cx (* 0.5 size))
         (cy (* 0.5 size))
         (radius (* 0.48 size))
         ;; 円の分割数
         (ndiv 32)
         (2pi/ndiv (* 2.0 (/ pi ndiv)))
         ;; 月齢をラジアンへ
         (age-rad (* 2.0 pi (/ (min (max age 0.0) 29.53) 29.53)))
         ;; 光が当たる部分の右側と左側の位置
         (right-edge (if (<= age-rad pi) 1.0 (- (cos age-rad))))
         (left-edge (if (>= age-rad pi) 1.0 (- (cos age-rad)))))
    ;; 光が当たっていない月の全球
    (svg-circle svg cx cy radius :fill "#000")
    ;; 光が当たっている部分
    (svg-polygon
     svg
     (cl-loop for i from 0 to ndiv
              collect (let ((i-rad (* i 2pi/ndiv))
                            (edge (if (< (* 2 i) ndiv) right-edge left-edge)))
                        (cons (+ cx (* (sin i-rad) radius edge))
                              (- cy (* (cos i-rad) radius)))))
     :fill "#ffc")
    (svg-image svg :ascent 'center)))

試しに画像をバッファに挿入してみると正しく表示されます。

(cl-loop for i from 0 to 28 do
         (insert-image (moonrise-create-moon-svg i 16)))
EmacsのSVGでレンダリングされた月画像
図7: EmacsのSVGでレンダリングされた月画像

これを使う設定は次のようになります。

(setq moonrise-moon-age-format
      (lambda (age)
        (concat (format "(%.2f) " age)
                (propertize
                 "A" 'display
                 (moonrise-create-moon-svg age 16)))))

ちゃんと狙い通り表示されました。

org-agendaで正しく表示された月SVG画像
図8: org-agendaで正しく表示された月SVG画像

Emacsでは、設定に文字列をフォーマットする関数を指定することが良くあると思います。そういった場所では今回と同様の手法でSVG画像を使える可能性があります(返した文字列のテキストプロパティがそのままバッファにinsertされていることが必要です)。他の用途でも応用が利くかもしれないなと思いましたので紹介してみました。

#モードラインなんて全体をSVGで書いちゃえば良いんじゃないの?(テキストまわりの制御が難しいか?)

2021-07-23

neotreeで(setq neo-smart-open t)すると固まる(Windows)

neotreeを試してみたのですが、neo-smart-openをtにするとf8に割り当てたneotree-toggleを押したときに固まることがありました。普通のファイルを開いているときは固まらず、scratchやdiredでディレクトリを開いているときに固まるようです。

コードを追ってみたところ neo-buffer--select-file-node 関数の中で無限ループに陥ってしまう場所を見つけました。

https://github.com/jaypei/emacs-neotree/blob/98fe21334affaffe2334bf7c987edaf1980d2d0b/neotree.el#L1632

親ディレクトリへ移動するループで、ルートに到達したかの判定を"/"と比較することで行っています。Windowsでは"c:/"等がルートで何度neo-path--updirを適用しても決して"/"になりませんからいつまで経っても終わりません。

とりあえず"/"を"c:/"にしたら直ったのですが、それではあんまりなのでupdirしたときにパスが変化しなかったら終わらせるようにしてみました。

うまくadviceもかけられないしneotree.el読み込み後に関数まるごと再定義。neotreeはそんなに頻繁に更新されていないみたいなのでまあいいか。

(with-eval-after-load "neotree"
  (defun neo-buffer--select-file-node (file &optional recursive-p)
    "Select the node that corresponds to the FILE.
If RECURSIVE-P is non nil, find files will recursively."
    (let ((efile file)
          (iter-prev-dir nil) ;;ADD
          (iter-curr-dir nil)
          (file-node-find-p nil)
          (file-node-list nil))
      (unless (file-name-absolute-p efile)
        (setq efile (expand-file-name efile)))
      (setq iter-curr-dir efile)
      (catch 'return
        (while t
          (setq iter-prev-dir iter-curr-dir) ;;ADD
          (setq iter-curr-dir (neo-path--updir iter-curr-dir))
          (push iter-curr-dir file-node-list)
          (when (neo-path--file-equal-p iter-curr-dir neo-buffer--start-node)
            (setq file-node-find-p t)
            (throw 'return nil))
          (let ((niter-curr-dir (file-remote-p iter-curr-dir 'localname)))
            (unless niter-curr-dir
              (setq niter-curr-dir iter-curr-dir))
            (when (or (string= iter-curr-dir iter-prev-dir) ;;ADD
                      (neo-path--file-equal-p niter-curr-dir "/"))
              (setq file-node-find-p nil)
              (throw 'return nil)))))
      (when file-node-find-p
        (dolist (p file-node-list)
          (neo-buffer--set-expand p t))
        (neo-buffer--save-cursor-pos file)
        (neo-buffer--refresh nil)))))
2021-07-22 ,

Feedly Open All Unread Button Jul.2021

久しぶりにFeedlyを使ってみているのですが、未読のページ(全文)を一括で開く機能が無いととても使う気になれないので以前作っていたスクリプトを修正しました。

ChromeのTampermonkeyで確認。

// ==UserScript==
// @name         Feedly Open All Unread
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://feedly.com/i/*
// @icon         https://www.google.com/s2/favicons?domain=feedly.com
// @grant        GM_openInTab
// ==/UserScript==

(function() {
    'use strict';

    function openUnreadEntries(limit){
        const unreads = document.querySelectorAll(".entry--unread");
        const count = Math.min(unreads.length, limit || 5);
        console.log("count="+count);
        for(let i = 0; i < count; ++i){
            const entry = unreads[i];
            const url = entry && entry.dataset.alternateLink;
            //window.open(url, "_blank");
            //browser.tabs.create({url, active: false});
            GM_openInTab(url, {active:false});
            console.log("open " + url);

            // mark as read and hide
            const readAndHideButton = entry && (
                entry.querySelector(".EntryHideButton") || //Title Only
                    entry.querySelector(".EntryMarkAsReadButton") //Magazine, Card
            );
            if(readAndHideButton){
                readAndHideButton.click();
            }
        };
    }
    function createButton(){
        const div = document.createElement("div");
        div.style.display = "inline-block";
        div.style.verticalAlign = "top";
        div.className = "button-dropdown OpenUnreadButton";

        const button = document.createElement("button");
        div.appendChild(button);
        button.type = "button";
        button.innerText = "Open Unread";
        button.style = "padding-right:0px; margin-right:0px;";
        button.addEventListener("click", function(e){
            openUnreadEntries(parseInt(inputCount.value, 10));
        }, false);

        const inputCount = document.createElement("input");
        div.appendChild(inputCount);
        inputCount.type = "number";
        inputCount.value = "5";
        inputCount.style = "width:4em; padding:12px 5px 10px 5px; border:1px solid transparent; appearance: normal;";

        return div;
    }
    function updatePageUI(){
        for(const bar of document.getElementsByClassName("actions-container")){
            if(!bar.querySelector(".OpenUnreadButton")){
                bar.insertBefore(createButton(), bar.firstChild);
            }
        }
    }
    setInterval(updatePageUI, 1000);

})();

Inoreaderというのも使ってみたのですが、どっちみち全文を一気に読むには拡張機能が必要なことに変わりないのでとりあえずFeedlyでいいやと思いました。

2021-07-18 , ,

FontForgeでMeiryoKe_ConsoleとInconsolataをくっつける

これまでEmacsのフォント設定でASCIIはInconsolata、それ以外はMeiryoKe_Consoleを使うように設定していたのだけど、細かい調整が出来ないのでそれならばとFontForgeで一つのフォントに合成してしまうことにした。そうすれば細かい調整はFontForgeの方である程度行えるからだ。また、フォントサイズ(奇数サイズ)によって全角の幅が半角の幅のちょうど二倍にならない場合があるのもフォントを一つにしてしまえば解決すると思われた。ちなみにInconsolataは0に斜線が入っていて細身で綺麗なので長年使っている。MeiryoKe_Consoleを使っているのはぼやけたフォントが嫌いなのでヒンティングがしっかりしているメイリオでかつ等幅にしたものが欲しいからだ(ヒンティングには賛否両論あるとは思うが私はどちらかと言えば下手にアンチエイリアスしてぼやけたものよりもシャープな文字の方が好きだ。低解像度環境で細いフォントならば)。

ダウンロード先:

  • Inconsolata : Boldもダウンロードしておくと良い。さらにFontForge等で斜体版を作っておくとなお良い。
  • MeiryoKe : こちらもFontForgeで斜体版を作っておくと良い。
  • FontForge : 最新版にしたら常時カナロックがかかってしまう現象が発生した。IMEで半角変換して入力すればなんとか使える。最新の開発版では問題なかった。

合成する手順:

  1. Inconsolata側:
    1. FontForgeでフォントを開く。
    2. EMサイズを2048にする。「エレメント」→「フォント情報」→「一般情報」→「EMの大きさ」を「2048」にしてOKを押す。すると(「輪郭を拡大/縮小」にチェックが入っていれば)自動的にグリフが拡大縮小される。InconsolataのEMサイズは1000、MeiryoKe_Consoleは2048なので合成する前に合わせなければならない。
    3. ヒント情報・ヒント命令を削除する。「編集」→「選択」→「出力に値するグリフ」, 「ヒント」→「ヒントを削除」, 「ヒント→ヒント命令を削除」。MeiryoKe側に移したときにヒント命令は単純には引き継げないので削除する。
    4. ASCII文字(等MeiryoKe側へコピーしたい文字)のグリフを選択して右クリックし「参照を解除」。iとjがuni0307を参照していたりするのでこれを解除する。しないとコピーしたときにiやjの上の点がメイリオのものになってしまう。
    5. ASCII文字(等MeiryoKe側へコピーしたい文字)のグリフを選択して右クリックし「コピー」する。
  2. MeiryoKe_Console側:
    1. FontForgeでフォントを開く。
    2. フォント情報を書き替える。
      • PS Names
        • フォント名は「MeiryoKe_Inconsolata_スタイル名」等とする。
        • ファミリー名は「MeiryoKe_Inconsolata」とする。
      • 一般情報
        • (斜体なら)イタリックの傾きを傾けた角度にする。
      • OS/2
        • その他/Style MapをRegular, Bold, Italic, Bold Italicのいずれかに。
        • メトリック/WinDescentを360へ増やす(jの下が切れてしまうので。値はjやyの一番下の座標を見て決める)。
        • (斜体なら)Panose/文字の形状を「斜体/箱入」に。
        • TTF名のファミリー名、スタイル名、フルネーム等を適切に変更。
    3. ASCII文字(等Inconsolataから持ってきたい範囲)のグリフをカットして削除する。
    4. Inconsolata側でコピーしたグリフを貼り付ける(または、「エレメント」→「フォントの統合」で持ってきた方がトラブルが少ないかもしれない。少なくともアンカー情報はコピーだと引き継がれない模様。どちらを使うにせよすでにあるグリフには上書きされないので統合・コピーする前に取りこみたい箇所を消しておくこと)。
    5. フォントを出力する。「ファイル」→「フォントを出力」。 オプションはほとんどデフォルトだが、一応私が出力したときのを書いておく:
      • ヒント
        • Flexヒント
      • TrueType ヒント
      • PostScriptグリフ名
      • TrueType
      • オプション
        • OpenTypeの仕様
        • 旧来のkernテーブル(不要かも?)
        • Windows Compatible kern(不要かも?)
        • Prefer native kerning(不要かも?)
  3. 出力したフォントの平均文字幅情報を書き替える。平均文字幅(xAveCharWidth)はFontForgeが勝手に計算してしまうので設定で変更できない。半角文字の幅である1024にしたいのでバイナリエディタで書き替える。
    1. バイナリエディタで開いて先頭付近のOS/2という文字列を見つける。
    2. OS/2の後の4バイトはチェックサムなので無視し、次の4バイトが示すオフセットを読み取り(ビックエンディアン)、ファイル先頭からそのオフセットの場所に飛ぶ。
    3. 最初の2バイトはバージョン番号なので無視し、次の2バイトを1024(04 00)に書き替える。

メモ:

  • ○や■の記号などInconsolataから持ってきたくないものもあるのでASCIIのグリフだけInconsolataからMeiryoKe_Consolasにコピーした。
  • 変形は極力しない方が良い(特にMeiryoKe側は)。ヒント情報がどうなるか分からないので。
  • Inconsolataのiやjは他のグリフを参照しているので、「参照を解除」してから移すこと。でないとiやjの上ポチがずれた位置に出てしまう。参照先であるuni0307もコピーする手もあるかもしれない。
  • WinDescentを少し増やさないとjの下が切れてしまう。座標を見て 360 400にした。一般情報の「深さ」は変えなくて大丈夫。
  • 「エレメント」→「フォント情報」→「OS/2」→「Panose」→「幅の比率」が「等幅」だと全角文字の後に空白が空いてしまう。FontForgeのバグだそう(FontForgeで生成した日本語TrueTypeフォント文字幅広すぎ対策 - itouhiroはてなブログ)なので「等幅」のまま出力してxAvgCharWidthを別途書き替える。「任意」にすると一見問題が解決するように見えるが、フォントサイズが奇数のときに全角の幅が半角のぴったり二倍にならなくなる。例えば全角の幅が19pxにときに半角の幅が10pxになってしまう。「等幅」で出力して後でxAvgCharWidthを書き替えるとこの問題は起きない(フォントサイズ19pxのとき全角の幅が20pxになる)。
  • Emacsで使うとフレームの横幅が異様に大きくなってしまうことがある。(default-font-width)が大きな値を返してくる。これもxAvgCharWidthの問題。OS/2テーブルのxAvgCharWidthを書き替える必要がある。FontForgeは勝手に計算して出力するので設定で変更できない。
  • xAvgCharWidthの書き替え方:

    1. TTFファイルの中でOS/2テーブルの位置は、ファイル先頭付近の「OS/2」と書いてある所の直後を見れば分かる。「OS/2」の直後には32ビット整数(ビックエンディアン)でチェックサム、オフセット、サイズと続く。手元のファイルでは、オフセットは1D8hや1C8h等であった。このオフセットはファイル先頭からOS/2テーブル先頭までのバイト数。
    2. OS/2テーブル先頭からバージョン(uint16)、xAvgCharWidth(int16)と続く。手元のファイルでは xAveCharWidthは1959(7a7h)だった。これを1024(400h)に書き替える。

    (参考: OpenTypeフォント: vanillaの日記, OpenTypeフォントの続き(5)・・・OS/2テーブル: vanillaの日記, OS/2 - OS/2 and Windows metrics table (OpenType 1.8.4) - Typography | Microsoft Docs)

  • ヒント命令はコピーでもフォントの統合でも引き継げないのでInconsolataの分は諦める。大人しく消してしまった方が良い。各グリフのヒント命令(glyfテーブル)はフォント全体(fpgmテーブル)で定義している関数を呼び出したりしているので単純にグリフをコピーしても正しく動作しない。頑張って解析すれば統合できるかもしれないが大変。FDEF命令で定義する関数の番号をずらせれば統合できるかもしれない。CALLの前が必ずしも番号のPUSHとは限らないみたいなので慎重に追っていく必要がありそう。関連する他のテーブルにも注意を払う必要がありそう。(参考: OpenTypeフォントの続き(9)・・・インストラクション: vanillaの日記, TrueType Instruction Set (OpenType 1.8.4) - Typography | Microsoft Docs)
  • ハイフンマイナス(U+002D)がなぜか太く表示されてしまうようなので 縦110%拡大したら小さなフォントサイズでチルダの波形が波に見えない場合があったので、そのグリフだけはヒント情報とヒント命令を生成した。他の文字はヒント情報をFontForgeで自動生成すると文字の形や幅がおかしくなることがあった。
  • ベースラインが揃っていないのが気になる。元々メイリオ自体漢字や仮名は少しベースラインより下にはみ出してる。調整したいがメイリオ側を変形するのはヒンティングが大丈夫が不安。Inconsolataをベースラインより下げてしまうのも手かも?
  • なぜかInconsolata部分の高さが微妙に低い(ヒントがないせい?)ので縦方向に少し(110%ほど)拡大した。
2021-07-17 ,

指定した任意のorgファイルへエントリー(サブツリー)を移動する方法

org-refile (C-c C-w) でorg-modeのエントリー(サブツリー)を他の場所へ移動できるのだけど、デフォルトだとカレントバッファ内の第1レベルのエントリ内へしか移動できない。

移動先の候補は org-refile-targets 変数で変更できるのだけど、その場で好きなファイルを指定してその中へ移動というのは出来ないように見える。これまで org-refile-targets には (org-agenda-files :maxlevel . 2) を指定していたのでアジェンダ処理対象ファイルへは移動できるのだけど、特定のプロジェクト専用のorgファイル(あちこちのディレクトリにある)へ移動したい場合には困る。

やりたいことはシンプルに、C-c C-wの後にファイルを指定して、次に移動先のエントリーを指定したいだけなのだけど。

変数 org-refile-use-outline-path や org-outline-path-complete-in-steps を使うと最初にファイルを指定できるようにもなるけれど、やっぱり org-refile-targets の制限を受けてしまう。

これまではエントリーをカット&ペーストしていたのだけど、やはり C-c C-w で出来た方が便利だ。ちなみにペーストは任意のエントリーの子としてペーストする機能が見当たらずペーストしてからレベルを調整する必要があって手間がかかる。C-c C-x C-y (org-paste-special) は現在の場所と同一レベルにペーストするので M-right で一段下げなければならない。

どうにもならないのかと諦めかけたところ、次の記事を見た。

目にとまったのはこの部分。

org-refile-targets lets you control which files to consider, e.g.:

nil  ;; only the current file
'((org-agenda-files :maxlevel . 2)) ;; all agenda files, 1st/2nd level
'((org-files-list :maxlevel . 4)) ;; all agenda and all open files
'((my-org-files-list :maxlevel . 4)) ;; all files returned by `my-org-files-list'

org-refile-targets には任意の関数を指定できるのであった。その関数は移動候補となるファイル名のリストを返すことになっている。

(org-files-list) 関数はアジェンダファイルと現在開いているorgファイルを返すので、それを指定すればアジェンダファイルの他に現在開いているorgファイルへも移動できるようになる。

完全に任意のファイルとはいかないが、先にファイルを開いておけばいいだけなので当分これで我慢しておこうと思う。