Program ttyconv powstał na bazie wcześniejszego programu ogonki, przeznaczonego do konwersji "w locie" znaków przechodzących między progremem użytkowym i terminalem. Początkowym przeznaczeniem tego programu było przekodowywanie między różnymi standardami polskich liter, tam gdzie sam program nie umożliwia wybrania odpowiedniego standardu. Program ten jednak został na tyle rozbudowany, że z łatwością można używać go także do innych celów, jak np. przedefiniowywanie klawiatury terminala, zdefiniowanie sobie skrótów klawiszowych do najczęściej używanych poleceń, czy używanie nietypowych znaków, niedostępnych w normalny sposób.
Program ttyconv umożliwia konwersję nie tylko 1:1 (czyli jeden znak na wejściu powoduje jeden znak na wyjściu), ale także n:1, 1:n czy nawet n:n, czyli można zamieniać całe ciągi znaków na inne ciągi. Jest to szczególnie użyteczne w przypadku obsługi klawiatury, na której naciśnięcie pewnej kombinacji klawiszy generuje ciąg kilku znaków (zwykle rozpoczynający się od znaku ESC).
Program uruchamia się podając sposób konwersji na wejściu (to znaczy z klawiatury), oraz na wyjściu (czyli wyświetlanych na ekranie). Można używać tylko jednej z tych konwersji, lub obydwu jednocześnie. Poszczególne konwersje zdefiniowane są w plikach, które program wyszukuje w katalogach podanych - analogicznie jak w zmiennej PATH - w zmiennej środowiskowej TTYCONV_PATH.
Listę dostępnych konwersji można uzyskać, uruchamiając program ttyconv z opcją -l.
Przy podawaniu konwersji można podać więcej niż jedną tabelę konwersji. Po pierwsze - można tabele sumować (w sensie mnogościowym), podając ich nazwy połączone znakiem plus (+). W ten sposób np. można użyć kilku zestawów skrótów klawiszowych, pisząc skr1+skr2+skr5. Efekt podania takiego parametru jest identyczny z podaniem nazwy pliku, będącego połączeniem wszystkich tabel. Innymi słowy - w czasie konwersji będą wyszukiwane skróty występujące kolejno we wszystkich tabelach. Jeżeli dany skrót ma różne rozwinięcia w występujących tabelach (np. w skr1 podano że kombinacja klawiszy `1 ma wygenerować tekst "blabluga" a w skr2 ta sama kombinacja `1 powinna wygenerować tekst "barbapapa"), to brany pod uwagę jest skrót występujący jako ostatni (w tym wypadku "barbapapa").
Drugą z możliwości łączenia tabel konwersji, jest znak dwukropka. Zasadniczo każde określenie sposóbu konwersji ma format t1+t2+...+tn:u1+u2+...+un Tabele podawane po dwukropku, są przed użyciem "obracane", tzn. skróty w nich zdefiniowane z postaci A->B są zamieniane na B->A (jeżeli np. tabela definiuje konwersję ze standardu CP1250 na ISO-8859-2 to po umieszczeniu jej po dwukropku otrzymamy konwersję z ISO-8859-2 na CP1250). Dla każdego skrótu najpierw dokonywana jest konwersja według tabel przed dwukropkiem, a następnie wynik tej konwersji jest poddawany drugiej konwersji, za pomocą tabel u1+u2+...+un.
Brzmi to może nieco skomplikowanie, ale daje możliwość tworzenia całych zestawów tabel, co ma zastosowanie m.in. przy tabelach konwersji polskich znaków. Są one zdefiniowane tak, że każda z nich definiuje konwersję ze standardu X na standard ISO-8859-2. Jeżeli teraz chcemy np. otrzymać konwersję z mazovii na CP1250, to wystarczy podać mazovia:cp1250. W ten sposób dane wejściowe najpierw będą konwertowane za pomocą tabeli mazovia, dzięki czemu otrzymamy dane zgodne ze standardem ISO, a następnie wynik tej konwersji zostanie poddany dalszej konwersji za pomocą odwróconej tabeli cp1250, która dokona zamiany z ISO na CP1250, w wyniku czego otrzymamy żądaną konwersję z mazovii na CP1250. Jeżeli chcemy dokonać konwersji z/na standard ISO, to możemy podać oczywiście konwersję w postaci (np.) cp1250: lub :cp1250. Aby jednak wyglądało to "ładniej", zdefiniowana jest również tabela iso, która jest tabelą pustą (nie dokonuje żadnej konwersji), można zatem napisać cp1250:iso lub iso:cp1250.
Jeżeli któraś z części przed lub po dwukropku jest pusta, to przyjmowana jest oczywiście pusta tabela, czyli dana konwersja nie jest dokonywana. Jeżeli w definicji konwersji w ogóle nie występuje dwukropek, traktowana jest ona jako część przed dwukropkiem t1+t2+...+tn, natomiast część po dwukropku jest przyjmowana jako pusta.
Pierwszą rzeczą do zrobienia jest oczywiście ściągnięcie źródeł programu.
Program składa się z jednego pliku źródłowego, napisanego w języku C z użyciem funkcji dostępnych w większości standardowych systemów Unix, w związku z czym jest skompilowanie powinno ograniczyć się do wydania polecenia:
cc -o ttyconv ttyconv.cPrzed kompilacją należy jednak sprawdzić czy odpowiadają nam opcje definiowane w pliku ttyconv.c w części zaznaczonej jako "Configurable section". A oto co można tam ustawić:
Po ustawieniu wyżej opisanych parametrów program można już skompilować. Otrzymany w wyniku skompilowany program możemy przetestować i jeżeli wszystko działa prawidłowo można go już umieścić w publicznie dostępnym katalogu. Drugim krokiem instalacji jest umieszczenie w innym publicznie dostępnym katalogu (np. /usr/local/lib/ttyconv) plików *.cnv, dostarczanych wraz z programem. Ostatnim krokiem jest globalne ustawienie zmiennej środowiskowej TTYCONV_PATH tak, aby wskazywała na katalog z (globalnymi) plikami *.cnv, a następnie na katalog aktualny (czyli np. /usr/local/lib/ttyconv:.). Oczywiście użytkownicy w razie potrzeby mogą sobie zdefiniować tę zmienną zgodnie z własnymi wymaganiami.
Pliki te określają tabele konwersji. Każdy taki plik zawiera definicje konwersji w postaci 'skrót' -> 'rozwinięcie', gdzie skrót to ciąg bajtów który należy odnaleźć, a rozwinięcie to ciąg jakim należy go zastąpić. Zarówno skrót jak i rozwinięcie muszą być ograniczone znakami cudzysłowu lub innymi, praktycznie dowolnymi znakami, z tym tylko ograniczeniem że znak rozpoczynający skrót musi być identyczny ze znakiem kończącym, lub w przypadku rozpoczęcia skrótu od nawiasu otwierającego, na końcu należy umieścić nawias zamykający. Innymi słowy można pisać np. 'Jasio', "Jasio", $Jasio$ czy {Jasio}, ale już nie 'Jasio's' (pierwszy apostrof zostałby potraktowany jako koniec tekstu). W tekście można stosować powszechnie znaną notację z użyciem znaku backslash, a konkretnie - \n, \r, \t, oznaczają odpowiednio znaki o kodach 13, 10 i 9, kombinacja \\ oznacza pojedynczy backslash, \ddd gdzie ddd to cyfry dziesiętne oznacza znak o podanym ósemkowo kodzie, natomiast w pozostałych przypadkach znak \ oznacza skopiowanie następnego znaku, nawet gdyby normalnie miał on oznaczać koniec ciągu (czyli przytoczony powyżej przykład można by zapisać jako 'Jasio\'s').
Zamiast tekstu w cudzysłowach, można też wpisać liczbę całkowitą - zostanie ona wtedy potraktowana jako kod znaku (np. 122 oznacza literę "z" - zgodnie z kodem ASCII).
Każdy skrót musi być umieszczony w oddzielnej linijce i musi się w tej linijce zmieścić w całości. Linie puste oraz zaczynające się od znaku # są traktowane jako komentarze i ignorowane przez program.
Pierwsza linijka programu musi być komentarzem (zaczynać się od znaku #) i powinna zawierać krótki (do 50 znaków) opis danego pliku. Opis ten będzie wyświetlany przy uruchomieniu programu ttyconv z opcją -l (wypisanie dostępnych tablic konwersji).
Sposób działania programu jest dość prosty, opiera się on na wykorzystaniu terminali wirtualnych (plików /dev/ptyXX i /dev/ttyXX), które pozwalają na uruchamianie programu jak na zwykłym terminalu, ale z pełną kontrolą wszystkich danych na wejściu i wyjściu tego terminalu.
Po uruchomieniu program przygotowuje drzewa konwersji (o czym dalej), po czym otwiera pierwszą wolną parę plików terminala wirtualnego. Po otwarciu terminalu program uruchamia nowy proces, ustawia otwarty wcześniej terminal wirtualny jako główny terminal tego procesu, po czym funkcją exec dokonuje uruchomienia właściwego procesu, podanego jako argument dla programu ttyconv (lub programu powłoki jeżeli nie podano nazwy programu do uruchomienia).
W tym samym czasie proces podstawowy rozpoczyna cykliczne odczytywanie standardowego wejścia oraz wyjścia z terminalu wirtualnego. Jeżeli zostaną odczytane jakieś dane, są one poddawane konwersji i przesyłane dalej, odpowiednio - dane ze standardowego wejścia przesyłane są na wejście terminalu wirtualnego, a dane z wyjścia terminalu wirtualnego przesyłane są na standardowe wyjście. Przetwarzanie takie jest wykonynane tak długo, jak długo istnieje proces potomny.
Znacznie ciekawszy od samego mechanizmu terminali wirtualnych, jest sposób konwersji danych. Ponieważ z założenia tabele konwersji mogą zawierać skróty w postaci n:n, czyli zamiana może dotyczyć nie tylko pojedynczych znaków ale także ich ciągów, dlatego też nie można było zastosować prostej tabeli z gotowymi skrótami dla 256 możliwych wartości każdego bajtu na wejściu. Najprostszym rozwiązaniem jest zbieranie przychodzących danych w tablicy, oraz cykliczne przeszukiwanie tabeli ze zdefiniowanymi skrótami za każdym razem gdy pojawi się nowy bajt (lub więcej bajtów). Jest to jednak bardzo nieoptymalne, ponieważ wymaga przeszukiwania tabeli skrótów dla każdego bajtu pojawiającego się na wejściu. Nawet gdyby zastosować najszybsze algorytmy wyszukiwania, to i tak program wykonywałby dla każdego bajtu masę operacji, których można uniknąć.
W programie ttyconv zastosowano szybszą i optymalniejszą metodę, wymagającą jedynie dodatkowego nakładu pracy przy uruchamianiu programu, a mianowicie bezpośrednio po uruchomieniu programu na podstawie podanych definicji tabel konwersji, tworzone są struktury danych nazwane przeze mnie drzewami konwersji.
Drzewo konwersji jest to klasyczne drzewo, w którym węzłami są kolejne bajty, a dodatkowo w każdym węźle może znajdować się ciąg znaków, wskazujący na rozwinięcie danego skrótu. Proces przetwarzania wygląda tak, że zaczynając od korzenia przeskakujemy do kolejnych węzłów drzewa zgodnie z otrzymywanymi znakami, zapamiętując jednocześnie ostatnio odnalezione rozwinięcie skrótu. W momencie gdy znajdziemy się w miejscu z którego nie jesteśmy w stanie przejść do kolejnego węzła, zwracamy (jako wynik konwersji) ostatnio odnaleziony skrót (oraz ewentualnie bajty który pojawiły się później i nie mogły zostać wykorzystanie w konwersji).
Przykład. Mamy zdefiniowaną następującą tablicę konwersji:
'!x' -> 'extraordinary' '!i' -> 'internationalization' '!is' -> 'i18n'
Na jej podstawie zostanie przy uruchomieniu programu wygenerowane następujące drzewko (można je obejrzeć po skompilowaniu programu z opcją #define DEBUG i podaniu opcji -d):
(ROOT) | +--(!) | +--(x) -> 'extraordinary' | +--(i) -> 'internationalization' | +--(s) -> 'i18n'
Przypuśćmy że na wejściu mamy znak 'u'. Program rozpocznie od korzenia drzewa, nie znajdzie możliwości przejścia dalej, w związku z czym zwróci ostatnio znaleziony skrót (w tym wypadku pusty) wraz ze znakiem 'u' nie wykorzystanym do konwersji. W efekcie znak 'u' zostanie po prostu zamieniony na znak 'u'. (i podobnie będzie ze wszystkimi znakami różnymi od '!').
Sprawa wygląda inaczej dopiero gdy na wejściu pojawi się znak '!'. Przypuśćmy że mamy na wejściu ciąg '!i?'. Pierwszy znak spowoduje przejście do węzła z wykrzyknikiem. Drugi znak przejście do węzła ze znakiem 'i' oraz zapisanie ciągu internalization jako ostatnio odnalezionego skrótu. Wreszcie znak '?' spowoduje że program nie będzie mógł znaleźć dalszej drogi w drzewie konwersji, zwróci zatem ciąg internalization? (znak '?' jest dodany na końcu, ponieważ nie został użyty do konwersji). A zatem wszystko zgodnie z tym, co zostało zdefiniowane w tabeli konwersji.
Patrząc na powyższe, należy zwrócić uwagę, że znaki na wyjściu programu pojawiają się dopiero wtedy, gdy można jednoznacznie ustalić wynik konwersji. W związku z tym, jeżeli zdefiniujemy sobie filtr wejściowy wyłapujący skróty rozpoczynające się od wykrzynika, tenże wykrzyknik stanie się automatycznie klawiszem "martwym" - jego naciśnięcie nie będzie powodować żadnej reakcji, dopiero naciśnięcie kolejnego klawisza spowoduje reakcję.
Dzięki drzewom konwersji program działa szybko - oczywiście nic za darmo, jest to okupione większym zużyciem pamięci. Jednak przeciętne drzewo konwersji zajmuje w zaledwie kilka KB pamięci, nie jest to zatem duży problem.
Piotr Piątkowski, kompas@uci.agh.edu.pl