Yearly Archives: 2022

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
2022-01-02 ,

org言語のソースブロックをエクスポートしたときにリンクをそのまま出力する

例えば次のようなソースブロックがあった時に……

リンクは次のように書きます。
#+begin_src org
[[https://example.com/][example.com]] へ行ってみよう。
#+end_src

エクスポートすると……

リンクは次のように書きます。

example.com へ行ってみよう。

みたいに出力されてしまいます。リンクの書き方を説明したいのに、リンクのブラケットやパスの部分が消えてしまうわけです。

昔からそうだったけ?とモヤモヤしながらもう長いこと経つのですが、org-modeの書き方を例示することが多い私はよくこの問題に引っかかります(最近ではこういうのとかこういうのとか)。

C-x 8 RET zero width space と打って見えない空白を[と[の間に入れて回避するのが常なのですが、そろそろ何とかしたいなぁと思って調べました。そのコードをコピペすると空白までコピーされて機能しませんからね。

結果、次のようなコードで回避できました。

(defvar-local my-org-in-html-fontify-code nil)

(advice-add 'org-html-fontify-code :around 'my-org-html-fontify-code-advice)
(advice-add 'org-html-htmlize-region-for-paste :around 'my-org-html-htmlize-region-for-paste)

(defun my-org-html-fontify-code-advice (old-func &rest rest)
  (cl-letf (((default-value 'org-link-descriptive) nil)
            ((default-value 'my-org-in-html-fontify-code) t))
    (apply old-func rest)))

(defun my-org-html-htmlize-region-for-paste (old-fun beg end &rest rest)
  ;; Erase htmlize-link properties
  (when (and my-org-in-html-fontify-code
             (eq major-mode 'org-mode))
    (remove-text-properties beg end '(htmlize-link nil)))
  ;; Call original
  (apply old-fun beg end rest))

ソースブロックがどのようにHTMLへ変換されるかは org-html-fontify-code を見ると良いです。テンポラリバッファを作って言語用のメジャーモードを起動し、ソースブロックの内容を挿入します。確実にfont-lockしたら、テキストプロパティを元にhtml化するわけです。

org-modeにはリンクを文字通りに表示する org-link-descriptive というオプションがあるので、この関数が呼ばれている間だけ nil になってもらうのが先のコードです。

org-link-descriptive はorg-modeが立ち上がるとバッファローカル変数になってしまいます。なのでエクスポート元のバッファでいくら let で nil にしても、新しく作られるテンポラリバッファでは nil になりません。そこでcl-letfでデフォルト値の方を nil にしています。

何かエクスポート中であることを判別する方法があるならorg-mode-hookでorg-toggle-link-displayでも呼んでしまおうかと思ったのですが、残念ながら見つかりませんでした。

まずはその辺りを修正してからエクスポートしたのがこちら。

リンクは次のように書きます。

[[https://example.com/][example.com]] へ行ってみよう。

リンクがクリックできるようになっているのがちょっと気持ち悪いです。この例では正しいURLになっていますが、URLとは解釈できないようなorg-mode独自のリンクパスも問答無用でHTMLのリンクにしてくれます。

これはhtmlize-linkというテキストプロパティの作用です。ソースのHTML化はhtmlizeというパッケージがバッファ内のテキストプロパティを元に行いますが、org-modeのfont-lockがhtmlize-linkというテキストプロパティを付けるので、それを見たhtmlizeが自動的にリンクに置き換えてしまうのです。

ソースブロックの中からリンクを張りたいなどとは思わないので、こちらも問答無用でhtmlize-linkというテキストプロパティを削除するようにしました。

最終結果は次の通りです。

リンクは次のように書きます。

[[https://example.com/][example.com]] へ行ってみよう。