More example scripts


#1

I thought I woulld share a few example scripts that aren’t yet in the app either because I haven’t pollished them or because they aren’t yet scripts that performs a meaningful task but rather just a showcase of APIs.

Some of them are scripts I use daily and some of them are just for fun. Maybe you’ll find some of them useful as inspiration for your own scripts.

Pollen
Fetches the latest pollen counts from the Danish weather provider DMI.
APIs: Request, XMLParser, UITable, QuickLook

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-green; icon-glyph: sun-2;
// Shows the pollen count for today.
const url = "http://www.dmi.dk/vejr/services/pollen-rss/"
const r = new Request(url)
let resp = await r.load()
const targetCityName = "København"
let elementName = ""
let currentValue = null
let items = []
let currentItem = null
const xmlParser = new XMLParser(resp)
xmlParser.didStartElement = name => {
  currentValue = ""
  if (name == "item") {
    currentItem = {}
  }
}
xmlParser.didEndElement = name => {
  const hasItem = currentItem != null
  if (hasItem && name == "title") {
    currentItem["title"] = currentValue
  }
  if (hasItem && name == "description") {
    currentItem["description"] = currentValue
  }
  if (name == "item") {
    items.push(currentItem)
    currentItem = {}
  }
}
xmlParser.foundCharacters = str => {
  currentValue += str
}
xmlParser.didEndDocument = () => {
  const cph = items.filter(i => {
    return i.title == "København"
  })[0]
  const lines = cph.description
    .trim()
    .split("\n")
    .map(l => l.trim().replace(";", ""))
    .filter(l => l.length > 0)
  let values = lines.join("\n")
  let rows = lines.map(mapRow)
  let table = new UITable()
  for (row of rows) {
    table.addRow(row)
  }
  let focusedType = "Græs"
  let regex = new RegExp(focusedType + ": ([0-9]+|-)")
  let match = values.match(regex)
  let textToSpeak = null
  if (match) {
    let pollenCount = match[1]
    if (match == "-") {
      textToSpeak = "There are no grass pollen"
    } else {
      textToSpeak = "The pollen count for grass is " +  pollenCount
    }
  } else {
    textToSpeak = "Pollen count for grass is unavailable"
  }
  QuickLook.present(table)
  if (config.runsWithSiri) {
    Speech.speak(textToSpeak)
  }
}
xmlParser.parse()

function mapRow(value) {
  let comps = value.split(":")
  let row = new UITableRow()
  row.addText(mapName(comps[0].trim()))
  row.addText(mapValue(comps[1].trim()))
  return row
}

function mapName(name) {
  if (name == "Birk") {
    return "Birch"
  } else if (name == "Bynke") {
    return "Mugwort"
  } else if (name == "El") {
    return "Alder"
  } else if (name == "Elm") {
    return "Elm"
  } else if (name == "Græs") {
    return "Grass"
  } else if (name == "Hassel") {
    return "Hazel"
  } else {
    return name
  }
}

function mapValue(value) {
  if (value == "Høj") {
    return "High"
  } else if (value == "Middel") {
    return "Moderate"
  } else if (value == "Lav") {
    return "Low"
  } else {
    return value
  }
}

Postpone Event
Shows future events take place today and promps user to select one of them and then promps to select a time interval to postpone the event, e.g. 15 minutes.
APIs: Calendar, Alert

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-blue; icon-glyph: calendar;
// Fetches upcoming events today and prompts to select an event. Then prompts user to select a number of minutes to postpone the selected event.
let calendarName = "Work"
let cal = await Calendar.forEventsByTitle(calendarName)
let events = await CalendarEvent.today([cal])
events = events.filter(event => {
  return event.startDate > new Date()
})
// Check if we found any events
if (events.length == 0) {
  let alert = new Alert()
  alert.message = "There are no more events today."
  alert.present()
  return
}
// Prompt to choose which event to postpone
let alert = new Alert()
for (event of events) {
  let hours = event.startDate.getHours()
  let mins = event.startDate.getMinutes()
  let title = ""
    + zeroPrefix(hours)
    + ":"
    + zeroPrefix(mins)
    + ": "
    + event.title
  alert.addAction(title)
}
alert.addCancelAction("Cancel")
let eventIdx = await alert.presentSheet()
if (eventIdx == -1) {
  return
}
// Prompt to choose minutes to offset
let minutes = [ 15, 30, 45, 60 ]
alert = new Alert()
for (offset of minutes) {
  alert.addAction(offset + " minutes")
}
alert.addCancelAction("Cancel")
let minsIdx = await alert.presentSheet()
if (minsIdx == -1) {
  return
}
// Update date with offset and save
let offset = minutes[minsIdx] * 60 * 1000
let event = events[eventIdx]
let startTime = event.startDate.getTime()
let endTime = event.endDate.getTime()
event.startDate = new Date(startTime + offset)
event.endDate = new Date(endTime + offset)
event.save()

// Format hours and minutes
function zeroPrefix(num) {
  return (num < 10 ? "0" : "") + num
}

Charles response time
Takes an exported session file from the Charles Proxy app as input and shows the response times for the requests in the sessions. The user can select to view the response times as a bar chart or in plain text.
APIs: FileManager, Alert, DrawContext, QuickLook

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// always-run-in-app: true; icon-color: blue;
// icon-glyph: clock-1; share-sheet-inputs: plain-text, file-url, url;
// Shows response times from the Charles iOS app.
const fileURL = args.fileURLs[0]
const fm = FileManager.local()
const rawJSON = fm.read(fileURL)
const json = JSON.parse(rawJSON)
// Extract response times from Charles session
let responseTimesForPath = {}
for (let transaction of json) {
  if (!("path" in transaction)) {
    continue
  }
  const path = transaction["path"]
  if (path == null) {
    continue
  }
  if (!("times" in transaction)) {
    continue
  }
  const times = transaction["times"]
  if (!("responseBegin" in times) || !("end" in times)) {
    continue
  }  
  const responseBeginTime = Date.parse(times["responseBegin"])
  const endTime = Date.parse(times["end"])
  const responseTime = endTime - responseBeginTime
  let responseTimes = responseTimesForPath[path] || []
  responseTimes.push(responseTime)
  responseTimesForPath[path] = responseTimes
}
// Compute average times
let averageResponseTimesForPath = {}
const reducer = (accumulator, currentValue) => accumulator + currentValue
for (let path in responseTimesForPath) {  
  const responseTimes = responseTimesForPath[path]
  averageResponseTimesForPath[path] = Math.round(responseTimes.reduce(reducer, 0) / responseTimes.length)
}
// Sort paths by average response time, greatest to lowest
let allPaths = Object.keys(averageResponseTimesForPath)
let allAverageResponseTimes = Object.values(averageResponseTimesForPath)
allPaths.sort((a, b) => {
  const aResponseTime = averageResponseTimesForPath[a]
  const bResponseTime = averageResponseTimesForPath[b]
  if (aResponseTime < bResponseTime) {
    return 1
  } else if (aResponseTime > bResponseTime) {
    return -1
  } else {
    return 0
  }
})
const alert = new Alert()
alert.addAction("Plain text")
alert.addAction("Graph")
alert.addCancelAction("Cancel")
alert.presentSheet().then(idx => {
  if (idx == 0) {
    // Create pretty string with response times
    let timesString = ""
    for (let i = 0; i < allPaths.length; i++) {
      const path = allPaths[i]
      timesString += path + ": " + averageResponseTimesForPath[path] + " ms"
      if (i < allPaths.length - 1) {
        timesString += "\n"
      }
    }
    QuickLook.present(timesString)
  } else if (idx == 1) {
    // Draw graph
    const maxBarWidth = 400
    const barHeight = 20
    const barSpacing = 30
    const inset = 20
    const totalWidth = maxBarWidth + inset * 2
    const totalHeight = (barHeight + barSpacing) * allPaths.length + inset * 2
    const maxAverageResponseTime = Math.max.apply(Math, allAverageResponseTimes)
    const c = new DrawContext()
    c.respectScreenScale = true
    c.size = new Size(totalWidth, totalHeight)
    c.setFillColor(Color.white())
    c.fillRect(new Rect(0, 0, totalWidth, totalHeight))
    for (let i = 0; i < allPaths.length; i++) {
      const path = allPaths[i]
      const responseTime = averageResponseTimesForPath[path]
      const textResponseTime = responseTime + "ms"
      const percentage = responseTime / maxAverageResponseTime
      const xPos = inset
      const yPos = inset + (barHeight + barSpacing) * i
      const barWidth = Math.round(maxBarWidth * percentage)
      const rect = new Rect(xPos, yPos, barWidth, barHeight)
      const responseTimeRect = new Rect(xPos, yPos + 2, barWidth - 7, barHeight - 2)
      c.setFillColor(Color.black())
      c.fillRect(rect)
      c.setTextAlignedLeft()
      c.setTextColor(Color.black())
      c.drawText(path, new Point(xPos, yPos + barHeight + 2))
      c.setTextColor(Color.white())
      c.setTextAlignedRight()
    c.drawTextInRect(textResponseTime, responseTimeRect)
    }
    const image = c.getImage()
    QuickLook.present(image)
  }
}).catch(err => {
  console.logError(err)
})

Draw
Uses the DrawContext API to draw the word “Hi”.
APIs: DrawContext, QuickLook

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: orange; icon-glyph: pencil;
const lineWidth = 4
const horSpacing = 60
const verSpacing = 20
const size = new Size(200, 200)
const c = new DrawContext()
c.size = size
c.respectScreenScale = true
// c.beginDrawing()
c.setStrokeColor(Color.black())
c.setLineWidth(lineWidth)

// Set background color
c.setFillColor(new Color("#f5a623"))
// c.setFillColor(Color.red())
c.fill(new Rect(0, 0, size.width, size.height))

// Create H
const hPath = new Path()
hPath.move(new Point(horSpacing, verSpacing))
hPath.addLine(new Point(horSpacing, size.height - verSpacing))
hPath.move(new Point(horSpacing, verSpacing + (size.height - verSpacing * 2) / 2))
hPath.addLine(new Point(100, verSpacing + (size.height - verSpacing * 2) / 2))
hPath.addLine(new Point(100, size.height - verSpacing))
hPath.move(new Point(100, verSpacing + (size.height - verSpacing * 2) / 2))
hPath.addLine(new Point(100, verSpacing))

// Draw H
c.addPath(hPath)
c.setStrokeColor(Color.black())
c.setLineWidth(4)
c.strokePath()

// Create I
const iPath = new Path()
iPath.move(new Point(size.width - horSpacing, 40))
iPath.addLine(new Point(size.width - horSpacing, size.height - verSpacing))

// Draw I
c.addPath(iPath)
c.strokePath()

// Draw I dot
c.setFillColor(Color.black())
c.fillEllipse(new Rect(size.width - horSpacing - lineWidth, verSpacing, lineWidth * 2, lineWidth * 2)) 

const img = c.getImage()
// c.endDrawing()
QuickLook.present(img)  

FileManager
Small example of working with files. Stores a text file in iCloud and reads it back.
APIs: FileManager

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-blue; icon-glyph: magic-wand;
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
let path = fm.joinPath(dir, "myfile.txt")
fm.write(path, "Hello world")
let text = fm.read(path)
QuickLook.present(text)

#2

Overcast to Bear
Takes a URL from the Overcast app as input and stores a “bookmark” to the podcast at the current playback position in Bear.
APIs: Request, Safari

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: red; icon-glyph: headphone; share-sheet-inputs: url;
let url = args.urls[0]
let req = new Request(url)
let html = await req.loadString()
let titleRegExp = new RegExp("<div class=\"caption2 singleline\"><a.*>(.*)</a>")
let episodeNameRegExp = new RegExp("<div class=\"title\">(.*)</div>")
let titleMatch = html.match(titleRegExp)
let episodeNameMatch = html.match(episodeNameRegExp)
let episodeName = episodeNameMatch[1]
let title = titleMatch[1]
let note = "*"+decodeURIComponent(title)+"*\n"+decodeURIComponent(episodeName)+"\n"+url
let bearURL = "bear://x-callback-url/create/?text="+encodeURIComponent(note)+"&tags=podcast"
Safari.open(bearURL)

#3

Thank you for these examples!


Script to show upcoming U2 concerts
#4

Here’s a few more that uses some of the new features in build 16.

Watch WWDC 2018 keynote in Siri
Presents a WebView plays the WWDC 2018 keynote using YouTube. Works great in Siri.
APIs: WebView

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-brown; icon-glyph: magic-wand;
let html = `
<style>
body {
  margin: 0;
  padding 0;
  background: black;
}
.vid {
  width: 100%;
  height: 100%;
}
</style>
<div id="player"></div>
<script>
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
var player;
function onYouTubeIframeAPIReady() {
  player = new YT.Player('player', {
    height: '100%',
    width: '100%',
    videoId: 'UThGcWBIMpU',
    events: {
    'onReady': onPlayerReady
  }
  });
}
function onPlayerReady(event) {
  event.target.playVideo();
  event.target.seekTo(1923, true);
}
</script>
`
WebView.loadHTML(html, null, new Size(0, 202))

Load HTML file stored in iCloud Drive
Reads the file at the path html/fun/index.html, relative to Scriptables directory in iCloud Drive, and presents it in a web view.
APIs: FileManager, WebView

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: document-1;
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
let fileName = "html/fun/index.html"
let path = fm.joinPath(dir, fileName)
WebView.loadFile(path, new Size(0, 300))

#5

Read file status from Working Copy
Uses x-callback-url to read the file status of all repositories in the Working Copy app.
APIs: CallbackURL

// Fetch list of repos
let secretKey = "" // Find in Working Copy app settings
let url = "working-copy://x-callback-url/repos"
let callback = new CallbackURL(url)
callback.addParameter("key", secretKey)
let result = await callback.open()
let repos = JSON.parse(result.json)
for (repo of repos) {
  await listFileStatus(repo)
}

async function listFileStatus(repo) {
  let url = "working-copy://x-callback-url/branches"
  let callback = new CallbackURL(url)
  callback.addParameter("repo", repo.name)
  callback.addParameter("key", secretKey)
  let result = await callback.open()
  let branches = JSON.parse(result.json)
  log(repo.name)
  log(branches)
  log("")
}

#6

*Read weights from Health app"
Reads weight data from the Health app and displays the results in a table. N.B: The Health API is very much considered “work in progress”.
APIs: Health, UITable

let health = new Health()
let weights = await health
  .setTypeIdentifier("bodyMass")
  .setUnit("kg")
  .quantitySamples()
let table = new UITable()
for (w of weights) {
  let row = new UITableRow()
  row.addText(
    w.startDate.toString(),
    w.value.toString())
  table.addRow(row)
}
QuickLook.present(table)

#7

I’m trying to read heart rate data but can’t find a way of setting the correct units. I tried “bpm” (which is not in the documentation) and app crashed. Any help?


#8

Try to set your unit to “count/min”.


#9

That works, thank you!


#10

If you want to know for others:

https://developer.apple.com/documentation/healthkit/hkunit


#11

Thanks @rob ! Actually I was following the documentation in the app and tried “bpm” and “count” but having the HealthKit documentation can be really handy!


#12

Backup scripts in Working Copy
Backs up scripts to a remote repository using Working Copy. Before running the script, you should replace the value in the key variable with your key from Working Copy. Find that in the Working Copy settings.
When prompted to pick a folder, you’ll need to pick the folder for the repository in which you want to store the files.

let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
// Fetch all current scripts
let filePaths = allScripts(dir)
// Pick the folder that contains your repository
let dirs = await DocumentPicker.open(["public.folder"])
let dstDir = dirs[0]
let repoName = getFilename(dstDir)
// Remove all scripts in the repository. We'll replace the mwith the current ones.
removeScripts(dstDir)
// Copy all current scripts into the repository.
for (filePath of filePaths) {
  copyFile(filePath, dstDir)
}
// Find your key in the Working Copy settings
let key = "YOUR_KEY_GOES_HERE"
await pushChanges(repoName, key)

async function pushChanges(repo, key) {
  let baseURL = "working-copy://x-callback-url/chain/"
  let msg = "Backup from Scriptable"
  let cbu = new CallbackURL(baseURL)
  cbu.addParameter("key", key)
  cbu.addParameter("repo", repo)
  cbu.addParameter("command", "commit")
  cbu.addParameter("message", msg)
  cbu.addParameter("limit", "999")
  cbu.addParameter("command", "push")
  await cbu.open()
}

function copyFile(srcFilePath, dstDir) {
  let fm = FileManager.iCloud()
  let filename = getFilename(filePath)
  let dstFilePath = fm.joinPath(dstDir, filename)
  fm.copy(srcFilePath, dstFilePath)
}

function removeScripts(dstDir) {
  let fm = FileManager.iCloud()
  let filePaths = allScripts(dstDir)
  for (filePath of filePaths) {
    fm.remove(filePath)
  }
}

function allScripts(dir) {
  let fm = FileManager.iCloud()
  let files = fm.listContents(dir)
  let filePaths = files.map(file => {
    return fm.joinPath(dir, file)
  })
  return filePaths.filter(isScript)
}

function isScript(filePath) {
  let fm = FileManager.iCloud()
  let uti = fm.getUTI(filePath)
  return uti == "com.netscape.javascript-source"
}

function getFilename(filePath) {
  let idx = filePath.lastIndexOf("/")
  return filePath.substring(idx + 1)
}

Example:


#13

I am learning JS, I’ve been going through the examples, sorry for the basic question, why in the code above the cal value is passed with square brackets to the method in CalendarEvent.today([cal])? What is the meaning when the value is passed [value], is it a requirement of the method .today? Any explanation will be very much appreciated, I’ve been looking around but I haven’t found anything yet, thank you very much


#14

The square brackets denotes an array. If you look in the CalendarEvent today() documentation you’ll see it can accept multiple calendars expressed as an array.

The array above has but a single element.


#15

Thank you, but why you have to put the [ ] with cal, isn’t the cal variable/object already an Array?


#16

No it is not. Calendar’s forEventsByTitle() returns a non-array. See the documentation in Scriptable. Hence why it is placed into the array as an element.


#17

Thank you very much, I get it now


#18

Is there any examples on args ? I would like to pass an array as input to the script from workflow?

Also is it possible to send the output to the workflow, once the script is executed?


#19

The “Charles response time” example in this thread uses args. iOS does not yet allow passing inputs and outputs between shortcuts. The best way to model this is using the pasteboard. You can but a string into the pasteboard as an output in one shortcut and read it in another shortcut. Scriptable provides the Pasteboard API to do this.


#20

Got it. Thanks a heap!