vim-jp / issues

有志で既知のバグや要望を検討・管理し、オフィシャルへの還元をしていきます。
https://vim-jp.org/
341 stars 11 forks source link

CUI 版の Vim のターミナルサイズをピクセルレベルで取得することはできるでしょうか? #1438

Closed mikoto2000 closed 3 weeks ago

mikoto2000 commented 3 weeks ago

質問の内容

Vim のターミナル上で ioctltermios.TIOCGWINSZ を実行し、ターミナルのサイズを取得するスクリプトを実行しました。 See: https://github.com/mikoto2000/sixelPreviewer/blob/master/termsize.py

wsltty 上で直接実行すると、ピクセル単位の幅と高さがゼロになるのに対し、 Vim のターミナル上で実行すると、ピクセル単位の幅と高さに値が入っていました。

image

image

ただ、入っている数値が大分小さい値になっています。 (前述のVimのスクリーンショットの場合、w:880, h:220 が画面上のサイズでした。

こちら、Vim はどういう仕組みでこの値が入っているのでしょうか? この値を正確なピクセル数に修正できる可能性はあるでしょうか?

Vimのバージョン

9.1.697

OSの種類/ディストリ/バージョン

使用している or 関係していそうなプラグイン

なし

その他

以下環境でも同様の結果となりました。

mattn commented 3 weeks ago

CSI リクエストの応答は、Vim ではなく端末の仕事なのでお使いの端末が返してる値だと思います。

mikoto2000 commented 3 weeks ago

@mattn コメントありがとうございます。

CSI リクエストの応答は、Vim ではなく端末の仕事なのでお使いの端末が返してる値だと思います。

だとすると、wsltty 上で直接件の python スクリプトを実行したときにも値は入るものなのではないでしょうか?

mattn commented 3 weeks ago

CSI リクエストは端末がサポートする端末種別によって問い合わせ結果が異なります。

https://www.xfree86.org/current/ctlseqs.html

ここにある VT-XXX という部分です。端末によっては設定により自分の端末がどの端末として動作するかのふるまいを変える事ができる物もあります。(putty や teraterm など) screen/tmux の様に環境変数で挙動を変える物もあります。

おそらく wsltty がその CSI リクエストに対応していない(もしくは設定が CSI 14 を返せる状態ではない)のだと思います。

mattn commented 3 weeks ago

あとはその CSI リクエストをどこに投げているのかもあります。 ssh 超しの端末内で動作するアプリケーションからは、ウィンドウサイズは知る全てないので、おそらく 0 を返すと思います。

mikoto2000 commented 3 weeks ago

コンソールアプリの対応状況や設定・計算条件の問題という事はなんとなくわかりましたが、混乱しています。

Vim のターミナルは、 wsltty に依存せずに値を返しているのでしょうか? 何から値を取得しているのでしょう?

mattn commented 3 weeks ago

コンソールアプリである vim.exe や、WSL 内の vim は、画面のサイズやフォントサイズなどは知る術がありません。 それらのサイズを知っているのは端末ソフトウェアだけです。 つまり WezTerm や Windows Terminal だけが知っています。 CSI リクエストは、端末内アプリケーションが標準出力に対してリクエストを投げます。すると端末ソフトウェアがそれをフックして標準出力に送り返してきます。そうやってコンソールアプリケーションはウィンドウサイズ等を知ることができる様になっています。ただし前述のとおり、端末種別によってはこの問い合わせ機能は持っていない事になっていますので、wsltty の有無で結果が異なるのであれば、それは wsltty が何かをサポートしてないのだと思います。

mikoto2000 commented 3 weeks ago

実行環境に誤解があるかもしれません。

wsltty 上で直接スクリプトを起動するとゼロが返却され、 wsltty 上でコンソール版 vim を起動し、 :term した中でスクリプトを起動すると数値が入るという状態です。

mattn commented 3 weeks ago

wsltty 無しで確認しました。 Windows Terminal から起動した WSL で直接スクリプトを動かすと 0 0 が、その中で起動した vim の terminal で実行すると値が取れました。 Windows Terminal の CTRL-+ でフォントサイズを変えながら実行するとちゃんとサイズが取れているので、これは間違いなく

terminal -> libvterm -> vim -> windows terminal

という経路で CSI リクエストが飛ばされ、その応答が python スクリプトに返っている事になります。おそらくですが、Windows Terminal そのものは幅を返せる機能があるにもかかわらず、Windows Terminal から直接起動されている端末ソフトウェア(conpty?)が、対応してないだけなんだと思います。 おそらく WSL から1回、tmux や screen を起動すれば取れたりするかも。

mattn commented 3 weeks ago

ビンゴですね。 image

mikoto2000 commented 3 weeks ago

ありがとうございます。

というのが今の理解です。 元々の疑問である以下の答えとしては、「Vimは関係ない」で間違いなさそうですので、 issue を閉じたいと思います。

こちら、Vim はどういう仕組みでこの値が入っているのでしょうか? この値を正確なピクセル数に修正できる可能性はあるでしょうか?

@mattn 検証までしていただきありがとうございました

mikoto2000 commented 3 weeks ago

不思議なことが起こっています... tmux 上で直接スクリプトを動かした場合と、 tmux 上で vim を起動して :term してスクリプトを動かした場合でサイズが異なります。 縦はともかく横は同じ値になると思っていたのですが...どんな理由が考えられるでしょうか?

vim-terminal

mikoto2000 commented 3 weeks ago

動画にしなくてもよかったですね... image

mattn commented 3 weeks ago

vim の terminal を通すと width pixels と height pixels の順が逆になってる気がします。

mattn commented 3 weeks ago

os_unix.c にちょっと怪しいコードがありますね。

    int
mch_report_winsize(int fd, int rows, int cols)
{
    int     tty_fd;
    int     retval = -1;

    tty_fd = get_tty_fd(fd);
    if (tty_fd < 0)
    return FAIL;

# if defined(TIOCSWINSZ)
    struct winsize ws;

    ws.ws_col = cols;
    ws.ws_row = rows;
    ws.ws_xpixel = cols * 5;
    ws.ws_ypixel = rows * 10;
    retval = ioctl(tty_fd, TIOCSWINSZ, &ws);
    ch_log(NULL, "ioctl(TIOCSWINSZ) %s", retval == 0 ? "success" : "failed");
# elif defined(TIOCSSIZE)
    struct ttysize ts;

    ts.ts_cols = cols;
    ts.ts_lines = rows;
    retval = ioctl(tty_fd, TIOCSSIZE, &ts);
    ch_log(NULL, "ioctl(TIOCSSIZE) %s", retval == 0 ? "success" : "failed");
# endif
    if (tty_fd != fd)
    close(tty_fd);
    return retval == 0 ? OK : FAIL;
}
mikoto2000 commented 3 weeks ago

えーとこれは、 「Vim の :term はそれ自身が端末なので、サイズを返却する側であるが、同時にコンソールアプリケーションであるためフォントのサイズがわからない。そのため、固定値の 5 と 10 で計算してアプリに報告している」 という認識で合っているでしょうか?

gcrtnst commented 3 weeks ago

横から失礼します。 TIOCSWINSZ(2const) によると、ws_xpixel、ws_ypixel は unused となっています。

mikoto2000 commented 3 weeks ago

@gcrtnst man ではそうなっているのですが、一部ターミナルエミュレーターは、そこにぴピクセル数を入れてくれる実装になっているようです。

mikoto2000 commented 3 weeks ago

対応端末であれば、「親プロセスの tty に対して TIOCSWINSZ を発行し、サイズを取得、そこからフォントサイズを計算。計算したフォントサイズで 5 と 10 を置き換える」で正確な値を返せそうですかね...?

GUI の方の処理の検討も必要ですが...

mikoto2000 commented 3 weeks ago

前述のロジックを追加すると、それっぽい値にはなりますね... image

diff --git a/src/os_unix.c b/src/os_unix.c
index 0aa6f3ca1..db6c9e284 100644
--- a/src/os_unix.c
+++ b/src/os_unix.c
@@ -4349,6 +4349,92 @@ mch_get_shellsize(void)
 }

 #if defined(FEAT_TERMINAL) || defined(PROTO)
+
+#if defined(TIOCSWINSZ)
+    int
+calc_x_font_size() {
+#if defined(FEAT_GUI)
+    if (!gui.in_use) {
+#endif
+      pid_t ppid = getppid();
+
+      // /proc/[親プロセスID]/fd/0 のパスを作成
+      char tty_path[256];
+      snprintf(tty_path, sizeof(tty_path), "/proc/%d/fd/0", ppid);
+
+      // シンボリックリンクから TTY デバイスのパスを取得
+      char actual_tty[256];
+      ssize_t len = readlink(tty_path, actual_tty, sizeof(actual_tty) - 1);
+      if (len == -1) {
+          return 5;
+      }
+      actual_tty[len] = '\0'; // 文字列の終端を追加
+
+      // TTY デバイスファイルをオープン
+      int tty_fd = open(tty_path, O_RDWR);
+      if (tty_fd == -1) {
+          return 5;
+      }
+
+      // ウィンドウサイズを取得するための構造体
+      struct winsize ws;
+      if (ioctl(tty_fd, TIOCGWINSZ, &ws) == -1) {
+          close(tty_fd);
+          return 5;
+      }
+
+      close(tty_fd);
+
+      return ws.ws_xpixel / ws.ws_col;
+#if defined(FEAT_GUI)
+    } else {
+      return 5;
+    }
+#endif
+}
+
+    int
+calc_y_font_size() {
+#if defined(FEAT_GUI)
+    if (!gui.in_use) {
+#endif
+      pid_t ppid = getppid();
+
+      // /proc/[親プロセスID]/fd/0 のパスを作成
+      char tty_path[256];
+      snprintf(tty_path, sizeof(tty_path), "/proc/%d/fd/0", ppid);
+
+      // シンボリックリンクから TTY デバイスのパスを取得
+      char actual_tty[256];
+      ssize_t len = readlink(tty_path, actual_tty, sizeof(actual_tty) - 1);
+      if (len == -1) {
+          return 10;
+      }
+      actual_tty[len] = '\0'; // 文字列の終端を追加
+
+      // TTY デバイスファイルをオープン
+      int tty_fd = open(tty_path, O_RDWR);
+      if (tty_fd == -1) {
+          return 10;
+      }
+
+      // ウィンドウサイズを取得するための構造体
+      struct winsize ws;
+      if (ioctl(tty_fd, TIOCGWINSZ, &ws) == -1) {
+          return 10;
+      }
+
+      close(tty_fd);
+
+      return ws.ws_ypixel / ws.ws_row;
+#if defined(FEAT_GUI)
+    } else {
+      return 10;
+    }
+#endif
+}
+#endif
+
 /*
  * Report the windows size "rows" and "cols" to tty "fd".
  */
@@ -4367,8 +4453,8 @@ mch_report_winsize(int fd, int rows, int cols)

     ws.ws_col = cols;
     ws.ws_row = rows;
-    ws.ws_xpixel = cols * 5;
-    ws.ws_ypixel = rows * 10;
+    ws.ws_xpixel = cols * calc_x_font_size();
+    ws.ws_ypixel = rows * calc_y_font_size();
     retval = ioctl(tty_fd, TIOCSWINSZ, &ws);
     ch_log(NULL, "ioctl(TIOCSWINSZ) %s", retval == 0 ? "success" : "failed");
 # elif defined(TIOCSSIZE)
@@ -4385,6 +4471,7 @@ mch_report_winsize(int fd, int rows, int cols)
 }
 #endif

+
 /*
  * Try to set the window size to Rows and Columns.
  */

commit: https://github.com/mikoto2000/vim/commit/638a1ce91d773fb5dd89af734fa0a875db9f83bd

mikoto2000 commented 3 weeks ago

ピクセル数まで必要ないケースでパフォーマンス問題が発生しそう...

mattn commented 3 weeks ago

いちおう xterm の terminal report にはサイズを返す CSI はあるので、TIOCSWINSZ でなく端末に問い合わせたらもしかしたら(端末によっては)サイズが得られるかもです。

image

mikoto2000 commented 3 weeks ago

あれ? python スクリプトが TIOCGWINSZ をした応答として Vim の TIOCSWINSZ が発火するのだと思っていたのですが、違いましたでしょうか?

mattn commented 3 weeks ago
printf "\x1b[14t"

をシェルで実行して貰えますか。

mikoto2000 commented 3 weeks ago

wsltty 上で直接実行

printf "\x1b[14t"
528;880t

wsltty 上の tmux で実行

printf "\x1b[14t"
736;1280t

wsltty 上の tmux 上の Vim の :term で実行

printf "\x1b[14t"
(何も表示されず)

となりました。

mikoto2000 commented 3 weeks ago

前述のロジックを整理。 https://github.com/vim/vim/compare/master...mikoto2000:vim:improve-tiocswinsz-pixel-size

mikoto2000 commented 3 weeks ago

wslbridge2 がピクセルをゼロで設定して返却していそう。 https://github.com/Biswa96/wslbridge2/blob/5f444e639cc5ff6eff8945094ee68d0583adaafe/src/wslbridge2-backend.cpp#L301-L303

mikoto2000 commented 3 weeks ago

とすると、件の対応をしたとしても、 wslbridge2 上で直接 Vim を起動した場合、ゼロが返却されることになるのか...。なるほどなあ

mikoto2000 commented 3 weeks ago

非常に道のりが遠いことがわかりましたのでこの issue はクローズとさせていただきます。 (おおもとのやりたいことは、 Sixel 画像をウィンドウにフィットさせるためにピクセルレベルでサイズを取得する方法を探していたという感じでした)

mikoto2000 commented 2 weeks ago

@mattn やっと TIOCSWINSZ と「端末に問い合わせ」の違いが解った気がします。 ioctl ではなく fprintf でエスケープシーケンスを送信することで、 wsltty 上で直接 Vim を開いた場合でもピクセルレベルのサイズ取得ができることを確認しました。 ありがとうございました!