From f190e1431c36ef320ff9eda9a01408be89015662 Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Wed, 15 Jul 2026 00:16:04 +0200 Subject: [PATCH 1/2] feat: group the Other tab into labeled sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The display-trigger list arrived with the tab's only section header, leaving the two handoff toggles as unlabeled one-row islands and the license/version rows trailing unlabeled. Label the groups — General, Peripheral handling (both handoff toggles together), the display list, About (license + version + updates merged) — so the tab reads as four topics instead of six fragments. --- Magic Switch/View/Settings/OtherSettingsView.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Magic Switch/View/Settings/OtherSettingsView.swift b/Magic Switch/View/Settings/OtherSettingsView.swift index 8ee00e2..8f6e16a 100644 --- a/Magic Switch/View/Settings/OtherSettingsView.swift +++ b/Magic Switch/View/Settings/OtherSettingsView.swift @@ -20,19 +20,17 @@ struct OtherSettingsView: View { private var formContent: some View { Form { if #available(macOS 13.0, *) { - Section { + Section(header: Text("General")) { Toggle("Launch at Login", isOn: $launchAtLogin) .onChange(of: launchAtLogin, perform: setLaunchAtLogin) .help("Start Magic Switch automatically when you log in to this Mac.") } } - Section { + Section(header: Text("Peripheral handling")) { Toggle("Release peripherals when this Mac sleeps", isOn: $releaseOnSleep) .help( "When this Mac sleeps, hand its Magic peripherals back so your other Mac can take them. A sleeping Mac can't be asked to release them over the network, so Magic Switch releases them just before sleeping. Turn off to keep a peripheral bonded to this Mac while it sleeps." ) - } - Section { Toggle("Reconnect peripherals if they drop", isOn: $autoReconnect) .help( "If a Magic peripheral that should be on this Mac drops — for example after closing the lid, or when you power-cycle a peripheral that got stuck — keep trying to reconnect it until it's back. When your other Mac goes to sleep or drops off the network, this Mac also adopts the peripherals it left behind. Magic Switch won't take a peripheral your other Mac is actively using." @@ -54,14 +52,12 @@ struct OtherSettingsView: View { } } } - Section { + Section(header: Text("About")) { SettingsRowView( title: "License Information", help: "Open the project license in your browser.", action: showLicenseInfo ) - } - Section { if updateChecker.updateAvailable, let latest = updateChecker.latestVersion { SettingsRowView( title: "Update Available — v\(latest)", From 058535c21c3beb1f84b4469ada43d6754681d89e Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Wed, 15 Jul 2026 00:18:11 +0200 Subject: [PATCH 2/2] feat: add refresh and forget affordances to the display-trigger list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A refresh button in the section header (the Device tab's arrow.clockwise idiom) re-reads the online displays immediately, bypassing the reconcile debounce; opening the tab does the same silently, so the list is current the moment the user looks. - A remembered-but-disconnected display now shows a trash button (the Device tab's row idiom) instead of a toggle. Unmarking is forgetting — the trigger map is the only memory — so the old toggle was a remove control in disguise; the trash button says what it does. Connected rows keep their toggle. --- Magic Switch/Manager/DisplayMonitor.swift | 10 +++ .../View/Settings/OtherSettingsView.swift | 63 +++++++++++++------ 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/Magic Switch/Manager/DisplayMonitor.swift b/Magic Switch/Manager/DisplayMonitor.swift index 3ea19d9..61f3fdb 100644 --- a/Magic Switch/Manager/DisplayMonitor.swift +++ b/Magic Switch/Manager/DisplayMonitor.swift @@ -179,6 +179,16 @@ final class DisplayMonitor: ObservableObject { DispatchQueue.main.async { monitor.scheduleReconcile() } } + /// Re-read the online displays right now, bypassing the debounce. Wired to + /// the Settings section's refresh button and its onAppear, so the list is + /// current the moment the user looks. Runs the same reconcile as the event + /// path — a pending genuine dock edge fires now instead of a beat later. + /// Main-thread only. + func refreshNow() { + guard started else { return } + reconcile() + } + private func scheduleReconcile() { reconcileTimer?.cancel() let timer = DispatchSource.makeTimerSource(queue: .main) diff --git a/Magic Switch/View/Settings/OtherSettingsView.swift b/Magic Switch/View/Settings/OtherSettingsView.swift index 8f6e16a..14e2416 100644 --- a/Magic Switch/View/Settings/OtherSettingsView.swift +++ b/Magic Switch/View/Settings/OtherSettingsView.swift @@ -36,21 +36,30 @@ struct OtherSettingsView: View { "If a Magic peripheral that should be on this Mac drops — for example after closing the lid, or when you power-cycle a peripheral that got stuck — keep trying to reconnect it until it's back. When your other Mac goes to sleep or drops off the network, this Mac also adopts the peripherals it left behind. Magic Switch won't take a peripheral your other Mac is actively using." ) } - Section( - header: Text("Take peripherals when a display connects") - .help( - "Displays connected to this Mac appear here. A display you mark acts as a docking trigger: whenever it connects to this Mac, Magic Switch switches your peripherals to this Mac automatically." - ) - ) { + Section { if displayRows.isEmpty { Text("No external displays connected") .foregroundColor(.secondary) .help("Connect a display to this Mac and it will appear here.") } else { ForEach(displayRows) { row in - displayToggle(for: row) + displayRow(for: row) } } + } header: { + HStack { + Text("Take peripherals when a display connects") + .help( + "Displays connected to this Mac appear here. A display you mark acts as a docking trigger: whenever it connects to this Mac, Magic Switch switches your peripherals to this Mac automatically." + ) + Spacer() + Button(action: { displayMonitor.refreshNow() }) { + Image(systemName: "arrow.clockwise") + } + .buttonStyle(.borderless) + .help("Re-scan the displays connected to this Mac.") + .accessibilityLabel("Refresh connected displays") + } } Section(header: Text("About")) { SettingsRowView( @@ -122,7 +131,7 @@ struct OtherSettingsView: View { /// Connected external displays first (already name-sorted by the monitor), /// then remembered trigger displays that aren't currently connected — - /// still labeled, still toggleable off. + /// still labeled, removable with their trash button. private var displayRows: [DisplayRow] { let connected = displayMonitor.connectedDisplays.map { DisplayRow(id: $0.id, name: $0.name, isConnected: true) @@ -135,25 +144,37 @@ struct OtherSettingsView: View { return connected + remembered } + /// A connected display gets the trigger toggle; a remembered-but- + /// disconnected one gets a trash button instead — with nothing to toggle + /// *to* (unmarking is forgetting; the trigger map is the only memory), + /// a toggle would just be a remove control in disguise. @ViewBuilder - private func displayToggle(for row: DisplayRow) -> some View { - Toggle(isOn: triggerBinding(for: row)) { - if row.isConnected { - Text(row.name) - } else { + private func displayRow(for row: DisplayRow) -> some View { + if row.isConnected { + Toggle(row.name, isOn: triggerBinding(for: row)) + .help( + "When \(row.name) connects to this Mac — for example when you dock — automatically switch your Magic peripherals to this Mac, taking them from your other Mac if needed. Display ID: \(row.id)." + ) + } else { + HStack { VStack(alignment: .leading, spacing: 2) { Text(row.name) Text("Not connected") .font(.caption) .foregroundColor(.secondary) } + .help( + "\(row.name) isn't connected right now. It still triggers the switch when it next connects. Display ID: \(row.id)." + ) + Spacer() + Button(action: { displayMonitor.setTriggerEnabled(false, id: row.id, name: row.name) }) { + Image(systemName: "trash") + .foregroundColor(.red) + } + .help("Forget \(row.name) — it will no longer trigger a switch when it connects.") + .accessibilityLabel("Forget \(row.name)") } } - .help( - row.isConnected - ? "When \(row.name) connects to this Mac — for example when you dock — automatically switch your Magic peripherals to this Mac, taking them from your other Mac if needed. Display ID: \(row.id)." - : "\(row.name) isn't connected right now. It still triggers the switch when it next connects; turn off to forget it. Display ID: \(row.id)." - ) } private func triggerBinding(for row: DisplayRow) -> Binding { @@ -194,10 +215,12 @@ struct OtherSettingsView: View { openURL(url) } - /// Refresh launch-at-login state and nudge the update check. `checkIfNeeded` - /// respects the 24h cadence, so opening Settings rarely fires a real request. + /// Refresh launch-at-login state, the display list, and nudge the update + /// check. `checkIfNeeded` respects the 24h cadence, so opening Settings + /// rarely fires a real request. private func refreshOnAppear() { refreshLaunchAtLogin() + displayMonitor.refreshNow() updateChecker.checkIfNeeded() }