关于 Emacs 对 ANSI 事件的处理(或:为何 Emacs 有时自动插入方括号)

我的 Emacs 配置其实有个问题:有时在终端里打开的时候它会自动给我插入一对方括号。 例如:

;; 源文件
something
;; 打开后
[something]

今天终于想起来用 C-h l 看了一下原因,发现是有个 ESC [ I 的 ANSI 事件发了过来 (xterm 的 focus 事件)。而众所周知 Emacs 会把 ESC [ 当成 M-[ 来 处理,因此我的 evil-cp-wrap-next-square: <normal-state> M-[ 的绑定覆盖了 xterm-translate-focus-in: ESC [ I 的绑定,所以当终端得到 focus 时 Emacs 会 M-[ 插入一对方括号,然后 I 给我自动切成 evil 的 insert mode。

我感觉这有点像 bug 但又不确定,所以暂时还没有报 bug:

  • xterm-translate-focus-in: ESC [ I 是绑在 input-decode-map 上的,理应是优先 于普通的键绑定的;
  • 但是 ANSI 事件它的确又是发过来了三个字符,先优先两个字符的处理再考虑三个字符似 乎也理所当然,这应该属于 ANSI 的问题,感觉想不出支持的方法。可能有点类似 ESCC-[ 无法区分的问题。
    • 一个可能方法是把当前能读的字符全读完之后再决定哪个 keymap。目前的话至少 ESC [ I 是无法触发 evil-normal-state: <escape>ESC 绑定的,不知道内部实现是否是类似的逻辑。

不知道大家怎么看?

另:如果不用 evil-cleverparens 的也可以试一下 emacs -Q -nw 然后加入下面的绑定:

(local-set-key (kbd "M-[") (lambda () (interactive) (insert " ESC [ "))) ; focus 事件
(local-set-key (kbd "M-O") (lambda () (interactive) (insert " ESC O "))) ; 箭头键等等

我是把ESC O和ESC [专门留出来转译vt的控制序列。

输入序列的转译本质就是一级一级的查表,只有覆盖,没有优先。

1 个赞

我很早也发现过这个问题了。具体的问题是在用 copilot 时想把选择上一条补全项这个命令绑定到 M-[ 这个键位,就遇到了会给我插入奇奇怪怪的输入的问题。

M-[ 在终端里面这个按键一按就会出问题。

如果解绑掉这个按键,换成别的键位就不会出现这个问题了。

我个人倾向于这个是一个 bug。因为 vim/neovim 是可以正常的绑定 M-[ 这个按键的,不会出现绑定了以后出现奇奇怪怪的输入的问题。

更新: 今天在 emacs-30 上试了一下,发现 M-[ 可以在终端里正常的绑定到命令同时不会出现奇奇怪怪的输入问题了,非常神奇。

更新: M-[ 依然有问题,是我测试的时候疏忽了。

1 个赞

在邮件列表里翻到了几个有点类似的情况,但最后处理方法基本都是“不要用 M-[/M-O”。

总结

bug#45834: 28.0.50; Mouse events in terminal emacs

bug#1721: marked as done (23.0.60; visual-line-mode under terminal (-nw

Re: Bug with tmux/screen and special characters

主楼我提到了 evil 的 ESC 绑定不影响终端里的使用,例如按下箭头发送了 ESC O A/B/C/D,但是这并不会触发 evil-normal-state。查了一下发现这其实是 evil 手动实现的:触发 ESC 事件之后先等待 evil-esc-delay 时间,如果有后续内容的话就按 input-decode-map 来处理。虽然不太优雅,但可能是应对 ANSI 事件的最好方法了。也翻到了个 SO 上的回答,也基本是这个思路:

https://stackoverflow.com/questions/48039759/how-to-distinguish-between-escape-and-escape-sequence

目前 Emacs read_key_sequence C 代码看得我有点头大,不知道实现起来麻不麻烦……

我这边倒是稳定出现奇怪输入(中括号)。可能和终端也有关?

另外,我测试的时候倒是发现箭头有两种 ANSI 表示法,cat - 回显的箭头是 ESC [ A/B/C/D 而 Emacs 下接收的是 ESC O A/B/C/D,也许 Emacs 协商下 ESC [ 开头的 ANSI 事件基本没有了,所以不会触发?我也只是在切窗口事件时会有问题。

vt100的箭头用的是ESC O,但是现代终端一般都转译成ESC [,关于用哪些序列它有一套很复杂的陈年协议(tl;dr)。不同终端的支持不一样,有的支持软件声明控制序列用的字符集,有的没做。

1 个赞

去把 Evil 支持 TUI ESC 的代码改了一下,似乎勉强算是实现了对终端下 M-[ 以及 ESC [ I 的区分:

这个方法比 Evil 的其实更加 hack 一点。因为 Emacs 的 Escape 键在 GUI 下发送的是 escape 事件,而 TUI 下的则是 ESC 字符,所以茴字 Escape 键有两种绑法。

  • Evil 可以绑定到 escape 事件上从而避免 ESC 绑定覆盖终端 ANSI 事件。而 Evil 对 input-decode-map 的改动实际上也是把检测到的用户 Escape 按键映射到了 escape 事件,“分流”了按键与 ANSI 的处理。
  • 但是 M-OM-[ 则只有一种绑法,因此按键和 ANSI 的处理必然会相互影响。
  • 这里的 hack 就是手动添加 meta-big-o 这种自定义事件并 advice define-key 使所有试图绑定到 M-O 的调用实际绑定到 meta-big-o 上,这样就可以通过 input-decode-map 来分流了。

目前用起来暂时没出现什么异样,至少打开终端时终于不会有莫名其妙的中括号了。

3 个赞

感慨一下楼主的 ansi-workaround 的 hack 很神奇,刚才又仔细做了一番测试。我用的是 kitty 终端。绑定 M-O 为命令后,会导致 f1-f12 和方向键键位绑定不可用。绑定 M-[ 键位后,则会导致触摸板不可用,但是方向键和 f1-f12 是可用的。

所以似乎是 kitty 终端会把 F1-F12 和方向键按键发送到 ESC O 开头的序列,而把鼠标事件发送到 ESC [ 开头的序列。

1 个赞