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 шагов. За счет того, что общий "корень" проходится ровно один раз, а не каждый раз заново. Но это и требует предварительной подготовки данных: превращения строк в дерево!

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

Используются технологии uCoz