终端中的行与列
在终端模拟器中,似乎有一些关于“行”和“行数”的概念,我想了解更多。
行与行数的示例
下面的Python脚本显示了三行'a',然后暂停,再显示三行'b'。
import sys, struct, fcntl, termios
write = sys.stdout.write
def clear_screen(): write('\x1b[2J')
def move_cursor(row, col): write('\x1b['+str(row)+';'+str(col)+'H')
def current_width(): #taken from blessings so this example doesn't have dependencies
return struct.unpack('hhhh', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, '\000' * 8))[1]
clear_screen()
for c in 'ab':
#clear_screen between loops changes this behavior
width = current_width()
move_cursor(5, 1)
write(c*width+'\n')
move_cursor(6, 1)
write(c*width+'\n')
move_cursor(7, 1)
write(c*width+'\n')
sys.stdout.flush()
try: input() # pause and wait for ENTER in python 2 and 3
except: pass
如果在这个暂停期间把终端窗口的宽度缩小一个字符,你会看到
这看起来很合理——每一行都被单独换行了。当我们再次按下回车键打印b时,
一切都按预期工作。我使用了绝对光标定位,并且写入了之前写入的相同行——当然不会覆盖所有的a,因为很多a在其他行上。
然而,当我们再把窗口缩小一个字符时,换行的效果就不同了:
为什么第二行和第三行的b会合并在一起,为什么最后一行的与第一行的合并了?
在上面可见的行中有两个,这给了我们一些提示——这两行仍然是连接在一起的——当然如果我们再移动窗口,那一行会继续以相同的方式换行。即使是我们替换了整行的行,这种情况似乎也在发生。
事实证明,之前换行的行现在与它们对应的父行连接在一起;当我们把终端宽度扩大很多时,这种关系就更明显了:
我的问题
实际上,我的问题是如何防止或预测这些行被合并成一行的情况。清空整个屏幕可以消除这种行为,但如果可能的话,我希望只针对需要的单独行这样做,这样我可以保持按行缓存,这大大加快了我的应用程序。清空到一行的末尾可以将该行与下面的行断开,但清空到一行的开头并不能将该行与上面的行断开。
我很好奇——这些行到底是什么?我可以在哪里阅读到相关内容?我能否找出哪些行是同一行的一部分?
我在terminal.app和iterm中观察到了这种行为,无论是否使用tmux。我想深入研究这些内容,即使没有规范,我也能找到答案——但我想应该有相关的规范!
背景:我想制作一个终端用户界面,可以预测当用户缩小窗口宽度时,终端的换行方式。我知道像全屏模式(tput smcup
,或者python -c 'print "\x1b[?1049h"
,这是ncurses使用的)可以防止换行,但我不想在这里使用它。
编辑:我已经更清楚地表明我理解脚本的覆盖行为,并想要解释换行行为。
2 个回答
正如0x783czar所指出的,关键的区别在于是否打印了一个明确的换行符,这导致终端开始新的一行,或者因为右边没有空间了,想打印的字符就溢出了。
记住这一点很重要,特别是在每行的末尾,这关系到复制粘贴时是否会在缓冲区中有换行符,很多终端中三次点击选中行为,以及当窗口大小改变时内容的重新换行(在支持这个功能的终端中)。
在终端中运行的应用程序几乎不在乎这个区别,它们通常把“行”和“列”这两个词混用。因此,当我们在gnome-terminal中实现窗口大小改变时的内容重新换行时,我们更倾向于用“行”或“列”来表示终端中的一行视觉内容,而用“段落”来表示两个相邻换行符之间的内容。如果一个段落比终端宽,它会换成多行显示。(这并不是官方术语,但我认为这样说很合理,有助于讨论这些概念。)
好的。让我们先来看看你遇到的情况的原因:
我测试了你的代码,发现这个问题只在你调整窗口大小的时候发生。当窗口不动时,它会正常输出字母a,按下回车后会用字母b覆盖它们(我猜这就是你想要的效果)。
看起来发生的情况是,当你在中间调整窗口大小时,行的索引发生了变化,所以在下一次循环时,你不能再相信调用move_cursor()时的坐标了。
有趣的是,当你调整窗口大小时,文字会因为换行而向上推,这样光标前的文本就会被推上去。我猜这和终端模拟器的代码有关(因为我们通常希望光标保持在可见状态,如果光标在屏幕底部,调整窗口大小可能会把它遮住)。
你会注意到,在调整大小后按下回车时,只有两行字母a是可见的(而不是三行)。这似乎是因为:
首先,我们从初始输出开始。(为了清晰起见,添加了行号)
1
2
3
4
5 aaaaaaaaaaaaaaa\n
6 aaaaaaaaaaaaaaa\n
7 aaaaaaaaaaaaaaa\n
8
注意,每行的末尾都有一个换行符(这就是为什么你的光标看起来在最后一行下面,尽管你没有再次移动光标)。
当你把窗口缩小一个字符时,会发生这样的情况:
1
2 aaaaaaaaaaaaaa
3 a\n
4 aaaaaaaaaaaaaa
5 a\n
6 aaaaaaaaaaaaaa
7 a\n
8
你会注意到我所说的“推上去的文本”。
现在,当你按下回车并且循环重新开始时,光标被移动到第5行第1列(根据你的代码),并直接覆盖了第二行最后一个字母a。当它开始写字母b时,会把第二行最后一个字母a和后面的行也覆盖掉。
1
2 aaaaaaaaaaaaaa
3 a\n
4 aaaaaaaaaaaaaa
5 bbbbbbbbbbbbbb\n
6 bbbbbbbbbbbbbb
7 bbbbbbbbbbbbbb\n
8
重要的是,这也覆盖了第二行字母a末尾的换行符。这意味着现在第二行字母a和第一行字母b之间没有换行符,所以当你扩大窗口时,它们看起来就像是一行。
1
2
3
4
5 aaaaaaaaaaaaaaa\n
6 aaaaaaaaaaaaaabbbbbbbbbbbbbb\n
7 bbbbbbbbbbbbbbbbbbbbbbbbbbbb\n
8
我不太确定为什么这第二行的字母b也会被合并,但这可能和第一个字母b覆盖的字母a缺少换行符有关。不过,这只是我的猜测。
如果你再把窗口缩小一个字符,你会发现有两个字符的换行,这是因为现在你在缩小同一行文本的两个部分,这意味着一个部分会推着另一个部分,导致最后出现两个字符而不是一个。
例如:在我展示的这些测试窗口中,宽度开始是15个字符,然后我把它缩小到14并打印字母b。仍然有一行字母a是15个字符长,现在有一行14个字母a和14个字母b,换行在14个字符处。出于某种原因,最后两行字母b也是如此(它们是一行28个字符,换行在14)。所以当你再把窗口缩小一个字符(到13个字符)时:第一行15个字母a现在有两个尾随字符(15 - 13 = 2);接下来的28个字符现在必须适应一个宽度为13的窗口(28 / 13 = 2 余 2),最后的字母b也是如此。
0 aaaaaaaaaaaaa
1 aa\n
2 aaaaaaaaaaaaa
3 abbbbbbbbbbbb
4 bb\n
5 bbbbbbbbbbbbb
6 bbbbbbbbbbbbb
7 bb\n
8
为什么会这样工作?:
这种情况是你在尝试在另一个程序中运行你的程序时遇到的困难,这个程序有能力根据需要重新定位文本。在调整大小时,你的索引变得不可靠。你的终端模拟器试图为你处理重新对齐,并在滚动中上下移动光标前的文本,以确保你始终可以看到你的活动提示。
行和列是由终端/终端模拟器定义的,如何解释它们的位置取决于它。当给出适当的控制序列时,终端会相应地解释它们以进行正确显示。
注意,有些终端的行为不同,在模拟终端中,通常有一个设置可以更改它模拟的终端类型,这也可能影响某些转义序列的响应。这就是为什么UNIX环境通常有一个设置或环境变量($TERM),告诉它正在与哪种类型的终端通信,以便知道发送什么控制序列。
大多数终端使用标准的ANSI兼容控制序列,或者基于DEC VT系列硬件终端的系统。
在Terminal.app的偏好设置中,进入Preferences->Settings->Advanced,你实际上可以看到(或更改)你的窗口正在模拟的终端类型,旁边有一个下拉菜单“Declare terminal as:”
如何克服这个问题:
你可以通过存储最后已知的宽度并检查是否有变化来减轻这个问题。在这种情况下,你可以改变你的光标逻辑以适应变化。
或者,你可以考虑使用设计用于相对光标移动的转义序列(而不是绝对的),以避免在调整大小后意外覆盖之前的行。还有一种能力是使用转义序列保存和恢复特定的光标位置。
Esc[<value>A Up
Esc[<value>B Down
Esc[<value>C Forward
Esc[<value>D Backward
Esc[s Save Current Position
Esc[u Restore Last Saved Position
Esc[K Erase from cursor position to end of line
然而,你不能真正保证所有终端模拟器在窗口调整大小时的处理方式是相同的(据我所知,这并不是任何终端标准的一部分),或者将来不会改变。如果你希望制作一个真正的终端模拟器,我建议你先设置好你的GUI窗口,以便你可以控制所有的调整大小逻辑。
但是如果你想在终端模拟器窗口中运行,并处理你正在编写的命令行工具的窗口调整大小问题。我建议你看看Python的curses库。这是我所知道的所有能够处理这种变化的窗口调整大小程序(如vim、yum、irssi)所使用的功能。虽然我个人没有使用过它。
它可以通过curses
模块在Python中使用。
(如果你打算重新分发你的程序,请考虑用Python3编写。为了孩子们 :D)
资源:
这些链接可能会对你有帮助:
希望这能帮到你!