5

终端仿真器中似乎有一些行与行的概念,我想了解更多。

演示我所说的行与行的意思

下面的 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

在此处输入图像描述

如果在此中断期间将终端窗口宽度缩小一个字符,您会看到

在此处输入图像描述

这似乎很合理——每一行都被单独包装。当我们再次按回车键打印bs 时,

在此处输入图像描述

一切都按预期工作。我使用了绝对光标定位,并写入了我之前写入的相同行——这当然不会覆盖所有的 a,因为它们中的许多都在其他行上。

但是,当我们将窗口再缩小一个字符时,换行的工作方式会有所不同:

在此处输入图像描述

为什么第二行和第三行bwrap在一起,为什么'的最后一行a和b'的第一行合并了?为什么在上面可见的顶部行中给出了一个提示——我们看到了两个a,因为它们的两行仍然是链接的——当然,如果我们再次移动窗口,那一行将继续以相同的方式换行。即使对于我们替换了一整行的行,这似乎也在发生。

事实证明,之前包装的行现在链接到它们相应的父行;更明显的是,一旦我们将终端扩大很多,它们属于同一条逻辑线:

在此处输入图像描述

我的问题

实际上,我的问题是如何防止或预测这些行组合成行。清除整个屏幕可以消除这种行为,但如果可能的话,最好只对需要它的个别行执行此操作,这样我就可以按行保持缓存,这显着加快了我的应用程序。清除到行尾会取消该行与其下方行的链接,但清除到行首不会取消该行与其上方行的链接。

我很好奇 - 这些线是什么?我在哪里可以了解它们?我可以找出哪些行是同一行的一部分吗?

我已经使用 terminal.app 和 iterm 观察到了这种行为,使用和不使用 tmux。我想即使没有规范,深入研究其中任何一个都会产生答案——但我想某处有规范!


背景:我想制作一个终端用户界面,如果用户减小窗口宽度,它可以预测终端换行的方式。我知道全屏模式( ncurses 使用的 'tput smcuppython -c 'print "\x1b[?1049h"')之类的东西可以防止换行,但不想在这里使用它。

编辑:更清楚地表明我已经了解脚本的覆盖行为并想要解释包装行为。

4

2 回答 2

7

好的。让我们从您所看到的行为的原因开始:

我测试了您的代码并注意到它仅在您调整窗口大小时发生。当窗口被单独留下时,它会写出 a's,然后按 enter 会用 b's 覆盖它们(我认为这是预期的行为)。

似乎正在发生的事情是,当您在中途调整窗口大小时,线索引会发生变化,因此在您的下一次迭代中,当您调用 move_cursor() 时,您不能信任相同的坐标。

有趣的是,当您调整窗口大小时,自动换行会将光标前的文本向上推。我假设这是终端仿真器代码的一部分(因为我们几乎总是希望将焦点保持在光标上,并且如果光标位于屏幕底部,如果自动换行将其向下推,则调整大小可能会使其超出窗口高度)。

您会注意到,当您按下回车键调整大小后,只有两行 a 仍然可见(而不是全部 3 行)。以下是似乎正在发生的事情:

首先,我们从初始输出开始。(为清楚起见添加了行号)

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

你会注意到我所说的“向上推动文本”的意思

现在,当您按 Enter 并重复循环时,光标将发送到第 5 行第 1 行(由您的代码指定)并直接放置在第二行的最后 a 上。当它开始写 b's 时,它会用 b's 覆盖第二行的最后一个 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 的第二行也被放在一起,但它似乎与第一个覆盖的 a 行现在缺少它自己的换行终止这一事实有关。然而,这只是一个猜测。

如果您尝试将窗口缩小另一个字符,则会得到两个换行字符的原因是因为现在您正在缩小同一行文本的两半,这意味着一个推动另一个,导致两个字符最后一个。

例如:在我展示的这些测试窗口中,宽度从 15 个字符开始,然后我将其缩小到 14 并打印出 b。仍然有一行 15 个字符长的 a,现在一行 14 个 a 和 14 b 行换行为 14 个字符。最后两行 b 也是如此(出于某种原因)(它们是一行 28 个字符,包装在 14 处)。因此,当您将窗口再缩小一个字符(降至 13 个)时:15 个 a 的第一行现在有两个尾随字符(15 - 13 = 2);下一行 28 个字符现在必须适合 13 个字符宽的窗口(28 / 13 = 2 R2),最后一个 b 也是如此。

0 aaaaaaaaaaaaa
1 aa\n
2 aaaaaaaaaaaaa
3 abbbbbbbbbbbb
4 bb\n
5 bbbbbbbbbbbbb
6 bbbbbbbbbbbbb
7 bb\n
8

为什么它会这样工作?:

当您尝试在另一个程序中运行您的程序时,您会遇到这类问题,该程序有权根据需要重新定位文本。如果调整大小,您的索引将变得不可靠。您的终端模拟器正在尝试为您处理重新对齐,并在回滚中上下推动提示(固定在第 8 行)之前的文本,以确保您始终可以看到您的活动提示。

行和列是由终端/终端仿真器定义的,由它来相应地解释它们的位置。当给出适当的控制序列时,终端会相应地解释它们以正确显示。

请注意,某些终端的行为确实不同,并且在模拟终端中,通常有一个设置来更改它正在模拟的终端类型,这也可能影响某些转义序列的响应方式。这就是为什么 UNIX 环境通常有一个设置或环境变量 ($TERM),它告诉它正在与哪种类型的终端通信,以便它知道要发送什么控制序列。

大多数终端使用符合 ANSI 标准的控制序列,或基于 DEC VT 系列硬件终端的系统。

在 Preferences->Settings->Advanced 下的 Terminal.app 首选项中,您实际上可以在“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

但是,您无法真正保证所有终端仿真器都会以相同的方式处理窗口大小调整(这实际上不是任何终端标准 AFAIK 的一部分),或者将来不会改变。如果您希望制作一个真正的终端模拟器,我建议您首先设置您的 GUI 窗口,以便您可以控制所有调整大小的逻辑。

但是,如果您想在终端仿真器窗口中运行并处理正在编写的给定命令行实用程序的缓解窗口调整大小。我建议查看 python 的 curses 库。这是我所知道的所有窗口调整大小感知程序(vim、yum、irssi)使用的那种功能,并且可以处理这种变化。虽然我个人没有任何使用它的经验。

它可通过curses模块用于 python。

(如果你打算重新分发你的程序,请考虑用 Python3 编写它。为孩子们做:D)

资源:

这些链接可能会有所帮助:

我希望这会有所帮助!

于 2014-09-09T05:30:44.093 回答
1

正如 0x783czar 指出的那样,关键区别在于是否打印了显式换行符导致终端开始新行,或者由于右侧没有空间打印所需字符而导致隐式溢出。

重要的是要在每行的末尾记住这一点,以便复制粘贴(缓冲区中是否有换行符)、在许多终端中的三次单击突出显示行为以及在窗口中重新包装内容调整大小(在那些支持它的终端中)。

在终端内运行的应用程序几乎不关心这种差异,它们可以互换使用“行”和“行”这两个词。因此,当我们在 gnome-terminal 中实现 resize 时重新包装内容时,我们更喜欢用“行”或“行”来表示终端的一个视觉线条,而“段落”则表示两个相邻换行符之间的内容。如果段落比终端宽,则段落将换行成多行。(这绝不是官方术语,但 IMO 非常合理,有助于讨论这些概念。)

于 2015-04-22T22:06:28.903 回答