折腾完https://maskray.me/blog/2016-03-13-terminal-emulator-fullwidth-color-emoji后发现canonical mode下emoji字符退格只后退了一列,后发现所有宽字符都有问题,因此做了一番调研。
早期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。
通常为^D
,可以用ctrl d
输入,作用是使得read()
立即返回该行所有字符,若位于行首则返回0。很多地方把它视为到达文件结束位置的信号,并不再继续读入。但实际上终端输入并没有被关闭,仍可以继续读取字符。
|
|
编译运行上面C程序,试试输入若干字符后按^D
的输出。
stty -a
中erase =
显示当前ERASE字符设置,通常为^?
或^H
。终端模拟器vte是^?
,退格键发送^?
;xterm是^H
,退格键发送^H
。倘若终端ERASE字符与之不同则可能导致退格不删除字符反而输入了一个^?
或^H
。
通常为^C
,若local modes中ISIG
开启,则前台进程组会收到SIGINT。
通常为^\
,若local modes中ISIG
开启,则前台进程组会收到SIGQUIT。
通常为^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:
之后用strace -e read -p $(pgrep -n termite) |& grep 13
,在termite窗口键入退格,观察master端fd读到的数据。
这很可能是内核的问题,简易的修复方式是头痛医脚,修改使用pseudoterminal的程序(如终端模拟器、tmux)的代码。对于canonical mode并开启IUTF8
时,从pseudoterminal master处读到\b
时,判断左侧字符是否为宽字符,是则左移2格(目前尚无更宽的字符)。我做了两个patch:
于是这是我第一次和第二次创建PKGBUILD……
info '(libc) Terminal Modes'