Write Script Commands in Swift to trigger every-day tasks from Raycast.
Script Commands let's you tailor Raycast to your needs. Combined with Swift and some of Apple's frameworks, they are a powerful tool to automate every-day tasks on your Mac. Let's dive into three examples to show what you can achieve with them.
If you haven't installed or written any Script Commands, check out the guides in our repository.
Let's say it's the end of the day and you want to tear down your work environment to start binging your favorite series. Why not quitting all your running applications with a few keystrokes?!
Thanks to Apple's AppKit, we can achieve this with a few lines of Swift code:
#!/usr/bin/swift
// Required parameters:
// @raycast.schemaVersion 1
// @raycast.title Quit All Applications
// @raycast.mode silent
// Optional parameters:
// @raycast.icon π₯
// @raycast.needsConfirmation true
import AppKit
let finderBundleIdentifier = "com.apple.finder"
NSWorkspace.shared.runningApplications
.filter { $0 != NSRunningApplication.current }
.filter { $0.activationPolicy == .regular }
.filter { $0.bundleIdentifier != finderBundleIdentifier }
.forEach { $0.terminate() }
print("Quit all applications")
Let's walk through the script step by step:
NSWorkspace
to get an array of all NSRunningApplication
instances.ActivationPolicy
to filter them.bundleIdentifier
property to remove it from the array of running applications.terminate()
to quite them.After we quit all applications, we print a message that Raycast displays in a toast alongside the icon of the command. The toast informs the user about the successful execution. This is how the command looks in action:
Foundation provides easy access to the file system with the FileManager
APIs. Let's use it to quickly copy the last download. This makes it convenient to share it with your team-mates in the messenger of your choice.
#!/usr/bin/swift
// Required parameters:
// @raycast.schemaVersion 1
// @raycast.title Copy Last Download
// @raycast.mode silent
// Optional parameters:
// @raycast.icon π
import AppKit
// MARK: - Main
guard let download = latestDownloadURL() else {
print("No recent downloads")
exit(1)
}
copyToPasteboard(download)
print("Copied \(download.lastPathComponent)")
// MARK: - Convenience
func latestDownloadURL() -> URL? {
guard let downloadsDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first else { return nil }
return try? FileManager.default
.contentsOfDirectory(at: downloadsDirectory, includingPropertiesForKeys: [.addedToDirectoryDateKey], options: .skipsHiddenFiles)
.sorted { $0.addedToDirectoryDate > $1.addedToDirectoryDate }
.first
}
func copyToPasteboard(_ url: URL) {
NSPasteboard.general.clearContents()
NSPasteboard.general.writeObjects([url as NSPasteboardWriting])
}
extension URL {
var addedToDirectoryDate: Date {
return (try? resourceValues(forKeys: [.addedToDirectoryDateKey]).addedToDirectoryDate) ?? .distantPast
}
}
Ok, what happens here?! The script is split into a main and convenience part, separated by the marks. In the main part, we grab the latest download, copy it to the pasteboard and display a message to the user β Fairly straight forward. Let's look into the convenience part:
latestDownloadURL()
method uses the FileManager
to get the contents of the downloads directory. It sorts the files by the date they got added. The sorting is achieved with an extension on URL
that exposes the addedToDirectoryDate
. The last step is to return the first
URL of the array.NSPasteboard
. First, we clear the contents of the pasteboard, followed by writing the URL of the download as an object to the pasteboard.Now you can execute the new command from Raycast and hit β
V
to paste your last download in any other application, like this:
Another handy framework from Apple is EventKit
. It provides access to the system calendar to handle events and reminders. Let's use the framework to copy our availability. This way, the next time a colleague asks you for a virtual Zoom coffee, you just execute the command and respond.
#!/usr/bin/swift
// Required parameters:
// @raycast.schemaVersion 1
// @raycast.title Copy Availability
// @raycast.mode silent
// Optional parameters:
// @raycast.icon π
import AppKit
import EventKit
// MARK: - Main
let now = Date()
let startOfToday = Calendar.current.startOfDay(for: now)
let endOfToday = Calendar.current.date(byAdding: .day, value: 1, to: startOfToday)!
let eventStore = EKEventStore()
let predicate = eventStore.predicateForEvents(withStart: startOfToday, end: endOfToday, calendars: nil)
let eventsOfToday = eventStore.events(matching: predicate).filter { !$0.isAllDay }
let availability: String
if eventsOfToday.isEmpty {
availability = "I'm available the full day."
} else if eventsOfToday.allSatisfy({ $0.endDate.isAfternoon }) {
availability = "I'm available in the morning."
} else if eventsOfToday.allSatisfy({ $0.endDate.isMorning }) {
availability = "I'm available in the afternoon."
} else {
let busyTimes = eventsOfToday.map { $0.startDate...$0.endDate }
let availableTimes = availableTimesForToday(excluding: busyTimes)
let prettyPrintedAvailableTimes = availableTimes
.map { (from: DateFormatter.shortTime.string(from: $0.lowerBound), to: DateFormatter.shortTime.string(from: $0.upperBound)) }
.map { "* \($0.from) - \($0.to)" }
.joined(separator: "\n")
availability = "Here's my availability for today:\n\(prettyPrintedAvailableTimes)"
}
copy(availability)
print("Copied availability")
// MARK: - Convenience
extension DateFormatter {
static var shortTime: DateFormatter {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .none
dateFormatter.timeStyle = .short
return dateFormatter
}
}
extension Date {
var isMorning: Bool { Calendar.current.component(.hour, from: self) <= 11 }
var isAfternoon: Bool { Calendar.current.component(.hour, from: self) >= 12 }
}
func availableTimesForToday(excluding excludedTimes: [ClosedRange<Date>]) -> [ClosedRange<Date>] {
let startOfWorkDay = Calendar.current.date(bySettingHour: 9, minute: 0, second: 0, of: startOfToday)!
let endOfWorkDay = Calendar.current.date(bySettingHour: 17, minute: 0, second: 0, of: startOfToday)!
let workDay = startOfWorkDay...endOfWorkDay
let busyTimes = [startOfToday...startOfWorkDay] + excludedTimes + [endOfWorkDay...endOfToday]
var previousBusyTime = busyTimes.first
var availableTimes = [ClosedRange<Date>]()
for time in busyTimes {
if let previousEnd = previousBusyTime?.upperBound, previousEnd < time.lowerBound {
var newAvailability = previousEnd...time.lowerBound
if let lastAvailability = availableTimes.last, newAvailability.overlaps(lastAvailability) {
newAvailability = newAvailability.clamped(to: lastAvailability).clamped(to: workDay)
availableTimes.insert(newAvailability, at: availableTimes.count - 1)
} else {
newAvailability = newAvailability.clamped(to: workDay)
availableTimes.append(newAvailability)
}
}
previousBusyTime = time
}
return availableTimes
}
func copy(_ string: String) {
NSPasteboard.general.declareTypes([NSPasteboard.PasteboardType.string], owner: nil)
NSPasteboard.general.setString(string, forType: NSPasteboard.PasteboardType.string)
}
This is a slightly more complex script. Let's go through it from the top to the bottom:
EKEventStore
to query all events of today. We filter out all-day long events.EKEvent
and handle common cases for no events and events only in the morning or afternoon. We generate a nice message that we can copy to the clipboard at the end of the script.availableTimesForToday()
. Inside the helper, we add some more ranges for non-working hours. Then we can iterate over all busy times and convert them to available time slots. We make sure to handle overlapping ranges accordingly and return an array of all available times throughout the current work-day.Here is how you can use it:
Swift is a great programming language to automate small tasks in your daily workflow on macOS. Raycast puts these scripts at your fingertips and you execute them from anywhere on your desktop. You can take it one step further, and assign a global hotkey to your script command. This way, you can press a keyboard shortcut to execute your script, e.g. assign β
β₯
Q
to quit all applications in Raycast.
All scripts of this blog post are available in our official repository. Try them out and feel free to fork them to adjust to your needs and contribute to our growing catalog of script commands.