raw-mode Unixowego terminala

Autor: Wojciech Muła
Dodany:2003(?)

Osoby przyzwyczajone do DOS-owych funkcji getch() lub kbhit() odczuwają ich brak w Linuksie. Odpowiedniki tych funkcji są dostarczane w bibliotekach ncurses i slang, ale nie zawsze można pozwolić sobie na luksus ich używania, wtedy samodzielna obsługa raw-mode jest jedynym rozwiązaniem.

Interfejs do terminali zawarty jest w pliku nagłówkowym termios.h. Tryb pracy terminala jest opisany przez strukturę termios. Pobraniu tychże parametrów terminala służy funkcja tcgetattr(), zapisaniu tcsetattr(). Po szczegóły odsyłam do man termios, oraz info libc — rozdział "Low-Level Terminal Interface".

Terminale mogą pracować w dwóch trybach:

  1. kanonicznym (ang. canonical) -- tryb standardowy; linie są buforowane, terminal obsługuje klawisze specjalne (kill, erase itp.);
  2. niekanonicznym (ang. raw mode) — linie nie są buforowane, dane wejściowe są dostępne na poziomie bajtów, czyli tak samo jak w DOS.

Spośród wielu parametrów terminala interesujące są wyłącznie dwie flagi ICANON oraz ECHO przechowywane w polu c_lflag wspomnianej struktury. Flaga ICANON włącza tryb kanoniczny, natomiast ECHO włącza wyświetlanie wprowadzanych znaków na ekran; poniżej przykład przełączenia w tryb raw.

terminal.c:

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>

#include <unistd.h>
#include <fcntl.h>
#include <termios.h>

int tty_fileno; /* deskryptor terminala */

typedef struct {
          int  num;      /* ilość bajtów (0..8) */
          char array[8]; /* odczytane bajty     */
        } keycode;

keycode get_key()
{
 static keycode kc;

 /* odczytane zostanie co najwyżej 8 bajtów */
 if ((kc.num=read(tty_fileno, kc.array, 8)) == -1)
        kc.num = 0;

 errno = 0;
 return kc;
}

keycode wait_for_key()
{
 fd_set read_set;

 FD_ZERO(&read_set);
 FD_SET (tty_fileno, &read_set);

 /* czekaj na klawisz */
 select(tty_fileno+1, &read_set, NULL, NULL, NULL);
 return get_key();
}

extern int errno;
void check_errno()
{ if (errno)
        { perror("");
          exit(EXIT_FAILURE); }
}

int main()
{
 struct termios term;
 char    eof; /* kod znaku EOF */
 keycode k;
 int     i;

 tty_fileno = open("/dev/tty", O_RDONLY | O_NONBLOCK);
 check_errno();

 /* pobranie parametrów terminala */
 tcgetattr(tty_fileno, &term);
 check_errno();
 eof = term.c_cc[VEOF]; /* znak EOF, prawdopodobnie będzie to \004 */

 /* wyłączenie trybu kanonicznego oraz echa */
 term.c_lflag &= ~(ICANON | ECHO);
 tcsetattr(tty_fileno, TCSAFLUSH, &term);
 check_errno();

 /* wyświetlane są kody klawiszy, koniec po naciśnięciu CTRL-D */
 while (1)
        {
         k = wait_for_key(); /* pobierz kod klawisza */

         if (k.num == 1 && k.array[0] == eof)
                {
                 puts("CTRL-D!");
                 break;
                }

         printf("%d: ", k.num);
         for (i=0; i<k.num; i++)
                printf("\\%03o ", (unsigned char)k.array[i]);
         putchar('\n');
        }

 puts("naciśnij dowolny klawisz...");
 wait_for_key();

 /* przywrócenie zmienionych parametrów */
 term.c_lflag |= (ICANON | ECHO);
 tcsetattr(tty_fileno, TCSAFLUSH, &term);
 check_errno();

 return 0;
}

Plik terminala /dev/tty otwierany jest w trybie nieblokującym, dzięki czemu można było łatwo zaimplementować sekwencję:

if (kbhit())
        return getch();
else
        return NO_KEY;

przy pomocy pojedynczego wywołania funkcji read; w przeciwnym razie należałoby użyć funkcji select(). Jeśli Czytelnik programował w DOS-ie pamięta zapewne sprawdzanie czy naciśnięty został 2-bajtowy klawisz rozszerzony, czy zwyczajny 1-bajtowy, w Linuksie jest jeszcze gorzej, bowiem klawiszom przypisywane są kody nawet 5-bajtowe. Dlatego też, by zaoszczędzić powtarzających się sekwencji kodu, funkcje get_key() oraz wait_for_key() zwracają od razu wszystkie bajty wygenerowane przez klawisz w strukturze keycode. Niestety sama klasyfikacja poszczególnych kodów musi zostać wykonana samodzielnie, gdyż w zależności od terminala te same klawisze mogą wysyłać różne kody. Proponuję zapoznać się z man termcap.