Nachdem meine verehrte Leserschaft über meine Berichte über die Entwicklung von
newsbeuter das ncurses-UI-Toolkit
STFL zumindest vom Namen her kennt, möchte ich ein wenig mehr über die Eigenschaften und Features dieser genialen Library schreiben, da sie mir bisher sehr beim Erstellen von Konsolen-UIs geholfen hat.
Die Basis von STFL ist die Layout-Beschreibungssprache, mit der man die grundsätzliche Ausrichtung und Positionierung sämtlicher Controls inklusive (optional) des Inhalts und der Farbgebung. Diese Beschreibungssprache wird in eine Datenstruktur geparsed, die dann später zur Laufzeit einfach modifiziert und abgefragt werden kann. Ein wichtiges Prinzip des Layouting bei STFL sind dabei Horizontal und Vertical Boxes. Man stelle sich z.B. den gesamten Bildschirm vor, den man zuerst in mehrere Boxen "untereinander" teilt. In so eine Box kann man dann ein Control platzieren, oder eine weitere vbox (macht nicht sehr viel Sinn), oder aber eine hbox, welche die Box wiederum in mehrere Boxen nebeneinander teilt, usw. usf. Zusätzlich kann man jeder Box noch die Information mitgeben, in welche Richtung sie expandieren soll. Mit diesem simplen Konzept hat man ein mächtiges Werkzeug bei der Hand, um brauchbare und komplexe Layouts für Konsolen-UIs zu designen.
Zur Laufzeit wird diese Layoutbeschreibung dann eingelesen (entweder aus einer Zeichenkette oder aus einem externen File, ich persönlich verwende
dieses Skript um STFL-Layouts direkt in Programme einzubinden, aber trotzdem die Layouts in schönen externen Files im Sourcetree liegen zu haben), und die resultierende Datenstruktur wird dem Programmierer dann als opakes "stfl_form *" zur Verfügung gestellt. Dieses Handle kann dann verwendet werden, um den aktuellen Inhalt von Controls auszulesen, zu verändern, den aktuellen Fokus zu bestimmen und zu ändern, und auch um die Datenstruktur selbst zu modifizieren (damit ließe sich theoretisch zur Laufzeit das Layout umstellen, auch wenn das in der Praxis i.A. wenig zielführend ist). Und natürlich kann man das Formular starten und kriegt Benutzereingaben weitergereicht, auf die man entsprechend reagieren kann.
Eine derartige Layoutbeschreibung sieht übrigens so aus:
vbox
@style_normal:bg=black,fg=white
@info#style_normal:bg=blue,fg=yellow,attr=bold
label#info
text[head]:"Your feeds"
.expand:h
list[feeds]
style_normal:bg=black,fg=white
style_focus:fg=yellow,bg=blue,attr=bold
.expand:vh
pos_name[feedposname]:
pos[feedpos]:0
label#info
text[help]:"q:Quit ENTER:Open r:Reload R:Reload All A:Mark All Read C:Catchup All"
.expand:h
label
text[msg]:""
.expand:h
Das ist das Layout der Feedlist, wie sie bei newsbeuter beim Programmstart erscheint. Es besteht aus einer vbox, welches von oben nach unten ein Label für die Überschrift, eine Liste mit den Feeds, ein Label mit Keyboard Shortcuts sowie ein Label für Information und Fehler enthält, alles gespickt mit diversen Details wie Farbgebung und der Information für die Labels, dass sie horizontal expandieren sollen, sowie für die Liste, dass sie horizontal und vertikal expandieren soll (und damit den meisten Platz ausfüllt).
Insgesamt ist das Interface von STFL sehr kompakt (14 API-Funktionen). Die Konstruktion eines Programms erfolgt meistens nach dem gleichen Prinzip: Initialisierung - Event Loop - Ressourcenfreigabe. Die Initialisierung ist bei STFL eine ganz simple:
char * my_form = "...";
struct stfl_form * f1 = stfl_create(my_form);
struct stfl_form * f2 = stfl_create("<form.stfl>");
Der erste Schritt ist immer, ein Formular anzulegen, und zwar mit
stfl_create(). Dabei gibt es zwei Varianten, und zwar, einen normalen String mitzugeben, dann wird der String als Layoutbeschreibung geparsed, oder aber, man übergibt einen String, der mit '<' anfängt, und mit '>' endet, dann wird der Inhalt der Datei, deren Name zwischen '<' und '>' steht, als Layoutbeschreibung verwendet. Damit kann man auch Layouts aus externen Dateien laden, ohne diese Datei selbst öffnen und auslesen zu müssen, und man muss auch keinen extra API-Call bedienen.
Der nächste Schritt ist dann, das Formular zu starten, und optional auch auf Eingaben des Users zu warten. Und zwar funktioniert das mit
stfl_run().
char * event = NULL;
event = stfl_run(f1, 0);
stfl_run(f1, -1);
Der zweite Parameter gibt dabei, wie lange auf Benutzereingabe gewartet werden soll, welche dann zurückgegeben wird. Der Wert "-1" bedeutet, dass keine Benutzereingaben eingelesen werden (wenn man z.B. lediglich das Formular neu zeichnen will), und 0 bedeutet, dass unendlich lange auf Benutzereingabe gewartet wird. Bei einem Wert > 0 wird soviele Sekunden gewartet. Findet bis dahin keine Benutzereingabe statt, so wird von
stfl_run() NULL zurückgeliefert. Hierbei sollte man allerdings auf jeden Fall beachten, dass es auch sein kann, dass auch bei einer Wartezeit von 0 NULL als Rückgabewert zurückkommen kann, weil z.B. ein Tastendruck von dem Widget, das den Fokus hat, "konsumiert" wurde.
Die zurückgegebene Eingabe kann folgendes sein:
- "TIMEOUT": bei einem Timeout
- "ENTER": wenn die Eingabetaste gedrückt wurde
- "ESC": wenn die Escape-Taste gedrückt wurde
- "F0" .. "F63": wenn eine F-Taste gedrückt wurde
- "CHAR(%u)" (wobei %u einem unsigned integer entspricht): eine andere Taste wurde gedrückt, und der numerische Wert dieser Taste befindet sich zwischen den Klammern. Der numerische Werte für die Taste "a" ist z.B. 97. Und auch Ctrl-Tastenkombination sind damit abfragbar, so ist etwa Ctrl-A der numerische Wert 1, Ctrl-B der Wert 2, usw.
Am Ende eines Programmes ist es dann noch zweckmäßig, die Funktion
stfl_reset() aufzurufen, um den Bildschirm entsprechend zu resetten. Mit diesem Wissen kann man schon ein nettes kleines Programm schreiben, das so eine typische Event Loop demonstriert, und zusätzlich führe ich auch noch ein paar weitere Funktionen ein:
static char form_str[] = "vbox\n"
" @style_normal:bg=black,fg=white\n"
" @info#style_normal:bg=blue,fg=yellow,attr=bold\n"
" list[results]\n"
" style_normal:bg=black,fg=white\n"
" style_focus:fg=yellow,bg=blue,attr=bold\n"
" pos_name[listposname]:\n"
" pos[listpos]:0\n"
" label#info\n"
" .expand:h .height:1\n"
" text[help]:\"q:Quit\"\n"
" label\n"
" .expand:h .height:1\n"
" text[msg]:\"\"\n";
#include <stfl.h>
#include <stdio.h>
int main(void) {
int quit = 0;
struct stfl_form *f = stfl_create(form_str);
do {
const char * event = stfl_run(f, 0);
if (!event) continue;
if (strncmp(event,"CHAR(",5)==0) {
unsigned int key;
if (sscanf(event,"CHAR(%u)",&key)==1) {
switch (key) {
case 'q':
quit = 1;
break;
case 'a':
stfl_set(f, "msg", "you just pressed a");
break;
case 'b':
stfl_set(f, "msg", "you just pressed b");
break;
case 'c':
stfl_modify(f, "results", "replace_inner", "{list{listitem[item1] text:\"list item 1\"}{listitem[item2] text:\"list item 2\"}}");
stfl_set(f, "msg", "you just pressed c and replaced the content of the list");
break;
case 'd':
stfl_modify(f, "results", "replace_inner", "{list{listitem[line1] text:\"line 1\"}{listitem[line2] text:\"line 2\"}{listitem[line3] text:\"line 3\"}}");
stfl_set(f, "msg", "you just pressed d and replaced the content of the list");
break;
case 'e': {
const char * listpos = stfl_get(f, "listpos");
const char * listposname = stfl_get(f, "listposname");
char line[1024];
snprintf(line,sizeof(line),"listpos = %s listposname = %s", listpos, listposname);
stfl_set(f, "msg", line);
}
}
} else {
char line[1024];
snprintf(line,sizeof(line),"you pressed: %s", event);
stfl_set(f, "msg", line);
}
}
} while (!quit);
stfl_free(f);
stfl_reset();
return 0;
}
Was man hier sieht, ist eine klassische Event Loop, wobei auf die Tasten a bis e sowie q reagiert wird. Es werden ja nach Tastendruck ein Label bzw. die Liste modifiziert. Mit der Taste e enthält man zusätzlich noch Informationen über die aktuelle Auswahl in der Liste.
Man kann noch wesentlich mehr mit der STFL machen, und dieser Artikel soll nur einen kleinen Einblick in die grundsätzliche Funktionsweise von STFL bringen, um dem einen oder anderen unter euch das Entwickeln von eigenen, neuen Texttools schmackhaft zu machen. Für mehr Informationen sowie andere Widgets wie Eingabefelder oder mehrzeige Text Views ist es ratsam, in die STFL-Dokumentation sowie die existierenden Beispielprogramme reinzuschauen. Es lohnt sich.
Wer übrigens die STFL "C++iger" verwenden will, den kann ich nur auf meinen "stflpp"-Wrapper hinweisen, den man
hier (Header) und
hier (Source) findet (MIT/X-Lizenz, BTW). Die STFL selbst unterstützt neben C übrigens auch
SPL, Ruby, Python und Perl.