Jun 12, 2013 - Von Windowmanager am Beispiel XMonad

Comments

Was ist ein WindowManager?

Beim verwenden einer graphischen Oberfläche geht es dem Benutzer darum, dass er mehrere Programme gleichzeitig auf einem Computer ausführt. Gleichzeitig möchte der Benutzer aber nicht den Überblick darüber verlieren, welche Programme gerade laufen. Der Benutzer kann auswählen, mit welchem Programm er interagieren möchte. So laufen auf meinem Computer im Moment mehrere Programme, darunter: * eins um Musik abzuspielen, * ein Texteditor in dem ich diesen Text hier schreibe, * ein Browser und noch ein paar andere.

Entscheidend ist, dass ich die Möglichkeit habe, zwischen den Programmen hin und her zu schalten. Mal möchte ich die Musik leiser machen, mal möchte ich im Netz was nachschlagen und dann will ich wieder am Text weiterschreiben.

Da mein Monitor nicht sehr groß ist, wäre für alle Programme gleichzeit nicht genügend Platz auf dem Monitor. Ich bin daher glücklich, dass es in meinem Computer ein Stück Software gibt, dass mir dabei hilft, immer nur das Programm anzuzeigen, dass ich auch gerade verwenden möchte. Eine solche Software nennt man WindowManager. In meinem Fall handelt es sich dabei um XMonad, ein in Haskell geschriebenen WindowMananger.

Aber fangen wir ganz einfach an:

WindowManager unter Windows

Ohne mich hier dem üblichen Windows-gebashe anschließen zu wollen, stelle ich fest, dass der WindowManager von Windows relativ dämlich ist. Man ist es gewohnt, dass es um jedes Fenster einen Rahmen gibt. Oben am Fenster ist dieser etwas dicker und darin findet man die typischen Schaltflächen zum minimieren, zum maximieren und zum schließen, sowie den Namen des Fensters und noch etwas mehr.

Ich behaupte dass der Windows-WindowManager deshalb recht dämlich ist, da das Management der Fenster eigentlich vom Benutzer erledigt wird. Die drei Schaltflächen (oben rechts am Fenster) stellen dabei die einzigen Funktionalitäten des WindowManagers dar. Möchte man seine Fenster in komplizierterer Art und Weise auf dem Bildschirm anordnen, so muss man selber Hand anlegen. Komplizierter ist dabei alles, was nicht im Vollbild läuft.

Hat man z.B. das Bedürfnis, seinen Browser neben dem Texteditor laufen zu lassen, so muss der Benutzer selber die Fenster auf eine sinnvolle Größe ziehen. Auch das auf dem Bildschirm verteilen/anordnen muss der Benutzer selber erledigen.

Wer jetzt ein paar Tricks und Kniffe in Windows kennt, wird sagen, dass ein Rechtsklick auf die Startleiste so etwas wie "gekachelte Ansicht" ermöglicht. Das ist auch richtig und stellt tatsächlich etwas mehr Intelligenz des WindowManagers dar. Allerdings handelt es sich dabei auch nicht um sehr pfiffige Lösungen. Zusätzlich vergisst der WindowManager beim neu anordnen alle zuvor getroffenen Einstellungen unwiderruflich.

Neuere Windows Versionen bieten die Möglichkeit an, Fenster im "split Screen" verfahren darzustellen. Dabei belegt ein Fenster die rechte Bildschirmhälfte, ein anderes kann die linke Hälfte belegen. Mit Windows8 ist dann die Möglichkeit dazu gekommen, Fenster in etwa $1/3$ bzw. $2/3$ Anordnungen anzeigen zu können.

Diese Anordnungen von Fenstern sind es, die vom WindowMananger erzeugt werden sollten.

WindowManager unter Linux

Um jetzt fair zu bleiben, kann man festhalten, dass auch die meisten WindowManager unter Linux ähnlich dämlich sind. Auch unter Linux muss sich in den allermeisten Fällen der Benutzer selber um die Verteilung der Fenster auf dem Bildschirm kümmern.

Abhilfe schaffen hier sogenannte "tiling" WindowManager; XMonad ist ein solcher tiling WindowManager.

Tiling WindowManager

Ein schon relativ alter tiling WindowManager, larsWM, hatte als Tagline den Spruch: "... because managing windows is the windowmanagers task". Und genau das trifft den Kern der Sache ziemlich gut.

Ein tiling WindowManager enthält eine Reihe von verschiedenen Layouts. Jedes Fenster das erzeugt wird (z.B. nachdem ein Programm gestartet wird), wird aufgrund seiner Eigenschaften nach einem dieser Layouts auf dem Bildschirm dargestellt. Die Kriterien, die dabei für das Auswählen des Layouts herangezogen werden, sind vielfältig. Letztlich sind die Kriterien sowie auch die Layouts nur durch die Fantasie des Benutzers beschränkt.

Im Folgenden möchte ich das Prinzip eines tiling WindowManagers anhand des Beispiels xmonad erläutern. Dabei werde ich einfach gefühlt durch die config meiner xmonad Installation gehen.

Noch ein paar Worte zu xmonad

xmonad ist ein WindowManager, der ausschließlich in der funktionalen Sprache Haskell geschrieben ist. Dies ist ziemlich beachtlich, da funktionalen Sprachen gerne die unterstellt wird, auf Probleme der "echten" Welt nicht performant anwendbar zu sein.

Neben der funktionalen Herkunft xmonads, ist dieses beachtliche Stück Software auch noch extrem klein. Xmonad liegt mit seinen ca. 500 Zeilen Code, Größenordnungen von anderen WindowManagern entfernt. Der Code selber ist dabei erstaunlich leserlich. Ich würde sogar behaupten, dass xmonad eine gute Anlaufstelle für Neulinge ist.

Neben xmonad gibt es noch das sogenannte xmonad-contrib Packet. Hierbei handelt es sich um zusätzliche Bibliotheken, die xmonad um eine Reihe nützlicher Funktionen erweitern. Selbstverständlich ist xmonad sowie auch das contrib Packet open-source.

Die Konfiguration von xmonad wird übrigens direkt in Haskell geschrieben. Eigentlich schreibt man genau die Datei neu, die dann die main Funktion enthält. Somit ist man äußerst flexibel und kann sich mithilfe typischer Haskell-Kniffe die Konfiguration erstellen.

Wenn man gewohnt ist, dass config-files realtiv primitiv in der Eingabesprache sind, so ist diese Herrangehensweise sehr erfrischend.

Und nun genug der Vorrede, fangen wir an...

Desktops

Einen Desktop bietet jede graphische Benutzungsoberfläche und immer häufiger hat man die Möglichkeit seine Programme auf mehrere Oberflächen aufteilen zu können. Auch unter Windows gibt es Zusatzprogramme (wie z.B. Finestra), die dem Benutzer mehr als einen Desktop bringen.

Unter xmonad lassen sich auch mehrere Desktops einrichten. In der Regel verwendet man hier nicht mehr als neun Stück, da jeder Desktop mit einer Tastenkombination geöffnet wird. Es ist üblich, dazu die Nummern 1-9 zu verwenden, was den Wert 9 erklärt:

    myWorkspaces = map show [1..9]

In bekannter Haskell-Logik sind die Desktops in einer Liste organisiert; jeder Einzelne Desktop ist mit einem Handle (den Zahlen 1 - 9) ansprechbar. Gültige Handle für einen Desktop ist ein beliebiger String. Ich neige dazu meinen Desktop nach den Programmen zu benennen, die dort laufen:

myWorkspaces = map show [1] ++ ["code", "web", "im", "mail", "skype", "gimp", "musi", "irc"]

Im nächsten Schritt definiere ich in meiner Konfiguration xmonad.hs die Kriterien fest, nachdenen entschieden wird, ob das aktuelle Programm besonders von xmonad behandelt werden soll. Dazu werden wieder Listen angelegt, die Strings enthalten, die entsprechend die Programme identifizieren. Beispielsweise verwende ich als IM gerne Jabber aber öfter mal unterschiedliche Programme. Darunter pidgin, empathy oder eines, dass sich über den String Jabber verrät:

 webApps    = ["Firefox", "Google-chrome", "Chromium"]
    comApps    = ["Pidgin", "jabber", "Jabber", "Empathy"]
    mailApps   = ["OUTLOOK.EXE", "Wine", "mutt", "mail"]
    gimpApp    = ["Gimp", "gimp"]
    skypeApp   = ["Skype", "skype", "MainWindow"]
    ircApps    = ["workies", "irc"]

Alle die Strings, die es ermöglichen, ein Programm zuordnen zu können werden in einer Liste aufgesammelt. In einem weiteren Konfigurationnspunkt werden dann die Aktionen für diese Programme festgelegt.

Zuordnungen

Bis hierhin haben wir jetzt eine Liste der unterschiedlichen Desktops erstellt. Außerdem haben wir mehrere weitere Listen, die Identifier für unterschiedliche Programmklassen enthalten. Der nächste logische Schritt ist nun, die Verknüpfung beider Informationen herzustellen. Dazu wird eine managedHook erstellt.

 myManagedHook = composeAll . concat $
    [ [ className =? c --> doFloat               | c <- floatClass ]
    , [ title     =? t --> doFloat               | t <- floatTitle ]
    , [ title     =? x --> doF (S.shift "1")     | x <- hacking]
    , [ title     =? x --> doF (S.shift "code")  | x <- coding]
    , [ className =? x --> doF (S.shift "web")   | x <- webApps ]
    , [ className =? x --> doF (S.shift "im")    | x <- comApps ]
    , [ title     =? x --> doF (S.shift "im")    | x <- comApps ]
    , [ className =? x --> doF (S.shift "mail")  | x <- mailApps ]
    , [ title     =? x --> doF (S.shift "mail")  | x <- mailApps ]
    , [ className =? x --> doF (S.shift "gimp")  | x <- gimpApp ]
    , [ className =? x --> doF (S.shift "skype") | x <- skypeApp ]
    , [ title     =? x --> doF (S.shift "irc")   | x <- ircApps ]
    , [ isFullscreen   --> doFullFloat]
    ]

Ich möchte hier garnicht genau die verwendeten Operatoren im Detail erklären. Die Funktionalität, die mit dem obigen Codeblock erreicht wird, ist trotzdem ersichtlich. Beispielsweise werden alle Programme auf den Desktop "web" geschoben, die im "className" einen String stehen haben, der in der Liste der "webApps" definiert wurde. Für Programme des "im"-Desktops wird der String im "className" sowie im "title" gesucht. Hierbei sind className und title Eigenschaften üblicher X11-Fenster.

Layouts

Wir haben jetzt Anwendungen und deren zugehörige Desktops festgelegt. Im nächsten Schritt müssen wir festlegen, was mit den Anwendungen passiert, wenn sie auf dem entsprechenden Desktop gelandet sind.

Diese Stelle ist nun die Stelle, an der das aktuelle Aussehen eines Desktops festgelegt wird. Ich werde hier exemplarisch ein paar meiner Layouts vorstellen. Da es so viele verschiedene Layouts gibt, werden Layouts an dieser Stelle nur Beispielhaft beschrieben.

Für alle die aber trotzdem noch mehr Layouts betrachten wollen, gibt es hier eine ganze Ansammlung von Leuten, die ihre xmonad Konfiguration online gestellt haben.

TODO xmonad-wiki verlinken (das mit den ganzen konfigs) empfohlen.

Und zu guter Letzt lohnt sich immer auch ein Blick in die Sourcen vom xmonad-contrib Packet, da in dem Paket alle möglichen LayoutHooks enthalten sind. Die Dokumentation ist (wie bei jedem gutem Projekt) von eher spärlich bis oft garnicht vorhanden.

Aber jetzt zu meinen Layouts. Ein Layout wird mit onWorkspaces begonnen. Es folgen sowohl eine Liste der Desktops, auf denen dieses Layout so angewandt werden soll, als auch die Layoutbeschreibung selber.

Auf dem Desktop "1" verwende ich drei verschiedene Layouts, ein Full, ein tiled sowie ein Mirror tiled layout. Diese Layouts sind in dem xmonad-contrib Packet vordefiniert und können in der Konfiguration verwendet werden (die notwendigen include's nicht vergessen).

 myLayout  = smartBorders 
        $ onWorkspaces ["1"] (Full ||| tiled ||| Mirror tiled)
    where tiled  = Tall nmaster delta ratio
          ratio   = 1/2
          delta   = 3/100

Im Konfigurationsfile werden die Layouts mit dem |||-Operator voneinender getrennt. Dies sorgt dafür, dass zwischen den Layouts gewechselt werden kann. In meinem Fall kann ich mit Alt+Space das Layout wechseln. So kann ich also zwischen einer Vollbild-Ansicht und den zwei gekachelten Ansichten hin- und herschalten.

Auf dem Desktop Nr. 2 verwende ich ein Layout, das mir beim programmieren hilft. Dabei wird ein Fenster über $2/3$ des Bildschirmes aufgeteilt. Das übrige drittel verwende ich dann, um darin den Interpreter (in aller Regel ghci) zu verwenden. So lassen sich bequem Quellcode und der Interpreter auf dem Desktop anordnen.

        $ onWorkspaces ["code"] (codeColumn ||| codeRow)
    where codeColumn = named "Column Code" $ Tall nmaster delta' ratio'
          codeRow    = named "Row Code" $ Mirror $ reflectHoriz $ Tall nmaster delta' ratio'
          ratio'     = 1/3
          delta'     = 0

Auch für diesen Desktop gibt es nun zwei Layouts, die mit Alt+Space gewechselt werden können. Das eine Layout verwendet das untere drittel für den Interpreter, das andere Layout verwendet den linken Bereich für das Interpreterfeld.

Für den Desktop des Browsers habe ich lange Zeit lediglich ein Vollbild-Layout verwendet. Das hier gezeigte Layout besteht nun auch wieder aus zwei unterschiedlichen Layouts. Zum einen ist das Vollbild-Layout (Full). Zum anderen gibt es noch ein zweites Layout, welches aus dem Vollbild-Layout gebildet wird.

        $ onWorkspaces ["web"] (Full ||| (gaps [(L,250), (R,250)] $ Full))

Allerdings wird links und rechts etwas Platz gelassen (jeweils 250px). Dieser Platz sorgt dafür, dass der Browser noch eine Spalte in der Mitte belegen kann. Auf manchen Webseiten kann man so die Ads am linken und rechten Rand ausblenden. Auch hilft es dem Auge beim lesen, wenn die Zeilen nicht übermäßig lang werden.

Als letztes Beispiel möchte ich hier noch mein Layout für Instant Messaging Anwendungen beschreiben. Dieses Layout reserviert am rechten Bildschirmrand etwas Platz um dort den Roaster (also die Freundesliste) darstellen zu können. Der übrige Bereich wird entweder über ein Grid-Layout, ein Full-Layout oder ein Circle-Layout dargestellt.

        $ onWorkspaces ["im"] (   reflectHoriz (pidginLayout gridLayout)
                              ||| reflectHoriz (pidginLayout Full)
                              ||| reflectHoriz (pidginLayout Circle)                                        
                              )
    where gridLayout     = spacing 8 $ Grid      
          pidginLayout l = withIM (18/100) (Role "buddy_list") l

Damit möchte ich den Ausflug in die unterschiedlichen Layouts meiner Konfiguration beenden.

Tastenkombinationen

Neben der Verteilung der unterschiedlichen Anwendungen auf die unterschiedlichen Desktops, und neben den Layouts der Desktops sind auch die Tastenkombinationen von fundamentaler Bedeutung. Die Tastenkombinationen sind so wichtig, da XMonad fast ausschließlich mit der Tastatur bedient wird. Es gibt Kürzel für das öffnen einer Konsole, für das verschieben von Fenstern auf die Desktops oder aber für das Wechseln des aktuellen Desktops.

Eine Liste der default Tastenkombinationen kann man HIER finden. TODO TODO (Link zur Liste der Shortcuts)

Zusätzlich ist es natürlich möglich, die Tastenkombinationen zu ändern oder eigene hinzuzufügen. Ach hier ein paar Beispiele aus meiner aktuellen XMonad Konfiguration:

 myKeys x = M.fromList $
    [ ((modMask x,                 xK_p), shellPrompt myXPConfig)
    , ((modMask x,                 xK_y), scratchpadSpawnActionTerminal "urxvt")
    , ((modMask x .|. shiftMask,   xK_m), manPrompt myXPConfig)
    , ((modMask x .|. shiftMask,   xK_s), sshPrompt myXPConfig)
    , ((modMask x .|. shiftMask,   xK_f), focusUrgent)
    , ((modMask x .|. shiftMask,   xK_j), windows swapDown)
    , ((modMask x .|. shiftMask,   xK_k), windows swapUp)
    , ((modMask x,                 xK_m), windows shiftMaster)
    , ((modMask x .|. shiftMask,   xK_Return), spawn "/usr/bin/urxvt")
    , ((modMask x .|. controlMask, xK_l),    spawn "gnome-screensaver-command --lock")
    , ((modMask x .|. controlMask, xK_a),    spawn "/home/frosch03/whatIsThisPlace")
    , ((0        ,   xK_XF86ScreenSaver),    spawn "gnome-screensaver-command --lock")
    , ((0      ,xK_XF86AudioRaiseVolume),    spawn "/home/frosch03/.xmonad/volUp.sh")
    , ((0      ,xK_XF86AudioLowerVolume),    spawn "/home/frosch03/.xmonad/volDn.sh")
    ]
    newKeys x = myKeys x `M.union` keys defaultConfig x

An diese Stück Code lässt sich schön sehen, dass ich meine Tastenkombinationen nur zusätzlich definiere. Die erste Zeile besagt, dass meine Tastenkombinationen nach der Definition unter dem Namen myKeys ansprechbar sind. In der letzten Zeile werden dann die selbst definierten mit der defaultConfig vereinigt (M.union)

Die Definition an sich findet in einem Tupel statt. Dabei legt das vordere Element die Tastenkombination fest, das hintere Element beschreibt das Kommando, welches ausgeführt werden soll. Dabei stehen modMask x für die "modifier" Taste, welche häufig "ALT" ist. Ich selber verwendet die "Windows-Taste" als modifier. shiftMask steht für "SHIFT" und controlMask ist die Taste "CONTROL". Mit dem Operator .|. wird beschrieben, dass beide Tasten gedrückt werden müssen. Zusätzlich zu diesen drei modifiern wird immer noch eine "wirkliche" Taste angegeben. In XMonad sind diese immer mit dem Präfix xK_ versehen, so dass xK_Return für Enter steht und xK_XF86AudioLowerVolume für die "leiser"-Taste.

Die Kommandos, die Ausgeführt werden sollen, sind beliebig. Häufig werden solche ausgeführt, die XMonad intern verändern, also z.B. ein Fenster auf einen anderen Desktop verschieben oder aber die Reihenfolge der Fenster ändern, wie mit windows swapDown.

spawn ... ist ein Kommando, das einen neuen Prozess erzeugt und darin das Programm am übergebenen Pfad ausführt. So starte ich z.B. meine Konsole oder aber ein Bash-Script, welches die Lautstärke verändert.

TL;DR

Xmonad ist ein funktionaler Windowmanager und ist vollständig in Haskell geschrieben ist. Außerdem ist er auch ein tiling Windowmanager, das heißt: xmonad verteilt Fenster selbständig auf Desktops. Dazu werden die vorhandene Regeln verwendet. Weiter oben ist beschrieben, wie solche Regeln aussehen können. Weiter wurden oben die wichtigen Stellen aus der xmonad Konfiguration beschrieben.