Category Archives: 未分類

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]] へ行ってみよう。
2022-01-01

all-the-icons-diredを修正

dired-details-rをいじったついでに、前々から気になっていた挙動を手元で二点修正した。

  • ファイル操作でアイコンが崩れることがある
  • リモート(tramp)越しに沢山のファイルがあるディレクトリを開こうとすると待たされる

(ちなみに、アイコンの幅が不揃いでズレている件はフォントファイルをFontForgeで加工して解決した)

修正点

wyuenho/all-the-icons-dired at 5e9b097f9950cc9f86de922b07903a4e5fefc733 のバージョンからの修正、のはず。

ファイル操作でアイコンが崩れることがある

evaporate

オーバーレイは基本的にevaporateにした方が安全。diredはファイルを移動したり削除したりしたときに行を消すので、その時に自動的にオーバーレイも消えてくれる。

 (defun all-the-icons-dired--add-overlay (pos string)
   "Add overlay to display STRING at POS."
   (let ((ov (make-overlay (1- pos) pos)))
     (overlay-put ov 'all-the-icons-dired-overlay t)
-    (overlay-put ov 'after-string string)))
+    (overlay-put ov 'after-string string)
+    (overlay-put ov 'evaporate t)))

after-readin-hookのみで更新

基本的にafter-readin-hookのタイミングで更新すれば十分なはず。……だけど、すでにdired-readinはadviceをかけてるな……。不十分だったらこれはキャンセル。

-(defun all-the-icons-dired--refresh-advice (fn &rest args)
-  "Advice function for FN with ARGS."
-  (prog1 (apply fn args)
-    (when all-the-icons-dired-mode
-      (all-the-icons-dired--refresh))))
-
-(defvar all-the-icons-dired-advice-alist
-  '((dired-aux     dired-create-directory       all-the-icons-dired--refresh-advice)
-    (dired-aux     dired-do-create-files        all-the-icons-dired--refresh-advice)
-    (dired-aux     dired-do-kill-lines          all-the-icons-dired--refresh-advice)
-    (dired-aux     dired-do-rename              all-the-icons-dired--refresh-advice)
-    (dired-aux     dired-insert-subdir          all-the-icons-dired--refresh-advice)
-    (dired-aux     dired-kill-subdir            all-the-icons-dired--refresh-advice)
-    (dired         wdired-abort-changes         all-the-icons-dired--refresh-advice)
-    (dired         dired-internal-do-deletions  all-the-icons-dired--refresh-advice)
-    (dired-narrow  dired-narrow--internal       all-the-icons-dired--refresh-advice)
-    (dired-subtree dired-subtree-insert         all-the-icons-dired--refresh-advice)
-    (dired-subtree dired-subtree-remove         all-the-icons-dired--refresh-advice)
-    (dired         dired-readin                 all-the-icons-dired--refresh-advice)
-    (dired         dired-revert                 all-the-icons-dired--refresh-advice)
-    (find-dired    find-dired-sentinel          all-the-icons-dired--refresh-advice))
-  "A list of file, adviced function, and advice function.")
 
+(defun all-the-icons-dired--after-readin-hook ()
+  (when all-the-icons-dired-mode
+    (if (> (line-number-at-pos (point-max)) 1000)
+        ;; If there are many files, it will be very slow, so disable icons.
+        (all-the-icons-dired--remove-all-overlays)
+      (all-the-icons-dired--refresh))))
 
 (defun all-the-icons-dired--setup ()
   "Setup `all-the-icons-dired'."
   (setq-local tab-width 1)
-  (pcase-dolist (`(,file ,sym ,fn) all-the-icons-dired-advice-alist)
-    (with-eval-after-load file
-      (advice-add sym :around fn)))
-  (all-the-icons-dired--refresh))
+  (add-hook 'dired-after-readin-hook #'all-the-icons-dired--after-readin-hook nil t))
 
 (defun all-the-icons-dired--teardown ()
   "Functions used as advice when redisplaying buffer."
   (kill-local-variable 'tab-width)
-  (pcase-dolist (`(,file ,sym ,fn) all-the-icons-dired-advice-alist)
-    (with-eval-after-load file
-      (advice-remove sym fn)))
+  (remove-hook 'dired-after-readin-hook #'all-the-icons-dired--after-readin-hook t)
   (all-the-icons-dired--remove-all-overlays))

(2023-04-10追記: ファイル数が1000を超えたらアイコンを表示しないようにした。重いので)

現在ナローイングされている範囲だけ削除

上の修正をしたせいか(?)、iキーでサブディレクトリを追加したら追加したもの以外が消えてしまったので。いや、 all-the-icons-dired–remove-all-overlaysではwidenしているのに、all-the-icons-dired–refreshでwidenしていないからとも言えるかもしれない。

そもそもevaporateにしたから自動的に消えるので不要かもしれない(?)

 (defun all-the-icons-dired--remove-all-overlays ()
   "Remove all `all-the-icons-dired' overlays."
   (save-restriction
     (widen)
     (mapc #'delete-overlay
           (all-the-icons-dired--overlays-in (point-min) (point-max)))))

+(defun all-the-icons-dired--remove-narrowed-overlays ()
+  "Remove all `all-the-icons-dired' overlays."
+  (mapc #'delete-overlay
+        (all-the-icons-dired--overlays-in (point-min) (point-max))))

 (defun all-the-icons-dired--refresh ()
   "Display the icons of files in a dired buffer."
-  (all-the-icons-dired--remove-all-overlays)
+  (all-the-icons-dired--remove-narrowed-overlays)

リモート(tramp)越しに沢山のファイルがあるディレクトリを開こうとすると長時間待たされる

ファイル毎にファイル種別判別のための関数を呼んでいるのが原因。リモートの場合は避ける。

 (defun all-the-icons-dired--refresh ()
   "Display the icons of files in a dired buffer."
   (all-the-icons-dired--remove-narrowed-overlays)
   (save-excursion
     (goto-char (point-min))
     (while (not (eobp))
       (when (dired-move-to-filename nil)
         (let ((case-fold-search t))
-          (when-let* ((file (dired-get-filename 'relative 'noerror))
+          (when-let* ((file (dired-get-filename nil 'noerror)) ;;フルパスで取得
-                      (icon (if (file-directory-p file)
-                                (all-the-icons-icon-for-dir file
-                                                            :face 'all-the-icons-dired-dir-face
-                                                            :v-adjust all-the-icons-dired-v-adjust)
-                              (apply 'all-the-icons-icon-for-file file
-                                     (append
-                                      `(:v-adjust ,all-the-icons-dired-v-adjust)
-                                      (when all-the-icons-dired-monochrome
-                                        `(:face ,(face-at-point))))))))
+                      (icon
+                       (if (save-excursion (forward-line 0) (looking-at-p dired-re-dir)) ;;file-directory-pはリモートアクセスを引き起こすので避ける
+                           (if (file-remote-p file)
+                               ;; all-the-icons-icon-for-dirの中でも file-*-p 関数を使っているので避ける
+                               (all-the-icons-octicon "file-directory"  :height 1.0 :v-adjust -0.1
+                                                      :face 'all-the-icons-dired-dir-face
+                                                      :v-adjust all-the-icons-dired-v-adjust)
+                             (all-the-icons-icon-for-dir file
+                                                         :face 'all-the-icons-dired-dir-face
+                                                         :v-adjust all-the-icons-dired-v-adjust))
+                         (apply 'all-the-icons-icon-for-file file
+                                (append
+                                 `(:v-adjust ,all-the-icons-dired-v-adjust)
+                                 (when all-the-icons-dired-monochrome
+                                   `(:face ,(face-at-point))))))))
             (if (member file '("." ".."))
-                (all-the-icons-dired--add-overlay (point) "  \t")
-              (all-the-icons-dired--add-overlay (point) (concat icon "\t"))))))
+                (all-the-icons-dired--add-overlay (point) "   ") ;;この辺は個人的な修正。タブを記号で表示しているので。どのみち位置は揃わないのでフォントの方を加工して揃えた。
+              (all-the-icons-dired--add-overlay (point) (concat icon " "))))
       (forward-line 1))))
2022-01-01

Emacsでdisplayプロパティを使って改行を置き換えると非常に遅くなる件

私はDiredをファイル名が一番左に来るように改造して使っているのですが、ファイル数が多いディレクトリを開くと動作が重くなって困ることが度々ありました(一時的に効果を切れば回避できます)。

オーバーレイが多いから仕方が無いくらいに思っていたのですが、今日少し調べたら原因は行末の "\n" を "文字列… \n" に置き換えているのが原因だと分かりました。オーバーレイでもテキストプロパティでも関係ありません。

次のコードは "\n" を "EOL\n" に置き換えるdisplayプロパティがついた文字列を20000行追加するものです(バッファにはオーバーレイではなくテキストプロパティのdisplayプロパティが設定されます)。

(dotimes (_ 20000)
  (insert "1234567890" (propertize "\n" 'display "EOL\n"))) ;;NG

結果の見た目は次のようになります。

1234567890EOL
1234567890EOL
...19997行略...
1234567890EOL

scratchバッファで実行した後バッファの末尾でprevious-line(C-p)してみると一行上に移動するのに1秒程度かかります。上に行けば行くほど時間は短くなり、バッファの冒頭付近では全く気がつかないくらいの時間になります。

"\n" を置換しなければこの現象は発生しません。例えば "0" を "0EOL" に置換しても(見た目は同じですが)全く遅くはなりません。

(dotimes (_ 20000)
  (insert "123456789" (propertize "0" 'display "0EOL") "\n")) ;;OK

オーバーレイのbefore-stringで"\n"の前に文字列を挿入しても(C-pは)遅くなりません(挿入自体の時間はテキストプロパティに比べてややかかります)。

(dotimes (_ 20000)
  (insert "1234567890\n")
  (let ((ov (make-overlay (1- (point)) (point)))) ;;\nのところを覆う
    (overlay-put ov 'before-string "EOL") ;;OK
    (overlay-put ov 'evaporate t)))

また、空の範囲のオーバーレイを許容するのであれば、"0"と"\n"の間にオーバーレイを挟むこともできます。この場合displayプロパティは効かないのでbefore-stringかafter-stringを使うことになります(evaporateが使えないので消すのが面倒になるので注意)。

(dotimes (_ 20000)
  (insert "1234567890\n")
  (let ((ov (make-overlay (1- (point)) (1- (point))))) ;;\nの前の空の範囲!
    (overlay-put ov 'after-string "EOL"))) ;;OK: before-stringでも同じ。displayは空の範囲では表示されないので使えない
;; 消すときは (remove-overlays (point-min) (point-max)) あたりで。

displayプロパティで "\n" 込みの文字列で置換してしまうと、やっぱり遅くなるわけです(激重注意)。

(dotimes (_ 20000)
  (insert "1234567890\n")
  (let ((ov (make-overlay (1- (point)) (point)))) ;;\nのところを覆う
    (overlay-put ov 'display "EOL\n") ;;NG
    (overlay-put ov 'evaporate t)))

しかもテキストプロパティに比べて格段に遅いです。一行上に移動するのに何十秒もかかります。

私が"\n"を置換したかったのは、そうしないとカーソルをファイル名の末尾に置けないからです。例えば上の問題が起きないどのケースを使用しても"0"の直後にカーソルを置くことができません。"0"を指しているところでforward-charすると"0"の直後ではなく"EOL"の直後に飛んでしまいます。"0"を"0EOL"に置換した場合ならともかく、"\n"にbefore-stringをかけたときはbefore-stringの前にカーソルが来て欲しいものですが残念ながらそうはなりません。diredで表示を変えるだけならそれほど問題にはならないのですが、wdiredでファイル名を直接編集するときには問題になります(対策はwdiredが起動したら一時的に効果を消すくらいか)。

面白いのは一行下に移動するnext-line(C-n)は遅くならないこと。また、同じ一行上に移動するのでもM-: (forward-line -1)では遅くなりません。(previous-line)は(forward-line -1)に比べると色々な処理を追加で行っているので、そのどこかに原因があるのでしょう。previous-line → line-move → line-move-1 → vertical-motion と呼び出していて、vertical-motionはindent.cの中にあり細々とした処理をしているので追っていませんがdisplayとか'\n'とかが出てくるのでそのあたりで何かあるのでしょう。

ちなみに、連続した行でなければ問題は起きません。

(dotimes (_ 20000)
  ;; 最初に\nを入れる
  (insert "\n1234567890" (propertize "\n" 'display "EOL\n"))) ;;OK

1行空行を入れるととたんに問題は起きなくなります。

重いのは嫌なので結局一番速いテキストプロパティで改行の一つ前の文字を置き換えるように変更しました。

Improve performance · misohena/dired-details-r@c7699cb

(2022-01-02追記) before-stringの前にカーソルが置けないと書きましたが、cursorプロパティを使うと置けることに気がつきました。次のコードを使うと、previous-lineで遅くならず(\nをdisplayプロパティで置き換えていないので)、かつ、0とEOLの間にカーソルが置けて('cursor 1の部分の効果)、さらにそこで文字を入力するとEOLの前に挿入されます(make-overlayの第四引数の効果)。

(dotimes (_ 20000)
  (insert "1234567890\n")
  (let ((ov (make-overlay (1- (point)) (point) nil t))) ;;\nのところを覆う。直前に入力した文字はオーバーレイに含めない。
    (overlay-put ov 'before-string (propertize "EOL" 'cursor 1)) ;;EOLのテキストプロパティに1を付けるとなぜかEOLの直前にカーソルを置けるようになる。
    (overlay-put ov 'evaporate t)))

cursorテキストプロパティはマニュアルを読んでも正直意味が分からないので、なぜこうなるのかは不明です。

dired-details-rですが、大量のオーバーレイは移動こそ重くならないまでも追加と削除には時間がかかるので、テキストプロパティのままで行こうと思います。カーソルの移動に問題が残りますが我慢できないほどではないです。いや、行数で実装を切り替えるというのもアリですかね……?

(2022-01-06追記) wdiredでファイル名末尾にカーソルが置けないのがやっぱりストレスなので上記cursorプロパティを使う方法をdired-details-rに採用しました。オーバーレイはテキストプロパティよりも遅いので、1000行越えたらテキストプロパティに切り替える(+wdired起動時は表示を戻す)という荒技も組み合わせました。なおcursorプロパティの挙動は相変わらずよく分かっていません。

Fix issue can't move to the end of file names in wdired mode · misohena/dired-details-r@ae2f690