Obsługa terminala i klawiatury

Dawno temu było sobie 100 terminali (może nawet więcej), takich materialnie istniejących. Terminal podłączało się do komputera linią szeregową (dla przyjaciół RS-232), każdy miał monitor i klawiaturę. Każdy też potrafił wysyłać znaki ASCII odpowiadające naciśniętym klawiszom i odbierać znaki ASCII do wyświetlania na ekranie.

Terminale miały jednak większe możliwości, potrafiły obsługiwać klawisze sterujące (strzałki, klawisze funkcyjne) oraz wyświetlać otrzymywane znaki w dowolnym miejscu na ekranie, dodatkowo dodając atrybuty takie jak kolor czy podkreślanie.

Aby korzystać z dodatkowych możliwości, używało się sekwencji sterujących, zwykle zaczynających się znakiem Escape (były to czasy znaków ASCII i nie było w kodowaniu znaków miejsca na takie fanaberie). Cóż, kiedy każdy terminal używał innej sekwencji sterującej do wykonania tej samej operacji.

Programiści musieli pisać kody wariantowe dla różnych rodzajów terminali. Szybko powstały więc specjalne biblioteki izolujące od szczegółów terminali. Były oparte o specjalizowane bazy danych: termcap, a potem terminfo. Trzeba było ustawić zmienną środowiska TERM na używany model terminala, po czym z bazy danych wydobywano opis jego sekwencji sterujących.

Ponieważ bezpośrednie operowanie sekwencjami było nużące, wymyślono bibliotekę curses, obudowującą te sekwencje poręcznymi funkcjami

Jak to zwykle bywa, niektóre terminale były lepsze od innych, więc inni producenci stopniowo się do nich dostosowywali, dodając mechanizm dodatkowych trybów pracy. Najpopularniejsze były terminale firmy DEC (Digital Equipment): VT52 a potem VT100. Ich sekwencje sterujące stały się podstawą quasi-standardu ANSI.

Na współczesnych małych komputerów nie ma powodu używania sekwencji sterujących, cały ekran lub okno mamy ,,u siebie''. Tradycja jednak pozostała, zwłaszcza że przydaje się gdy pracujemy zdalnie w sieci.

Minimum wysiłku

Spośród emulatorów terminali (czyli programów udających terminale, popularnie zwanych ,,konsolą'') wybieramy taki, których obsługuje sekwencje ANSI. Sprawdzamy ustawienie zmiennej TERM, w prawdziwym trybie znakowym powinno być ansi albo linux, w trybie graficznym pod Debianem na ogół mają xterm, też ok.

Odnajdujemy tabelkę z sekwencjami sterującymi ANSI (na przykład man console_codes) i wybieramy z niej sekwencje, najlepiej obudować je funkcjami lub makrami. Na przykład dla przesunięcia kursora do pozycji 5,10 sekwencją jest "^[[5;10H" (pierwszy znak to Escape).

Jeśli nie mam pod ręką tabelki, mogę posłużyć się poleceniem tput z Terminfo

tput cup 5 10 >foo.txt

Na pliku foo.txt będzie gotowa sekwencja do wklejenia do programu (dobrym edytorem, np. Emacs). Przy pisaniu na ekran nie należy używać funkcji typu printf(), tylko zwykłego write(). Przykład tutaj Normalny sposób to użyć biblioteki terminfo lub curses (dalej).

Prosta obsługa klawiatury

Niektóre klawisze po naciśnięciu wysyłają więcej niż jeden znak, na przykład klawisze strzałek. Można je obejrzeć poleceniem

zbyszek@katastrofa5:~/txt/unix$ cat -v
^[[A^[[B^[[D^[[C^[OP
^[[A^[[B^[[D^[[C^[OP

Widzimy efekt naciśnięcia klawiszy kolejnych strzałek, a potem klawisza F1. Kazda sekwencja znaków zaczyna się od Escape (^[), a potem kolejnych znaków, np. "[A". Druga linia to echo wypisane przez cat. W ten sposób możemy badać,co nacisnął użytkownik.

Przestawienia kursora można dokonać poleceniem tput z terminfo, używając opcji cup

tput cup 12 30

Zwraca ona ciąg znaków, którego wypisanie na ekranie powoduje przestawienie kursora w zadane miejsce.

Program setraw.c pokazuje, jak przełączyć klawiaturę w stan surowy, gdy czytanie odbywa się pojedynczymi klawiszami. Dla przetestowania należy ten program skompilować ze zdefiniowaną stałą TEST. Do zmiany atrybutów terminala używa się w nim funkcji tcgetattr i tcsetattr, opisanych dalej.

Polecam zwłaszcza naciśnięcie ^C. To taki zwykły znak. Program kończy się po naciśnięciu klawisza q.

Inny przykład to kbhit.c: procedura czytająca ciągi i znaków i każdorazowo przełączająca terminal w tryb surowy. Testowanie podobnie, wyjście gdy w pierwszej pozycji bufora znak q.

Obsługa klawiatury

Wczytywanie znaków z klawiatury przez program użytkowy musi być dokonywane funkcjami systemu operacyjnego. Sterownik klawiatury ma dwa podstawowe tryby pracy: cooked (,,ugotowany'' czytaj normalny) oraz raw (surowy).

W normalnym trybie pracy sterownik klawiatury wyświetla wprowadzane znaki na ekranie (daje tzw. ,,echo'') oraz buforuje je wewnętrznie, udostępniając je dopiero po skompletowaniu całej linii (czyli po naciśnięciu klawisza oznaczającego znak końca linii). Dzięki temu działają takie operacje jak ,,backspace''. Nie można pobrać pojedynczego znaku (chyba, że jest to znak końca linii ;-).

W trybie surowym sterownik staje się przezroczysty. Tak naprawdę obecnie nie ma jednego trybu surowego, można przełączać dowolną opcję sterownika niezależnie.

Kto chce poznać opcje terminala, powinien zaprzyjaźnić się z poleceniem stty. Służy do przestawiania opcji terminala.

Uwaga: może się zdarzyć, że program ,,zawiśnie'', ekran jest czarny albo zachowuje się dziwnie. Pomaga wpisanie

stty sane
z naciśnięciem Ctrl-J zamiast Enter. Istnieje też taki tryb surowy, w którym program nie reaguje na żaden znak sterujący, zapomnijmy o Ctrl-C, Ctrl-\ itp. Pomaga zdalne zalogowanie z innego komputera i ubicie sesji.

Do sterowania pracą terminala tekstowego służy systemowa struktura termios:

struct termios { 
  tcflag_t c_iflag; /* input mode flags */
  tcflag_t c_oflag; /* output mode flags */
  tcflag_t c_cflag; /* control mode flags */
  tcflag_t c_lflag; /* local mode flags */
  cc_t c_line; /* line discipline */
  cc_t c_cc[NCCS]; /* control characters */
  speed_t c_ispeed; /* input speed */
  speed_t c_ospeed; /* output speed */
};

Typ tcflag_t ma w Linuxie 16 bitów, zaś cc_t 8 bitów.

Dostęp do tej struktury uzyskuje się wywołaniem systemowym ioctl z odpowiednimi argumentami. Należy tego unikać na Linuksie, preferując specyficzne procedury dla poszczególnych urządzeń, dostępne z C, np. dla terminala tcgetattr, tcsetattr itp.

Flagi dla pól zdefiniowane są w pliku termios.h.

Poszczególne indeksy do tablicy c_cc zdefiniowano następująco:

/* c_cc characters */
#define VINTR 0
#define VQUIT 1
#define VERASE 2
#define VKILL 3
#define VEOF 4
#define VTIME 5
#define VMIN 6
#define VSWTC 7
#define VSTART 8
#define VSTOP 9
#define VSUSP 10
#define VEOL 11
#define VREPRINT 12
#define VDISCARD 13
#define VWERASE 14
#define VLNEXT 15
#define VEOL2 16

Gdyby ktoś był ciekawy: fizyczne adresy portów klawiatury to 0x60-0x6f. Ale jeśli lubicie swój komputer, to nie programujcie z konta root'a.

Terminfo

Baza danych terminfo znajduje się w katalogu /usr/lib/terminfo lub /usr/share/terminfo w postaci skompilowanej. Do oglądania (dekompilacji) pozycji z bazy danych terminfo służy program infocmp. Program tic służy natomiast do kompilacji nowego (lub zmienionego) opisu terminfo.

Polecenie

$ tput longname
linux console
$ _
daje na ekran krótki opis terminala lub drukarki.

Testowanie pozycji terminfo (np. smso, rmso):

tput smso
echo "Wszystko w porządku"
tput rmso

so to skrót od standout, prefiksy sm i rmso włączają i wyłączają daną opcję. Na przykład smacs i rmacs włączają i wyłączają alternatywny zbiór znaków.

Aby włączyć wyłączony kursor, należy wpisać tput cnorm przy prompcie konsoli.

Prawy dolny róg ekranu jest niedostępny do pisania, bo powoduje na większości prawdziwych terminali scrolling ekranu. Nie zachodzi to tylko jeżeli terminal nie posiada własności am (automatic margins). Między innymi z tego względu zmniejszano wszystkie ekrany do 24 linii.

Dla takich opcji boolowskich polecenie tput zwraca 0 (dla prawdy) lub 1 (dla fałszu)

zbyszek@katastrofa5:~/txt/unix$ tput am
zbyszek@katastrofa5:~/txt/unix$ echo $?
0

Curses

Każdy program w curses pownien rozpoczynać się wywołaniem initscr(), a kończyć wywołaniem endwin().

Procedury dla znaków takie, jak addch czy waddch, mają argument typu chtype (32/64 bity), a nie char. Na górnych bitach atrybuty.

Znaki do budowy ramek są typu chtype, np. ACS_ULCORNER (opis w man curs_addch).

Funkcje inch, winch, ... odczytują znak, znajdujący się w danej pozycji okna, zwracają chtype.

Do formatowanego wypisywania używa się funkcji rodziny printw.

Procedura curs_set(typ) służy do zmieniania kursora, przy czym typ równy 0 powoduje zgaszenie kursora, zaś 1 lub 2 jego zapalenie w wybranej postaci.

Klawiatura

Do czytania z klawiatury służy getch(), zwraca int. Konfiguracja czytania:

cbreak() oczekuj na naciśnięcie
nocbreak() nie czekaj, zwróć ERR
echo() wyświetlaj wpisywane znaki
noecho() nie wyświetlaj

Aby czytał klawisze specjalne należy użyć keypad(WINDOW*, bool), na przykład

keypad(stdscr, TRUE);
i wtedy zwraca takie wartości, jak KEY_DOWN (inne zob. man curs_getch).

Do czyszczenia bufora klawiatury służy flushinp().

Kolor

Funkcja has_colors() zwracająca bool bada, czy terminal jest kolorowy. Kolory startujemy przez start_color().

Funkcja init_pair inicjuje nową parę kolorów, stałe dla argumentów to COLOR_WHITE itp.

Wybór nowego sposobu wyświetlania robi się funkcjami acoderset(atrybut) [zwracają int ???]. Argument najczęściej buduje się makrem

attr_t COLOR_PAIR(n)
albo używa predefiniowanych atrybutów, np. A_NORMAL. Atrybuty (raczej tylko predefiniowane) można też dodawać i wyłączać selektywnie
attron(atrybut),  attroff(atrybut)
np. dodając A_BOLD otrzymuje się błękitny (jasnoniebieski) z niebieskiego. Może to nie działać na niektórych terminalach, np. na ansi działa, ale na linux nie.