Chinaunix首页 | 论坛 | 博客
  • 博客访问: 900986
  • 博文数量: 96
  • 博客积分: 10681
  • 博客等级: 上将
  • 技术积分: 2449
  • 用 户 组: 普通用户
  • 注册时间: 2006-05-16 17:52
文章分类

全部博文(96)

文章存档

2011年(30)

2009年(36)

2008年(30)

分类: LINUX

2008-07-20 09:17:23

终端输出

使用termios结构,我们可以控制键盘输入,但是如果在显示在屏幕上的输出上可以进行同样级别的控制也许会更好。在我们本章的开始,我们使用printf来向屏幕输出字符,但是却没有办法将输出定位在屏幕上的某个特定位置上。

终端类型


许 多Unix系统使用终端,尽管在今天的许多情况下,终端也许实际上是一个运行终端程序的PC。从历史上来说,不同的生产产商提供了大量的硬件终端。尽管他 们都是使用转义序列(以转义字符开始的字符串)来提供对光标与属性的控制,例如粗体与闪烁等,但是他们并没有以标准的方式来提供这些特性。某些老的终端同 时还具有不同的滚动功能,当发送backspace滚动条也许会消失。

硬件终端的多样性对于那些希望编写控制屏幕以及运行在多个终端类型上的软件的程序员是一个极大的问题。例如,ANSI标准使用转义序列Escape+[+A来将光标上移一行,然而ADM-3a终端却使用单独的控制字符Ctrl+K。

要编写处理各种不同的连接到Unix系统上的终端类型的程序是一件极其困难的任务。程序也许要为每一个终端类型提供不同的源代码。

这 样在一个名为terminfo的包中提供一个解决方案就显得并不惊奇。程序并不会迎合各种终端类型,相反,程序会查找一个终端类型数据库来得到正确的信 息。在大多数的现代Unix系统中,包括Linux,这些已经被集成到一个名为curses的软件包中,这就是我们下一章要了解的内容。

在 Linux上,我们也许要使用curses的一个名为ncurses的实现,并且要包含ncurses.h文件来提供我们terminfo函数的原型。 terminfo函数本身声明在他们自己的头文件term.h中,或者至少以前是这种情况。而在新版本的Linux系统上,在terminfo与 ncurses之间有一个模糊的界线,许多需要terminfo函数的程序必须同时包含ncurses头文件。为避免以后的混乱,现代的Linux发行版 本同时提供一个与Unix系统更兼容的curses头文件与库。在这些系统上,我们推荐使用curses.h与-lcurses。

标识我们的终端类型


Linux 环境包含一个变量,TERM,他被设置为我们正在使用的终端类型。通常他是在系统登陆时由系统自动设置的。系统管理员也许会为每一个直接连接到终端的用户 设置一个默认的终端类型,这些用户也许是要提供终端类型的远程或是网络用户。TERM的值可以通过telnet协商,并通过rlogin传递。

用户可以查询shell来确定他正使用的终端类型。

$ echo $TERM
xterm
$

在这个例子中,shell是由一个名为xterm的程序来运行的,这是一个X Window系统的终端模拟器,或是提供类似功能的程序,例如KDE的Konsole或是Gnome的gnome-terminal。

terminfo软件包包含了一个功能与大量终端的转义序列的数据库,并且为程序员提供了统一的接口。这样编写的程序就可以在数据库扩展时利用未来终端的优点,而不是每一个程序都必须为不同的终端提供支持。

terminfo 的功能是通过属性来描述的。这些属性存储在已编译的terminfo文件集合中,并且通常可以在/usr/lib/terminfo或是/usr /share/terminfo中找到。对于每一个终端(也有一些可以在terminfo中指定的打印机),有一个文件来定义其功能以及如何访问这些特 性。为了避免创建一个非常大的目录,实际的文件存储在子目录中,而子目录的名字只是简单的终端类型的第一个字符。所以,VT100的定义可以 在...terminfo/v/vt100中找到。

对于每一个终端类型都会以可读的源码的格式来编写一个terminfo文件,然后使用 tic命令将其编译为应用程序可用的更为紧凑和高效的格式。奇怪的是,X/Open规范谈到源码以及编译的格式定义,但是却没有提到实际编译源码的tic 命令。我们可以使用infocmp程序来输出一个已编译的terminfo实体的可读版本信息。

下面是一个VT100终端的terminfo文件的例子:

$ infocmp vt100
vt100|vt100-am|dec vt100 (w/advanced video),
 am, mir, msgr, xenl, xon,
 cols#80, it#8, lines#24, vt#3,
 acsc=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
 bel=^G, blink=\E[5m$<2>, bold=\E[1m$<2>,
 clear=\E[H\E[J$<50>, cr=\r, csr=\E[%i%p1%d;%p2%dr,
 cub=\E[%p1%dD, cub1=\b, cud=\E[%p1%dB, cud1=\n,
 cuf=\E[%p1%dC, cuf1=\E[C$<2>,
 cup=\E[%i%p1%d;%p2%dH$<5>, cuu=\E[%p1%dA,
 cuu1=\E[A$<2>, ed=\E[J$<50>, el=\E[K$<3>,
 el1=\E[1K$<3>, enacs=\E(B\E)0, home=\E[H, ht=\t,
 hts=\EH, ind=\n, ka1=\EOq, ka3=\EOs, kb2=\EOr, kbs=\b,
 kc1=\EOp, kc3=\EOn, kcub1=\EOD, kcud1=\EOB,
 kcuf1=\EOC, kcuu1=\EOA, kent=\EOM, kf0=\EOy, kf1=\EOP,
 kf10=\EOx, kf2=\EOQ, kf3=\EOR, kf4=\EOS, kf5=\EOt,
 kf6=\EOu, kf7=\EOv, kf8=\EOl, kf9=\EOw, rc=\E8,
 rev=\E[7m$<2>, ri=\EM$<5>, rmacs=^O, rmkx=\E[?1l\E>,
 rmso=\E[m$<2>, rmul=\E[m$<2>,
 rs2=\E>\E[?3l\E[?4l\E[?5l\E[?7h\E[?8h, sc=\E7,
 sgr=\E[0%?%p1%p6%|%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;m%?%p9%t^N%e^O%;,
 sgr0=\E[m^O$<2>, smacs=^N, smkx=\E[?1h\E=,
 smso=\E[1;7m$<2>, smul=\E[4m$<2>, tbc=\E[3g,

每一个terminfo定义由三种类型的实体构成。每一个实体被称之为capname并且定义了一个终端功能。

布尔功能只是简单的指示一个终端是否支持一个特定的特性。例如,如果终端支持XON/XOFF流控制就会显示xon布尔功能。

数字功能定义了尺寸,例如lines定义了屏幕上的行数,而cols定义了屏幕上的列数。实际的数字是通过#字符与功能相区分的。要定义一个具有80列与24行的终端,我们可以写成cols#80,lines#24。

字 符串功能要显得有些复杂。他们用于两种不同的功能:定义访问终端所需要的输出字符串以及定义当用户按下特定的按键时会接收的输入字符串,通常为功能键或是 数字键盘上的特殊键。某些字符功能相当简单,例如el,表示"清除直到一行结束"。在一个VT100的终端上,要完成这个任务的转义序列为Esc+ [+K。在terminfo的源码格式中则为el=\E[K。

特殊的键的定义与此相类似。例如,VT100上的功能键F1发送的转义序列为Esc+O+P,其定义为kf1=\EOP。

当 转义序列需要一些参数时,其定义会显得有些复杂。大多数的终端可以将光标移动到一个特定的行与列位置。对于每一个可能的光标位置都会有一个不 的功能是不切实际的,所有使用一个通用字符串,并且带有参数定义当字符串被使用时要插入的值。例如,VT100终端使用转义序列Esc+ [++;++H来将光标移到一个特定的位置。在terminfo源码格式中,其定义为cup=\E[%i %p1%d;%p2%dH$<5>。

其含义为:

\E:发送Escape
[:发送[字符
%i:增加参数
%p1:将第一个参数放入堆栈
%d:将堆栈上的数字作为十进制数字输出
;:发送;字符
%p2:将第二个参数放入堆栈
%d:将堆栈上的数字作为十进制数字输出
H:发送H字符。

这 看起来似乎有一些复杂,但是却允参数以固定的顺序出现,独立于终端希望他们出现在最终的转义序列中的顺序。增加参数的%i是必须的,因为标准的光标位置是 屏幕的左上角(0,0),但是VT100的光标位置为(1,1)。最后的$<5>表明需要等同于输出5个字符的时间来允许终端处理光标移动。

注 意,我们将会定义许多许多终端,但是幸运的是,大多数的Unix和Linux系统已经预定义了大多数的终端。如果我们需要添加一个新的终端,我们可以在 terminfo手册页中查找到完整的功能列表。一个好的起点就是定位那些与我们的新终端相似的终端,将新终端定义为已存在终端的一个变体。

使用terminfo功能


现 在我们知道了如何定义终端功能,我们需要了解如何访问他们。当我们使用terminfo时,我们需要做的第一件事就是通过调用setupterm来设置终 端类型。这会为当前的终端类型初始化一个TERMINAL结构。然后我们就可以访问并使用终端功能。setupterm函数原型如下:

#include
int setupterm(char *term, int fd, int *errret);

setupterm 库函数将当前的终端类型设置为参数term所指定的终端类型。如果term为一个空指针,那么就会使用TERM环境变量。写入终端所用的打开的文件描述符 必须由参数fd传递。函数的执行结果存储在由errret所指向的整型变量中(如果他不为空)。写入的值可能是:

-1:没有terminfo数据库
0:在terminfo数据库中没有匹配的实体
1:成功

如果成功,setupterm函数会返回常量OK,如果失败则会返回ERR。如果errret设置为一个空指针,函数执行失败时就会输出一个诊断信息并且退出程序,如下面的例子所示:

#include
#include
#include
int main()
{
    setupterm(“unlisted”,fileno(stdout),(int *)0);
    printf(“Done.\n”);
    exit(0);
}

运行在我们系统上的程序输出也许并不是这里给出的样子,但是其含义已经足够明显了。在这里并没有打印出Done,因为setupterm函数执行失败从而导致程序退出。

$ cc -o badterm badterm.c -I/usr/include/ncurses -lncurses
$ badterm
‘unlisted’: unknown terminal type.
$

注 意上面例子中的编译命令:在这个Linux系统上,ncurses头文件位于/usr/include/ncurses目录,所以我们必须使用-I选项来 指示编译器在这里进行查找。而某些Linux系统也许会将ncurses库可以由标准位置进行访问。在这些系统上,我们只需要简单的包含curses.h 头文件,并且为库指定-lcurses选项。

对于我们的菜单选择函数,我们希望可以清屏,在屏幕上移动光标,并且可以屏幕上的任意位置写入。一旦我们调用了setupterm函数,我们就可以使用不同的函数来访问terminfo功能,功能类型如下:

#include
int tigetflag(char *capname);
int tigetnum(char *capname);
char *tigetstr(char *capname);

函数tigetflag,tigetnum,tigetstr分别返回布尔,数字值以及字符串terminfo功能。如果失败,tigetflag会返回-1,tigetnum会返回-2,而tigetstr会返回(char *)-1。

下面我们使用程序sizeterm.c程序取得cols与lines功能来确定终端尺寸:

#include
#include
#include
int main()
{
    int nrows, ncolumns;
    setupterm(NULL, fileno(stdout), (int *)0);
    nrows = tigetnum(“lines”);
    ncolumns = tigetnum(“cols”);
    printf(“This terminal has %d columns and %d rows\n”, ncolumns, nrows);
    exit(0);
}

$ echo $TERM
vt100
$ sizeterm
This terminal has 80 columns and 24 rows
$

如果我们在工作站的一个窗口内运行这个程序,我们会得到反映当前窗口尺寸的答案:

$ echo $TERM
xterm
$ sizeterm
This terminal has 88 columns and 40 rows
$

如果我们使用tigetstr来取得xterm终端类型的光标移动功能(cup),我们会得到一个参数化的答案: \E[%p1%d;%p2%dH。

这个功能需要两个参数:光标要移动到的行与列。这两个坐标都是由屏幕左上角的零点处开始计量的。

我们可以使用tparm函数用实际的值来代替功能中的参数。最多可以替换九个参数,并且会返回一个可用的转义序列:

#include
char *tparm(char *cap, long p1, long p2, ..., long p9);

一旦我们使用tparm来组织终端转义序列,我们必须将其发送到终端。要正确的处理,我们不应使用printf来向终端发送字符串,相反,我们要使用特殊的函数,这些函数为终端完成一个操作的正确处理提供了必要的延时。这些函数为:

#include
int putp(char *const str);
int tputs(char *const str, int affcnt, int (*putfunc)(int));

如果成功,putp返回OK,如果失败,则会返回ERR。putp函数将终端控制字符串作为参数并且将其发送到标准输出。

所以要移动到屏幕的第5行,第30列,我们可以使用下面的代码块:

char *cursor;
char *esc_sequence;
cursor = tigetstr(“cup”);
esc_sequence = tparm(cursor,5,30);
putp(esc_sequence);

tputs 函数是为那些不可以通过stdout访问终端并且允许我们指定输出字符所使用的函数的情况而提供的。他会返回用户指定的函数putfunc的结要。 affcnt参数用来指明更改会影响到的行数,通常将其设置为1。用于输出字符串的函数必须与putchar函数具有相同的参数与返回结果。事实 上,putp(string)等同于调用tputs(string,1,putchar)。我们将会在下面的例子中的看到用用户指定的输出函数来使用 tputs函数。

要小心,一些老的Linux发行版本将tputs函数的最后一个参数定义为int (*putfunc)(char),这会强制我们修改在我们的下一个试验中所定的char_to_terminal函数。

注意,如果我们查看tparm与终端功能的信息手册页,我们也许会遇到一个tgoto函数。他为移动光标提供了一个更为简单的方案,但是我们不使用这个函数的原因是因为X/Open规范并没有将其包含在1997版本中。所以我们推荐不要在新程序中使用这些函数。

现在我们准备好为我们的菜单选择功能添加屏幕处理了。还有一件需要做的事就是简单的使用clear来清除屏幕。某些终端并不支持clear功能,从而会使得光标停留在屏幕的左上角。在这种情况下,我们可以将光标放置在左上角,并且使用"删除直到显示结尾"命令ed。

将所有这些信息结合在一起,我们可以编写我们例子菜单程序的最终版本,screen-menu.c,在这里我们将会在屏幕上"画"出选项,从而供用户选择。

试验--完全终端控制

我们可以重新编写menu4.c的getchoice函数从而为我们提供完全的终端控制。在这个列表中,省略了main函数,因为他并没有改变。

#include
#include
#include
#include
#include
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 getchoice(char *greet, char *choices[], FILE *in, FILE *out)
{
    int chosen = 0;
    int selected;
    int screenrow, screencol = 10;
    char **option;
    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,”%s”, *option);
        screenrow++;
        option++;
    }
    fprintf(out, “\n”);
    do {
        fflush(out);
        selected = fgetc(in);
        option = choices;
        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;
}

工作原理

重写的getchoice函数实现了与我们前面的例子中相同的菜单,但是输出函数进行了修改从而来使用terminfo功能。如果我们希望在屏幕被清除之前看到You have chosen:信息停留一会,可以使用下面的选择,在main函数中添加一个sleep调用:

do {
         choice = getchoice(“Please select an action”, menu, input, output);
         printf(“\nYou have chosen: %c\n”, choice);
         sleep(1);
     } while (choice != ‘q’);

这个程序中的最后一个函数,char_to_terminal,包含了一个我们在第3章提到的putc函数调用。

要结束这一章,我们将会看一个如何检测击键的例子。
阅读(1406) | 评论(0) | 转发(0) |
0

上一篇:Linux终端(一)

下一篇:Linux终端(四)

给主人留下些什么吧!~~