Monthly Archives: 1月 2022

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を調整しているときに画像が巨大化して非常に重くなる時があって前々から直したいなと思っていたので。

2022-01-08 ,

WordPressのプラグインを作る

これまで画像の表示にEasy FancyBoxを使っていたのですが表示する情報を増やせず追加の拡張に課金するのもなぁと思ったので自分で作りました。

とりあえず表示するだけなら簡単だったのですが、スクロール出来るようにしようと思ったら案外大変。適当にoverflow: scrollでスクロールバー出しておけば良いだろうと思ったのですが、ホイールでスクロールするときに後ろのページがスクロールしてしまうことが判明。元々背景のdivでpreventDefaultしてホイールを抑制していたのですが、それだとホイールで画像をスクロール出来ないのでスクロール領域だけpreventDefaultしないように変更。しかしそうすると画像のスクロールが必要ない状況(高さが足りているとか上限に達しているとか)の時に後ろのページがスクロールするという。部分的にブラウザ既定の動作に任せるというのは思っていたよりも難しいらしく、どうすればよく分からなかったため、最終的にはスクロール機能は自前で実装することに。しかしそうするとタッチイベントでも同じ問題が発生。もうピンチイン/アウトも含めて対応してしまえ、そうするとホイールも拡大縮小に割り当ててマウスでパンするように修正。

というわけでCSSでちょろっとスクロール出来るようにしておけば良いだろうと思ったのが、思いのほか時間を取られてしまいました。他にも自分で作ってみると細かい改善点が沢山見えてきますね。ヤレヤレ。

動作例:

白馬大池

WordPress部分はheadにscriptを追加するだけなので簡単でした。しかし、deferにするのってこんなことしなきゃいけないの?? マジで??

2022-01-05

360Photo System

これまでに歴代Googleスマホ(Nexus/Pixel)のPhoto Sphereで撮った写真を共有する仕組みを作った。

必要な作業は次の通り。

  1. HDDの中からそれらしき画像をかき集める
  2. 画像ファイル名とタイトルの対応表を作る
  3. 8192x4096に変換
  4. 2~4MBくらいのJPEGに頑張って圧縮(mozjpegで-quality 40くらいに落ち着いた)
  5. 600x314のサムネイル画像を作る(ImageMagickで1800幅に縮小して中心600幅を切り抜く)
  6. ついでに、押せるマーク付きサムネイルも作る
  7. 画像毎にHTMLを生成する(中身はメタ情報と外部JS起動のみ。OGP、TwitterCard情報付き)
  8. サーバにアップロードする

1と2はある程度手動でやらざるを得ないとして、3以降は自動的に行う。変換処理のスクリプトはEmacs Lispで書いた。私はシェルスクリプトは何もわからんので。Emacs Lispでは directory-files して shell-command 呼べばいいだけ。後はある種のテンプレートエンジンというか、 {{{key}}} をalistを元に置換するような仕組みを作ってHTMLを生成する。わずかにMakefileも使用。必要な操作はtransientで作ったメニューにまとめたので忘れても安心。

一番手間がかかったのは圧縮の方法を決めるところ。元の画像は6~16MBくらいもあるので、転送量・転送時間的にもサーバ容量的にも厳しい。2MB程度に収まらないか色々試したが、あまり品質を低くすると空のグラデーションがはっきり帯状になってしまうので無理だった。最初はImageMagickで圧縮したが、Photoshopで保存した方が綺麗だった。最終的にはmozjpegを使った。

ブラウザでの表示は以前星空を描画するために作ったもの(misohena/drawstars)を転用。正距円筒図法(equirectangular)の画像をWebGLのテクスチャにして描画する。1枚のテクスチャで描画する仕組みになっていたが、8192ピクセルサイズのテクスチャは手元のAndroid端末ではエラーになったので、急遽複数のテクスチャに分割して描画するように変更した。こういうのがあるから私はどうにも3Dグラフィックスハードウェアというのが好きになれない。ただ、WebGLは素のOpenGL ESをいじるよりは(主に周辺的な事情により)幾分気が楽である。変なバグを発見。起動したときになぜか中途半端な方向を向いているなと思ったら現在の恒星時の方向を向いていた。機能を切り忘れていたらしい。内部的には北極に立って全宇宙を眺めているという扱いになっているので。

こうやってブログにも簡単に貼り付けられる。

20180715_074423_thb.jpg
20190104_120508_thb.jpg
20200826_080913_thb.jpg
20211105_130232_thb.jpg
20190121_110427_thb.jpg
20211106_104109_thb.jpg
20181002_123850_thb.jpg