1. Python Curses 编程

官方文档:https://docs.python.org/3/howto/curses.html

1.1. curses 是什么?

curses 库为基于文本的终端提供独立于终端的屏幕绘制和键盘处理功能;这些终端包括 VT 100s、Linux 控制台以及各种程序提供的模拟终端。显示终端支持各种控制代码,以执行常见操作,比如移动光标、滚动屏幕和擦除区域。不同终端使用相差很大的代码,并且往往有自己的小怪癖。

在图形显示的世界中,有人可能问“为什么自找麻烦”?毕竟字符单元显示终端确实是过时的技术,但在某些领域,能用它们做出花样仍然具有价值。其中一个领域是在未运行 X 服务的小型或嵌入式 Unix 上。另一个领域是在任何图形支持可用之前,需要运行的工具,比如 OS 安装器和内核配置器。

curses 库提供相当基本的功能,为程序员提供包含多个非重叠文本窗口的显示抽象。可以通过多种方式更改窗口的内容,比如添加文本、擦除文本、更改外观等。curses 库将计算需要向终端发送哪些控制代码,以生成正确的输出。curses 未提供诸多用户界面概念,比如按钮、复选框或对话框;如果需要这些功能,请考虑使用用户界面库,比如 Urwid

curses 库最初是为 BSD Unix 编写的;来自 AT&T 的后期 System V 版本的 Unix 添加许多增强和新功能。如今,BSD curses 已不再维护,已经被 ncurses 替代,后者是 AT&T 接口的开源实现。如果使用的是开源 Unix,比如 Linux 或 FreeBSD,那么系统几乎肯定使用 ncurses。由于大多数当前的商业 Unix 版本都基于 System V 代码,因此这里描述的所有功能可能都可用。但是,某些专有 Unix 所搭载的旧版本 curses 可能不支持所有功能。

Windows 版本的 Python 不包含 curses 模块,一个可用的移植版本是 UniCurses

1.1.1. Python curses 模块

Python 模块是对 curses 提供的 C 函数的简单包装;如果已经熟悉 C 中的 curses 编程,那么将这些知识转移到 Python 中非常容易。最大的区别在于,Python 接口通过将不同的 C 函数(比如 addstr()、mvaddstr() 和 mvwaddstr())合并成一个 addstr() 方法的方式,使事情更加简单。稍后,将看到更详细的介绍。

本 HOWTO 是关于使用 curses 和 Python 编写文本模式程序的介绍。不是 curses API 的完整指南;对于完整指南,请查看 Python 库指南中关于 ncurses 的部分,以及 ncurses 的 C 手册页面。不过,本文将为你提供基本思路。

1.2. 开始和结束 curses 应用程序

在做任何事情前,必需初始化 curses。通过调用 initscr() 函数,完成此工作,它将确定终端类型,向终端发送任何必需的设置代码,并且创建各种内部数据结构。如果成功,initscr() 将返回表示整个屏幕的窗口对象;通常根据相应的 C 变量,命名为 stdscr

通常,curses 应用程序关闭将按键自动回显到屏幕的功能,以便能读取按键,并且在特定情况下,显示它们。这需要调用 noecho() 函数。

应用程序通常还需要立即响应按键,而无需按 Enter 键;这被称为 cbreak 模式,与通常的缓冲输入模式相对应。

终端通常将特殊键,比如光标键或导航键(比如 Page Up 和 Home),作为多字节转义序列返回。虽然可以编写应用程序期望此类序列,并且相应地处理它们,但 curses 可以完成此操作,返回诸如 curses.KEY_LEFT 之类的特殊值。要让 curses 执行该任务,必须启用 keypad 模式。

终止 curses 应用程序比启动一个更容易。只需调用:

还原对终端作出的 curses 友好设置。然后,调用 endwin() 函数将终端恢复到其原始操作模式。

调试 curses 应用程序时,常见的问题是,当应用程序在未将终端恢复到先前状态的情况下崩溃时,终端将出现混乱。在 Python 中,这通常发生在代码存在 Bug,并且引发未捕获异常的时候。比如,当键入按键时,它们不再被回显在屏幕,这使得 shell 变得难用。

在 Python 中,可以通过导入 curses.wrapper() 函数,并且像下面一样使用它的方式,避免这些复杂性,使调试变得更容易:

wrapper() 函数接受一个可调用对象,并且执行上述初始化操作,如果支持颜色,那么也初始化颜色。然后 wrapper() 运行提供的可调用对象。一旦可调用对象返回,wrapper() 将恢复终端的原始状态。在捕获异常,恢复终端状态,然后重新抛出异常的 try...except 内部调用可调用对象。因此,在异常情况下,终端不会处于奇怪的状态,并且能够读取异常的消息和回溯信息。

1.3. 窗口和面板

窗口是 curses 中的基本抽象。窗口对象代表屏幕的一个矩形区域,并且支持显示文本、擦除文本、允许用户输入字符串等方法。

initscr() 函数返回的 stdscr 对象是覆盖整个屏幕的窗口对象。许多程序可能仅需该窗口,但你可能希望将屏幕切分成更小的窗口,以便单独地重绘或清除它们。newwin() 函数创建给定大小的新窗口,返回新窗口对象。

注意,curses 中使用的坐标系统与平常不同。总是按照 y,x 的顺序传递坐标,窗口的左上角是坐标 (0, 0)。这打破了处理坐标的常规约定,即先处理 x 坐标。这与大多数其它计算机应用程序的做法不同,但自从 curses 诞生以来,就是如此,现在更改,为时已晚。

应用程序可以使用 curses.LINES 和 curses.COLS 获取屏幕的 y 和 x 尺寸。合法的坐标从 (0, 0) 延伸到 (curses.LINES - 1, curses.COLS - 1)。

当调用用于显示或擦除文本的方法时,效果不会立即显示。相反,必须调用窗口对象的 refresh() 方法,更新屏幕。

这是因为 curses 最初是为较慢的 300 波特终端连接编写的;对于这些终端,最小化重新绘制屏幕所需的时间非常重要。curses 积累对屏幕的更改,在调用 refresh() 时,以最高效的方式显示它们。比如,如果程序在窗口中显示一些文本,然后清除窗口,那么无需发送原始文本,因为它们从未可见。

实际上,显式地告诉 curses 重绘窗口不会使 curses 编程变得更复杂。多数程序进入一系列活动,然后暂停等待按键或其它用户侧动作。你需要做的全部事情是确保在暂停等待用户输入之前,重绘屏幕,只需先调用 stdscr.refresh() 或其它相关窗口的 refresh() 方法。

面板是特殊的窗口;它可以比实际的显示屏幕更大,每次只显示面板的一部分。创建面板需要面板的高度和宽度,刷新面板需要给定将被展示的面板子区域所在的屏幕上的区域的坐标。

refresh() 在屏幕上的从坐标 (5,5) 到坐标 (20,75) 的矩形区域内显示面板的一部分;被显示的部分的左上角是面板的坐标 (0, 0)。除上述差异外,面板与普通窗口完全相同,支持相同的方法。

如果屏幕上有多个窗口和面板,那么有一种更有效的方法,来更新窗口,避免每部分单独更新时导致烦人的屏幕闪烁。refresh() 实际上做两件事:

可以在多个窗口上调用 noutrefresh(),更新数据结构,然后调用 doupdate() 更新屏幕。

1.4. 显示文本

从 C 程序员的角度来看,curses 有时看起来像一堆略有差异的函数组成的扭曲迷宫。比如,addstr() 在 stdscr 窗口的当前光标位置显示字符串,而 mvaddstr() 在显示字符串之前,先移动到给定的 y,x 坐标。waddstr() 与 addstr() 类似,但允许指定要使用的窗口,而非默认使用 stdscr。mvwaddstr() 允许同时指定窗口和坐标。

幸运的是,Python 接口隐藏了所有这些细节。stdscr 和其它任何窗口一样是一个窗口对象,并且诸如 addstr() 之类的方法接受多种参数形式。通常有四种不同的形式。

形式描述
str 或 ch在当前位置显示字符串 str 或字符 ch
str 或 ch,attr在当前位置,使用属性 attr,显示字符串 str 或字符 ch
y,x,str 或 ch在窗口内,移动到位置 y,x;然后显示 str 或 ch
y,x,str 或 ch,attr在窗口内,移动到位置 y,x;然后使用属性 attr,显示字符串 str 或字符 ch

属性允许用加粗、下划线、反相或彩色等突出形式显示文本。下一小节将更详细地解释它们。

addstr() 方法接受 Python 字符串或字节字符串作为要显示的值。字节字符串的内容将被原样地发送到终端。使用窗口的 encoding 属性值将字符串编码成字节;默认情况下,使用由 locale.getencoding() 返回的默认系统编码。

addch() 方法接受字符作为参数,参数可以是长度为 1 的字符串、长度为 1 的字节字符串或整数。

对于扩展字符有一些常量;这些常量是大于 255 的整数。比如 ACS_PLMINUS 是 "+/-" 符号,ACS_ULCORNER 是框的左上角(对于绘制边框很方便)。也可以使用适当的 Unicode 字符。

窗口记忆上次操作后光标停留的位置,因此如果省略 y,x 坐标,那么将在上次操作结束的位置显示字符串或字符。也可以使用 move(y,x) 方法移动光标。因为有些终端始终显示闪烁的光标,你可能希望确保光标定位在某个不会使人分散注意力的位置;光标在某个看似随机的位置闪烁可能令人困惑。

如果应用程序根本不需要闪烁的光标,可以调用 curs_set(False) 使其不可见。为与旧版 curses 兼容,leaveok(bool) 函数是 curs_set() 的同义词。当 bool 为真时,curses 库将尝试抑制闪烁的光标,无需担心其停留在奇怪的位置。

1.4.1. 属性和颜色

可以用不同的方式显示字符。在基于文本的应用程序中,常常反相显示状态行,文本查看器可能需要突出显示特定的单词。curses 通过允许为屏幕上的每个单元格指定属性的方式,支持该功能。

属性是一个整数,每位代表不同的属性。可以尝试显示具有多个属性位设置的文本,但 curses 不保证所有可能的组合可用,或者它们在视觉上有明显的不同。这取决于所使用的终端的能力,所以最稳妥的方式是只使用下面列出的最常见的可用属性。

属性描述
A_BLINK闪烁文本
A_BOLD超亮或加粗文本
A_DIM半明亮文本
A_REVERSE反相显示文本
A_STANDOUT可用的最佳突出显示模式
A_UNDERLINE带下划线的文本

因此,为在屏幕的顶部显示反相状态行,需要编写代码:

curses 库还支持在提供颜色的终端上使用颜色。最常见的这种终端可能是 Linux 控制台,其次是彩色 xterm。

为使用颜色,必须在调用 initscr() 后,尽快调用 start_color() 函数,以初始化默认颜色集(curses.wrapper() 自动地执行该操作)。完成该操作后,如果使用的终端可以显示颜色,那么 has_colors() 函数返回 TRUE。(注意:curses 使用美式拼写 “color”,而非加拿大/英式拼写 “colour”。如果你已习惯英式拼写,那么需要避免拼写错误。)

curses 库维护有限数量的颜色对,每个颜色对包含一个前景(或文本)颜色和一个背景颜色。可以使用 color_pair() 函数获取与颜色对对应的属性值;这可以与其它属性(如 A_REVERSE)进行按位 OR 操作,但是再强调一遍,不保证这种组合在所有终端上都生效。

下面的示例使用颜色对 1 显示一行文本。

如前所述,颜色对由一个前景颜色和一个背景颜色组成。init_pair(n, f, b) 函数将颜色对 n 的定义更改为前景颜色 f 和背景颜色 b。颜色对 0 硬编码为黑底白字,不能更改。

颜色已被编号,start_color() 在激活颜色模式时,初始化 8 种基本颜色。它们是:0:黑色,1:红色,2:绿色,3:黄色,4:蓝色,5:洋红色,6:蓝绿色,7:白色。curses 模块为这些颜色定义了名称常量:curses.COLOR_BLACKcurses.COLOR_RED 等。

我们做个综合练习。为将颜色 1 更改为白色背景上的红色文本,应该调用:

更改颜色对时,已经使用该颜色对显示的任何文本都将更改为新颜色。可以使用以下方式在该颜色中显示新文本:

非常高级的终端可以将实际颜色的定义更改为给定的 RGB 值。这使你可以将通常是红色的颜色 1 更改为紫色、蓝色或任何其它你喜欢的颜色。不幸的是,Linux 控制台不支持该功能,因此我无法尝试,不能提供任何示例。你可以通过调用 can_change_color() 的方式,检查你的终端是否支持该功能,如果有该功能,那么该函数返回 True。如果你拥有这样强大的终端,那么查阅系统的手册页面,获取更多信息。

1.5. 用户输入

C curses 库仅提供非常简单的输入机制。Python curses 模块支持基本的文件输入控件。(其它库,比如 Urwid,拥有更丰富的控件集。)

从窗口获取输入的方法有两种:

使用 nodelay() 窗口方法可以不等待用户。在 nodelay(True) 之后,窗口的 getch() 和 getkey() 方法将变为非阻塞的。为表明输入未就绪,getch() 返回 curses.ERR,getkey() 引发异常。此外还有 halfdelay() 函数,它其实被用于在每个 getch() 上设置定时器;如果在指定的延迟内(以十分之一秒为单位)输入未就绪,那么 curses 将引发异常。

getch() 方法返回一个整数;如果它在 0 到 255 之间,那么它表示所按键的 ASCII 码。大于 255 的值是特殊键,比如 Page Up、Home 或光标键。可以将返回的值与诸如 curses.KEY_PPAGE、curses.KEY_HOME 或 curses.KEY_LEFT 之类的常量进行比较。程序的主循环看起来可能像这样:

curses.ascii 模块提供 ASCII 类成员函数,它们接受整数或 1 个字符的字符串参数;这些函数在为这样的循环编写更具可读性的测试时可能很有用。它还提供转换函数,这些函数接受整数或 1 个字符的字符串参数,返回相同的类型。比如 curses.ascii.ctrl() 返回与其参数相对应的控制字符。

还有一个提取整个字符串的方法,getstr()。它不经常被使用,因为其功能相当有限;可用的编辑键只有 Backspace 键和终止字符串的 Enter 键。还可以选择性地限制为固定数量的字符。

curses.textpad 模块提供一个文本框,它支持类似 Emacs 的键绑定集。Textbox 类的各种方法支持带输入验证的编辑,包含或不包含末尾空格地收集编辑结果。下面是一个示例:

查看关于 curses.textpad 的库文档,获取更多细节。


2. ANSI 非常规字符序列

2.1. 什么是 ANSI 非常规字符序列,它的作用是什么,使用时有什么注意事项?

ChatGPT:ANSI 非常规字符序列是一种特殊的字符序列,用于在控制台或终端中控制文本的格式和外观。它们通常以特殊的转义序列开始,并以特殊的控制字符或参数结尾。

这些非常规字符序列的作用是可以在控制台或终端中实现一些高级文本处理功能,例如更改文本的颜色、移动光标位置、清除屏幕等。它们在命令行界面、文本编辑器和一些程序中都有广泛的应用。

在使用 ANSI 非常规字符序列时,需要注意以下几点:

  1. 不同的终端和操作系统可能支持的非常规字符序列不同,因此需要根据具体的环境和要求使用适当的序列。
  2. ANSI 非常规字符序列通常以转义字符(例如 ESC)开头,因此需要注意在代码中正确地引用和处理这些字符。
  3. 非常规字符序列可以包含参数,例如颜色代码或光标位置,因此需要注意正确处理这些参数,以避免出现意外结果。
  4. 在使用 ANSI 非常规字符序列时,需要确保它们不会与文本内容冲突或干扰其他代码的正常执行。

2.2. 常用的 ANSI 非常规字符序列有哪些?

ChatGPT:下面列举了一些常用的 ANSI 非常规字符序列及其功能:

  1. 清空屏幕:"\033[2J",将光标移动到左上角:"\033[1;1H"。

  2. 控制文本颜色:

    • 设置前景色(文字颜色):"\033[30m"(黑色)、"\033[31m"(红色)、"\033[32m"(绿色)、"\033[33m"(黄色)、"\033[34m"(蓝色)、"\033[35m"(洋红色)、"\033[36m"(青色)、"\033[37m"(白色)。
    • 设置背景色:"\033[40m"(黑色)、"\033[41m"(红色)、"\033[42m"(绿色)、"\033[43m"(黄色)、"\033[44m"(蓝色)、"\033[45m"(洋红色)、"\033[46m"(青色)、"\033[47m"(白色)。
    • 重置颜色:"\033[0m"。
  3. 控制光标位置:

    • 移动光标到指定位置:"\033[y;xH",其中 y 和 x 分别表示行和列数。
    • 向上/下/左/右移动光标:"\033[nA"、"\033[nB"、"\033[nC"、"\033[nD",其中 n 表示移动的距离。
  4. 隐藏/显示光标:"\033[?25l"(隐藏)和"\033[?25h"(显示)。

注意:不同的终端和操作系统可能支持的非常规字符序列不同,因此需要根据具体的环境和要求使用适当的序列。