Author Archives: misohena

2022-02-23 ,

Emacsから気象庁の天気予報にアクセスする

Emacs上で気象庁の天気予報を取得するものを作りました。

misohena/el-jma: Emacs Interface for Japan Meteorological Agency Data

例によってorg-modeのagenda上にも天気を表示できます。

org-modeのAgenda Viewに表示された天気予報
図1: org-modeのAgenda Viewに表示された天気予報

気象庁の提供するデータについては次のTweetを始め色々な資料がありました。

実際に天気予報を取得するにあたって、一番理解が難しかったのが予報区に関する知識でした。東京の天気を知りたかったら東京のコードを使ってデータを取得するだけなんじゃないの? と思いきや、事情はもう少し複雑でした。

天気予報のデータは概ね都道府県単位に近い区分(府県予報区)毎に取得(ダウンロード)できるのですが、そのデータの中はさらに場所が細分化されています。東京であれば、東京地方、伊豆諸島北部、伊豆諸島南部、小笠原諸島といった具合です。このサブエリアを一次細分区域と呼びます。東京都と言ってもこれらを一緒くたにはできませんから当然な話です。

それで済むかと思いきや、気温のデータだけ別のエリアコードが使われています。調べたところ、最低気温と最高気温の予報はアメダス観測所の地点のものが発表されており、そのアメダス観測所の名前とコードが使われています。東京の場合は、東京、大島、八丈島、父島といった具合です。それぞれ一次細分区域と対応していますが、一つの一次細分区域に複数のアメダス観測所がある府県もあります。

これで終わりかと思いきや、週間天気予報はまた別なエリア分けが使われています。気象庁|週間天気予報の解説には次のような記述があります。

「府県週間天気予報」は原則として府県予報区ごとに予報していますが、東京都と鹿児島県では、常に予報区内の区域を細分して予報しています(東京都は、東京地方と伊豆諸島と小笠原諸島に、鹿児島県は、鹿児島県(奄美地方除く)と奄美地方に、それぞれ細分)。また、季節を限定して区域を細分している予報区もあります。

基本的には週間予報は府県予報区単位の荒い予報ということになっているようですが、地形的・季節的な要因で多少細分化されています。

この週間天気予報で使用する区分けは府県予報区とも一次細分区域とも異なります。府県予報区や一次細分区域と同等になる区域は同じ名前やコードが使われていますが、どちらとも一致しない区域は独自の名前やコードが使われています。この区分けには明確な用語の定義が見当たらなかったので、とりあえず「週間予報区域」と呼ぶことにしました(今から思えば「府県週間予報区」くらいだったかなとも思いますが)。JSONの中ではweekと書いてあることが多いです。

一次細分区域と異なる範囲になるのでその下の選択すべきアメダス観測所も変わってきます。ただ、この週間予報区域に対応するアメダス観測所は一つのみに限定されるようです。

というわけで、場所を指定するのに五つもコードを指定しなければならなくなっていますがお許しください。一応 jma-forecast-area-read コマンドでコードを調べられるようになってます。市区町村から調べられればより良いのですが、その辺りは今後の課題と言うことで。

天気予報の取得方法については https://github.com/misohena/el-jma/blob/main/docs/how-to-get-jma-forecast.org にもメモを残しておきました。具体的なデータ例も https://github.com/misohena/el-jma/tree/main/example-data にあります。

2022-02-22

2022年春の新番組

開始日 タイトル 時刻 配信元
× 2022/01/04(火) イロドリミドリ 01:05 dアニメ
2022/01/05(水) テイコウペンギン -   -
× 2022/01/05(水) ハコヅメ~交番女子の逆襲~ 00:00 dアニメ
2022/01/05(水) リアデイルの大地にて 23:00 dアニメ
2022/01/05(水) 東京24区 12:00 dアニメ
× 2022/01/07(金) 終末のハーレム 01:30 dアニメ
2022/01/07(金) スローループ 22:30 dアニメ
2022/01/07(金) ジョジョの奇妙な冒険 第6部 ストーンオーシャン -   -
× 2022/01/07(金) ドールズフロントライン 12:00 dアニメ
2022/01/07(金) からかい上手の高木さん3 02:00 dアニメ
× 2022/01/07(金) CUE! 02:55 dアニメ
2022/01/08(土) その着せ替え人形は恋をする 00:30 dアニメ
2022/01/08(土) 明日ちゃんのセーラー服 01:00 dアニメ
2022/01/08(土) 現実主義勇者の王国再建記 第2期 -   -
× 2022/01/08(土) 怪人開発部の黒井津さん 02:30 dアニメ
2022/01/08(土) 失格紋の最強賢者 23:30? ABEMA
2022/01/09(日) 時光代理人-LINK CLICK- 22:30 Amazon
× 2022/01/09(日) 錆色のアーマ-黎明- 22:30 dアニメ
× 2022/01/09(日) 薔薇王の葬列 23:00 dアニメ
× 2022/01/09(日) フットサルボーイズ!!!!! 23:30 ABEMA
2022/01/09(日) 進撃の巨人 The Final Season 第2クール -   -
2022/01/09(日) 王子の本命は悪役令嬢 01:00 dアニメ
2022/01/10(月) ガル学。Ⅱ~Lucky Stars~ -   -
2022/01/10(月) トライブナイン 22:30 dアニメ
2022/01/10(月) 範馬刃牙(バキ 続編) -   -
× 2022/01/06(木) 最遊記RELOAD -ZEROIN- 00:00 dアニメ
2022/01/10(月) プリンセスコネクト!Re:Dive Season2 -   -
× 2022/01/10(月) 幻想三國誌 天元霊心記 12:00 dアニメ
2022/01/11(火) 賢者の弟子を名乗る賢者 00:00 dアニメ
2022/01/10(月) 錆喰いビスコ 00:30 ABEMA
2022/01/11(火) 天才王子の赤字国家再生術 ? ABEMA
2022/01/11(火) 異世界美少女受肉おじさんと ? ABEMA
2022/01/12(水) オンエアできない! 00:00 dアニメ
2022/01/12(水) BABY-HAMITANG -   -
2022/01/12(水) 漢化日記 -   -
2022/01/12(水) 殺し愛 23:30 dアニメ
2022/01/12(水) 平家物語 12:00 dアニメ
× 2022/01/13(木) オリエント 01:30 ABEMA
2022/01/13(木) ありふれた職業で世界最強 2nd season -   -
2022/01/13(木) あたしゃ川尻こだまだよ 01:30? Gyao!
2022/01/14(金) ヴァニタスの手記 第2クール -   -
2022/01/14(金) ニンジャラ 12:00 dアニメ
2022/01/16(日) 佐々木と宮野 00:00 dアニメ
2022/01/20(木) お昼のショッカーさん      
2022/01/21(金) テイルズ オブ ルミナリア -The Fateful Crossroad-      
2022/01/30(日) リーマンズクラブ 12:00 dアニメ
2022/01/28(金) 地球外少年少女      
2022/01/30(日) 永遠の831      
2022/01/–(-) 闇芝居 十期      
2021/02/09(水) デリシャスパーティ♡プリキュア 00:00 dアニメ
2022-01-22 ,

TermuxでEmacsを使うためにやったこと

Termuxのインストール

次の方法があります。

必ずしもF-Droidアプリは必要なくTermuxパッケージのページから直接apkをダウンロードすることもできます。GitHubからも(Debug版ですが)apkファイルをダウンロードできます。ビルドはそれほど難しくは無いと思います。

署名の問題があるので別の供給元から入手したものは共存できません。切り替えるにはいったんアンインストールが必要です(必要に応じてバックアップすること)。

私は最終的に次の二つを自分でビルドしてインストールしました。

ソースをgit cloneで入手し、既にPCに入っていたAndroid Studioでプロジェクトを開いてRunボタンを押したらすんなり成功しました。

Termuxの分かりにくそうな操作

ドロワー

画面左端から右へスワイプすると、ドロワー(サイドバー)が出てきます。

KEYBOARDを 長押し すると、下部ツールバーの表示状態を切り替えられます。

ツールバーの左スワイプ

下部ツールバーを左スワイプすると、Text Input Viewが出てきます。ここはInput Methodが効くので日本語を入力するのに使えます。私は上部で入力できるように改造したので今はほぼ使っていません。

Volume Up+ソフトキーボード

色々ショートカットが割り当てられています。

Touch Keyboard - Termux Wiki

キーボードまわりの設定

先日書きました。

Termuxでハードウェアキーボードからスムーズに日本語入力したい | Misohena Blog

  • .termux/termux-properties
    • soft-keyboard-toggle-behaviour = enable/disable
    • disable-hardware-keyboard-shortcuts = true
    • bell-character = ignore
    • ctrl-space-workaround = true
  • キーレイアウト問題(CtrlとCaps、BACKSLASH、無変換)
  • Input Methodがらみは独自ビルドで解決

Emacsのインストール

pkg upgrade
pkg install emacs
emacs

普通に起動します!

SSHとGitのインストール

自分の設定を持ってくるためにsshとgitをインストールしました。

pkg install openssh
pkg install git

鍵を作ってサーバに設定。

ssh-agentは .bashrcに . source-ssh-agent を追加するだけ。簡単!

(https://www.reddit.com/r/termux/comments/b8il5p/sshagent_want_to_start_again_when_new_terminal_is/ より)

Emacsの設定を持ってくる

Gitで自分の設定を持ってきて……

git clone --recursive ssh://hogehoge/my-emacs-config.git
cp my-emacs-config/.emacs.el ~/.emacs.d/init.el
emacs

エラー箇所を修正。最近色々判定をサボっていたので少し出ましたが、それほど多くはありませんでした。(display-graphic-p)はnilなのでそのあたりの機能はごっそり無効化。

Termux上で動いているかの判別は次のようにしました。

(setq my-termux-p (not (null (executable-find "termux-info"))))

How to detect in a BASH script that I'm in Termux? : termux によればtermux-toolsパッケージは常に存在すると考えて良いみたいです。他にもパスにcom.termuxという特徴的な文字列が含まれているのでそれをチェックするのも(コマンドを検索するより速くて)良さそうです。

パッケージのインストール。

M-x package-refresh
M-x package-install-selected-packages
C-x C-c
emacs

その他Emacsの修正

emacsclientが動かない

TMPDIR"/data/data/com.termux/files/usr/tmp" なのに server-socket-dir のデフォルト値が "/data/data/com.termux/files/usr/var/run/<uid>" になっているのが原因みたいです。次のようにします。

(setq server-socket-dir
      (and (featurep 'make-network-process '(:family local))
           (format "%s/emacs%d" (or (getenv "TMPDIR") "/data/data/com.termux/files/usr/var/run") (user-uid))))

(emacsclient can not find server socket · Issue #4230 · termux/termux-packagesより)

パッチが当たらなくなったのかな?

browse-urlでブラウザが開かない

(setq browse-url-browser-function 'browse-url-xdg-open)

(How can I make Emacs function 'browse-url-at-point work on a tablet running Android? - Emacs Stack Exchangeより)

browse-url-xdg-openxdg-open コマンドを使います。Termuxから使えるようです。 termux-open-url というコマンドもあります。

Androidのクリップボードと連携する

事前にtermux-apiアプリの導入が必要です。私はtermux-appを自分でビルドしてしまったので、こちらも自分でビルドする必要がありました。

その上でTermux内にもパッケージのインストールが必要です。

pkg install termux-api

そうすると次の二つのコマンドが使えるようになります。

termux-clipboard-set <text>
termux-clipboard-get

Emacsからは xclip というパッケージを導入するとこれらのコマンドを使ってくれるようになります。

M-x package-install xclip
M-x xclip-mode

(Copy/paste between apps and Emacs in Termux · Issue #6266 · termux/termux-packagesより)

sdcardの読み書き

termux-setup-storage を実行し権限を許可。すると ~/storage が追加され、 /sdcard にもアクセスできるようになりました。

その他インストールしたツール

  • pkg install ripgrep : Emacsから検索の際に使用
  • pkg install wget : ファイルをダウンロードするときに使用
2022-01-21 , ,

Termuxの曖昧幅文字の全角化

TermuxでのEmacs環境の整備を続けています。

Termux 上で Emacs を普段使いする為の設定 - Qiita

を読んでいて、曖昧幅なんてものもあったなぁ……と、手元のorgファイルの表の部分を見てみると……ありましたありました。メチャクチャ崩れています。○△×とか書く欄が表にあったらもうダメですね。どうしたら良いんだろう。

と、上のページをよく読むと

Termux が純粋な Linux アプリであれば、wcwidh()のハックにより解消出来ますが、Termux は Java アプリなのでハックの仕方が分かりません…。 (ソースを修正すべき箇所は多分ここです→ https://github.com/termux/termux-app/blob/master/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java)

おおお、そんな場所があるとは。幸いTermuxはビルドしたばかりですから直せるかもしれません。

hamano/locale-eaw: East Asian Ambiguous Width問題と絵文字の横幅問題の修正ロケール

ええと、このページからたどれるeaw.elをダウンロードして……ふむふむ、曖昧幅の文字と全角確定の文字を合わせて、その範囲のリストを作れば良さそうですね。

(let* (
       ;; From https://github.com/hamano/locale-eaw/blob/master/eaw.el
       (east-asian-ambiguous
        '(
          #x00A1 ; Po         INVERTED EXCLAMATION MARK
          #x00A4 ; Sc         CURRENCY SIGN
          #x00A7 ; Po         SECTION SIGN
          ;;...略...
          ))
       ;; From WIDE_EASTASIAN in termux-app/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.java
       (east-asian-wide
        '(
          (#x01100 . #x0115f) ;; // Hangul Choseong Kiyeok  ..Hangul Choseong Filler
          (#x0231a . #x0231b) ;; // Watch                   ..Hourglass
          ;;...略...
          (#x30000 . #x3fffd) ;; // (nil)                   ..(nil)
          ))
       (full-width-chars
         (sort
          (append
           (cl-loop for range in east-asian-wide
                    nconc (cl-loop for c from (car range) to (cdr range)
                                   collect c))
           east-asian-ambiguous)
          #'<)))

  (let ((p full-width-chars))
    (while p
      (let ((first (car p))
            (last (progn
                    (while (and (cadr p)
                                (or
                                 (= (car p) (cadr p)) ;;Omit duplication
                                 (= (1+ (car p)) (cadr p))))
                      (setq p (cdr p)))
                    (car p))))
        (insert (format "{0x%x, 0x%x},\n" first last))
        (setq p (cdr p))))))

結果はこんな感じ。

{0xa1, 0xa1},
{0xa4, 0xa4},
{0xa7, 0xa8},
{0xaa, 0xaa},
{0xad, 0xae},
{0xb0, 0xb4},
{0xb6, 0xba},
{0xbc, 0xbf},
{0xc6, 0xc6},
{0xd0, 0xd0},
{0xd7, 0xd8},
{0xde, 0xe1},
{0xe6, 0xe6},
{0xe8, 0xea},
{0xec, 0xed},
{0xf0, 0xf0},
{0xf2, 0xf3},
{0xf7, 0xfa},
{0xfc, 0xfc},
{0xfe, 0xfe},
{0x101, 0x101},
{0x111, 0x111},
{0x113, 0x113},
{0x11b, 0x11b},
{0x126, 0x127},
{0x12b, 0x12b},
{0x131, 0x133},
{0x138, 0x138},
{0x13f, 0x142},
{0x144, 0x144},
{0x148, 0x14b},
{0x14d, 0x14d},
{0x152, 0x153},
{0x166, 0x167},
{0x16b, 0x16b},
{0x1ce, 0x1ce},
{0x1d0, 0x1d0},
{0x1d2, 0x1d2},
{0x1d4, 0x1d4},
{0x1d6, 0x1d6},
{0x1d8, 0x1d8},
{0x1da, 0x1da},
{0x1dc, 0x1dc},
{0x251, 0x251},
{0x261, 0x261},
{0x2c4, 0x2c4},
{0x2c7, 0x2c7},
{0x2c9, 0x2cb},
{0x2cd, 0x2cd},
{0x2d0, 0x2d0},
{0x2d8, 0x2db},
{0x2dd, 0x2dd},
{0x2df, 0x2df},
{0x391, 0x3a1},
{0x3a3, 0x3a9},
{0x3b1, 0x3c1},
{0x3c3, 0x3c9},
{0x401, 0x401},
{0x410, 0x44f},
{0x451, 0x451},
{0x1100, 0x115f},
{0x2010, 0x2010},
{0x2013, 0x2016},
{0x2018, 0x2019},
{0x201c, 0x201d},
{0x2020, 0x2022},
{0x2024, 0x2027},
{0x2030, 0x2030},
{0x2032, 0x2033},
{0x2035, 0x2035},
{0x203b, 0x203b},
{0x203e, 0x203e},
{0x2074, 0x2074},
{0x207f, 0x207f},
{0x2081, 0x2084},
{0x20ac, 0x20ac},
{0x2103, 0x2103},
{0x2105, 0x2105},
{0x2109, 0x2109},
{0x2113, 0x2113},
{0x2116, 0x2116},
{0x2121, 0x2122},
{0x2126, 0x2126},
{0x212b, 0x212b},
{0x2153, 0x2154},
{0x215b, 0x215e},
{0x2160, 0x216b},
{0x2170, 0x2179},
{0x2189, 0x2189},
{0x2190, 0x2199},
{0x21b8, 0x21b9},
{0x21d2, 0x21d2},
{0x21d4, 0x21d4},
{0x21e7, 0x21e7},
{0x2200, 0x2200},
{0x2202, 0x2203},
{0x2207, 0x2208},
{0x220b, 0x220b},
{0x220f, 0x220f},
{0x2211, 0x2211},
{0x2215, 0x2215},
{0x221a, 0x221a},
{0x221d, 0x2220},
{0x2223, 0x2223},
{0x2225, 0x2225},
{0x2227, 0x222c},
{0x222e, 0x222e},
{0x2234, 0x2237},
{0x223c, 0x223d},
{0x2248, 0x2248},
{0x224c, 0x224c},
{0x2252, 0x2252},
{0x2260, 0x2261},
{0x2264, 0x2267},
{0x226a, 0x226b},
{0x226e, 0x226f},
{0x2282, 0x2283},
{0x2286, 0x2287},
{0x2295, 0x2295},
{0x2299, 0x2299},
{0x22a5, 0x22a5},
{0x22bf, 0x22bf},
{0x2312, 0x2312},
{0x231a, 0x231b},
{0x2329, 0x232a},
{0x23e9, 0x23ec},
{0x23f0, 0x23f0},
{0x23f3, 0x23f3},
{0x2460, 0x24e9},
{0x24eb, 0x254b},
{0x2550, 0x2573},
{0x2580, 0x258f},
{0x2592, 0x2595},
{0x25a0, 0x25a1},
{0x25a3, 0x25a9},
{0x25b2, 0x25b3},
{0x25b6, 0x25b7},
{0x25bc, 0x25bd},
{0x25c0, 0x25c1},
{0x25c6, 0x25c8},
{0x25cb, 0x25cb},
{0x25ce, 0x25d1},
{0x25e2, 0x25e5},
{0x25ef, 0x25ef},
{0x25fd, 0x25fe},
{0x2600, 0x27e5},
{0x27ee, 0x27ff},
{0x2b1b, 0x2b1c},
{0x2b50, 0x2b50},
{0x2b55, 0x2b59},
{0x2e80, 0x2e99},
{0x2e9b, 0x2ef3},
{0x2f00, 0x2fd5},
{0x2ff0, 0x2ffb},
{0x3000, 0x303e},
{0x3041, 0x3096},
{0x3099, 0x30ff},
{0x3105, 0x312f},
{0x3131, 0x318e},
{0x3190, 0x31e3},
{0x31f0, 0x321e},
{0x3220, 0x4dbf},
{0x4e00, 0xa48c},
{0xa490, 0xa4c6},
{0xa960, 0xa97c},
{0xac00, 0xd7a3},
{0xf900, 0xfaff},
{0xfe10, 0xfe19},
{0xfe30, 0xfe52},
{0xfe54, 0xfe66},
{0xfe68, 0xfe6b},
{0xff01, 0xff60},
{0xffe0, 0xffe6},
{0xfffd, 0xfffd},
{0x16fe0, 0x16fe4},
{0x16ff0, 0x16ff1},
{0x17000, 0x187f7},
{0x18800, 0x18cd5},
{0x18d00, 0x18d08},
{0x1b000, 0x1b11e},
{0x1b150, 0x1b152},
{0x1b164, 0x1b167},
{0x1b170, 0x1b2fb},
{0x1f000, 0x1f02b},
{0x1f030, 0x1f093},
{0x1f0a0, 0x1f0ae},
{0x1f0b1, 0x1f0bf},
{0x1f0c1, 0x1f0cf},
{0x1f0d1, 0x1f0f5},
{0x1f100, 0x1f1ad},
{0x1f1e6, 0x1f202},
{0x1f210, 0x1f23b},
{0x1f240, 0x1f248},
{0x1f250, 0x1f251},
{0x1f260, 0x1f265},
{0x1f300, 0x1f6d7},
{0x1f6e0, 0x1f6ec},
{0x1f6f0, 0x1f6fc},
{0x1f700, 0x1f773},
{0x1f780, 0x1f7d8},
{0x1f7e0, 0x1f7eb},
{0x1f800, 0x1f80b},
{0x1f810, 0x1f847},
{0x1f850, 0x1f859},
{0x1f860, 0x1f887},
{0x1f890, 0x1f8ad},
{0x1f8b0, 0x1f8b1},
{0x1f900, 0x1f978},
{0x1f97a, 0x1f9cb},
{0x1f9cd, 0x1fa53},
{0x1fa60, 0x1fa6d},
{0x1fa70, 0x1fa74},
{0x1fa78, 0x1fa7a},
{0x1fa80, 0x1fa86},
{0x1fa90, 0x1faa8},
{0x1fab0, 0x1fab6},
{0x1fac0, 0x1fac2},
{0x1fad0, 0x1fad6},
{0x1fb00, 0x1fb92},
{0x1fb94, 0x1fbca},
{0x1fbf0, 0x1fbf9},
{0x20000, 0x2fffd},
{0x30000, 0x3fffd},

これを termux-app/terminal-emulator/src/main/java/com/termux/terminal/WcWidth.javaWIDE_EASTASIAN の定義と差し替えます。

そしてビルドして実行してみると……ちゃんと綺麗に揃った幅で表示されました!

Emacsの方は (set-language-environment "Japanese") すればちゃんと幅が2になっているみたいですね。昔のEmacsは自分で設定しないといけなくて、でもいつの間にか設定しなくても良くなってた記憶があります。

フォントの方は前に作ったキメラのようなフォントを適用したところ問題なく表示されました。決して綺麗なフォントではありませんが、まぁ、ある意味カッチリしているというべきか……。ついline-spacingを指定しようとしてしまいましたが効くわけがありません(笑)

何はともあれ予想されていたもう一つの方法でも解決できたということで。

2022-01-21 , ,

TermuxのrcloneでDropboxと双方向同期

引き続きTermuxの環境構築をしています。

Dropboxが使いたかったのですが当然のことながら公式アプリで同期、なんてできません。検索したらrcloneというツールでクラウドストレージとやりとりできるそうです。

How to access dropbox files via termux? : termux

rcloneは pkg install rclone でインストールできて、設定も対話形式で簡単です。

ただ、双方向同期のような機能はありません。ああいうのはちゃんとやろうとすると結構大変ですからね。

代わりと言っては何ですが、diffとpatchで同期する仕組みを作ってみました。

  1. まずリモートの同期対象ディレクトリをrcloneでローカルの最新リモート置き場に取りこみます。
  2. 前回取りこんだ内容とdiffを取って、それをローカルのワーキングコピーにpatchします。
  3. うまく当たらないハンクがあったらここで終了。手動で解決してもらいます。
  4. うまく当たったら、最新のリモートとローカルのワーキングコピーのdiffを取って表示し、ユーザーに確認を促します。
  5. 問題ないようなら最新のリモートファイルを置く場所にローカルのワーキングコピーをコピーして、それをrcloneでアップロードします。

この作業の間に別PCがリモートを書き替えたら……THE ENDですw。

あとバイナリファイルとかもどうなるか知りません。

限定されたディレクトリで使う分には大丈夫でしょう。Dropboxなら消してしまってもすぐに気がつけば復元できるはずですしね。

#!/bin/bash

DROPBOXDIR=~/Dropbox
SYNCTARGET=sync-test-dir

set -eu

# Ensure Current Directory
cd "$(dirname "$0")"

# Pull Remote Files (Destructive)
# _remote(old) => _remote.before_pull
# remote => _remote
rm -fR _remote.before_pull
if [ -e _remote ]; then
    cp -a _remote _remote.before_pull
fi
rclone sync dropbox:${SYNCTARGET} _remote/${SYNCTARGET}

# Create Remote Diff (Non Destructive)
set +e
diff -urN _remote.before_pull/${SYNCTARGET} _remote/${SYNCTARGET} >_remote.diff
REMOTE_DIFF_STATUS=$?
if [ $REMOTE_DIFF_STATUS -ge 2 ] ; then
    exit 2
fi
set -e
#rm -fR _remote.before_pull

# Apply Remote Changes to Local (Destructive)
# _remote(new) => _last_pull
# (local) => _local.before_remote_change
# (remote changes) => (local)
if [ $REMOTE_DIFF_STATUS -eq 0 ] ; then
    echo "No Remote changes."
else
    # Save Last Pull State
    rm -fR _last_pull
    cp -a _remote _last_pull

    # Backup Local
    rm -fR _local.before_remote_change/${SYNCTARGET}
    if [ -e ${DROPBOXDIR}/${SYNCTARGET} ] ; then
        mkdir -p _local.before_remote_change/${SYNCTARGET}
        cp -a ${DROPBOXDIR}/${SYNCTARGET} _local.before_remote_change/${SYNCTARGET}
    fi

    # Apply Remote Changes to Local
    echo "Apply remote changes."
    mkdir -p ${DROPBOXDIR}/${SYNCTARGET}
    patch --set-time -p2 -d ${DROPBOXDIR}/${SYNCTARGET} < _remote.diff
fi

# Check Local Changes (Non Destructive)
set +e
diff -urN _remote/${SYNCTARGET} ${DROPBOXDIR}/${SYNCTARGET} >_local.diff
LOCAL_DIFF_STATUS=$?
if [ $LOCAL_DIFF_STATUS -ge 2 ] ; then
    exit 2
fi
if [ $LOCAL_DIFF_STATUS -eq 0 ] ; then
    echo "No local changes."
    exit $LOCAL_DIFF_STATUS
fi
# Confirm to Upload Local Changes
echo "You have local changes."

function confirm_local_changes {
    while true; do
        read -n1 -p "Upload? (Y/n/=): " yn
        case $yn in
            [Yy])
                return 0
                ;;
            [Nn])
                return 1
                ;;
            [=])
                emacsclient _local.diff
                ;;
        esac
    done
}

confirm_local_changes
if [ $? -ne 0 ]; then
    echo "Not uploaded."
    exit 1;
fi

# Apply Local Changes (Destructive)
# _remote(new) => _remote.before_local_change
# (local)(new) => _remote
# (local)(new) => _last_pull
rm -fR _remote.before_local_change/${SYNCTARGET}
mkdir -p _remote.before_local_change/${SYNCTARGET}
mv _remote/${SYNCTARGET} _remote.before_local_change/${SYNCTARGET}
mkdir -p _remote/${SYNCTARGET}
cp -a ${DROPBOXDIR}/${SYNCTARGET} _remote
rm -fR _last_pull/${SYNCTARGET}
mkdir -p _last_pull/${SYNCTARGET}
cp -a ${DROPBOXDIR}/${SYNCTARGET} _last_pull

# Upload Local Changes 
rclone sync -i _remote/${SYNCTARGET} dropbox:${SYNCTARGET}

双方向同期でなくてもこのrcloneは結構便利ですね。ちょっとしたファイルのやりとりが気軽にできます。

2022-01-20 , ,

Termuxでハードウェアキーボードからスムーズに日本語入力したい

最近AndroidでEmacsが動いているのを見かけたので私も試してみました。

F-DroidからTermuxを入れて pkg install emacs だけでEmacsが動いて感激。普段使っているorg-modeをはじめ、あれもこれもほとんどそのまま動きます。楽しすぎる。思い切って普段使いの電話に入れたので、これでいつでもどこでもEmacsが使えます。

ソフトウェアキーボードで日本語も入力できました。日本語じゃないときはHacker's Keyboardに切り替えて使うのが便利です。

ハードウェアキーボードでの問題点

気を良くしてBluetoothキーボードを接続してみたのですが色々問題点が。それぞれ次のように対処しました。

Ctrl+Spaceが効かない
~/.termux/termux.propertiesctrl-space-workaround = true を追加。さらにAndroidの設定で物理キーボードのレイアウト候補を一つにする(複数あるとCtrl+Spaceが効いた上でレイアウトが変わるという…)。
CtrlとCaps Lockの入れ替え
106/109ハードウェアキーボード配列変更 (+親指Ctrl) [日本語配列] - Google Play のアプリ で解決。
Back Spaceの左のキーで¥(#xA5)が入力されてしまい\(#x5C)が入力できない
一時的に (keyboard-translate #xa5 #x5c) で回避しましたが、最終的には shiftrot/caps2ctrl を修正してビルド、インストールすることで解決。
Ctrl+Altで始まるキーの一部がTermuxに取られる
~/.termux/termux.propertiesdisable-hardware-keyboard-shortcuts = true を追加。

Input MethodのON/OFFをハードウェアキーボードから切り替える

その上で、Input Method(以下IM。私はATOKを使っています)がらみの問題が残りました。

  • ハードウェアキーボードからIM経由で(ツールバーの上の領域に直接)日本語が入力できない場合がある
  • ハードウェアキーボードからIMのON/OFFを切り替えられない (disable-hardware-keyboard-shortcuts = true にしたので)

下部ツールバーを左スワイプしてEditTextを出すとなぜか上の領域でも直接日本語が入力できるようになったりするのですが今度はそれを元の状態に戻せなくなったりと何かとうまくいきませんでした(細かい状況は再現して確認するのが面倒なので省きます)。AndroidのIMを捨てるという手もあるのかもしれませんが、かな入力をしていることもありますし学習の同期のこともありますからできればATOKが使いたいです。

幸いTermuxはソースコードが公開されているので調査したところ、次の場所に原因があることが分かりました。

terminal-view/src/main/java/com/termux/view/TerminalView.java

    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        // Ensure that inputType is only set if TerminalView is selected view with the keyboard and
        // an alternate view is not selected, like an EditText. This is necessary if an activity is
        // initially started with the alternate view or if activity is returned to from another app
        // and the alternate view was the one selected the last time.
        if (mClient.isTerminalViewSelected()) {
            if (mClient.shouldEnforceCharBasedInput()) {
                // Some keyboards seems do not reset the internal state on TYPE_NULL.
                // Affects mostly Samsung stock keyboards.
                // https://github.com/termux/termux-app/issues/686
                // However, this is not a valid value as per AOSP since `InputType.TYPE_CLASS_*` is
                // not set and it logs a warning:
                // W/InputAttributes: Unexpected input class: inputType=0x00080090 imeOptions=0x02000000
                // https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:packages/inputmethods/LatinIME/java/src/com/android/inputmethod/latin/InputAttributes.java;l=79
                outAttrs.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
            } else {
                // Using InputType.NULL is the most correct input type and avoids issues with other hacks.
                //
                // Previous keyboard issues:
                // https://github.com/termux/termux-packages/issues/25
                // https://github.com/termux/termux-app/issues/87.
                // https://github.com/termux/termux-app/issues/126.
                // https://github.com/termux/termux-app/issues/137 (japanese chars and TYPE_NULL).
                outAttrs.inputType = InputType.TYPE_NULL;
                // ★↑ここがTYPE_NULLだとTerminalViewにハードウェアキーボード+IMEで日本語が入力できない。
                // outAttrs.inputType =  InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL; に変更する。
            }
        } else {
            // Corresponds to android:inputType="text"
            outAttrs.inputType =  InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
        }

原因は outAttrs.inputType = InputType.TYPE_NULL という部分。これは受け付けるテキストの種類(単なるテキストなのか、数値なのか、パスワードなのか、URLなのか等々)を指定する部分です。TYPE_NULLを指定するとIMは変換を行わずキーイベントを垂れ流すようになるみたいです。なのでこの状態でハードウェアキーボードから日本語が入力できないのは当然です。

そこで、ここを下のelse部分と同じようにInputType.TYPE_CLASS_TEXTを設定するように変更したところ、ソフトウェアキーボードが出さえすれば直接上の領域(TerminalView。下部ツールバーではない部分)に日本語が入力できるようになりました(ただし確定前の文字列は表示されません(後述))。

次にキーボードからIMのON/OFFを切り替えられるようにする必要があります。日本語ではない部分で常に文字列を確定させながら打たなければならないのはかったるくて仕方がありませんからね。Ctrl+Alt+Kというショートカットが用意されていたのですが上で書いたように無効化してしまったので使えません。

そのために次のように変更しました。

app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java

     @Override
     public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
+        if (e.getAction() == KeyEvent.ACTION_DOWN &&
+            (e.getKeyCode() == KeyEvent.KEYCODE_HENKAN && KeyEvent.metaStateHasModifiers(e.getModifiers(), KeyEvent.META_CTRL_ON))){ //Ctrl+変換
+            onToggleSoftKeyboardRequest();
+            return true;
+        }
+        //この下に disable-hardware-keyboard-shortcuts で無効化される処理があるので、それ以前にCtrl+変換の処理を行うこと。
+
         if (handleVirtualKeys(keyCode, e, true)) return true;

Ctrl+変換でソフトウェアキーボードをトグルするようにしてみました。私は普段PCでもCtrl+変換でIMEをON/OFFしています。PC-98の名残です。CTRL+XFER。人によっては半角/全角に割り当ててもいいかもしれません。(2022-01-22追記:半角/全角に割り当てるとcaps2ctrlとの兼ね合いでかな/英字切り替えに不都合が出るかもしれません。caps2ctrlは結構いろんなキーを半角/全角にリマップしています)

さらに ~/.termux/termux.propertiessoft-keyboard-toggle-behaviour = enable/disable の指定が 必要 です。デフォルトのshowing/hidingだと隠すだけなので何かを入力するとすぐに復活してしまいます。

それと下部のツールバーですが、もし左スワイプして出てくるEditText(テキストボックス)にフォーカスがあるのならTABキーで上の領域にフォーカスを移せます。移せないようならキーボード操作で移せるように改造しようと思ったのですが不要でした。

後はビルドですが、たまたま手元のPCに入っていたAndroid Studioでgit cloneしてきたディレクトリのトップを開いてRunボタンを押したらすんなり成功してしまいました。超簡単です。デバッグビルドでも事足りるのかもしれませんが、一応自前の署名を施してリリース版のapkを作成し、実機にインストールしました。署名が変わるので他で入手したバージョンはいったんアンインストールする必要があります(必要ならデータをバックアップすること)。

これでハードウェアキーボードからIMをON/OFFできるようになりました。ONのときは日本語入力ができますし、OFFのときは煩わしい変換処理無しに直接任意のキーが入力できます。

ソフトウェアキーボードから変換確定せずに入力できなくなった

ここでいったん手を止めて外出したときにソフトウェアキーボードだけで操作してみようとしたのですが、ソフトウェアキーボードから変換せずに直接キーを入力できないことに気がつきました。Hacker's Keyboardですら文字の確定が必要になってしまいました。原因はもちろん inputType を TYPE_CLASS_TEXT にしたからです。ただ、これはHacker's Keyboardに何とかして欲しい気がします。このキーボードのコンセプト的にどんなときでも変換を通さない直接入力ができてしかるべきでしょう。他のアプリでもそういうシチュエーションはありそうです。というわけでHacker's Keyboardの設定を調べたら「入力候補を表示」というチェックボックスがありました。解除したところ確定の必要無しに直接キーを入力できるようになりました。

inputType を TYPE_CLASS_TEXT にしたのが原因なので、そこを修正するという手もあります。例えばCtrl+変換でinputType自体を切り替えてしまうというのはどうでしょう。

terminal-view/src/main/java/com/termux/view/TerminalView.java

@@ -28,6 +29,7 @@ import android.view.autofill.AutofillValue;
 import android.view.inputmethod.BaseInputConnection;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
 import android.widget.Scroller;
 
 import androidx.annotation.RequiresApi;
@@ -85,6 +87,8 @@ public final class TerminalView extends View {
 
     private static final String LOG_TAG = "TerminalView";
 
+    private int mInputType = InputType.TYPE_NULL;
+
     public TerminalView(Context context, AttributeSet attributes) { // NO_UCD (unused code)
         super(context, attributes);

@@ -279,7 +286,7 @@ public final class TerminalView extends View {
                 // https://github.com/termux/termux-app/issues/87.
                 // https://github.com/termux/termux-app/issues/126.
                 // https://github.com/termux/termux-app/issues/137 (japanese chars and TYPE_NULL).
-                outAttrs.inputType = InputType.TYPE_NULL;
+                outAttrs.inputType = mInputType;
             }
         } else {
             // Corresponds to android:inputType="text"
@@ -704,6 +717,18 @@ public final class TerminalView extends View {
             stopTextSelectionMode();
         }
 
+        // Ctrl+変換で inputType を切り替える。切り替え方法はTextViewのsetInputTypeメソッドを参考のこと。
+        if (event.getAction() == KeyEvent.ACTION_DOWN &&
+            (event.getKeyCode() == KeyEvent.KEYCODE_HENKAN && KeyEvent.metaStateHasModifiers(event.getModifiers(), KeyEvent.META_CTRL_ON))){ //Ctrl+螟画鋤
+            mInputType = mInputType == InputType.TYPE_NULL
+                ? (InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL)
+                : InputType.TYPE_NULL;
+            // Input Methodをリスタートする(参考: TextView.setInputType()のソース)
+            InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
+            if (imm != null){imm.restartInput(this);}
+            return true;
+        }
+

これでもハードウェアキーボードから入力方式を切り替えることができました。

ただ、ハードウェアキーボードが無しでもinputTypeを切り替える方法を別途用意しないと意味がありません(Volume Up+何かのキーに割り当てるとか? サイドバーにボタンを追加するとか?)。

それとこの方式はIMはずっと有効にしたまま(inputTypeを切り替えて)使うことになるので、ハードウェアキーボード使用時でIMが不要なときでも無駄に画面の下部をソフトウェアキーボードが占有してしまいます(ATOKの場合物理キーボード入力時は小さく折りたたまれるとはいえ)。ハードウェアキーボードから使うなら、どちらかと言えばIM自体をON/OFFしたほうが素直な気がしました。

この辺はどのソフトウェアキーボードを使いたいかにもよるでしょう。Hacker's Keyboard以外のソフトウェアキーボードで確定無し入力がしたいなら、やはりinputTypeを切り替えるUIを追加してTYPE_NULLにもできるようにすべきだと思います。

ハードウェアキーボードを考慮しないのであれば、上(TerminalView)にフォーカスがあるときは直接入力(TYPE_NULL)、下(ツールバーのEditText)にあるときはIM入力(EditTextは当然TYPE_CLASS_TEXTになっています)という使い分けができます(おそらくこれが本来の意図でしょう)。しかしハードウェアキーボードを使う場合は常に上にフォーカスを当てていたいのです。Ctrl+変換でフォーカスを移動するという手もあるのですが、下にフォーカスがあるときに日本語を入力して確定してからもう一度Enterをおさないといけなかったり、IMが有効でもできた(移動等の)操作がIMを切らないと(フォーカスを上に戻さないと)出来なくなったり、とにかく下で入力するのは不便なのでやめました。考えてみれば入力領域が二つに分かれているのも変な話で、やはり本来入力領域は一つにするのが筋でしょう。

(2022-01-22追記: ツールバー内のExtra Keys Rowにテキスト種別切り替えボタンがあると良いのでは? ソフトウェアキーボードのenable/disableの切り替えボタンなんかもここにあるといいかも)

確定前文字列のプレビュー

確定前の文字列が表示されない問題ですが、出なくても概ね打てるので私は許容範囲内なのですが、気になるなら自分でプレビューを実装すれば良いのだと思います。BaseInputConnectionのendBatchEditあたりをオーバーライドすれば確定前文字列の変化をフックできそうです。とりあえず getEditable().toString() をログ出力してみたら確定前文字列が取得できるところまで確認しました。ここで自前で追加したTextViewにでも表示してしまえばとりあえず見られるようにはなると思います。何ならツールバーのEditTextをプレビュー用に使ってしまう手もあるでしょう。ターミナル内のカーソル位置に他の文字と同じような見た目で表示するにはもっと色々調べる必要がありそうです。

この辺りをいじるとAndroidのIMまわりに少し詳しくなれそうですよ。

caps2ctrlのビルド

上で触れたCapsとCtrlを入れ替えるアプリですが、ソースはGitHubにあります。これもAndroid Studioで簡単にビルドできました。自分の好きなキーボードレイアウトにしたい人は是非ビルドしてみましょう。私はバックスラッシュと無変換キーを変更しました。

shiftrot/caps2ctrl: Add a physical keyboard layout for Android to replace Caps Lock with Ctrl.

Termuxといいcaps2ctrlといい、ソースコードが公開されていて本当に助かりますね。先ほどのinputTypeの所を見ると沢山のissueコメントがくっついています。その多くは開発者が自分では使わない環境の問題でしょうに。その対処だけでも私なら疲弊してしまいそうです。本当に頭が下がります。

現在の環境

org-modeが使えて楽しすぎる
スタバでドヤれそうな環境

キーボードはMOBO Keyboard2です。私は日本語配列Loveなので待ちに待った折りたたみキーボードです。(カバーを除く)キーボード部分だけならば278g。これなら登山にだって持って行けそう。

UserLAnd?

ところでUserLAndというアプリもあるらしいのですが、こちらはどうなのでしょう。Termuxよりも重いけれど問題は少ないという話を見かけましたが……。

2022-01-21追記:確定前文字列のプレビュー

あくまで一例ですが…… (v.0.118.0からの修正)

app/src/main/res/layout/activity_termux.xml (画面下部にプレビュー用のTextViewを追加)

@@ -101,6 +101,16 @@
 
     </RelativeLayout>
 
+    <TextView
+        android:id="@+id/activity_termux_composing_text"
+        android:visibility="gone"
+        android:layout_width="match_parent"
+        android:layout_height="24dp"
+        android:background="#202020"
+        android:textColor="#ffffff"
+        android:textSize="18dp"
+        />
+
     <View
         android:id="@+id/activity_termux_bottom_space_view"
         android:layout_width="match_parent"

terminal-view/src/main/java/com/termux/view/TerminalView.java (変換中の文字列をClientへ送信)

@@ -292,6 +298,12 @@ public final class TerminalView extends View {
 
         return new BaseInputConnection(this, true) {
 
+            @Override
+            public boolean endBatchEdit() {
+                mClient.onComposingTextChange(getEditable());
+                return super.endBatchEdit();
+            }
+
             @Override
             public boolean finishComposingText() {
                 if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) mClient.logInfo(LOG_TAG, "IME: finishComposingText()");
@@ -299,6 +311,7 @@ public final class TerminalView extends View {
 
                 sendTextToTerminal(getEditable());
                 getEditable().clear();
+                mClient.onComposingTextChange(getEditable());
                 return true;
             }
 
@@ -314,6 +327,7 @@ public final class TerminalView extends View {
                 Editable content = getEditable();
                 sendTextToTerminal(content);
                 content.clear();
+                mClient.onComposingTextChange(getEditable());
                 return true;
             }
 

terminal-view/src/main/java/com/termux/view/TerminalViewClient.java (Clientへ送信するためのインタフェースを追加)

@@ -1,5 +1,6 @@
 package com.termux.view;
 
+import android.text.Editable;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.ScaleGestureDetector;
@@ -59,6 +60,8 @@ public interface TerminalViewClient {
     boolean readFnKey();
 
 
+    void onComposingTextChange(Editable text);
+
 
     boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
 

termux-shared/src/main/java/com/termux/shared/terminal/TermuxTerminalViewClientBase.java (Clientのデフォルトの実装を追加)

@@ -1,5 +1,6 @@
 package com.termux.shared.terminal;
 
+import android.text.Editable;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 
@@ -78,6 +79,11 @@ public class TermuxTerminalViewClientBase implements TerminalViewClient {
     }
 
 
+    @Override
+    public void onComposingTextChange(Editable text)
+    {
+    }
+
 
     @Override
     public boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session) {

app/src/main/java/com/termux/app/terminal/TermuxTerminalViewClient.java (クライアントは送られてきた文字列をTextViewに表示。後は表示非表示の切り替えなど)

@@ -10,6 +10,7 @@ import android.content.Intent;
 import android.media.AudioManager;
 import android.net.Uri;
 import android.os.Environment;
+import android.text.Editable;
 import android.text.TextUtils;
 import android.view.Gravity;
 import android.view.InputDevice;
@@ -18,6 +19,7 @@ import android.view.MotionEvent;
 import android.view.View;
 import android.widget.EditText;
 import android.widget.ListView;
+import android.widget.TextView;
 import android.widget.Toast;
 
 import com.termux.R;
@@ -70,6 +72,8 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
 
     private boolean mTerminalCursorBlinkerStateAlreadySet;
 
+    private TextView mComposingText;
+
     private static final String LOG_TAG = "TermuxTerminalViewClient";
 
     public TermuxTerminalViewClient(TermuxActivity activity, TermuxTerminalSessionClient termuxTerminalSessionClient) {
@@ -87,6 +91,7 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
     public void onCreate() {
         mActivity.getTerminalView().setTextSize(mActivity.getPreferences().getFontSize());
         mActivity.getTerminalView().setKeepScreenOn(mActivity.getPreferences().shouldKeepScreenOn());
+        mComposingText = mActivity.findViewById(R.id.activity_termux_composing_text);
     }
 
     /**
@@ -229,6 +234,12 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
     @SuppressLint("RtlHardcoded")
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession currentSession) {
+        if (e.getAction() == KeyEvent.ACTION_DOWN &&
+            (e.getKeyCode() == KeyEvent.KEYCODE_HENKAN && KeyEvent.metaStateHasModifiers(e.getModifiers(), KeyEvent.META_CTRL_ON))){ //Ctrl+変換
+            onToggleSoftKeyboardRequest();
+            return true;
+        }
+
         if (handleVirtualKeys(keyCode, e, true)) return true;
 
         if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
@@ -346,6 +358,12 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
     }
 
 
+    @Override
+    public void onComposingTextChange(Editable text)
+    {
+        mComposingText.setText(text);
+    }
+
 
     @Override
     public boolean onCodePoint(final int codePoint, boolean ctrlDown, TerminalSession session) {
@@ -501,10 +528,12 @@ public class TermuxTerminalViewClient extends TermuxTerminalViewClientBase {
         if (mActivity.getProperties().shouldEnableDisableSoftKeyboardOnToggle()) {
             // If soft keyboard is visible
             if (!KeyboardUtils.areDisableSoftKeyboardFlagsSet(mActivity)) {
+                mComposingText.setVisibility(View.GONE);
                 Logger.logVerbose(LOG_TAG, "Disabling soft keyboard on toggle");
                 mActivity.getPreferences().setSoftKeyboardEnabled(false);
                 KeyboardUtils.disableSoftKeyboard(mActivity, mActivity.getTerminalView());
             } else {
+                mComposingText.setVisibility(View.VISIBLE);
                 // Show with a delay, otherwise pressing keyboard toggle won't show the keyboard after
                 // switching back from another app if keyboard was previously disabled by user.
                 // Also request focus, since it wouldn't have been requested at startup by

一応最低限入力中の文字は見えます。

2022-01-18

EmacsからWindows Search再び

以前Helm用に作ったのですが、Ivy/Counselへ移行して以降放置していたのを復活させました。

misohena/el-winsearch: Access Windows Search from Emacs

とりあえずHelmと分離して M-x winsearch でgrep風に結果を表示させてみました。

その上で counsel-winsearch も作ってあります。

最近検索メニューを整備しているのでその一環です。

2022-01-16 ,

org-modeでテーブルの列幅を指定してshrinkすると日本語で表示が乱れる件

(2022-01-16追記: 切り取る範囲の計算を修正しました。invisibleな文字があるので複数の答えがあり得て、その中で最も長いものを選ばなければなりませんでした)

phscrollのついでと言っては何ですがこれも以前から気になっていたので。

2022-01-16-fix-org-table-shrink-field.png

見るからに指定した幅以上の最小の幅で切り取っているのが原因ですね。私がカラム幅の指定をあまり使ってこなかったのはこういうのがあるからだったりします。超えない最大の幅で切り取って足りない分を埋めるのが良いでしょう。

問題は org-table–shrink-field 内にあるのでこれにadviceを追加することにしました。

(defun my-org-table--shrink-field (old-fun width align start end contents)
  (if (or (= start end)
          (org-table--shrunk-field)
          (= 0 width)
          (eq contents 'hline)
          (equal contents ""))
      ;; 関係ないところは従来のコードを呼び出す。
      (funcall old-fun width align start end contents)

    (let* ((lead (org-with-point-at start (skip-chars-forward " ")))
           (trail (org-with-point-at end (abs (skip-chars-backward " "))))
           (contents-width (org-string-width
                            (buffer-substring (+ start lead) (- end trail)))))
      (cond
       ((<= width contents-width)
        (let ((pre
               (and (> lead 0)
                    (org-table--make-shrinking-overlay
                     start (+ start lead) "" contents t)))
              (post
               ;; widthを超えない最後の位置を分割点とする。

               ;; Find cut location so that WIDTH characters are
               ;; visible using dichotomy.
               (let* ((begin (+ start lead))
                      (lower begin)
                      (upper (1+ (1- end))) ;;+
                      ;; Compensate the absence of leading space,
                      ;; thus preserving alignment.
                      (width (if (= lead 0) (1+ width) width))
                      (divpos
                       (progn
                         (while (> upper lower)
                           (let ((mean (ash (+ lower upper) -1)))
                             (if (< width (org-string-width (buffer-substring begin mean)))
                                 (setq upper mean)
                               (setq lower (1+ mean)))))
                         (1- upper)))
                      ;; 分割点までの幅を計算する。
                      (str-w (org-string-width (buffer-substring begin divpos))))
                 (org-table--make-shrinking-overlay
                  divpos
                  end
                  ;; 足りない分を空白で埋める。
                  (make-string (- width str-w) ? )
                  contents))))
          (if pre (list pre post) (list post))))
       (t
        ;; 関係ないところは従来のコードを呼び出す。
        (funcall old-fun width align start end contents))))))

(advice-add #'org-table--shrink-field
            :around #'my-org-table--shrink-field)

;; (advice-remove #'org-table--shrink-field
;;                #'my-org-table--shrink-field)

丸丸差し替えても良いのですが、結構長い関数だったので影響のある部分だけ別の処理をしてそれ以外は従来のコードを呼び出すようにしてみました。

幅広文字を考慮しているようでしていないというなんともよく分からないコードですね。

https://git.savannah.gnu.org/cgit/emacs/org-mode.git/tree/lisp/org-table.el?h=release_9.5.2#n3977

2022-01-16 ,

phscrollの修正

折り返しモードでも水平スクロールさせる件。前々から気になっていた問題を色々修正した。

misohena/phscroll: Enable partial horizontal scroll in Emacs

修正点:

  • 左右スクロールの可能性をフリンジで表示
  • sort-lines(やorg-table-sort-lines)でフリーズする問題の修正
  • org-tableの各種オーバーレイに対応
    • カラムの伸縮(org-table-shrink等)
    • 座標表示(org-table-toggle-coordinate-overlays)
    • ヘッドライン表示(org-table-header-line-mode)
  • org-indentでレイアウトが乱れることがある問題の修正

これまでは<と>でスクロールできることを表示していたのだけど、フリンジに表示すれば良いことに気がついた。幅が広がって見た目も良くなった。

ソートするとフリーズすることがあるので時々タスクマネージャからプロセスを強制終了していた。原因はsort-linesが内部でナローイングしていることで、modification-hooks経由でphscrollがスクロール領域を更新するときにforward-lineで更新終了ポイントまで到達できず無限ループに陥っていた。sort-linesはご丁寧にinhibit-quitを立てるので停止できないというわけ。

org-tableが作る様々なオーバーレイ表示に正式に対応した。これまでもカラムの伸縮くらいは最低限の対応をしていたが、伸縮した直後にレイアウトが乱れていた。座標表示はbefore-stringを使っているのでさらに特別な対応が必要だった。ヘッドラインモードは表内の一行に丸丸オーバーレイを被せ、それをpost-command-hookで更新する。phscrollと丸被りするので調整に手こずった。基本的にオーバーレイの追加・更新を効率よく確実に検出する一般的な方法はないので、個別の対応が必要。オーバーレイを追加・削除する関数をadviceでフックして解決した。

org-indentはファイルを開いた直後によくレイアウトが乱れるので不快だった(更新すればすぐに直ったが)。これも関連する関数にadviceを追加して解決した。

フリンジ、座標、ヘッダーラインに対応
図1: フリンジ、座標、ヘッダーラインに対応

phscrollは仕組み的にかなり無理があるものだがギリギリ実用に耐えるのが面白い(複数のウィンドウで同一の場所を見ない限りね!)。しかしできればEmacs側に行毎に折り返しを制御するようなプロパティを追加してほしいものだ。

2022-01-15 ,

org-modeのインライン画像の改善

最大サイズを制限する件。:widthプロパティ指定時にも最大幅を超えないようにした。displayプロパティは :width と :max-width が同時に指定されていると :max-width を無視してしまうので。

https://github.com/misohena/org-inline-image-fix/blob/master/org-limit-image-size.el

即時自動更新の件。マイナーモード化した。一時的に無効にしやすくなった。

https://github.com/misohena/org-inline-image-fix/blob/master/org-flyimage.el

インライン画像を自動更新しているとATTR_HTMLの:widthを調整しているときに画像が巨大化して非常に重くなる時があって前々から直したいなと思っていたので。