Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,6 @@ CLAUDE.md

# Node tooling (semantic-release)
node_modules/

# Raw screenshot captures (local inputs to docs/annotate-screenshots.sh)
docs/assets/raw/
20 changes: 20 additions & 0 deletions Magic Switch/Model/Store/NetworkDeviceStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,7 @@ enum ManualAddError: Error {
case selfDial
case legacyPeer
case anotherMacRegistered(String)
case listenerNotReady
case outgoing(OutgoingFailure)

var userMessage: String {
Expand All @@ -845,6 +846,19 @@ enum ManualAddError: Error {
"The other Mac runs an older version of Magic Switch that can't be added by address. Update it first."
case .anotherMacRegistered(let name):
return "Only one Mac can be connected at a time. Remove \(name) first."
case .listenerNotReady:
return "This Mac isn't accepting connections yet. Try again in a moment."
case .outgoing(.connectTimeout), .outgoing(.connectionFailed(_)),
.outgoing(.handshakeFailed(.handshakeTimeout)),
.outgoing(.handshakeFailed(.connectionClosed)),
.outgoing(.handshakeFailed(.sendFailed(_))),
.outgoing(.handshakeFailed(.framingFailed)),
.outgoing(.handshakeFailed(.frameTooLarge)):
// An unpaired peer refuses the connection before the handshake, so it
// is indistinguishable here from a firewall drop, a dead host, or a
// different service answering on the port.
return
"Couldn't reach the other Mac securely. Check that it's running Magic Switch and paired with the same code (Settings → Pairing), and that no firewall is blocking the port."
case .outgoing(let failure):
return failure.userMessage
}
Expand Down Expand Up @@ -1258,6 +1272,12 @@ extension NetworkDeviceStore {
host: String, port: UInt16,
completion: @escaping (Result<NetworkDevice, ManualAddError>) -> Void
) {
// Without a bound listener there's no local identity to INTRODUCE with;
// `executeIntroduce` would report it as a peer-shaped connection failure.
guard servicePublisher.currentIdentity() != nil else {
completion(.failure(.listenerNotReady))
return
}
let target = NetworkDevice(id: host, name: host, host: host, port: Int(port))
executeIntroduce(on: target) { [weak self] result in
DispatchQueue.main.async {
Expand Down
40 changes: 23 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This is a security-hardened fork of [HoshimuraYuto/blue-switch](https://github.c

<p align="center">
<img src="docs/assets/menu.png" alt="The Magic Switch menu-bar dropdown" width="340"><br>
<em>It lives in the menu bar: click a Mac to move every peripheral to it, or a single peripheral to move just that one. A checkmark marks whatever's on this Mac right now.</em>
<em>It lives in the menu bar: click a Mac to move every peripheral to it, or a single peripheral to move just that one. A checkmark marks whatever's on this Mac right now, with its battery level.</em>
</p>

## Installation
Expand All @@ -33,7 +33,7 @@ Then launch Magic Switch, grant **Bluetooth** and **Local Network** when prompte

### 2. Peripheral tab — choose what to manage

Tick the Magic devices you want Magic Switch to hand back and forth. Each row's leading icon shows the detected device type; click it to override the type or reset it to Automatic.
Tick the Magic devices you want Magic Switch to hand back and forth. Each row's leading icon shows the detected device type; click it to override the type or reset it to Automatic. Rows show a live battery level while the peripheral is connected to this Mac.

<p align="center">
<img src="docs/assets/peripheral-tab.png" alt="Peripheral tab showing registered and available peripherals" width="600"><br>
Expand All @@ -45,20 +45,9 @@ Tick the Magic devices you want Magic Switch to hand back and forth. Each row's
<em>That leading icon is also a picker — Magic Switch auto-detects the type (keyboard, mouse, trackpad, headphones, AirPods, microphone), and you can override it or set it back to Automatic.</em>
</p>

### 3. Macs tab — pick the other Mac
### 3. Pairing tab — link the two Macs (required)

Choose the other Mac under **Macs Found on the Network**. It shows up once it's on the same network running Magic Switch; a greyed-out row means it isn't reachable right now.

If your network blocks Bonjour (some MDM-managed Macs can't advertise; some Wi-Fi networks filter multicast), the other Mac may never appear here. Use the **+** button to add it by IP address instead — the sheet on each Mac shows the address and port to enter on the other one, and one side is enough: the other Mac then lists this one automatically. Magic Switch listens on TCP port **41952**; allow it through any firewall between the two Macs.

<p align="center">
<img src="docs/assets/device-tab.png" alt="Macs tab showing the connected Mac and available Macs" width="600"><br>
<em>Macs tab — pick the other Mac, sync peripherals to it, and check it's reachable.</em>
</p>

### 4. Pairing tab — link the two Macs (required)

Generate a twelve-character code on one Mac and enter it on the other; either direction works, since both Macs derive the same key from the same code. They should then show the same eight-character fingerprint — if they differ, the code was mistyped. Until this is done, switching and peripheral sync refuse to talk to the peer.
Generate a twelve-character code on one Mac and enter it on the other; either direction works, since both Macs derive the same key from the same code. They should then show the same eight-character fingerprint — if they differ, the code was mistyped. Until this is done, switching and peripheral sync refuse to talk to the peer, and an unpaired Mac can't be added by address in the next step. (Pairing needs no connection between the Macs; the code alone is enough.)

<p align="center">
<img src="docs/assets/pairing-not-paired.png" alt="Pairing tab before pairing" width="600"><br>
Expand All @@ -76,6 +65,22 @@ Generate a twelve-character code on one Mac and enter it on the other; either di
<em>After pairing — both Macs show the same fingerprint. If they differ, the code was mistyped.</em>
</p>

### 4. Macs tab — pick the other Mac

Choose the other Mac under **Macs Found on the Network**. It shows up once it's on the same network running Magic Switch; a greyed-out row means it isn't reachable right now.

If your network blocks Bonjour (some MDM-managed Macs can't advertise; some Wi-Fi networks filter multicast), the other Mac may never appear here. Use the **+** button to add it by IP address instead — the sheet on each Mac shows the address and port to enter on the other one, and one side is enough: the other Mac then lists this one automatically. Adding by address only works once **both Macs are paired** (step 3) and running Magic Switch — an unpaired Mac refuses the connection, and the add fails. Magic Switch listens on TCP port **41952**; allow it through any firewall between the two Macs.

<p align="center">
<img src="docs/assets/macs-tab.png" alt="Macs tab showing the connected Mac and available Macs" width="600"><br>
<em>Macs tab — pick the other Mac, sync peripherals to it, and check it's reachable.</em>
</p>

<p align="center">
<img src="docs/assets/add-by-address.png" alt="The Add a Mac by Address sheet" width="600"><br>
<em>Add a Mac by Address (the <strong>+</strong> button) — the sheet shows this Mac's address and port to enter on the other one.</em>
</p>

### 5. Sync your peripherals to the other Mac

On the **Macs** tab, find the other Mac under **Your Other Mac** and click its **Sync** button (the circular arrows, beside **Ping**). A "Synced N peripherals to …" line confirms it. The button is greyed out while that Mac is offline.
Expand All @@ -85,8 +90,8 @@ On the **Macs** tab, find the other Mac under **Your Other Mac** and click its *
**Launch at Login**, recordable **keyboard shortcuts** that send, take, or toggle every peripheral from anywhere in macOS, two peripheral-handling toggles (**Release peripherals when this Mac sleeps** and **Reconnect peripherals if they drop** — see [Troubleshooting](#troubleshooting)), a **Take peripherals when a display connects** list (mark a display to make docking this Mac to it switch your peripherals over automatically — see [Troubleshooting](#troubleshooting)), the installed version, and update notifications (see [Updates](#updates)). A **"Notifications are off"** warning appears at the top of this tab when macOS notifications are disabled for Magic Switch — see [Troubleshooting](#troubleshooting) for why that matters.

<p align="center">
<img src="docs/assets/other-tab.png" alt="Other tab showing app preferences" width="600"><br>
<em>Other tab — Launch at Login, the sleep-release and auto-reconnect toggles, license info, version, and a manual update check.</em>
<img src="docs/assets/other-tab.png" alt="Other tab showing all app preferences" width="600"><br>
<em>Other tab — Launch at Login, the peripheral-handling toggles, keyboard shortcut recorders, the display-connect list, version, and update check.</em>
</p>

## Usage
Expand Down Expand Up @@ -129,6 +134,7 @@ Magic Switch tells you when there's a new version — it never updates itself. A
- Same network; TCP port 41952 not blocked by firewall.
- Bluetooth and Local Network permissions granted in System Settings → Privacy & Security.
- A **greyed-out device** — in the Macs tab or the right-click menu — means it isn't reachable on the network right now (the other Mac is asleep, off Wi-Fi, or not running Magic Switch). Ping, Sync, and switching stay disabled until it's back online.
- **Both Macs changed IP address at once (Bonjour-blocked networks).** An IP change normally heals itself: a reachability probe that gets through teaches the receiving Mac the dialing Mac's current address from the connection itself, so as long as one Mac can still reach the other, both sides end up with working addresses. But when *both* Macs get new addresses at the same time on a network where Bonjour is blocked — a router reassigning every lease, or both Macs moving to another network — each keeps dialing the other's old address and neither probe ever lands. Re-add the other Mac by IP address (the **+** button on the Macs tab) from either side; one side is enough.
- **A switch failed and nothing told you.** Failures — a peripheral that won't connect, a Mac that can't be reached, an identity mismatch — are reported via system notifications, because a hotkey, URL-scheme, or automatic switch has no window to show an error in. If macOS notifications are off for Magic Switch, those failures are completely silent; **Settings → Other** shows a **"Notifications are off"** warning when that's the case. Re-enable them under System Settings → Notifications → Magic Switch. (The Pairing and Macs tabs also show their errors inline, so actions taken *there* still report failures either way.)
- On the **Macs** tab, **Ping** tests whether the two Macs can reach each other over the secure channel.
- **Closing or sleeping one Mac hands its peripherals to the other.** When this Mac sleeps (or you close its lid), it hands the peripherals it holds to your other Mac — or, if that Mac isn't reachable yet, frees them so it can pick them up the moment it wakes. That's why you can close one Mac and find the keyboard and mouse already on the other. This is on by default; you can turn it off under **Settings → Other → "Release peripherals when this Mac sleeps."**
Expand Down
71 changes: 45 additions & 26 deletions docs/annotate-screenshots.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ annotate() {
local name="$1" bg="$2" pad="$3" lx="$4" ly0="$5" ldy="$6" badges="$7" legend="$8"
local legpt="${9:-27}"
local input="$RAW/$name.png" output="$OUT/$name.png"
[ -f "$input" ] || { echo "skip $name (no raw capture in $RAW)"; return; }
local W H; read W H < <(magick identify -format "%w %h\n" "$input")

local args=( "$input" )
Expand Down Expand Up @@ -77,47 +78,65 @@ annotate() {
}

# ---- Peripheral tab ----
annotate peripheral-tab "#292929" 0 88 583 47 \
"2 838 205 895 205
3 1108 352 1108 312
1 1050 483 1092 483" \
annotate peripheral-tab "#292929" 0 88 560 47 \
"2 988 145 988 182
3 1105 350 1105 314
1 1050 478 1082 478
4 815 204 855 204" \
"1|Register a Magic device so Magic Switch can manage and hand it off
2|Release — hand this peripheral to the other Mac
3|Remove — stop managing this peripheral"
3|Remove — stop managing this peripheral
4|Battery level, live while the peripheral is connected to this Mac"

# ---- Peripheral tab: type picker (single badge) ----
annotate peripheral-type-picker "#292929" 0 88 730 47 \
"1 185 158 118 200" \
"1|Click a peripheral's icon to pick a type — or Automatic to auto-detect"

# ---- Device tab ----
annotate device-tab "#282828" 0 88 465 47 \
"1 932 272 932 230
2 1021 272 1021 230
3 1097 272 1097 230
4 1075 322 1102 322" \
# ---- Macs tab ----
annotate macs-tab "#282828" 0 88 490 47 \
"1 938 270 938 234
2 1028 270 1028 234
3 1105 270 1105 234
4 1025 321 1064 321
5 1166 321 1136 321" \
"1|Ping — check the other Mac is reachable
2|Share — sync your registered peripherals to that Mac
2|Sync — sync your registered peripherals to that Mac
3|Remove — forget this Mac
4|Refresh — rescan the network for nearby Macs"
4|Add a Mac by IP address — for networks that block Bonjour
5|Refresh — rescan the network for nearby Macs"

# ---- Other tab (legend extends below the window) ----
annotate other-tab "#292929" 150 88 715 47 \
"1 1055 148 1085 148
2 1055 240 1085 240
3 1055 332 1085 332
4 1078 424 1108 424
5 160 655 160 612" \
# ---- Other tab ----
# The tab is taller than the window, so the raw is stitched from three
# scrolled captures (other-tab-{top,mid,bottom}.png): each slice is cut on
# flat background between sections, aligned by crop offset (the window was
# identical; only the capture regions drifted), and has the transient
# overlay scrollbar painted out.
if [ -f "$RAW/other-tab-top.png" ]; then
magick \
\( "$RAW/other-tab-top.png" -crop 1188x530+2+0 +repage -fill "#282828" -draw "rectangle 1164,74 1188,530" \) \
\( "$RAW/other-tab-mid.png" -crop 1188x340+0+240 +repage -fill "#282828" -draw "rectangle 1164,0 1188,340" \) \
\( "$RAW/other-tab-bottom.png" -crop 1188x565+4+115 +repage -fill "#282828" -draw "rectangle 1164,0 1188,565" \) \
-append "$RAW/other-tab.png"
fi
annotate other-tab "#282828" 320 88 1480 47 \
"1 1053 201 1083 201
2 1053 388 1083 388
3 1053 464 1083 464
4 1030 640 1062 640
5 270 993 218 993
6 160 1415 160 1382" \
"1|Launch at Login — start Magic Switch when you log in
2|Release peripherals to the other Mac when this Mac sleeps
3|Reconnect peripherals automatically if they drop
4|License Information — open-source license details
5|Check for Updates — check now (status shows on the right)"
4|Record a shortcut — send, take, or toggle every peripheral from anywhere
5|A marked display — when it connects, peripherals switch to this Mac
6|Check for Updates — check now (status shows on the right)"

# ---- Menu (translucent material background, smaller legend) ----
annotate menu "#2D2D30" 150 24 405 36 \
"1 435 72 380 72
2 435 223 380 223" \
annotate menu "#25272A" 150 24 405 36 \
"1 462 72 400 72
2 330 172 288 172" \
"1|Click a Mac — move all peripherals there
2|Click a peripheral — move just that one
|✓ = it's on this Mac now" 20
|✓ = it's on this Mac now · %% = its battery" 20
Binary file added docs/assets/add-by-address.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed docs/assets/device-tab.png
Binary file not shown.
Binary file added docs/assets/macs-tab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/other-tab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/peripheral-tab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading