Automate your Mac with Swift

Write Script Commands in Swift to trigger every-day tasks from Raycast.

Written by
AvatarThomas Paul Mann
Published onNovember 10, 2020

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.

Quit running applications

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:

  1. We use AppKit's NSWorkspace to get an array of all NSRunningApplication instances.
  2. We filter out the currently running application to not close Raycast. Alternatively, we could do this with the bundle identifier.
  3. We're only interested in ordinary apps that appear in the dock and use the ActivationPolicy to filter them.
  4. The Finder is a special app. We don't want to quit it and use the bundleIdentifier property to remove it from the array of running applications.
  5. Last step, we iterate over the filtered applications and call 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:

Quit all running applications with a script command

Copy your last download

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:

  1. The 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.
  2. We want to copy the download to the pasteboard to let users quickly insert it into other applications. This can be done with the 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:

Copy the last downloaded file and share it via Slack

Share your availability

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:

  1. To start, we use the EKEventStore to query all events of today. We filter out all-day long events.
  2. Then, we check the array of 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.
  3. If there are events spread throughout the day, we calculate the available times and format them in a pretty message. For this, we map the range of busy times and pass them to the helper method 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.
  4. The last step is to copy the availability to the clipboard and display a message.

Here is how you can use it:

Share today's availablitliy via Slack

Wrap up

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.