Programujeme v C s ncurses
                           -=-=-=-=-=-=-=-=-=-=-=-=-=-=

                             Karel Žák, 25. října 1998

          Tento článek by měl být malou  rychlokuchařkou do světa ncurses.
      Snad  jen  na  začátek,   malé   vysvětlení  co  to  ncurses   jsou.
      Název  "ncurses"  je  složeninou  ze slov  "new  curses".  Jedná  se
      o volně  distibuovatelný  klon  System V Release 4.0 (SVr4)  curses.
      A jak  říkají  FAQ,  je to  slovní  hříčka k "cursor  optimization".
      Ale  jinak  (a  vážně)  jedná  se o  knihovnu k managamentu  výstupu
      na "character-cell terminals" tedy na znakově orientované terminály.

          Ale rychle k programování. Tato knihovna  není žádným  drobečkem
      (při  pohledu do adresáře  /lib je - alespoň na mém  disku - po libc
      tou  největší)  takže  snad  někteří  prominou  pokud se o některých
      možnostech  nezmíním.  Pokusím se zaměřit  pouze na to jak nenásilně
      a rychle dosáhnout "počmárání screenu ve stylu ncurses" :-)

                                    Inicializace

          Jako první by si měl program nainicializovat obrazovku do curses
      módu. To se provede  funkcí  initscr(). Po  provedení  této  funkce,
      by programátor  nikdy  neměl  zapomenout,  že po  ukončení  programu
      by vše mělo být jako před jeho  spuštěním.  Návrat z curses  módu se
      provede funkcí  endwin(). Pokud si nejste jisti, že jste tuto funkci
      zavolali tak se o tom můžete přesvědčit  pomocí isendwin().  Protože
      nikdy není tak úplně jisté kdy a kde program skončí, tak např. já to
      dělám tak, že si do atexit() přidám endwin(), tedy

      atexit((void *) endwin)


          Pokud se divíte,  proč taková  přemíra  starostí o endwin(), tak
      proto, že program,  který se ukončí bez zavolání  této funkce  nechá
      váš terminál v tom lepším  stavu.  Funkce  initscr()  vrací  pointer
      stdscr typu WINDOW (jedná se o globální  proměnnou, ke které ncurses
      vztahuje funkce, které nepoužívají okno jako parametr.

          Po inicializaci je možné si doopravit  některé další  vlastnosti
      screenu / terminálu. Například:

          curs_set() - nastavení kursoru
          echo()  /  noecho()  - mají  se  zobrazovat  znaky  přicházející
             z klávesnice (např. při volání funkce mvwgetstr())
          cbreak() - nastaven "line bufferingu"
          keypad() - nastavení používání kláves F1 až Fn

          U poslední  zmíněné  funkci snad ještě poznámka: pokud používáte
      ve svém  programu  WINDOW,  tak je nutné pro každé  nové  okno  tuto
      funkci volat, jinak při nepoužívání oken je možné zavolat na počátku

      keypad(stdscr, TRUE)


          Po přepnutí do curses módu jsou  dostupné dvě globální  proměnné
      (int)  LINES  a  COLS,  ve  kterých  je  uložena  aktuální  velikost
      obrazovky. Časté je např. pořadí inicializace:  initscr(), cbreak(),
      noecho() - pak už je  obrazovka  připravena ve stavu  "neřádkování",
      ale  naopak  ve  stavu  "kam  přesně  co  chceš"  (na  jakou   řádku
      a do jakého sloupce).

          Někdy  je  nutné   se  z  ncurses   programu   přepnout   nazpět
      do  původního  (v  terminologii  ncurses - shell)  módu. To je nutné
      například při volání  nějakého  externího  programu,  který  používá
      terminál  standardním  způsobem. K tomu  se používá  skupina  funkcí
      okolo reset_shell_mode() (více manuál).

          Pochopitelně, ze ncurses  obsahují  ještě  další  přepínače, ale
      to už by mohlo být otázkou  samostudia nad manuály :-) Podobně  jako
      záležitosti  týkající se možnosti  otevřít si vlastní  screen pomocí
      funkce newterm().

                                   Výstup a okna

          Už jsem se zmínil o WINDOW. Nové okno si vytvoříte pomocí funkce
      newwin().  Jako  parametry  se zadávají  poloha  okna a to  vzhledem
      k levému  hornímu  rohu a velikost  okna. Po vytvoření okna je možné
      s ním  zacházet  jako se samostatnou  podmnožinou a tedy  volat  pak
      funkce s parametry  řádek a sloupců  vzhledem k levému  hornímu roku
      okna.  Funkce  newwin()  vrací  pointer na nové  okno  (funkce  sama
      alokuje pomocí malloc strukturu WINDOW) proto se okno deklaruje jako
      pointer  (podobně jako FILE). Naopak uvolnění  paměti a zrušení okna
      se provede  funkcí  delwin().  Smazání ze screenu se provede  funkcí
      werase() (a pak je vhodné  wrefresh()). V okně může existovat i tzv.
      subwin,  jedná se to samé  jako  WINDOW, ale vzhledem k rodičovskému
      oknu  (narozdíl od okna,  které je definováno  vzhledem ke screenu).
      S  okny  lze  vytvářet   docela   zajímavé  věci.  Například  zapsat
      do souboru a pak naopak  načíst okno ze souboru.  Často je využívána
      i například funkce redrawwin(), která znova nakreslí okno na screen.
      Je možné s okny pohybovat pomocí mvwin(), duplikovat okna - dupwin()
      atd.

          Funkce zacházející s okny lze jednoduše  poznat dle prefixu mvw-
      nebo samotného w-. Takže obdoba klasického printf() je v ncurses při
      použití oken

      mvwprintw(okno, řádka, sloupec, "něco: %s", řetězec);

          (pozor na znak \n - v ncurses se dostáváme na další  řádku  tím,
      že zvýšíme "řádku" o jedna).

#include <curses.h>             /* stdio.h neni nutne je v curses.h */
#include <ctype.h>

#define COLOR2      1            /* barvicka 1. */
#define COLOR1      2            /* barvicka 2. */

int main () {
   int      c;
   char      *s;

   initscr ();
   cbreak ();
   noecho ();                  /* vypnuti echa */
   start_color ();                  /* chceme barvy */
   keypad (stdscr, TRUE);            /* chceme klavesy pod makry KEY_neco */
   curs_set(0);                  /* at tam ta mrska neblika */

   if (!has_colors ()) {            /* umi terminal barvy ? */
       endwin ();
       fputs ("Hmm.. tady barvy nejdou !", stderr);
       exit (1);
   }
   /*         barva - popredi   -       pozadi            */
   init_pair (COLOR1, COLOR_RED,       COLOR_BLUE);      /* barvicka 1. */
   init_pair (COLOR2, COLOR_YELLOW, COLOR_BLACK);      /* a druha */

   attron (COLOR_PAIR( COLOR1 ));                  /* pouzivat barvu 1. */
   mvaddstr (2, 5, "Cervene na modrem";);

   attron (COLOR_PAIR( COLOR2 ));
   mvaddstr (3, 5, "Zlute na cernem");

   attron (A_BOLD);                        /* od ted vse BOLD */
   mvaddstr (4, 5, "Zlute na cernem a tucne");

   attroff (COLOR_PAIR( COLOR2 ));                  /* vypne barvu */
   mvhline(LINES-2, 0, ACS_HLINE, COLS);            /* nakresli caru */
   mvaddstr (LINES-1, COLS-15, "F10 - konec");
   mvaddstr (10, 5, "Jmeno klavesy");

   while ( (c=getch()) != KEY_F(10)) {
           s = (char *) keyname(c);            /* jmeno klavesy ? */
           mvhline(10, 20, ' ', COLS);            /* smaz */
           if (s)
                 mvprintw (10, 20, "'%s'", s);
           else
                 mvprintw (10, 20, "'%c'", (isprint(c) ? c : '.'));
   }
   erase ();                        /* smaz nase vytvory */
   refresh ();
   endwin ();                        /* konec curses */
   exit (0);                        /* ....bye */
}


          Další  možností  jak  tisknout  na screen je  nevyužívat  WINDOW
      a využívat funkcí, které nepožadují WINDOW jako svůj parametr. Lines
      a cols v parametrech  těchto funkcí se pak vztahují k levému hornímu
      rohu screenu. Tyto funkce pak většinou  začínají  prefixem mv- např.
      mvaddstr(y,x, str).

          Ncurses  ještě  umožňují  používat  funkce  bez  parametrů  line
      a cols.  Takové  funkce pak vypisují svůj výstup na aktuální  pozici
      kursoru. Nutno říct, že tento  způsob je asi nejméně  programátorsky
      příjemný.  Takovouto funkcí je např. addstr(str). Jak je z uvedených
      příkladů   zřejmé,  tak  u  většiny  funkcí  existují   všechny  tři
      alternativy, tedy např. mvwaddstr, mvaddstr i addstr.

          V ncurses jsou pak ještě alternativy  těchto  funkcí  umožňující
      přesné  zacházení se řetězci  (podobně jako známé  stringové  strcpy
      a strncpy), tedy např. addstr() a addnstr().

          Pokud   potřebujete   tisknout   některé   speciální  znaky  tak
      doporučuji   podívat  se  na  man  addch,  kde  jsou  popsána  makra
      ASC_něco  obsahující tyto znaky (např.  čáry,  různé  šipky  apod.).
      Ke snadnějšímu kreslení čar existují i funkce hline() (horizontální)
      a vline()  (vertikální) a jejich  alternativy  pro  okna.  Často  se
      u oken  používají  rámečky.  Rámeček  můžete  nakreslit  pomocí výše
      zmíněných  funkcí a nebo  pomocí,  k  těmto  účelům  předdefinované,
      funkce wborder().

          Pro  přesnost  snad  ještě  poznámku  -  většina   funkcí  které
      nepožadují WINDOW jsou ve skutečnosti makra typu:

      #define mvaddstr(y, x, str) mvwaddstr(stdscr, y, x, str)


          Pro dobré  zacházení s výstupem ještě  doporučuji  prohlédnut si
      manuály k funkcím  refresh(),  wrefresh(),  redrawwin() atd. Co tyto
      dělají je myslím patrné už z jejich názvu.

          Zaznělo zde již i něco o pozici  kursoru, i tu lze  pochopitelně
      měnit. Pomocí wmove(y, x, win) v rámci okna a pomocí move() vzhledem
      ke screenu. Další a již zmíněnou možností je zapínaní resp. vypínaní
      cursoru a to pomocí curs_set(0) a curs_set(1).

          Možná se ptáte, co je lepší - používat v programu  okna nebo ne?
      Abych  upřímně  odpověděl  -  tak  nevím.  Používání  oken  s  sebou
      nese několik  výhod.  Části  výstupu  jsou  umístěny v podmnožinách,
      se  kterými  lze  samostatně   zacházet.   Například   pokud  jedním
      oknem  překryjete  druhé,  pak  snadno to překryté  obnovíte  pomocí
      redrawwin(),  zatímco  pokud okna  nepoužíváte tak tím, co na screen
      napíšete  nenávratně  přepíše  to, co tam  bylo  (na  dané  pozici).
      Ale při nepoužívání oken ve vašem programu ubude volání  malloc (při
      každém  newwin),  nebudete muset programem vláčet  struktury  WINDOW
      a pravděpodobně  program bude rychlejší. Na zamyšlení snad uvedu, že
      např. v Midnight Commanderu okna používána nejsou, já sám jsem (malá
      reklama :-)) např. v programu kim (manažer procesů) okna použil, ale
      v následující  verzi už nebudou. To vše, ale neznamená, že okna jsou
      něco nedobrého, jen je někdy dobré, pokud se program stará o některé
      věci sám, a i v programování platí, že méně je někdy více.

          Za výstup snad lze ještě  považovat to, co dělá  funkce  beep().
      Jak už je z názvu patrno, jedná se o zvukové znamení.

                                     Vstup

          Aby mohl program  "vnímat"  uživatele tak pochopitelně  knihovna
      obsahuje  funkce  vracející znaky nebo řetězce  zadané z klávesnice.
      Základní je getch() a při použití oken wgetch(). Pozor,  nezapomínat
      při používání oken volat po vytvoření okna funkci  keypad().  Těmito
      funkcemi  vrácená  hodnota (int) obsahuje znak nebo v případě stisku
      nějaké  speciální  (funkční)  klávesy v curses.h  nadefinované makro
      mající  prefix  KEY_,  např. při stisku  klávesy  "šipka  nahoru" je
      vráceno KEY_UP.

          Klávesu  je  možné  i  vrátit  (podobně  jako  u  souboru  znak)
      pomocí  ungetch().  Pochopitelně jsou definovány i funkce  vracející
      znak  nejen do programu, ale opisující ho také na screen  nebo  okno
      (pozor - nesmí být vypnuto echo pomocí  noecho()).  Např. mvgetch(),
      mwvgetch(). Také je možné pomocí has_key()  zjistit, zná-li terminál
      některou  klávesu (ta je jako parametr této  funkce) nebo  například
      jméno klávesy a to pomocí keyname().

          Pro přijetí celého řetězce lze využít funkce z rodiny  getstr().
      Tyto  načítají  všechny  zadané  znaky  až do stisku  klávesy  Enter
      do  řetězce.  Existují  pochopitelně i bezpečnější  varianty  těchto
      funkcí, tedy getnstr().

          Zajímavostí může být nastavení  timeoutů pro čtení klávesy. Toho
      lze efektivně  vyžít například k tomu, že během toho co program čeká
      na to až se uživatel rozhodne něco stisknout tak může např. testovat
      nějakou událost. Já to například používám pokud chci provést nějakou
      složitější a náročnější  reakci na signál, kdy není vhodné, aby tuto
      činnost vykonával  handler  signálu.  Nastavení  timeoutu se provede
      funkcí timeout() nebo wtimeout() s parametrem v milisekundách. Pokud
      do nastavené doby nedošlo k stisknutí  nějaké  klávesy  vrací  např.
      funkce getch() hodnotu ERR.

          Další  možností jak načítat  "něco" do vaší  aplikace je možnost
      číst ze screenu  nebo  okna  znak  nebo  řetězec  ležící na zadaných
      souřadnicích  a to  pomocí  rodiny  funkcí  inch a instr  (mvinch(),
      mvwinch(),  mvinstr()). U funkcí inch lze nejen zjistit znak na dané
      pozici, ale i jeho barvu a atributy.

                                       Barvy

          Chcete-li  používat ve svém  programu  barvy  musí  být  splněno
      několik   základních   podmínek.   Prvně  je nutné  po  inicializaci
      screenu  zavolat  funkci  start_color(),  kterou  sdělíte   ncurses,
      že máte takovýto úmysl. Podporuje-li  terminál barvy zjistíte funkcí
      has_colors().

          Jak bude vypadat výstup je možné nastavit  pomocí  attron() nebo
      pro okno watrron() a naopak  zrušit pomocí  attroff()  (wattroff()).
      Kde jako  parametr je uvádí  požadovaný  stav,  tedy  např.  A_BOLD,
      A_BLINK,  A_NORMAL atd. Od chvíle  nastavení  pomocí attron  všechen
      výstup  používá toto nastavení a to až do další změny pomocí  attron
      nebo do jeho zrušení pomocí attroff.

          Před  používáním  barev je nutné je  nadefinovat.  To se provede
      pomocí  init_pair(barva,  popředí,   pozadí).  Kde  barva  je  číslo
      a  popředí  (písmo)  a  pozadí  jsou  jedna z  nadefinovaných  barev
      (COLOR_RED,   COLOR_BLUE,   COLOR_YELLOW,  COLOR_GREEN,  COLOR_CYAN,
      COLOR_BLACK, COLOR_WHITE a COLOR_MAGENTA).

          Chceme-li  pak  použít  takto  nadefinovaných  barev   provedeme
      to zase  pomocí  attron(). A to  takto:  attron(COLOR_PAIR(barva));.
      Pochopitelně  lze  kombinovat i s bold.  Použití  bude  asi  nejlépe
      viditelné na příkladu a na jeho výstupu.

          Ncurses pochopitelně  dovolují i další operace s barvami, ale to
      je nad  rámec  tohoto  článku a lze to poměrně  pohodlně  nastudovat
      z manuálu.

                                       Resize

          Je to také  trochu  nad  rámec  tohoto  článku,  ale  protože je
      to  častým  dotazem  na  různých  konferencích,  tak to zde  zmíním.
      Pokud provozujete  ncurses  program pod xtermem, tak se běžně stává,
      že  uživatel  mění  velikost  okna  xtermu. Je tedy  vhodné  zjistit
      novou  velikost  LINES a COLS. O změně  velikosti  okna je  aplikace
      informována signálem  SIGWINCH. Občerstvit LINES a COLS můžete např.
      takto:

   struct winsize size;
   size.ws_row = size.ws_col = 0;

   ioctl(0, TIOCGWINSZ, &size);
   if (size.ws_row && size.ws_col) {
      LINES = size.ws_row;
      COLS = size.ws_col;
   }


                                        Myška

          Při důkladném  pročítání  manuálu  ncurses  zjistíte, že ncurses
      obsahují  funkce  getmouse() atd. Tyto funkce, ale nejsou v původním
      SVr4. Já osobně jsem těchto  funkcí nikdy nepoužil.  Stalo se zvykem
      používat gpm a jsem toho  názoru, že některé  zvyky jsou  dobré, tak
      proč je měnit. Knihovna gpm dokonce na programy s ncurses  pamatuje,
      takže jsou zde vytvořené alternativy getch() a wgetch() (Gpm_Getch()
      a Gpm_Wgetch()) pro používání myši. Každopádně gpm s ncurses pracuje
      bezvadně. Ale o gpm až někdy příště.

                                        Závěr

          Protože  některým  programátorům   nevyhovovaly  pouze  základní
      možnosti   ncurses,  tak  vznikla  řada  nástaveb.  Standardně  jsou
      s ncurses  distribuované  menu,  panel,  form. Je  otázkou  jak  moc
      jsou tyto různá  "udělátka"  šikovná a hlavně útlá a jestli  náhodou
      programátora  nesvazují a konečnému  produktu  nevtiskují tvář jakou
      chtějí oni.

          K  přiloženým  příkladům:  kompilace  programu  s  knihovnou  se
      provede např. takto:


      gcc -Wall -O3 program.c -o program -lncurses


          (to -Wall doporučuji,  často ukáže programátorům věci "netušené"
      :-) Ti dloubavější si možná v místech, kde se zmiňuji o mc vzpomněli
      na knihovnu slang. Ano, v současné době se tento program distribuuje
      kompilovaný s touto  knihovnou  (s  ncurses  to ale  jde  kompilovat
      také). Programy psané pro ncurses, lze po malých úpravách kompilovat
      i  s  knihovnou  slang.  Je  jen  nutné  místo  curses.h  includovat
      slcurses.h.

          Jako  další  studijní  materiál  doporučuji  obsáhlý  man curses
      a ještě (a to nejlépe) zdrojáky nějakého ncurses programu.

          Přiložený      příklad     lze    najít     včetně      Makefile
      v   adresáři  s   Linuxovými       novinami     nebo    na    adrese
      http://home.zf.jcu.cz/zakkr/LN/.


            výheň