终端模拟器下宽字符退格

折腾完https://maskray.me/blog/2016-03-13-terminal-emulator-fullwidth-color-emoji后发现canonical mode下emoji字符退格只后退了一列,后发现所有宽字符都有问题,因此做了一番调研。

Canonical/noncanonical mode

早期Unix有cooked/cbreak/raw mode三种模式,raw mode和cbreak模式区别在于signal和输入输出处理,输入都是以字符为单位,即read(STDIN_FILENO, buf, 1)在键入一个字符后即返回。Cooked mode与它们差别较大,最重要的区别是终端输入以行为单位进行,并自带一个基础行编辑器,可以使用退格和WERASE(默认为^W)删除光标前的单词。

termios引入后对输入输出行为(input/output/local modes)有了更精细的控制,通过一些选项可以定制出原始的cooked/cbreak/raw mode三种模式。Local modes中的ICANON最为重要,区分canonical/noncanonical mode,canonical mode与早期cooked mode类似,带行编辑器,一些字符如CR EOF EOL ERASE KILL NL WERASE等有特殊含义,下面介绍几个比较重要的。更多介绍参见The Linux Programming Interface 62.4 Terminal Special Characters。

EOF

通常为^D,可以用ctrl d输入,作用是使得read()立即返回该行所有字符,若位于行首则返回0。很多地方把它视为到达文件结束位置的信号,并不再继续读入。但实际上终端输入并没有被关闭,仍可以继续读取字符。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <unistd.h>

int main()
{
char buf[9];
for(;;) {
printf("%d\n", read(0, buf, 9));
}
}

编译运行上面C程序,试试输入若干字符后按^D的输出。

ERASE

stty -aerase =显示当前ERASE字符设置,通常为^?^H。终端模拟器vte是^?,退格键发送^?;xterm是^H,退格键发送^H。倘若终端ERASE字符与之不同则可能导致退格不删除字符反而输入了一个^?^H

INTR

通常为^C,若local modes中ISIG开启,则前台进程组会收到SIGINT。

QUIT

通常为^\,若local modes中ISIG开启,则前台进程组会收到SIGQUIT。

SUSP

通常为^Z,若local modes中ISIG开启,则前台进程组会收到SIGTSTP,默认会停止成为后台任务,shell回到前台。

对于readline等使用noncanonical mode的应用程序,它们会检测TERM环境变量获取terminfo信息,从中找出不同功能对应的输出字符序列。

退格

\b字符的解析方式是光标左移一格。

在canonical mode下当前行仅有一个两个宽字符时,按下退格,光标左移一格并擦除了该字符,但继续按退格也无法回到行首,产生显示问题。

原因是内核tty驱动似乎没有考虑字符宽度信息,只给pseudoterminal master发送一个\b,终端模拟器收到\b后将光标左移了一格。再次按退格时,内核tty驱动判断该行已空,因此不再发送\b,光标也就无法退回到行首。

介绍一个测试方式:在pseudoterminal的slave端运行canonical mode的cat程序,输入退格,可以在master端看到内核发来"\b \b"三个字符,即后退一格,空格擦除,再后退一格。可以用下面的方法查看:

termite -e cat创建一个termite终端运行cat,然后找出termite在pseudoterminal pair的master端的fd:

1
2
% lsof -p $(pgrep -n termite) -Fn | sed -n '/^n\/dev\/ptmx/{g;p};s/^f//;h'
13

之后用strace -e read -p $(pgrep -n termite) |& grep 13,在termite窗口键入退格,观察master端fd读到的数据。

这很可能是内核的问题,简易的修复方式是头痛医脚,修改使用pseudoterminal的程序(如终端模拟器、tmux)的代码。对于canonical mode并开启IUTF8时,从pseudoterminal master处读到\b时,判断左侧字符是否为宽字符,是则左移2格(目前尚无更宽的字符)。我做了两个patch:

于是这是我第一次和第二次创建PKGBUILD……

参考