Skip to content

Instantly share code, notes, and snippets.

@fopina
Last active April 1, 2025 09:07
Show Gist options
  • Save fopina/71345e937195d88f98899420f147d8b5 to your computer and use it in GitHub Desktop.
Save fopina/71345e937195d88f98899420f147d8b5 to your computer and use it in GitHub Desktop.
Script to pair/unpair magic mouse and keyboard
#!/usr/bin/env python3
"""
Magic Switch
Script to pair/unpair magic mouse and keyboard
Usage:
* Save somewhere in $PATH as `magic-switch`
* Whenever switching magic devices between laptops:
* Run `magic-switch off` on the currently connected one
* Then `magic-switch on` in the new one
"""
import argparse
import subprocess
import urllib.request
import sys
from pathlib import Path
from time import sleep
GIST_URL = "https://gist.githubusercontent.com/fopina/71345e937195d88f98899420f147d8b5/raw/magic_switch.py"
CONFIG_DIR = Path.home() / ".config" / "magic-switch"
CONFIG_FILE = CONFIG_DIR / "devices.txt"
def run_command(command):
"""Runs a shell command and prints output."""
try:
result = subprocess.run(command, capture_output=True, text=True, check=True).stdout.strip()
if result:
print(result)
return 0
except subprocess.CalledProcessError as e:
print(f"Error: {e.stderr.strip()}")
return e.returncode
def print_notify(message, notify=False):
print(message)
if notify:
run_command(['osascript', '-e', f'display notification "{message}" with title "magic-switch"'])
def get_device_ids():
"""Reads device IDs from the config file."""
if not CONFIG_FILE.exists():
print(f"Error: Device list not found. Create {CONFIG_FILE} and add one device ID per line.")
sys.exit(1)
return [line.strip() for line in CONFIG_FILE.read_text().splitlines() if line.strip() and line.strip()[0] != '#']
def bluetooth_on(retries=0, force_connect=False, notify=False):
"""Unpairs and then pairs all devices from the config file."""
devices = get_device_ids()
for device in devices:
print(f"Unpairing {device}...")
run_command(["blueutil", "--unpair", device])
for device in devices:
for _t in range(retries + 1):
print(f"Pairing {device}...")
c = run_command(["blueutil", "--pair", device])
if c == 0:
break
if c == 0:
print_notify(f"Paired {device}...", notify=notify)
if force_connect:
sleep(1)
run_command(["blueutil", "--connect", device])
else:
print_notify(f"FAILED to pair {device} ({c})...", notify=notify)
def bluetooth_off(notify=False):
"""Unpairs all devices from the config file."""
devices = get_device_ids()
for device in devices:
print(f"Unpairing {device}...")
c = run_command(["blueutil", "--unpair", device])
if c == 0:
print_notify(f"Unpaired {device}...", notify=notify)
else:
print_notify(f"FAILED to unpair {device}...", notify=notify)
def update_script():
"""Checks for updates and replaces the script if confirmed."""
print("Checking for updates...")
try:
with urllib.request.urlopen(GIST_URL) as response:
latest_content = response.read().decode("utf-8")
except Exception as e:
print(f"Error: Failed to fetch latest script from Gist ({e})")
sys.exit(1)
script_path = Path(__file__)
current_content = script_path.read_text(encoding="utf-8")
if current_content.strip() == latest_content.strip():
print("The script is already up to date.")
return
print("A new version is available. Do you want to update? (y/N): ", end="")
choice = input().strip().lower()
if choice == "y":
script_path.write_text(latest_content, encoding="utf-8")
print("Script updated successfully! Restarting...")
sys.exit(0) # Exit cleanly (user can restart manually)
else:
print("Update canceled.")
def ensure_config_file():
"""Ensures the configuration directory and file exist."""
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
if not CONFIG_FILE.exists():
CONFIG_FILE.write_text("# Add one device ID per line\n", encoding="utf-8")
print(f"Created config file at {CONFIG_FILE}. Please add your device IDs.")
def main():
parser = argparse.ArgumentParser(description="Control Bluetooth devices using blueutil.")
parser.add_argument('--notify', action='store_true', help='Show notification after connecting or disconnecting devices')
subparsers = parser.add_subparsers(dest="command", required=True)
on_cmd = subparsers.add_parser("on", help="Unpair and re-pair all devices.")
on_cmd.add_argument('--retries', '-r', type=int, default=0, help="Number of pairing retries, as it sometimes fails for no good reason.")
on_cmd.add_argument('--connect', '-c', action="store_true", help="Force connect after pairing, as it sometimes does not connect automatically.")
subparsers.add_parser("off", help="Unpair all devices.")
subparsers.add_parser("update", help="Check for script updates.")
args = parser.parse_args()
ensure_config_file()
if args.command == "on":
bluetooth_on(retries=args.retries, force_connect=args.connect, notify=args.notify)
elif args.command == "off":
bluetooth_off(notify=args.notify)
elif args.command == "update":
update_script()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment