Ncurses: Porovnání verzí
(založena nová stránka s textem „<h1>Psaní aplikací pro terminál (s ncurses)</h1> Poslední 4 roky jsem hodně času věnoval vývoji [https://github.com/okbob/pspg pspg]. A nyní, kdy…“) |
(→Závěr) |
||
(Není zobrazeno 12 mezilehlých verzí od stejného uživatele.) | |||
Řádek 1: | Řádek 1: | ||
[[category:Články]] | |||
= Ncurses = | |||
Autor: [[Pavel Stěhule|Pavel Stěhule]], 7.4.2021 | |||
Poslední 4 roky jsem hodně času věnoval vývoji [https://github.com/okbob/pspg pspg]. A nyní, když je <code>pspg</code> dokončen, tak bych zde | Poslední 4 roky jsem hodně času věnoval vývoji [https://github.com/okbob/pspg pspg]. A nyní, když je <code>pspg</code> dokončen, tak bych zde | ||
Řádek 5: | Řádek 7: | ||
bez které bych aplikaci jako je <code>pspg</code> nemohl napsat (asi mohl, ale dalo by mi to výrazně víc práce). | bez které bych aplikaci jako je <code>pspg</code> nemohl napsat (asi mohl, ale dalo by mi to výrazně víc práce). | ||
== Úvod == | |||
<i>ncurses</i> je knihovna, která poskytuje základní funkcionalitu pro vývoj terminálových aplikací. I když se jedná | <i>ncurses</i> je knihovna, která poskytuje základní funkcionalitu pro vývoj terminálových aplikací. I když se jedná | ||
o novější inkarnaci knihovny <i>curses</i>, má svoje léta. Historie této knihovny spadá do přelomu 70 a 80 let, a | o novější inkarnaci knihovny <i>curses</i>, má svoje léta. Historie této knihovny spadá do přelomu 70 a 80 let, a | ||
Řádek 16: | Řádek 19: | ||
(kdy už letadlo vypadalo jako letadlo) - rok 1918, se stíhačkou se začátku 2 světové (podívejte se na videa | (kdy už letadlo vypadalo jako letadlo) - rok 1918, se stíhačkou se začátku 2 světové (podívejte se na videa | ||
přípravy Spitfire k letu). | přípravy Spitfire k letu). | ||
Přístup k počítačům s Unixem v 70 a 80 letech zprostředkovávaly terminály. Původně se jednalo o modifikované | Přístup k počítačům s Unixem v 70 a 80 letech zprostředkovávaly terminály. Původně se jednalo o modifikované | ||
Řádek 376: | Řádek 378: | ||
s aplikací. Dodnes terminály (ani ty softwarové) nepodporují [https://cs.wikipedia.org/wiki/Mnohon%C3%A1sobn%C3%BD_buffering double buffering]. Pomocí ncurses je double buffering alespoň | s aplikací. Dodnes terminály (ani ty softwarové) nepodporují [https://cs.wikipedia.org/wiki/Mnohon%C3%A1sobn%C3%BD_buffering double buffering]. Pomocí ncurses je double buffering alespoň | ||
částečně emulován. | částečně emulován. | ||
== Zpracování vstupů == | |||
=== Vstup z tty (inicializace ncurses) === | |||
Ve velké většině případů se knihovna ncurses inicializuje voláním funkce | |||
<code>initscr</code>. V tomto případě se vstup čte ze standardního vstupu <code>stdin</code>, a výstup | |||
zapisuje na standardní výstup <code>stdout</code>. V případech, kdy jsou tyto soubory | |||
asociovány se zařízením typu <code>tty</code> nebo <code>pty</code>, tak vše bude fungovat jak | |||
očekáváme. Pokud si ale budete chtít napsat pager nebo filter, tak tam narazíte. | |||
Standardní vstup bude výstupem předchozího příkazu v [https://www.pslib.cz/milan.kerslager/BASH:_Kolony koloně] a nikoliv soubor | |||
asociovaný s terminálem. | |||
Když jsem začínal s <code>pspg</code>, tak jsem ncurses neměl zažité, a docela dlouho jsem | |||
vymýšlel, jak tento problém vyřešit. První funkční řešení, které jsem dlouho | |||
používal, byla taková opičárna - po načtení vstupních dat jsem použil funkci | |||
<code>reopen</code> nad standardním vstupem a dostupným terminálovým zařízením: | |||
<pre> | |||
if (!isatty(fileno(stdin))) | |||
{ | |||
if (freopen("/dev/tty", "r", stdin) != NULL) | |||
noatty = false; | |||
else if (freopen(ttyname(fileno(stdout)), "r", stdin) != NULL) | |||
noatty = false; | |||
else | |||
{ | |||
/* | |||
* cannot to reopen terminal device. See discussion to issue #35 | |||
* fallback solution - read keys directly from stderr. Just check | |||
* it it is possible. | |||
*/ | |||
if (!isatty(fileno(stderr))) | |||
{ | |||
fprintf(stderr, "missing a access to terminal device\n"); | |||
exit(EXIT_FAILURE); | |||
} | |||
noatty = true; | |||
fclose(stdin); | |||
} | |||
} | |||
</pre> | |||
Výše uvedený kód na spoustě unixů funguje, ale není to nic hezkého. Určitě není | |||
dobrý (a obvyklý) nápad otevírat standardní vstup v běžné aplikaci. To je práce | |||
pro shell. Navíc je to úplně zbytečné. Přímo v ncurses máme jednoduché a | |||
elegantní řešení. Nejdříve si člověk musí uvědomit, že ncurses "jen" čte ze | |||
souboru nebo zapisuje do souboru. Typicky těmi soubory je <code>stdin</code> a <code>stdout</code>, ale | |||
mělo by to fungovat pro všechny soubory. Jen zjistit jak (stačilo si přečíst | |||
manuálovou stránku pro <code>initscr</code>, ale kdo čte dokumentaci). | |||
Fígl je v inicializaci ncurses funkcí <code>newterm</code> namísto <code>initscr</code>: | |||
<pre> | |||
#include <ncurses.h> | |||
#include <stdlib.h> | |||
#include <unistd.h> | |||
int | |||
main() | |||
{ | |||
FILE *f_tty = NULL; | |||
SCREEN *term = NULL; | |||
#ifndef __APPLE__ | |||
f_tty = fopen("/dev/tty", "r+"); | |||
#endif | |||
if (!f_tty) | |||
{ | |||
f_tty = fopen(ttyname(fileno(stdout)), "r"); | |||
if (!f_tty && isatty(fileno(stderr))) | |||
f_tty = stderr; | |||
} | |||
if (!f_tty) | |||
{ | |||
fprintf(stderr, "cannot to open tty device\n"); | |||
exit(1); | |||
} | |||
term = newterm(termname(), stdout, f_tty); | |||
if (!term) | |||
{ | |||
fprintf(stderr, "cannot to initialize ncurses screen\n"); | |||
exit(1); | |||
} | |||
mvprintw(10, 10, "press ENTER"); | |||
getch(); | |||
endwin(); | |||
delscreen(term); | |||
} | |||
</pre> | |||
Bohužel na macOS je implementace <code>/dev/tty</code> polofunkční, což jsem zjistil i já | |||
(nedá se použít ve funkci <code>poll</code>), a nedoporučuje se moc používat (tato chyba je | |||
známá minimálně od roku 2005). | |||
ncurses umožňuje v rámci jedné aplikace vytvoření a používání více terminálů. | |||
Voláním funkce <code>set_term</code> mohu pak volit jeden aktivní. Nenašel jsem žádnou | |||
aplikaci, nad ncurses, která by toto umožňovala. Například <code>emacs</code> podobný režim | |||
podporuje. | |||
=== Ladění, ladící výstup === | |||
Když jsem začínal s ncurses, tak jsem hodně bojoval s laděním aplikace. Jelikož | |||
je standardní výstup <code>stdout</code> stejně jako standardní chybový výstup <code>stderr</code> | |||
směrován na stejné tty zařízení, tak jakýkoliv pokus o tisk na <code>stderr</code> rozhodí | |||
layout aplikace. Nakonec jsem skončil u explicitně vytvořené pojmenované roury, | |||
kterou používám pro ladící výpisy. Pokud spustím <code>pspg</code> v debug režimu, tak si | |||
tuto rouru otevřu a tisknu do ní. Šlo by to ale i jednodušeji - systémověji. | |||
Otevřu si další terminál, a příkazem <code>tty</code> si vypíšu cestu k souboru terminálu. | |||
Poté při spuštění aplikace přesměruji chybový výstup do tohoto souboru: | |||
<pre>sh | |||
[pavel@localhost ~]$ tty | |||
/dev/pts/8 | |||
</pre> | |||
V jiném terminálu: | |||
<pre>sh | |||
mojeapp ... 2>/dev/pts/8 | |||
</pre> | |||
V aplikaci pak mohu používat <code>stderr</code> bez omezení. | |||
=== Vstup znaků (vstup širokých znaků) === | |||
Za nenápadnou funkcí <code>getch</code> se skrývá hodně funkcionality. Tato funkce nevrací | |||
jen kód aktuálně stisknuté klávesy, ale řeší dekódování vstupních escape | |||
sekvencí, řeší ošetření signálu [https://www.root.cz/clanky/terminaly-tajemstvi-zbavene-procesy-a-signaly/ <code>SIGWINCH</code>] (změna velikosti terminálu), a v | |||
kombinaci s nastavením timeoutu (funkce <code>timeout</code>) umožňuje volbu blokujícího | |||
nebo neblokujícího čtení. Dnes už je tato funkce překonaná, protože nepodporuje | |||
vstup širokých znaků (u nás například znaků s diakritikou v UTF). Místo této | |||
funkce by se měla používat funkce <code>get_wch</code>. Ta vyžaduje kompilaci a linkování | |||
vůči wide charové verzi ncurses <code>ncursesw</code>. Pokud nezbytně nutně nepotřebujete | |||
ošetřovat široké znaky, tak můžete použít <code>#ifdef</code>: | |||
Historicky bylo možné nastavit numerickou klávesnici (keypad) do dvou režimů. | |||
V numerickém vracela čísla, v aplikačním pak escape sekvence odpovídající kurzorovým | |||
šipkám. Možná touto funkcionalitou se inspirovali autoři ncurses, když psali funkci | |||
<code>keypad</code>, která mění konfiguraci obsluhy funkčních kláves. Ve výchozím nastavení | |||
<code>keypad(FALSE)</code> se přijatá posloupnost escape znaků <code>^[ [ A</code> posílá aplikaci jako | |||
tři přijaté znaky. Pokud nastavíme <code>keypad(TRUE)</code>, pak zmíněnou escape sekvenci | |||
ncurses rozpozná a nahradí svým kódem <code>KEY_UP</code>. | |||
<pre> | |||
/* | |||
* chceme podporu sirokych znaku, obycejne se | |||
* nastavi autoconfem. Musi byt nastaveno pred | |||
* include ncurses.h | |||
*/ | |||
#define NCURSES_WIDECHAR 1 | |||
#include <ncurses.h> | |||
#include <locale.h> | |||
static bool | |||
_getch(int *c) | |||
{ | |||
#if NCURSES_WIDECHAR > 0 | |||
wint_t ch; | |||
int ret; | |||
ret = get_wch(&ch); | |||
*c = ch; | |||
return ret == KEY_CODE_YES; | |||
#else | |||
/* | |||
* V ASCII verzi nemohlo dojit k prekryti kodu znaku | |||
* a specialnich kodu ncurses (jako KEY_UP, ..). | |||
*/ | |||
*c = getch(); | |||
return true; | |||
#endif | |||
} | |||
int | |||
main() | |||
{ | |||
int eventno = 0; | |||
int c; | |||
bool is_key_code; | |||
setlocale(LC_ALL, ""); | |||
initscr(); | |||
/* necekej na ENTER */ | |||
cbreak(); | |||
/* neviditelny kurzor */ | |||
curs_set(0); | |||
/* neviditelne znaky pri psani */ | |||
noecho(); | |||
/* chci aby hlavni obrazovka skrolovala */ | |||
scrollok(stdscr, TRUE); | |||
/* | |||
* bez aktivniho keypad modu nedochazi k dekodovani escape sekvenci, | |||
* kurzorove klavesy, funkcni klavesy, ... | |||
*/ | |||
keypad(stdscr, TRUE); | |||
move(0,0); | |||
is_key_code = _getch(&c); | |||
while (c != 'q') | |||
{ | |||
if (is_key_code) | |||
printw("%4d: key: %s\n", ++eventno, keyname(c)); | |||
else | |||
printw("%4d: c: %lc (%d)\n", ++eventno, c, c); | |||
refresh(); | |||
is_key_code = _getch(&c); | |||
} | |||
endwin(); | |||
} | |||
</pre> | |||
=== Funkční klávesy === | |||
Práce s funkčními klávesami je jednoduchá - pomocí makra <code>KEY_F</code> můžeme jednoduše | |||
vygenerovat kódy pro jednotlivé funkční klávesy. Pokud při stisku funkční klávesy | |||
stiskneme <kbd>SHIFT</kbd>, tak pracujeme jakoby s druhou řadou funkčních kláves | |||
<kbd>F13</kbd> - <kbd>F24</kbd>. Tady pak dochází k jakési schíze - např. pro | |||
kombinaci kláves, kterou označujeme jako <kbd>SHIFT</kbd>+<kbd>F3</kbd>, | |||
používáme kód <code>KEY_F(15)</code>. | |||
Přiznám se, že netuším, jestli dále popisované chování je standard nebo pouze | |||
specifikum <code>mc</code> midnight commandera. Stisk funkčních kláves může být emulován | |||
stiskem klávesy <kbd>ALT</kbd> a číslo nebo posloupností kláves <kbd>ESCAPE</kbd> | |||
a číslo. Toto chování ncurses neimplementuje, ale je možné je implementovat | |||
aplikačně (je to například implementované v <code>pspg</code>, protože mi to přijde jako | |||
šikovný nápad). | |||
<pre> | |||
#include <ncurses.h> | |||
int | |||
main() | |||
{ | |||
int eventno = 0; | |||
int c; | |||
bool alt = false; | |||
initscr(); | |||
cbreak(); | |||
curs_set(0); | |||
noecho(); | |||
scrollok(stdscr, TRUE); | |||
keypad(stdscr, TRUE); | |||
move(0,0); | |||
c = getch(); | |||
while (c != 'q') | |||
{ | |||
switch (c) | |||
{ | |||
case 27: /* ESCAPE */ | |||
if (alt) | |||
{ | |||
alt = false | |||
addstr("zadost o ESCAPE\n"); | |||
} | |||
else | |||
alt = true; | |||
break; | |||
case '2': | |||
if (alt) | |||
addstr("stisknuta klaveska F2\n"); | |||
break; | |||
case KEY_F(2): | |||
addstr("stisknuta klavesa F2\n"); | |||
break; | |||
case KEY_F(14): | |||
addstr("stisknuta klavesa SHIFT+F2\n"); | |||
break; | |||
default: | |||
addstr("stisknuto neco jineho\n"); | |||
} | |||
if (c != 27) | |||
alt = false; | |||
refresh(); | |||
c = getch(); | |||
} | |||
endwin(); | |||
} | |||
</pre> | |||
=== Speciální klávesy === | |||
Pro základní speciální klávesy má ncurses definované speciální kódy jako např. | |||
<code>KEY_UP</code>, <code>KEY_DOWN</code> atd. Pokud chceme detekovat stisk těchto kláves zároveň se | |||
stisknutou klávesou <kbd>CTRL</kbd> případně ještě se <kbd>SHIFT</kbd>em, tak už | |||
je nutné dynamicky získat kód příslušné kombinace kláves. Nejdříve si musíme | |||
sestavit [https://man7.org/linux/man-pages/man5/user_caps.5.html identifikátor kombinace kláves]. Například | |||
<code>kEND6</code> je kombinace kláves <kbd>CONTROL</kbd>+<kbd>SHIFT</kbd>+<kbd>END</kbd>. K | |||
tomuto identifikátoru si můžeme vytáhnout z databáze terminfo odpovídající | |||
sekvenci, a následně si voláním funkce <code>key_defined</code> můžeme zjistit kód | |||
definovaný pro tuto sekvenci znaků. Jelikož zmíněné funkce patří k rozšíření | |||
ncurses, je praktické volání těchto funkcí vložit do <code>#ifdef</code> a počítat s | |||
náhradním řešením, když tyto funkce nebudou dostupné. Na rozšířených platformách | |||
jsou tyto kódy stejné. | |||
<pre> | |||
static int | |||
get_code(const char *capname, int fallback) | |||
{ | |||
#ifdef NCURSES_EXT_FUNCS | |||
char *s; | |||
int result; | |||
s = tigetstr((NCURSES_CONST char *) capname); | |||
if (s == NULL || s == (char *) -1) | |||
return fallback; | |||
result = key_defined(s); | |||
return result > 0 ? result : fallback; | |||
#else | |||
return fallback; | |||
#endif | |||
} | |||
/* | |||
* Set a value of CTRL_HOME and CTRL_END key codes. These codes | |||
* can be redefined on some plaforms. | |||
*/ | |||
void | |||
initialize_special_keycodes() | |||
{ | |||
#ifdef NCURSES_EXT_FUNCS | |||
use_extended_names(TRUE); | |||
#endif | |||
CTRL_HOME = get_code("kHOM5", 538); | |||
CTRL_END = get_code("kEND5", 533); | |||
CTRL_SHIFT_HOME = get_code("kHOM6", 537); | |||
CTRL_SHIFT_END = get_code("kEND6", 532); | |||
} | |||
</pre> | |||
=== ALT === | |||
Kombinace kláves <kbd>CONTROL</kbd> + znak, je mapována do intervalu 1..27. Takže, když chci | |||
detekovat stisk <kbd>CTRL</kbd> + <kbd>O</kbd>, tak se podívám do ASCII tabulky, kde dohledám, že tato | |||
kombinace kláves má kód 15. S <kbd>ALT</kbd>em to funguje úplně jinak. Při stisku kláves <kbd>ALT</kbd> | |||
+ <kbd>O</kbd>, tak ncurses vygenerují dva kódy: ESCAPE (27) a O. V aplikačním kódu je nutné | |||
tuto dvojici detekovat. V <code>pspg</code> volám <code>get_wch</code> v samostatném cyklu, který mi | |||
umožní opakované volání, pokud získaná událost není validní nebo je ESCAPE. Tím | |||
jak je to navržené a implementované, tak místo klávesy <kbd>ALT</kbd> můžeme používat | |||
klávesu <kbd>ESCAPE</kbd> (jen jde o posloupnost kláves, nikoliv o současné stisknutí, tj | |||
<kbd>ALT</kbd> + <kbd>O</kbd> = <kbd>ESCAPE</kbd>, <kbd>O</kbd>). ESCAPE, ve smyslu přerušení nějaké operace (klasický MSDOS | |||
ESCAPE), je definován dvojím stisknutím klávesy <kbd>ESCAPE</kbd> (prvním stiskem, jak už | |||
bylo řečeno, se přepínám do alternativní klávesnice). Kombinace kláves <kbd>CTRL</kbd>+<kbd>ALT</kbd>+<kbd>O</kbd> | |||
vygeneruje kódy ESCAPE a 15 (^O). | |||
<kbd>ALT</kbd>, potažmo <kbd>ESCAPE</kbd> se také může používat jako náhrada funkčních kláves <kbd>F1</kbd> až | |||
<kbd>F10</kbd>. To se hodí, když vám některé funkční klávesy "sežere" terminál. Například | |||
<kbd>F10</kbd>aktivuji menu v Gnome Terminálu, a tudíž stisk <kbd>F10</kbd> se k aplikaci, která mi | |||
běží v terminálu nedostane. Pokud to aplikace implementuje (je to aplikační | |||
záležitost, která musí obsluhovat <kbd>ALT</kbd>+<kbd>0</kbd>), tak mohu zkusit alternativu <kbd>ALT</kbd>+<kbd>0</kbd> | |||
nebo ekvivalent <kbd>ESCAPE</kbd>, <kbd>0</kbd>. | |||
=== Použití knihovny readline === | |||
Občas může být potřeba editovat řetězec. V ncurses máme funkci <code>getstr</code>. Určitě | |||
tím uživatelům umožníte editaci, ale asi je neoslníte. Editace je dost | |||
primitivní. Nefungují kurzorové klávesy, mazaní jde jedině zprava backspacem. | |||
Když to porovnáte z možnostmi, které máme dnes v bashi, tak to zamrzí. A rovnou | |||
vás napadne, proč nepoužít tu samou knihovnu <code>readline</code>. Při podrobnějším | |||
zkoumání se ukáže, že to není až tak jednoduché. Knihovna <code>readline</code> je navržená | |||
pro použití v REPL aplikacích, nikoliv v celoobrazovkových aplikacích. Nicméně | |||
obsahuje funkce, které umožňují používat tuto knihovnu, aniž by měla přímý | |||
přístup ke vstupu a výstupu (alternative callback interface). Vůbec netuším, proč | |||
je toto API tak zbytečně složitě navržené (ale je fakt, že o této knihovně skoro | |||
nic nevím): | |||
<pre> | |||
/* For wcwidth() */ | |||
#define _XOPEN_SOURCE 700 | |||
#include <locale.h> | |||
#include <ncurses.h> | |||
#include <readline/readline.h> | |||
#include <stdlib.h> | |||
#include <string.h> | |||
#include <wchar.h> | |||
static int readline_proxy; | |||
static bool readline_proxy_is_valid = false; | |||
char *string = NULL; | |||
/* | |||
* Zkopiruje znak z proxy do readline | |||
*/ | |||
static int | |||
readline_getc(FILE *dummy) | |||
{ | |||
readline_proxy_is_valid = false; | |||
return readline_proxy; | |||
} | |||
static int | |||
readline_input_avail(void) | |||
{ | |||
return readline_proxy_is_valid; | |||
} | |||
/* | |||
* Touto funkci readline predava vysledek editace | |||
*/ | |||
static void | |||
readline_callback(char *line) | |||
{ | |||
free(string); | |||
string = NULL; | |||
if (line) | |||
string = strdup(line); | |||
} | |||
/* | |||
* Chceme mit zobrazeni ve vlastni rezii | |||
*/ | |||
static void | |||
readline_redisplay() | |||
{ | |||
/* do nothing here */ | |||
} | |||
/* | |||
* Vrati (zobrazovaci) sirku retezce. | |||
*/ | |||
static size_t | |||
strnwidth(const char *s, size_t n) | |||
{ | |||
mbstate_t shift_state; | |||
wchar_t wc; | |||
size_t wc_len; | |||
size_t width = 0; | |||
size_t ch_width; | |||
memset(&shift_state, '\0', sizeof shift_state); | |||
for (size_t i = 0; i < n; i += wc_len) | |||
{ | |||
wc_len = mbrtowc(&wc, s + i, MB_CUR_MAX, &shift_state); | |||
if (!wc_len) | |||
return width; | |||
if ((wc_len == (size_t) -1) || (wc_len == (size_t) -2)) | |||
return width + strnlen(s + i, n - i); | |||
if (iswcntrl(wc)) | |||
width += 2; | |||
else if ((ch_width = wcwidth(wc)) > 0) | |||
width += ch_width; | |||
} | |||
return width; | |||
} | |||
int | |||
main() | |||
{ | |||
int c = 0; | |||
bool alt = false; | |||
setlocale(LC_ALL, ""); | |||
initscr(); | |||
cbreak(); | |||
noecho(); | |||
/* ENTER neni \n */ | |||
nonl(); | |||
/* | |||
* readline vyzaduje neprekodovany vstup tj | |||
* vypnuty keypad a cteni vice bajtovych | |||
* znaku po bajtech (emuluje se binarni | |||
* cteni ze souboru v aktualnim kodovani) | |||
*/ | |||
keypad(stdscr, FALSE); | |||
/* | |||
* Instalace hooku - pokud se pouzije rl_getc_function, | |||
* tak by se mel VZDY pouzit i rl_input_available_hook. | |||
*/ | |||
rl_getc_function = readline_getc; | |||
rl_input_available_hook = readline_input_avail; | |||
rl_redisplay_function = readline_redisplay; | |||
/* | |||
* Nechceme obsluhu signalu v readline, a nechceme tab | |||
* complete (neni nakonfigurovano, hrozi pady. | |||
*/ | |||
rl_catch_signals = 0; | |||
rl_catch_sigwinch = 0; | |||
rl_inhibit_completion = 0; | |||
/* Zahajeni editace */ | |||
rl_callback_handler_install(": ", readline_callback); | |||
/* Vlozi vychozi (default) text */ | |||
rl_insert_text("Editaci ukonci 2x stisk ESCAPE"); | |||
while (1) | |||
{ | |||
int cursor_pos; | |||
clear(); | |||
mvaddstr(10, 10, rl_display_prompt); | |||
addstr(rl_line_buffer); | |||
if (string) | |||
{ | |||
mvaddstr(12, 6, "text: "); | |||
attron(A_REVERSE); | |||
addstr(string); | |||
attroff(A_REVERSE); | |||
} | |||
/* nastav kurzor */ | |||
cursor_pos = strnwidth(rl_display_prompt, SIZE_MAX) + | |||
strnwidth(rl_line_buffer, rl_point); | |||
move(10, 10 + cursor_pos); | |||
refresh(); | |||
c = getch(); | |||
/* ignoruj tabelatory */ | |||
if (c == '\t') | |||
continue; | |||
if (c == 27) | |||
{ | |||
if (alt) | |||
break; | |||
else | |||
alt = true; | |||
} | |||
else | |||
alt = false; | |||
readline_proxy = c; | |||
readline_proxy_is_valid = true; | |||
/* posli echo readline, ze jsou nova data k precteni */ | |||
rl_callback_read_char(); | |||
} | |||
rl_callback_handler_remove(); | |||
endwin(); | |||
} | |||
</pre> | |||
S mírnými úpravami byl výše uvedený příklad přeložitelný i s knihovnou | |||
<code>editline</code>, která by mohla být zajímavou alternativou zejména díky BSD licenci. | |||
Tato knihovna implementuje podmnožinu API knihovny <code>readline</code>, nicméně zrovna | |||
callback API nefunguje dobře. (A stávající správce mi napsal, že ho to vůbec | |||
nepřekvapuje). Proměnná <code>rl_point</code> není aktualizována a stále obsahuje 0 (což je | |||
evidentně chyba knihovny). Podařilo se mi zadat i českou diakritiku, ale bylo | |||
potřeba knihovně předávat široké znaky (a s těmi zase nefungovala knihovna | |||
<code>readline</code>). U <code>editline</code> není podporován hook <code>rl_input_available_hook</code> (a ani | |||
není nutný, jelikož místo více bajtových znaků knihovna vyžaduje široké znaky). U | |||
<code>readline</code> je jeho použití nutnost (jinak dojde k zacyklení při zadání více | |||
bajtového znaku). | |||
Všimněte si, že zde již používám "pokročilejší" posloupnost operací - napřed | |||
zobrazím obsah, a teprve potom čekám na stisk klávesy. Jednoduché ukázky, a | |||
školní příklady většinou mají tyto operace prohozené. Začíná se stiskem klávesy, | |||
a pak podle stisknuté klávesy se kreslí výstup. Zřejmou nevýhodou je prázdná | |||
obrazovka při prvním čekání na stisk klávesy. | |||
Při práci s knihovnou readline jsem se setkal ještě s jedním docela "zákeřným" | |||
problémem. readline vyžaduje vypnutý keypad režim. Naopak v normální aplikaci | |||
budeme asi chtít mít keypad zapnutý. Jelikož se v ncurses keypad nastavuje na | |||
okno, tak jsem na standardním okně <code>stdscr</code> keypad povolil, a v okně, které jsem | |||
používal pro vstup, jsem měl keypad vypnutý. Skoro to fungovalo, až na případy, | |||
kdy první událost byl stisk kurzoru (čehokoliv, co funguje pouze se zapnutým | |||
keypadem). Tady se ukázala další vlastnost ncurses. Strukturu <code>WINDOW</code> do jisté | |||
míry můžeme chápat i jako konfiguraci terminálu. V ncurses vždy "čteme" data z | |||
okna. Nic takového reálně neexistuje, data čteme z tty. Okna jsou pouze abstrakce | |||
ncurses. Při čtení se nastaví příslušná konfigurace (pro dané okno). Pokud je | |||
viditelný kurzor nebo echo, tak se nastaví kurzor a čeká se na znak. Po získání | |||
znaku se nastavuje konfigurace hlavního okna (si myslím). | |||
Pokud používáte funkci [https://man7.org/linux/man-pages/man2/poll.2.html <code>poll</code>] nebo <code>select</code> (pokud se na událost čeká ve vašem | |||
kódu), tak skoro vždy událost v terminálu vzniká ještě před tím, než ncurses | |||
nakonfiguruje terminál podle konfigurace odpovídající použitému oknu), a může se | |||
stát, že událost neodpovídá požadované konfiguraci (požadovanému oknu). Nejedná | |||
se o chybu. Je to vlastnost architektury. Když plánujete psát komplexnější | |||
terminálovou aplikaci, tak nejlepším začátkem je, si nastudovat možnosti a funkce | |||
nějakého jednoduššího terminálu (VT52 nebo VT100) a vaši aplikaci psát jakoby pro | |||
tento terminál - externí krabici s klávesnicí připojenou na sériový port | |||
počítače. Případně jít ještě dál. Pracovat s mentálním modelem: "čtu data ze | |||
souboru, který mi krmí klávesnice, a zapisuji do souboru, kterým krmím tiskárnu". | |||
Pro vývojáře, který v životě nepracoval na fyzickém terminálu to není moc | |||
přirozené. Na druhou stranu, tato architektura má něco do sebe. Jednoduše a | |||
hlavně naprosto přirozeně umožňuje vzdálenou správu, jednoduše umožňuje | |||
zanořování aplikací - např. aplikace jako <code>screen</code>, <code>tmux</code> atd jsou díky tomu v | |||
podstatě velice jednoduše realizovatelné aplikace. | |||
=== Používání myši === | |||
Implementace obsluhy událostí generovaných myší v ncurses neměla nejlepší pověst. | |||
Narazil jsem na pár aplikací, které ačkoliv používaly ncurses, tak myš | |||
obsluhovaly pomocí knihovny <code>gpm</code>. Já jsem u <code>pspg</code> zůstal s obsluhou myši v | |||
ncurses, ale něco málo jsem si musel přeprogramovat (double click), a pro režim, | |||
kdy pohybuji s myší a zároveň držím tlačítko, jsem musel použít rozšířený režim, | |||
který není nativně podporován ncurses (musím ho zapínat, vypínat escape | |||
sekvencí). | |||
Události, které generuje myš (pohyb, stisknutí tlačítka, rotace kolečka), | |||
primárně zpracovává terminál (emulátor terminálu). Podle aktuálního režimu na | |||
tuto událost sám zareaguje (v primární obrazovce (primary screen) zobrazením | |||
historie) nebo vygeneruje escape sekvenci, kterou pošle aplikaci (v alternativní | |||
obrazovce). Tady za nás ncurses udělá docela dost práce - escape sekvenci převede | |||
na událost <code>KEY_MOUSE</code>, kterou získám funkci <code>getch</code> a připraví datovou strukturu | |||
<code>MEVENT</code> (obsahuje polohu kurzoru myši, a bitmapu s kombinací stisknutých | |||
tlačítek myši a vybraných kláves na klávesnici). <code>MEVENT</code> získáme voláním funkce | |||
<code>getmouse</code>. U této funkce je dobré kontrolovat vrácenou hodnotu. Stalo se mi, že | |||
ncurses špatně detekoval událost, a teprve při dekódování celé escape sekvence, | |||
se zjistilo, že poslední událost byla zmatečná. Funkce <code>getmouse</code> také může | |||
vrátit <code>ERR</code> pokud k získání události používám <code>wgetch</code>, přičemž kurzor myši byl | |||
mimo dané okno. Souřadnice kurzoru jsou vždy vztažené k celé obrazovce. Voláním | |||
funkce <code>wenclose</code> můžeme zjistit, zdali byl kurzor myši uvnitř okna, a použitím | |||
funkce <code>wmouse_trafo</code> získáme souřadnice kurzoru vztažené k počátku zadaného | |||
okna. | |||
Starší ncurses ještě nepodporovaly kolečko na myši, a bohužel se ještě s touto | |||
verzí ncurses často potkávám. Tudíž vždy je nutné používat <code>#ifdef NCURSES_MOUSE_VERSION > 1</code>, | |||
pokud testujete tlačítka 4 a 5 (což odpovídá pohybu kolečka od sebe nebo k sobě). | |||
Je to například problém RHEL7 s ncurses 5.9 z roku 2014. A firem, které jsou | |||
ještě na této verzi RHELu je pořád dost. | |||
<pre> | |||
#include <ncurses.h> | |||
int | |||
main() | |||
{ | |||
int eventno = 0; | |||
int c; | |||
initscr(); | |||
cbreak(); | |||
curs_set(0); | |||
noecho(); | |||
scrollok(stdscr, TRUE); | |||
/* | |||
* bez aktivniho keypad modu nedochazi k dekodovani | |||
* vstupnich escape sekvenci, vcetne mysi, musi byt TRUE | |||
*/ | |||
keypad(stdscr, TRUE); | |||
move(0,0); | |||
mousemask(BUTTON1_PRESSED | | |||
BUTTON1_RELEASED | | |||
BUTTON1_CLICKED | | |||
BUTTON1_DOUBLE_CLICKED | |||
#if NCURSES_MOUSE_VERSION > 1 | |||
| BUTTON4_PRESSED | |||
| BUTTON5_PRESSED | |||
#endif | |||
, NULL); | |||
c = getch(); | |||
while (c != 'q') | |||
{ | |||
if (c == KEY_MOUSE) | |||
{ | |||
MEVENT mevent; | |||
int res; | |||
if (getmouse(&mevent) == OK) | |||
{ | |||
printw("%4d: key: %s, y: %d, x: %d", | |||
++eventno, | |||
keyname(c), | |||
mevent.y, | |||
mevent.x); | |||
/* | |||
* tam, kde to neni nutne se nedoporucuje pouzivat | |||
* funkci print, ktera je vyrazne narocnejsi na CPU | |||
*/ | |||
if (mevent.bstate & BUTTON1_PRESSED) | |||
addstr(", BUTTON1_PRESSED"); | |||
if (mevent.bstate & BUTTON1_RELEASED) | |||
addstr(", BUTTON1_RELEASED"); | |||
if (mevent.bstate & BUTTON1_CLICKED) | |||
addstr(", BUTTON1_CLICKED"); | |||
if (mevent.bstate & BUTTON1_DOUBLE_CLICKED) | |||
addstr(", BUTTON1_DOUBLE_CLICKED"); | |||
#if NCURSES_MOUSE_VERSION > 1 | |||
if (mevent.bstate & BUTTON4_PRESSED) | |||
addstr(", BUTTON4_PRESSED"); | |||
if (mevent.bstate & BUTTON5_PRESSED) | |||
addstr(", BUTTON5_PRESSED"); | |||
#endif | |||
addch('\n'); | |||
} | |||
else | |||
addstr("broken mouse event\n"); | |||
} | |||
else | |||
printw("%4d: key: %s\n", ++eventno, keyname(c)); | |||
refresh(); | |||
c = getch(); | |||
} | |||
endwin(); | |||
} | |||
</pre> | |||
Při zpracování události od myši ncurses čeká ve výchozím nastavení 250 ms | |||
(nastavuje se funkcí <code>mouseinterval</code>) na případnou další událost od myši, a podle | |||
toho jestli přijde nebo nepřijde nastaví flag <code>PRESSED</code> nebo <code>CLICKED</code>. Mne to v | |||
<code>pspg</code> způsobovalo nepříjemné jakoby opožděné reakce na myš. Přeci jen 250ms už | |||
si všimnete. Takže jsem si nastavil <code>mouseinterval(0)</code>, čímž jsem vyblokoval | |||
generování flagu <code>CLICKED</code>, a na flag <code>PRESSED</code> jsem napojil získání fokusu, a na | |||
flag <code>RELEASED</code> pak provedení akce. Tím <code>pspg</code> uživatele nenutí mít příliš rychlé | |||
prsty, a zároveň ovládání <code>pspg</code> není "líné" | |||
V <code>mc</code> se mi líbí, že uživatel může pohybovat myší, a držením levého tlačítka mít | |||
neustále zvýrazněný fokus. Dost dlouho mi trvalo, jak toho v ncurses docílit. | |||
Sice si mohu nastavit masku myši <code>REPORT_MOUSE_POSITION</code>, ale to samo od sebe nic | |||
neudělá. Je potřeba na terminálu vybrat některý z protokolů pro sledování myši | |||
"xterm Mouse Tracking", a pro tento účel mi vyhovoval protokol označený číslem | |||
1002. Zapíná se escape sekvencí <code>\033[?1002h</code> a vypíná <code>\033[?1002l</code> (flag | |||
<code>REPORT_MOUSE_POSITION</code> musí být aktivní). | |||
=== Velikost terminálu === | |||
Nejstarší terminály měly fixní konstantní velikost danou typem terminálu. Na | |||
základě údajů ze své databáze vlastností jednotlivých terminálů pak ncurses | |||
nastaví proměnné <code>LINES</code> a <code>COLS</code>. U terminálů, které jsou emulovány softwarově | |||
tento přístup není možný. Jejich velikost je dynamická, a proto by pseudoterminál | |||
při své inicializaci měl nastavit systémové proměnné <code>$LINES</code> a <code>$COLUMNS</code>, | |||
odkud hodnoty převezme ncurses, které nestaví své proměnné <code>LINES</code> a <code>COLS</code>. U | |||
softwarově emulovaných terminálů běžně dochází ke změně velikosti i v průběhu | |||
běhu aplikací, tudíž by terminálová aplikace měla ošetřit signál <code>SIGWINCH</code>, | |||
který je generován terminálem při změně velikosti. | |||
Zde nastávají první komplikace, kdy některé aplikace neošetřují vše, co by měly. | |||
Mějme posloupnost aplikací <code>bash</code>, <code>psql</code> a <code>pspg</code>. Pokud by <code>bash</code> neošetřoval | |||
<code>SIGWINCH</code>, tak se může stát, že systémové proměnné <code>$LINES</code> a <code>$COLUMNS</code> nebudou | |||
obsahovat aktuální hodnoty. Zrovna tak se může stát, že ačkoliv <code>pspg</code> správně | |||
ošetřuje <code>SIGWINCH</code>, tak nedokáže nastavit zmíněné systémové proměnné vnějšímu | |||
procesu, a po ukončení <code>pspg</code> jsou tyto proměnné opět neaktuální. Chování ncurses | |||
můžeme ovlivnit voláním konfiguračních funkcí <code>use_env</code> a <code>use_tioctl</code>. V | |||
závislosti na konfiguraci ncurses bere v potaz systémové proměnné <code>$LINES</code> a | |||
<code>$COLUMNS</code>, případně je ignoruje a velikost terminálu si zjišťuje samotná | |||
knihovna. | |||
V moderních systémech a v moderních ncurses se můžete spolehnout na vestavěnou (v | |||
ncurses) obsluhu signálu, a na obsah proměnných <code>LINES</code> a <code>COLS</code> (po změně | |||
velikosti aplikace dostane událost <code>KEY_RESIZE</code>. U starších systémů jsem se | |||
setkal s tím, že tyto proměnné při startu aplikace, případně po změně velikosti | |||
okna nebyly aktuální, a musel jsem si je načíst sám. Jinak <code>KEY_RESIZE</code> funguje | |||
asi všude. Správné automatické nastavení <code>LINES</code> a <code>COLS</code> funguje tak na 99.9% | |||
(ale je možné, že bych si možná pomohl laděním <code>use_env</code> a <code>use_tioctl</code>, a možná | |||
také ne. <code>ioctl</code> mi fungovalo všude): | |||
<pre> | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include <ncurses.h> | |||
int | |||
main() | |||
{ | |||
int c = ' '; | |||
initscr(); | |||
noecho(); | |||
cbreak(); | |||
curs_set(0); | |||
while (c != 'q') | |||
{ | |||
int lines, cols; | |||
mvprintw(9, 10, "keyname: %s", keyname(c)); | |||
if (c == KEY_RESIZE) | |||
{ | |||
/* velikost terminalu */ | |||
mvprintw(10, 10, "terminal: %d, %d", LINES, COLS); | |||
/* velikost hlavniho okna */ | |||
getmaxyx(stdscr, lines, cols); | |||
mvprintw(11, 10, "stdscr: %d, %d", lines, cols); | |||
} | |||
refresh(); | |||
c = getch(); | |||
} | |||
endwin(); | |||
exit(EXIT_SUCCESS); | |||
} | |||
</pre> | |||
V případě, že si sami ošetřujete signál <code>SIGWINCH</code>, je nutné o změně velikosti | |||
terminálu informovat ncurses: | |||
<pre> | |||
#include <stdio.h> | |||
#include <stdlib.h> | |||
#include <ncurses.h> | |||
#include <signal.h> | |||
#include <sys/ioctl.h> | |||
#include <termios.h> | |||
static bool handle_sigwinch = false; | |||
static void | |||
SigwinchHandler(int sig_num) | |||
{ | |||
handle_sigwinch = true; | |||
signal(SIGWINCH, SigwinchHandler); | |||
} | |||
int | |||
main() | |||
{ | |||
int c = ' '; | |||
initscr(); | |||
noecho(); | |||
cbreak(); | |||
curs_set(0); | |||
/* nastavi timeout pro funkci getch() */ | |||
timeout(250); | |||
/* | |||
* tim, ze nastavuji signal po initscr, si | |||
* vynucuji vlastni obsluhu signalu | |||
*/ | |||
signal(SIGWINCH, SigwinchHandler); | |||
while (c != 'q') | |||
{ | |||
int lines, cols; | |||
if (handle_sigwinch) | |||
{ | |||
struct winsize size; | |||
handle_sigwinch = false; | |||
if (ioctl(fileno(stdout), TIOCGWINSZ, (char *) &size) >= 0) | |||
resize_term(size.ws_row, size.ws_col); | |||
} | |||
/* pokud neni timeout */ | |||
if (c != -1) | |||
{ | |||
mvprintw(9, 10, "keyname: %s", keyname(c)); | |||
clrtoeol(); | |||
} | |||
/* velikost terminalu */ | |||
mvprintw(10, 10, "terminal: %d, %d ", LINES, COLS); | |||
/* velikost hlavniho okna */ | |||
getmaxyx(stdscr, lines, cols); | |||
mvprintw(11, 10, "stdscr: %d, %d ", lines, cols); | |||
refresh(); | |||
/* maximalne po 250 ms skonci na timeout */ | |||
c = getch(); | |||
} | |||
endwin(); | |||
exit(EXIT_SUCCESS); | |||
} | |||
</pre> | |||
Jelikož jsem přepsal ncurses obsluhu signálu, tak resize terminálu nepřeruší | |||
provádění funkce <code>getch</code> (signál spolkla moje aplikace, nikoliv ncurses), a abych | |||
zachoval určitou interaktivitu, tak nastavuji timeout 250 ms. Všimněte si. | |||
Aktuální velikost terminálu získám voláním funkce <code>ioctl</code>, a volám funkci | |||
<code>resize_term</code>. Uvnitř této funkce se přepočítají (a přealokují) okna a nastaví | |||
proměnné <code>LINES</code> a <code>COLS</code>. Tato funkce by se neměla volat uvnitř obsluhy signálu. | |||
U jednodušších aplikací, když se potřebujeme vyhnout dlouhém blokujícímu čekání | |||
na klávesnici, tak si můžeme pomoct nastavením timeoutu. Pozor ale, příliš krátký | |||
timeout zbytečně zatěžuje CPU (a je trend od používání krátkých timeoutů | |||
ustupovat, tak aby nedocházelo ke zbytečnému uspávání a probouzení procesu), | |||
Poznámka: ačkoliv se tato funkce jmenuje <code>resize_term</code> neznamená to, že s ní | |||
můžete změnit velikost terminálu. Touto funkcí si vynutíte aktualizaci datových | |||
struktur v ncurses, které jsou nějakým způsobem navázané na velikost terminálu. | |||
Změnu velikosti terminálu si lze vynutit escape sekvencemi (na některých | |||
terminálech, určitě ne všude). Například pro nastavení terminálu na velikost | |||
100x50 znaků je sekvence <code>\e[8;50;100t</code> | |||
==Psaní aplikací pro terminál: výstup== | |||
Když jsem začínal psát [https://github.com/okbob/pspg <code>pspg</code>], 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 <code>mc</code> 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 <code>pspg</code> dostal do | |||
podoby, se kterou jsem spokojený. V poslední části seriálu článku o | |||
ncurses bych se zaměřil na výstup. Spíš než, abych systematicky prošel | |||
výstupní API, se budu věnovat tématům se kterými jsem válčil, a kde jsem se | |||
občas zasekl. 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 [https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/index.html ncurses HOWTO]. | |||
V [https://invisible-island.net/ncurses/ncurses.faq.html 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 | |||
[https://lists.gnu.org/archive/html/bug-ncurses/ 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 | |||
<code>stdout</code> nebo <code>stderr</code>, tak to buď nevidí nebo | |||
rozbije layout aplikace. Proto je nutný redirect <code>stderr</code>. | |||
Postup je jednoduchý: | |||
<ol> | |||
<li>V jiném terminálu spustit příkaz <code>tty</code>, který vrátí jméno souboru | |||
zařízení spojeného se standardním výstupem terminálu</li> | |||
<li>V aplikaci ladící výpisy posílat do <code>stderr</code>(a nezapomínat na <code>'\n'</code>) | |||
na konci řádku</li> | |||
<li>Laděnou aplikaci spustit s přesměrováním <code>stderr</code> do souboru jehož | |||
jméno vrátí příkaz <code>tty</code></li> | |||
</ol> | |||
<pre>#terminal 1 | |||
[pavel@localhost ~]$ tty | |||
/dev/pts/3 | |||
#terminal 2 | |||
[pavel@localhost pspg-master]$ ./pspg -f tests/pg_class.txt 2>/dev/pts/3 | |||
</pre> | |||
Ladění ncurses aplikace napsané v Cčku je stejné, jako | |||
jakékoliv aplikace napsané v Cčku. Hodně pomohou nástroje jako je | |||
[https://www.cse.unsw.edu.au/~learn/debugging/modules/asan/ <code>ASan</code>] nebo | |||
[https://valgrind.org/docs/manual/quick-start.html <code>Valgrind</code>]. | |||
===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. [https://github.com/magiblot/tvision Nový port] | |||
[https://en.wikipedia.org/wiki/Turbo_Vision 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í. | |||
<pre>[pavel@localhost ~]$ set|grep TERM | |||
COLORTERM=truecolor | |||
TERM=xterm-256color | |||
</pre> | |||
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 <code>WINDOW</code>. 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 <code>stdscr</code>. Při | |||
refreshi okna se obsah okna zkopíruje do okna <code>newscr</code>. | |||
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 <code>refresh</code>. | |||
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í | |||
<code>touchwin</code>. Poté, co se zkompletuje obsah okna | |||
<code>newscr</code> (tím, že se provede refresh <code>stdscr</code> a | |||
oken vytvořených aplikací), tak se provede detekce změn jeho obsahu | |||
(včetně detekce posunu řádků) s obsahem okna <code>curscr</code>. 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 <code>curscr</code>. | |||
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í | |||
<code>refresh</code> a případně <code>tochwin</code>, 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: <code>menu</code>, <code>pad</code>, | |||
<code>panel</code> a [https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/forms.html 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 <code>pad</code> 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 <code>pspg</code> 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 <code>wnoutrefresh</code> (namísto <code>wrefresh</code>). Tato | |||
funkce pouze propaguje obsah okna do okna <code>newscr</code>. Druhá fáze | |||
zobrazení (optimalizace) se pak provede ve funkci <code>doupdate</code>. | |||
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 <code>pspg</code> je na | |||
obrazovce cca 6 oken v jednu chvíli, a než jsem začal používat | |||
<code>wnoutrefresh</code>, 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 <code>getch()</code> se volá | |||
<code>refresh(stdscr)</code>, z rutiny <code>wgetch(win)</code> se volá | |||
<code>wrefresh(win)</code>. 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 <code>newwin</code> a <code>subwin</code>=== | |||
Už jsem zmínil, že okno (<code>WINDOW</code>) 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 <code>stdscr</code>, | |||
nebo jestli se pracuje s oknem, které si vytvořila aplikace (používá se | |||
prefix <code>w</code>). Například máme funkce <code>refresh</code> a | |||
<code>wrefresh</code> . Funkce pro standardní obrazovku jsou dost často | |||
řešené jako makro: | |||
<pre>#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)) | |||
</pre> | |||
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 <code>mv</code>). Existuje modifikace | |||
funkce <code>printf</code> přizpůsobená pro použití v ncurses. Její variace jsou | |||
příkladem výše popsaného designu: | |||
<pre>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); | |||
</pre> | |||
Funkce <code>printf</code> 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 <code>addstr</code>: | |||
<pre>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); | |||
</pre> | |||
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): | |||
<pre>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); | |||
</pre> | |||
Pro začátek je asi obtížné se v tom zorientovat, díky množství | |||
variacím. Praxe je pak poměrně jednoduchá (jakmile člověk pochopí ten | |||
systém). Wide char funkce jsem v <code>pspg</code> 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é <code>stdscr</code>. 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 <code>newwin</code> a zrusí | |||
voláním funkce <code>delwin</code> (viz manuálová stránka [https://invisible-island.net/ncurses/man/curs_window.3x.html <code>newwin</code>]). Kromě oken | |||
můžete používat podokna (<code>subwin</code>), 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 <code>refresh</code> nebo <code>wrefresh</code>. 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: | |||
<pre>#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(); | |||
} | |||
</pre> | |||
Kontrolní otázka: Když si otestujete výše uvedený příklad, tak na | |||
obrazovce neuvidíte rámeček 10x10 znaků. Proč? | |||
<pre>#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(); | |||
} | |||
</pre> | |||
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 <code>ch</code>, která ořezává text. | |||
Problém je, že tyto funkce existují pouze pro interní typy <code>chtype</code> string | |||
(8bit) a pro <code>cchar_t</code> 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 | |||
[https://invisible-island.net/ncurses/man/curs_pad.3x.html <code>pad</code>]: | |||
<pre>#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(); | |||
} | |||
</pre> | |||
S touto komponentou se docela jednoduše implementuje skrolování ve | |||
všech směrech. Ve funkci <code>prefresh</code> jen změním počátek výřezu uloženého v | |||
bufferu <code>pad</code>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 | |||
[http://melvilletheatre.com/articles/ncurses-extended-characters/index.html rozšířené | |||
znakové sady]. Navíc, díky této funkci, kterou terminály poskytují, | |||
tak 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ř <code>ACS_ULCORNER</code>). K vytištění znaků | |||
(včetně speciálních) se používá funkce <code>addch</code>. Zajímavé je, že hodnotu | |||
znaku zadáváme dohromady se stylem v parametru typu <code>chtype</code>. 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 | |||
<code>A_ALTCHARSET</code>. Jelikož hodnota, styl a barva je zakódována v hodnotě | |||
typu <code>chtype</code>, 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 <code>A_CHARTEXT</code>. Atributy získáme vymaskováním hodnotou | |||
<code>A_ATTRIBUTES</code>, a posunutý index barevného páru získáme vymaskováním | |||
hodnoty <code>A_COLOR</code>. Originální index barevného páru získáme makrem | |||
<code>PAIR_NUMBER</code> (z hodnoty typu <code>chtype</code>). 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 <code>ncursesw</code>) | |||
rozšířila o typ <code>cchar_t</code>. 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 <code>cchar_t</code> se používají funkce <code>setcchar</code> a <code>getcchar</code>. | |||
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 <code>BOLD</code> ( 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 <code>BOLD</code> 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 <code>COLORS</code>), 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 <code>BOLD</code> 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 <code>$TERM</code> 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ů <code>BOLD</code> a <code>REVERSE</code>, a to už bylo | |||
docela nepřehledné. Pokud by terminál podporoval pouze 8 barev, pak je | |||
jediná možnost nahodit <code>BOLD</code> 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ě. | |||
<pre>#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(); | |||
} | |||
</pre> | |||
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í | |||
<code>refresh</code> nebo <code>wrefresh</code>). 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 <code>stdscr</code>) | |||
nebo vybraného okna (pokud používáte <code>wgetch</code>). | |||
===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í <code>assume_default_colors</code>. Pokud se jako index barvy | |||
použije hodnota <code>-1</code>, 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 <code>use_default_colors</code>, což | |||
je ekvivalent <code>assume_default_colors(-1, -1)</code>: | |||
<pre>#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(); | |||
} | |||
</pre> | |||
===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á <code>COLORS</code>. U mne má hodnotu | |||
[https://www.ditig.com/256-colors-cheat-sheet 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ř. | |||
<code>COLOR_BLACK</code>, <code>COLOR_RED</code>, … ). Pro další barvy žádné konstanty | |||
nejsou. | |||
Pokud chci pracovat s truecolors barvami, musím si je nejdřív vytvořit | |||
funkcí <code>init_color</code>. 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á <code>COLORS</code>, | |||
a v podstatě to znamená, že si přepisujete základní paletu 256 barev. V | |||
rozšíření ncurses je ještě definovaná funkce <code>init_extended_color</code>, která | |||
by měla umožňovat pracovat s větším rozsahem, ale v ncurses je to alias | |||
na <code>init_color</code>, 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 | |||
<code>start_color</code>: | |||
Aby se mi lépe pracovalo s mnou definovanými barvami, přidal jsem si do | |||
aplikace tabulku <code>ColorCache</code>, a funkci <code>color_index_rgb</code>, 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: | |||
<pre>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)); | |||
</pre> | |||
Pozor, některé terminály (např. terminál <code>konsole</code>) neumožňují změnu barev. Podporu změny barev lze otestovat funkcí <code>can_change_color()</code>. | |||
Ve Fedoře jsem narazil na problém s výchozím nastavením proměnné <code>TERM</code> na <code>xterm-256color</code>, a to právě pokud se používal terminál <code>konsole</code>, jelikož <code>xterm</code> | |||
změnu barev podporuje a <code>konsole</code> nikoliv. Pro korekní chování bylo nutné nastavit <code>TERM</code> na <code>konsole-256color</code>. | |||
ncurses řady 6 podporují direct color mode. V tomto režimu je barva identifikována přímo rgb hodnotou. Barva se již nemusí definovat, ale její rgb kód se použije ve funkci <code>init_extended_color</code>. | |||
Tento režim lze použít pouze tehdy, pokud jej term info podporuje (např. <code>xterm-direct</code>). Režim lze detekovat podle hodnoty <code>COLOR</code>, která je nastavená na 16777216 (resp. 0x10000000). Ve starých | |||
ncurses se pro identifikaci barvy používala hodnota o velikosti 1byte (short). Pro práci s rgb se požívají tzv extended funkce, kde barva je uložena v hodnotě typu int. Pro test, zda-li jsou | |||
extended funkce pro práci s barvami lze použít konstantu <code>NCURSES_EXT_COLORS</code>. | |||
===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 <code>wrefresh</code> nemá žádný | |||
efekt). Systémovým řešením je použití komponenty [https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/panels.html <code>PANEL</code>]. 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 (<code>show_panel</code>), | |||
zneviditelnit (<code>hide_panel</code>), přesunout | |||
(<code>move_panel</code>). Panel můžeme přesunout úplně nahoru | |||
(<code>top_panel</code>) nebo úplně dospod (<code>bottom_panel</code>). | |||
Příkaz <code>update_panels</code> zavolá <code>wnoutrefresh</code> | |||
asociovaných oken ve správném pořadí. Aby vše fungovalo, tak samozřejmě | |||
uživatel nesmí volat ručně <code>wrefresh</code> nebo | |||
<code>wnoutrefresh</code>. Na závěr se zavolá funkce | |||
<code>doupdate</code>, která zajistí vygenerování změnové sekvence a její | |||
odeslání souboru terminálu. | |||
<pre>#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(); | |||
} | |||
</pre> | |||
Kód se musí linkovat s knihovnou <code>panel</code> - | |||
<code>gcc -lncursesw -lpanel test.c</code>. | |||
===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 <code>pspg</code> 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 | |||
<code>wl-clipboard</code> (Wayland), <code>xclip</code> (XWindows) a <code>pbcopy</code> (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 <code>popen</code> 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 | |||
<code>popen</code>, kde používáte dvě (tři) roury pro <code>stdin</code>, | |||
<code>stdout</code> (případně <code>stderr</code>). 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 [https://github.com/okbob/ncurses-st-menu 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 <code>pspg</code> 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 <code>mvwin_wch</code>, která | |||
vrací hodnotu typu <code>cchar_t</code>. Pak pomocí funkce | |||
<code>getcchar</code> jsem <code>cchar</code> rozdělil na jednotlivé | |||
složky - široký znak, atributy a index páru barev. Pak jsem pomocí funkce | |||
<code>setcchar</code> opět sestavil hodnotu typu <code>cchar_t</code> | |||
(obsahuje veškeré informace o tištěném znaku) a funkcí | |||
<code>mvwadd_wch</code> 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 <code>A_ALTCHARSET</code>, kterým se vynucuje použití | |||
alternativní znakové sady. | |||
Tím, že jsem resetoval <code>A_ALTCHARSET</code>, jsem si ve stínu rozbíjel rámečky | |||
(a některé speciální znaky). Tento atribut se musí zachovat. | |||
<pre>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); | |||
</pre> | |||
Později jsem zjistil, že výše uvedený kód lze zjednodušit použitím | |||
funkcí, které pracují s typem <code>chtype</code>: | |||
<pre> | |||
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, | |||
</pre> | |||
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 <code>less</code> má velice praktický přepínač | |||
<code>--no-init</code> nebo <code>-X</code>. Pokud se zapne, tak si | |||
<code>less</code> 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 <code>pspg</code> 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 <code>pspg</code>, 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 <code>stdout</code> 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í <code>wresize</code>. | |||
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 <code>pspg</code> 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. | |||
===Závěr=== | |||
Ohledně ncurses mám mírně rozporuplné pocity (ve výsledku ale spíš | |||
kladné). Aplikace s jednoduší vizuální podobou jako je <code>vi</code>, | |||
<code>top</code>, <code>htop</code> <code>emacs</code> 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 <code>vim</code>) znamená napsat si vlastní | |||
vrstvu (tak jako jsem si napsal [https://github.com/okbob/ncurses-st-menu 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 | |||
<code>midnight commander</code> nebo <code>dialog</code>. 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 tam 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 <code>ifdef</code>ů). 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. | |||
Po zkušenostech, které teď mám, bych asi ncurses použil pouze jako plátno (canvas). | |||
Hodně jsem zápasil s chováním objektů v ncurses při změně velikosti terminálu. U fyzických | |||
terminálů změna velikosti nebyla možná, a v ncurses je několikrát implementována | |||
jakási heuristika, která pomáhala s provozem starších aplikací. Nejde vypnout, | |||
a já jsem s ní dost bojoval. Začátkem 90 let, kdy se bojovalo o každý kb v | |||
RAM nebo na disku mělo velký význam sdílet určitou funkcionalitu, to už dnes | |||
v TUI aplikacích nemá smysl. |
Aktuální verze z 20. 1. 2023, 16:37
Ncurses
Autor: Pavel Stěhule, 7.4.2021
Poslední 4 roky jsem hodně času věnoval vývoji pspg. A nyní, když je pspg
dokončen, tak bych zde
chtěl shrnout zkušenosti z vývoje této aplikace, a napsat něco o ncurses. ncurses je základní knihovna,
bez které bych aplikaci jako je pspg
nemohl napsat (asi mohl, ale dalo by mi to výrazně víc práce).
Úvod
ncurses je knihovna, která poskytuje základní funkcionalitu pro vývoj terminálových aplikací. I když se jedná
o novější inkarnaci knihovny curses, má svoje léta. Historie této knihovny spadá do přelomu 70 a 80 let, a
svým konceptem a možnostmi odpovídá této době. Jednoduché terminálové aplikace se nad touto knihovnou
napíší jednoduše (typu unixového příkazu top
). Napsat komplexnější aplikace typu mc
nebo pspg
je
už výrazně pracnější. Bohužel, žádná rozumná alternativa (co se týče rozšíření a životaschopnosti, akceptace)
neexistuje. S touto knihovnou se pracuje docela jednoduše. Také je, co se týče funkčnosti hodně omezená.
Když porovnám vývoj nad ncurses s cca o 20 let mladší knihovnou libxml2, tak je programování v ncurses báječně
jednoduché. Za těch 20 let se programování dostalo někam úplně jinam - libxml2 umí neskutečné věci, ale správně
ji použít je výrazně obtížnější. Napadá mne analogie náročnosti řízení a možnosti stíhačky z první světové války
(kdy už letadlo vypadalo jako letadlo) - rok 1918, se stíhačkou se začátku 2 světové (podívejte se na videa
přípravy Spitfire k letu).
Přístup k počítačům s Unixem v 70 a 80 letech zprostředkovávaly terminály. Původně se jednalo o modifikované zařízení pro dálnopis, posléze to už byly jednoduché počítače s CRT monitorem a primitivními CPU. Terminály z 80 let už byla relativně pokročilá zařízení podporující barvy, jemnou grafiku (ať vektorovou nebo bitovou). Vývoj terminálů nebyl nijak zvlášť revoluční - dlouho to bylo elektro mechanické zařízení. Posléze byl mechanický tisk znaků na papír nahrazen zobrazením znaků v CRT monitoru, a postupně se přidávaly další možnosti (alternativní znakové sady, možnost skrolování obrazu, barvy, zvýraznění, ...), tak jak rostly schopnosti mikropočítačů. Způsob komunikace s terminálem byl a je stále stejný od dob dálnopisu. Když zobrazuji znak na obrazovku terminálů, používám ty samé prostředky, jako kdybych zobrazoval znak na papír připojeného dálnopisu. To je asi kritická vlastnost unixových terminálových aplikací - běží úplně stejně se stejnými prostředky, ať aplikaci pustíte lokálně nebo vzdáleně. Není tak důležité, kde je terminál umístěný fyzicky nebo jaký vlastně terminál používáte.
Díky terminálům bylo možné relativně jednoduše sdílet výpočetní výkon tehdejších počítačů. Na druhou stranu programátor neměl přímý přístup k hardware terminálu, a s terminálem komunikoval po relativně pomalé síti. To se samozřejmě podepsalo na vizuální podobě tehdejších aplikací. Unixové aplikace, alespoň pro mne, měly auru něčeho výjimečného, zvláštního. Ale skoro pokaždé jsem si říkal, proč jsou tak ošklivé. Aplikace napsané pro MS DOS koncem 80 a v první polovině 90 let vypadaly výrazně lépe. Vzpomínám na Turbo Pascal verze 5.5, pak do dokonalosti dotažené Turbo Vision v 6tce. Podívejte se na screenshoty FoxPro nebo WordPerfectu, nebo českého PC FANDu. Nicméně, když o tom tak přemýšlím, ono to jinak s těmi terminály nešlo.
Aby Turbo Vision mohl nějak rozumně běžet, tak potřeboval přímý přístup k hardware grafické karty. Z důvodů
rychlosti se obcházel i DOS. Při tehdejší rychlosti sítí (často se jednalo modemové spojení přes telefonní linku),
tak by aplikace s vizuálem FoxPra byla nepoužitelná. V momentě, kdy padla technická omezení (konec 80 let), tak
se vývoj terminálových aplikací hodně upozadil. Tam, kde šlo ergonomii UI se rychle přešlo na grafiku
(XWindow), a vývoj samotné technologie terminálových aplikací se zastavil, zakonzervoval. Pokud
používáme terminálové aplikace, tak se pohybujeme v prostředí, kde vývoj skončil v druhé polovině 90 let.
Napadá mne, že poslední větší změnou byl přechod na UTF8 (a pořád po více než 20 letech tato "novinka"
způsobuje problémy - viz dvě varianty knihovny ncurses a ncursesw a důležitost správného nastavení proměnné LANG
).
Vyjma muzea už není šance narazit na fyzický terminál. Nicméně naše stávající softwarové emulace terminálů jsou velice
věrné emulace hardware z půli osmdesátých let (včetně emulace chyb). Nejrozšířenější akceptovaný standard je
VT220 z roku 1983 (8bit CPU 8051). Z nekomerčních softwarových emulátorů
pouze xterm
umí emulovat některé funkce vyšších verzí (VT240,
VT340,VT520) jako je podpora protokolů ReGIS nebo Sixel. xterm se už ale používá málo a nejrozšířenější
terminály jako je Gnome Terminál nebo Konsole tyto funkce nepodporují (ve stávajících verzích). Vývojová
verze knihovny VTE (kterou používá Gnome Terminál) podporuje Sixel (bitmapová grafika) a experimentálně se
pracuje na podpoře ReGISu (vektorová grafika). Je to na jednu stranu obdivuhodné, ale také šílené, protože
softwarová emulace je poměrně komplexní, a vlastně narážíme na limity hardware, které už fyzicky 40 let nepoužíváme.
Nikdo od té doby neměl dostatečnou tržní sílu a ambice něco s tím udělat (pro komerční sféru je to absolutně
nezajímavá oblast, a jednotlivec nic nezmůže). Vím o jednom experimentu, ale je to experiment - nepřikládám tomu
žádnou váhu. Je fakt, že je vidět určitý posun k vlastnostem, které definuje xterm, a tudíž už pomalu opouštíme
omezení terminálu VT220. Mimochodem, ncurses a xterm posledních 25 let udržuje tentýž vývojář - Thomas E. Dickey.
Thomas také udržuje prohlížeč Lynx a pokud potřebujete poradit s čímkoliv ohledně ncurses, tak Thomas
je ta pravá osoba.
Default na mé Fedoře (v Gnome Terminálu):
[pavel@localhost ~]$ set | grep TERM COLORTERM=truecolor TERM=xterm-256color
Tím, že se jedná o relativně starou technologii, tak se zákonitě musíme zeptat, proč tuto starou
technologii ještě dnes používat - v 21 století (40-50 let po jejím vzniku). Odpověď bude vždy subjektivní.
Já osobně preferuji některé terminálové aplikace z důvodu rychlosti a určité ergonomie. Vzhledem
k tomu, že se jedná o aplikace často napsané v 90 letech, a tehdy běžely relativně rychle, tak
na soudobém hardware jsou opravdu velice rychlé, což pro mne je základ komfortu. Potom je tu další
faktor. Možnosti, které má vývojář, když chce napsat terminálovou aplikaci, jsou opravdu omezené.
Tudíž si vývojář musí hodně dobře rozmyslet, co bude zobrazovat a jak. Ty dobré terminálové aplikace
mají velice promyšlené uživatelské rozhraní - musí dobře fungovat v 16 barvách při základním rozlišení
24 x 80 znaků. S takovými aplikacemi se mi dobře pracuje. Je fakt, že jich moc není, ale zas jich, pro
svou práci, tolik nepotřebuji. Samozřejmě, že platí, že zvyk je železná košile, a s aplikacemi spouštěnými
v terminálu jsem nikdy neměl problém. A to i když jsem pracoval vzdáleně a rychlost internetu nebyla nic moc.
Ne, že by občas nezalagovaly, ale tou dobou už se s aplikacemi ovládanými přes vnc
nedalo pracovat už vůbec.
Zpět k ncurses a k terminálům. Jak zobrazím znak na terminálu? Použiji funkci fprintf
a na standardní
výstup pošlu nějaký text. Pokud je standardní výstup procesu nastavený na terminál, tak někde uvidím
vytištěný text. Z pohledu operačního souboru je terminál zařízení, které zastupuje speciální soubor
(/dev/tty
) do kterého mohu zapisovat, a zrovna tak z něj mohu číst. Zapsaná data se zobrazí na obrazovce
(kdybych měl připojený dálnopis, tak by se vytiskla na tiskárně dálnopisu). Čtení z tohoto souboru vlastně znamená
přečtení bufferu klávesnice.
[pavel@localhost ~]$ echo "AHOJ" > /dev/tty AHOJ
Primitivní textový editor (s možnostmi dálnopisu 70 let) vytvoříme voláním příkazu cat
, který čte z
/dev/tty
a zapisuje do souboru (v tomto případě dopis.txt
)
[pavel@localhost ~]$ cat /dev/tty > dopis.txt Ahoj, jmenuji se Pavel [pavel@localhost ~]$ cat dopis.txt Ahoj, jmenuji se Pavel
tty
zařízení je pro většinu uživatelů naprosto transparentní - každý proces při svém startu podědí prostředí,
které určuje, co je vstupem (zařízení zastoupené souborem, soubor nebo roura), stejně tak co je výstupem,
a kam se má tisknout chybový výstup. Pokud nedojde k přesměrování nebo k použití roury, tak vstupy a výstupy
procesu jsou na začátku nastavené na /dev/tty
.
Na mé Fedoře mám po startu rovnou používat 3 terminály - z Gnome přístupné přes CTRL-ALT3
- CTRL-ALT6
, a pro ně
existují zařízení (speciální soubory) /dev/tty3
- /dev/tty6
. Zařízení /dev/tty
je virtuální. Pro každou session
je namapované na nějaký terminál tty
nebo pseudo terminál - pty
. To, které zařízení se používá, vám zobrazí
příkaz tty
[pavel@localhost ~]$ tty /dev/pts/1
Zrovna tak, jako můžeme pracovat s /dev/tty
, tak můžeme pracovat s konkrétními zařízeními. Například mohu přesměrovat
standardní chybový výstup do jiného terminálu (což se dá dobře použít pro debugování ncurses aplikací).
[pavel@localhost ~]$ tty /dev/pts/1 [pavel@localhost ~]$ ./a.out 2> /dev/pts/3
Pod pseudoterminálem si můžeme představit dynamicky vytvořenou proxy, mezi kontrolovanou (ovládanou)
aplikací, a mezi aplikací, která zajišťuje služby terminálu. xterm
, konzole
, gnome terminál
, screen
nebo tmux
- tyto aplikace, při svém startu, otevírají (vytváří) nové pseudo terminály. Dá se to použít všelijak.
Například příkaz script
" otevře pseudoterminál, ve kterém můžeme normálně pracovat, nicméně vstup a výstup je zaznamenán,
a později může být přehrán příkazem scriptreplay
.
Dříve se samozřejmě intenzivně řešila komunikace, rychlost, způsob, protokol. Pokud si nehrajete se zařízeními připojenými na sériový nebo paralelní port, tak to už řešit nemusíte. To rád nechám na operačním systému.
Kdybych měl ncurses přirovnat k nějaké rozšířenější knihovně, tak mne napadá GDK (GIMP Drawing Kit - grafická primitiva), i když nabízí určitou funkcionalitu, kterou má knihovna GTK (GIMP Toolkit - komponenty uživatelského rozhraní - menu, tlačítka, edit box, ..). Část ncurses, která se věnuje komponentám, se označuje jako ncurses forms. Nemám ale pocit, že by se tato část ncurses dnes používala, ale mohu se mýlit. Mně nevyhovuje, protože všechny komponenty dodávané v ncurses jdou mimo standard CUA (Common User Access), který je určitým jednotícím konceptem textových DOS aplikací z přelomu 80 a 90 let (na tom jsem vyrostl). Ale jak jsem zmínil dříve, návrh UI terminálových aplikací musel být úplně jiný z důvodu naprosto odlišné architektury. ncurses se vetšinou používá pro low level operace. Když to hodně zjednoduším - tak primárně jde o zobrazení textu na konkrétní pozici x,y terminálu. Je to poměrně hloupé, nízkoúrovňové, definované možnostmi tehdejších terminálů. Umí to vyčistit určitou oblast na obrazovce, a vrchol funkcionality je vertikální odskrolování části obrazovky.
Když jsem s ncurses začínal, tak jsem narážel na to, že jsem si do určitých funkcí promítal svoje představy, ovlivněné tím, že jsem se napřed setkal s o 15 let novější knihovny. Základní strukturou ncurses je WINDOW. Ani ve snu by mne nenapadlo, že okna se (v ncurses) nesmí překrývat (jinak dochází k vizuálním defektům). V dokumentaci to samozřejmě je, kdo ale čte dokumentaci (napřed). Samozřejmě, že používání knihovny ncurses bude výrazně jednodušší pro člověka, který má zkušenosti s psaním terminálových aplikací a ví, co znamená raw mode, echo, atd. Na Unixech v 80 a 90 letech to musel být denní chleba každého vývojáře.
V momentě, kdy si člověk nastaví správná očekávání, tak už pak práce s ncurses tolik nebolí, pořád ale musí být vývojář dost akurátní. Jeden špatně zobrazený znak může snadno a výrazně rozhodit layout aplikace. Jsou situace, kdy ncurses správně ořízne výstup, a layout se nerozhodí. Nicméně po pár měsících se vám někdo ozve, že v určitých případech je zobrazení rozhozené, a ukáže se, že jednak zobrazujete znaky, které by se zobrazovat nemusely, druhak, že na operačním systému dotyčného je ncurses v 15 let staré verzi, s hromadou neopravených chyb. Tudíž se vyplatí být hodně precizní. Pak zas vaše aplikace funguje prakticky všude - Linux, BSD, MacOS, Solaris, ... Problém s neaktuálními zastaralými verzemi ncurses je docela velký. Někdy se jedná o systémy, které se už nevyvíjí. Jindy možná dodavatelé nechtějí riskovat problémy s kompatibilitou (může se jednat o provoz 30 let starých kritických aplikací), a někdy možná na to pečou. Na Linuxu to není tak velký problém, i když je možné potkat uživatele, kteří mají stále 5 kovou verzi. V roce 2015 se z důvodu podpory více než 256 barev změnilo ABI a číslo řady se změnilo na 6 (to byla jedna z největších změn za posledních 20 let). Thomas vydává nové verze skoro každý týden, ale až na těžké výjimky se jedná jen o bugfixové verze.
Jak tedy vytisknout řetězec na 3 řádek 10 sloupec v terminálu? Musíme si zjistit, jakou escape sekvencí nastavíme kurzor na danou pozici. Tu vytiskneme, a pak vytiskneme text.
#include <stdio.h> int main() { /* vymaz obrazovku */ fprintf(stdout, "\033[2J"); /* presun kurzor */ fprintf(stdout, "\033[3;10f"); /* vytiskni znak */ fprintf(stdout, "AHOJ"); /* a nesysli si to v cache */ fflush(stdout); /* cekej na stisk klavesy */ (void) getchar(); }
Tento jednoduchý kód by se dal určitě vylepšit. Pokud si tuto ukázku spustíte, tak zjistíte, tak jako zjistili už dávno před Vámi, že není příjemné, když se na jedné obrazovce míchá výstup řádkově orientovaných aplikací, a celo-obrazovkově orientovaných aplikací (screen oriented). Proto některé terminály podporují (pro tento typ aplikací) tzv alternativní obrazovku (alternate screen). Stačí jen poslat na terminál správnou sekvenci znaků (a neměli bychom zapomenout vytisknout escape sekvenci, která alternativní obrazovku vypne před ukončením aplikace). Pak bychom asi chtěli schovat kurzor, a také bychom raději, aby aplikace nevypisovala znaky, které napíše uživatel, a skončila při prvním stisku klávesy a nečekala na stisk ENTERu.
#include <stdio.h> #include <termios.h> #include <unistd.h> struct termios orig_termios; int main() { struct termios current; /* uloz aktualni nastaveni terminalu */ tcgetattr(STDIN_FILENO, &orig_termios); /* vypni echo, a necekej na enter */ tcgetattr(STDIN_FILENO, ¤t); current.c_lflag &= ~(ECHO | ICANON); tcsetattr(STDIN_FILENO, TCSAFLUSH, ¤t); /* aktivuj alternativni obrazovku */ fprintf(stdout, "\033[?1049h"); /* schovej kurzor */ fprintf(stdout, "\033[?25l"); /* vymaz obrazovku */ fprintf(stdout, "\033[2J"); /* presun kurzor */ fprintf(stdout, "\033[3;10f"); /* vytiskni znak */ fprintf(stdout, "AHOJ"); /* a nesysli si to v cache */ fflush(stdout); /* cekej na stisk klavesy */ (void) getchar(); /* prepni se na standardni obrazovku */ fprintf(stdout, "\033[?1049l"); /* zobraz kurzor */ fprintf(stdout, "\033[?25h"); /* vrat puvodni nastaveni terminalu */ tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); }
Tahle ta blbůstka mi zabrala víc půl hodiny práce než jsem vygoglil správná API a správné escape sekvence. Kdybych použil ncurses, tak bych si mohl tuhle práci ušetřit, ale to bych zas nevěděl, kolik práce mi ncurses ušetří. Zjevnou nevýhodou kódu, který řídí tisk na terminálu explicitními escape sekvencemi, je jeho nepřehlednost. Ačkoliv většina terminálů je alespoň částečně kompatibilní s VT50, VT100, tak to neplatí na 100% a můj příklad na některých terminálech nemusí fungovat (s některými sekvencemi jsem měl problém i v Gnome terminálu). Tudíž takhle by se terminálové aplikace psát neměly (že se tak občas píší, je věc jiná).
Je jasné, že se vývojáři snažili práci s escape sekvencemi sjednotit a obalit nějakým API. Výsledkem takových snah, byla dvě konkurující si API.
Starší termcap a novější (přehlednější, rychlejší) terminfo. ncurses implementuje obě tato API jako svou nízko úrovňovou vrstvu. Některé aplikace si vystačí pouze s touto vrstvou.
less
a screen
používají termcap API. Naopak např. tmux
používá terminfo. Také vyšší vrstva ncurses používá terminfo.
#include <stdio.h> #include <stdlib.h> #include <termcap.h> #include <termios.h> #include <unistd.h> struct termios orig_termios; int main() { struct termios current; char buf[1024]; /* nacti definici terminalu podle sys prom TERM */ tgetent(buf, getenv("TERM")); /* uloz aktualni nastaveni terminalu */ tcgetattr(STDIN_FILENO, &orig_termios); /* vypni echo, a necekej na enter */ tcgetattr(STDIN_FILENO, ¤t); current.c_lflag &= ~(ECHO | ICANON); tcsetattr(STDIN_FILENO, TCSAFLUSH, ¤t); /* aktivuj alternativni obrazovku */ fputs(tgetstr("ti", NULL), stdout); /* schovej kurzor */ fputs(tgetstr("vi", NULL), stdout); /* vymaz obrazovku */ fputs(tgetstr("cl", NULL), stdout); /* presun kurzor */ fputs(tgoto(tgetstr("cm", NULL), 10, 3), stdout); /* vytiskni znak */ fprintf(stdout, "AHOJ"); /* a nesysli si to v cache */ fflush(stdout); /* cekej na stisk klavesy */ (void) getchar(); /* prepni se na standardni obrazovku */ fputs(tgetstr("te", NULL), stdout); /* zobraz kurzor */ fputs(tgetstr("ve", NULL), stdout); /* vrat puvodni nastaveni terminalu */ tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios); }
Přenositelnost kódu je určitě mnohem vyšší, pracnost stejná (nebo možná ještě větší). Alespoň mně dalo mnohem víc práce dohledat správné termcap kódy. API termcap je z konce 70 a začátku 80 let. Tehdy to bezpochyby muselo hodně přinést. Předek ncurses, knihovna curses, používala termcap API. Osobně jsem rád, že tak nízko úrovňová API už nemusím používat, a že mám k dispozici ncurses (od roku 1993, pcurses (free verze curses) je dostupná od roku 1986 (v té době jsem ještě neměl počítač), curses samotné je z roku 1980).
Konečně se dostávám k ncurses.
#include <ncurses.h> int main() { /* inicializuj ncurses a aktivuj alternativni obrazovku */ initscr(); /* vypnuti echa, radkoveho bufferu, a zneviditelneni kurzoru */ noecho(); cbreak(); curs_set(0); /* vymaz obrazovku */ clear(); /* na 3 radek 10 sloupec defaultni obrazovky tiskni retezec */ mvprintw(3, 10, "AHOJ"); /* zobraz obsah obrazovky na terminal */ refresh(); (void) getchar(); /* uklid */ endwin(); }
I vůči předchozímu příkladu je použití ncurses neskutečný pokrok. Kód je mnohem čitelnější, a měl jsem ho napsaný za pár minut.
Překlad není také nijak složitý (musíte mít nainstalovaný devel balíček pro knihovnu ncurses).
[pavel@localhost ~]$ gcc test-ncurses.c -lncurses -o test-ncurses [pavel@localhost ~]$ ./test-ncurses
Nicméně originální knihovna (curses) se z různých důvodů příliš neujala (co čtu na wiki - tak tam byla určitá funkční omezení, a také tam byly nějaké problémy s licencováním). Došlo k několika reimplementacím. pcurses Pavla Curtise z roku 1982 je přímým předkem ncurses z roku 1993. PDCurses (Public Domain curses) Marka Hesslinga (snad rok 1987) se dodnes používá na jiných než Unix platformách jako je DOS, OS/2, MS Windows konzole (ale také na XWindows nebo SDL). Rozumně napsaná aplikace pro ncurses běží s drobnými úpravami (mají jinak umístěné hlavičkové soubory) i na PDCurses. Je to jedna z cest, jak psát multiplatformní aplikace. I když asi dnes bych si zvolil nový port Turbo Visionu, který vypadá hodně hezky, je rychlý a hlavně podporuje UTF8. Na druhou stranu, někdy se chcete chytřejším frameworkům vyhnout (z různých důvodů - závislosti, rychlost startu, paměťová náročnost). Takže stejně skončíte u ncurses nebo u escape sekvencí.
Výhodou ncurses není jen přehlednější zápis aplikace a pohodlnější programování. ncurses se snaží optimalizovat objem dat
posílaných na terminál, a snaží se redukovat problém s nestabilním obrazem (flickering). Komplexnější aplikace jsou většinou
napsané tak, že v cyklu překreslí obrazovku, a čekají na interakci uživatele. V ncurses se nezapisujeme přímo na standardní
výstup, ale zapisujeme do interního bufferu ncurses, který simuluje virtuální obrazovku. Příkaz refresh
porovnává aktuální
a předchozí virtuální obraz, a na standardní výstup (a přes něj do terminálu), pošle příkazy, které upraví stávající obsah
terminálu do požadovaného nového obsahu. Není to úplně hloupé. Na základě porovnání dvou obsahů je knihovna ncurses schopná samostatně
vygenerovat příkazy pro odskrolování obsahu, a potom na terminál poslat pouze tu část obrazovky, která je na terminálu nově. V době
hardwarových terminálů to muselo mít velký význam. Svůj význam to může mít i dnes. Když si přes ssh pustím v terminálu ncurses
aplikaci, tak tato optimalizace pomáhá.
Navíc tím, že v ncurses se nezapisuje přímo na standardní výstup ale do interního bufferu, který se aktualizuje jedním příkazem, tak se redukuje problém s problikáváním obrazu (flickering). Ten je způsobený tím, že obnova (refresh) obrazovky není sesynchronizována s aplikací. Dodnes terminály (ani ty softwarové) nepodporují double buffering. Pomocí ncurses je double buffering alespoň částečně emulován.
Zpracování vstupů
Vstup z tty (inicializace ncurses)
Ve velké většině případů se knihovna ncurses inicializuje voláním funkce
initscr
. V tomto případě se vstup čte ze standardního vstupu stdin
, a výstup
zapisuje na standardní výstup stdout
. V případech, kdy jsou tyto soubory
asociovány se zařízením typu tty
nebo pty
, tak vše bude fungovat jak
očekáváme. Pokud si ale budete chtít napsat pager nebo filter, tak tam narazíte.
Standardní vstup bude výstupem předchozího příkazu v koloně a nikoliv soubor
asociovaný s terminálem.
Když jsem začínal s pspg
, tak jsem ncurses neměl zažité, a docela dlouho jsem
vymýšlel, jak tento problém vyřešit. První funkční řešení, které jsem dlouho
používal, byla taková opičárna - po načtení vstupních dat jsem použil funkci
reopen
nad standardním vstupem a dostupným terminálovým zařízením:
if (!isatty(fileno(stdin))) { if (freopen("/dev/tty", "r", stdin) != NULL) noatty = false; else if (freopen(ttyname(fileno(stdout)), "r", stdin) != NULL) noatty = false; else { /* * cannot to reopen terminal device. See discussion to issue #35 * fallback solution - read keys directly from stderr. Just check * it it is possible. */ if (!isatty(fileno(stderr))) { fprintf(stderr, "missing a access to terminal device\n"); exit(EXIT_FAILURE); } noatty = true; fclose(stdin); } }
Výše uvedený kód na spoustě unixů funguje, ale není to nic hezkého. Určitě není
dobrý (a obvyklý) nápad otevírat standardní vstup v běžné aplikaci. To je práce
pro shell. Navíc je to úplně zbytečné. Přímo v ncurses máme jednoduché a
elegantní řešení. Nejdříve si člověk musí uvědomit, že ncurses "jen" čte ze
souboru nebo zapisuje do souboru. Typicky těmi soubory je stdin
a stdout
, ale
mělo by to fungovat pro všechny soubory. Jen zjistit jak (stačilo si přečíst
manuálovou stránku pro initscr
, ale kdo čte dokumentaci).
Fígl je v inicializaci ncurses funkcí newterm
namísto initscr
:
#include <ncurses.h> #include <stdlib.h> #include <unistd.h> int main() { FILE *f_tty = NULL; SCREEN *term = NULL; #ifndef __APPLE__ f_tty = fopen("/dev/tty", "r+"); #endif if (!f_tty) { f_tty = fopen(ttyname(fileno(stdout)), "r"); if (!f_tty && isatty(fileno(stderr))) f_tty = stderr; } if (!f_tty) { fprintf(stderr, "cannot to open tty device\n"); exit(1); } term = newterm(termname(), stdout, f_tty); if (!term) { fprintf(stderr, "cannot to initialize ncurses screen\n"); exit(1); } mvprintw(10, 10, "press ENTER"); getch(); endwin(); delscreen(term); }
Bohužel na macOS je implementace /dev/tty
polofunkční, což jsem zjistil i já
(nedá se použít ve funkci poll
), a nedoporučuje se moc používat (tato chyba je
známá minimálně od roku 2005).
ncurses umožňuje v rámci jedné aplikace vytvoření a používání více terminálů.
Voláním funkce set_term
mohu pak volit jeden aktivní. Nenašel jsem žádnou
aplikaci, nad ncurses, která by toto umožňovala. Například emacs
podobný režim
podporuje.
Ladění, ladící výstup
Když jsem začínal s ncurses, tak jsem hodně bojoval s laděním aplikace. Jelikož
je standardní výstup stdout
stejně jako standardní chybový výstup stderr
směrován na stejné tty zařízení, tak jakýkoliv pokus o tisk na stderr
rozhodí
layout aplikace. Nakonec jsem skončil u explicitně vytvořené pojmenované roury,
kterou používám pro ladící výpisy. Pokud spustím pspg
v debug režimu, tak si
tuto rouru otevřu a tisknu do ní. Šlo by to ale i jednodušeji - systémověji.
Otevřu si další terminál, a příkazem tty
si vypíšu cestu k souboru terminálu.
Poté při spuštění aplikace přesměruji chybový výstup do tohoto souboru:
sh [pavel@localhost ~]$ tty /dev/pts/8
V jiném terminálu:
sh mojeapp ... 2>/dev/pts/8
V aplikaci pak mohu používat stderr
bez omezení.
Vstup znaků (vstup širokých znaků)
Za nenápadnou funkcí getch
se skrývá hodně funkcionality. Tato funkce nevrací
jen kód aktuálně stisknuté klávesy, ale řeší dekódování vstupních escape
sekvencí, řeší ošetření signálu SIGWINCH
(změna velikosti terminálu), a v
kombinaci s nastavením timeoutu (funkce timeout
) umožňuje volbu blokujícího
nebo neblokujícího čtení. Dnes už je tato funkce překonaná, protože nepodporuje
vstup širokých znaků (u nás například znaků s diakritikou v UTF). Místo této
funkce by se měla používat funkce get_wch
. Ta vyžaduje kompilaci a linkování
vůči wide charové verzi ncurses ncursesw
. Pokud nezbytně nutně nepotřebujete
ošetřovat široké znaky, tak můžete použít #ifdef
:
Historicky bylo možné nastavit numerickou klávesnici (keypad) do dvou režimů.
V numerickém vracela čísla, v aplikačním pak escape sekvence odpovídající kurzorovým
šipkám. Možná touto funkcionalitou se inspirovali autoři ncurses, když psali funkci
keypad
, která mění konfiguraci obsluhy funkčních kláves. Ve výchozím nastavení
keypad(FALSE)
se přijatá posloupnost escape znaků ^[ [ A
posílá aplikaci jako
tři přijaté znaky. Pokud nastavíme keypad(TRUE)
, pak zmíněnou escape sekvenci
ncurses rozpozná a nahradí svým kódem KEY_UP
.
/* * chceme podporu sirokych znaku, obycejne se * nastavi autoconfem. Musi byt nastaveno pred * include ncurses.h */ #define NCURSES_WIDECHAR 1 #include <ncurses.h> #include <locale.h> static bool _getch(int *c) { #if NCURSES_WIDECHAR > 0 wint_t ch; int ret; ret = get_wch(&ch); *c = ch; return ret == KEY_CODE_YES; #else /* * V ASCII verzi nemohlo dojit k prekryti kodu znaku * a specialnich kodu ncurses (jako KEY_UP, ..). */ *c = getch(); return true; #endif } int main() { int eventno = 0; int c; bool is_key_code; setlocale(LC_ALL, ""); initscr(); /* necekej na ENTER */ cbreak(); /* neviditelny kurzor */ curs_set(0); /* neviditelne znaky pri psani */ noecho(); /* chci aby hlavni obrazovka skrolovala */ scrollok(stdscr, TRUE); /* * bez aktivniho keypad modu nedochazi k dekodovani escape sekvenci, * kurzorove klavesy, funkcni klavesy, ... */ keypad(stdscr, TRUE); move(0,0); is_key_code = _getch(&c); while (c != 'q') { if (is_key_code) printw("%4d: key: %s\n", ++eventno, keyname(c)); else printw("%4d: c: %lc (%d)\n", ++eventno, c, c); refresh(); is_key_code = _getch(&c); } endwin(); }
Funkční klávesy
Práce s funkčními klávesami je jednoduchá - pomocí makra KEY_F
můžeme jednoduše
vygenerovat kódy pro jednotlivé funkční klávesy. Pokud při stisku funkční klávesy
stiskneme SHIFT, tak pracujeme jakoby s druhou řadou funkčních kláves
F13 - F24. Tady pak dochází k jakési schíze - např. pro
kombinaci kláves, kterou označujeme jako SHIFT+F3,
používáme kód KEY_F(15)
.
Přiznám se, že netuším, jestli dále popisované chování je standard nebo pouze
specifikum mc
midnight commandera. Stisk funkčních kláves může být emulován
stiskem klávesy ALT a číslo nebo posloupností kláves ESCAPE
a číslo. Toto chování ncurses neimplementuje, ale je možné je implementovat
aplikačně (je to například implementované v pspg
, protože mi to přijde jako
šikovný nápad).
#include <ncurses.h> int main() { int eventno = 0; int c; bool alt = false; initscr(); cbreak(); curs_set(0); noecho(); scrollok(stdscr, TRUE); keypad(stdscr, TRUE); move(0,0); c = getch(); while (c != 'q') { switch (c) { case 27: /* ESCAPE */ if (alt) { alt = false addstr("zadost o ESCAPE\n"); } else alt = true; break; case '2': if (alt) addstr("stisknuta klaveska F2\n"); break; case KEY_F(2): addstr("stisknuta klavesa F2\n"); break; case KEY_F(14): addstr("stisknuta klavesa SHIFT+F2\n"); break; default: addstr("stisknuto neco jineho\n"); } if (c != 27) alt = false; refresh(); c = getch(); } endwin(); }
Speciální klávesy
Pro základní speciální klávesy má ncurses definované speciální kódy jako např.
KEY_UP
, KEY_DOWN
atd. Pokud chceme detekovat stisk těchto kláves zároveň se
stisknutou klávesou CTRL případně ještě se SHIFTem, tak už
je nutné dynamicky získat kód příslušné kombinace kláves. Nejdříve si musíme
sestavit identifikátor kombinace kláves. Například
kEND6
je kombinace kláves CONTROL+SHIFT+END. K
tomuto identifikátoru si můžeme vytáhnout z databáze terminfo odpovídající
sekvenci, a následně si voláním funkce key_defined
můžeme zjistit kód
definovaný pro tuto sekvenci znaků. Jelikož zmíněné funkce patří k rozšíření
ncurses, je praktické volání těchto funkcí vložit do #ifdef
a počítat s
náhradním řešením, když tyto funkce nebudou dostupné. Na rozšířených platformách
jsou tyto kódy stejné.
static int get_code(const char *capname, int fallback) { #ifdef NCURSES_EXT_FUNCS char *s; int result; s = tigetstr((NCURSES_CONST char *) capname); if (s == NULL || s == (char *) -1) return fallback; result = key_defined(s); return result > 0 ? result : fallback; #else return fallback; #endif } /* * Set a value of CTRL_HOME and CTRL_END key codes. These codes * can be redefined on some plaforms. */ void initialize_special_keycodes() { #ifdef NCURSES_EXT_FUNCS use_extended_names(TRUE); #endif CTRL_HOME = get_code("kHOM5", 538); CTRL_END = get_code("kEND5", 533); CTRL_SHIFT_HOME = get_code("kHOM6", 537); CTRL_SHIFT_END = get_code("kEND6", 532); }
ALT
Kombinace kláves CONTROL + znak, je mapována do intervalu 1..27. Takže, když chci
detekovat stisk CTRL + O, tak se podívám do ASCII tabulky, kde dohledám, že tato
kombinace kláves má kód 15. S ALTem to funguje úplně jinak. Při stisku kláves ALT
+ O, tak ncurses vygenerují dva kódy: ESCAPE (27) a O. V aplikačním kódu je nutné
tuto dvojici detekovat. V pspg
volám get_wch
v samostatném cyklu, který mi
umožní opakované volání, pokud získaná událost není validní nebo je ESCAPE. Tím
jak je to navržené a implementované, tak místo klávesy ALT můžeme používat
klávesu ESCAPE (jen jde o posloupnost kláves, nikoliv o současné stisknutí, tj
ALT + O = ESCAPE, O). ESCAPE, ve smyslu přerušení nějaké operace (klasický MSDOS
ESCAPE), je definován dvojím stisknutím klávesy ESCAPE (prvním stiskem, jak už
bylo řečeno, se přepínám do alternativní klávesnice). Kombinace kláves CTRL+ALT+O
vygeneruje kódy ESCAPE a 15 (^O).
ALT, potažmo ESCAPE se také může používat jako náhrada funkčních kláves F1 až F10. To se hodí, když vám některé funkční klávesy "sežere" terminál. Například F10aktivuji menu v Gnome Terminálu, a tudíž stisk F10 se k aplikaci, která mi běží v terminálu nedostane. Pokud to aplikace implementuje (je to aplikační záležitost, která musí obsluhovat ALT+0), tak mohu zkusit alternativu ALT+0 nebo ekvivalent ESCAPE, 0.
Použití knihovny readline
Občas může být potřeba editovat řetězec. V ncurses máme funkci getstr
. Určitě
tím uživatelům umožníte editaci, ale asi je neoslníte. Editace je dost
primitivní. Nefungují kurzorové klávesy, mazaní jde jedině zprava backspacem.
Když to porovnáte z možnostmi, které máme dnes v bashi, tak to zamrzí. A rovnou
vás napadne, proč nepoužít tu samou knihovnu readline
. Při podrobnějším
zkoumání se ukáže, že to není až tak jednoduché. Knihovna readline
je navržená
pro použití v REPL aplikacích, nikoliv v celoobrazovkových aplikacích. Nicméně
obsahuje funkce, které umožňují používat tuto knihovnu, aniž by měla přímý
přístup ke vstupu a výstupu (alternative callback interface). Vůbec netuším, proč
je toto API tak zbytečně složitě navržené (ale je fakt, že o této knihovně skoro
nic nevím):
/* For wcwidth() */ #define _XOPEN_SOURCE 700 #include <locale.h> #include <ncurses.h> #include <readline/readline.h> #include <stdlib.h> #include <string.h> #include <wchar.h> static int readline_proxy; static bool readline_proxy_is_valid = false; char *string = NULL; /* * Zkopiruje znak z proxy do readline */ static int readline_getc(FILE *dummy) { readline_proxy_is_valid = false; return readline_proxy; } static int readline_input_avail(void) { return readline_proxy_is_valid; } /* * Touto funkci readline predava vysledek editace */ static void readline_callback(char *line) { free(string); string = NULL; if (line) string = strdup(line); } /* * Chceme mit zobrazeni ve vlastni rezii */ static void readline_redisplay() { /* do nothing here */ } /* * Vrati (zobrazovaci) sirku retezce. */ static size_t strnwidth(const char *s, size_t n) { mbstate_t shift_state; wchar_t wc; size_t wc_len; size_t width = 0; size_t ch_width; memset(&shift_state, '\0', sizeof shift_state); for (size_t i = 0; i < n; i += wc_len) { wc_len = mbrtowc(&wc, s + i, MB_CUR_MAX, &shift_state); if (!wc_len) return width; if ((wc_len == (size_t) -1) || (wc_len == (size_t) -2)) return width + strnlen(s + i, n - i); if (iswcntrl(wc)) width += 2; else if ((ch_width = wcwidth(wc)) > 0) width += ch_width; } return width; } int main() { int c = 0; bool alt = false; setlocale(LC_ALL, ""); initscr(); cbreak(); noecho(); /* ENTER neni \n */ nonl(); /* * readline vyzaduje neprekodovany vstup tj * vypnuty keypad a cteni vice bajtovych * znaku po bajtech (emuluje se binarni * cteni ze souboru v aktualnim kodovani) */ keypad(stdscr, FALSE); /* * Instalace hooku - pokud se pouzije rl_getc_function, * tak by se mel VZDY pouzit i rl_input_available_hook. */ rl_getc_function = readline_getc; rl_input_available_hook = readline_input_avail; rl_redisplay_function = readline_redisplay; /* * Nechceme obsluhu signalu v readline, a nechceme tab * complete (neni nakonfigurovano, hrozi pady. */ rl_catch_signals = 0; rl_catch_sigwinch = 0; rl_inhibit_completion = 0; /* Zahajeni editace */ rl_callback_handler_install(": ", readline_callback); /* Vlozi vychozi (default) text */ rl_insert_text("Editaci ukonci 2x stisk ESCAPE"); while (1) { int cursor_pos; clear(); mvaddstr(10, 10, rl_display_prompt); addstr(rl_line_buffer); if (string) { mvaddstr(12, 6, "text: "); attron(A_REVERSE); addstr(string); attroff(A_REVERSE); } /* nastav kurzor */ cursor_pos = strnwidth(rl_display_prompt, SIZE_MAX) + strnwidth(rl_line_buffer, rl_point); move(10, 10 + cursor_pos); refresh(); c = getch(); /* ignoruj tabelatory */ if (c == '\t') continue; if (c == 27) { if (alt) break; else alt = true; } else alt = false; readline_proxy = c; readline_proxy_is_valid = true; /* posli echo readline, ze jsou nova data k precteni */ rl_callback_read_char(); } rl_callback_handler_remove(); endwin(); }
S mírnými úpravami byl výše uvedený příklad přeložitelný i s knihovnou
editline
, která by mohla být zajímavou alternativou zejména díky BSD licenci.
Tato knihovna implementuje podmnožinu API knihovny readline
, nicméně zrovna
callback API nefunguje dobře. (A stávající správce mi napsal, že ho to vůbec
nepřekvapuje). Proměnná rl_point
není aktualizována a stále obsahuje 0 (což je
evidentně chyba knihovny). Podařilo se mi zadat i českou diakritiku, ale bylo
potřeba knihovně předávat široké znaky (a s těmi zase nefungovala knihovna
readline
). U editline
není podporován hook rl_input_available_hook
(a ani
není nutný, jelikož místo více bajtových znaků knihovna vyžaduje široké znaky). U
readline
je jeho použití nutnost (jinak dojde k zacyklení při zadání více
bajtového znaku).
Všimněte si, že zde již používám "pokročilejší" posloupnost operací - napřed zobrazím obsah, a teprve potom čekám na stisk klávesy. Jednoduché ukázky, a školní příklady většinou mají tyto operace prohozené. Začíná se stiskem klávesy, a pak podle stisknuté klávesy se kreslí výstup. Zřejmou nevýhodou je prázdná obrazovka při prvním čekání na stisk klávesy.
Při práci s knihovnou readline jsem se setkal ještě s jedním docela "zákeřným"
problémem. readline vyžaduje vypnutý keypad režim. Naopak v normální aplikaci
budeme asi chtít mít keypad zapnutý. Jelikož se v ncurses keypad nastavuje na
okno, tak jsem na standardním okně stdscr
keypad povolil, a v okně, které jsem
používal pro vstup, jsem měl keypad vypnutý. Skoro to fungovalo, až na případy,
kdy první událost byl stisk kurzoru (čehokoliv, co funguje pouze se zapnutým
keypadem). Tady se ukázala další vlastnost ncurses. Strukturu WINDOW
do jisté
míry můžeme chápat i jako konfiguraci terminálu. V ncurses vždy "čteme" data z
okna. Nic takového reálně neexistuje, data čteme z tty. Okna jsou pouze abstrakce
ncurses. Při čtení se nastaví příslušná konfigurace (pro dané okno). Pokud je
viditelný kurzor nebo echo, tak se nastaví kurzor a čeká se na znak. Po získání
znaku se nastavuje konfigurace hlavního okna (si myslím).
Pokud používáte funkci poll
nebo select
(pokud se na událost čeká ve vašem
kódu), tak skoro vždy událost v terminálu vzniká ještě před tím, než ncurses
nakonfiguruje terminál podle konfigurace odpovídající použitému oknu), a může se
stát, že událost neodpovídá požadované konfiguraci (požadovanému oknu). Nejedná
se o chybu. Je to vlastnost architektury. Když plánujete psát komplexnější
terminálovou aplikaci, tak nejlepším začátkem je, si nastudovat možnosti a funkce
nějakého jednoduššího terminálu (VT52 nebo VT100) a vaši aplikaci psát jakoby pro
tento terminál - externí krabici s klávesnicí připojenou na sériový port
počítače. Případně jít ještě dál. Pracovat s mentálním modelem: "čtu data ze
souboru, který mi krmí klávesnice, a zapisuji do souboru, kterým krmím tiskárnu".
Pro vývojáře, který v životě nepracoval na fyzickém terminálu to není moc
přirozené. Na druhou stranu, tato architektura má něco do sebe. Jednoduše a
hlavně naprosto přirozeně umožňuje vzdálenou správu, jednoduše umožňuje
zanořování aplikací - např. aplikace jako screen
, tmux
atd jsou díky tomu v
podstatě velice jednoduše realizovatelné aplikace.
Používání myši
Implementace obsluhy událostí generovaných myší v ncurses neměla nejlepší pověst.
Narazil jsem na pár aplikací, které ačkoliv používaly ncurses, tak myš
obsluhovaly pomocí knihovny gpm
. Já jsem u pspg
zůstal s obsluhou myši v
ncurses, ale něco málo jsem si musel přeprogramovat (double click), a pro režim,
kdy pohybuji s myší a zároveň držím tlačítko, jsem musel použít rozšířený režim,
který není nativně podporován ncurses (musím ho zapínat, vypínat escape
sekvencí).
Události, které generuje myš (pohyb, stisknutí tlačítka, rotace kolečka),
primárně zpracovává terminál (emulátor terminálu). Podle aktuálního režimu na
tuto událost sám zareaguje (v primární obrazovce (primary screen) zobrazením
historie) nebo vygeneruje escape sekvenci, kterou pošle aplikaci (v alternativní
obrazovce). Tady za nás ncurses udělá docela dost práce - escape sekvenci převede
na událost KEY_MOUSE
, kterou získám funkci getch
a připraví datovou strukturu
MEVENT
(obsahuje polohu kurzoru myši, a bitmapu s kombinací stisknutých
tlačítek myši a vybraných kláves na klávesnici). MEVENT
získáme voláním funkce
getmouse
. U této funkce je dobré kontrolovat vrácenou hodnotu. Stalo se mi, že
ncurses špatně detekoval událost, a teprve při dekódování celé escape sekvence,
se zjistilo, že poslední událost byla zmatečná. Funkce getmouse
také může
vrátit ERR
pokud k získání události používám wgetch
, přičemž kurzor myši byl
mimo dané okno. Souřadnice kurzoru jsou vždy vztažené k celé obrazovce. Voláním
funkce wenclose
můžeme zjistit, zdali byl kurzor myši uvnitř okna, a použitím
funkce wmouse_trafo
získáme souřadnice kurzoru vztažené k počátku zadaného
okna.
Starší ncurses ještě nepodporovaly kolečko na myši, a bohužel se ještě s touto
verzí ncurses často potkávám. Tudíž vždy je nutné používat #ifdef NCURSES_MOUSE_VERSION > 1
,
pokud testujete tlačítka 4 a 5 (což odpovídá pohybu kolečka od sebe nebo k sobě).
Je to například problém RHEL7 s ncurses 5.9 z roku 2014. A firem, které jsou
ještě na této verzi RHELu je pořád dost.
#include <ncurses.h> int main() { int eventno = 0; int c; initscr(); cbreak(); curs_set(0); noecho(); scrollok(stdscr, TRUE); /* * bez aktivniho keypad modu nedochazi k dekodovani * vstupnich escape sekvenci, vcetne mysi, musi byt TRUE */ keypad(stdscr, TRUE); move(0,0); mousemask(BUTTON1_PRESSED | BUTTON1_RELEASED | BUTTON1_CLICKED | BUTTON1_DOUBLE_CLICKED #if NCURSES_MOUSE_VERSION > 1 | BUTTON4_PRESSED | BUTTON5_PRESSED #endif , NULL); c = getch(); while (c != 'q') { if (c == KEY_MOUSE) { MEVENT mevent; int res; if (getmouse(&mevent) == OK) { printw("%4d: key: %s, y: %d, x: %d", ++eventno, keyname(c), mevent.y, mevent.x); /* * tam, kde to neni nutne se nedoporucuje pouzivat * funkci print, ktera je vyrazne narocnejsi na CPU */ if (mevent.bstate & BUTTON1_PRESSED) addstr(", BUTTON1_PRESSED"); if (mevent.bstate & BUTTON1_RELEASED) addstr(", BUTTON1_RELEASED"); if (mevent.bstate & BUTTON1_CLICKED) addstr(", BUTTON1_CLICKED"); if (mevent.bstate & BUTTON1_DOUBLE_CLICKED) addstr(", BUTTON1_DOUBLE_CLICKED"); #if NCURSES_MOUSE_VERSION > 1 if (mevent.bstate & BUTTON4_PRESSED) addstr(", BUTTON4_PRESSED"); if (mevent.bstate & BUTTON5_PRESSED) addstr(", BUTTON5_PRESSED"); #endif addch('\n'); } else addstr("broken mouse event\n"); } else printw("%4d: key: %s\n", ++eventno, keyname(c)); refresh(); c = getch(); } endwin(); }
Při zpracování události od myši ncurses čeká ve výchozím nastavení 250 ms
(nastavuje se funkcí mouseinterval
) na případnou další událost od myši, a podle
toho jestli přijde nebo nepřijde nastaví flag PRESSED
nebo CLICKED
. Mne to v
pspg
způsobovalo nepříjemné jakoby opožděné reakce na myš. Přeci jen 250ms už
si všimnete. Takže jsem si nastavil mouseinterval(0)
, čímž jsem vyblokoval
generování flagu CLICKED
, a na flag PRESSED
jsem napojil získání fokusu, a na
flag RELEASED
pak provedení akce. Tím pspg
uživatele nenutí mít příliš rychlé
prsty, a zároveň ovládání pspg
není "líné"
V mc
se mi líbí, že uživatel může pohybovat myší, a držením levého tlačítka mít
neustále zvýrazněný fokus. Dost dlouho mi trvalo, jak toho v ncurses docílit.
Sice si mohu nastavit masku myši REPORT_MOUSE_POSITION
, ale to samo od sebe nic
neudělá. Je potřeba na terminálu vybrat některý z protokolů pro sledování myši
"xterm Mouse Tracking", a pro tento účel mi vyhovoval protokol označený číslem
1002. Zapíná se escape sekvencí \033[?1002h
a vypíná \033[?1002l
(flag
REPORT_MOUSE_POSITION
musí být aktivní).
Velikost terminálu
Nejstarší terminály měly fixní konstantní velikost danou typem terminálu. Na
základě údajů ze své databáze vlastností jednotlivých terminálů pak ncurses
nastaví proměnné LINES
a COLS
. U terminálů, které jsou emulovány softwarově
tento přístup není možný. Jejich velikost je dynamická, a proto by pseudoterminál
při své inicializaci měl nastavit systémové proměnné $LINES
a $COLUMNS
,
odkud hodnoty převezme ncurses, které nestaví své proměnné LINES
a COLS
. U
softwarově emulovaných terminálů běžně dochází ke změně velikosti i v průběhu
běhu aplikací, tudíž by terminálová aplikace měla ošetřit signál SIGWINCH
,
který je generován terminálem při změně velikosti.
Zde nastávají první komplikace, kdy některé aplikace neošetřují vše, co by měly.
Mějme posloupnost aplikací bash
, psql
a pspg
. Pokud by bash
neošetřoval
SIGWINCH
, tak se může stát, že systémové proměnné $LINES
a $COLUMNS
nebudou
obsahovat aktuální hodnoty. Zrovna tak se může stát, že ačkoliv pspg
správně
ošetřuje SIGWINCH
, tak nedokáže nastavit zmíněné systémové proměnné vnějšímu
procesu, a po ukončení pspg
jsou tyto proměnné opět neaktuální. Chování ncurses
můžeme ovlivnit voláním konfiguračních funkcí use_env
a use_tioctl
. V
závislosti na konfiguraci ncurses bere v potaz systémové proměnné $LINES
a
$COLUMNS
, případně je ignoruje a velikost terminálu si zjišťuje samotná
knihovna.
V moderních systémech a v moderních ncurses se můžete spolehnout na vestavěnou (v
ncurses) obsluhu signálu, a na obsah proměnných LINES
a COLS
(po změně
velikosti aplikace dostane událost KEY_RESIZE
. U starších systémů jsem se
setkal s tím, že tyto proměnné při startu aplikace, případně po změně velikosti
okna nebyly aktuální, a musel jsem si je načíst sám. Jinak KEY_RESIZE
funguje
asi všude. Správné automatické nastavení LINES
a COLS
funguje tak na 99.9%
(ale je možné, že bych si možná pomohl laděním use_env
a use_tioctl
, a možná
také ne. ioctl
mi fungovalo všude):
#include <stdio.h> #include <stdlib.h> #include <ncurses.h> int main() { int c = ' '; initscr(); noecho(); cbreak(); curs_set(0); while (c != 'q') { int lines, cols; mvprintw(9, 10, "keyname: %s", keyname(c)); if (c == KEY_RESIZE) { /* velikost terminalu */ mvprintw(10, 10, "terminal: %d, %d", LINES, COLS); /* velikost hlavniho okna */ getmaxyx(stdscr, lines, cols); mvprintw(11, 10, "stdscr: %d, %d", lines, cols); } refresh(); c = getch(); } endwin(); exit(EXIT_SUCCESS); }
V případě, že si sami ošetřujete signál SIGWINCH
, je nutné o změně velikosti
terminálu informovat ncurses:
#include <stdio.h> #include <stdlib.h> #include <ncurses.h> #include <signal.h> #include <sys/ioctl.h> #include <termios.h> static bool handle_sigwinch = false; static void SigwinchHandler(int sig_num) { handle_sigwinch = true; signal(SIGWINCH, SigwinchHandler); } int main() { int c = ' '; initscr(); noecho(); cbreak(); curs_set(0); /* nastavi timeout pro funkci getch() */ timeout(250); /* * tim, ze nastavuji signal po initscr, si * vynucuji vlastni obsluhu signalu */ signal(SIGWINCH, SigwinchHandler); while (c != 'q') { int lines, cols; if (handle_sigwinch) { struct winsize size; handle_sigwinch = false; if (ioctl(fileno(stdout), TIOCGWINSZ, (char *) &size) >= 0) resize_term(size.ws_row, size.ws_col); } /* pokud neni timeout */ if (c != -1) { mvprintw(9, 10, "keyname: %s", keyname(c)); clrtoeol(); } /* velikost terminalu */ mvprintw(10, 10, "terminal: %d, %d ", LINES, COLS); /* velikost hlavniho okna */ getmaxyx(stdscr, lines, cols); mvprintw(11, 10, "stdscr: %d, %d ", lines, cols); refresh(); /* maximalne po 250 ms skonci na timeout */ c = getch(); } endwin(); exit(EXIT_SUCCESS); }
Jelikož jsem přepsal ncurses obsluhu signálu, tak resize terminálu nepřeruší
provádění funkce getch
(signál spolkla moje aplikace, nikoliv ncurses), a abych
zachoval určitou interaktivitu, tak nastavuji timeout 250 ms. Všimněte si.
Aktuální velikost terminálu získám voláním funkce ioctl
, a volám funkci
resize_term
. Uvnitř této funkce se přepočítají (a přealokují) okna a nastaví
proměnné LINES
a COLS
. Tato funkce by se neměla volat uvnitř obsluhy signálu.
U jednodušších aplikací, když se potřebujeme vyhnout dlouhém blokujícímu čekání
na klávesnici, tak si můžeme pomoct nastavením timeoutu. Pozor ale, příliš krátký
timeout zbytečně zatěžuje CPU (a je trend od používání krátkých timeoutů
ustupovat, tak aby nedocházelo ke zbytečnému uspávání a probouzení procesu),
Poznámka: ačkoliv se tato funkce jmenuje resize_term
neznamená to, že s ní
můžete změnit velikost terminálu. Touto funkcí si vynutíte aktualizaci datových
struktur v ncurses, které jsou nějakým způsobem navázané na velikost terminálu.
Změnu velikosti terminálu si lze vynutit escape sekvencemi (na některých
terminálech, určitě ne všude). Například pro nastavení terminálu na velikost
100x50 znaků je sekvence \e[8;50;100t
Psaní aplikací pro terminál: výstup
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ý. V poslední části seriálu článku o
ncurses bych se zaměřil na výstup. Spíš než, abych systematicky prošel
výstupní API, se budu věnovat tématům se kterými jsem válčil, a kde jsem se
občas zasekl. 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 je
ASan
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ím. 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 10x10 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 [http://melvilletheatre.com/articles/ncurses-extended-characters/index.html rozšířené znakové sady]. Navíc, díky této funkci, kterou terminály poskytují, tak 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));
Pozor, některé terminály (např. terminál konsole
) neumožňují změnu barev. Podporu změny barev lze otestovat funkcí can_change_color()
.
Ve Fedoře jsem narazil na problém s výchozím nastavením proměnné TERM
na xterm-256color
, a to právě pokud se používal terminál konsole
, jelikož xterm
změnu barev podporuje a konsole
nikoliv. Pro korekní chování bylo nutné nastavit TERM
na konsole-256color
.
ncurses řady 6 podporují direct color mode. V tomto režimu je barva identifikována přímo rgb hodnotou. Barva se již nemusí definovat, ale její rgb kód se použije ve funkci init_extended_color
.
Tento režim lze použít pouze tehdy, pokud jej term info podporuje (např. xterm-direct
). Režim lze detekovat podle hodnoty COLOR
, která je nastavená na 16777216 (resp. 0x10000000). Ve starých
ncurses se pro identifikaci barvy používala hodnota o velikosti 1byte (short). Pro práci s rgb se požívají tzv extended funkce, kde barva je uložena v hodnotě typu int. Pro test, zda-li jsou
extended funkce pro práci s barvami lze použít konstantu NCURSES_EXT_COLORS
.
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říkaz update_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.
Závěr
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 tam 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.
Po zkušenostech, které teď mám, bych asi ncurses použil pouze jako plátno (canvas). Hodně jsem zápasil s chováním objektů v ncurses při změně velikosti terminálu. U fyzických terminálů změna velikosti nebyla možná, a v ncurses je několikrát implementována jakási heuristika, která pomáhala s provozem starších aplikací. Nejde vypnout, a já jsem s ní dost bojoval. Začátkem 90 let, kdy se bojovalo o každý kb v RAM nebo na disku mělo velký význam sdílet určitou funkcionalitu, to už dnes v TUI aplikacích nemá smysl.