Albert Oviedo

SwiGi: Syncing Logitech Easy-Switch Across Keyboard and Mouse on macOS

How I turned a Python HID++ script into a native macOS menu bar app that keeps Logitech Bluetooth keyboard and mouse on the same host.

  • macos
  • swift
  • hid
  • open-source

If you run a Logitech keyboard and mouse over Bluetooth with Easy-Switch, you have probably hit this annoyance: press the host button on the keyboard and the machine changes — but the mouse stays connected to the previous host. Both devices advertise Easy-Switch, yet nothing keeps them in sync.

SwiGi closes that gap. It listens for host-change events on the keyboard and forwards the same HID++ CHANGE_HOST command to the mouse so both land on the same machine.

The problem in one sentence

Logitech devices speak HID++ over Bluetooth, but Easy-Switch on the keyboard does not automatically propagate to paired peripherals.

From script to app

The project started as a self-contained Python script — swigi.py — with a single runtime dependency on hidapi. That proved the protocol, device discovery, and reconnect logic worked. The next step was packaging it as something you could leave running in the background without a terminal window.

The macOS branch is a Swift port distributed as a menu bar agent:

  • No Dock icon (LSUIElement)
  • Start / Stop / Quit from the menu bar
  • Status icon reflects engine state (stopped, starting, running, error)
  • Pre-built binary for Apple Silicon, macOS 26+ in releases/

Other platforms live on separate branches: windows-11 (tray app) and macos-13 (Intel Mac, older macOS). On macOS 12 Monterey, the Python script remains the supported path.

How it works

SwiGi runs a tight loop built around Logitech’s HID++ protocol:

  1. Discover keyboard and mouse over Bluetooth via hidapi, filtering by Logitech vendor ID and known usage pairs
  2. Subscribe to CHANGE_HOST (0x1814) notifications from the keyboard
  3. Forward the matching host switch command to the mouse when Easy-Switch fires
  4. Reconnect automatically if either device drops off Bluetooth

At the protocol layer, messages use HID++ short/long report formats. Device discovery scores candidates by usage page — vendor-specific pages (0xFF00, 0xFF43, 0xFF0C) rank above generic desktop keyboard/mouse entries — so the right Bluetooth endpoints are chosen on a busy machine.

Architecture (Swift)

The native app splits concerns cleanly:

ModuleRole
DeviceDiscovery.swiftEnumerate hidapi devices, score candidates, open keyboard/mouse handles
HIDPPProtocol.swiftBuild and parse HID++ frames (ping, feature queries, CHANGE_HOST)
HIDTransport.swiftRead/write raw HID reports with timeouts
SwiGiEngine.swiftAsync worker loop, status publishing, reconnect on failure
MenuBarContentView.swiftSwiftUI controls for Start / Stop / verbose logging

hidapi is linked statically from vendor/hidapi-static/ — no Homebrew dependency at runtime. A thin CHIDAPI module map bridges the C library into Swift.

The engine runs on a detached task and reports state back to the main actor, which drives the menu bar icon (arrow.triangle.2.circlepath variants for idle, active, and error).

Build and release tooling

Source builds require Xcode 15+ and macOS 26+. Supporting scripts automate the boring parts:

  • scripts/generate-app-icon.sh — icon set from a 1024×1024 master asset
  • scripts/package-release.sh — zip a signed-ready .app for distribution
  • scripts/build-hidapi-static.sh — produce the static library vendored into the Xcode project

What I learned

Protocol work rewards patience. HID++ is documented unevenly; the Python reference implementation was the Rosetta stone for the Swift port. Keeping constants (FEATURE_CHANGE_HOST, device types, report lengths) in one place (HIDPPConstants) made both code paths easier to compare.

Menu bar apps are a good fit for hardware daemons. Users do not want a Dock tile for a background synchronizer — they want a small icon, a Start button, and the confidence that keyboard and mouse will follow each other.

Static linking simplifies distribution. Shipping libhidapi.a inside the repo means the release zip is self-contained. Download, unzip, move to /Applications, allow the unsigned app once in Privacy & Security, click Start.

Try it

If you maintain a multi-machine desk with Easy-Switch, SwiGi removes the extra click on the mouse every time you change hosts. Pull requests and issue reports are welcome on the repo.