Keyboards, Layouts und ich

Kurz vor Weihnachten, fast schon Urlaub und dazu noch der Lockdown, sprich endlich mal wieder ein wenig Zeit um privat ein bisserl was IT-technisches zu machen.

Die Aufgabenstellung war wie folgt. Als Arbeitsgerät habe ich ein Macbook mit englischer Tastatur (inkl. Bluetooth Keyboard ebenfalls englisch), privat habe ich hingegen ein Gerät mit einem deutschsprachigen Keyboard im Einsatz.

Nachdem ich mich inzwischen an das englische Layout gewöhnt habe, nutze ich die Bluetooth Tastatur auch gerne in Kombination mit meinem privaten Mac. Bisher - wohl auch meiner Trägheit geschuldet - hab ich entweder die Layout Settings manuell umgestellt oder schlicht und ergreifend die Transferleistung erbracht, die richtigen Tasten zu drücken (wo waren nochmal z und y :-))

Da dies aber kein Dauerzustand ist, bin ich mal wieder ein wenig Forschen gegangen und dabei auf keyboardSwitcher gestossen einem netten kleinen Tool mit dem man die Tastatureinstellungen manipulieren kann. Mittels der list  gibt man alle verfügbaren Layouts aus, select inkl. der Angabe des Layouts stellt selbiges ein.

Auf der Webseite des Projektes ist recht detailliert beschrieben, wie man in Kombination mit der App Controlplane die Einstellungen anpasst, sobald ein Bluetooth Keyboard angeschlossen bzw. entfernt wird. Nachdem der Funktionsumfang von Controlplane aber viel grösser als meine Bedarf ist, habe ich mir mal das Thema LaunchAgent unter MacOs etwas genauer angesehen. https://medium.com/swlh/how-to-use-launchd-to-run-services-in-macos-b972ed1e352 war hierbei mein Einstiegspunkt in das Thema.

Die Definition des LaunchAgent, habe ich hierbei unter ~/Library/LaunchAgents/com.apple.bluetoothkbd.plist hinterlegt

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>

    <key>Label</key>
    <string>Bluetooth keyboard</string>

    <key>ProgramArguments</key>
    <array>
       <string>/Users/kurt/.local/scripts/kbd.py</string>
    </array>

    <key>WatchPaths</key>
    <array>
       <string>/Library/Preferences/com.apple.Bluetooth.plist</string>
    </array>

</dict>
</plist>

Das Label spezifiziert den Namen unter dem wir den Job später identifizieren können. ProgramArguments enthält den Pfad zum - in unserem Fall Python - Skript, welches die eigentliche Umstellung des Keyboard-Layouts mittels keyboardSwitcher vornimmt. Mittels WatchPaths definiert man das File, welches überwacht wird. Wird die Datei modifiziert wird das hinterlegte Programm angestossen.

/Library/Preferences/com.apple.Bluetooth.plist ist hierbei die für Bluetooth Geräte relevante Datei.

Das zugehörige Python Skript parst den Output des system_profiler Aufrufs und schaut ob  das spezifizierte Device (in meinem konkreten Fall "Kurt Klinner's Keyboard) verbunden und getrennt ist.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Check if a bluetooth device is connected or not on macOS.
"""

import sys
import subprocess
from plistlib import loads as read_plist_from_string

def check_bluetooth_connection(device_identifier):
    """check if the specified device is connected/disconnected"""
    output = subprocess.check_output(["system_profiler",
                                      "-xml",
                                      "-detailLevel",
                                      "basic",
                                      "SPBluetoothDataType"], stderr=subprocess.DEVNULL)

    plist = read_plist_from_string(output)
    devices = plist[0]['_items'][0]['device_title']

    for device in devices:
        if device_identifier in device.keys():
            device_info = device[device_identifier]
            break
    else:
        msg = u"\"{}\" not found".format(device_identifier)
        raise ValueError(msg)

    if device_info['device_isconnected'] == "attrib_Yes":
        return True


device_name = "Kurt Klinner’s Keyboard"

try:
    is_connected = check_bluetooth_connection(device_name)
except (OSError, subprocess.CalledProcessError) as error:
    print(error)
    print("Failed to run system_profiler")
    sys.exit(1)
except ValueError as error:
    print(error)
    sys.exit(1)

if is_connected:
    subprocess.run(["/usr/local/bin/keyboardSwitcher", "select", "ABC"],
                    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                    check=False)
else:
    subprocess.run(["/usr/local/bin/keyboardSwitcher", "select", "German"],
                    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                    check=False)

Mit der aktuellen MacOS Version hat sich Datenstruktur bzw. Attributnamen etwas verändert von daher hier die angepasste Version

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Check if a bluetooth device is connected or not on macOS.
"""

import sys
import subprocess
from plistlib import loads as read_plist_from_string

def check_bluetooth_connection(device_identifier):
    """check if the specified device is connected/disconnected"""
    output = subprocess.check_output(["system_profiler",
                                      "-xml",
                                      "-detailLevel",
                                      "basic",
                                      "SPBluetoothDataType"], stderr=subprocess.DEVNULL)

    plist = read_plist_from_string(output)
    devices = plist[0]['_items'][0]['devices_list']
    #devices = plist[0]['_items'][0]['device_title']

    for device in devices:
        if device_identifier in device.keys():
            device_info = device[device_identifier]
            break
    else:
        msg = u"\"{}\" not found".format(device_identifier)
        raise ValueError(msg)

    if device_info['device_connected'] == "Yes":
        return True


device_name = "Kurt Klinner’s Keyboard"

try:
    is_connected = check_bluetooth_connection(device_name)
except (OSError, subprocess.CalledProcessError) as error:
    print(error)
    print("Failed to run system_profiler")
    sys.exit(1)
except ValueError as error:
    print(error)
    sys.exit(1)

if is_connected:
    subprocess.run(["/usr/local/bin/keyboardSwitcher", "select", "ABC"],
                    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                    check=False)
else:
    subprocess.run(["/usr/local/bin/keyboardSwitcher", "select", "German"],
                    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
                    check=False)

Nicht vergessen, das Skript ausführbar zu machen und mittels

launchctl load ~/Library/LaunchAgents/com.apple.bluetoothkbd.plist 

den Launcher zu aktivieren und los gehts

In diesem Sinne fröhliches Layout wechseln.