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.
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).
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.
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 sanez 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.
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
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.
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()
.
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.