Screenshooting on the iPad

About 55 minutes in to the current episode of Upgrade, Myke Hurley and Jason Snell talk about taking screenshots on the new iPad Pro. With the home button gone, the process has become more like that on the iPhone X line: press the top1 and volume up buttons simultaneously. This does seem, as Myke says, a little awkward, but I’m surprised either of them use that method of taking a screenshot very often.

If you’re using a keyboard with your iPad—as I believe Myke and Jason usually do when they’re working—the traditional Mac keyboard shortcuts of ⇧⌘3 and ⇧⌘4 work, and the difference between the two is an interesting twist on the Mac behavior.

As you probably remember, ⇧⌘3 on a Mac takes a screenshot of the whole screen and ⇧⌘4 turns the cursor into crosshairs so you can select a rectangular portion of the screen to capture.2. On the iPad, ⇧⌘3 captures the whole screen, just like the Mac (and just like capturing with the top and volume up buttons). The ⇧⌘4 shortcut also captures the whole screen, but in a neat analogy to the Mac, it immediately puts you into editing mode so you can crop the capture down to a smaller size.

Screenshot in editing mode before saving

One oddity about screenshots on iOS that has no analogy with the Mac is that their file format depends on whether they’ve been edited. If you take a screenshot on your iPhone or iPad and save it directly to Photos with no changes, it’s saved as a PNG. But if you crop it or draw on it before saving to Photos, it’s saved as a JPEG. Check the full-screen image above, and you’ll see that its a PNG. This cropped one, though, is a JPEG.

Cropped screenshot saved as a JPEG

As with many things in the Apple world, I have no idea why this is the case.

  1. What used to be called the sleep/wake button on both machines is now called the top button on the iPad and the side button on the iPhone. 

  2. Yes, I know you can also use ⇧⌘4 to capture a window at a time, but I’m focusing on the default behavior here. 

An unzipping shortcut

I’ve been planning to write a post about the new Apple products for over a week, but I keep getting distracted. Today, I went to Apple’s PR pages for the MacBook Air, the Mac mini, and the iPad Pro to download images and went off on another tangent. As usual, I will inflict that tangent on you.

Apple provides the product images as zipped archives, so when I clicked on the link in the press release, I was confronted with this “what do I do?” screen in Safari.

Zip file in Safari

The efficient thing would have been to walk ten feet over to my iMac and download the zip files there, where they can be expanded with almost no thought. But I took the procrastinator’s way out, deciding to solve the problem of dealing with zip files on iOS once and for all.

In the past, I’ve tried out a few zipping/unzipping apps, and they’ve all sucked, with user interfaces that are clumsy to navigate and look like something out of Windows 3.1. What I wanted was a clean, one-click solution similar to what we have on a Mac. A shortcut, if you will…

I went to the Shortcuts Gallery and searched on “zip,” “unzip,” and “archive.” There was a shortcut for zipping up a bunch of files and putting them into an email message, but nothing for unzipping and saving. I also couldn’t find anything by Googling. So I made my own.

Unzip to iCloud shortcut

As you can see, there’s not much to it. You can make it yourself or download it.

You run it as an Action Extension by bringing up the Share Sheet and selecting the Shortcuts Action. After you choose it from your list of Extension shortcuts, you’ll be presented with a Files-like location picker, from which you can choose where the expanded archive will be saved.

Choose where to save the expanded archive

Even though “iCloud Drive” is the Service chosen in Step 2 of the shortcut, it can also save locally and into Dropbox. I suspect it can use services like Box and Google Drive, too, but I don’t use those services and haven’t tested them.

There are a few thing to keep in mind:

I make no claim of originality with this. I’m sure plenty of people have written similar shortcuts, but I couldn’t find them. I didn’t even know there was an Extract Archive step in Shortcuts until today. I should spend more time on this page and this one.

Update Nov 12, 2018 6:53 PM
A few more things:

  • As with the Mac’s Archive Utility and the default behavior of the unzip command, the Extract Files action expands the archive exactly the way it was compressed. Whatever directory structure was zipped up will reappear within the folder you select in Step 2 of the shortcut. This could be a problem, as we’ll discuss below.
  • As you might expect, Federico Viticci has already made a shortcut to do this, which you can download and install. Federico’s is more complicated than mine. He uses the name of the archive (without the .zip extension) to create a new folder within the iCloud Drive Shortcuts folder and extracts the contents of the archive there.

    I’m of two minds on Federico’s shortcut. On the one hand, by creating a folder, it keeps things neat. With my shortcut, when you expand an archive that has lots of top-level files, those files can make a mess of the folder you save them in. On the other hand, Federico’s shortcut puts the expanded archive in a place I’ll never want it to go. It would be easy to edit Federico’s shortcut to put the extracted files somewhere other than the Shortcuts folder, but no matter what, it will go into a fixed location.

    As best I can tell, there’s no way to get the best of both worlds: a neat folder of the extracted files and a choice of where that folder will be.

  • Federico is putting together a massive list of shortcuts he’s built. The list is organized by category and includes both a download link for each shortcut and a brief description of what it does. The list is being continually updated, so it’ll be worth returning to after your first perusal.

Keeping up with IP number changes

Last weekend I was at a hotel working on my iPad and launched Prompt to connect to my iMac at home and run a command. It wouldn’t connect. I launched Transmit (which still works well, despite its zombie status) and couldn’t connect there, either. Obviously, my service provider had changed my home IP number. This happens rarely,1 but I didn’t want to be caught like this again.

My first thought was to write a script that would check the IP number periodically and save it in a file on the server. But a bit of searching led me to this script by Charlie Hawker, which checks for the IP number and sends an email if it’s changed, a much better idea.2

There were, however, two things about the script I didn’t like:

  1. It’s written in PHP. I don’t disklike PHP—I use it to build this blog—but it’s not a language I’m deeply familiar with, and I’d prefer a Python script so I can easily make changes if necessary.
  2. It gets the IP number by loading a website ( and scraping it. I wanted something a little more direct.

The solution to the second problem was to use dig, the domain information groper (a program that was undoubtedly named by a man). Here’s a command that does it by returning information from OpenDNS3

dig +short

With that command in mind, the script, checkip, was easy to write:

 1:  #!/usr/bin/python
 3:  import smtplib
 4:  import subprocess as sb
 5:  import os
 6:  import sys
 8:  # Parameters
 9:  ipFile = '{}/.ipnumber'.format(os.environ['HOME'])
10:  mailFrom = ''
11:  mailTo = ''
12:  gmailUser = 'user'
13:  gmailPassword = 'kltpzyxm'
14:  cmd = '/usr/bin/dig +short'
15:  msgTemplate = '''From: {}
16:  To: {}
17:  Subject: Home IP number has changed
18:  Content-Type: text/plain
20:  {}
21:  '''
23:  # Get the current IP number and the filed IP number
24:  dig = sb.check_output(cmd.split(), universal_newlines=True)
25:  currentIP = dig.strip()
26:  with open(ipFile, 'r') as ip:
27:    oldIP =
29:  # Update the file and send an email if they differ
30:  if currentIP != oldIP:
31:    with open(ipFile, 'w') as ip:
32:      ip.write(currentIP)
34:    msg = msgTemplate.format(mailFrom, mailTo, currentIP)
35:    smtp = smtplib.SMTP_SSL('', 465)
36:    smtp.ehlo()
37:    smtp.login(gmailUser, gmailPassword)
38:    smtp.sendmail(mailFrom, mailTo, msg)

Lines 9–21 set the parameters that are used later in the script. As you can see from Line 9, I save the IP number in the hidden file .ipnumber in my home directory. Line 14 is the dig command we just talked about. Lines 15–21 is the template for the email that the script sends when the IP number change, and Lines 10–13 are the personal settings needed to send the email.

Lines 24–25 run the dig command4 and store its result (after stripping the trailing newline) in the currentIP variable. Lines 26–27 read the .ipnumber file, which contains the IP number from the last time the script was run, and store the contents in oldIP.

Line 30 then compares the two numbers and continues the script only if they differ. Lines 31–32 puts the new IP number into the .ipnumber file, and Lines 34–38 build an email message and send it via GMail. This script works only if you have a GMail account—if you don’t, you’ll have to tweak these lines to get your email service to send the message.

The email it sends is nothing fancy: a subject line telling me the IP number has changed and a body with the new number (which I’ve obscured in this screenshot).

IP number change email

To avoid an error the first time this script is run, create the .ipnumber file and put any old IP number in it. I seeded mine with an incorrect value,, so I could make sure the file changed and the email was sent when the script ran.

I decided to have the script run every hour. If I were using Linux, I’d set this up through cron, but the (more complicated) Mac way is with launchd. Here’s the plist file that controls the running of the script. It’s called com.leancrew.checkip.plist and is saved in my ~/Library/LaunchAgents directory:

 1:  <?xml version="1.0" encoding="UTF-8"?>
 2:  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "">
 3:  <plist version="1.0">
 4:  <dict>
 5:    <key>Label</key>
 6:    <string>com.leancrew.checkip</string>
 7:    <key>ProgramArguments</key>
 8:    <array>
 9:      <string>/Users/drang/Dropbox/bin/checkip</string>
10:    </array>
11:    <key>StartCalendarInterval</key>
12:    <array>
13:      <dict>
14:        <key>Minute</key>
15:        <integer>12</integer>
16:      </dict>
17:    </array>
18:  </dict>
19:  </plist>

The key entries here are Line 9, which gives the full path to the checkip script, and Lines 11–17, which tell launchd to run the script at 12 minutes past the hour.5 Because there are no Hour, Day, or Month entries in this section, it runs every hour of every day of every month.

Because of where it’s stored, this Launch Agent gets loaded into the launchd system every time I log in. To get it to load without logging out and logging back it, I ran these two commands:6

launchctl load ~/Library/LaunchAgents/com.leancrew.checkip.plist
launchctl start com.leancrew.checkip

With it in the system, the agent will continue to run as long as I’m logged in. For me, this is essentially forever, as I keep this computer running all the time specifically so I can log into it whenever I need to.

I’ve thought of improving the script by adding an error-handling section to alert me when the dig command fails. This would let me know, for example, if OpenDNS has stopped running its service. But the possibility of being inundated by false positive emails has kept me from trying that out.

  1. I managed to get the command run by connecting to my work computer, where the IP number is fixed. 

  2. Yes, I know there are services that will do this sort of thing. I don’t want to rely on them. 

  3. This does rely on OpenDNS, but if it ever shuts down or curtails this service, there will always be another similar way to get my IP number. I’ll just change my script to use that instead. 

  4. Note: I typically write scripts in Python 3 nowadays, where the subprocess module is considerably different. This script was easier to do in the Python 2 that comes with macOS because it gets run by the launchd system, which uses an environment that doesn’t know where my installation of Python 3 is. Rather than telling it, I decided to just write it for Python 2. You could call this the $PATH of least resistance. 

  5. Why 12 minutes? No particular reason. Any number under 60 would work just as well. 

  6. The load and start subcommands are now considered legacy features of launchctl and will probably be removed in the future. But for now they still work, so I’ll worry about learning the new subcommands later. 

Building a tweet in Shortcuts

I occasionally tweet links to Kindle books that are on sale. Although I do include my Amazon Associate tag in the link so I get a commission on sales, this is not what you would call a significant money maker. The commission on Kindle books is 4%, which amounts to 8¢ on the typical $2 sale price. I do it because I think the people who follow me might be interested in the book.1

In the past, these tweets were purely functional: such-and-such book is on sale for $2—here’s a link to it. I’d use the Associate app—actually its Share Sheet extension—to get the link. A couple of days ago, I decided it would be fun to jazz up the tweet with an image of the book. I already had some Python code that could use Amazon’s product advertising API to get the URL of a book image; I figured it wouldn’t be too hard to jigger with it to pull out the author and title, too, and combine it with a Shortcuts to build a tweet.

Here’s how it works. While I’m on the web page for the book, I invoke Shortcuts from the Share Sheet and choose the Tweet Kindle Book shortcut. This launches Pythonista to extract the relevant information from the Amazon API. At this point, the shortcut gets stuck with my screen showing Pythonista. To get it unstuck, I tap the ◀︎Safari link in the top left corner.

Shortcut paused in Pythonista step

This little kick to get the shortcut going again is needed because Pythonista is not a particularly good iOS automation citizen. It has a URL scheme, which is what allows Shortcuts to invoke it, but it doesn’t implement callbacks, so it won’t automatically return to the app that called it. Pythonista does have ways to jump to other apps, but I haven’t figured out a way to get it to automatically continue this shortcut.

Anyway, after unsticking the shortcut, it assembles the book information into a tweet that I can edit and send.

Ready to edit and tweet

Here are the steps of the shortcut.

Tweet Kindle Book steps

The Python script (which we’ll get to in minute) takes the URL of the page being shared as input and puts a JSON dictionary of the book information on the clipboard. For this example, the JSON is

{"title": "Provenance",
 "author": "Ann Leckie",
 "imgURL": ""
 "link": ""}

The Get Dictionary from Input step parses the JSON and turns it into a Shortcuts dictionary, from which we can extract the values. The first thing we do is get the URL of the book cover image and use Get Contents of URL to get the image itself. The image is saved in the variable Tweet.

Next, we assemble the text of the tweet from the other parts of the dictionary and a few linking words. This text is added to the Tweet variable (yes, you can add text to a variable that contains an image—I was surprised at that, too) and the whole thing is finally sent off to the Tweet step. You’ll need to have the official Twitter app installed for the Tweet step to be available.

The Python script,, is this:

 1:  import requests
 2:  from datetime import datetime
 3:  import base64, hashlib, hmac
 4:  import urllib.parse
 5:  from bs4 import BeautifulSoup
 6:  import clipboard
 7:  import sys
 8:  import re
 9:  import json
11:  # Get the Amazon URL from the first argument and extract the ASIN
12:  amzURL = sys.argv[1]
13:  itemASIN ='(dp/|gp/product/)([^/?%]+)', amzURL).group(2)
15:  # Date and time
16:  t = datetime.utcnow()
17:  timeStamp = urllib.parse.quote(t.strftime('%Y-%m-%dT%H:%M:%SZ'))
19:  # Parameters
20:  associateTag = 'yourtag'
21:  accessKey = 'youraccesskey'
22:  secretKey = b'yourlongsecretkey'
23:  parameters = ['Service=AWSECommerceService',
24:     'Operation=ItemLookup',
25:     'ResponseGroup=Large',
26:     'ItemId={}'.format(itemASIN),
27:     'AWSAccessKeyId={}'.format(accessKey),
28:     'AssociateTag={}'.format(associateTag),
29:     'Timestamp={}'.format(timeStamp)]
30:  parameters.sort()
31:  paramString = '&'.join(parameters)
33:  # Generate signature from parameters and secret key
34:  unsignedString = '''GET
36:  /onca/xml
37:  {}'''.format(paramString)
38:  signedString =, msg=unsignedString.encode(), digestmod=hashlib.sha256).digest()
39:  sig = urllib.parse.quote(base64.b64encode(signedString))
41:  # Generate URL from parameters and signature
42:  url = '{}&Signature={}'.format(paramString, sig)
44:  # Get image information
45:  resp = requests.get(url)
46:  xml = resp.text
48:  # Extract the information from from the XML
49:  # response and build a dictionary
50:  soup = BeautifulSoup(xml, 'html5lib')
51:  info = {}
52:  info['imgURL'] = soup.item.largeimage.url.text
53:  info['author'] =
54:  info['title'] = soup.item.itemattributes.title.text
55:  info['link'] = '{}/?tag={}'.format(itemASIN, associateTag)
57:  clipboard.set(json.dumps(info))

The first item given to it on the “command line” (which in Shortcuts is what flows into the Run Script step) is expected to be the URL of the web page for the book. Lines 12–13 slurp it in and extract its unique Amazon ID, the ASIN.

Lines 16–17 get the current date and time and format it for later use with the API. Lines 20–31 put together all the data needed to make an API request. The associateTag is what you get from Amazon when you sign up for the Associates program. It’s attached to the link URLs that earn you a commission. The accessKey and secretKey are ID strings you get when you sign up for the Product Advertising API. The secret key is defined as a byte string rather than a normal Python 3 string because the encoding that’s part of the signing process (see below) requires a byte string.

Your request to the API has to be “signed” by encoding the request with your secret key. That’s done in Lines 34–39. The details of this are a little hairy, but Amazon explains them pretty well. Line 42 puts everything together into a URL to send to the API.

An important part of the request is the ResponseGroup parameter. There are many response groups available for plucking out different bits of information on a product. As best I could tell from the documentation, the only one that provides all the pieces I want is the Large group. That’s why you see it on Line 25.

Lines 45–46 use the Requests module to send the request URL to the API and get the response, which is in XML format. Lines 50–54 use the Beautiful Soup module to parse the XML and build a Python dictionary with the author, title, and image URL. Line 55 uses the ASIN and the associate tag to create the link entry in the dictionary.

Finally, Line 57 uses the JSON module to convert the Python info dictionary into JSON format and put it on the clipboard. This is the result of the first step of the Tweet Kindle Book shortcut.

This was a relatively simple shortcut to put together, mainly because I already had a Python script written to get the image URL. I just had to figure out which response group also included the author and title data and where they fit within the XML hierarchy.

I’m still a little annoyed about the pause at the Pythonista step. Maybe there’s a trick I don’t know about to get it to return to the shortcut. Or maybe I should consider rewriting that step in JavaScript so I can use Scriptable, which I think is more suited for use as a Shortcuts step.

  1. If I’m buying the linked book myself because I don’t already own it, it takes 25 book sales for me to break even. Given how cheap frugal discerning my Twitter followers are, that many sales is rare.