c't 13/2021
S. 148
Praxis
Hammerspoon
Bild: Thorsten Hübner

Hammer, Lua, Löffel

macOS anpassen mit Hammerspoon

Das Open-Source-Projekt ­Hammerspoon vereinfacht Automatisierungen und ­An­passungen von macOS. Mit de­r Skriptsprache Lua ­können Sie etwa Programm­fenster per ­Tastenkürzel positionieren, auf angeschlossene Bildschirme ­reagieren und Ihrem Mac ein ­dynamisches Menü verpassen.

Von Immo Junghärtchen

Nicht jedes Detail der Oberfläche von macOS kann man mit einfachen Mitteln automatisieren und nicht jede Software für macOS ist per Tastenkombination oder Skript steuerbar. Das stört oft schon bei alltäglichen Aufgaben: Wenn Sie sich schon immer gewünscht haben, dass Ihre Anwendungen auf dem MacBook nach dem Anstöpseln des heimischen Bildschirms an die richtige Stelle rutschen, ist das Open-Source-Projekt Hammerspoon einen Blick wert. Es macht das Betriebssystem zugänglich für eigene Automatismen – deutlich flexibler und schneller als der bordeigene Automator.

Dabei erhalten Sie schon mit wenigen Skriptzeilen Zugriff auf Schnittstellen, die tief in macOS verborgen liegen. Sie können unter anderem Programme fernsteuern, Dateien verschieben und Informationen auf dem Bildschirm einblenden. Die Makros rufen Sie zum Beispiel per Tastenkürzel oder über ein selbst gebasteltes Menü auf. Mit den richtigen Befehlen dressiert, reagiert Hammerspoon zudem auf Systemereignisse und erkennt angeschlossene Monitore, USB-, Audio-Geräte oder einen Wechsel des WLAN.

Ein solches Skript heißt im Hammerspoon-Jargon „Spoon“ und im Internet kursieren zahlreiche Beispielkonfigurationen und fertige Erweiterungen, die Sie herunterladen oder als Basis für eigene Spoons nutzen können. Um Tastatur­kürzel, Fensterarrangements und Übersetzungen solcher Fundstücke anzupassen, bedarf es nicht einmal umfangreicher Programmierkenntnisse.

Für komplexere Individualisierungen muss man sich allerdings mit der Syntax von Lua vertraut machen. Schnell haben Sie dann eine ungewohnt individualisierte macOS-Umgebung, die Sie jederzeit optimieren und über eine Textdatei auch auf andere Rechner übertragen können.

Digitalnomaden

Als Beispiel für diesen Artikel dient das Szenario des mobilen Mac-Anwenders, der regelmäßig zwischen Arbeitsplatz, Internetcafé und Homeoffice wechselt. Sein treuer Begleiter, ein Arbeitstier der Gattung MacBook, sieht sich den unterschiedlichsten Umgebungen ausgesetzt: Ständig wechselt das WLAN und auch die Monitore wachsen und schrumpfen je nach Einsatzort. Mal darf das MacBook keinen Mucks von sich geben, beim nächsten Aufklappen kann es nicht genug Krach machen, damit man keine neuen Nachrichten verpasst.

Um den Mac mit der großen Klappe nicht jedes Mal umkonfigurieren zu müssen, soll Hammerspoon die Fensteraufteilung dynamisch dem aktuellen Monitor-­Setup anpassen. Dazu können Sie Tastenkürzel verwenden oder individuelle Menü-­Icons anlegen, die die bevorzugten Arbeitsumgebungen und Funktionen per Mausklick aktivieren.

Installation

Zuerst brauchen Sie Hammerspoon. Am bequemsten installieren Sie das über die mac­OS-Paketverwaltung Homebrew [1] mit dem Terminal-Befehl brew install hammerspoon.

Alternativ können Sie Hammerspoon von dessen GitHub-Projektseite herunterladen und das Programmpaket vom Download- in den Programme-Ordner verschieben. Die Links zu allen erwähnten Programmen und Tools finden Sie über ct.de/yzwk.

Nach einem Doppelklick auf das Programm finden Sie das Hammerspoon-Icon oben rechts in der macOS-Menüzeile. Beim ersten Start öffnen sich meist die Programmeinstellungen – wenn nicht, finden Sie es über das Hammerspoon-Menü unter „Preferences ...“.

Im Dialog signalisiert ein roter Punkt neben der Schaltfläche „Enable Accessibility“, dass Hammerspoon noch die Freigabe braucht, den Mac zu steuern. Das ist nötig, damit Hammerspoon über Lua auf Systemschnittstellen zugreifen darf. Vergessen Sie diese Freigabe am Anfang, bewegt sich später nichts.

Ein Klick auf die Schaltfläche öffnet das Kontrollfeld „Sicherheit/Bedienhilfen“ der Systemeinstellungen. Nach einem weiteren Klick auf das Schloss unten links und der Eingabe des Administratorpassworts können Sie den Haken neben „Hammerspoon“ setzen. Dann wechselt der rote Indikator in den Hammerspoon-Einstellungen auf grün – die Automatisierungsumgebung ist betriebsbereit. Wenn Hammerspoon Sie außerdem fragt, ob es Benachrichtigungen einblenden darf, sollten Sie zustimmen, wenn Sie solche später nutzen wollen. Es empfiehlt sich auch, den Haken neben „Show dock icon“ zu setzen, um per Cmd+Tab von einem anderen Fenster zu Hammerspoon zurückwechseln zu können.

Kopieren Sie die hier vorgestellten Beispiele am besten nicht direkt aus dem Artikel in Hammerspoon, um Probleme durch Umbrüche zu vermeiden, sondern laden Sie die Spoons besser über ct.de/yzwk herunter.

Unaufdringlich wartet Hammerspoon in der Menüleiste auf Eingaben.
In den Sicherheitseinstellungen unter Bedienhilfen muss man Hammerspoon die Freigabe erteilen, macOS steuern zu dürfen.
Nachdem man Hammerspoon die Berechtigung zum Steuern des Macs vergeben hat, signalisiert der grüne Punkt, dass der Automatisierung nichts mehr im Weg steht.

Hallo Hammerspoon

Bevor Sie Spoons als Datei abspeichern, können Sie mit der interaktiven Konsole erste Erfahrungen sammeln: Rufen Sie „Console …“ aus dem Hammerspoon-­Menü auf. Am unteren Rand der Konsole können Sie Befehle ausprobieren, im oberen Teil des Fensters erscheinen dann etwaige Fehlermeldungen, Funktions­resultate und mit dem Befehl print() ausgegebene Meldungen. Die Konsole brauchen Sie später nur, wenn Sie an eigenen Spoons arbeiten.

Die Syntax der Spoons entspricht der Objektnotation, die man in vielen Programmiersprachen findet. Hammerspoon-Methoden stammen von einem Modul ab, alle Module wiederum von hs. Die Funktion show des Moduls alert dient dazu, eine Nachricht auf den Bildschirm zu bringen. Wenn Sie

 hs.alert.show("Hallo Mac")

in die Konsole eingeben und mit Enter bestätigen, erscheint die in Anführungszeichen gesetzte Textnachricht für einen kurzen Moment in der Bildschirmmitte in einer grau hinterlegten Overlay-Einblendung.

Die Anführungszeichen in der Klammer benötigen Sie immer dann, wenn Sie einen String übergeben: hs.alert.show(hello) blendet nil in der Bildschirmmitte ein, da Lua in dem Fall eine Variable namens hello auszugeben versucht, die es nicht gibt. Mit

hello = "Hallo Mac"

definieren Sie die Variable hello. Wiederholen Sie jetzt das Experiment, erscheint statt „nil“ „Hallo Mac“. Wenn Sie „Reload Config“ im Menü wählen, vergisst die Konsole die Variable wieder.

Kurz gefasst

Die Konsole ist essenziell beim Programmieren der eigenen Hammerspoon-Konfiguration – umso wichtiger ist es, sie schnell aufrufen zu können. Darum soll sie ein eigenes Tastenkürzel bekommen. Die passende Funktion heißt hs.hot­key.bind() und erwartet mehrere Argumente hintereinander. Die ersten beiden sind Modifikator und Taste. Damit es keine Konflikte mit anderen Programmen gibt, kann man zur Sicherheit Tastenkürzel belegen, bei denen Ctrl-, Option- und Command-Taste gleichzeitig gedrückt werden müssen – also beispielsweise Ctrl+Option+­Command+Y:

hs.hotkey.bind(
 {"ctrl", "alt", "cmd"}, "Y",
 "Hammerspoon-Konsole",
 hs.toggleConsole)

Nach Modifikator und Taste folgt als drittes Argument (optional) ein Text, der beim Auslösen als Overlay angezeigt wird (ähnlich wie bei hs.alert.show()). Die Funktion hs.toggleConsole() ist die Aktion, die von der Tastenkombination ausgelöst werden soll. Die Funktion toggleConsole() gehört zu den Kernfunktionen des Hammerspoon-Pakets, die Sie in der API-Dokumentation unter „hs“ finden; den Link zur Doku finden Sie unter ct.de/yzwk.

Führen Sie den Schnipsel in der Konsole aus und testen Sie das Tastenkürzel: Das Konsolenfenster verschwindet und taucht beim zweiten Aufruf wieder auf.

Diese Einstellung ist noch flüchtig. Mit einem Klick auf „Reload Config“ oder nach einem Neustart ist sie wieder weg. Um sie zu verewigen, überführen Sie den Befehl in die Konfigurationsdatei ~/.hammerspoon/init.lua im Heimverzeichnis des aktuellen Benutzers. Nur was dort steht, überdauert einen Neustart und einen Klick auf „Reload Config“. Am besten bearbeiten Sie die Datei mit einem Text-Editor, der Zeilennummern anzeigt und sich auf Lua-Syntax versteht. Wenn Sie noch keine bevorzugte IDE haben, können Sie zum Beispiel die Open-Source-IDE VSCode ausprobieren.

Kopieren Sie die Konsoleneingabe in die Datei init.lua und speichern Sie sie. Über den Befehl „Reload Config“ übergeben Sie die Änderung an Hammerspoon. Alle weiteren Beispiele entstehen jetzt direkt in init.lua, sofern nicht explizit etwas anderes erwähnt wird. Die Konsole lassen Sie am besten geöffnet, um keine Fehlermeldungen zu übersehen.

Ein Tipp: Wird Ihnen init.lua zu unübersichtlich, können Sie Funktionen und Variablen in eine separate Datei mit der Endung .lua im selben Ordner (~/.hammerspoon) auslagern. Die Datei example.lua binden Sie zum Beispiel über require("example") ein. Wenn Hammerspoon verschiedene Aufgaben für Sie erledigen soll, sollten Sie am besten für jede eine eigene Datei benutzen. Die Init-Datei enthält dann nur noch eine Reihe von Einbindungen.

Auf den Schirm

Im nächsten Schritt soll Hammerspoon zwei Programmfenster arrangieren. Das kann ganz nützlich sein, wenn man zum Beispiel immer wieder eine Aufgabe erledigen muss und dafür mehrere Programmfenster braucht. Zwei Szenarien soll es geben: eines zum Arbeiten und ein zweites zum Lesen und Kommentieren von Dokumenten.

Dafür bauen Sie sich für jeden Aufgabenbereich eine eigene Funktion mit den Namen loadWorkingSpace und load­Reading­Space. Hinter dem Funktionskopf function loadWorkingSpace() folgen die Befehle, die beim Aufruf der Funktion ausgeführt werden. Am Ende schließt ein end die Funktionsdefinition ab. Direkt danach oder weiter unten im Dokument verknüpft dann jeweils ein Aufruf von hs.hotkey.bind() ein Tastenkürzel mit der Funktion. Bei Lua ist, anders als bei vielen anderen Skriptsprachen, die Reihenfolge wichtig: Eine Funktion wie load­Working­Space() muss zuerst definiert sein, bevor Sie sie aufrufen können.

Alle Fenster sauber aufgereiht, kein Platz verschenkt – das ist der tägliche Lohn für die einmalige Konfigurationsarbeit.

Für das Arrangement mehrerer Programmfenster muss man zuerst eine verschachtelte Datenstruktur zusammenbauen, die man dann an hs.layout.apply übergibt. Der Kasten „Bildschirmaufteilung“ erklärt, wie Sie ein solches Layout ent­werfen.

Mit diesem Wissen können Sie sich an die Aufgabe machen, einen Lesemodus für PDF-Dateien zu bauen. Dabei soll der Mac den Öffnen-Dialog zur Auswahl eines PDFs anzeigen, der mit bereits eingeblendeten Markierungswerkzeugen die linke Bildschirmhälfte einnimmt. Die rechte Bildschirmhälfte soll die Notizen-App ­füllen und dabei in den Ordner „Lesen“ wechseln. So können Sie mit einem Aufruf einen Text lesen und Ihre Gedanken dazu notieren.

Der Funktionsaufruf

loadPdf =  hs.dialog.chooseFileOrFolder(
    "PDF wählen:", -- Fragetext
    "~/Dokumente", -- Standardordner
    true, -- Dateiauswahl
    false, -- keine Ordnerauswahl
    false, -- keine Mehrfachauswahl
    {"pdf"}, -- nur PDFs
    true -- Aliasse auflösen
  ) 

erzeugt einen Dialog zum Auswählen einer Datei. Mit -- werden Kommentare im Lua-Code gekennzeichnet.

In der Variable loadPdf liegt anschließend eine Tabelle mit den ausgewählten Dateien. Der erste Tabelleneintrag enthält den Dateipfad, auf den Sie mit loadPdf["1"] zugreifen können. Diesen Pfad können Sie mittels hs.execute einem Kommandozeilenbefehl übergeben, um die PDF-Datei mit der macOS-Vorschau (engl. „Preview“) zu öffnen:

hs.execute("open -a 'Preview' '" 
  .. loadPdf["1"].."'")

Hammerspoon kann also mit klassischen Kommandozeilenbefehlen kombiniert werden. Mit zwei Punkten verknüpft man Strings in Lua. Vorschau wird mit dieser Zeile instruiert, die zuvor ausgewählte Datei zu öffnen.

Doch Hammerspoon führt nicht nur Terminalbefehle aus, sondern kann auch AppleScript starten: Die Funktion hs.osascript.applescript() verarbeitet Einzeiler in Apples Skriptsprache. Darüber kann man etwa das Programm „Notizen“ dazu bringen, den Ordner „Lesen“ anzuzeigen:

hs.osascript.applescript(
[[tell application "Notes" to show 
folder "Lesen" of account "iCloud" ]])

Wenn Sie innerhalb eines Strings Anführungszeichen brauchen (hier zum Beispiel, weil die AppleScript-Zeile welche erwartet), dürfen Sie den String nicht in Anführungszeichen einrahmen. Lua erlaubt als Alternative für solche Fälle die Einrahmung in [[ ]].

Die Projekt-Website beschreibt die richtige Anwendung der Module und ihrer Funktionen.

So hat es Methode

Außerdem wäre es praktisch, wenn die Werkzeugleiste in Vorschau automatisch geöffnet wird, damit man direkt mit dem Markieren loslegen kann. Hammerspoon kann für Sie Elemente in der Menüleiste virtuell anklicken – und damit zum Beispiel die Werkzeugleiste aufklappen:

local preview = hs.application.get("Vorschau")
preview:selectMenuItem({"Darstellung", "Werkzeugleiste einblenden"})

Um die Zeilen zu verstehen, ist noch etwas Syntaxkunde in Lua nötig. selectMenuItem() ist eine sogenannte Methode, die auf ein Objekt angewendet wird (eingeleitet mit einem :). Ein solches Objekt (hier mit dem Namen preview) wird vorher mit einem Konstruktor instanziiert.

Das Modul hs.application hat drei Konstruktoren – find, open und get. Da Apples PDF-Viewer bereits weiter oben gestartet wurde, genügt get("Vorschau"), um ein Objekt des Typs hs.application in der Variable preview anzulegen. Als Argument erwartet selectMenuItem den lokalisierten (eingedeutschten) Menübefehl, also Menüname und Unter-Menüeintrag, in Lua-Tabellenschreibweise.

Im Listing „Leseansicht“ oben sehen Sie die fertige Funktion load­Reading­Space(). Diese wird direkt an das Tastenkürzel Cmd+Alt+Ctrl+L gebunden. Dass hier Deutsch und Englisch gemischt werden, lässt sich nicht vermeiden: Hammerspoon liest lokalisierte Menü- und Programmnamen aus, während Terminal und AppleScript nur die englischen Bezeichnungen verstehen.

Multi-Monitor

Zusätzlich zur Leseansicht entsteht jetzt eine Arbeitsansicht. Der Clou: Diese soll sich automatisch an verschiedene Bildschirmkonfigurationen anpassen. An einem großen Monitor soll der mobile Mac ein anderes Layout anzeigen und Apps zwischen internem und externem Bildschirm aufteilen. Dafür muss Hammerspoon beim Ausführen der Tastenkombination Cmd+Alt+Ctrl+A die aktuell angeschlossenen Monitore zählen. Diese Aufgabe kommt in eine eigene Funktion getMonitorCount(), damit man sie wiederverwenden kann.

Im Kasten „Arbeitsansicht“ auf Seite 151 sehen Sie ein Beispiel für verschiedene Arbeitsumgebungen. Wenn nur ein Monitor erkannt wird, werden Kalender, Notizen und Terminal auf diesem bildschirmfüllend angeordnet. Bei zwei Monitoren wird der Name des angeschlossenen Monitors ausgewertet: Heißt er „LG 5K“, wird ein anderes Setup geladen. So entstehen Layouts für unterschiedliche Monitorkombinationen, etwa für zu Hause oder im Büro. Um herauszufinden, wie Ihr Monitor genau heißt, damit Sie die Zeile anpassen können, geben Sie den Befehl

print(hs.screen.allScreens()[2]:name()) 

in der Hammerspoon-Konsole ein.

Aber Vorsicht: Diese Funktion geht davon aus, dass die betreffenden Programme bereits laufen. Wurde eine App beendet oder wurden alle Fenster geschlossen, klafft eine Lücke im Layout. Um eine App, etwa den Kalender, zu öffnen, schreibt man den Funktionsaufruf hs.application.launchOrFocus("Kalender") an den Anfang der jeweiligen Funktion.

Möglichkeiten, um per Skript verschiedene Umgebungen zu erkennen, gibt es viele: Neben Displays kann Hammerspoon eine Menge anderer Systemvariablen abfragen, etwa USB-Geräte (hs.usb.attachedDevices()), Audiogeräte (hs.audio.allOutputDataSources()), WLANs (hs.wifi.currentNetwork()) oder derzeit aktive Programme (hs.application.running­Applications()). Zur Spoon-Entwicklung hilft es, diese schnell in der Hammerspoon-Konsole anzuzeigen, etwa:

print(hs.wifi.currentNetwork())

Menü nach Wahl

Zwischen Bildschirmlayouts per Tastenkombination zu wechseln ist schon bequem; noch schicker ist aber ein eigenes Menü in der Menüleiste mit allen Einträgen. Auch das ist schnell gebaut: Das Grundgerüst entsteht in einer Variablen, welches das Menü in einer verschachtelten Tabellenstruktur abbildet. Für einen ersten Test genügt eine doppelte geschweifte Klammer mit einem Test-Menüeintrag:

testMenu = {{ title = "Test" }}

Da Hammerspoon mehrere Menüs verwalten kann, muss wieder ein Konstruktor bemüht und dann das Menü per Methode befüllt werden. Der Befehl

menu =  hs.menubar.new():setIcon("icon.pdf")

definiert die Variable menu. Dieses bekommt sogleich mit der Methode :setIcon() ein Icon zugewiesen. Der Aufruf

menu:setMenu(testMenu)

richtet dann das Menü mit der einzelnen Testzeile ein.

Das Icon müssen Sie im Ordner neben der Datei init.lua bereitstellen. Wir haben Ihnen eine schwarze Zeichnung auf transparentem Hintergrund in der Größe 16 × 16 Pixel zusammen mit dem Code zum Download bereitgestellt (siehe ct.de/yzwk). Die als PDF gespeicherte Vektorgrafik nimmt lediglich 6 KByte ein.

Wenn das Bild bereitliegt und Sie die drei Zeilen Code in die Datei init.lua eingefügt haben, laden Sie die Hammerspoon-Konfiguration neu. Wenn alles funktioniert hat, erscheint ein neues Icon, das ein noch funktionsloses Menü beherbergt.

Mit dem selbst erstellten Menü gelingt der Wechsel zwischen verschiedenen Layouts schnell.

Zeit, das Menü mit Leben zu füllen: Jede Zeile bekommt einen Titel, der dann als Menüzeile verwendet wird, und eine Funktion, die beim Klick ausgeführt wird. Anstelle einer Funktion kann man auch eine weitere Tabelle mit ebenso strukturierten Menüeinträgen einfügen – sie erscheint dann als Untermenü.

Die Konfiguration im Code-Beispiel unten rechts versammelt vier Fensterarrangements in einem Untermenü namens „Umgebung“. Ein weiteres Untermenü mit dem Namen „Hammerspoon“ enthält die wichtigsten (und dabei gleich eingedeutschten) Befehle von Hammerspoon. Der oberste, inaktive Eintrag zeigt die Anzahl aktuell angeschlossener Monitore an – die Funktion getMonitorCount() aus der Layoutgestaltung lässt sich dafür prima recyclen.

Da das Menü später dynamisch auf aktuelle Begebenheiten reagieren soll, wird es von einer eigenen Funktion dynamisch erzeugt, die nach der Instanziierung einmalig aufgerufen wird:

Ein praktischer Nebeneffekt: Da alle Menüpunkte der Automatisierungslösung nun im persönlichen Menü integriert sind, können Sie das Hammerspoon-Menü in dessen Einstellungen deaktivieren. Die vielen Leerzeichen zwischen dem Namen des Menüeintrags und dem zugehörigen Kürzel kommen dadurch zustande, dass macOS nichtproportionale Schriftsätze für seine Menüs nutzt. Mit etwas Ausprobieren gelingt es, die in Mac-Apps nativ rechtsbündig angezeigten Tastenkürzeln zu simulieren.

Wächter des verlorenen Monitors

Bisher sind die definierten Funktionen in der Datei init.lua statisch: Damit das Skript auf eine Veränderung reagiert, müssen erst ein Tastenkürzel aufgerufen oder ein Menüeintrag angeklickt werden. Auch die Zählung der Monitore weiter oben musste explizit per Funktionsaufruf erfolgen.

Das geht besser: Hammerspoon bringt Module mit, die aktiv bestimmte Einstellungen beobachten und Funktionen auslösen, wenn sich etwas verändert. Sie tragen allesamt watcher im Namen, sind fast immer als Submodul integriert und erfordern eine separate Funktion (eine sogenannte Callback-Funktion), die sie bei einer beobachteten Veränderung aufrufen. Da im Beispiel die Menüerstellung mit der Funktion createMenu() erzeugt wird, können Sie auch diese als Callback verwenden.

Damit ein Watcher dauerhaft aktiv bleibt, weist man ihn mittels des Kons­truktors new() einer Variablen zu. Der Konstruktor erwartet den Namen der Callback-Funktion in den Klammern. Danach aktiviert man sie mit der Methode start() – durch direktes Anhängen per Doppelpunkt:

sW = hs.screen.watcher.new(createMenu):start()

So wird im Menü stets die aktuelle Anzahl an Monitoren angezeigt.

Offen für Neues

Anhand dieses Beispiels haben Sie einen Überblick bekommen, was mit Hammerspoon möglich ist. Mit individuellen Tastenkürzeln, variablen Fensterlayouts und eigenem Menü ist der Mac gut aufgestellt für weitere Anpassungen.

Auf der Projektseite von Hammerspoon finden Sie im Bereich „Spoons“ viele vorgefertigte Erweiterungen, die nach dem Download per Doppelklick in den Ordner ~/.hammerspoon/spoons bewegt werden müssen. Ein paar Zeilen in der init-Datei aktivieren deren Fähigkeiten. Wie das funktioniert, erklärt das englischsprachige „Getting Started“-Tutorial auf der Projektseite und liefert einige hilfreiche Einstellungen. Dann sind dem kreativen Anpassen der eigenen macOS-Umgebung kaum Grenzen gesetzt. (jam@ct.de)

Weitere Infos: ct.de/yzwk

Kommentieren