8.17.
Функциональные клавиши большинства дисплеев посылают в линию не один, а несколько символов. Например на терминалах, работающих в системе команд стандарта ANSI,
кнопки со стрелками посылают такие последовательности:
стрелка вверх "\033[A" кнопка Home "\033[H"
стрелка вниз "\033[B" кнопка End "\033[F"
стрелка вправо "\033[C" кнопка PgUp "\033[I"
стрелка влево "\033[D" кнопка PgDn "\033[G"
(поскольку первым символом управляющих последовательностей обычно является символ
'\033' (escape), то их называют еще escape-последовательностями). Нам же в программе
удобно воспринимать такую последовательность как единственный код с целым значением большим 0xFF. Склейка последовательностей символов, поступающих от функциональных клавиш, в такой внутренний код - также задача экранной библиотеки (учет системы
команд дисплея на вводе).
Самым интересным является то, что одиночный символ '\033' тоже может прийти с
клавиатуры - его посылает клавиша Esc. Поэтому если мы строим распознаватель клавиш,
который при поступлении кода 033 начинает ожидать составную последовательность - мы
должны выставлять таймаут, например alarm(1); и если по его истечении больше никаких
символов не поступило - выдавать код 033 как код клавиши Esc.
Напишите распознаватель кодов, поступающих с клавиатуры. Коды обычных букв выдавать как есть (0..0377), коды функциональных клавиш выдавать как числа >= 0400.
Учтите, что разные типы дисплеев посылают разные последовательности от одних и тех же
функциональных клавиш: предусмотрите настройку на систему команд ДАННОГО дисплея при помощи библиотеки termcap. Распознаватель удобно строить при помощи сравнения поступающих символов с ветвями дерева (спускаясь по нужной ветви дерева при поступлении
очередного символа. Как только достигли листа дерева - возвращаем код, приписанный этому листу):
---> '\033' ---> '[' ---> 'A' --> выдать 0400
| \--> 'B' --> 0401
| \--> 'C' --> 0402
| \--> 'D' --> 0403
\--> 'X' -----------> 0404
...
Нужное дерево стройте при настройке на систему команд данного дисплея.
Библиотека curses уже имеет такой встроенный распознаватель. Чтобы составные
последовательности склеивались в специальные коды, вы должны установить режим keypad:
int c; WINDOW *window;
...
keypad(window, TRUE);
...
c = wgetch(window);
Без этого wgetch() считывает все символы поодиночке. Символические названия кодов
для функциональных клавиш перечислены в <curses.h> и имеют вид KEY_LEFT, KEY_RIGHT
и.т.п. Если вы работаете с единственным окном размером с весь экран, то в качестве параметра window вы должны использовать стандартное окно stdscr (это имя предопределено в include-файле curses.h).
# ======================================== Makefile для getch
getch: getch.o
cc getch.o -o getch -ltermlib
getch.o: getch.c getch.h
cc -g -DUSG -c getch.c
/* Разбор составных последовательностей клавиш с клавиатуры. */
/* ================================================== getch.h */
#define FALSE 0
#define TRUE 1
#define BOOLEAN unsigned char
#define INPUT_CHANNEL 0
#define OUTPUT_CHANNEL 1
#define KEY_DOWN 0400
#define KEY_UP 0401
#define KEY_LEFT 0402
#define KEY_RIGHT 0403
#define KEY_PGDN 0404
#define KEY_PGUP 0405
#define KEY_HOME 0406
#define KEY_END 0407
#define KEY_BACKSPACE 0410
#define KEY_BACKTAB 0411
#define KEY_DC 0412
#define KEY_IC 0413
#define KEY_DL 0414
#define KEY_IL 0415
#define KEY_F(n) (0416+n)
#define ESC ' 33'
extern char *tgetstr();
void _put(char c);
void _puts(char *s);
void keyboard_access_denied(void);
char *strdup(const char *s);
void keyinit(void);
int getc_raw(void);
void keyreset(void);
int getch(void);
int lgetch(BOOLEAN);
int ggetch(BOOLEAN);
int kgetch(void);
void _sigalrm(int n);
void init_keytry(void);
void add_to_try(char *str, short code);
void keypad_on(void);
void keypad_off(void);
int dotest(void);
void tinit(void);
void main(void);
/* ===================================================== getch.c
* The source version of getch.c file was
* written by Pavel Curtis.
*
*/
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <termios.h>
#include <ctype.h>
#include <string.h>
#include <locale.h>
#include "getch.h"
#define keypad_local S[0]
#define keypad_xmit S[1]
#define key_backspace S[2]
#define key_backtab S[3]
#define key_left S[4]
#define key_right S[5]
#define key_up S[6]
#define key_down S[7]
#define key_ic S[8]
#define key_dc S[9]
#define key_il S[10]
#define key_dl S[11]
#define key_f1 S[12]
#define key_f2 S[13]
#define key_f3 S[14]
#define key_f4 S[15]
#define key_f5 S[16]
#define key_f6 S[17]
#define key_f7 S[18]
#define key_f8 S[19]
#define key_f9 S[20]
#define key_f10 S[21] /* f0 */
#define key_f11 S[22] /* f11 */
#define key_f12 S[23] /* f12 */
#define key_home S[24]
#define key_end S[25]
#define key_npage S[26]
#define key_ppage S[27]
#define TOTAL 28
/* descriptors for keys */
char *KEYS[TOTAL+1] = {
"ke", "ks",
"kb", "kB",
"kl", "kr", "ku", "kd",
"kI", "kD", "kA", "kL",
"f1", "f2", "f3", "f4", "f5",
"f6", "f7", "f8", "f9", "f0",
"f.", "f-",
"kh", "kH", "kN", "kP",
NULL
}, *S[TOTAL];
void _put (char c) { write( INPUT_CHANNEL, &c, 1 ); }
void _puts(char *s) { tputs ( s, 1, _put ); }
static int _backcnt = 0;
static char _backbuf[30];
static struct try {
struct try *child;
struct try *sibling;
char ch;
short value;
} *_keytry;
BOOLEAN keypadok = FALSE;
struct termios new_modes;
void keyboard_access_denied(){ printf( "Клавиатура недоступна.\n" ); exit(1); }
char *strdup(const char *s) { return strcpy((char *) malloc(strlen(s)+1), s); }
/* Инициализация таблицы строк */
void keyinit(){
char *key, nkey[80], *p;
register i;
keyreset();
for( i=0; i < TOTAL; i++ ){
p = nkey;
printf("tgetstr(%s)...", KEYS[i]);
key = tgetstr(KEYS[i], &p);
if(S[i]) free(S[i]);
if(key == NULL){
S[i] = NULL; /* No such key */
printf("клавиша не определена.\n");
}else{
/* Decrypted string */
S[i] = strdup(key);
printf("считано.\n");
}
}
init_keytry();
if( tcgetattr(INPUT_CHANNEL, &new_modes) < 0 ){
keyboard_access_denied();
}
/* input flags */
/* отменить преобразование кода '\r' в '\n' на вводе */
new_modes.c_iflag &= ~ICRNL;
if ((new_modes.c_cflag & CSIZE) == CS8) /* 8-битный код */
new_modes.c_iflag &= ~ISTRIP; /* отменить & 0177 на вводе */
/* output flags */
/* отменить TAB3 - замену табуляций '\t' на пробелы */
/* отменить ONLCR - замену '\n' на пару '\r\n' на выводе */
new_modes.c_oflag &= ~(TAB3 | ONLCR);
/* local flags */
/* выключить режим ICANON, включить CBREAK */
/* выключить эхоотображение набираемых символов */
new_modes.c_lflag &= ~(ICANON | ECHO);
/* control chars */ /* при вводе с клавиш ждать не более ... */
new_modes.c_cc[VMIN] = 1; /* 1 символа и */
new_modes.c_cc[VTIME] = 0; /* 0 секунд */
/* Это соответствует режиму CBREAK */
/* Символы, нажатие которых заставляет драйвер терминала послать сигнал
* либо отредактировать набранную строку. Значение 0 означает,
* что соответствующего символа не будет */
new_modes.c_cc[VINTR] = '\0'; /* символ, генерящий SIGINT */
new_modes.c_cc[VQUIT] = '\0'; /* символ, генерящий SIGQUIT */
new_modes.c_cc[VERASE] = '\0'; /* забой (отмена последнего символа)*/
new_modes.c_cc[VKILL] = '\0'; /* символ отмены строки */
}
/* Чтение одного символа непосредственно с клавиатуры */
int getc_raw(){
int n; char c;
n = read(INPUT_CHANNEL, &c, 1);
if (n <= 0) return EOF;
return (c & 0xFF);
}
static BOOLEAN _getback = FALSE;
static char _backchar = '\0';
/* Чтение символа - либо из буфера (если не пуст), либо с клавиатуры */
#define nextc() (_backcnt > 0 ? _backbuf[--_backcnt] : \
_getback ? _getback = FALSE, _backchar : \
getc_raw())
#define putback(ch) _backbuf[_backcnt++] = ch
void keyreset(){
_backcnt = 0; _backchar = '\0';
_getback = FALSE;
}
/* Функция чтения составного символа */
int getch(){
int c = lgetch(TRUE);
keypad_off();
return c;
}
/*
ВНИМАНИЕ!
Если в процессе будет получен сигнал,
в то время как процесс находится внутри вызова getch(),
то системный вызов read() вернет 0 и errno == EINTR.
В этом случае getch() вернет '\0'.
Чтобы избежать этой ситуации используется функция lgetch()
*/
int lgetch(BOOLEAN kpad) {
int c;
while((c = ggetch(kpad)) <= 0);
return c;
}
int ggetch(BOOLEAN kpad) {
int kgetch();
if( kpad ) keypad_on();
else keypad_off();
return keypadok ? kgetch() : nextc();
}
/*
** int kgetch()
**
** Get an input character, but take care of keypad sequences, returning
** an appropriate code when one matches the input. After each character
** is received, set a one-second alarm call. If no more of the sequence
** is received by the time the alarm goes off, pass through the sequence
** gotten so far.
**
*/
#define CRNL(c) (((c) == '\r') ? '\n' : (c))
/* борьба с русской клавиатурой */
#if !defined(XENIX) || defined(VENIX)
# define unify(c) ( (c)&(( (c)&0100 ) ? ~0240 : 0377 ))
#else
# define unify(c) (c)
#endif
/* ==================================================================== */
#if !defined(XENIX) && !defined(USG) && !defined(M_UNIX) && !defined(unix)
/* Для семейства BSD */
static BOOLEAN alarmed;
jmp_buf jbuf;
int kgetch()
{
register struct try *ptr;
int ch;
char buffer[10]; /* Assume no sequences longer than 10 */
register char *bufp = buffer;
void (*oldsig)();
void _sigalrm();
ptr = _keytry;
oldsig = signal(SIGALRM, _sigalrm);
alarmed = FALSE;
if( setjmp( jbuf )) /* чтоб свалиться сюда с read-а */
ch = EOF;
do
{
if( alarmed )
break;
ch = nextc();
if (ch != EOF) /* getc() returns EOF on error, too */
*(bufp++) = ch;
if (alarmed)
break;
while (ptr != (struct try *)NULL &&
(ch == EOF || unify(CRNL(ptr->ch)) != unify(CRNL(ch)) ))
ptr = ptr->sibling;
if (ptr != (struct try *)NULL)
{
if (ptr->value != 0)
{
alarm(0);
signal(SIGALRM, oldsig);
return(ptr->value);
}
else
{
ptr = ptr->child;
alarm(1);
}
}
} while (ptr != (struct try *)NULL);
alarm(0);
signal(SIGALRM, oldsig);
if (ch == EOF && bufp == buffer)
return ch;
while (--bufp > buffer)
putback(*bufp);
return(*bufp & 0377);
}
void _sigalrm(int n)
{
alarmed = TRUE;
longjmp(jbuf, 1);
}
/* ==================================================================== */
#else /* XENIX or USG */
/* Для семейства SYSTEM V */
static BOOLEAN alarmed;
int kgetch()
{
register struct try *ptr;
int ch;
char buffer[10]; /* Assume no sequences longer than 10 */
register char *bufp = buffer;
void (*oldsig)();
void _sigalrm();
ptr = _keytry;
oldsig = signal(SIGALRM, _sigalrm);
alarmed = FALSE;
do
{
ch = nextc();
if (ch != EOF) /* getc() returns EOF on error, too */
*(bufp++) = ch;
if (alarmed)
break;
while (ptr != (struct try *)NULL &&
(ch == EOF || unify(CRNL(ptr->ch)) != unify(CRNL(ch)) ))
ptr = ptr->sibling;
if (ptr != (struct try *)NULL)
{
if (ptr->value != 0)
{
alarm(0);
signal(SIGALRM, oldsig);
return(ptr->value);
}
else
{
ptr = ptr->child;
alarm(1);
}
}
} while (ptr != (struct try *)NULL);
alarm(0);
signal(SIGALRM, oldsig);
if (ch == EOF && bufp == buffer)
return ch;
while (--bufp > buffer)
putback(*bufp);
return(*bufp & 0377);
}
void _sigalrm(int n)
{
alarmed = TRUE;
signal(SIGALRM, _sigalrm);
}
#endif /*XENIX*/
/* ==================================================================== */
/*
** init_keytry()
** Построение дерева разбора последовательностей символов.
**
*/
void init_keytry()
{
_keytry = (struct try *) NULL;
add_to_try(key_backspace, KEY_BACKSPACE);
add_to_try("\b", KEY_BACKSPACE);
add_to_try("\177", KEY_BACKSPACE);
add_to_try(key_backtab, KEY_BACKTAB);
add_to_try(key_dc, KEY_DC);
add_to_try(key_dl, KEY_DL);
add_to_try(key_down, KEY_DOWN);
add_to_try(key_f1, KEY_F(1));
add_to_try(key_f2, KEY_F(2));
add_to_try(key_f3, KEY_F(3));
add_to_try(key_f4, KEY_F(4));
add_to_try(key_f5, KEY_F(5));
add_to_try(key_f6, KEY_F(6));
add_to_try(key_f7, KEY_F(7));
add_to_try(key_f8, KEY_F(8));
add_to_try(key_f9, KEY_F(9));
add_to_try(key_f10, KEY_F(10));
add_to_try(key_f11, KEY_F(11));
add_to_try(key_f12, KEY_F(12));
add_to_try(key_home, KEY_HOME);
add_to_try(key_ic, KEY_IC);
add_to_try(key_il, KEY_IL);
add_to_try(key_left, KEY_LEFT);
add_to_try(key_npage, KEY_PGDN);
add_to_try(key_ppage, KEY_PGUP);
add_to_try(key_right, KEY_RIGHT);
add_to_try(key_up, KEY_UP);
add_to_try(key_end, KEY_END);
}
void add_to_try(char *str, short code)
{
static BOOLEAN out_of_memory = FALSE;
struct try *ptr, *savedptr;
if (str == NULL || out_of_memory)
return;
if (_keytry != (struct try *) NULL)
{
ptr = _keytry;
for (;;)
{
while (ptr->ch != *str && ptr->sibling != (struct try *)NULL)
ptr = ptr->sibling;
if (ptr->ch == *str)
{
if (*(++str))
{
if (ptr->child != (struct try *)NULL)
ptr = ptr->child;
else
break;
}
else
{
ptr->value = code;
return;
}
}
else
{
if ((ptr->sibling =
(struct try *) malloc(sizeof *ptr)) == (struct try *)NULL)
{
out_of_memory = TRUE;
return;
}
savedptr = ptr = ptr->sibling;
ptr->child = ptr->sibling = (struct try *)NULL;
ptr->ch = *str++;
ptr->value = 0;
break;
}
} /* end for (;;) */
}
else /* _keytry == NULL :: First sequence to be added */
{
savedptr = ptr = _keytry = (struct try *) malloc(sizeof *ptr);
if (ptr == (struct try *) NULL)
{
out_of_memory = TRUE;
return;
}
ptr->child = ptr->sibling = (struct try *) NULL;
ptr->ch = *(str++);
ptr->value = 0;
}
/* at this point, we are adding to the try. ptr->child == NULL */
while (*str)
{
ptr->child = (struct try *) malloc(sizeof *ptr);
ptr = ptr->child;
if (ptr == (struct try *)NULL)
{
out_of_memory = TRUE;
ptr = savedptr;
while (ptr != (struct try *)NULL)
{
savedptr = ptr->child;
free(ptr);
ptr = savedptr;
}
return;
}
ptr->child = ptr->sibling = (struct try *)NULL;
ptr->ch = *(str++);
ptr->value = 0;
}
ptr->value = code;
return;
}
/* Включение альтернативного режима клавиатуры */
void keypad_on(){
if( keypadok ) return;
keypadok = TRUE;
if( keypad_xmit ) _puts( keypad_xmit );
}
/* Включение стандартного режима клавиатуры */
void keypad_off(){
if( !keypadok ) return;
keypadok = FALSE;
if( keypad_local ) _puts( keypad_local );
}
/* Тестовая функция */
int dotest()
{
struct termios saved_modes;
int c;
char *s;
char keyname[20];
if( tcgetattr(INPUT_CHANNEL, &saved_modes) < 0 ){
err: keyboard_access_denied();
}
if( tcsetattr(INPUT_CHANNEL, TCSADRAIN, &new_modes) < 0 )
goto err;
keyreset();
for(;;){
c = getch();
switch(c){
case KEY_DOWN: s = "K_DOWN" ; break;
case KEY_UP: s = "K_UP" ; break;
case KEY_LEFT: s = "K_LEFT" ; break;
case KEY_RIGHT: s = "K_RIGHT" ; break;
case KEY_PGDN: s = "K_PGDN" ; break;
case KEY_PGUP: s = "K_PGUP" ; break;
case KEY_HOME: s = "K_HOME" ; break;
case KEY_END: s = "K_END" ; break;
case KEY_BACKSPACE: s = "K_BS" ; break;
case '\t': s = "K_TAB" ; break;
case KEY_BACKTAB: s = "K_BTAB" ; break;
case KEY_DC: s = "K_DEL" ; break;
case KEY_IC: s = "K_INS" ; break;
case KEY_DL: s = "K_DL" ; break;
case KEY_IL: s = "K_IL" ; break;
case KEY_F(1): s = "K_F1" ; break;
case KEY_F(2): s = "K_F2" ; break;
case KEY_F(3): s = "K_F3" ; break;
case KEY_F(4): s = "K_F4" ; break;
case KEY_F(5): s = "K_F5" ; break;
case KEY_F(6): s = "K_F6" ; break;
case KEY_F(7): s = "K_F7" ; break;
case KEY_F(8): s = "K_F8" ; break;
case KEY_F(9): s = "K_F9" ; break;
case KEY_F(10): s = "K_F10" ; break;
case KEY_F(11): s = "K_F11" ; break;
case KEY_F(12): s = "K_F12" ; break;
case ESC: s = "ESC" ; break;
case EOF: s = "K_EOF" ; break;
case '\r': s = "K_RETURN"; break;
case '\n': s = "K_ENTER" ; break;
default:
s = keyname;
if( c >= 0400 ){
sprintf(keyname, "K_F%d", c - KEY_F(0));
} else if( iscntrl(c)){
sprintf(keyname, "CTRL(%c)", c + 'A' - 1);
} else {
sprintf(keyname, "%c", c );
}
}
printf("Клавиша: %s\n\r", s);
if(c == ESC)
break;
}
tcsetattr(INPUT_CHANNEL, TCSADRAIN, &saved_modes);
}
/* Функция настройки на систему команд дисплея */
void tinit (void) {
/* static */ char Tbuf[2048];
/* Tbuf должен сохраняться все время, пока могут вызываться функции tgetstr().
* Для этого он либо должен быть static, либо вызов функции keyinit()
* должен находиться внутри tinit(), что и сделано.
*/
char *tname;
extern char *getenv();
if((tname = getenv("TERM")) == NULL){
printf("TERM не определено: неизвестный тип терминала.\n");
exit(2);
}
printf("Терминал: %s\n", tname);
/* Прочесть описание терминала в Tbuf */
switch (tgetent(Tbuf, tname)) {
case -1:
printf ("Нет файла TERMCAP (/etc/termcap).\n");
exit (1);
case 0:
printf ("Терминал '%s' не описан.\n", tname);
exit (2);
case 1:
break; /* OK */
}
if(strlen(Tbuf) >= 1024)
printf("Описание терминала слишком длинное - возможны потери в конце описания\n");
keyinit(); /* инициализировать строки, пока Tbuf[] доступен */
}
void main(void){
setlocale(LC_ALL, "");
tinit();
/* keyinit(); */
dotest();
exit(0);
}
По поводу этого алгоритма надо сказать еще пару слов. Его модификация может с успехом
применяться для поиска слов в таблице (команд, ключей в базе данных, итп.): список
слов превращается в дерево. В таком поисковом алгоритме не требуются таймауты, необходимые при вводе с клавиатуры, поскольку есть явные терминаторы строк - символы
'\0', которых нет при вводе с клавиатуры. В чем эффективность такого алгоритма?
Сравним последовательный перебор при помощи strcmp и поиск в дереве букв:
"zzzzzzzzzza"
"zzzzzzzzzzb"
"zzzzzzzzzzbx"
"zzzzzzzzzzc"
"zzzzzzzzzzcx"
Для линейного перебора (даже в отсортированном массиве) поиск строки zzzzzzzzzzcx потребует
zzzzzzzzzza | 11 сравнений, отказ
zzzzzzzzzzb | 11 сравнений, отказ
zzzzzzzzzzbx | 12 сравнений, отказ
zzzzzzzzzzc | 11 сравнений, отказ
zzzzzzzzzzcx V 12 сравнений, успех
Всего: 57 шагов. Для поиска в дереве:
__z__z__z__z__z__z__z__z__z__z__a__\0
|_b__\0
| |_x__\0
|
|_c__\0
|_x__\0
потребуется проход вправо (вниз) на 10 шагов, потом выбор среди 'a','b','c', потом выбор среди '\0' и 'x'. Всего: 15 шагов. За счет того, что общий "корень" проходится
ровно один раз, а не каждый раз заново. Но это и требует предварительной подготовки данных: превращения строк в дерево!
© Copyright А. Богатырев, 1992-95
Си в UNIX
Назад | Содержание | Вперед