Když jsem začínal psát pspg
, tak jsem byl netknutý znalostmi o ncurses. Jen jsem tušil něco o terminálech, a jelikož jsem dlouholetý uživatel midnight commandera, tak jsem věděl, že to co chci nějak jde (protože jsem věděl, že mc
běží nad ncurses (případně nad slang). Musel jsem si ale projít hromadou záseků, než jsem alespoň trochu pochopil, jak ncurses fungují a než jsem pspg
dostal do podoby, se kterou jsem spokojený.
Dokumentace, jak používat ncurses je, pokud nepotřebujete expertní úroveň nebo náhled na interní implementaci, dost. Naopak, pokud potřebujete vyřešit něco méně obvyklého, tak jsou tři možnosti – projít si zdrojáky, případně dema ve zdrojácích k ncurses, nebo zkusit kontaktovat Thomase E. Dickeyho. Thomas je dlouholetý stávající maintainer ncurses, a asi i jediný nebo jeden z mála žijících expertů na ncurses. Radí velice ochotně (a zasvěceně).
Dokumentace k ncurses
Základem dokumentace jsou manuálové stránky. Někdy jsem tam, to co jsem potřeboval, nenašel, ale pro každodenní práci jsou docela v pohodě. Potom určitě je dobré si předem přečíst ncurses HOWTO. V ncurses FAQ od Thomase Dickeyho je také dost podstatných informací. U všech ostatních dokumentů, které jsem našel na internetu, jsem měl pocit, že kloužou po povrchu, a nenašel jsem v nich nic zajímavého. Neoficiálním kanálem ohledně supportu k ncurses je mailing list bug-ncurses, kde můžete kontaktovat Thomase E. Dickeyho. Myslím si, že v tomto mailing listu budou všichni na planetě, kteří o ncurses něco ví.
Ladění
Ještě jednou zopakuji jednoduchý postup, jak tisknout ladící texty. Ve výchozím nastavení veškerý výstup z aplikace (ať generovaný ncurses nebo uživatelem) jde na terminál. Jakmile vývojář si pošle cokoliv na stdout
nebo stderr
, tak to buď nevidí nebo rozbije layout aplikace. Proto je nutný redirect stderr
. Postup je jednoduchý:
- V jiném terminálu spustit příkaz
tty
, který vrátí jméno souboru zařízení spojeného se standardním výstupem terminálu - V aplikaci ladící výpisy posílat do
stderr
(a nezapomínat na'\n'
) na konci řádku - Laděnou aplikaci spustit s přesměrováním
stderr
do souboru jehož jméno vrátí příkaztty
#terminal 1 [pavel@localhost ~]$ tty /dev/pts/3 #terminal 2 [pavel@localhost pspg-master]$ ./pspg -f tests/pg_class.txt 2>/dev/pts/3
Ladění ncurses aplikace napsané v Cčku je stejné, jako jakékoliv aplikace napsané v Cčku. Hodně pomohou nástroje jako jeASan
nebo Valgrind
.
Architektura ncurses a optimalizace zápisů na výstupní zařízení (terminál)
Funkcionalitu ncurses bychom mohli rozdělit na několik částí nebo vrstev. Nejnižší vrstvou je databáze escape sekvencí, která umožňuje provozovat aplikaci napsanou nad ncurses vůči širokému spektru terminálů. To mělo svůj význam primárně v 90 letech, kdy se ještě používaly fyzické terminály. V posledních několika letech smysl této databáze klesá. Minimálně v Linuxu je shoda v emulaci xtermu s podporou true color, a jinde v ANSI escape sekvencích. Nový port Turbo Vision v defaultu jede nad ANSI escape sekvencemi, a jede jak víno. Mimochodem, komplexnější terminálovou aplikaci s dialogy, s vestavěným editorem, s vestavěným terminálem bych určitě psal nad Turbo Visionem (v nové portaci, je to rychlé, podporuje to utfko) než v ncurses. ncurses má smysl, jen pokud chcete napsat něco opravdu lehkého, bez větších závislostí.
[pavel@localhost ~]$ set|grep TERM COLORTERM=truecolor TERM=xterm-256color
Prostřední vrstva v ncurses se stará o optimalizaci zobrazení obsahu. Smyslem této vrstvy je redukce objemu dat posílaných terminálům. Nezapomínejme, že nejstarší fyzické terminály byly pomalé, a byly připojené relativně pomalou sítí (což v případě vzdáleného připojení může platit dodnes). Místo toho, aby se bezprostředně při tisku generovaly escape sekvence, tak se informace o obsahu a atributech uloží do lokálního bufferu. Abstrakcí pro takový buffer je objekt v ncurses označovaný jako WINDOW
. V dnešní terminologii by se spíš použil název tiling window (dlaždicové okno). Okna si můžeme vytvářet vlastní, nebo můžeme použít před připravené okno stdscr
. Při refreshi okna se obsah okna zkopíruje do okna newscr
.
Důrazně se doporučuje, aby se okna nepřekrývala. Nikde se neudržuje informace o hloubce. Pořadí kopírování obsahu neurčuje „hloubka“, ale pořadí volání funkce refresh
. Navíc, pokud se obsah okna nezměnil, tak se (v závislosti na implementaci), ignoruje požadavek na refresh. Pokud bychom měly okna, která se překrývají, tak musíme refreshovat okna v závislosti na hloubce (odzadu), a musíme si vynucovat provedení refreshe funkcí touchwin
. Poté, co se zkompletuje obsah okna newscr
(tím, že se provede refresh stdscr
a oken vytvořených aplikací), tak se provede detekce změn jeho obsahu (včetně detekce posunu řádků) s obsahem okna curscr
. Na základě této analýzy se pak generují escape sekvence, které se pošlou na terminálové zařízení, a aktualizuje se obsah okna curscr
. Jedná se o určitou implementaci double bufferu. Není to úplně dokonalé, skutečný double buffering na straně terminálu to plně zastoupit nemůže.
V praxi se ukazuje, že popsaná optimalizace funguje (flickering to redukuje docela dobře). Hlavní je, že programátor nemusí u vizuálně náročnější aplikace řešit optimalizaci výstupu. V dokumentaci se dost zdůrazňuje použití funkcí refresh
a případně tochwin
, tudíž předpokládám, že důsledky této optimalizace jsou pro nezkušené programátory problémovým bodem v ncurses.
Třetí nejvyšší vrstvou je implementace několika komponent: menu
, pad
, panel
a podpora formulářů (tj komponent pro label a edit box). Menu se používá docela hodně. Tab complete v knihovně readline používá tuto komponentu. Není to mnou očekávané CUA menu (jako je v Turbo Vision), ale spíš výběr z n hodnot, které mohou být zobrazené v několika sloupcích. Komponentu pad
můžeme použít k zobrazení delšího a širšího textu než jsou rozměry monitoru – umožňuje skrolování v obou směrech. V úplně nejstarší verzi pspg
jsem tuto komponentu používal pro zobrazení obsahu. Pak jsem s tím přestal, protože inicializace pro větší obsah (celý text musí být vložený do komponenty před zobrazením) byla pomalá (mám pocit, že i samotné vytvoření většího padu nebylo extra rychlé). Ale pro kratší texty (nápověda) v rozsahu několika set řádek může fungovat perfektně. Panely se používají hodně. Panely implementují překrývající se okna. Udržují informaci o hloubce, a ve správném pořadí pak refreshují okna. Naopak formuláře moderní aplikace prakticky nepoužívají (co je mi známo).
U aplikace, která používá více oken, se doporučuje sloučit zápis na výstupní zařízení pro všechna okna dohromady. Toho se dosáhne použitím funkce wnoutrefresh
(namísto wrefresh
). Tato funkce pouze propaguje obsah okna do okna newscr
. Druhá fáze zobrazení (optimalizace) se pak provede ve funkci doupdate
. U oken je to možnost (silně doporučovaná), u panelů pak nutnost (pokud používáte panely a sami refreshujete okna, které panely obsahují, tak můžete vidět nechtěné vizuální defekty), U mne v pspg
je na obrazovce cca 6 oken v jednu chvíli, a než jsem začal používat wnoutrefresh
, tak jednak aplikace byla i na relativně moderním železe trochu lína, a nepříjemně často si uživatelé mohli všimnout flickeringu.
V souvislosti se snahou o redukci flickeringu je potřeba si dát pozor na volání vstupních funkcí ncurses. Ty implicitně volají refresh okna vůči kterému jsou volány, pokud došlo k jeho změně od posledního refreshe. Tedy – z rutiny getch()
se volá refresh(stdscr)
, z rutiny wgetch(win)
se volá wrefresh(win)
. Dává to smysl. Dříve než budu čekat na stisk klávesy, tak chci mít zobrazený výstup. V halfdelay režimu nebo nodelay režimu ale implicitní refresh může hodně zpomalovat aplikaci (zkoušel jsem implemetaci přednačítání (prefetch) a slučování některých událostí generovaných myší), a musel jsem si dát velký pozor, abych vstupní funkce volal z jednoho místa, kdy už jsem měl nový obsah kompletně připravený.
Hodně mi pomohlo, když jsem si uvědomil, že okna jsou jen abstrakce ncurses. V terminálu nic jako okno není. Některé terminály maximálně umí odskrolovat část obrazovky. Dost konfigurace ncurses je pověšeno na okna. Ve výsledku to znamená, že se při použítí nějaké operace nad oknem, která vyžaduje určité nastavení, musí změnit odpovídající globální nastavení na terminálu. Po provedení operace se toto globální nastavení (stav terminálu), může (ale také nemusí) vrátit do výchozího stavu (před voláním operace).
Tisk na obrazovku, tisk do okna, rozdíl mezi funkcemi newwin
a subwin
Už jsem zmínil, že okno ( WINDOW
) je ústředním objektem ncurses. Kromě bufferu pro zobrazovaná data drží ještě pozici kurzoru a konfiguraci (například aktuální styl). K modifikaci tiskového bufferu máme několik funkcí, přičemž pro každou z těchto funkcí ještě existuje několik variant s odlišným prefixem názvu (relativně klasický design knihoven v Cčku v 80 letech). Variace jsou ve dvou dimenzích. První dimenze určuje jestli se pracuje se standardní obrazovkou, což je před připravené okno referencované globální proměnnou stdscr
, nebo jestli se pracuje s oknem, které si vytvořila aplikace (používá se prefix w
). Například máme funkce refresh
a wrefresh
. Funkce pro standardní obrazovku jsou dost často řešené jako makro:
#define insstr(s) winsstr(stdscr,(s)) #define instr(s) winstr(stdscr,(s)) #define move(y,x) wmove(stdscr,(y),(x)) #define refresh() wrefresh(stdscr) #define scrl(n) wscrl(stdscr,(n))
Další dimenze určuje jestli se má text pozicovat na aktuální pozici kurzoru (bez prefixu) nebo se má nejdříve kurzor posunout na specifikovanou pozici a pak tisknout (prefix mv
). Existuje modifikace funkce printf
přizpůsobená pro použití v ncurses. Její variace jsou příkladem výše popsaného designu:
int printw(const char *fmt, ...); int wprintw(WINDOW *win, const char *fmt, ...); int mvprintw(int y, int x, const char *fmt, ...); int mvwprintw(WINDOW *win, int y, int x, const char *fmt, ...); int vw_printw(WINDOW *win, const char *fmt, va_list varglist);
Funkce printf
je v IT klasika. Pozor, pokud intenzivněji tisknete, a optimalizujete na rychlost, tak tyto funkce používejte pouze v případech, kdy opravdu potřebujete pracovat s formátovacím řetězcem. Interpretace formátovacího řetězce má svojí režii. Navíc se uvnitř těchto funkcí ještě dynamicky alokuje a uvolňuje paměť. Jednodušší a rychlejší alternativou jsou funkce z rodiny addstr
:
int addstr(const char *str); int addnstr(const char *str, int n); int waddstr(WINDOW *win, const char *str); int waddnstr(WINDOW *win, const char *str, int n); int mvaddstr(int y, int x, const char *str); int mvaddnstr(int y, int x, const char *str, int n); int mvwaddstr(WINDOW *win, int y, int x, const char *str); int mvwaddnstr(WINDOW *win, int y, int x, const char *str, int n);
Pro aplikace s vizuálně statickým obsahem je to jedno (na soudobém hw). Pokud aplikace umožňuje intenzivní skrolování (např. kolečko myši generuje hodně událostí rychle za sebou), tak už je tam pocitový rozdíl (samozřejmě viditelný v profileru). Tyto funkce pracují s základními řetězci případně s řetězci UTF8 znaků.
Většina ze zmíněných funkcí ještě existuje pro řetězce širokých znaků (wide char strings):
int add_wchstr(const cchar_t *wchstr); int add_wchnstr(const cchar_t *wchstr, int n); int wadd_wchstr(WINDOW * win, const cchar_t *wchstr); int wadd_wchnstr(WINDOW * win, const cchar_t *wchstr, int n); int mvadd_wchstr(int y, int x, const cchar_t *wchstr); int mvadd_wchnstr(int y, int x, const cchar_t *wchstr, int n); int mvwadd_wchstr(WINDOW *win, int y, int x, const cchar_t *wchstr); int mvwadd_wchnstr(WINDOW *win, int y, int x, const cchar_t *wchstr, int n);
Pro začátek je asi obtížné se v tom zorientovat, díky množství variací. Praxe je pak poměrně jednoduchá (jakmile člověk pochopí ten systém). Wide char funkce jsem v pspg
použil asi na dvou místech. Záleží na aplikaci jestli jede nad UTFkem nebo nad wide chary.
Pod pojmem standardní obrazovka se v ncurses většinou míní implicitní okno v proměnné stdscr
. Okna mají vlastní souřadný systém, což může zjednodušit programování, když pracujete s okny, které se mohou posouvat po obrazovce terminálu. Okno se vytvoří voláním funkce newwin
a zrusí voláním funkce delwin
(viz manuálová stránka newwin
). Kromě oken můžete používat podokna ( subwin
), což je okno svázané se svým rodičem bez vlastního bufferu pro zobrazovaný obsah. Pokud zapisuji do podokna, tak fakticky přímo zapisuji do bufferu kořenového okna. Primárním účelem poddoken je separátní konfigurace a separátní souřadný systém.
Pokud chci zobrazit obsah standardní obrazovky, okna (případně podokna) na terminálu, musím zavolat funkci refresh
nebo wrefresh
. Bez toho ncurses nevygeneruje tu změnovou sekvenci znaků, kterou posléze pošle terminálu. Jakmile člověk pochopí, že se zapisuje do lokálních bufferů a ne do terminálu, tak je to jasné, a nejsou s tím žádné problémy.
Konvence zápisu pozice je číslo řádku, číslo sloupce s nulovým počátkem v levém horním rohu. Bloky se v některých funkcích definují počet řádků, počet sloupců, a pozice levého horního rohu. Případně se ale také můžete setkat s definicí pozice levého horního rohu, pozice pravého dolního rohu:
#include <ncurses.h> /* * POZOR PAST */ int main() { WINDOW *form; initscr(); clear(); form = newwin(10, 10, 5, 5); box(form, 0, 0); wrefresh(form); getch(); endwin(); }
Kontrolní otázka: Když si otestujete výše uvedený příklad, tak na obrazovce neuvidíte rámeček 10×10 znaků. Proč?
#include <ncurses.h> #include <locale.h> int main() { WINDOW *form; WINDOW *s; /* pro tisk UTF znaku je nutne!!! */ setlocale(LC_CTYPE,"C.UTF8"); initscr(); start_color(); form = newwin(10, 10, 5, 5); /* vytvor vnorene okno uvnitr formulare */ s = derwin(form, 8, 8, 1, 1); box(form, 0, 0); init_pair(1, COLOR_RED, COLOR_BLACK); /* nastav pro vnorene okno styl cervena na cernem */ wattron(s, A_BOLD | COLOR_PAIR(1)); /* text bude zalaman do vnoreneho okna */ mvwaddstr(s, 6, 0, "Příliš žluťoučký kůň"); /* * neni nutne refreshovat s, a v tomto pripade ani form. * wgetch vola wrefresh interne. */ wgetch(form); endwin(); }
Poměrně nepříjemná vlastnost ncurses je zalamování textů přesahujících šířku okna. Pokud text přesáhne výšku okna, tak se odskroluje (pokud je to povolené (ve výchozím nastavení je to zakázané)). S tím se docela zápasí, protože širší text, než je okno, vám rozbije layout.
V ncurses existuje rodina funkcí s prefixem ch
, která ořezává text. Problém je, že tyto funkce existují pouze pro interní typy chtype
string (8bit) a pro cchar_t
string (wide chars). S těmito transformacemi je docela dost práce, takže jsem tyto funkce nepoužíval. Místo toho jsem si počítal, kolik bajtů z řetězce se mi vejde do okna, a používal jsem funkce, kde mohu určit, kolik bajtů se má tisknout.
Mnohem jednodušší je použít “komponentu” (okno) typu pad
:
#include <ncurses.h> #include <locale.h> int main() { WINDOW *boxs; WINDOW *pad; /* pro tisk UTF znaku je nutne!! * dale je nutne linkovat s knihovnou ncursesw */ setlocale(LC_CTYPE,"C.UTF8"); initscr(); start_color(); clear(); boxs = subwin(stdscr, 10, 10, 5, 5); /* 10 radku, 50 sloupcu */ pad = newpad(12, 50); box(boxs, 0, 0); init_pair(1, COLOR_RED, COLOR_BLACK); /* nastav pro vnorene okno styl cervena na cernem */ wattron(pad, A_BOLD | COLOR_PAIR(1)); mvwaddstr(pad, 0, 0, "Příliš žluťoučký kůň"); mvwaddstr(pad, 1, 0, "se napil žluté vody."); mvwaddstr(pad, 2, 0, "Příliš žluťoučký kůň"); mvwaddstr(pad, 3, 0, "se napil žluté vody."); mvwaddstr(pad, 4, 0, "Příliš žluťoučký kůň"); mvwaddstr(pad, 5, 0, "se napil žluté vody."); mvwaddstr(pad, 6, 0, "Příliš žluťoučký kůň"); mvwaddstr(pad, 7, 0, "se napil žluté vody."); mvwaddstr(pad, 8, 0, "Příliš žluťoučký kůň"); mvwaddstr(pad, 9, 0, "se napil žluté vody."); mvwaddstr(pad, 10, 0, "Příliš žluťoučký kůň"); mvwaddstr(pad, 11, 0, "se napil žluté vody."); /* musim dodrzet poradi refreshe */ refresh(); /* prekryj vyrezem z padu od interni pozice 1, 1 * vnitrek okna form definovany levym hornim rohem * a pravym dolnim rohem. */ prefresh(pad, 1, 1, 6, 6, 13, 13); getch(); endwin(); }
S touto komponentou se docela jednoduše implementuje skrolování ve všech směrech. Ve funkci prefresh
jen změním počátek výřezu uloženého v bufferu pad
u (první dva číselné argumenty).
Rozšířená znaková sada, speciální znakové typy v ncurses
Bez ohledu na použitou znakovou sadu fyzické terminály umožňovaly používat několik znaků pro vykreslení rámečků, případně dalších znaků. Speciální escape sekvencí se vybrala jiná znaková sada, další sekvencí se vrátila původní znaková sada. Bavíme se o době, kdy se ještě používala 7bitová kódování, a 8 bit se používal pro paritu. ncurses samozřejmě tuto funkcionalitu obaluje a zpřístupňuje, a to pomocí tzv rozšířené znakové sady. Navíc, díky této funkci, kterou terminály poskytují, si ncurses nemusí udržovat tabulky znakových sad, aby se rámečky zobrazily korektně napříč různými znakovými sadami.
ncurses definuje 32 konstant ACS_xxx (např ACS_ULCORNER
). K vytištění znaků (včetně speciálních) se používá funkce addch
. Zajímavé je, že hodnotu znaku zadáváme dohromady se stylem v parametru typu chtype
. Tento typ se používá pro uložení hodnoty znaku a stylu (atribut a index barvy) v 8bit ncurses. Speciální znaky se kódují jako normální znak s plus atribut A_ALTCHARSET
. Jelikož hodnota, styl a barva je zakódována v hodnotě typu chtype
, tak všechny tři složky zase můžeme snadno dekódovat. K tomu můžeme použít několik maker nebo připravených masek. Hodnotu znaku (lze to použít pouze pro 1bajtová kódování) získáme prostým vymaskováním hodnotou A_CHARTEXT
. Atributy získáme vymaskováním hodnotou A_ATTRIBUTES
, a posunutý index barevného páru získáme vymaskováním hodnoty A_COLOR
. Originální index barevného páru získáme makrem PAIR_NUMBER
(z hodnoty typu chtype
). Celkově 1 bajt drží hodnotu znaku, 2 bajty styl a 1 bajt index barevného páru.
S podporou více bajtových kódování se ncurses (knihovna ncursesw
) rozšířila o typ cchar_t
. Tento typ je paměťově výrazně náročnější (28 bajtů), jedná se o strukturu, která obsahuje 4 bajty na atributy, pole 5 widecharů, a 4bajtový index barevného páru. Pro kompozici a dekompozici hodnoty typu cchar_t
se používají funkce setcchar
a getcchar
.
Rozšířenou znakovou sadu má smysl používat pouze tehdy, pokud chcete kreslit rámečky, a máte uživatele, kteří ještě používají 8 bitová kódování. Jinak je jednodušší použít znaky z unicode a na rozšířenou znakovou sadu zapomenout.
Zobrazení žluté
Už od dob Turbo Pascalu mám zafixováno, že nej ergonomičtější kombinace barev je žlutá na modré. Samozřejmě, že jsem chtěl, aby se v pspg záhlaví sloupců a řádků také zobrazovalo ve žluté barvě na modrém pozadí. V ncurses je žlutá barva samozřejmě podporovaná. K mému překvapení můj terminál (klasický gnome terminál) místo žluté zobrazoval hnědou. Abych získal skutečně žlutého textu, tak jsem musel použít atribut BOLD
( v dokumentaci se u tohoto atributu píše “tučné nebo jasné”). U těch nejstarších terminálů nebylo možné používat tučný font, a tak se zvýrazňovalo použitím světlejší barvy. To už samozřejmě desítky let nedává smysl, ale zachovávalo se to z důvodu zpětné kompatibility.
3–4 roky zpátky došlo ke změně konsensu, a terminály začaly používat atribut BOLD
ve smyslu tučného písma. Původní chování si lze v některých terminálech vynutit nastavením volby “zobrazit tučný text v jasných barvách”. Tato změna má smysl – jinou cestou se k tučnému písmu nedostanu, pro barvy jsou alternativní cesty. Otázkou je, jak pracovat s barvami, aby co nejvíc uživatelů vidělo barvy korektní. Dost uživatelů používá hodně staré terminálové aplikace a i staré verze ncurses.
Jelikož už většina terminálů umí minimálně 16 barev (lze ověřit v proměnné ncurses COLORS
), tak lze (a dnes už je to nutnost) použít jednoduchý trik. V případě, že chcete světlý odstín barvy, tak místo atributu BOLD
přičtěte k hodnotě barvy 8. V ncurses se barvy označují celočíselným indexem (0..7 je vyhrazeno pro základní barvy, 8–15 pro světlejší odstíny barev). Tento způsob nemusí fungovat u starších terminálů (v tmuxu pokud v $TERM
byla hodnota “screen”). Nový způsob práce s barvami je jednodušší a pohodlnější. Pokud jsem chtěl dříve žluté pozadí, tak jsem musel použít kombinaci atributů BOLD
a REVERSE
, a to už bylo docela nepřehledné. Pokud by terminál podporoval pouze 8 barev, pak je jediná možnost nahodit BOLD
a doufat, že to zafunguje. Na tom, že musím přičítat magickou konstantu, je vidět, že ncurses režim 256barev ještě úplně podchycený nemá, a že tento způsob nastavení žluté není úplně systémový. Ale funguje perfektně.
#include <ncurses.h> #define YELLOW_ON_BLUE 10 #define BLACK_ON_LWHITE 11 int main() { initscr(); start_color(); clear(); init_pair(YELLOW_ON_BLUE, COLOR_YELLOW + 8, COLOR_BLUE); attron(COLOR_PAIR(YELLOW_ON_BLUE)); mvaddstr(10, 10, "Ahoj"); attroff(COLOR_PAIR(YELLOW_ON_BLUE)); /* cerna na jasne bilem pozadi tucne */ init_pair(BLACK_ON_LWHITE, COLOR_BLACK, COLOR_WHITE + 8); attron(COLOR_PAIR(BLACK_ON_LWHITE) | A_BOLD); mvaddstr(11, 10, "Ahoj"); attroff(COLOR_PAIR(BLACK_ON_LWHITE) | A_BOLD); getch(); endwin(); }
Všimněte si – v ncurses definujeme barevné kombinace, které pak používáme při nastavení atributů zobrazovaných textů.
V předchozím příkladu je drobnost, která možná měla pomáhat programátorům, já si z ní ale málem trhal vlasy. Všude v dokumentaci se píše, že se výstup zobrazí až po refreshnutí okna (volání funkcí refresh
nebo wrefresh
). V příkladu, ale žádnou funkci pro refresh nevolám, a výstup se mi zobrazuje korektně. V manuálových stránkách je drobná zmínka, že funkce vstupu si vynutí (implicitně) refresh standardního obrazovky (okno stdscr
) nebo vybraného okna (pokud používáte wgetch
).
Výchozí barvy terminálu
Když se v ncurses zapnou barvy, tak se nastaví jako výchozí barva pozadí černá. Někdy by se vám mohlo hodit zachovat barvu pozadí, kterou má uživatel nastavenou. K té barvě se ale nedostanete. V ncurses vždy pracujete s páry barev, takže tady je problém. Řešení je jednoduché (pokud nepoužíváte hodně starou verzi ncurses). ncurses jako výchozí barevný pár (color pair) používá pár s číslem nula (a ve výchozím nastavení je to bílá na černé). Tuto kombinaci barev lze předdefinovat funkcí assume_default_colors
. Pokud se jako index barvy použije hodnota -1
, tak je použita výchozí barva terminálu (aniž by bylo nutné znát tuto barvu (využívá se standard SGR48, SGR49)). Alternativou k této funkci je funkce use_default_colors
, což je ekvivalent assume_default_colors(-1, -1)
:
#include <ncurses.h> int main() { initscr(); start_color(); use_default_colors(); /* vychozi barva pisma, vychozi barva pozadi */ init_pair(1, -1, -1); /* jasne bila na cernem pozadi */ init_pair(2, COLOR_WHITE + 8, COLOR_BLACK); clear(); attron(COLOR_PAIR(1)); mvaddstr(10, 10, "Ahoj"); attroff(COLOR_PAIR(1)); attron(COLOR_PAIR(2)); mvaddstr(11, 10, "Ahoj"); attroff(COLOR_PAIR(2)); getch(); endwin(); }
Používání barev
Původní terminály byly černobílé s několika odstíny šedi. Potom se přidalo 8barev s jedním (případně dvěma) úrovni jasu. Později 256 barev. Posledních 10–15 let už terminály umí truecolor barvy (cca 16mil barev). Horní číslo indexu barev vrací proměnná COLORS
. U mne má hodnotu 256, což znamená, že prvních 16 barev jsou základní barvy (nebo také 8 základních a 8 odstínů) (0–15), a pak 216 barev z připravené palety (16–231), a 24 odstínů šedi (232–255). Pro prvních 8 barev existují pojmenované konstanty (např. COLOR_BLACK
, COLOR_RED
, … ). Pro další barvy žádné konstanty nejsou.
Pokud chci pracovat s truecolors barvami, musím si je nejdřív vytvořit funkcí init_color
. Barva se definuje pomocí RGB. Barevná složka má rozsah 0..1000. Katalogy barev většinou používají rozsah 0..255, takže hodnoty z katalogu se musí přepočítat. Podle dokumentace by mělo být možné si vytvořit 37K barev. Narazil jsem mnohem dřív. Podle zdrojového kódu, index nově vytvořené barvy musí být menší než je proměnná COLORS
, a v podstatě to znamená, že si přepisujete základní paletu 256 barev. V rozšíření ncurses je ještě definovaná funkce init_extended_color
, která by měla umožňovat pracovat s větším rozsahem, ale v ncurses je to alias na init_color
, takže si moc nepomohu. Jestli tomu správně rozumím, tak v aktuálních nurses můžeme používat max 256 barev z palety 16M barev.
Reset do původního nastavení barev se provede dalším voláním start_color
:
Aby se mi lépe pracovalo s mnou definovanými barvami, přidal jsem si do aplikace tabulku ColorCache
, a funkci color_index_rgb
, která mi vrátí index barvy. Pokud barva pro dané rgb neexistuje, tak tuto barvu přidá do mé color cache a zaregistruje ji v ncurses:
static short color_index_rgb(unsigned int rgb) { short r; short g; short b; int i; for (i = 0; i <nColorCache; i++) { if (ColorCache[i].rgb == rgb) return ColorCache[i].color; } /* rgb is not in cache, new registration is necessary */ if (ncurses_color_index >= 255) return -1; ColorCache[nColorCache].color = ncurses_color_index++; ColorCache[nColorCache].rgb = rgb; r = ((rgb >> 16) & 0xff) / 255.0 * 1000.0; g = ((rgb >> 8) & 0xff) / 255.0 * 1000.0; b = ((rgb) & 0xff) / 255.0 * 1000.0; init_color(ColorCache[nColorCache].color, r, g, b); return ColorCache[nColorCache++].color; } … /* reset color cache */ start_color(); nColorCache = 0; ncurses_color_index = 16; … init_pair(1, color_index_rgb(0xd7d6af), color_index_rgb(0xffffd7)); init_pair(2, color_index_rgb(0x262626), color_index_rgb(0xffffd7));
Když ale chceme překrývající okna – extenze panel
Pokud se okna na obrazovce překrývají, tak je nutné je nutné je refreshovat ve správném pořadí, a je nutné si vynutit provedení refreshe (pokud okno nebylo změněno, tak volání funkce wrefresh
nemá žádný efekt). Systémovým řešením je použití komponenty PANEL
. Panel se vždy vztahuje k jednomu konkrétnímu oknu, a udržuje si informaci o hloubce a viditelnosti okna.
Panel (s oknem) můžeme zviditelnit (show_panel
), zneviditelnit (hide_panel
), přesunout (move_panel
). Panel můžeme přesunout úplně nahoru (top_panel
) nebo úplně dospod (bottom_panel
). Příkazupdate_panels
zavoláwnoutrefresh
asociovaných oken ve správném pořadí. Aby vše fungovalo, tak samozřejmě uživatel nesmí volat ručněwrefresh
nebo wnoutrefresh
. Na závěr se zavolá funkce doupdate
, která zajistí vygenerování změnové sekvence a její odeslání souboru terminálu.
#include <ncurses.h> #include <panel.h> #define YELLOW_ON_BLUE 10 #define YELLOW_ON_RED 11 #define BLACK_ON_LWHITE 12 int main() { WINDOW *win1, *win2; PANEL *pan1, *pan2; int c; initscr(); /* nezobrazuj znaky po stisku klavesy */ noecho(); /* priprav barevnou paletu */ start_color(); /* priprav vlastni barevne pary */ init_pair(YELLOW_ON_BLUE, COLOR_YELLOW + 8, COLOR_BLUE); init_pair(YELLOW_ON_RED, COLOR_YELLOW + 8, COLOR_RED); init_pair(BLACK_ON_LWHITE, COLOR_BLACK, COLOR_WHITE + 8); /* nastav pozadi - znakem a barvou */ wbkgd(stdscr, ACS_CKBOARD | COLOR_PAIR(BLACK_ON_LWHITE)); wnoutrefresh(stdscr); win1 = newwin(15, 15, 5, 5); win2 = newwin(15, 15, 7, 7); pan1 = new_panel(win1); pan2 = new_panel(win2); /* nastav pozadi */ wbkgd(win1, COLOR_PAIR(YELLOW_ON_BLUE)); wbkgd(win2, COLOR_PAIR(YELLOW_ON_RED)); wattron(win1, COLOR_PAIR(YELLOW_ON_BLUE)); wattron(win2, COLOR_PAIR(YELLOW_ON_RED)); /* ramecky kolem oken */ box(win1, 0, 0); box(win2, 0, 0); do { PANEL *bottomp; /* zkopiruj obsah panelu do newscr */ update_panels(); /* zobraz newscr */ doupdate(); /* cekej na stisk klavesy */ c = getch(); /* okno zespod presun navrch */ bottomp = panel_above(NULL); top_panel(bottomp); } while (c != 'q'); endwin(); }
Kód se musí linkovat s knihovnou panel
– gcc -lncursesw -lpanel test.c
.
Zkopírování dat do clipboardu (práce s clipboardem)
Docela dost dlouho jsem hledal způsob, jak z terminálové aplikace uložit určitý obsah do clipboardu. Existuje escape sekvence, která upozorňuje aplikaci na masivní vstup (bracketed paste mode), ale nic, co by fungovalo v opačném směru (Tedy ono to existuje pod názvem OSC52. Tyto sekvence ale nejsou implementované v Gnome Terminálu, a kdo ví kdy budou a jestli vůbec). Takže přes escape sekvence se na to jít nedá (byly by nejlepší, pak bylo by možné použít clipboard vzdáleně).
Problém je v tom, že v základním posixovém API (a potažmo v Linuxu) nemáme žádné funkce pro práci s clipboardem. O implementaci clipboardu se stará X11 Server (knihovna XLib). Některé terminálové aplikace umí volat XLib, ale mně se do takového řešení moc nechtělo. Jednak mám už studentských let hrůzu z X11 API, dále závislost na XLib není nic, co bych chtěl do pspg
přidávat. Samozřejmě, dalo by se to udělat skrze dynamická volání, ale to je pořád dost nepříjemná práce. Navíc dnes XLib dožívá, a je nahrazována Waylandem.
Nakonec jsem našel asi ne úplně hezké řešení (přiznám se, že mimo shell nerad volám aplikace z aplikace). Používám jednoúčelové aplikace wl-clipboard
(Wayland), xclip
(XWindows) a pbcopy
(MacOS). Napřed detekuji, kterou z těchto aplikací dokáži spustit. Tu pak spustím a přes rouru jí pošlu obsah, který chci poslat do clipboardu. Funguje to relativně dobře, a jsem schopný naimportovat tabulku bez dodatečných akcí do Libre Office Calcu. Musím ale konstatovat, že s možnostmi, které jsou v MS Win (pro práci s clipboardem), je to hodně osekané. Na druhou stranu MS Windows byl primárně desktopový systém, a interoperabilita skrze clipboard byla klíčovou vlastností Windows a zejména MS Office.
Ale aby to nebylo tak jednoduché, tak neexistuje POSIX API, které by umožňovalo obousměrnou komunikaci s aplikací, kterou voláte ze své aplikace. Klasická funkce popen
je dost omezená – u některých chyb získáte result code, ale nikdy nezískáte text chyby (na BSD ano, ale ne na Linuxu). Je nutné si napsat vlastní náhradu funkce popen
, kde používáte dvě (tři) roury pro stdin
, stdout
(případně stderr
). To je trochu práce navíc, a riziko, že na některých platformách váš kód nebude funkční. Uživatelé ale žádné chyby nereportují, tak to asi funguje.
Jak vykreslit stín (přečtení a změna atributu pozice)?
Když jsem si psal podporu CUA menu (knihovna ncurses-st-menu), tak jsem samozřejmě chtěl, aby se pod menu vykresloval stín. Totiž všechny pěkně vypadající aplikace, které jsem znal z DOSu, měly kolem rámečků stín (v dobách DOSu to byla známka luxusu). V DOSu je to docela jednoduché. Vykreslíte menu, a tam kde chcete mít stín, změníte atribut znaku. Kdybych psal pspg
komerčně, a už v té době věděl, co je s tím práce, tak bych se asi s nějakým stínem nepatlal. Bylo to docela dost práce a přemýšlení (na takovou blbost).
Předně menu stoprocentně bude překrývat jiná okna, což pro zobrazení bez artefaktů vyžaduje použití panelů (viz výše). Panely zapouzdřují okna. Samozřejmě, že není dovoleno si sáhnout na obsah mimo okno, a tudíž jej ani nelze změnit. Takže musím pracovat s více okny (potažmo panely). Nejspodnější okno musí být roztažené přez celou obrazovku, a bude sloužit jako plocha na kterou se bude zobrazovat stín. To, že pracujete s dvěma okny mírně komplikuje práci. Menu je ve vlastním okně (ve vlastním souřadném systému), při zobrazení stínu musíme souřadnice přepočítávat do souřadnic okna, které slouží jako plocha.
Nejvíc práce jsem ale měl s tím, jak změnit barvu a styl znaku na konkrétní pozici. Věděl jsem, že ncurses pro zobrazení používá buffery, takže mi bylo jasné, že ten obsah tam někde je. Když ovšem nevíte, jak se přesně jmenuje funkce, kterou hledáte, tak vám Google moc nepomůže. V tomhle konkrétním případě mne Google navigoval na funkce, kterými lze část obrazovky uložit do souboru, což je pro zobrazení stínů nepoužitelné.
Jako první jsem dohledal jsem funkci mvwin_wch
, která vrací hodnotu typu cchar_t
. Pak pomocí funkce getcchar
jsem cchar
rozdělil na jednotlivé složky – široký znak, atributy a index páru barev. Pak jsem pomocí funkce setcchar
opět sestavil hodnotu typu cchar_t
(obsahuje veškeré informace o tištěném znaku) a funkcí mvwadd_wch
jsem umístil tuto hodnotu na určenou pozici. Docela jsem s tím zápasil – při resetu atributů jsem omylem resetoval i atribut A_ALTCHARSET
, kterým se vynucuje použití alternativní znakové sady.
Tím, že jsem resetoval A_ALTCHARSET
, jsem si ve stínu rozbíjel rámečky (a některé speciální znaky). Tento atribut se musí zachovat.
cchar_t cch; wchar_t wch[CCHARW_MAX]; attr_t attr; short int cp; /* ziskej hodnotu z pozice, a dekomponuj ji */ mvwin_wch(menu->shadow_window, i, j, &cch); getcchar(&cch, wch, &attr, &cp, NULL); /* * When original attributte holds A_ALTCHARSET bit, * then updated attributte have to hold this bit too, * else ACS chars will be broken. */ setcchar(&cch, wch, shadow_attr | (attr & A_ALTCHARSET), config->menu_shadow_cpn, NULL); mvwadd_wch(menu->shadow_window, i, j, &cch);
Později jsem zjistil, že výše uvedený kód lze zjednodušit použitím funkcí, které pracují s typem chtype
:
if (mvwinch(menu->shadow_window, i, j) & A_ALTCHARSET) mvwchgat(menu->shadow_window, i, j, 1, shadow_attr | A_ALTCHARSET, config->menu_shadow_cpn, NULL); else mvwchgat(menu->shadow_window, i, j, 1, shadow_attr, config->menu_shadow_cpn,
První řešení by bylo nutné, kdybych potřeboval číst z obrazovky široké znaky, což pro vykreslení stínu nepotřebuji. A jelikož měním jen atributy, tak je pro mne výhodnější použití staršího API, které nevyžaduje wide char verzi ncurses. Na těchto dvou různých API vidíte hlavní dnešní problém ncurses. Kvůli zpětné kompatibilitě (případně kvůli kompatibilitě s proprietárními curses) je v ncurses na můj vkus příliš vzájemně si podobných funkcí. Co vím, tak v ncurses nikdy neproběhla žádná revize, žádná drsnější modernizace a je to znát. Bohužel nikdo nebude riskovat problémy s kompatibilitou, a předpokládám, že veškeré investice do ncurses jdou kvůli starým dodnes provozovaným kritickým aplikacím.
Jak zachovat obsah obrazovky po ukončení aplikace?
Pager less
má velice praktický přepínač --no-init
nebo -X
. Pokud se zapne, tak si less
pro svůj běh nebude aktivovat alternativní screen. To má smysl hlavně ve chvíli jeho ukončení. Na obrazovce nám totiž zůstane prohlížený obsah. Tuhle funkci jsem v pspg
určitě chtěl. Když si v zobrazovaných datech najdu něco zajímavého, tak nechci, aby mi tato informace, když ukončím pager, zmizela z obrazovky. Otázkou je, jak toho docílit. Nepřišel jsem na způsob, jak zachovat obsah alternativního screenu. Na internetu jsem dohledal, že je možné “hacknout” databázi escape sekvencí, a nahradit sekvenci, která přepíná screeny prázdným řetězcem. Tím se docílí toho, mi terminál zůstane v primárním screenu, a pak vše, co jsem zobrazil v ncursis aplikaci, tam zůstane i po ukončení aplikace. Nakonec jsem touto cestou nešel, protože podpora myši, tak jak jsem jí chtěl mít v pspg
, funguje, jen pokud je aktivní alternativní screen (i když je možné, že jsem tam dělal něco špatně, a nějakou konfigurací ncurses bych to přesvědčil).
Skončil jsem nakonec u velice jednoduchého řešení, které funguje na jedničku. Po ukončení ncurses, a pokud je to vyžadováno, tak viditelný obsah ještě jednoduše jednou vytisknu na stdout
s pomocí escape sekvencí. Je fakt, že bych to mohl vylepšit použitím termcapu, ale pro tento účel stačí základní escape sekvence, a ty jsou podporované všude.
Přesun okna, změna velikosti okna, změna velikosti terminálu
V ncurses jsou instrukce pro přesun okna. Pokud se okno vejde celé na obrazovku, tak mi operace přesunu okna fungovala dobře. Jakmile ale okno bylo částečně mimo obrazovku, tak se při přesunu okna zároveň někdy měnila i jeho velikost, a to tak že hranice okna zůstala zafixovaná s okrajem terminálu.
Zde je potřeba zmínit jednu nectnost ncurses. Je tam implementována heuristika, která bohužel nejde vypnout. Mohu si vyrobit okno, které “přesahuje” obrazovku terminálu. Pokud měním velikost terminálu, tak se testuje, jestli okno nepřesahuje v nějaké dimenzi obrazovku terminálu více než o n znaků (n je dynamické číslo něco mezi 1 a 10). Pokud ano, tak se okno ořízne, v té dimenzi, na velikost terminálu, a nahodí se tam nějaký interní flag, který způsobí, že toto okno už je napořád zafixované na okraj terminálu. Takže při zmenšení terminálu můžete ztratit původní velikost okna, a při zvětšení terminálu se okno může zvětšit výrazně nad původní meze. Výše popsaná heuristika se aktivovala, ať už jsem posouval okno mimo terminál, nebo jsem provedl resize terminálu mimo existující okno. Vzhledem k tomu, že výstup v ncurses se neořezává, ale zalamuje, tak vám špatná velikost okna totálně deformuje výstup. To je na zabití. Je možné, že to je udělátko, jak naučit staré aplikace resize terminálu. Možná je to side efekt implementace slk labels (soft-function-key labels). Možná je to jen přehlédnutá chyba, která se dnes už špatně napravuje. Tohle mi při programování dělalo asi největší problémy, a při zobrazování jsem u některých oken musel neustále kontrolovat jejich velikost s hodnotou, kterou jsem měl uloženou mimo, a případně vracet velikost okna na mnou požadovaný rozměr funkcí wresize
.
Navíc jsem zjistil, že při přesunu okna se nemusí korektně přesunout okna, která jsou na přesouvané okno navázaná, resp. jejich některé interní meta data zůstanou nezměněná. Což mi pak rozbíjelo detekci jestli klick myši byl nebo nebyl v okně. V pspg
jsem našel workaround, dnes bych se ale přesouvání oken vyhnul. Místo toho bych si vytvořil nové okno, a staré smazal. Režie toho je plus mínus nic.
Není to špatná knihovna
Ohledně ncurses mám mírně rozporuplné pocity – ve výsledku ale spíš kladné. Aplikace s jednoduší vizuální podobou jako je vi
, top
, htop
emacs
se nad touto knihovnou napíší hrozně jednoduše, a fungují napříč ohromným spektrem platforem. Cokoliv složitějšího s komplexnější vizuální podobou (ne každý ocení strohost a efektivitu vim
) znamená napsat si vlastní vrstvu (tak jako jsem si napsal ncurses-st-menu), která implementuje základní komponenty a obaluje zpracování událostí. Pro Cčko jsem nic nenašel, pro C++ a další jazyky existuje několik kvalitních frameworků.
Trochu mne mrzí, že se nedá nějak jednoduše vykrást midnight commander
nebo dialog
. ncurses je také hrozně letitá knihovna trochu nabobtnalá z důvodů podpory POSIXu a API curses z komerčních UNIXů. Na druhou stranu, když člověk pochopí systém ncurses, architekturu (to opravdu důležité je popsané v jednom článku ve dvou odstavcích), tak se v ncurses programuje docela dobře.
API je jednoduché, a je v něm minimum chyb. Reportované chyby se opravují a knihovna nebo dokumentace se mírně upravují. Přenositelnost je na Cčko luxusní (v kódu mám minimum ifdef
ů). Není to špatná knihovna. Chybí mi tam vrstva nad tím. Nebo možná několik vrstev nad tím. V ideálním případě bych si představoval formuláře z FoxPro 2.0. Ale to už není problém ncurses.