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

Назад | Содержание | Вперед