Python unter Linux: Curses

In diesem Kapitel geht es um rein textorientierte Benutzeroberflächen, wie Sie sie möglicherweise von rein textorientierten Programmen wie mutt, lynx oder dem Midnight-Commander her kennen. Es werden Teile des Curses-Moduls vorgestellt. Zu Curses gibt es ein eigenes hervorragendes Wikibook, nämlich Ncurses. Dieser Abschnitt benutzt einige der dort vorgestellten Ideen.

Hallo, Welt!

Bearbeiten

Unser erstes Beispiel erzeugt ein Fenster, in dem sich der historisch bekannte Spruch Hallo, Welt! zeigt. Bitte beachten Sie, dass dieses Beispiel Farben voraussetzt. Es wird nicht geprüft, ob ihr Terminal Farben darstellen kann.

#!/usr/bin/python
# -*- coding: utf-8 -*-

import curses
#start
stdscr = curses.initscr()
# Keine Anzeige gedrückter Tasten
curses.noecho()
# Kein line-buffer
curses.cbreak()
# Escape-Sequenzen aktivieren
stdscr.keypad(1)

# Farben
curses.start_color()
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLUE)
curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK)

# Fenster und Hintergrundfarben
stdscr.bkgd(curses.color_pair(1))
stdscr.refresh()

win = curses.newwin(5, 20, 5, 5)
win.bkgd(curses.color_pair(2))
win.box()
win.addstr(2, 2, "Hallo, Welt!")
win.refresh()

# Warten auf Tastendruck
c = stdscr.getch()

# Ende
curses.nocbreak()
stdscr.keypad(0)
curses.echo()
curses.endwin()
 
Hallo Welt

Die beiden wichtigen Initialisierungsfunktionen in diesem Beispiel sind curses.initscr(), mit der die Curses-Umgebung eingerichtet wird und curses.start_color(), mit der das Programm die Möglichkeit erhält, Farbe darzustellen. Farben werden als Paar erzeugt, mit curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK) wird ein Paar mit der Nummer 2 erzeugt, welches gelb auf schwarzem Grund darstellen kann. stdscr.bkgd(curses.color_pair(1)) referenziert ein solches Paar und legt den Hintergrund für das Terminal fest. Es wird nichts angezeigt, ohne dass ein Refresh (stdscr.refresh()) erfolgt.

Ein Fenster kann mit curses.newwin(Höhe, Breite, Y0, X0) erzeugt werden. Die Parameter sind ungewöhnlich, stehen hier Y-Werte vor den betreffenden X-Werten. Das ist eine Besonderheit der Terminal-Bibliotheken curses und ncurses und war schon immer so.

Einem so erzeugten Fenster kann ebenfalls ein Hintergrund zugewiesen werden, zur Verschönerung dient auch die Funktion box(). Sie zeichnet einen Rahmen rund um das Fenster. addstr(Y, X, Text) schreibt sodann einen Text auf das Fenster. Auch hier benötigt man wieder einen Refresh, um etwas sehen zu können. Die Ereignisbehandlung ist anders als in bisherigen Nutzeroberflächen rein von der Tastatur gesteuert. Mausereignisse werden ebenfalls zuerst von getch() entgegengenommen.

Nach dem der Nutzer eine Taste drückte, wird das Programm freundlich beendet. Hier ist insbesondere curses.endwin() zu nennen, das Gegenstück zu curses.initscr().

Einige der im Quellcode genutzten Funktionen haben wir Ihnen bei der Erläuterung unterschlagen. Das wollen wir gleich nachholen.

Kurzreferenz Curses

Bearbeiten

Hier werden ausgewählte Funktionen und Konstanten vorgestellt. Die Liste ist selbstverständlich nicht vollständig. Eine Darstellung der Art [attrib] bedeutet, dass der Wert optional ist und damit weggelassen werden darf.

Curses Funktionen

Bearbeiten

Hier eine Auflistung einiger gebräuchlicher curses-Funktionen:

Funktion Bedeutung
cbreak()
nocbreak()
Im CBreak-Modus werden Tasten einzeln angenommen, es wird nicht auf die Bestätigung durch Return gewartet.
attrib = color_pair(Farbnummer)
init_pair(nummer, vordergrund, hintergrund)
color_pair(): Es wird auf eine vorher mit Farbnummer vordefinierte Vorder- und Hintergrundfarbe verwiesen. Die Attribute werden zurückgegeben.
init_pair(): Erzeugt ein Farbpaar aus Vordergrundfarbe und Hintergrundfarbe und weist diese Kombination der Nummer zu. nummer kann ein Wert zwischen 1 und COLOR_PAIRS-1 sein.
doupdate()
noutrefresh()
refresh()
Diese Gruppe von Funktionen kümmert sich um den Refresh. refresh() ist ein noutrefresh() gefolgt von doupdate(). noutrefresh() updatet den virtuellen Bildschirm, markiert also den Bereich als einen, den es zu refreshen gilt, während doupdate() den physikalischen Bildschirm, also die echte Anzeige, auffrischt.
echo()
noecho()
echo() bewirkt, dass Tastendrücke angezeigt werden. Dieses lässt sich mit noecho() ausschalten.
mouse = getmouse()
(alle, aktuell) = mousemask(neu)
vorher= mouseinterval(neu)
getmouse(): Bei einem Mausereignis kann mit dieser Funktion der Mauszustand als Tupel (ID, X, Y, unbenutzt, Knopf) bereitgestellt werden.
mousemask(): erwartet eine Maske, bestehend aus den zu behandelnden Mausereignissen. Es wird ein Tupel aus allen verfügbaren Mausereignissen als Maske und den zuletzt aktiven zurückgeliefert.
mouseinterval(): Setzt ein neues Intervall in Millisekunden, welches zwischen zwei Maustastendrücken vergehen darf, damit das Ereignis noch als Doppelklick erkannt wird. Die Funktion gibt das zuletzt aktive Intervall zurück.
bool = has_colors() True, wenn das Teminal Farben darstellen kann. Sonst False.

Window-Funktionen

Bearbeiten

Einige Funktionen, die nur mit einem Window-Argument Sinn ergeben:

Funktion Bedeutung
addch([y, x,] Z[,attrib])
addstr([y,x,] str[,attrib])
addch():Zeichnet an der (optionalen) Stelle (y, x) das einzelne Zeichen Z. Dieses Zeichen kann mit Attributen versehen werden.
addstr(): Selbiges für Strings
bkgd(Z[,attr]) Legt die Hintergrundfarbe / Zeichen / Attribute fest.
box([vertZ, horZ]) benutzt vertZ und horZ (beide optional) um einen Rahmen um ein Fenster zu Zeichnen. Ohne diese Parameter wird ein Linienrand gezeichnet.
clear() Löscht beim nächsten refresh() das komplette Fenster
clrtoeol() Löscht von der momentanen Cursorposition bis zum Ende der Zeile
keypad(value) Ist value == 1, dann werden spezielle Tasten wie Funktionstasten von Curses als solche interpretiert. Mit 0 schaltet man dieses Verhalten wieder ab.

Konstanten

Bearbeiten
  • Attribute, mit denen das Verhalten von Zeichen und geändert werden kann:
Konstante Bedeutung
A_BLINK Text wird blinkend dargestellt
A_BOLD Text wird in Fettdruck dargestellt
A_NORMAL Text wird wieder normal dargestellt.
A_UNDERLINE Text wird unterstrichen angezeigt
  • Tastaturkonstanten:
Konstante Bedeutung
KEY_UP, KEY_LEFT, KEY_RIGHT, KEY_DOWN Cursortasten
KEY_F1...KEY_Fn Funktionstasten
KEY_BACKSPACE Backspace-Taste
KEY_MOUSE Ein Mausereignis trat ein
  • Mauskonstanten:
Konstante Bedeutung
BUTTON1_PRESSED .. BUTTON4_PRESSED Knopf wurde gedrückt
BUTTON1_RELEASED .. BUTTON4_RELEASED Knopf wurde losgelassen
BUTTON1_CLICKED .. BUTTON4_CLICKED Knopf gedrück und wieder losgelassen, innerhalt eines mit mouseinterval() einstellbaren Intervalles.
BUTTON1_DOUBLE_CLICKED .. BUTTON4_DOUBLE_CLICKED, Doppelklick
BUTTON1_TRIPLE_CLICKED, .. BUTTON4_TRIPLE_CLICKED, 3 klicks hintereinander
BUTTON_SHIFT, BUTTON_CTRL, BUTTON_ALT Es wurde die Shift-, Steuerungs, oder die Alt-Taste beim Klicken gedrückt. Man beachte, dass einige Terminals selbst darauf reagieren, das Programm diese Ereignisse also nicht mitbekommt.
  • Farbkonstanten: COLOR_BLACK, COLOR_BLUE, COLOR_CYAN, COLOR_GREEN, COLOR_MAGENTA, COLOR_RED, COLOR_WHITE, COLOR_YELLOW

Mehr Fenster

Bearbeiten

Im folgenden Beispiel geht es um das Lesen von Log-Dateien (Sie müssen Leserecht darauf haben, sonst funktioniert es nicht) und Menüs. Es werden auf Wunsch zwei Fenster erzeugt, eines stellt ein Menü dar, im anderen wird wahlweise /var/log/syslog und /var/log/messages dargestellt. Scrollen kann man in den Dateien mit den Cursor-Tasten. Beendet wird mit der Taste x.

#!/usr/bin/python
# -*- coding: utf-8 -*-
import curses
import linecache

def init_curses():
    stdscr = curses.initscr()
    curses.noecho()
    curses.cbreak()
    stdscr.keypad(1)
    curses.start_color()
    curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLUE)
    curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK)
    stdscr.bkgd(curses.color_pair(1))
    stdscr.refresh()
    return stdscr

def show_menu(win):
    win.clear()
    win.bkgd(curses.color_pair(2))
    win.box()
    win.addstr(1, 2, "F1:", curses.A_UNDERLINE)
    win.addstr(1, 6, "messages")
    win.addstr(1, 20, "F2:", curses.A_UNDERLINE)
    win.addstr(1, 24, "syslog")
    win.addstr(1, 38, "x:", curses.A_UNDERLINE)
    win.addstr(1, 42, "Exit")
    win.refresh()

def read_file(menu_win, filename):
    menu_win.clear()
    menu_win.box()
    menu_win.addstr(1, 2, "x:", curses.A_UNDERLINE)
    menu_win.addstr(1, 5, "Ende ->")
    menu_win.addstr(1, 14, filename)
    menu_win.refresh()
    file_win = curses.newwin(22, 80, 4, 0)
    file_win.box()
    line_start = 1
    line_max = 20
    while True:
      file_win.clear()
      file_win.box()
      for i in xrange(line_start, line_max + line_start):
          line = linecache.getline(filename, i)
          s = ''
          if len(line) > 60:
              s = '[%d %s]' % (i, line[:60])
          else:
              s = '[%d %s]' % (i, line[:-1])
          file_win.addstr(i - line_start + 1, 2, s)
      file_win.refresh()
      c = stdscr.getch()
      if c == ord('x'):
          break
      elif c == curses.KEY_UP:
          if line_start > 1:
              line_start -= 1
      elif c == curses.KEY_DOWN:
          line_start += 1

stdscr = init_curses()
mwin = curses.newwin(3, 80, 0, 0)

while True:
  stdscr.clear()
  stdscr.refresh()
  show_menu(mwin)
  c = stdscr.getch()
  if c == ord('x'):
    break
  elif c == curses.KEY_F1:
    read_file(mwin, '/var/log/messages')
    show_menu(mwin)
  elif c == curses.KEY_F2:
    read_file(mwin, '/var/log/syslog')
    show_menu(mwin)

curses.nocbreak()
stdscr.keypad(0)
curses.echo()
curses.endwin()
 
Logdateien

Es wird ein neues Modul mit dem Namen linecache eingebunden. Dieses Modul enthält die Funktion linecache.getline(filename, i). Hiermit kann eine bestimmte Zeile aus der Datei ausgegeben werden. Wir nutzen sie in read_file().

In read_file() wird eine Datei gelesen. Zuerst wird das Menüfenster aktualisiert, dann wird ein neues Fenster erzeugt und eine Datei zeilenweise eingelesen. Es wird darauf geachtet, dass maximal 60 Zeichen einer Zeile dargestellt werden. Anschließend wird eine eigene Warteschleife durchlaufen, die die Tastatureingaben empfängt. Bei der Eingabe von x wird die Schleife abgebrochen und zum Hauptprogramm zurückverzweigt. Falls eine Cursortaste gedrückt wurde, so wird der Bereich der Logdatei, der beim nächsten Durchgang gelesen wird, neu berechnet. Hier ist nicht wichtig, wie lang die Datei ist, denn linecache.getline() liefert uns einen leeren String zurück, wenn wir versuchen, über das Dateiende hinaus zu lesen.

Im Hauptprogramm wird ein neues Fenster stdscr erzeugt. Von dort aus wird die Hauptschleife abgearbeitet. Bei F1 wird /var/log/messages und bei F2 /var/log/syslog gelesen. Mit x wird das Programm abgebrochen. Bei jedem Durchgang wird das Menü aktualisiert und das alte Log-Fenster, welches vielleicht noch auf dem Bildschirm erscheint, gelöscht.

Große Fenster

Bearbeiten

Im letzten Beispiel haben wir gesehen, dass es Mühe kostet, einen Text in einem Fenster darzustellen, wenn die Zeilenlänge oder die Textlänge größer ist als die Fensterausmaße. Es gibt einen Weg darum herum. Wir können mit Curses sehr große Fenster erzeugen, und nur einen Teil auf den Bildschirm bringen. Solche großen Fenster heißen Pad und sind nicht an die Ausmaße des darunter liegenden Fensters gebunden. Das folgende Programm nutzt solch ein Pad, es ist 10000 Zeilen hoch und 3000 Spalten breit. Es wird diesmal die Datei /var/log/messages gelesen, navigieren können Sie mit allen vier Cursortasten, bei der Eingabe von x wird das Programm beendet.

#!/usr/bin/python
# -*- coding: utf-8 -*-
import curses
import linecache

def init_curses():
    stdscr = curses.initscr()
    curses.noecho()
    curses.cbreak()
    stdscr.keypad(1)
    curses.start_color()
    curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_BLUE)
    curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLUE)
    stdscr.bkgd(curses.color_pair(1))
    stdscr.box()
    stdscr.refresh()
    return stdscr

stdscr = init_curses()
# Aktuelle Screengröße ermitteln
maxy, maxx = stdscr.getmaxyx()

# Großes Pad erzeugen
pad = curses.newpad(10000, 3000)
for i in xrange(10000):
    line = linecache.getline('/var/log/messages', i)
    s = '[%5d] %s' % (i, line)
    pad.addstr(i, 1, s)

# Wir merken uns, wo wir sind
line_start = 1
col_start = 1

while True:
  # Teile des Pads aufs Display bringen
  pad.refresh(line_start, col_start, 1, 1, maxy-2, maxx-2)
  c = stdscr.getch()
  if c == ord('x'):
    break
  elif c == curses.KEY_DOWN:
    line_start += 1
  elif c == curses.KEY_UP:
    if line_start > 1: line_start -= 1
  elif c == curses.KEY_LEFT:
    if col_start > 1: col_start -= 1
  elif c == curses.KEY_RIGHT:
    col_start += 1

# ende
curses.nocbreak()
stdscr.keypad(0)
curses.echo()
curses.endwin()

Um das innere Pad an das äußere Fenster anzupassen, fragen wir mit stdscr.getmaxyx() die Ausmaße des Fensters ab. Unser Pad wird dann im Inneren liegen, umgeben von einem Rand. Das eigentliche Pad wird mit curses.newpad(10000, 3000) erzeugt. Es ist 10000 Zeilen hoch und 3000 Zeilen lang. Das sollte für diese Datei reichen. Mit der schon bekannten Methode wird das Fenster mit dem Dateiinhalt gefüllt. Neu ist noch, dass wir lediglich einen Ausschnitt aus dem Pad auf den Bildschirm bringen. Dies erledigt pad.refresh(PadStartZeile, PadStartSpalte, WinY, WinX, WinBreite, WinHöhe). Hierbei sind "Win*"-Argumente die Ausmaße des umgebenden Fensters. "PadStartZeile" und "PadStartSpalte" hingegen sind derjenige Ursprung, der im inneren des Pads liegt und in der linken oberen Ecke des Fensters zu sehen sein soll. Beim Druck auf eine Cursor-Taste werden diese Werte entsprechend modifiziert, dann wird das Pad neu gezeichnet. Es wird also innerhalb des Pads gescrollt.

Mausereignisse

Bearbeiten

Auf Mausereignisse sind wir bisher noch nicht eingegangen. Grund genug, das hier in einem kleinen Abschnitt nachzuholen. Mausereignisse werden in der Hauptschleife ebenso bearbeitet wie Tasten, die spezielle Taste ist curses.KEY_MOUSE.

#!/usr/bin/python
# -*- coding: utf-8 -*-
import curses
import linecache

def init_curses():
    stdscr = curses.initscr()
    curses.noecho()
    curses.cbreak()
    stdscr.keypad(1)
    curses.start_color()
    curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_BLUE)
    stdscr.bkgd(curses.color_pair(1))
    stdscr.box()
    stdscr.refresh()
    return stdscr

stdscr = init_curses()
# Maus initialisieren
avail, oldmask = curses.mousemask(curses.BUTTON1_PRESSED)
curses.mousemask(avail)

while True:
  c = stdscr.getch()
  if c == ord('x'):
    break
  elif c == curses.KEY_MOUSE:
    id, x, y, z, button = curses.getmouse()
    s = "Mouse-Ereignis bei (%d ; %d ; %d), ID= %d, button = %d" % (x, y, z, id, button)
    stdscr.addstr(1, 1, s)
    stdscr.clrtoeol()
    stdscr.refresh()

# ende
curses.nocbreak()
stdscr.keypad(0)
curses.echo()
curses.endwin()

Damit auf Mausereignisse überhaupt reagiert werden kann, muss eine Maus-Maske erstellt werden. Die Funktion curses.mousemask() erwartet eine solche Maske und liefert ein Tupel aus allen verfügbaren Mausereignissen und der letzten Maske zurück. Wir wollen wirklich alle Ereignisse behandelt wissen und nutzen einen zweiten Aufruf dieser Funktion, um das mitzuteilen.

curses.getmouse() liefert uns ein Tupel mit den benötigten Mausinformationen. Diese werden auf dem Bildschirm dargestellt.

Curses enthält einen rudimentären Editor, den man nutzen kann, um innerhalb eines Fensters Text zu schreiben. Er wird eingebunden durch curses.textpad.Textbox(Fenster) und beinhaltet eine Methode edit(), um Text entgegenzunehmen. Diese Methode kann mit einem Validator zum Filtern von Tastendrücken versehen werden. Das folgende Programm demonstriert, wie man diesen Editor einbindet und den eingegebenen Text in einem anderen Fenster darstellt. Beendet wird der Editor mit STRG+G. Dann wird im anderen Fenster der eingegebene Text dargestellt, das Programm kann anschließend mit einem Tastendruck beendet werden.

#!/usr/bin/python
# -*- coding: utf-8 -*-

import curses
import curses.textpad

# Initialisieren
stdscr = curses.initscr()
curses.noecho()
curses.cbreak()
stdscr.keypad(1)
curses.start_color()
curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLUE)
curses.init_pair(2, curses.COLOR_YELLOW, curses.COLOR_BLACK)
stdscr.bkgd(curses.color_pair(1))
stdscr.refresh()

# Edit-Fenster
win1 = curses.newwin(20, 40, 10, 5)
win1.bkgd(curses.color_pair(2))

# Darstellungsfenster
win2 = curses.newwin(20, 40, 10, 50)
win2.bkgd(curses.color_pair(2))
win2.refresh()

# Textbox
textbox = curses.textpad.Textbox(win1)
text = textbox.edit()

# Text übernehmen
win2.addstr(0, 0, text)
win2.refresh()

# Ende
c = stdscr.getch()
curses.nocbreak()
stdscr.keypad(0)
curses.echo()
curses.endwin()

Zusammenfassung

Bearbeiten

Sie haben in diesem Kapitel einen groben Überblick über einige ausgewählte Fähigkeiten von Curses erhalten. Curses selbst kann nur sehr einfache Dinge, wie Rechtecke als Fenster verwalten und auf Tastendrücke reagieren. Dafür sind mit Curses geschriebene Oberflächen schneller als grafische, was aber auch kein Kunststück ist. Als Ausblick können wir Ihnen das Modul UrWid nennen, welches ein recht vollständiges TUI-Toolkit darstellt und auf Curses basiert.