08 | linux 终端重新认识(2)——屏幕的输出
终端的输出
有没有办法将输出的内容放置到屏幕上的特定位置
终端的类型
科普
对程序员来说,如果他希望编写-一个可以控制屏幕输出的软件,并且能够运行在各种类型的终端之上,则硬件终端的多样性是程序员要面对的一一个主要问题。例如,ANSI终端使用转义序列Escape,[A将光标移动到上一行, 而ADM-3a终端(多年前它是一种很常见的终端)只需使用一个控制字符Ctrl+K就可以完成这一-任务。
头文件
让人欣慰的是,terminfo软件包的出现解决了这一一问题。程序不再需要去迎合每种类型的终端,取而代之的是,程序通过查询终端类型数据库来找到正确的终端信息。在大多数现代UNIX系统(包括Linux)中,这个软件包和另一个软件包curses集成在一一起。你将在下一章学习后者。
为了使用terminfo函数,你通常需要在程序中包括curses头文件curses . h和terminfo自己的头文件term.h。在一些Linux系统 上,你可能不得不使用被称为ncurses的curses实现,并在程序中包括ncurses . h头文件以提供对terminfo函数的原型定义。
识别终端类型
echo $TERM
xtem-256color
在这个例子中,shell是通过xterm程序(一个X视窗系统中的终端仿真程序)或是提供类似功能的程序(如KDE的Konsole或GNOME的gnome-terminal) 运行的。
terminfo软件包包含-一个由大量不同类型终端的功能标志和escape转义序列等信息构成的数据;库,并且为使用它们提供了-一个统一的编 程接口。一个使用这个软件包的程序能够随着数据库的扩展来适应未来的终端类型,对不同类型终端的支持不再需要由应用程序自身来提供。
terminfo的功能标志由属性描述,它们被保存在一组编译好的terminfo文件中,这些文件通常可以在/usr/1ib/terminfo或/usr/share/terminfo目录中找到。每个终端(包括许多不同类型的打印机,它们也可以通过terminfo来定义)都有一一个定 义其功能标志和如何访问其特征的文件。为避免创建一-个很大的目录,真正的文件都保存在下一-级的子目录中, 子目录名就是终端类型名的第一个字母。例如,VT100终端的定义就放在文件...terminfo/v/vt100中。
每个终端类型对应-一个terminfo文件, 文件格式是可读的源代码,然后通过tic命令将源文件编译为更加紧凑、有效的格式,以方便应用程序的使用。奇怪的是,x/Open规范提到了源文件和编译格式的定义,但却未提到把源文件转换为编译格式的tic命令。你可以用infocmp程序输出已编译terminfo数据项的可读版本。
每个terminfo 定义三种类型的数据项 每个数据项被称为 capname
- 布尔:指出终端是否支持某个特定的功能。例如,如果终端支持XON XOFF流控,则在该终端对应的terminfo文件中定义布尔功能标志xon。
- 数值:标志定义长度,例如: lines定 义的是屏幕上可以显示的行数,cols定 义的是屏幕上可以显示的列数。具体数字和功能标志名之间用字符#隔开。如果要定义一个有80列24行显示范围的终
端,可以写为cols#80,lines#24. - 字符串: 它用来定义两种截然不同的终端功能:用于访问终端功能的输出字符串和当用户按下特定按键(通常是功能键或在数字小键盘上的特殊键)时终端接收到的输入字符串。有些字符串功能标志非常简单,例如e1表示“删除到行尾”。在VT100终端上,用于完成这一功能的escape转义序列是Ese,[,K,在terminfo源文件中写为e1=\E[K。
特殊键的定义也采用类似的方法。例如,VT100终 端上的F1功能键发送的escape转义序列是Esc,O,P,它被定义为kf1= \EOP。
当escape转义序列本身还需要带有参数时,情况会变得更加复杂。大多数终端都能将光标移动到一个特定的行列位置。很明显,为光标可能移动到的每个位置定义- - 个功能标志是不现实的,解决方法是使用一个通用的字符串功能标志,在使用这个字符串时,通过插入参数来确定光标要移动到的确定位置。
例如VT100终端通过转义序列Esc, [,
.;., H将光标移动到-一个特定位置。在terminfo源文件中,它被写为相当复杂的字符串cup= \E[&i号p1 8d; 8p2%dH$<5>.
下面给出了它的含义。
\E: 发送Escape字符。
0[:发送[字符。
%i:增加参数值。
%p1: 将第一一个参数放入栈。
%d:将栈上的数字输出为-一个十进制数。
;:发送;字符。
%p2: 将第二个参数放入栈。
%d: 将栈上的数字输出为- 一个十进制数。
H:发送H字符。
这种写法看起来非常复杂,但它允许参数以固定的顺序排列,与终端期望它们出现在最终escape
转义序列中的顺序无关。名i的作用是增加参数的值,它是必不可少的,因为标准的光标寻址方法是将
屏幕的左上角看做是(0,0), 而VT100终端把这个位置定义为(1,1)。 最后的$<5>表示需要延迟- -段
时间,该时间的长度为输出五个字符所花费的时间,终端将利用这段时间来处理光标的移动。
这种写法看起来非常复杂,但它允许参数以固定的顺序排列,与终端期望它们出现在最终escape转义序列中的顺序无关。名i的作用是增加参数的值,它是必不可少的,因为标准的光标寻址方法是将屏幕的左上角看做是(0,0), 而VT100终 端把这个位置定义为(1,1)。 最后的$<5>表示需要延迟-段时间,该时间的长度为输出五个字符所花费的时间,终端将利用这段时间来处理光标的移动。
使用 terminfo 功能标志
现在,你已知道如何定义终端的功能标志,你还需知道如何访问它们。当使用terminfo时,你要做的第一件事情就是调用函数setupterm来设置终端类型,这将为当前的终端类型初始化一个TERMINAL结构。然后,你就可以查看当前终端的功能标志并使用它们的功能了。setupterm函数的调用方法如下所示:
include <term.h>
int setupterm(char *term, int fd, int *errret);
setupterm库函数将当前终端类型设置为参数term指向的值,如果term是空指针,就使用环境变量TERM的值。
参数fd为一个打开的文件描述符,它用于向终端写数据。
如果参数errret不是一个空指针,则函数的返回值保存在该参数指向的整型变量中,下面给出了可能写入的值。
-1: terminfo数据库不存在。
0: terminfo数据库中没 有匹配的数据项。
1:成功
在成功调用setupterm函数后,你即可通过如下3个函数调用来访问terminfo的功能标志,每个函数对应一一个 功能标志类型:
include <term. h>
int tigetflag (char *capname) ;
int tigetnum(char *capname) ;
char *tigetstr (char *capname);函数tigetflag、tigetnum和tigetstr分 别返回terminfo中的布尔功能标志、数值功能标志和字符串功能标志的值。
失败时(例如,某个功能标志不存在),tigetflag函数返回-1, tigetnum函数返回-2,tigetstr函数返回(char *)-1.
#include <stdio.h>
#include <term.h>
#include <curses.h>
#include <stdlib.h>
int main ()
{
int nrows, ncolumns;
setupterm(NULL, fileno(stdout), (int *)0);
nrows = tigetnum("lines") ;
ncolumns = tigetnum("cols") ;
printf ("This terminal has %8d columns and %8d ows\n", ncolumns, nrows) ;
exit(0) ;
}
./sizeterm
如果用 tigetstr函数来获取 xterm 终端类型的光标移动功能标志cup的值,你将会得到一个参数化的结果\E[%p1%d; %p2%dH。
这个功能标志需要两个参数:光标要移动到的行号和列号。这两个坐标都是从0开始计算的,(0,0)表示屏幕的左上角。
1.你可以使用tparm函数用实际的数值替换功能标志中的参数,一次最多可以替换9个参数,并返回一个可用的escape转义序列。该函数的定义如下:
include <term.h>
char * tparm (char *cap, 1ong p1, 1ong p2,.... 1ong p9);
2.当用tparm函 数构造好终端的escape转义序列后,你必须将其发送到终端。要想正确地完成这一-操作,你不能通过printf函数将字符串发送到终端,而必须使用系统提供的如下几个特殊函数,这些函数可以正确地处理终端完成一个操作所需要的延时:
include <term.h>
int putp (char tconst str) ;
int tputs (char * const str, int affcnt, int (*putfunc) (int) ) ;
putp函数在成功时返回oK,失败时返回ERR。它以一一个终端控制字符串为参数,并将其发送到标准输出stdout。
所以,如果要将光标移动到屏幕上的第5行第30列,你可以使用如下代码段:
char *cursor;
char *esc_sequence;
cursor = tigetstr("cup") ;
esc_ sequence = tparm(cursor,5,30) ; 🚩
putp(esc_ sequence) ;
4.tputs函数是为不能通过标准输出stdout访问终端的情况准备的,它可以指定一个用于输出字符的函数。tputs函数的返回值是用户指定的函数putfunc的返回结果。参数affent的作用是表明受这一变化影响的行数,它一般被设置为1。 真正用于输出控制字符串的函数的参数和返回值类型必须与putchar函数相同。
事实上,函数调用putp (string)就等同于函数调用tputs (string,1, putchar)。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h> // 引入头文件
/////////////////
#include <term.h>
#include <curses.h>
static FILE *output_stream= (FILE *)0;
//////////////////
char *menu[]={
"a - add new record",
"d - delete record",
"q - quit",
NULL, //遍历的哨兵元素
};
int getchoice(char *greet,char *choices[],FILE *in,FILE *out);
int char_to_terminal(int char_to_write);
int main()
{
int choice=0;
FILE *input;
FILE *output;
struct termios initial_settings,new_settings; // 设置新变量
//判断是否重定向
if(!isatty(fileno(stdout))){
fprintf(stderr, "you are not a terminal,OK\n" );
}
input = fopen("/dev/tty","r");
output=fopen("/dev/tty","w");
if(!input || !output){
fprintf(stderr,"unable to open /dev/tty\n");
exit(1);
}
//-----------------------------------
tcgetattr(fileno(input),&initial_settings);
new_settings=initial_settings;
new_settings.c_lflag &= ~ICANON;
new_settings.c_lflag &= ~ECHO;
new_settings.c_cc[VMIN] = 1; //只取一个字符
new_settings.c_cc[VTIME] = 0; //无阻塞
new_settings.c_lflag &= ~ISIG; //禁止对特殊字符处理,ctrl+c 没有办法退出
if(tcsetattr(fileno(input),TCSANOW,&new_settings)!=0)
{
fprintf(stderr,"could not set attributes\n");
}
//-----------------------------------
do
{
choice = getchoice("please select an action",menu,input,output);
//这一行 printf 可能会被重定向
printf("you have chosen: %c\n",choice);
}while(choice!='q');
//-------------------------------------
tcsetattr(fileno(input),TCSANOW,&initial_settings);
//-------------------------------------
exit(0);
}
int getchoice(char *greet,char *choices[],FILE *in,FILE *out)
{
int chosen=0;
int selected;
char **option;
//////////////////
int screenrow,screencol=10;
char *cursor,*clear;
output_stream=out;
setupterm(NULL,fileno(out),(int *)0);
cursor=tigetstr("cup");
clear=tigetstr("clear");
screenrow=4;
tputs(clear,1,(int *)char_to_terminal); //清理
tputs(tparm(cursor,screenrow,screencol),1,char_to_terminal);//定位
fprintf(out,"choice:%s",greet);//输出
screenrow+=2;
option=choices;
while(*option){
tputs(tparm(cursor,screenrow,screencol),1,char_to_terminal);
fprintf(out,"choice:%s",*option);
screenrow++;
option++;
}
fprintf(out,"\n");
do{
fflush(out);
selected=fgetc(in);
while(*option){
if(selected==*option[0]){
chosen=1;
break;
}
option++;
}
if(!chosen){
tputs(tparm(cursor,screenrow,screencol),1,char_to_terminal);
fprintf(out,"incorrect choice,select again\n");
}
}while(!chosen);
tputs(clear,1,char_to_terminal);
return selected;
//////////////////
}
int char_to_terminal(int char_to_write){
if(output_stream) putc(char_to_write,output_stream);
return 0;
}