Skip to content
Open
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
125 changes: 125 additions & 0 deletions commands/system/toggle-sidecar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
#!/usr/bin/swift
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even though most part of the Script Commands written in Swift in this repository aren't compliant to the "Scripts that require installation of runtimes and dependencies" section of our CONTRIBUTING.md documentation, it is nicer if you add here the information the Script Command requires Swift installed in the user's machine. Otherwise, it would fail.


// Required parameters:
// @raycast.schemaVersion 1
// @raycast.title Toggle iPad Screen Mirroring (Automatic)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// @raycast.title Toggle iPad Screen Mirroring (Automatic)
// @raycast.title Toggle iPad Screen Mirroring

// @raycast.mode silent
//
// Optional parameters:
// @raycast.icon 🖥️
// @raycast.packageName Display
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// @raycast.packageName Display
// @raycast.packageName System

//
// Documentation:
// @raycast.description Automatically toggles your Mac's screen connection (Sidecar) to the first available iPad. Requires no additional modification. Based on the original script from Ocasio-J/SidecarLauncher.
// @raycast.author Marshal Fevzi
// @raycast.authorURL https://github.com/marshalfevzi

import Foundation

/// Runs a shell command and returns the standard output.
func shell(_ command: String) -> String {
let task = Process()
let pipe = Pipe()

task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
task.launchPath = "/bin/zsh"

do {
try task.run()
} catch {
// This error is for the shell itself, not the command
print("Shell Error: \(error.localizedDescription)")
return ""
}

let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8) ?? ""
return output
}

/// Checks if a Sidecar display is currently active.
func isSidecarConnected() -> Bool {
let displayProfile = shell("system_profiler SPDisplaysDataType")
return displayProfile.contains("Sidecar Display")
}

// --- Main Script Logic ---
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better if you isolate the main logic into a function and call it from the root of the file, then it will be more organized.


// 1. Load the private SidecarCore framework
guard let _ = dlopen("/System/Library/PrivateFrameworks/SidecarCore.framework/SidecarCore", RTLD_LAZY) else {
print("Error: Sidecar framework missing. Requires macOS 10.15+.")
exit(1)
}
Comment on lines +51 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make it even more beautiful than just isolating into a separate method, you could improve the errors that you raise by using an enum that inherits from Error (or Swift.Error).

i.e:

enum ScriptError: Error {
  case frameworkMissing
  case managerNotFound
  case failedToStartManager
  case failedToQueryPads
  // and so on... 
}


guard let cSidecarDisplayManager = NSClassFromString("SidecarDisplayManager") as? NSObject.Type else {
print("Error: Could not find Sidecar manager.")
exit(1)
}

guard let manager = cSidecarDisplayManager.perform(Selector(("sharedManager")))?.takeUnretainedValue() else {
print("Error: Failed to start Sidecar manager.")
exit(1)
}

// 2. Get the list of available devices
guard let devices = manager.perform(Selector(("devices")))?.takeUnretainedValue() as? [NSObject] else {
print("Error: Failed to query for iPads.")
exit(1)
}

// 3. Get connection status
let isConnected = isSidecarConnected()

// 4. Execute toggle logic
if isConnected {
// --- DISCONNECT LOGIC ---
guard let deviceToDisconnect = devices.first,
let deviceName = deviceToDisconnect.perform(Selector(("name")))?.takeUnretainedValue() as? String else {

print("Error: Connected, but can't find device to disconnect.")
exit(1) // Ambiguous state
}

let dispatchGroup = DispatchGroup()
let closure: @convention(block) (_ e: NSError?) -> Void = { e in
defer { dispatchGroup.leave() }
if let e = e {
print("Error: Disconnect failed. \(e.localizedDescription)")
exit(4)
} else {
print("Disconnected from \(deviceName)")
}
}

dispatchGroup.enter()
_ = manager.perform(Selector(("disconnectFromDevice:completion:")), with: deviceToDisconnect, with: closure)
dispatchGroup.wait() // Wait for the async disconnect to finish
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you would earn more just using async/await here


} else {
// --- CONNECT LOGIC ---
guard let deviceToConnect = devices.first,
let deviceName = deviceToConnect.perform(Selector(("name")))?.takeUnretainedValue() as? String else {

print("No iPad available to connect.")
exit(2) // No reachable devices
}

let dispatchGroup = DispatchGroup()
let closure: @convention(block) (_ e: NSError?) -> Void = { e in
defer { dispatchGroup.leave() }
if let e = e {
print("Error: Connection failed. \(e.localizedDescription)")
exit(4)
} else {
print("Connected to \(deviceName)")
}
}

dispatchGroup.enter()
_ = manager.perform(Selector(("connectToDevice:completion:")), with: deviceToConnect, with: closure)
dispatchGroup.wait() // Wait for the async connect to finish
}

exit(0) // Success