Author Archives: AKIYAMA

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でいいやと思いました。