Monthly Archives: 8月 2021

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のお節介な補完を抑制する

(2022-08-15追記:CompanyからCorfuへ移行しました)

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を押す習慣を付けなければなりません。あとモードによって選択状態から始めているのは統一感が無くてあまり良くなかったかもしれませんね。