所以这里是一个关于ANSI转义代码和终端控制的简短教程,因为你们非利士人不会停止使用ncurse,哦,我的上帝,为什么我们还在使用ncurses?这他妈的已经是二十一世纪了。
终端仿真器处理颜色和光标形状等奇特东西的方式并不是某种神秘的、不透明的黑匣子,您只能通过库才能访问它。访问这些功能实际上非常简单;甚至可以将它们硬编码到文本文件中,并用cat或更少的代码显示。或者甚至卷曲!实现这一点的方法是使用一种称为ANSI转义序列的方法。
终端中几乎所有的UI更改都是通过带内信令完成的。这些信号由ASCII/UTF-8字符<ESC>(0x1B或27)触发。它与您按键盘上的Esc键或涉及Alt键的键序列时发送到终端的<Esc>字符相同。(例如,键入将非常快速地连续发送字符;这就是为什么在您按下退出键之后,您会注意到某些终端程序会出现延迟-它正在等待尝试确定用户是按Escape键还是按Alt键和弦。)。
对于这些转义,我们能做的最简单的事情就是将文本设置为粗体(或明亮)。为此,我们向终端发送<Esc>字符,后跟[1M。[是向终端指示我们要发送的转义类型的字符,1表示粗体/明亮模式,m是格式化转义的控制字符。
在此转义序列之后发送的所有文本都将是粗体的,直到我们再次显式地将其关闭(即使您的程序终止)。有两种方法可以关闭明亮模式:完全清除格式化,使用不带参数或参数0的m格式命令,或者更具体地说,使用21m命令清除粗体位。(您会注意到,通常可以通过在同一数字前面加上2来关闭模式。)。
#include<;unistd.h>;#定义szstr(Str)str,sizeof(Str)int main(){write(1,szstr(";纯文本-\x1b[1mboltext\x1b[0m-纯文本";));}此处的\x1b转义是将十六进制字符0x1B(<Esc>)插入字符串的C字符串转义。如果你不习惯于阅读带有显式转义的源代码,它就有点难看和不可读。不过,你可以通过几个定义让事情变得不那么可怕:
#include<;unistd.h>;#定义szstr(Str)str,sizeof(Str)#定义纯文字";0";/*或";";*/#定义无";2";#定义亮线";1";#定义暗";2";#定义斜体";3";#定义下划线";4&。#DEFINE WITH";;";#DEFINE ANSI_ESC";\x1b";#DEFINE FMT(STYLE)ANSI_ESC";[";STYLE";m";INT Main(){WRITE(1,SZSTR(";纯文本-";FMT(BIGHT)";BIGHT TEXT";FMT(NOW BIGHT)";-";FMT(NOW BIGHT)";-";FMT(BIGHT)"。FMT(无暗色)";-";FMT(斜体)";斜体文本";FMT(无斜体)";-";FMT(反转)";反转视频";FMT(纯色)";-";FMT(下划线)";带下划线文本";FMT(无下划线));}这种方法的美妙之处在于,所有正确的序列都是在编译时生成的,这意味着编译器会将所有这些都转换为使用原始转义进行插值的单个字符串。它为编码器提供了更多的可读性,而对最终用户来说是零成本。
但是等一下,那个分号是从哪里来的?结果是,ANSI转义代码允许您为每个序列指定多个格式。您可以用;分隔每个命令。这将允许我们编写像fmt这样的格式化命令(用明亮的下划线,不加斜体),它在编译时转换为\x1b[4;1;23m。
当然,能够设置文本样式是远远不够的。我们还需要能够给它上色。颜色命令有两个组成部分:我们正在尝试更改的颜色以及我们要将其更改为什么颜色。前景和背景都可以单独指定颜色-无论Ncurses希望您相信什么,您都不必为要使用的每个前景-背景对定义颜色对。(#34;";";";";";##34;";##34;";##34;##34;";##34;##34;";##34;";)。这是一个荒谬的陈词滥调,他妈的21世纪的任何人都不应该受到限制。
要定位前景,我们发送字符3表示正常颜色或发送9表示明亮颜色;要定位背景,我们发送4表示正常或10表示明亮。然后是选择传统8种终端颜色之一的色码。
请注意,这里的明亮模式与我们前面提到的明亮模式既有相同之处,也有不同之处。在打开亮色模式时,如果将其设置为传统的8种颜色中的一种,则会自动将文本转换为亮色。如果将亮色设置为9或10,则不会自动将文本设置为粗体。
#include<;unistd.h>;#定义szstr(Str)str,sizeof(Str)#定义FG";3";#定义br_fg";9";#定义bg";4";#定义br_bg";10";#定义";;";#定义纯文本";";#定义黑色。#定义绿色";2";#定义黄色";3";#定义蓝色";4";#定义洋红色";5";#定义青色";6";#定义白色";7";#定义ansi_esc";\x1b";#定义fmt(Style)ansi_esc";[";style";Int main(){write(1,szstr(";纯文本-";fmt(FG蓝)";蓝色文本";fmt(纯)";-";fmt(br_fg蓝)";亮蓝色文本";fmt(纯)";-";fmt(br_bg红)";亮红色背景";fmt(纯)"。Fmt(带有br_bg洋红色的fg红色)";可怕的红色文本";fmt(普通));}当我们调用fmt(带有br_bg洋红色的fg红色)时,编译器会将其转换为命令字符串\x1b[31;105m。请注意,我们正在使用FMT(普通)(\x1b[m)清除此处的颜色;这是因为如果您尝试使用例如FMT(FG黑和BG白)重置颜色,您将覆盖将其终端配色方案设置为除该精确配色之外的任何颜色的用户的首选项。此外,如果用户碰巧有一个透明背景的终端,设置的背景颜色将在文本周围创建难看的色块,而不是让窗口后面的任何东西正确显示。
现在,虽然使用调色板颜色更有礼貌,因为它们是最终用户可以轻松配置的调色板(她可能比默认的严苛纯色版本更喜欢柔和的颜色,或者更改饱和度和亮度以更好地适应她的终端背景),但如果你正在做一些稍微有趣的UI方面的事情,你很快就会遇到这个限制。(如果你正在做一些稍微有趣的UI调色板,那么你很快就会遇到这个限制。),如果你正在做一些稍微有趣的UI调色板,最终用户可能会很容易地配置(她可能会比默认的严酷纯色版本更喜欢柔和的颜色,或者改变饱和度和亮度以更好地适应她的终端背景)。虽然您可以通过将颜色与样式命令混合使用来获得更多里程,但如果您希望在配色方案方面为用户提供任何可配置性(正如您应该做的那样),那么您将需要访问更广泛的调色板。
要从256色调色板中拾取,我们使用略有不同的转义方式:\x1b[38;5;(Color)m设置前景,\x1b[48;5;(Color)m设置背景,其中(Color)是我们要寻址的调色板索引。这些转义甚至比8+8颜色选择器更笨拙,所以拥有良好的抽象更加重要。
#include<;unistd.h>;#定义szstr(Str)str,sizeof(Str)#使用";;";#定义纯文本";;";#定义WFG(颜色)";38;5;";#color#定义wbg(颜色)";48;5;";#color#定义ansi_esc";\x1b"。Style";m";int main(){write(1,szstr(";纯文本-";fmt(WFG(198)with WBG(232))";fmt(纯文本)";-";fmt(wfg(232)with wbg(248))";浅灰色";fmt(纯文本)";-&。Fmt(wfg(248)with wbg(232))";浅灰色在深灰色上";fmt(平坦));}这里,节fmt(wfg(248)with wbg(232))翻译为\x1b[38;5;248;48;5;232m。为了简单起见,我们在这里对数字进行了硬编码,但经验法则是,任何时候您在终端中使用8位颜色时,都应该始终使其可由用户配置。
看起来不透明的指数实际上是非常系统的,您可以使用公式16+36*r+6*g+b来计算对特定颜色使用哪个指数,其中r、g和b是0到5之间的整数。指数232到255是从暗(232)到亮(255)的灰度渐变。
当然,这仍然是相当有限制性的。对于Windows上90年代的CD-ROM游戏来说,8位色可能已经足够了,但它早已过了有效期。使用真彩色要灵活得多。我们可以通过转义序列\x1b[38;2;(R);(G);(B)m来实现这一点,其中每个分量都是0到255之间的整数。
不幸的是,许多终端都不支持真彩色,不幸的是,urxvt也包括在内。因此,您的程序永远不应该依赖它,并将这些设置抽象出来由用户进行配置。默认为8位颜色是一个很好的选择,因为现在每个合理的现代终端都已经支持它很长一段时间了。
但是,对于基于XTerm、Kitty、Konsole和libVTE的终端模拟器(如GNOME终端、Mate终端和白蚁)的用户来说,使用24位颜色模式是礼貌的。例如:
#include<;stdio.h>;#include<;stdint.h>;#include<;stdbool.h>;struct color{enum color_mode{trad,trad_bright,b8,b24}mode;Union{uint8_t color;struct{uint8_t,g,b;};}};struct style{unsign char粗体:1;unsign char下划线:1。结构颜色fg,bg;};结构格式fmt_menu={{0,0,0,0,0},{trad,7},{trad,4}},fmt_menu_hl={{1,0,0,0,0},{trad_bright,7},{trad_bright,4},};void application_color(bool bg,struct color c){switch(c.mode){case trad:printf(。,bg?';4';:';3';,c.color);Break;case trad_bright:printf(";%s%u";,bg?";9";:";10";,c.color);Break;案例b8:printf(";%c8;5;%u";,bg?';4';:3和#39;,c.color);Break;case B24:printf(";%c8;2;%u;%u";,bg?';4';:';3';,C.R,C.b,C.g);}}void fmt(Struct Format F){printf(";\x1b[";);f.加粗&;&;printf(";F.下划线&;&;printf(";;4";);f.斜体&;&;printf(";;3";);f.反向&;&;printf(";;7";);f.dim&;&;printf(";;2";);application_color(false,f.fg);application_color(true,f.bg)。}int Main(){…。If(is_conf(";style/menu/color";)){if(strcmp(conf(";style/menu/color";,0),";rgb";)==0){fmt_menu.mode=b24;fmt_menu.r=atoi(conf(";style/Menu/color";,1));fmt_menu.g=atoi(conf,2));Fmt_menu.b=atoi(conf(";style/menu/color";,3));}false if(conf(conf(";style/menu/color";,0))>;8){fmt_menu.mode=b8;fmt_menu.color=atoi(conf(";style/menu.color";,1));}false{fmt_menu.color=att。,1));}}…。}这种基础结构为您提供了非常灵活的格式化系统,它可以优雅地降级,而不会将您绑定到庞大的古旧的库中,也不会用数百个愚蠢的函数和宏污染全局名称空间(当然,这是完全无法区分的)。
但是,如果您想要的不仅仅是格式化,那该怎么办呢?如果您想要一个真正的TUI,该怎么办?
根据您想要的TUI类型,您实际上可以使用普通的老式ASCII。例如,如果您只是尝试绘制进度条,则可以(并且应该)使用ASCII控制字符回车(在C中,\r):
#include<;unistd.h>;#include<;stdlib.h>;#include<;stdint.h>;#include<;time.h>;#定义条宽25#定义szstr(Str)str,sizeof(Str)tyecif uint8_t bar_t;int main(){srand(time(Null));bar_t prmax=-1;size。)){Write(1,";\r";,1);size_t barlen=进度/比率;for(size_t i=0;i<;barwidth;++i){size_t barlen=进度/比率;if(i<;=barlen)write(1,szstr(";█";));否则write(1,szstr(";░";));}fsync。//否则,终端只会在换行符size_tincr=rand()%(prmax/10);if(prmax-Progress<;incr)Break;//避免溢出进度+=incr;SLEEP(1);}}上更新。当然,如果我们真的想要美观一些,我们可以使用上面描述的转义序列用ANSI颜色装饰进度条。这将留给读者作为练习。
这对于基本的应用程序来说已经足够了,但是最终,我们将达到我们真正需要寻址和绘制终端的各个单元的地步。或者,如果我们希望根据终端窗口的大小动态调整进度条的大小,该怎么办?现在是再次打破ANSI转义序列的时候了。
编写TUI应用程序时,您应该始终做的第一件事是发送TI或smcup转义。这会通知终端切换到TUI模式(备用缓冲区),保护现有缓冲区,使其不会被覆盖,并且用户可以在应用程序关闭时返回到它。
在ANSI中,我们使用序列<ESC>[?1049H(或者,作为C字符串,";\x1b[?1049H";])来实现这一点。
(注:还有另一个转义,它的效果看起来与1049类似,但它的行为在某些终端(如xterm)上被微妙地破坏了,而且它在其他终端(如kitty)上完全不起作用。(";\x1b[?1049H";在支持备用缓冲区的任何地方都有正确的效果。)。
一旦您切换到备用缓冲区,您要做的第一件事就是清除屏幕并将光标放回原处,以清除以前应用程序可能留下的任何碎片。为此,我们使用序列<Esc>[2j,它清除屏幕并停止回滚。(我们不能使用终端重置序列c,因为它不仅会影响活动缓冲区,而且会影响整个终端会话,并且会破坏当前显示的所有内容!)。
同样,就在退出之前,您需要发送TE或rmcup转义符。这通知终端切换回先前模式。此序列作为C字符串,是";\x1b[?1049l";。为了礼貌起见,在发送这个换码之前,您应该自己清理一下,像以前一样清除回滚。
(在这些转义中,h和l似乎代表硬件IO线,其中高电流通常对应于1位,低电流通常对应于0位,实质上表示开和关。在过去的EON硬件终端中,像这样可能的程序可配置模式是通过将离散输出线设置到特定级别来实现的;也有可能ANSI转义代码设计者只是在布尔还不流行的时代找到了一个方便的比喻。如果任何人碰巧发现了这件事背后的真实故事,请务必让我知道)。
一旦我们进入备用缓冲区,我们就可以安全地开始抛出转义序列,将光标移动到任意位置。但是,在执行此操作之前,我们需要知道终端实际有多大,以便我们可以适当地布局UI。
有一个名为resize()的函数或类似的函数是很好的形式,您可以在程序启动时以及以后调整终端窗口大小时运行该函数。虽然有一种可怕的方法可以用ANSI转义来做到这一点,但最好还是咬紧牙关,学习如何使用ioctls和termios。
Termios是一个POSIX接口,允许您发现和设置当前终端的属性。这是一种邪恶的混乱,但幸运的是,我们只需要使用它的一个很小的角落就可以获得我们需要的信息。
我们从导入<;sys/ioctl.h>;头开始。这为我们提供了设置ioctls所需的函数和结构。Termios在名为struct winsize的结构中返回窗口的大小。(比你在咒骂中找到的任何东西都要理性得多,不是吗?)。此结构使用函数调用ioctl(1,TIOCGWINSZ,&;ws)填充,其中ws是结构的名称(1是标准输出的文件描述符)。然后可以在字段ws_ol(表示宽度)和ws_row(表示高度)中访问端子宽度和高度。
当然,当终端大小更改时,我们需要使这些值保持最新。这就是为什么resize()需要是它自己的函数-每当我们的程序收到SIGWINCH信号时都需要调用它。无论何时重塑窗口,控制终端仿真器都会将SIGWINCH自动发送给子进程。
#include<;sys/ioctl.h>;;#include<;signal.h>;;uint16_t width;uint16_t Height;void resize(Int I){//所需的伪参数,以便//函数签名与Signal(3)期望的struct winsize ws;ioctl(1,TIOCGWINSZ,&;ws);width=ws.ws_ol;high=ws.ws_row;/。\x1b[2j";)}int main(Void){Signal(SIGWINCH,RESIZE);RESIZE(0);//此处等待用户输入}在整个过程中,您可能已经注意到一件事:尽管我们试图创建干净、光滑的TUI,但光标本身仍然顽固地显示在屏幕上。不过,别担心,我们可以解决这个问题。
显示和隐藏光标的转义序列与切换到备用缓冲区和从备用缓冲区切换的转义序列非常相似,不同之处在于它的数字是25,而不是1049。因此,我们可以通过打印字符串";\x1b[?25L";来隐藏光标,并使用字符串";\x1b[?25H";再次显示光标。
不过,重要的是要跟踪您如何更改终端的行为,并在程序退出时将其恢复。否则,用户将不得不自己重置终端,而许多人甚至不知道该怎么做(根据记录,这是$RESET或$ECHO-NE&34;\EC&34;)。由于您不一定能够控制程序如何退出,因此使用atexit(3)和Signal(3)函数设置退出处理程序非常重要。这样,即使进程使用SIGTERM或SIGINT终止,它仍然会将终端恢复到其原始状态。
(当然,在SIGKILL的情况下,它不会做千斤顶拉屎,但在这一点上,无论如何它都是用户的责任。)。
#include<;stdlib.h>;#include<;unistd.h>;#include<;signal.h>;;#定义say(Str)write(1,str,sizeof(Str))void清理(Void){//清理备用缓冲区say(";\x1b[2J";);//切换回正常缓冲区say(";\x1b[?\x1b[?25h";);}void CLEANUP_DIE(Int I){EXIT(1);}int main(Void){//输入备用缓冲区SAY(";\x1b[?1049H&34;);//在EXIT(CLEANUP);SIGNAL(SIGTERM,CLEANUP_DIE);SIGINT(SIGINT,CLEANUP_DIE);//清理缓冲区SAY(";\x1\x1b[?25L";);睡眠(10);返回0;}