Quantcast
Channel: pythonista – MacStories
Viewing all 20 articles
Browse latest View live

Automating iOS: How Pythonista Changed My Workflow

$
0
0

A couple of months ago, I decided to start learning Python.

I say “start” because, as a hobby to fit in between my personal schedule and work for the site, learning the language is still very much a work in progress. I hope I’ll get to an acceptable level of knowledge someday. Coming from AppleScript, another language I started researching and playing with earlier this year, the great thing about Python is that it’s surprisingly easy to pick up and understand. As someone whose job primarily consists of writing, I set out to find how Python could improve my workflow based on text and Markdown; I found out – and I’m still finding out – that Python allows for more flexible and intelligent string manipulation[1] and that some very smart folks have created excellent formatting tools for Markdown writers.

But this article isn’t strictly about Python. Soon after I took my decision to (slowly) learn my way around it, I asked my friend Gabe Weatherhead about possible options to write and execute Python scripts on iOS. Thanks to Gabe’s recommendation I installed Pythonista, and this app has completely changed my iOS workflow.

The iOS Workflow

A bit of backstory first.

The fact that I immediately wondered whether it’d be possible to “do Python” on iOS shouldn’t be a surprise. For the past 12 months, due to changes in my personal schedule, I’ve been forced to rethink my workflow entirely. With a focus on plain text and Markdown, I set out to find ways to ensure I would be able to get work done on iOS devices without limitations. I wanted to get the “I need a Mac for this” daily problem out of the equation; I wanted to be able to do the same work on all my devices. I wanted to automate tedious tasks I was doing every day; ultimately, I consolidated my workflow around devices and software I trust to get work done for me. I wanted the best for me.

I got a 3G iPad with Retina display, so I could put less strain on my eyesight and work from anywhere independently of availability of WiFi hotspots; I got a 32 GB model so I could comfortably cache music and podcast episodes on it. I upgraded my iPhone 4S to an iPhone 5, as I knew the improved cellular options and new antenna would let me get a signal in places – such as hospital corridors – where I knew I wouldn’t be able to work with “regular” 3G.[2]

I didn’t get the best options available for iOS devices: I bought the models that I knew wouldn’t let me down in case of necessity.

From a software standpoint, I got rid of apps that I wasn’t using regularly or that hadn’t been updated in a long time. If I were to reconstruct my workflow – and therefore, indirectly, my income – on a solid software foundation, I needed to do so with developers I could trust. I picked my weapons carefully: I realized plain text was my preferred way of writing and Dropbox my favorite cloud filesystem, so I combined them. It also helped that the Markdown/plain text community is fervent, passionate, and active. I chose iCloud to sync my personal information and settings for apps that support it, such as Downcast or Due. I understood that Evernote wasn’t meant to be a Markdown editor, so I started using it for what it’s really great at: collecting rich notes with images and other reference material. In trying new apps and services, I carefully selected the ones that came with native OS X and iOS apps, possibly with sync; those who follow me on Twitter know that I strive to find services that are ubiquitous and reliable, even when people tell me “you don’t need that on the iPad”.

I need my iOS devices (and especially my iPad) to allow me to get work done from anywhere.

Curating apps that enhance my workflow and finding software I can trust with my data isn’t simply my job as a “reviewer”: it’s an investment for the future of my business. The fact that I take pleasure out of writing about software is a different matter.

I was all set up: I had my plain text workflow with Dropbox, everything else in Evernote, my podcasts and music synced and cached. I had a few games, Twitter clients with sync, communication with my team happening through iMessage. I’m surprised I didn’t think of hooking up my car with the cloud, honestly. [3]

The experiment failed miserably. I started taking the iPad with me to write and publish posts for MacStories, but I was constantly reaching out to my MacBook Air in the other bag. So I stopped carrying the MacBook altogether: too bad one day I had to post a piece of news quickly, and got stuck on creating and uploading the image I needed for the article.

I spent entire weeks thinking about how to solve the issue. Eventually, I came to the realization that I didn’t need more apps, I needed a system to automate iOS. I was missing the little scripts and shortcuts and macros that, for power users, make OS X computers powerful machines to get work done quickly and efficiently. I was missing my automation setup: my Keyboard Maestro, my Alfred, my Hazel. Little things quickly add up: Markdown links take longer to put together[4] and screenshots don’t come out like I want them to. Losing all my scripts and macros in the transition to iOS meant I could only do some things, and not as fast as I would on a Mac. I needed to fix it.

I turned to my friend Gabe. Over at Macdrifter, Gabe writes about “Mac and iOS related material with a slant towards the technical”. Macdrifter is, by far, my favorite tech/indie blog of 2012: Gabe doesn’t write rumors or linkbait; he links to stuff he finds interesting and only reviews software he actually uses. Even better, he comes up with tips and workflows that are very specific (the so-called “niche”) but also incredibly useful.

Initially, Gabe introduced me to Nebulous Notes, a Markdown text editor that works with Dropbox and has support for macros. It has become my default text editor after a couple of weeks of experiments. This is the result of fine-tuning the app to my own needs.

Later, I pinged Gabe about my interest in Python, and he suggested I’d take a look at Pythonista, developed by Ole Zorn. I’m glad I did: Pythonista has profoundly changed the way I approach iOS devices when I know that work needs to be done. I’m not afraid to leave my MacBook at home; in fact, several of the posts published on MacStories in the past month have been produced entirely from an iPad.

Let me show you how I achieved iOS workflow nirvana.

Writing Workflow

I’ve previously written about my writing workflow; you can, however, find more recent additions here and here. As far as iOS is concerned, the biggest change was Nebulous Notes, which, like I said, altered my perception of iOS text editors thanks to macros. If I need to write a blog post on the iPad or iPhone, I can’t use any other app.

I have very specific needs when it comes to “work”. In an unordered list of importance:

  • I need to publish blog posts to WordPress.
  • I need to generate valid HTML for the Markdown I write my posts with.
  • I want to visually preview the Markdown text to make sure the layout of the post is right.
  • I need to upload images to our CDN.
  • I need to convert images to another format and change their output quality.
  • I need to upload images to Dropbox quickly.
  • For articles that include iPhone screenshots, I want those screenshots to look like this.
  • Once I have the link to an image, I need to generate the proper img HTML for MacStories.
  • Occasionally, I may have to download files.
  • I generally create reminders for things I have to do in OmniFocus or Due.
  • I bookmark links I find interesting with Pinboard.

On the Mac, these tasks are made simple by Sublime Text 2 and the Finder. As I’ve previously outlined, Sublime Text can be extended with some fantastic Markdown-related plugins; with the Finder, I can easily upload images from the Desktop to our CDN, I can access any Dropbox file thanks to the Dropbox app, and, when it comes to quick image modifications, I’ve come to trust Acorn and Keyboard Maestro to do the heavy work for me.

Pythonista

Pythonista is a Universal app for writing and executing Python scripts on iOS. There are some differences in terms of navigation and interface between the iPhone and iPad versions, but, overall, Pythonista is consistent across both platforms.

Like any respectable code editor, Pythonista comes with features like syntax highlighting and code auto-completion. For highlighting, there are six color themes to choose from in the Settings, including two Solarized options (dark and light); in the Settings, you can also change the editor font (Adobe’s new Source Code Pro is available), font size, line spacing, and tab width.[5]

Code completion is my favorite feature of Pythonista’s editor. For someone like me who’s just getting started with Python, the app offers an unobtrusive yet highly convenient way to auto-complete your code: suggestions appear directly above the keyboard and they’re colored according to your color scheme. When typing, you can tap on the “auto-complete bubble” to let Pythonista complete your code; code completion is also smart in that only functions/class names/etc related to a module you’ve imported will be suggested.

Code completion can be deactivated in the Settings alongside other options enabled by default. You can set Pythonista to highlight matching (), which is a nice feature to have as it provides a subtle hint to confirm you’ve matched parentheses in the right way. But what I really like is auto-pairing: characters like parentheses, square brackets, and single/double quotes will be matched with closing characters automatically. If you’re coming from nvALT or Sublime Text 2, you should be familiar with this option. For instance, if you beging typing ( a closing ) character will be immediately put on the right, with the cursor in the middle ready to type. Or, if you select text and hit ( the entire text will be wrapped inside ( ).

Character pairing can be confusing to some people, but I find it extremely handy. It speeds up my typing considerably.

Typing in Pythonista, however, is also aided by the extra row available above the standard iOS keyboard. First introduced by iA Writer for iPad, Pythonista follows the trend of several Markdown editors and puts often-used characters directly above the main keyboard, so you won’t have to open the numeric or math keyboard when writing code. These characters include the usual suspects (quotes, parentheses, tab, etc), but with a spin: on the iPhone, due to the smaller screen, the extra row has been “split” in two modes that you can switch with a button on the right. The second mode is where you’ll find the app’s Undo function.

The extra keyboard row has two more tricks up its sleeves. First, most keys can be long-pressed to reveal additional keys in a popup: Undo becomes Undo/Redo and single quotes become single/double quotes and percent sign.

Additionally, Pythonista uses the extra row as a swipe area for the “Hooper Selection” made popular by a concept video earlier this year. Swipe selection only works with one finger (and you can’t move up/down through empty lines), but it’s still a nice touch to make editing quicker and more touch-oriented.

With a combination of code completion, character auto-pairing, extra keyboard row, and swipe selection, I find writing code in Pythonista intuitive and accessible. The fact that everything’s entirely touch-based gives the code editor a feeling of “manipulation” that I haven’t seen in desktop editors.[6]

[Update]: There’s a search field in the code editor; you can also navigate your code’s structure from a document browser available in the top title bar.

Pythonista lets you organize scripts in the Script Library. There are two views to choose from: a “snippet view”, which lets you see scripts as thumbnails with a preview of the first lines of code, and a more traditional list view. Both views can be sorted by name or modified date; you can add scripts with a + button at the top and delete existing ones by tapping on the Edit button.

A downside of Pythonista is that the Script Library doesn’t offer further organizational features to better sort your scripts. You can’t create folders, and it gets pretty confusing in snippet view; for the time being, I’m using list view, but it takes time to find scripts. I would definitely welcome an option to organize scripts by category or purpose.

Due to restrictions imposed by Apple, Pythonista can’t have a sync feature to import executable code from external sources like Dropbox. You can export, but you can’t import – not even from iTunes.[7]

There is, of course, a workaround. Developer Ole Zorn came up with a way to create new scripts off GitHub Gists: make sure you have a Gist URL in your clipboard, run the script, and you’ll have a new Python script in your Library. Alternatively, the Pythonista Community Forums are already filling up with ideas to simply click a bookmarklet to make the Gist importing process even easier. Overall, there’s no doubt Pythonista could use easier importing options, but unfortunately that is not allowed by Apple. Maybe someone will create a script to import a public .py file stored on Dropbox.

Personally, keeping scripts in sync between the two versions of Pythonista and my Mac (therefore Dropbox) is what makes me waste the most time in the app. There’s no way around the fact that you’ll need to organize files manually.

For someone who’s getting started with Python, Pythonista’s best feature is the in-app documentation. Available in a Help menu from the Script Library or code editor, it is based on the official Python 2.7 documentation and it has been reformatted for the iPad and Pythonista. The documentation browser has a Home button to go back to the initial screen, arrows to go back/forward, and a search box. There’s also a button to open a page’s Table of Contents and jump to a section directly, though this is only available on the iPad.

The documentation comes with a full Language Tutorial, Library Reference, Linguage Reference, Global Module Index, General Index, and Glossary. The Tutorial and Reference guides are fantastic tools to start learning Python right inside the app. Ole enhanced code snippets to include “Copy” and “Open in Editor” buttons that will let you easily copy code samples and play around with them in the editor.

The documentation is directly integrated into the code editor. Simply select any class, module, or Python term, hit Help, and a popover (on the iPad) will show the relevant documentation entries in an inline Quick Help menu. Tap, and the Quick Help will jump to the result, highlighting it. On the iPhone, Quick Help is shown in a standard view overlaying the code editor; on the iPad, you can expand the Quick Help popover to a full window with one tap.

Don’t underestimate the convenience of readily-accessible inline documentation: especially on the iPad, and again, especially when you’re not a master of Python, it’s incredibly handy to be able to quickly check out proper module names and syntax.[8]

Pythonista’s console deserves a mention too. Available with a single swipe to the left from the editor (grab the handle on the right side and pull), the console is where ouput gets printed and results are displayed. There’s an interactive prompt (with command history) to try out Python scripts quickly, and the same text field is also used for raw_input when you need to enter text manually in the console.

Pythonista and iOS

Pythonista supports several modules of the standard Python library, but, due to limitations of iOS, it can’t support everything a desktop Python installation can. Aside from importing external modules and libraries – something anyone can do with Python on a computer – Ole had to come up with specific and clever ways to let Pythonista access data outside of its confined app sandbox.

For example, you won’t be able to programmatically read files from the filesystem on iOS: there’s no “Desktop” or “Documents” folder to read from on iOS. For the same reason, you won’t be able to “save” files to specified locations on your local filesystem, as Pythonista can’t break open the sandbox and, say, process a file for GoodReader.

However, not all hope is lost. If you think about it, there is one layer of data that is constantly shared across iOS apps: the system clipboard. Text and images we copy on iOS are stored in the clipboard (also known as “pasteboard”), and they are available at a system-wide level, albeit it’s up to the single developer to determine which kind of content can be pasted or copied – e.g. you can’t paste a photo into Tweetbot’s compose box.

Pythonista can read from and write to the system clipboard. The clipboard module, in fact, has single-handedly reinvented my usage of iOS in combination with Pythonista and third-party apps. I am not dramatizing this: as I’ll explain later in this article, the possibility to read and set text strings and photos through the iOS clipboard has proven to be a simple, yet fantastic way to automate several areas of my iOS workflow. As I’ll detail, x-callback-url has also played an important role.

To overcome the limitations of iOS, Ole Zorn has tried to access every area of the system he could configure with Pythonista. The aforementioned clipboard module can get and set text and images; there’s a canvas module to display vector graphics, and a scene module to draw 2D graphics and animations. There are actual, playable games developed with these modules in the built-in examples[9] and I’ve seen users already experimenting with their own takes on graphical representations of Pythonista’s library.

There are more Pythonista modules that I’ve used in my scripts. The console module, in particular, is one I use on a daily basis: aside from controlling text output (it can clear, set font, and set colors), console can be used directly in the editor as well. Coming from AppleScript, console.alert and console.input_alert are reminiscent of display dialog and display alert in that they use native iOS alerts with buttons or text fields to enter text manually. The console module also allows for secure input, login alerts, and, my favorite, a show_activity command to show the standard iOS network activity indicator in the status bar. I have employed this in a variety of scripts that fetch web data.

The keychain module is another powerful addition. It is secure password storage that lets you store passwords within Pythonista to reuse in other scripts. You can set passwords with keychain.set_password and retrieve them by importing keychain in a script and using get_password. This is my preferred way to save passwords internally within the app without writing them as strings in a script.

The sound module shouldn’t be underestimated: it allows scripts to play simple sound effects such as various version of “ding” or 8-bit inspired Arcade “jump” and “powerup”. I like how sound adds a new layer of presentation to scripts, which are otherwise primarily text based.[10]

There are some modules that aren’t part of the Python Standard Library that are included in Pythonista. I’m using some of them in my workflow.

Last, the editor module. I don’t use this in any of my scripts, but I believe it could pave the way for an evolution of the Pythonista engine into something different.

Here’s why: editor gives you access to the text of the script you’re currently editing. You can get the current selection, get selected text, replace it, or set a new selection. The syntax is simple and it allows, through a couple of lines, to do things like select & replace – with all the combined functionalities of other Python modules. If this doesn’t yet ring a bell to you, think about this: plugins for Sublime Text 2 are written with the Sublime API and are based on Python. As it stands now, editor is a great way to automate text selections and replacements in the code editor; I can’t wait to see if this will evolve in something bigger.

The Actions Menu

The potential of the editor module can be grasped by playing around with the Actions menu of Pythonista. Configurable in the Settings, Actions are shortcuts to scripts from Pythonista’s Library; instead of switching back and forth between the editor and the library for workflows that require multiple scripts, you can just bring up the Actions popover and run a script from there.

If you’ve created scripts that rely on the editor module for text manipulation, it makes sense to add them to the Actions menu for easier access; I, however, have found another advantage of showing my scripts in the Actions menu – the clipboard. To concatenate scripts together, I can just make sure they write their results (usually images or text) to the system clipboard; in this way, I’ll be able to start another script from the Actions menu, as I know it will get data from the clipboard I’ve just set. Ideally, I’d like to see a future version of Pythonista with support for “workflows” – sets of “rules” that determine how data (the clipboard) should be processed by groups of separate scripts, in which order.

The URL Scheme

For the joy of URL scheme aficionados, Pythonista comes with a URL scheme. You can launch the app itself with pythonista://, but the good stuff lies in the parameters you can pass along with the URL. Firstly, you can open a specific script using something like pythonista://MyScript – but even better, you can open and run a script by using pythonista://MyScript?action=run in the URL. To use this, a script with that exact name will have to be in your Library, and no other script will have to be running upon calling action=run.

The really good stuff is what you get by combining the URL scheme with command line arguments. As documented by Ole, you can pass one argument at a time by using &args= or multiple ones with &argv= in the URL. This is best explained with an example: because of the URL scheme, you can create JavaScript bookmarklets that launch Pythonista and pass command line arguments in the URL.

Let’s say I want to pass a webpage’s URL and title as two separate arguments to a Pythonista script. I can use the following line of JavaScript as a bookmarklet in Safari:

javascript:window.location='pythonista://Pinboard?action=run&argv='+encodeURIComponent(document.title)+'&argv='+encodeURIComponent(document.location.href);

The bookmarklet will launch my Pinboard script (more on this later) with argv[1] being the webpage title, and argv[2] as the URL.

Unfortunately, there’s no Launch Center Pro for iPad yet, but once you start thinking about the possible implementations of Pythonista’s URL scheme with web data from Safari, you’ll see how your workflow can benefit from this kind of automation.[11]

Home Screen Shortcuts

Unsurprisingly, Ole thought of his own way to let users enjoy the convenience of the URL scheme without waiting for a decent launch manager to arrive on the iPad.[12]

You can create Home screen bookmarks for Pythonista scripts. Simply head over this webpage created by the developer, enter your script’s name (it’s case-sensitive), hit Create Shortcut, then add the page to your Home screen using Safari’s Add Bookmark menu. The webclip will have a nice Pythonista-themed icon.

Upon tapping, a Pythonista Home screen bookmark will briefly open a blank page and then immediately redirect to the script you’ve configured in the Pythonista app. I’m fairly certain there’s no way to avoid showing a blank page for a second before redirecting to Pythonista; fortunately, it’s really just the fraction of a second, as the redirecting process is instantaneous both on my iPad 3 and iPhone 5.

I use bookmarklets for URLs I need to send to Pythonista from Safari (or iCab), but I prefer Home screen icons for data that’s been copied to the clipboard.

My Scripts

The Pythonista scripts I use on a daily basis are primarily oriented towards speeding up my Markdown workflow. Thanks to input and directions from Ole, I’ve put together a collection of scripts that allow me to get from raw Markdown text to final HTML (with images) ready for publishing in just a matter of seconds, rather than several minutes. I’ve also created scripts that rely on URL schemes to make the iOS apps I use communicate better, automatically.

Markdown to Poster

My most-used script is actually just a combination of simple Python and x-callback-url. It sends the contents of the clipboard to Notesy, which renders the Markdown and passes it to Poster – an app I use to publish posts on MacStories – as HTML. It takes less than 3 seconds to complete and return a new HTML post in Poster.

import webbrowser
import urllib
import clipboard


base = 'notesy://x-callback-url/render-markdown?text='
url = clipboard.get()
text = url.encode('utf-8')
text = urllib.quote(text, safe='')
poster = urllib.quote("posterapp://x-callback-url/create?text=", safe='')
actions = '&output-param=text&x-success=' + poster + '&x-error=pythonista://'
webbrowser.open(base + text + actions)

Not nearly as adopted as it should be, x-callback-url is a standardized protocol for iOS inter-app communication that enhances URL schemes with parameters apps can send/receive to trigger specific actions. In the words of Greg Pierce, its developer:

The goal of the x-callback-url specification is to provide a standardized means for iOS developers to expose and document the methods they make available to other apps. Using x-callback-url’s source apps can launch other apps passing data and context information, and also provide parameters instructing the target app to return data and control back to the source app after executing an action.

In the protocol’s draft specification, Greg details the structure of x-callback-url and how developers can easily take advantage of it by supporting parameters for source and target apps.

My script does indeed take advantage of the x-success parameter provided by x-callback-url. Most iOS apps that use URL schemes have implemented them in a one-way communication method: launch this URL to open this app. Developers who have spent time structuring their URL schemes often come up with richer actions: launch this URL to open this app in a specific view. Developers who rely on x-callback-url and respect the specification can get access to extra parameters such as source, success, error, and cancel. These enable powerful if/then clauses in URL schemes: if the action in the target app fails, go back to source app.

I write and edit in Nebulous, but I publish articles using Poster. As I was researching options to automate my workflow, I stumbled across the Notesy URL scheme. It seemed too good to be true: it supports x-callback-url and comes with an action to render Markdown and pass it to another app or the system clipboard. Essentially, given properly encoded text, Notesy can act as a “bridge” between apps to render Markdown to HTML – without forcing you to tap any buttons.

Why Notesy instead of the built-in Markdown module, though? First, Notesy uses the Sundown Markdown library, the same implemented by the best Markdown editors for iOS. More importantly, Notesy can run SmartyPants upon generating Markdown, which I find particularly useful for my frequent use of en dash on MacStories. Last, Notesy is really fast at rendering Markdown and I like to support an app that properly and cleverly utilizes x-callback-url.

The script itself is very straightforward: base is the Notesy URL scheme we’ll use to send text; url gets the text from the clipboard, encodes it in UTF–8 and replaces special characters using percent-encoding with urllib.quote.

The second part of the script prepares the URL that Notesy will open if it succeeds in rendering Markdown. We’re telling Notesy: if you successfully render Markdown, then open Poster with the HTML as article text. There are two things to keep in mind here: to pass along text with render-markdown in Notesy, use output-param. As you can see, I’m using text to encode text and send it as the result of Notesy’s rendering. Second, when an app launched via URL scheme (such as Notesy) has to open another URL scheme (in our case, Poster’s one), always percent-encode the second URL.

In the last line, the webbrowser module of Pythonista simply launches the complete URL and executes the set of actions described above. The nice thing about webbrowser is that it’ll open standard http:// links within Pythonista, but it can be used to launch other URL schemes without any confirmation dialog.

To show how, in practice, the script takes seconds to complete, I’ve made a video.

- Download

Convert Markdown

If, for some reason, I don’t want to convert Markdown using Notesy, I can rely on Python-Markdown to do the conversion for me. Available in Pythonista 1.2, Python-Markdown expects and returns Unicode input.

import markdown
import clipboard

input_file = clipboard.get()

s = input_file

md = markdown.Markdown()
html = md.convert(s)

print html
clipboard.set(html)

The script simply expects Markdown text to be in the clipboard, converts it to HTML using Python-Markdown, and sets the clipboard to the newly generated HTML. I find it useful for quick Markdown to HTML conversions.

- Download

Markdown to Byword

I’ve already explained why I believe Byword has the best MultiMarkdown previews on iOS. In spite of the lack of true MultiMarkdown converting in Pythonista, I’ve still put together a modified version of the script above to open the preview in Byword via the app’s URL scheme.

import webbrowser
import markdown
import clipboard
import urllib

input_file = clipboard.get()

s = input_file

md = markdown.Markdown()
html = md.convert(s)

clipboard.set(html)

s = clipboard.get()
s = urllib.quote(s.encode('utf-8'))

webbrowser.open('byword://new?text=' + s)

I hope Metaclassy will consider adding a more powerful URL scheme to Byword.

- Download

formd

Thanks to Ole, I’m using a simple adaptation of Seth Brown’s formd Markdown formatting tool to flip the style of Markdown links from inline to reference and viceversa. As Seth explains, “inline Markdown is difficult to read, but useful for writing and editing because the linked text and URLs are adjacent to the words you are writing”. I like to write with inline links, but I prefer to archive my articles as reference-style Markdown files.

#!/usr/bin/env python
# encoding=utf8
"""
Seth Brown
02-24-12
"""
from sys import stdin, stdout
import re
from collections import OrderedDict

class ForMd(object):
    """Format mardown text"""
    def __init__(self, text):
        super(ForMd, self).__init__()
        self.text = text
        self.match_links = re.compile(r'(\[[^^]*?\])\s?(\[.*?\]|\(.*?\))',
                re.DOTALL | re.MULTILINE)
        self.match_refs = re.compile(r'(?<=\n)\[[^^]*?\]:\s?.*')
        self.data = []

    def _links(self, ):
        """find Markdown links"""
        links = re.findall(self.match_links, self.text)
        for link in links:
            # remove newline breaks from urls spanning multi-lines
            parsed_link = [s.replace('\n','') for s in link]
            yield parsed_link

    def _refs(self):
        """find Markdown references"""
        refs = re.findall(self.match_refs, self.text)
        refs.sort()
        refs = OrderedDict(i.split(":", 1) for i in refs)
        return refs

    def _format(self):
        """process text"""
        links = (i for i in self._links())
        refs = self._refs()
        for n, link in enumerate(links):
            text, ref = link
            ref_num = ''.join(("[",str(n+1),"]: "))
            if ref in refs.keys():
                url = refs.get(ref).strip()
                formd_ref = ''.join((ref_num, url))
                formd_text = ''.join((text, ref_num))
                self.data.append([formd_text, formd_ref])
            elif text in refs.keys():
                url = refs.get(text).strip()
                formd_ref = ''.join((ref_num, url))
                formd_text = ''.join((text, ref_num))
                self.data.append([formd_text, formd_ref])
            elif ref not in refs.keys():
                parse_ref = ref.strip("()")
                formd_ref = ''.join((ref_num, parse_ref))
                formd_text = ''.join((text,ref_num))
                self.data.append([formd_text, formd_ref])

    def inline_md(self):
        """generate inline markdown """
        self._format()
        text_link = iter([''.join((_[0].split("][",1)[0], 
            "](", _[1].split(":",1)[1].strip(), ")")) for _ in self.data])
        formd_text = self.match_links.sub(lambda _: next(text_link), md)
        formd_md = self.match_refs.sub('', formd_text).strip()
        yield formd_md

    def ref_md(self):
        """generate referenced markdown"""
        self._format()
        ref_nums = iter([_[0].rstrip(" :") for _ in self.data])
        formd_text = self.match_links.sub(lambda _: next(ref_nums), md)
        formd_refs = self.match_refs.sub('', formd_text).strip()
        references = (i[1] for i in self.data)
        formd_md = '\n'.join((formd_refs, '\n', '\n'.join(i for i in references)))
        yield formd_md

    def flip(self):
        """convert markdown to the opposite style of the first text link"""
        first_match = re.search(self.match_links, self.text).group(0)
        if '(' and ')' in first_match:
            formd_md = self.ref_md()
        else:
            formd_md = self.inline_md()
        return formd_md

if __name__ == '__main__':
    import clipboard
    import console
    md = clipboard.get()
    if md:
    		console.clear()
        	text = ForMd(md)
        	[clipboard.set(t) for t in text.flip()]
      	 	final = clipboard.get()
        	print final

The text output is set back to the clipboard so I can pass it to another script or paste it in my editor of choice. In the future, I’d love to see Nebulous Notes being capable of launching URL schemes if only to trigger my Pythonista scripts directly from the app.

- Download

Screenshots

This is one of the Pythonista scripts I use the most: when I was beta-testing Pythonista 1.2 and Ole added support for images, I knew this would become the script that would allow me not to require a Mac for image processing anymore.

import clipboard
import Image
import console


im1 = clipboard.get_image(idx=0)
im2 = clipboard.get_image(idx=1)
background = Image.new('RGBA', (746,650), (255, 255, 255, 255))

def main():
		console.clear()
		print "Generating image..."
		console.show_activity()

		_1 = im1.resize((366,650),Image.ANTIALIAS)
		_2 = im2.resize((366,650),Image.ANTIALIAS)
		background.paste(_1,(0,0))
		background.paste(_2,(380,0))
		background.show()
		console.hide_activity()

		clipboard.set_image(background, format='jpeg', jpeg_quality=0.80)
		print "\n\n Image set to clipboard"
	
	
console.clear()
print "Create now or Control? \n"

print "[1] Create"

print "[2] Control \n"

set_mode = raw_input("Select a mode: ")

if set_mode == "x":

    print "Exited"

elif set_mode == "1":
	
		if __name__ == '__main__':
			main()

elif set_mode == "2":

		print "\n\n"
		
		print "Which image goes on the left? (in Photos.app order) \n"

		print "[1] The first image"

		print "[2] The second image \n"
	
		set_im = raw_input("Select an image: ")

		if set_im == "x":

				print "Exited"

		else:
                        
    			print "\n\n"
                
		if set_im == "1":
	        
			if __name__ == '__main__':
				main()
                
		elif set_im == "2":
			console.clear()
			print "Generating image..."
			console.show_activity()
	                
			_1 = im1.resize((366,650),Image.ANTIALIAS)
			_2 = im2.resize((366,650),Image.ANTIALIAS)
                        
			background.paste(_1,(380,0))
			background.paste(_2,(0,0))
			background.show()
			console.hide_activity()
		        
			clipboard.set_image((background), format='jpeg', jpeg_quality=0.80)
			
			print "\n\n Image set to clipboard"

Essentially, it is a Python version of my Keyboard Maestro macro for iPhone screenshots. Given two iPhone 5 screenshots, it creates a single composited image showing both screenshots side by side. Using text output in the console it enables me to control the placement of the screenshots: I can specify whether I want the first or second screenshot to be on the left side of the final image.

Line 6/7 grabs images from the iOS clipboard using clipboard.get_image. The get_image command is interesting for two reasons: first, as part of the clipboard module, it is able to read directly from the iOS clipboard – i.e. items you have copied using iOS’ standard “Copy” menu. Second, Ole structured it so that it returns a PIL image from an image in the clipboard. If you copy multiple images at once, you can use the idx parameter to get an image at a given index.

Here’s how idx works, and how I use it. To compose the screenshot, I need two images in the clipboard at once. To do so, I can copy multiple images from the Photos app: from either Camera Roll or Photo Stream, Edit > Select photos > Share > Copy, and you’ll have two images in the clipboard. The important thing to understand with idx and the Photos app is that Pythonista will grab images in “Photos app order” starting at 0.

So, for instance, here’s how idx would get the images shown below:

I don’t always need to, but there are times when I want to control how the screenshots are displayed on the final image – i.e. the second screenshot in Photos-order should actually be pasted first onto the image. I could have probably created something more elegant or simpler, but, in short, here’s how I did it: I defined a main function that pastes the first screenshot (idx=0) to the left side of the final image (coordinates 0,0). The function takes care of clearing the console, printing status messages, showing a spinner in the iOS status bar (it’s not network activity, but I like the visual hint), and doing the image processing; the final image is shown and also set to the clipboard. Through a series of text inputs, the script asks me if I want to create an image with default placement, or, if I want to control it myself. If I choose option 2 (control), it asks me if I want the first or second image to be on the left. In case I want the second image, it uses inverted coordinates for pasting.

Allow me to explain some parts more in detail. Once collected from the clipboard and turned over to PIL, screenshots need to be resized to fit inside the 746x650 image I want as final result. To do so, my main function uses resize (line 15/16) at 366x650 with ANTIALIAS, a high-quality downsampling filter provided by PIL. The process will be a little slower, but the image will be resized maintaining a higher quality.

Line 8 creates a 746x650 white background; the main function pastes the screenshots onto the background, offsetting the second one by 14 pixels so to leave a white strip in the middle. Line 19 shows the final image in Pythonista’s console.

When you show() an image in the console, you can tap on it to Copy it or Save it, just like any other image on iOS. However, you can also set the image back to the clipboard automatically, so it’ll be ready for pasting in, say, a Mail message. Line 22 uses clipboard.set_image with format and jpeg_quality parameters to put the image as a JPEG saved at 80% of quality in the clipboard.

Once set up, you can create a Home screen shortcut for the script; select two iPhone screenshots from the clipboard, tap on the shortcut, and after a few seconds you’ll have a composited image ready in your clipboard.

This script has enabled me to a) save precious minutes, b) be faster and more efficient, and c) uninstall Photoshop Touch, which I was using solely to create this kind of screenshots. To manipulate iPhone screenshots on my iPad, I rely on Photo Stream for screenshots already in iCloud; when I’m on the go, I move screenshots between devices using Scotty.

- Download

iPad Screenshot to JPEG

I have an iPad 3. Whenever I take a screenshot, iOS saves a 2048x1536 .png file weighing at least 1 MB. I want that screenshot to become a lighter JPEG saved at 80% of the original quality.

import clipboard
import Image

image = clipboard.get_image()

background = Image.new('RGBA', (2048,1536), (255, 255, 255, 255))

background.paste(image,(0,0))

clipboard.set_image((background), format='jpeg', jpeg_quality=0.80)

Essentially, a simple adaptation of the screenshots script that could be done better (I’m not sure I even need to paste the image first), but still gets the job done.

- Download

Upload Screenshot and Read Text File from Dropbox

For MacStories image uploads, our Don Southard has set up a script that watches a Dropbox folder for images, and uploads every new image to our CDN. Every uploaded image returns a URL that is appended to a text file also in Dropbox. This allows every member of the team to simply drop images in Dropbox and get a CDN URL back after a few seconds; it is very convenient on the Mac thanks to the Dropbox app, but I wanted to automate the process on iOS as well. With input from Ole, I set up a script that uploads an image and waits 15 seconds before reading the last line of our .txt file from Dropbox.

First, create a Dropbox app and make sure to save a script that lets you log into Dropbox and prints your account info. I’m using this version provided on the Pythonista forums.

The script imports get_client and the account information from keychain.

from dropboxlogin import get_client
dropbox_client = get_client()
import clipboard
import keychain
import console
import time
import httplib
from io import BytesIO
import webbrowser

img = clipboard.get_image()
titles = console.input_alert('Image Upload', 'Enter your image name below')
buffer = BytesIO()
img.save(buffer, 'PNG')
buffer.seek(0)
response = dropbox_client.put_file('/LOLwat/Photos/upload-unedited/' + titles + '.png', buffer)

print "uploaded:", response

time.sleep(15) # delay for 15 seconds

console.clear()


def main():
	dropbox_client = get_client()
	f, meta = dropbox_client.get_file_and_metadata('/SecretLocation/Photos/upload-unedited/LeSecret.txt')
	content = f.read() # can use readlines here to get list of lines
	print 'file content:'
	print '=' * 40
	print content
	print '=' * 40
	print 'metadata:'
	print meta
	last = content.splitlines()[-1]
	clipboard.set(last)
	console.clear()
	print last
	webbrowser.open(last)
	
if __name__ == '__main__':
	main()

Line 11 gets an image from the clipboard, lets me enter a name with input_alert, and saves it as .png in a temporary buffer provided by BytesIO. Using the Dropbox command put_file, it uploads the file to a specific directory in my Dropbox.

Line 18 prints a response from Dropbox, then waits 15 seconds (my Mac mini usually takes 10 seconds to process an image) and the console is cleared. Last, dropbox_client reads a text file, and specifically the last line (the URL that was just appended) using splitlines[-1]. The URL is printed, set to the clipboard, and opened in Pythonista’s web browser for extra confirmation.

I’ve put this in my Actions menu and I use it after generating images with the scripts described above.

- Download

Images HTML

As I mentioned above, once I’ve written a post as Markdown and converted it to HTML, I need to insert images in the post. With Dropbox I can easily send an image from the clipboard to our CDN - but what about getting the proper HTML code inserted in the final post? Theoretically, I could use the built-in uploader of apps like Poster and Posts, paste an image’s URL, and adjust the way I want an image to be displayed. However, I like to have HTML for images inserted just right for MacStories, with more control over image alignment and title.

The script I use for this takes a URL in the clipboard and builds a series of HTML strings around it. I've embedded this script as image so its HTML tags wouldn't create issues with this page.

On line 9, the script asks for text to use for the image’s title and alt attributes in the final string. Said string, aptly named final, is simply a concatenation of various HTML tags and elements I have manually entered. Keep in mind double quotes need to be escaped with the \ character, as shown with aligncenter in the string.

Notably, the script uses console.input_alert to pop up native iOS alert boxes asking for user input.

Once constructed, the string is put back onto the clipboard, ready for pasting in a text editor.

- Download

Copy Webpage Title

I often need to grab a URL’s title when writing blog posts, saving bookmarks, or tweeting links. For those times when this kind of workflow isn’t implemented in another script (as you’ll see below), I have a standalone version that, given a URL in the clipboard, prints the title and URL so I can freely copy them from the console.

import urllib
import clipboard
import bs4
import console

link = clipboard.get()

console.show_activity()

soup = bs4.BeautifulSoup(urllib.urlopen(link))
pageTitle = soup.title.string +' '+ link

console.hide_activity()

console.clear()

print pageTitle

- Download

Download in iCab

iCab is a third-party iOS browser that comes with rich support for x-callback-url. The following script uses iCab’s download action to start downloading a link provided by the clipboard.

import webbrowser
import urllib
import clipboard

base = 'x-icabmobile://x-callback-url/download?url='
url = clipboard.get()
url = urllib.quote(url, safe='')
webbrowser.open(base + url)

This comes particularly in handy as a Home screen icon: copy link, tap the icon, start a download.

- Download

Bonus: Here’s a bookmarklet to send a URL to iCab, which will start downloading it.

javascript:window.location='x-icabmobile://x-callback-url/download?url='+encodeURIComponent(document.location.href);

Convert Twitter URLs

Previously detailed in this post, I’ve put together a script to turn Twitter.com URLs into Tweetbot-specific links that will open a tweet with Tweetbot’s detail view.

import clipboard
import console
import webbrowser

mytext = clipboard.get()
mytext = mytext.replace('https://twitter.com/', 'tweetbot://')
mytext = mytext.replace('statuses', 'status')
mytext = mytext.replace('http://twitter.com/', 'tweetbot://')
mytext = mytext.replace('http://mobile.twitter.com/', 'tweetbot://')
mytext = mytext.replace('https://mobile.twitter.com/', 'tweetbot://')

console.clear()
print mytext

clipboard.set(mytext)

webbrowser.open(mytext)

I use this to share links to tweets with my team, as we all use Tweetbot on our devices.

- Download

Percent Encode

This one percent-encodes text from the iOS clipboard.

import clipboard
import urllib

s = clipboard.get()

s = s.encode('utf-8')
s = urllib.quote(s, safe='')

print s

- Download

Post to Pinboard

When I’m not writing, I’m usually reading. And if I’m reading an interesting article I found on the Internet, there’s a high chance it’ll be bookmarked on Pinboard. Unfortunately, there aren’t fast and intuitive ways to save Pinboard bookmarks on iOS: there’s no iPad version of Pinbook yet, and using the bookmarklet works, but it’s not a great experience.

Annoyed by the cumbersome process of carefully tapping on text fields in a bookmarklet and the lack of simultaneous Instapaper & Pinboard saving in Tweetbot, I built a Python script that saves a URL to my Pinboard account in a few seconds. It works with a bookmarklet or the system clipboard.

import console
console.show_activity()
import urllib
from urllib import urlencode
import bs4
import requests
import webbrowser
import sys
import sound
sound.load_effect('Powerup_2')
import keychain
import clipboard
console.hide_activity()

numArgs = len(sys.argv)

if numArgs < 2:
	url = clipboard.get()

	console.show_activity()

	soup = bs4.BeautifulSoup(urllib.urlopen(url))
	title = soup.title.string
        text = title.encode('utf-8')

	console.hide_activity()

else:

	text = sys.argv[1]
	url = sys.argv[2]

PASSWORD = keychain.get_password('pinboard','ticci')
USER = 'ticci'

tags = console.input_alert('Tags', 'Enter your tags below')

console.show_activity()

query = {'url': url,
         'description': text,
         'tags': tags}
query_string = urlencode(query)

pinboard_url = 'https://api.pinboard.in/v1/posts/add?' + query_string

r = requests.get(pinboard_url, auth=(USER, PASSWORD))

console.clear()

if r.status_code != 200:
	print 'Could not post:', r.text
	
elif r.status_code == 200:
	tags = tags.split(' ')
	tags = ','.join(tags)
	sound.play_effect('Powerup_2')
	print 'Link saved successfully'
	print text
	print "tags: " + tags

Bookmarklet:

javascript:window.location='pythonista://Pinb?action=run&argv='+encodeURIComponent(document.title)+'&argv='+encodeURIComponent(document.location.href);

With the bookmarklet, a webpage’s URL and title are sent to Pythonista via argv. If a link is in the clipboard, the webpage title is fetched using bs4. The script checks if less than two argv are sent, and in that case it proceeds to get URL and title from the clipboard.

The bookmarklet scenario is the more frequent one as I tend to browse on my iOS devices using Safari (because of its superior performance to third-party browsers); however, I find it convenient to also be able to copy a URL from any app and run the script.

The bookmarklet is fairly simple: it composes a Pythonista URL scheme string using two argv parameters – the title and the URL. The title portion is required as it’s what Pinboard will use for the name of a bookmark; in the Pinboard API, it’s called description to maintain compatibility with the legacy Delicious API.

The script begins by assigning sys.argv[1] and sys.argv[2] to title and URL, respectively. On line 10, it pre-loads a sound effect it’ll use at the end if a bookmark has been successfully added to Pinboard. Line 33/34 detemines the password and username to contact the Pinboard API with a GET request; the password has been previously saved to Pythonista’s secure keychain, so I don’t have to show it as string in the script. Line 36 brings up an alert to enter space-separated tags.

Line 40–43 creates the final query string by combining URL, description, and tags and by running urlencode on it. Following Ole’s suggestions, the Pinboard API is contacted using requests on line 47. A spinner is shown in the iOS status bar to indicate the script is working in line 2 (before importing the modules), line 20, and line 38.

The final portion of the script uses an if statement to print an error if the Pinboard response code is different than 200. If the GET request is completed without error, the script prints a message with the title of the bookmark and tags assigned to it (the sound is also played). Previously space-separated tags are split by individual words and joined using commas in a single string.

If you’re running the script with a URL in the clipboard, the only difference is how it uses clipboard.get() to grab a URL and runs bs4.BeautifulSoup(urllib.urlopen()) to get a webpage name as string (a spinner is shown in the status bar during this process).

Because of the check for argv on line 17, the same script works regardless of bookmarklet or clipboard usage; if you’ve copied a URL to the clipboard, I recommend adding this script to the Home screen.

- Download

Link in Due

Due is a nice reminder app for iOS and OS X with support for iCloud and Dropbox sync, as well as x-callback-url for creating reminders from other apps. I like to save links as “quick reminders” to check out in a few minutes (or hours) in Due, which happens to have a feature to open URLs contained in a reminder upon hitting the “Done” button. Like the Pinboard one, this script works with a bookmarklet or the system clipboard. If you use the bookmarklet, it grabs a link’s URL and title from Safari ; if you’ve copied a link to the clipboard, it fetches the webpage’s title using bs4.

import console
console.show_activity()
import clipboard
import webbrowser
import urllib
from urllib import urlencode
import bs4
import sys

numArgs = len(sys.argv)

addnew = 'due://x-callback-url/add?title='

addtime = '&secslater='

console.hide_activity()

if numArgs < 2:
	
	console.show_activity()

	url = clipboard.get()

	soup = bs4.BeautifulSoup(urllib.urlopen(url))
	newlink = (soup.title.string + ' ' + url).encode('utf-8')

	console.hide_activity()

else:

	title = sys.argv[1]
	url = sys.argv[2]
	newlink = title + ' ' + url

newtask = console.input_alert('What is this?', 'Type your reminder below')

newtime = console.input_alert('When?', '3600 for 1 hour, 1800 for 30 minutes')

console.hide_activity()

text = newtask + ' - ' + newlink

encoded = urllib.quote(text, safe='')

err_handler = '&x-source=Source%20App&x-error=pythonista://&x-success=' + url

openDue = addnew + encoded + addtime + newtime + err_handler

console.clear()

webbrowser.open(openDue)

Bookmarklet:

javascript:window.location='pythonista://Due?action=run&argv='+encodeURIComponent(document.title)+'&argv='+encodeURIComponent(document.location.href);

Title and URL are represented by argv sent by the bookmarklet. The script revolves around the Due URL scheme, which is based on x-callback-url and is split up in multiple parts to allow me to enter a description and “due time” using console.input_alert in line 35 and 37.

Line 41 combines my description of the reminder with the title and URL using a dash as separator between the two; line 43 percent-encodes the reminder’s name as requested by Due. Last, line 45 specifies how to handle errors in Due: thanks to x-callback-url, Due can go back to another app if the user cancels the action of adding a reminder. Unfortunately, the friendly name for x-source isn’t dynamic – meaning, Due can’t display a different dialog depending on whether the user cancels the action or completes it. So, I chose to display a general “Return to Source App” dialog that returns to Pythonista in case of error (user canceled the reminder) or goes back to the link opened by the bookmarklet if the reminder is added.[13]

Line 47 composes the final URL to open in Due using all the pieces created and encoded in the script, and the last line uses webbrowser to launch Due.

I’ve been using Due to save quick reminders and I like the automation provided by this script, which I’ve added to my Home screen. In using it, I’ve noticed I’d like Pythonista to offer an option to open the iOS numeric keyboard when I need to enter numbers in input_alert.

- Download

Bonus: Here’s a bookmarklet to send a link (URL + title) to Due without Pythonista (you’ll have to enter any additional text and alarms manually). It also works on the Mac.

javascript:window.location='due://x-callback-url/add?title='%20+%20%20encodeURIComponent(document.title)+%20'%20'+%20encodeURIComponent(document.location.href)+'&x-source=Source%20App&x-error=due://x-callback-url/&x-success='+encodeURIComponent(document.location.href);

Sequential Task in Due

Inspired by Sean Korzdorfer, I’ve created a script that adds a reminder including an additional URL to create a sequential reminder in Due upon completing the first one.

import clipboard
import console
import urllib
import webbrowser

addnew = 'due://x-callback-url/add?title='

addtime = '&secslater='

newtask = console.input_alert('First Task', 'Type your reminder below')

newtime = console.input_alert('When?', '3600 for 1 hour, 1800 for 30 minutes, 300 for 5')

seqtask = console.input_alert('What next?', 'Type your reminder below')

seqtime = console.input_alert('Second Task?', '3600 for 1 hour, 1800 for 30 minutes, 300 for 5')

secondR = urllib.quote(seqtask, safe='')

newlink = 'due://x-callback-url/add?title=' + secondR + '&secslater=' + seqtime

encoded = newtask + ' ' + newlink

text = urllib.quote(encoded, safe='')

openDue = addnew + text + addtime + newtime

webbrowser.open(openDue)

Similar to first Due script, I’ve noticed spaces in the sequential reminder weren’t being encoded properly at the end; I’ve thus added an additional percent-encoding for seqtask that ensures spaces are preserved for the sequential reminder’s name.

This script can come in handy for reminders that need to be executed one after the other such as “do laundry” and “fold laundry”. When completing the first reminder, Due will ask you to launch the embedded Due URL scheme, which will create the second reminder without leaving the app.

- Download

Drafts

I use Agile Tortoise’s Drafts with a variety of scripts that, on my remote Mac mini, process text coming from email or text files and add it to other text files or OmniFocus. I also use Drafts to send text to other services such as Twitter and Evernote, and I’m a fan of the custom email actions added in the latest update to the app.

The script I use lets me send text to Drafts quickly. It grabs a webpage’s selection and URL if sent by a bookmarklet; if not, it pops up a console alert allowing me to enter text manually and send it to Drafts.

import sys
import webbrowser
import console
import urllib

numArgs = len(sys.argv)
base = 'drafts://x-callback-url/create?text='

if numArgs == 3:

	clip = sys.argv[2]
	link = sys.argv[1]
	
	if clip =="":
	
		text = link
		text = urllib.quote(text, safe='')
	
	else:
	
		text = clip + "\n" + "\n" + "From: " + link
	
		text = urllib.quote(text, safe='')
	
else:
	
	text = console.input_alert('Drafts', 'Enter your draft below')
	text = urllib.quote(text, safe='')
	
webbrowser.open(base + text)

Bookmarklet:

javascript:window.location='pythonista://Drafts?action=run&argv='+encodeURIComponent(location.href)+'&argv='+encodeURIComponent(window.getSelection());

On line 9, the script checks for the number of argv sent by the bookmarklet: if there are three and if the selection is empty, the text is prepared as a single string consisting of the URL. If there are three but the selection is not empty – meaning, the bookmarklet was sent with text selected in the browser – text is made of the selection, an empty line, and the URL. If there are no arguments (script wasn’t triggered by a bookmarklet), an input_alert lets me write a note manually. In all three cases urllib.quote encodes text for Drafts. The base Drafts x-callback-url is opened by webbrowser alongside text on line 30.

For me, this is the fastest way to send a URL or Safari’s selection and URL to Drafts, so I can forward them to other services configured with the app.

- Download

Append to Notesy

I keep a Scratchpad.txt file in my Dropbox where I append bits of text and information from my Mac and iOS devices. I’ll describe my workflow for this file in a future article. As an alternative to the Drafts script to append text “remotely”, I’ve set up a script based on Notesy’s local URL scheme that appends text like I want to: with a date string and the information copied from the clipboard directly below it.

Like the Pinboard, Due, and Drafts scripts, there’s an option to use this with a bookmarklet in Safari or your iOS browser of choice. The bookmarklet checks for text selected in the current tab and sends that selection to Pythonista to process it for Notesy. Due to the nature of iOS’ Safari, this only works on the iPad, where bookmarklets can be tapped in the browser’s toolbar so window.getSelection() can be preserved while tapping. With JavaScript, I can send the original page’s URL as argv to the script: this makes it possible to “round-trip” back to Safari after the text from a webpage has been clipped to Notesy. Unfortunately, due to a bug in the current version of Notesy, a “?” character is appended to a URL configured with x-success; the Notesy team is aware of the bug and working on a fix.

import console
console.show_activity()
import webbrowser
import urllib
import clipboard
import datetime
import sys

today = datetime.datetime.now()
base = 'notesy://x-callback-url/append?name=Scratchpad.txt'
file = '&text='

console.hide_activity()

numArgs = len(sys.argv)

if numArgs == 3:

	clip = sys.argv[2]
	link = sys.argv[1]
	
	actions = '&x-success=' + link + '&x-error=notesy://'
	
	if clip =="":
	
		text = "\n" + '_' + today.strftime("%Y-%m-%d") + '_' + "\n" + link
		text = urllib.quote(text, safe='')
	
	else:
	
		text = "\n" + '_' + today.strftime("%Y-%m-%d") + '_' + "\n" + clip + "\n" + "\n" + "From: " + link
		text = urllib.quote(text, safe='')

else:

	url = clipboard.get()
	clip = url.encode('utf-8')
	actions = '&x-success=notesy://&x-error=notesy://'
	text = "\n" + '_' + today.strftime("%Y-%m-%d") + '_' + "\n" + clip
	text = urllib.quote(text, safe='')

webbrowser.open(base + actions + file + text)

Bookmarklet:

javascript:window.location='pythonista://Notesy?action=run&argv='+encodeURIComponent(location.href)+'&argv='+encodeURIComponent(window.getSelection());

I’ve got to thank Gabe for suggesting the datetime module. Line 9 specifies what “today” is, and base creates the URL for appending text to a specific file in Notesy (using x-callback-url). When appending text, a new line is added with \n then the date is inserted using the format I prefer with strftime. Another new line is added, and a percent-encoded string is appended underneath the date. With x-callback-url, the script returns to Pythonista if it succeeds, or stays in Notesy in case of error. As usual, app URL schemes are launched from Pythonista using webbrowser.

Like Drafts, the script checks for an empty selection in Safari. If the clip argument is an empty string, only the URL is appended to Notesy’s text file.

This is yet another example of the powerful URL scheme provided by Notesy. I would like to see an x-callback-url parameter to make sure Notesy initiates a sync before and after appending the text and switching back to Pythonista, so I can make sure the text I appended is also synced to all my Dropbox clients (and to avoid conflicted copies).

- Download

OmniFocus Task for Drafts

My last script is another take on the bookmarklet/clipboard combination for Drafts that is formatted for OmniFocus task importing via email. It is primarily intended to quickly add web URLs to OmniFocus using Drafts’ email actions.

import console
console.show_activity()
import webbrowser
import urllib
import bs4
import sys

addDrafts = 'drafts://x-callback-url/create?text='

console.hide_activity()

numArgs = len(sys.argv)

if numArgs == 3:

	clip = sys.argv[2]
	link = sys.argv[1]
	
	if clip =="":
	
		text = link + ' ' + '@Links'
		text = urllib.quote(text, safe='')
		openDrafts = addDrafts + text
	
	else:
	
		text = link + ' ' + '@Links' + '\n' + clip
		text = urllib.quote(text, safe='')
		openDrafts = addDrafts + text

openDrafts = addDrafts + text

webbrowser.open(openDrafts)

Bookmarklet:

javascript:window.location='pythonista://OFLink2Drafts?action=run&argv='+encodeURIComponent(location.href)+'&argv='+encodeURIComponent(window.getSelection());

The script is essentially the same of the regular Drafts one, excepts it uses OmniFocus’ email syntax for contexts and notes; it works with the bookmarklet, and the browser selection is added after a new line so OmniFocus will see it as a note for a new task. Line 21/27 adds a “@Links” string that will identify the task as belonging to the “Links” context in OmniFocus.

- Download

iOS Automation

The scripts above are just an example of how my limited knowledge of Python allowed me to radically improve my iOS workflow thanks to the power of Pythonista.

I’ve been relying on these scripts for a few months now, and I’ve been able to get work done on iOS just like I do on my Mac. However, while the end result may be the same (articles get published and notes get saved), the setup is entirely different.

I don’t think iOS automation will ever be exactly like OS X automation. Pythonista has a series of limitations that, in comparison, running Python on OS X or any other desktop operating system doesn’t have. Hardcore Python users may not like this. At the same time, though, I also find working in Pythonista more intuitive, safer, and, ultimately, easier. Why shouldn’t I “compromise” if the end result is the same but I actually get more joy out of the experience?

I believe that, going forward, Pythonista and other similar apps will show a new kind of “scripting” and task automation built around the core strenghts of iOS. As we’ve seen, x-callback-url is a standard that leverages a part of iOS – URL schemes – to achieve simple, user-friendly and URL-based inter-app communication that can be used in a variety of ways. Looking ahead, there’s a chance rumored features such as XPC will bring more Mac-like functionalities to iOS, but developers will still find new ways to make iOS more powerful without giving up on positive aspects such as increased security and the simplicity of the app model.

We’re in the early stages of iOS automation, but Pythonista, Launch Center Pro, and x-callback-url are indicating the path other developers should follow.

Pythonista shows that it’s possible to be a power user on iOS while playing by Apple’s rules.


  1. Splitting text items at a specific delimiter takes two lines in Python. Don’t get me started on AppleScript Text Item Delimiters.  ↩
  2. My town, Viterbo, is (surprisingly) testing DC-HSDPA wireless networking with TIM, the carrier I chose for all my devices. With the iPad 3 and iPhone 5, I usually get between 10–15 Mbits (download) and 3–6 Mbits (upload) here in Viterbo; it’s faster than my home connection with Fastweb. When not on DC-HSDPA, the iPhone 5 still manages to find good 3G signal (2–3 bars) in areas where previously the iPhone 4S went to “No Service”.  ↩
  3. Actually, no, I did.  ↩
  4. More about this here. In short, I care about having proper title attributes when converting Markdown links to HTML.  ↩
  5. Font options are also available for the output of the interactive prompt.  ↩
  6. If you want, you can pair an external keyboard with Pythonista via Bluetooth. Ole Zorn implemented some shortcuts for the code editor exclusively for external keyboards. As for touch, I love how iOS lets you select an entire line by tapping with two fingers.  ↩
  7. The exporting options offered by Pythonista are very comprehensive. You can export Python scripts as Xcode Projects to build apps on the desktop, even selecting parameters like “Targeted Devices” and “Home Button” behavior beforehand, from Pythonista. I’ll cover this in a future post. To share scripts as scripts, you can send them via email, open in other apps (such as Textastic), or share on GitHub. Gists can be private or anonymous.  ↩
  8. If you select a term that hasn’t got a “best match”, Pythonista’s Quick Help will display possible search results for your query.  ↩
  9. If you delete the built-in examples (like I did), you can always restore them in the Settings. The fruit game is my favorite so far. Crazy to think it’s actually made in Python, on a phone.  ↩
  10. Check out the documentation for an example on how to play specific notes with the Piano sound.  ↩
  11. Not to mention using the URL scheme in combination with x-callback-url and multiple iOS apps that support the standard.  ↩
  12. Yes, I have tried Launch+.  ↩
  13. My reasoning for x-success and x-error is the following: go back to Pythonista if the user cancels or the reminder couldn’t be added; go back to the URL after adding the reminder so the user can close the tab (or re-confirm he wanted to really add that link).  ↩

Quickly Email A Picture On iOS Using Pythonista

$
0
0

Quickly Email A Picture On iOS Using Pythonista

In my review of Pythonista yesterday, I didn’t include any scripts to send email messages. Email is, however, a huge part of my iOS workflow, as I often send screenshots back and forth with my teammates about upcoming site features or new apps I’m testing. Fortunately, Pythonista developer Ole Zorn shared today a script that uses smtplib to quickly send an image via email. His script is available on GitHub Gists here.

I have modified it slightly to import my login data using keychain and send an image that’s been previously copied to the clipboard. In this way, I can take a screenshot/photo, open the Photos app, copy it, and send it via email in seconds, at full-size. You can save the script as shortcut on your Home screen and have one-tap access to it, or, even better, you can copy images from Safari without saving them first to the Camera Roll (though, in my tests, this hasn’t always worked reliably). My modification also uses console.input_alert to let you enter a different email address and Subject every time, and it plays a sound effect when an email is sent. Right now, the ImageMail script works with Gmail, but it could be easily modified to work for other email services.

In a future version of Pythonista, I think it’d be neat to have a dedicated Address Book module to return contact fields such as email addresses or Twitter usernames; Ole suggests Reminders and Calendar integration might be handy as well. I think Pythonista has a very bright future, so we’ll see. In the meantime, you can download my modified version of the ImageMail script here.

Pythonista is available at $4.99 on the App Store.

Quickly Create Pythonista Shortcuts with Custom Icons

$
0
0

Quickly Create Pythonista Shortcuts with Custom Icons

Interesting set of scripts posted on the Pythonista Community Forums (which, by the way, are becoming a daily appointment for me as Pythonista users are coming up with all sorts of tricks). Using Pythonista as a web server and Safari, you can create local (and unsigned) .mobileconfig files to automate the installation of Pythonista webclip icons. As I explained in my review, Pythonista can launch specific scripts using webclips created from a special webpage:

Upon tapping, a Pythonista Home screen bookmark will briefly open a blank page and then immediately redirect to the script you’ve configured in the Pythonista app. I’m fairly certain there’s no way to avoid showing a blank page for a second before redirecting to Pythonista; fortunately, it’s really just the fraction of a second, as the redirecting process is instantaneous both on my iPad 3 and iPhone 5.

Using the scripts linked in the forums, I easily managed to create a custom icon for my Markdown-Poster workflow that uses Poster‘s icon instead of the default Pythonista one. To extract and convert iOS icons for personal use, I recommend Crunch, which I’ve also previously reviewed. I was intrigued by how forum user pudquick figured out the installation of provisioning profiles from Pythonista:

When you run the code, it starts a web server in Pythonista – and copies the URL for the generated .mobileconfig file to the clipboard. When you switch to Safari and attempt to load the URL, the socket connects – but it’s waiting for communication from the web server in Pythonista (which is paused, since it’s in the background).

As soon as you switch back to Pythonista, this un-pauses the web server fast enough to cause Safari to finish loading the .mobileconfig file while it’s swapping to the background, which then triggers the installation screen!

Combining the script with this other one to easily generate base64 images, I suggest replacing Image.BILINEAR on line 20 with Image.ANTIALIAS for slower but better results in the overall crispness of the icon (I also changed the size to 114×114 for my Retina iPad).

Looking forward to improvements for Pythonista shortcuts (as mentioned by developer Ole Zorn in the thread), this is a nice stopgap solution to use scripts with custom shortcut icons in the Home screen.

Home Automation With An iPad

$
0
0

Home Automation With An iPad

Here’s another interesting use case for my ongoing coverage of Pythonista. From the Pythonista Community Forums, user nlecaude shares a script and a demo video showing how he managed to control the lights in his house using Pythonista (thanks, Gabe).

The Pythonista app is pretty simple, it’s basically crossfading between different images to show the current state of the lights. I have one layer for each state (3 lights so 2^3) and I have invisible layers that I use as buttons to trigger the lamps and transition on and off. I’m quite fascinated by the possibilities of Pythonista.

If you watch the video below, it basically looks like magic. This guy is tapping on a photo of his room on an iPad to turn the actual lights on and off. In practice, he’s using a Python library to control a Philips Hue system that reacts to touch input from Pythonista.

For those unaware of Philips’ product, Hue is a personal wireless lightning system that can be remotely controlled and programmed to offer different lightning settings and color combinations for every occasion. Philips isn’t offering an SDK for developers yet, but the Python library manages to directly connect to the Hue wireless bridge and send input commands.

As nlecaude writes, this is just a script put together in 10 minutes with an unofficial library. The possibilities for home automation programmed from an iPad are seriously intriguing.

From Instapaper and Pythonista To Dropbox and Evernote As PDF

$
0
0

I’ve already expressed my preference for archiving webpages as PDFs rather than simple “bookmarks” on an online service. When I come across a webpage that I know I want to keep for future reference, I like to generate a clean-looking PDF file with selectable text that I can rely on for years to come.

Lately, I have become obsessed with turning longer articles I find on the Internet also into PDFs for long-term archival. For as much as I like Instapaper, I can’t be sure that the service will be around in the next decades, and I don’t want my archive of longform and quality content to be lost in the cloud. So I have come up with a way to combine Instapaper with the benefit of PDFs, Dropbox, and automation to generate documents off any link or webpage, from any device, within seconds.

Yesterday I put together an iOS and OS X workflow to generate PDFs remotely on my Mac, starting from a simple bookmarklet on iOS. On an iPhone or iPad, I can simply hit a button in Safari, and wait for Pythonista to turn a webpage (that’s already been passed through Instapaper’s text bookmarklet) into an .html file in my Dropbox, which is then converted to PDF and added to Evernote. It sounds complex, but in actual practice I can go from a Safari webpage on iOS to a PDF in the Evernote app in around 30 seconds. Hopefully you’ll find this quick solution useful; feel free to modify it and/or send suggestions.

The script relies on Pythonista, which generates the actual .html file that is uploaded to Dropbox; Hazel and PDFpen, which monitor Dropbox and OCR the PDF on my remote Mac; and, last, Evernote, which creates a new note containing the file as PDF. I’ve already explained why I like using the Instapaper parser for PDF generation, and the underlying structure of the Pythonista script is based on the scripts I use with the app.

First, when I’m in Safari and I find a webpage that I want to archive, I hit the following bookmarklet in my Bookmarks Bar:

javascript:window.location='pythonista://MakePDF?action=run&argv='+encodeURIComponent(document.title)+'&argv='+encodeURIComponent(document.location.href);

The bookmarklet simply grabs a webpage’s name and URL as two separate parameters, which are sent to Pythonista using argv.

The script itself is fairly straightforward and inspired by Gabe Weatherhead’s use of urllib2.urlopen to get the text of a URL passed through the Instapaper bookmarklet. Like my previous scripts, the script checks if the number of arguments sent is less than two; in that case, it assumes I haven’t forwarded a webpage name and URL via bookmarklet, and proceeds to grab them using a link in the clipboard (Lines 18–24).

import clipboard
import urllib2
import console
from dropboxlogin import get_client
dropbox_client = get_client()
import keychain
import time
import webbrowser
import sys
import webbrowser
import urllib
import bs4

numArgs = len(sys.argv)

console.clear()

if numArgs < 2:
    url = clipboard.get()
    console.show_activity()

    soup = bs4.BeautifulSoup(urllib.urlopen(url))
    title = soup.title.string
    name = title.encode('utf-8')

    console.hide_activity()
    
else:
    webpage = sys.argv[1]
    name = webpage.encode('utf-8')
    url = sys.argv[2]

insta = 'http://instapaper.com/text?u='

URL = insta + url

print 'Generating HTML file...'

getText = urllib2.Request(URL)
openText = urllib2.urlopen(getText)
content = (openText.read().decode('utf-8'))

final = content.encode('utf-8')

print 'Uploading HTML file to Dropbox...'


response = dropbox_client.put_file('/Apps/PDFURL/' + name + '.html', final)

print 'Your HTML file has been uploaded'

print 'Converting to PDF...'

time.sleep(15)

urlstring = 'evernote://'

webbrowser.open(urlstring)

Line 33 composes a base URL for Instapaper’s parser, to which we’ll append the actual URL we want to turn into a PDF (as usual, strictly for personal use). Lines 39–43 request the URL, its contents (the HTML generated by Instapaper’s parser) and encode everything in UTF–8. Line 48 puts the HTML contents inside an .html file in my Dropbox using the method outlined in my original Pythonista review. At this point, the .html file is off to my Mac mini, which will process it. Line 54 waits 15 seconds (the time it usually takes my mini to finish conversion and OCR), and the last line of the script simply launches Evernote, which will have my PDF ready for further reading or organization.

On the Mac’s side, I rely on Hazel and PDFpen. I have started incorporating PDFpen in my paperless workflow recently, and I bought a Personal license last night because I absolutely love the software and the company behind it.

Yesterday, I noticed my previous solution to generate PDFs off .html files, wkpdf, wasn’t working properly on Mountain Lion. It always errors out in the shell on both 10.8 and 10.8 Server when I pass an .html file to it. Fortunately, per @themodernscientist recommendation I installed wkhtmltopdf, which is slightly slower than wkpdf but has a bunch of options to truly customize the look of your final PDF. You can read more about wkhtmltopdf here and here (I installed the “app” version and simply moved the executable using the Terminal command mentioned here). Specifically, I like how wkhtmltopdf lets you set a specific encoding and put a --no-background flag to strip an .html file off its background color.

The first Hazel rule monitors a folder for .html files, runs a shell script, and than trashes the original files.

for file in "$@"
do 
/usr/local/bin/wkhtmltopdf --no-background --encoding utf-8 "$file" "$file".pdf
done

The second rule in the same folder uses AppleScript and PDFpen to apply OCR on the newly generated PDF file. As @themodernscientist points out there is a way to fix selectable text in wkhtmltopdf, but I like PDFpen’s OCR anyway, so I decided to use it instead. I found the original script on DocumentSnap’s website; I like it because it simply opens PDFpen, performs the OCR, and then closes the document automatically, saving it.

tell application "PDFpen"
    open theFile as alias
    tell document 1
        ocr
        repeat while performing ocr
            delay 1
        end repeat
        delay 1
        close with saving
    end tell
end tell

Once OCR’d, Hazel moves the document in a ConvertedPDFs folder that, upon receiving a new file, runs an AppleScript to add it to Evernote. This is the same script I use for my paperless workflow, and it’s based on my previous article about sending Mail messages to Evernote.

set theTags to {"webpages"}
tell application "Evernote"
    synchronize
    set theNewNote to (create note from file theFile notebook "Inbox" tags theTags)
    synchronize
end tell

And that’s it. While my iOS device is waiting with a “Converting to PDF…” message, my Mac takes care of transforming the .html file, putting a nice white background on the already-nice Instapaper-parsed text, applying OCR to the PDF, and moving it to Evernote. It takes seconds, it happens remotely and automatically, and I just need to wait for the finished document to end up in Evernote, which synchronizes immediately.

The use of the last line in Pythonista is debatable. In a first version of the script, I used webbrowser.open on an iCab-powered URL scheme to open a PDF moved to my Dropbox Public folder because I could simply “guess” the URL it would have at the end of the process. If you’re interested in such a worklfow, use something like this at the end of the script:

If you have suggestions or ideas for improvement, feel free to get in touch.

Update: Thanks to wkpdf developer Christian Plessl, here’s a simple fix to run wkpdf on .html files on Mountain Lion: simply add a --ignore-http-errors to the conversion command and wkpdf will suppress errors while still generating the PDF. In Hazel, add the following:

for file in "$@"
do 
wkpdf --source "$file" --output "$file".pdf --ignore-http-errors
done

As expected, wkpdf takes considerably less time than wkhtmltopdf and works nicely with Instapaper-parsed files, using a white background.

iOS YouTube Downloader with Pythonista

$
0
0

iOS YouTube Downloader with Pythonista

Useful script created by “pudquick” on the Pythonista forums:

Browse to http://m.youtube.com in Mobile Safari, view a video that you like (press Stop if it starts playing, you need to be looking at the page – not the video actually playing), then click on “Bookmarks” and select the bookmarklet that you created.

This will launch my script, which will pull the URL of the page you were looking at as an argument, parse it, figure out the direct download URL for the .mp4 video file, then open iDownloads directly to that URL to start downloading it.

As pudquick says, third-party YouTube clients with a “download” functionality are usually removed from the App Store as Google doesn't allow downloading video files from the service. However, by using Pythonista to crawl the webpage and find the direct .mp4 link of a video, pudquick managed to put together a handy solution to go from your web browser to Pythonista and then directly to iDownloads to start downloading the .mp4 file.

However, I don't use iDownloads – I prefer Readdle's Documents and good.iWare's GoodReader. Replacing iDownloads with your favorite file manager is very easy: in the penultimate line of the script, replace the iDownloads://URL with the one of the app you want to use (the URL of the .mp4 will be appended to it). Unfortunately, Documents doesn't seem to be able to download .mp4 files in this way, but I had no problems with GoodReader. Simply use ghttp://to forward the .mp4 file to GoodReader and start downloading it automatically.

Make sure to check out pudquick's explanation of the script and bookmarklet here. For our previous coverage of Pythonista, check out our tag page and my original review.

Chaining Tweetbot, Pythonista, Drafts, and iMessage for URLs

$
0
0

DraftsMessages

Last night, Tweetbot for iOS was updated with support for the Twitter 1.1 API, which, among various requirements, includes the need of linking a tweet’s timestamp – the date and time when it was sent – to its unique URL on twitter.com. In Tweetbot, you can now open the tweet detail view and tap on the timestamp to automatically open the Twitter website in your default browser; in terms of interaction, I like this change because it lets me open tweets in Google Chrome with just one tap.

In thinking about the update last night, I realized that:

  • My team and I use iMessage for daily communication;
  • The majority of URLs we share are Twitter URLs;
  • We all use Tweetbot on iOS and OS X;
  • Easier browser access means easier bookmarklet triggering;
  • Drafts can access iMessage.

And I concluded that:

  • I could chain every piece of the puzzle together;
  • Hopefully somebody else will find it useful and adapt the workflow to other similar scenarios.

Therefore, I created a browser bookmarklet, a Python script, and a Drafts action to automate the entire process and demonstrate how you can convert Twitter URLs to tweetbot:// URLs and send text from Pythonista to Drafts.

As usual, I am posting the following workflow as a proof of concept that you can modify and adapt to your needs. For instance, you can change the action that is triggered in Drafts, the x-success parameter that will be triggered, or the way Twitter links are converted to Tweetbot-specific URLs.

The first step is a browser bookmarket that will take the current URL (in our case, most likely a Twitter link opened from Tweetbot) and send it to Pythonista.

Here’s the code:

javascript:window.location='pythonista://TweetbotDrafts?action=run&argv='+encodeURIComponent(document.title)+'&argv='+encodeURIComponent(document.location.href);

As you can see, we’re launching a Pythonista script called “TweetbotDrafts”, telling the app to run it with two arguments: the webpage’s title and URL. The title isn’t necessary – it is currently turned off in the script – but I included it so you can include it in your workflow if you want (say, to send both a webpage’s title and URL in a message).1

Chrome

The second step is the Python script itself.

import re import sys import urllib import webbrowser import clipboard numArgs = len(sys.argv) if numArgs < 2: url = clipboard.get() else: text = sys.argv[1] url = sys.argv[2] link = re.sub("(?Phttps://.*twitter\\.com/)(?P.+)/(?P(status|statuses))/(?P.+)", "tweetbot://\g/status/\g", url) encoded = urllib.quote(link, safe='') drafts = 'drafts://x-callback-url/create?text=' + encoded + '&action=iMessageIt' webbrowser.open(drafts)

Line 7 checks how many arguments have been sent: if they’re two, they will be recognized by lines 14-15, if it’s only one, the script will assume it’s something you copied in the system clipboard.

We want to convert Twitter links to tweetbot:// ones, leaving “normal URLs” untouched so you can use the script with any webpage – not just the Twitter website. Using a regex by our Don Southard, we can replace specific portions of a string (the twitter.com URL) with parts of the Tweetbot URL scheme that we want to use. Basically, something like this:

https://twitter.com/viticci/status/304079202880212996

becomes this:

tweetbot://viticci/status/304079202880212996

…entirely automatically. When someone will receive that link, he/she can click it to launch that single tweet’s detail view in Tweetbot for Mac or iOS. If the link didn’t need to be converted, it means we launched the bookmarklet (and therefore the script) from a non-Twitter webpage, which won’t need any substitution.

Line 20 URL-encodes the link to prepare it for Drafts; line 22 constructs the Drafts URL, calling an “iMessageIt” action. Finally, line 24 launches Drafts, passing along the URL we just processed.

Drafts is the last step, and the one you can personalize the most. I am sending a link via iMessage, but you can create another Drafts URL action to upload the link to Dropbox or Evernote, send it to another app, or any other service that Drafts supports.

My custom action simply triggers Drafts’ built-in Message action and tells the app to open Tweetbot again after a message is sent. Make sure to call this action “iMessageIt” to make it work with the script above.

drafts://x-callback-url/create?text=[[draft]]&action=Message&x-success={{tweetbot://}}

With this action ready to go, Drafts will receive the link from Pythonista and bring up a Messages panel with the link already filled in. Unfortunately, right now you can’t populate the To: field with specific contacts – meaning: you’ll have to type addresses manually. I wish Greg Pierce will consider the option of letting users specify default recipients for the Messages action in some way.

To sum up, here’s what the script accomplishes: by leveraging my appreciation for Tweetbot’s easier opening of single tweets, it uses a bookmarklet to grab a webpage’s title /URL and send it to Pythonista. If the link that was sent is a twitter.com one, Pythonista will process it and turn it into a Tweetbot URL, because I like Tweetbot. Once converted, Pythonista will send the link to Drafts. In receiving the link, Drafts will open a Messages panel to send it to someone; once set, it will go back to Tweetbot, as if nothing happened.

One last tip: keep in mind that Tweetbot recently received the option to close a Google Chrome tab that it created, taking you back to the timeline. Right now, my workflow goes back to Tweetbot, but it doesn’t close the tab created in Chrome. If you don’t want to keep that extra tab in Chrome, simply replace the last URL of the Drafts action with googlechrome://and you’ll be able to go back to Tweetbot (while closing the tab) from there.


  1. For a more detailed look at Pythonista, see my original review

Pythonista 1.3 Brings Camera Roll and Notification Center Support, New Library View, And More

$
0
0

Ole Zorn’s Pythonista is one of my favorite, most-used iOS apps to date. Combining a Python interpreter with scripting capabilities that take advantage of iOS through native interface elements and features like URL schemes, Pythonista has completely reinvented my iOS workflow. With Pythonista, I can work from the iPad without wishing I had a Mac.

Back in November, I wrote an extensive review of Pythonista 1.2, providing some sample scripts and an in-depth look at the app and its functionalities. I concluded my review saying:

I believe that, going forward, Pythonista and other similar apps will show a new kind of “scripting” and task automation built around the core strenghts of iOS.

Pythonista 1.3, released today, adds a number of features aimed at making the app more “connected” with the underpinnings of iOS, enabling users to create more complex workflows that go beyond running scripts inside Pythonista. I was able to use Pythonista 1.3 for the past weeks, and I believe it’s a very solid update.

Notifications and Photos

The big new feature is support for Notification Center and the iOS Camera Roll: with the addition of the notification and photos modules, Pythonista can now set and cancel native notifications, access existing photos from the Camera Roll and even save new ones directly into it.

Notification Center integration means the app can now schedule notifications to be displayed when a script (or even the entire) app is no longer running. Notifications can be displayed with Pythonista’s icon but they can have a custom message, sound, or URL to launch when tapped. An obvious implementation for this is using notification banners to display success messages after the app has completed a script and launched a URL (say, “Image Uploaded” while opening an image’s URL in a web browser); or, you can use the “action URL” attached to a notification to launch a specific app or website upon tapping the item in Notification Center.

Notification Center support is a powerful addition that allows Pythonista to be more tightly integrated with iOS. Because notifications are scheduled by indicating a delay in seconds (a notification scheduled in 180 seconds will appear in 3 minutes), you can schedule several notifications at once and they will be triggered at different time intervals while always being displayed in Notification Center.

Below, a quick proof of concept to show how you can schedule a notification that will launch Day One in x minutes, adding a new entry with text previously entered in Pythonista. The script uses the new notification module to schedule a notification containing a URL to launch Day One in creation mode.

import notification
import webbrowser
import console
import urllib
 
when = int(console.input_alert('Schedule', 'New Day One entry how many minutes from now?'))
 
minutes = when * (60)
 
text = console.input_alert('Day One', 'Write your entry below:')
 
entry = urllib.quote(text, safe='')
 
dayone = 'dayone://post?entry=' + entry
 
scheduled = notification.schedule('Time to update your Journal!', minutes, 'default', dayone)
 
print "Your Day One entry has been scheduled"

Line 6 uses input_alert to request text that will be converted to an integer; as you can see, the request asks for “minutes”, so I can later multiply for 60 seconds to transform the value entered in minutes in a delay that Pythonista can parse. Ideally, I would like to specifically request a numeric pad instead of a regular keyboard (which would be a nice addition to always return integers through input alerts) or add a check to see if the value that has been entered is, in fact, a numeric value.

Line 10 requests text to add to Day One, and line 12 URL-encodes it. Line 14 constructs a Day One URL that incudes our previously entered text, and line 16 schedules a notification that has a custom message, the delay value expressed in seconds, a default alert sound, and the Day One URL we want to launch.[1] Once triggered, tapping the notification will quickly fire up Pythonista and then immediately launch Day One with our entry ready to be saved.

This is just a quick proof of concept, but it gives you an idea of the possibilities opened by Notification Center integration with action URLs.

The photos integration is equally powerful and, especially for my workflow, useful on a daily basis. Building upon the possibility of reading images from the iOS clipboard and handle them with the Python Imaging Library, Pythonista can now request access to the Camera Roll to get and save images – while still treating them as PIL objects in a script.[2]

Using capture_image Pythonista can show a standard camera interface to take a new photo directly into the app and pass it along as a PIL object; you can get specific images (or thumbnails) using an index parameter, or – my favorite – you can show a native iOS photo picker to manually select images from the Camera Roll. You can only select single images; while I’d like to see Photos.app-like multiple selections, I’m pretty sure that can be done with repeat blocks in a script. Ping me if you create scripts to handle multiple images with the photo picker.

Here’s another super-simple demonstraton of how you can leverage the photos module to make working with the Camera Roll seamless inside Pythonista.

import photos
import Image
 
choose = photos.pick_image()
 
final = choose.resize((800,600),Image.ANTIALIAS)
 
saveit = photos.save_image(final)
 
if saveit is True:
	print 'Resized image has been saved'
elif saveit is False:
	print "Uh oh, not saved"

The script above simply requests an image with the photo picker, and uses Image to resize with an anti-alias filter, then save the same PIL object to the Camera Roll. If the image is successfully saved, a success message is printed in the console.[3]

Other Additions

Aside from the big additions, there are many other improvements in Pythonista 1.3 that are worth mentioning. In version 1.2, I lamented how the Library View could get out of hand with several scripts; in Pythonista 1.3, there’s a sidebar to view files as thumbnails or a list, sort them by name or modification date, and, more importantly, create folders and move scripts into them. This is a very welcome change that has allowed me to group scripts by purpose or app.[4]

The app can now explicitly launch Safari by prefixing standard http:// URLs with ‘safari-’: using ‘safari-http://apple.com’ in a script will launch Apple’s website directly in Safari rather than the built-in browser.[5] In the console, you can now create tappable links by using console.write_link().

Alongside other modules and tweaks, I want to point out the addition of the markdown2 module, which, unlike the previously supported markdown, has better support for extras like footnotes and header IDs (both of which I use extensively for MacStories).

Two More Scripts

As one last extra, I have modified two of my existing scripts to demonstrate the new features of Pythonista 1.3. For a more comprehensive overview of the scripts I use, you can refer to my Pythonista review and tag on the site.

The first script shows a camera interface to take a photo, upload it to my Dropbox Public folder, and open its public URL in Safari. It uses the Dropbox login methods and keychain access I described in November.

import photos
from dropboxlogin import get_client
dropbox_client = get_client()
import keychain
import console
import time
import httplib
from io import BytesIO
import datetime
import webbrowser
import urllib
import clipboard
 
today = datetime.datetime.now()
 
console.clear()
 
img = photos.capture_image()
titles = console.input_alert('Image Upload', 'Enter your image name below')
console.show_activity()
buffer = BytesIO()
img.save(buffer, 'JPEG')
buffer.seek(0)
imgname = today.strftime("%Y-%m-%d-at-%H-%M-%S") + '-' + titles + '.jpeg'
response = dropbox_client.put_file('/Public/' + imgname, buffer)
 
console.hide_activity()
 
encoded = urllib.quote(imgname, safe='')
 
final = 'http://dl.dropbox.com/u/YOURID/' + encoded
print "uploaded:", response, '\n'
console.write_link( titles, final)
 
clipboard.set(final)
 
browser = 'safari-' + final
 
webbrowser.open(browser)

Line 18 takes a new photo, and line 19 requests a title with an input alert. Line 22 saves the image in a buffer, and line 24 creates the name of the photo by using a strftime-based timestamp generated by the datetime module; line 25 uploads the photo (with the specified name) to my Dropbox Public folder.

Line 29 generates a URL-encoded version of the filename and line 31 combines that with a Dropbox URL of my Public folder. Note that this is done by using the old way of “guessing” public links by using your account’s ID: Dropbox appears to prefer the new “shareable” links now, but this trick still works (and looks cleaner to me, conceptually).

Line 32 prints the Dropbox response in the console and line 33 writes a link to the image.[6] Line 35 puts the same link in the clipboard, line 37 prepends ‘safari-’ to the link, and, last, line 39 launches the URL in your web browser.

The script gives you an idea of the power of Pythonista 1.3 with native Camera Roll access and better Safari integration. It’s highly customizable: you can change the format of the filename, remove the webbrowser module and just tap on the link written in the console (I left both), or perhaps launch another app and attach a custom message/URL to a notification.

The second script is a variation of the new Markdown conversion script already included by default in Pythonista 1.3. It allows you to preview Markdown-formatted text in a custom HTML page, or send it to Poster to publish it on your WordPress blog. I made it available in a GitHub Gist in order to avoid formatting issues with the HTML of this page.

Lines 10–46 set a custom HTML page to use for the preview of Markdown text. Line 49 gets Markdown text from the iOS clipboard and line 50 uses markdown2 with footnote and header IDs extras to convert it to valid HTML.

Lines 52–68 provide the two choices I want to have in this script: preview the HTML in Pythonista, or send it to Poster. The first block takes a look at an alert that was displayed on line 47 asking if I wanted to preview the text inside Pythonista or forward it to Poster. If the answer was “1” (Pythonista), the block uses the code provided by Ole to create a custom HTML file and view it using a file:// URL.

If the answer to the alert was “2”, the script asks for a title to use in Poster, then encodes everything in UTF–8 again and sends URL-encoded text and title to Poster’s creation screen through the webbrowser module and the Poster URL scheme. In this way, I converted Multi-Markdown text with footnotes and header IDs to HTML that has been sent to Poster in roughly 4 seconds.

Pythonista 1.3

Pythonista 1.3 is a great update that, once again, positions the app as a unique solution to write Python scripts that have direct access to several iOS features and UI elements. Like I said in my original review, Pythonista is not for everyone, but those willing to try it out will find an even more solid Python environment with native integration on the platform it runs on.

Pythonista 1.3 is available on the App Store.


  1. Note that 'default' isn’t really the name of a sound effect included in Pythonista; entering any name that is not recognized by the app will result in playing the default iOS alert sound. If you want, you can use 'ticci' in the sound’s name.  ↩
  2. In most cases: fetching an image’s EXIF metadata won’t obviously return a PIL object, but a dictionary instead.  ↩
  3. I use the script above to resize landscape iPad screenshots to an absolute value of 800 x 600 (the size I use for MacStories). You can make image resizing smarter by resizing proportionally like my friend Gabe does.  ↩
  4. Make sure to properly call modules if you move scripts that use them inside a sub-folder.  ↩
  5. The same can be done with https:// URLs.  ↩
  6. Line 33 is where you’ll want to add a check to see if the upload was successful. I just didn’t have time for it.  ↩

Create and Share Evernote Notes With Pythonista On iOS

$
0
0

I use Evernote on a daily basis, but there’s no easy and quick way to create new notes and receive their shared URLs on iOS. While I tend to prefer plain text files, Evernote notes are quite useful when I need to share rich text (containing formatting and inline images) with someone else. Sharing via the official Evernote app takes too long[1], and I don’t like the UI of other Evernote clients.

Yesterday, Pythonista developer Ole Zorn posted an installer script for the Python Evernote SDK. By putting together all the necessary dependencies, he created an installer script that will create an “evernote-sdk” sub-folder in Pythonista 1.3; with that, you’ll be able to access the entire Evernote API to create and manage notes – all while taking advantage of the uniqe iOS-related features of Pythonista.

Inspired by Ole’s demoes and the snippets posted by Brett Kelly in the past weeks, I created a script that does exactly what I need: it lets me enter text to save it in an Evernote note that will be shared publicly. If triggered by an app like Drafts or Launch Center Pro, the script will take the text sent by those apps. If formatted in Markdown, the text will be converted to HTML before saving it to Evernote.

Requirements

Install the Evernote SDK inside Pythonista using Ole’s installer.

To use the script, you’ll first need an Evernote account with an assigned developer token. You can generate a new token here; don’t ever share your private token with anyone else, as it’ll grant anyone access to your Evernote account.

To use the token in the script, rather than pasting it as a text string I recommend using the Pythonista keychain.

Obviously, I compiled the script for Pythonista 1.3, which was released last week.

What It Does

The script consider two possible scenarios: text sent from external apps like Drafts or Launch Center Pro, or text entered from Pythonista itself.

If you didn’t invoke the script from an external app, the script will ask you to enter a note title and body. Using your Evernote developer token, it will create a new note in your default notebook, share it, returning the public URL of the note. The script will display the link in the console as a tappable URL, and it’ll also place it in the iOS clipboard.

If the script, however, was triggered by another app, it will count the arguments passed in the URL scheme. If you like to write in Drafts and then launch the script, it’ll treat the first line as the note’s title, and the rest as the body. If you prefer Launch Center Pro, it will use the first keyboard prompt as title, the second as body.

In both cases, Markdown will be converted to HTML using markdown2 with support for footnotes and header IDs. On my iPad mini, a 1000-word, Markdown-formatted note takes around 15 seconds to be processed, saved, and shared in Evernote.

The Script

You can find the full script on GitHub.

This time, I tried to comment the script inline as much as possible. Lines 21–22 construct the first portion of the Evernote public URL using googlechromes:// – the Google Chrome URL scheme for “https://” links. If you want, you can use safari-https:// to open the link in Safari instead.

Lines 29–35 take care of understanding whether the note title and text should be entered in Pythonista or extracted from another app, whereas lines 41–42 convert the body of the note from Markdown to HTML.

The actual note is created and saved in lines 46–72 using Evernote’s own markup language called ENML; lines 73–87 share the note publicly using a note’s unique identifier and generating a “share key”; these two values will complete the final URL, which is printed as a tappable link in the console, and copied to the clipboard.

Below, a video of the script in action. I used Drafts to create a new note and share it through Evernote.

Once the script is done, you’ll be able to view the shared note in the browser or share the links with your friends, who will see the note you just created on evernote.com.

From Other Apps

Thanks to the Pythonista URL scheme, we can launch the script from other apps, passing along text directly in the URL scheme.

As I showed in the video above, you can launch the script from Drafts using this action (it assumes the script is called EvernoteTextShare):

pythonista://EvernoteTextShare?action=run&argv=[[title]]&argv=[[body]]

Or, if you prefer Launch Center Pro, you can create an action like this and use the keyboard prompt to enter title and body:

pythonista://EvernoteTextShare?action=run&argv=[prompt]&argv=[prompt]

Overall, the script is very flexible and requires minimal management after it has been set up for the first time. You can enable x-callback-url to be taken out of Pythonista automatically once the note is uploaded. You can remove the Markdown conversion, or even open another app like, say, a Twitter client to immediately tweet the link to the shared note.

If you have suggestions for improvements (for instance, I didn’t add any code to check for Evernote errors), ping me on Twitter.


  1. I just want to paste/enter some text and immediately receive the public Evernote URL of a shared note.  ↩

Automating iOS: How Pythonista Changed My Workflow

$
0
0

A couple of months ago, I decided to start learning Python.

I say “start” because, as a hobby to fit in between my personal schedule and work for the site, learning the language is still very much a work in progress. I hope I’ll get to an acceptable level of knowledge someday. Coming from AppleScript, another language I started researching and playing with earlier this year, the great thing about Python is that it’s surprisingly easy to pick up and understand. As someone whose job primarily consists of writing, I set out to find how Python could improve my workflow based on text and Markdown; I found out – and I’m still finding out – that Python allows for more flexible and intelligent string manipulation[1] and that some very smart folks have created excellent formatting tools for Markdown writers.

But this article isn’t strictly about Python. Soon after I took my decision to (slowly) learn my way around it, I asked my friend Gabe Weatherhead about possible options to write and execute Python scripts on iOS. Thanks to Gabe’s recommendation I installed Pythonista, and this app has completely changed my iOS workflow.

The iOS Workflow

A bit of backstory first.

The fact that I immediately wondered whether it’d be possible to “do Python” on iOS shouldn’t be a surprise. For the past 12 months, due to changes in my personal schedule, I’ve been forced to rethink my workflow entirely. With a focus on plain text and Markdown, I set out to find ways to ensure I would be able to get work done on iOS devices without limitations. I wanted to get the “I need a Mac for this” daily problem out of the equation; I wanted to be able to do the same work on all my devices. I wanted to automate tedious tasks I was doing every day; ultimately, I consolidated my workflow around devices and software I trust to get work done for me. I wanted the best for me.

I got a 3G iPad with Retina display, so I could put less strain on my eyesight and work from anywhere independently of availability of WiFi hotspots; I got a 32 GB model so I could comfortably cache music and podcast episodes on it. I upgraded my iPhone 4S to an iPhone 5, as I knew the improved cellular options and new antenna would let me get a signal in places – such as hospital corridors – where I knew I wouldn’t be able to work with “regular” 3G.[2]

I didn’t get the best options available for iOS devices: I bought the models that I knew wouldn’t let me down in case of necessity.

From a software standpoint, I got rid of apps that I wasn’t using regularly or that hadn’t been updated in a long time. If I were to reconstruct my workflow – and therefore, indirectly, my income – on a solid software foundation, I needed to do so with developers I could trust. I picked my weapons carefully: I realized plain text was my preferred way of writing and Dropbox my favorite cloud filesystem, so I combined them. It also helped that the Markdown/plain text community is fervent, passionate, and active. I chose iCloud to sync my personal information and settings for apps that support it, such as Downcast or Due. I understood that Evernote wasn’t meant to be a Markdown editor, so I started using it for what it’s really great at: collecting rich notes with images and other reference material. In trying new apps and services, I carefully selected the ones that came with native OS X and iOS apps, possibly with sync; those who follow me on Twitter know that I strive to find services that are ubiquitous and reliable, even when people tell me “you don’t need that on the iPad”.

I need my iOS devices (and especially my iPad) to allow me to get work done from anywhere.

Curating apps that enhance my workflow and finding software I can trust with my data isn’t simply my job as a “reviewer”: it’s an investment for the future of my business. The fact that I take pleasure out of writing about software is a different matter.

I was all set up: I had my plain text workflow with Dropbox, everything else in Evernote, my podcasts and music synced and cached. I had a few games, Twitter clients with sync, communication with my team happening through iMessage. I’m surprised I didn’t think of hooking up my car with the cloud, honestly. [3]

The experiment failed miserably. I started taking the iPad with me to write and publish posts for MacStories, but I was constantly reaching out to my MacBook Air in the other bag. So I stopped carrying the MacBook altogether: too bad one day I had to post a piece of news quickly, and got stuck on creating and uploading the image I needed for the article.

I spent entire weeks thinking about how to solve the issue. Eventually, I came to the realization that I didn’t need more apps, I needed a system to automate iOS. I was missing the little scripts and shortcuts and macros that, for power users, make OS X computers powerful machines to get work done quickly and efficiently. I was missing my automation setup: my Keyboard Maestro, my Alfred, my Hazel. Little things quickly add up: Markdown links take longer to put together[4] and screenshots don’t come out like I want them to. Losing all my scripts and macros in the transition to iOS meant I could only do some things, and not as fast as I would on a Mac. I needed to fix it.

I turned to my friend Gabe. Over at Macdrifter, Gabe writes about “Mac and iOS related material with a slant towards the technical”. Macdrifter is, by far, my favorite tech/indie blog of 2012: Gabe doesn’t write rumors or linkbait; he links to stuff he finds interesting and only reviews software he actually uses. Even better, he comes up with tips and workflows that are very specific (the so-called “niche”) but also incredibly useful.

Initially, Gabe introduced me to Nebulous Notes, a Markdown text editor that works with Dropbox and has support for macros. It has become my default text editor after a couple of weeks of experiments. This is the result of fine-tuning the app to my own needs.

Later, I pinged Gabe about my interest in Python, and he suggested I’d take a look at Pythonista, developed by Ole Zorn. I’m glad I did: Pythonista has profoundly changed the way I approach iOS devices when I know that work needs to be done. I’m not afraid to leave my MacBook at home; in fact, several of the posts published on MacStories in the past month have been produced entirely from an iPad.

Let me show you how I achieved iOS workflow nirvana.

Writing Workflow

I’ve previously written about my writing workflow; you can, however, find more recent additions here and here. As far as iOS is concerned, the biggest change was Nebulous Notes, which, like I said, altered my perception of iOS text editors thanks to macros. If I need to write a blog post on the iPad or iPhone, I can’t use any other app.

I have very specific needs when it comes to “work”. In an unordered list of importance:

  • I need to publish blog posts to WordPress.
  • I need to generate valid HTML for the Markdown I write my posts with.
  • I want to visually preview the Markdown text to make sure the layout of the post is right.
  • I need to upload images to our CDN.
  • I need to convert images to another format and change their output quality.
  • I need to upload images to Dropbox quickly.
  • For articles that include iPhone screenshots, I want those screenshots to look like this.
  • Once I have the link to an image, I need to generate the proper img HTML for MacStories.
  • Occasionally, I may have to download files.
  • I generally create reminders for things I have to do in OmniFocus or Due.
  • I bookmark links I find interesting with Pinboard.

On the Mac, these tasks are made simple by Sublime Text 2 and the Finder. As I’ve previously outlined, Sublime Text can be extended with some fantastic Markdown-related plugins; with the Finder, I can easily upload images from the Desktop to our CDN, I can access any Dropbox file thanks to the Dropbox app, and, when it comes to quick image modifications, I’ve come to trust Acorn and Keyboard Maestro to do the heavy work for me.

Pythonista

Pythonista is a Universal app for writing and executing Python scripts on iOS. There are some differences in terms of navigation and interface between the iPhone and iPad versions, but, overall, Pythonista is consistent across both platforms.

Like any respectable code editor, Pythonista comes with features like syntax highlighting and code auto-completion. For highlighting, there are six color themes to choose from in the Settings, including two Solarized options (dark and light); in the Settings, you can also change the editor font (Adobe’s new Source Code Pro is available), font size, line spacing, and tab width.[5]

Code completion is my favorite feature of Pythonista’s editor. For someone like me who’s just getting started with Python, the app offers an unobtrusive yet highly convenient way to auto-complete your code: suggestions appear directly above the keyboard and they’re colored according to your color scheme. When typing, you can tap on the “auto-complete bubble” to let Pythonista complete your code; code completion is also smart in that only functions/class names/etc related to a module you’ve imported will be suggested.

Code completion can be deactivated in the Settings alongside other options enabled by default. You can set Pythonista to highlight matching (), which is a nice feature to have as it provides a subtle hint to confirm you’ve matched parentheses in the right way. But what I really like is auto-pairing: characters like parentheses, square brackets, and single/double quotes will be matched with closing characters automatically. If you’re coming from nvALT or Sublime Text 2, you should be familiar with this option. For instance, if you beging typing ( a closing ) character will be immediately put on the right, with the cursor in the middle ready to type. Or, if you select text and hit ( the entire text will be wrapped inside ( ).

Character pairing can be confusing to some people, but I find it extremely handy. It speeds up my typing considerably.

Typing in Pythonista, however, is also aided by the extra row available above the standard iOS keyboard. First introduced by iA Writer for iPad, Pythonista follows the trend of several Markdown editors and puts often-used characters directly above the main keyboard, so you won’t have to open the numeric or math keyboard when writing code. These characters include the usual suspects (quotes, parentheses, tab, etc), but with a spin: on the iPhone, due to the smaller screen, the extra row has been “split” in two modes that you can switch with a button on the right. The second mode is where you’ll find the app’s Undo function.

The extra keyboard row has two more tricks up its sleeves. First, most keys can be long-pressed to reveal additional keys in a popup: Undo becomes Undo/Redo and single quotes become single/double quotes and percent sign.

Additionally, Pythonista uses the extra row as a swipe area for the “Hooper Selection” made popular by a concept video earlier this year. Swipe selection only works with one finger (and you can’t move up/down through empty lines), but it’s still a nice touch to make editing quicker and more touch-oriented.

With a combination of code completion, character auto-pairing, extra keyboard row, and swipe selection, I find writing code in Pythonista intuitive and accessible. The fact that everything’s entirely touch-based gives the code editor a feeling of “manipulation” that I haven’t seen in desktop editors.[6]

[Update]: There’s a search field in the code editor; you can also navigate your code’s structure from a document browser available in the top title bar.

Pythonista lets you organize scripts in the Script Library. There are two views to choose from: a “snippet view”, which lets you see scripts as thumbnails with a preview of the first lines of code, and a more traditional list view. Both views can be sorted by name or modified date; you can add scripts with a + button at the top and delete existing ones by tapping on the Edit button.

A downside of Pythonista is that the Script Library doesn’t offer further organizational features to better sort your scripts. You can’t create folders, and it gets pretty confusing in snippet view; for the time being, I’m using list view, but it takes time to find scripts. I would definitely welcome an option to organize scripts by category or purpose.

Due to restrictions imposed by Apple, Pythonista can’t have a sync feature to import executable code from external sources like Dropbox. You can export, but you can’t import – not even from iTunes.[7]

There is, of course, a workaround. Developer Ole Zorn came up with a way to create new scripts off GitHub Gists: make sure you have a Gist URL in your clipboard, run the script, and you’ll have a new Python script in your Library. Alternatively, the Pythonista Community Forums are already filling up with ideas to simply click a bookmarklet to make the Gist importing process even easier. Overall, there’s no doubt Pythonista could use easier importing options, but unfortunately that is not allowed by Apple. Maybe someone will create a script to import a public .py file stored on Dropbox.

Personally, keeping scripts in sync between the two versions of Pythonista and my Mac (therefore Dropbox) is what makes me waste the most time in the app. There’s no way around the fact that you’ll need to organize files manually.

For someone who’s getting started with Python, Pythonista’s best feature is the in-app documentation. Available in a Help menu from the Script Library or code editor, it is based on the official Python 2.7 documentation and it has been reformatted for the iPad and Pythonista. The documentation browser has a Home button to go back to the initial screen, arrows to go back/forward, and a search box. There’s also a button to open a page’s Table of Contents and jump to a section directly, though this is only available on the iPad.

The documentation comes with a full Language Tutorial, Library Reference, Linguage Reference, Global Module Index, General Index, and Glossary. The Tutorial and Reference guides are fantastic tools to start learning Python right inside the app. Ole enhanced code snippets to include “Copy” and “Open in Editor” buttons that will let you easily copy code samples and play around with them in the editor.

The documentation is directly integrated into the code editor. Simply select any class, module, or Python term, hit Help, and a popover (on the iPad) will show the relevant documentation entries in an inline Quick Help menu. Tap, and the Quick Help will jump to the result, highlighting it. On the iPhone, Quick Help is shown in a standard view overlaying the code editor; on the iPad, you can expand the Quick Help popover to a full window with one tap.

Don’t underestimate the convenience of readily-accessible inline documentation: especially on the iPad, and again, especially when you’re not a master of Python, it’s incredibly handy to be able to quickly check out proper module names and syntax.[8]

Pythonista’s console deserves a mention too. Available with a single swipe to the left from the editor (grab the handle on the right side and pull), the console is where ouput gets printed and results are displayed. There’s an interactive prompt (with command history) to try out Python scripts quickly, and the same text field is also used for raw_input when you need to enter text manually in the console.

Pythonista and iOS

Pythonista supports several modules of the standard Python library, but, due to limitations of iOS, it can’t support everything a desktop Python installation can. Aside from importing external modules and libraries – something anyone can do with Python on a computer – Ole had to come up with specific and clever ways to let Pythonista access data outside of its confined app sandbox.

For example, you won’t be able to programmatically read files from the filesystem on iOS: there’s no “Desktop” or “Documents” folder to read from on iOS. For the same reason, you won’t be able to “save” files to specified locations on your local filesystem, as Pythonista can’t break open the sandbox and, say, process a file for GoodReader.

However, not all hope is lost. If you think about it, there is one layer of data that is constantly shared across iOS apps: the system clipboard. Text and images we copy on iOS are stored in the clipboard (also known as “pasteboard”), and they are available at a system-wide level, albeit it’s up to the single developer to determine which kind of content can be pasted or copied – e.g. you can’t paste a photo into Tweetbot’s compose box.

Pythonista can read from and write to the system clipboard. The clipboard module, in fact, has single-handedly reinvented my usage of iOS in combination with Pythonista and third-party apps. I am not dramatizing this: as I’ll explain later in this article, the possibility to read and set text strings and photos through the iOS clipboard has proven to be a simple, yet fantastic way to automate several areas of my iOS workflow. As I’ll detail, x-callback-url has also played an important role.

To overcome the limitations of iOS, Ole Zorn has tried to access every area of the system he could configure with Pythonista. The aforementioned clipboard module can get and set text and images; there’s a canvas module to display vector graphics, and a scene module to draw 2D graphics and animations. There are actual, playable games developed with these modules in the built-in examples[9] and I’ve seen users already experimenting with their own takes on graphical representations of Pythonista’s library.

There are more Pythonista modules that I’ve used in my scripts. The console module, in particular, is one I use on a daily basis: aside from controlling text output (it can clear, set font, and set colors), console can be used directly in the editor as well. Coming from AppleScript, console.alert and console.input_alert are reminiscent of display dialog and display alert in that they use native iOS alerts with buttons or text fields to enter text manually. The console module also allows for secure input, login alerts, and, my favorite, a show_activity command to show the standard iOS network activity indicator in the status bar. I have employed this in a variety of scripts that fetch web data.

The keychain module is another powerful addition. It is secure password storage that lets you store passwords within Pythonista to reuse in other scripts. You can set passwords with keychain.set_password and retrieve them by importing keychain in a script and using get_password. This is my preferred way to save passwords internally within the app without writing them as strings in a script.

The sound module shouldn’t be underestimated: it allows scripts to play simple sound effects such as various version of “ding” or 8-bit inspired Arcade “jump” and “powerup”. I like how sound adds a new layer of presentation to scripts, which are otherwise primarily text based.[10]

There are some modules that aren’t part of the Python Standard Library that are included in Pythonista. I’m using some of them in my workflow.

Last, the editor module. I don’t use this in any of my scripts, but I believe it could pave the way for an evolution of the Pythonista engine into something different.

Here’s why: editor gives you access to the text of the script you’re currently editing. You can get the current selection, get selected text, replace it, or set a new selection. The syntax is simple and it allows, through a couple of lines, to do things like select & replace – with all the combined functionalities of other Python modules. If this doesn’t yet ring a bell to you, think about this: plugins for Sublime Text 2 are written with the Sublime API and are based on Python. As it stands now, editor is a great way to automate text selections and replacements in the code editor; I can’t wait to see if this will evolve in something bigger.

The Actions Menu

The potential of the editor module can be grasped by playing around with the Actions menu of Pythonista. Configurable in the Settings, Actions are shortcuts to scripts from Pythonista’s Library; instead of switching back and forth between the editor and the library for workflows that require multiple scripts, you can just bring up the Actions popover and run a script from there.

If you’ve created scripts that rely on the editor module for text manipulation, it makes sense to add them to the Actions menu for easier access; I, however, have found another advantage of showing my scripts in the Actions menu – the clipboard. To concatenate scripts together, I can just make sure they write their results (usually images or text) to the system clipboard; in this way, I’ll be able to start another script from the Actions menu, as I know it will get data from the clipboard I’ve just set. Ideally, I’d like to see a future version of Pythonista with support for “workflows” – sets of “rules” that determine how data (the clipboard) should be processed by groups of separate scripts, in which order.

The URL Scheme

For the joy of URL scheme aficionados, Pythonista comes with a URL scheme. You can launch the app itself with pythonista://, but the good stuff lies in the parameters you can pass along with the URL. Firstly, you can open a specific script using something like pythonista://MyScript – but even better, you can open and run a script by using pythonista://MyScript?action=run in the URL. To use this, a script with that exact name will have to be in your Library, and no other script will have to be running upon calling action=run.

The really good stuff is what you get by combining the URL scheme with command line arguments. As documented by Ole, you can pass one argument at a time by using &args= or multiple ones with &argv= in the URL. This is best explained with an example: because of the URL scheme, you can create JavaScript bookmarklets that launch Pythonista and pass command line arguments in the URL.

Let’s say I want to pass a webpage’s URL and title as two separate arguments to a Pythonista script. I can use the following line of JavaScript as a bookmarklet in Safari:

javascript:window.location='pythonista://Pinboard?action=run&argv='+encodeURIComponent(document.title)+'&argv='+encodeURIComponent(document.location.href);

The bookmarklet will launch my Pinboard script (more on this later) with argv[1] being the webpage title, and argv[2] as the URL.

Unfortunately, there’s no Launch Center Pro for iPad yet, but once you start thinking about the possible implementations of Pythonista’s URL scheme with web data from Safari, you’ll see how your workflow can benefit from this kind of automation.[11]

Home Screen Shortcuts

Unsurprisingly, Ole thought of his own way to let users enjoy the convenience of the URL scheme without waiting for a decent launch manager to arrive on the iPad.[12]

You can create Home screen bookmarks for Pythonista scripts. Simply head over this webpage created by the developer, enter your script’s name (it’s case-sensitive), hit Create Shortcut, then add the page to your Home screen using Safari’s Add Bookmark menu. The webclip will have a nice Pythonista-themed icon.

Upon tapping, a Pythonista Home screen bookmark will briefly open a blank page and then immediately redirect to the script you’ve configured in the Pythonista app. I’m fairly certain there’s no way to avoid showing a blank page for a second before redirecting to Pythonista; fortunately, it’s really just the fraction of a second, as the redirecting process is instantaneous both on my iPad 3 and iPhone 5.

I use bookmarklets for URLs I need to send to Pythonista from Safari (or iCab), but I prefer Home screen icons for data that’s been copied to the clipboard.

My Scripts

The Pythonista scripts I use on a daily basis are primarily oriented towards speeding up my Markdown workflow. Thanks to input and directions from Ole, I’ve put together a collection of scripts that allow me to get from raw Markdown text to final HTML (with images) ready for publishing in just a matter of seconds, rather than several minutes. I’ve also created scripts that rely on URL schemes to make the iOS apps I use communicate better, automatically.

Markdown to Poster

My most-used script is actually just a combination of simple Python and x-callback-url. It sends the contents of the clipboard to Notesy, which renders the Markdown and passes it to Poster – an app I use to publish posts on MacStories – as HTML. It takes less than 3 seconds to complete and return a new HTML post in Poster.

import webbrowser
import urllib
import clipboard


base = 'notesy://x-callback-url/render-markdown?text='
url = clipboard.get()
text = url.encode('utf-8')
text = urllib.quote(text, safe='')
poster = urllib.quote("posterapp://x-callback-url/create?text=", safe='')
actions = '&output-param=text&x-success=' + poster + '&x-error=pythonista://'
webbrowser.open(base + text + actions)

Not nearly as adopted as it should be, x-callback-url is a standardized protocol for iOS inter-app communication that enhances URL schemes with parameters apps can send/receive to trigger specific actions. In the words of Greg Pierce, its developer:

The goal of the x-callback-url specification is to provide a standardized means for iOS developers to expose and document the methods they make available to other apps. Using x-callback-url’s source apps can launch other apps passing data and context information, and also provide parameters instructing the target app to return data and control back to the source app after executing an action.

In the protocol’s draft specification, Greg details the structure of x-callback-url and how developers can easily take advantage of it by supporting parameters for source and target apps.

My script does indeed take advantage of the x-success parameter provided by x-callback-url. Most iOS apps that use URL schemes have implemented them in a one-way communication method: launch this URL to open this app. Developers who have spent time structuring their URL schemes often come up with richer actions: launch this URL to open this app in a specific view. Developers who rely on x-callback-url and respect the specification can get access to extra parameters such as source, success, error, and cancel. These enable powerful if/then clauses in URL schemes: if the action in the target app fails, go back to source app.

I write and edit in Nebulous, but I publish articles using Poster. As I was researching options to automate my workflow, I stumbled across the Notesy URL scheme. It seemed too good to be true: it supports x-callback-url and comes with an action to render Markdown and pass it to another app or the system clipboard. Essentially, given properly encoded text, Notesy can act as a “bridge” between apps to render Markdown to HTML – without forcing you to tap any buttons.

Why Notesy instead of the built-in Markdown module, though? First, Notesy uses the Sundown Markdown library, the same implemented by the best Markdown editors for iOS. More importantly, Notesy can run SmartyPants upon generating Markdown, which I find particularly useful for my frequent use of en dash on MacStories. Last, Notesy is really fast at rendering Markdown and I like to support an app that properly and cleverly utilizes x-callback-url.

The script itself is very straightforward: base is the Notesy URL scheme we’ll use to send text; url gets the text from the clipboard, encodes it in UTF–8 and replaces special characters using percent-encoding with urllib.quote.

The second part of the script prepares the URL that Notesy will open if it succeeds in rendering Markdown. We’re telling Notesy: if you successfully render Markdown, then open Poster with the HTML as article text. There are two things to keep in mind here: to pass along text with render-markdown in Notesy, use output-param. As you can see, I’m using text to encode text and send it as the result of Notesy’s rendering. Second, when an app launched via URL scheme (such as Notesy) has to open another URL scheme (in our case, Poster’s one), always percent-encode the second URL.

In the last line, the webbrowser module of Pythonista simply launches the complete URL and executes the set of actions described above. The nice thing about webbrowser is that it’ll open standard http:// links within Pythonista, but it can be used to launch other URL schemes without any confirmation dialog.

To show how, in practice, the script takes seconds to complete, I’ve made a video.

- Download

Convert Markdown

If, for some reason, I don’t want to convert Markdown using Notesy, I can rely on Python-Markdown to do the conversion for me. Available in Pythonista 1.2, Python-Markdown expects and returns Unicode input.

import markdown
import clipboard

input_file = clipboard.get()

s = input_file

md = markdown.Markdown()
html = md.convert(s)

print html
clipboard.set(html)

The script simply expects Markdown text to be in the clipboard, converts it to HTML using Python-Markdown, and sets the clipboard to the newly generated HTML. I find it useful for quick Markdown to HTML conversions.

- Download

Markdown to Byword

I’ve already explained why I believe Byword has the best MultiMarkdown previews on iOS. In spite of the lack of true MultiMarkdown converting in Pythonista, I’ve still put together a modified version of the script above to open the preview in Byword via the app’s URL scheme.

import webbrowser
import markdown
import clipboard
import urllib

input_file = clipboard.get()

s = input_file

md = markdown.Markdown()
html = md.convert(s)

clipboard.set(html)

s = clipboard.get()
s = urllib.quote(s.encode('utf-8'))

webbrowser.open('byword://new?text=' + s)

I hope Metaclassy will consider adding a more powerful URL scheme to Byword.

- Download

formd

Thanks to Ole, I’m using a simple adaptation of Seth Brown’s formd Markdown formatting tool to flip the style of Markdown links from inline to reference and viceversa. As Seth explains, “inline Markdown is difficult to read, but useful for writing and editing because the linked text and URLs are adjacent to the words you are writing”. I like to write with inline links, but I prefer to archive my articles as reference-style Markdown files.

#!/usr/bin/env python
# encoding=utf8
"""
Seth Brown
02-24-12
"""
from sys import stdin, stdout
import re
from collections import OrderedDict

class ForMd(object):
    """Format mardown text"""
    def __init__(self, text):
        super(ForMd, self).__init__()
        self.text = text
        self.match_links = re.compile(r'(\[[^^]*?\])\s?(\[.*?\]|\(.*?\))',
                re.DOTALL | re.MULTILINE)
        self.match_refs = re.compile(r'(?<=\n)\[[^^]*?\]:\s?.*')
        self.data = []

    def _links(self, ):
        """find Markdown links"""
        links = re.findall(self.match_links, self.text)
        for link in links:
            # remove newline breaks from urls spanning multi-lines
            parsed_link = [s.replace('\n','') for s in link]
            yield parsed_link

    def _refs(self):
        """find Markdown references"""
        refs = re.findall(self.match_refs, self.text)
        refs.sort()
        refs = OrderedDict(i.split(":", 1) for i in refs)
        return refs

    def _format(self):
        """process text"""
        links = (i for i in self._links())
        refs = self._refs()
        for n, link in enumerate(links):
            text, ref = link
            ref_num = ''.join(("[",str(n+1),"]: "))
            if ref in refs.keys():
                url = refs.get(ref).strip()
                formd_ref = ''.join((ref_num, url))
                formd_text = ''.join((text, ref_num))
                self.data.append([formd_text, formd_ref])
            elif text in refs.keys():
                url = refs.get(text).strip()
                formd_ref = ''.join((ref_num, url))
                formd_text = ''.join((text, ref_num))
                self.data.append([formd_text, formd_ref])
            elif ref not in refs.keys():
                parse_ref = ref.strip("()")
                formd_ref = ''.join((ref_num, parse_ref))
                formd_text = ''.join((text,ref_num))
                self.data.append([formd_text, formd_ref])

    def inline_md(self):
        """generate inline markdown """
        self._format()
        text_link = iter([''.join((_[0].split("][",1)[0], 
            "](", _[1].split(":",1)[1].strip(), ")")) for _ in self.data])
        formd_text = self.match_links.sub(lambda _: next(text_link), md)
        formd_md = self.match_refs.sub('', formd_text).strip()
        yield formd_md

    def ref_md(self):
        """generate referenced markdown"""
        self._format()
        ref_nums = iter([_[0].rstrip(" :") for _ in self.data])
        formd_text = self.match_links.sub(lambda _: next(ref_nums), md)
        formd_refs = self.match_refs.sub('', formd_text).strip()
        references = (i[1] for i in self.data)
        formd_md = '\n'.join((formd_refs, '\n', '\n'.join(i for i in references)))
        yield formd_md

    def flip(self):
        """convert markdown to the opposite style of the first text link"""
        first_match = re.search(self.match_links, self.text).group(0)
        if '(' and ')' in first_match:
            formd_md = self.ref_md()
        else:
            formd_md = self.inline_md()
        return formd_md

if __name__ == '__main__':
    import clipboard
    import console
    md = clipboard.get()
    if md:
    		console.clear()
        	text = ForMd(md)
        	[clipboard.set(t) for t in text.flip()]
      	 	final = clipboard.get()
        	print final

The text output is set back to the clipboard so I can pass it to another script or paste it in my editor of choice. In the future, I’d love to see Nebulous Notes being capable of launching URL schemes if only to trigger my Pythonista scripts directly from the app.

- Download

Screenshots

This is one of the Pythonista scripts I use the most: when I was beta-testing Pythonista 1.2 and Ole added support for images, I knew this would become the script that would allow me not to require a Mac for image processing anymore.

import clipboard
import Image
import console


im1 = clipboard.get_image(idx=0)
im2 = clipboard.get_image(idx=1)
background = Image.new('RGBA', (746,650), (255, 255, 255, 255))

def main():
		console.clear()
		print "Generating image..."
		console.show_activity()

		_1 = im1.resize((366,650),Image.ANTIALIAS)
		_2 = im2.resize((366,650),Image.ANTIALIAS)
		background.paste(_1,(0,0))
		background.paste(_2,(380,0))
		background.show()
		console.hide_activity()

		clipboard.set_image(background, format='jpeg', jpeg_quality=0.80)
		print "\n\n Image set to clipboard"
	
	
console.clear()
print "Create now or Control? \n"

print "[1] Create"

print "[2] Control \n"

set_mode = raw_input("Select a mode: ")

if set_mode == "x":

    print "Exited"

elif set_mode == "1":
	
		if __name__ == '__main__':
			main()

elif set_mode == "2":

		print "\n\n"
		
		print "Which image goes on the left? (in Photos.app order) \n"

		print "[1] The first image"

		print "[2] The second image \n"
	
		set_im = raw_input("Select an image: ")

		if set_im == "x":

				print "Exited"

		else:
                        
    			print "\n\n"
                
		if set_im == "1":
	        
			if __name__ == '__main__':
				main()
                
		elif set_im == "2":
			console.clear()
			print "Generating image..."
			console.show_activity()
	                
			_1 = im1.resize((366,650),Image.ANTIALIAS)
			_2 = im2.resize((366,650),Image.ANTIALIAS)
                        
			background.paste(_1,(380,0))
			background.paste(_2,(0,0))
			background.show()
			console.hide_activity()
		        
			clipboard.set_image((background), format='jpeg', jpeg_quality=0.80)
			
			print "\n\n Image set to clipboard"

Essentially, it is a Python version of my Keyboard Maestro macro for iPhone screenshots. Given two iPhone 5 screenshots, it creates a single composited image showing both screenshots side by side. Using text output in the console it enables me to control the placement of the screenshots: I can specify whether I want the first or second screenshot to be on the left side of the final image.

Line 6/7 grabs images from the iOS clipboard using clipboard.get_image. The get_image command is interesting for two reasons: first, as part of the clipboard module, it is able to read directly from the iOS clipboard – i.e. items you have copied using iOS’ standard “Copy” menu. Second, Ole structured it so that it returns a PIL image from an image in the clipboard. If you copy multiple images at once, you can use the idx parameter to get an image at a given index.

Here’s how idx works, and how I use it. To compose the screenshot, I need two images in the clipboard at once. To do so, I can copy multiple images from the Photos app: from either Camera Roll or Photo Stream, Edit > Select photos > Share > Copy, and you’ll have two images in the clipboard. The important thing to understand with idx and the Photos app is that Pythonista will grab images in “Photos app order” starting at 0.

So, for instance, here’s how idx would get the images shown below:

I don’t always need to, but there are times when I want to control how the screenshots are displayed on the final image – i.e. the second screenshot in Photos-order should actually be pasted first onto the image. I could have probably created something more elegant or simpler, but, in short, here’s how I did it: I defined a main function that pastes the first screenshot (idx=0) to the left side of the final image (coordinates 0,0). The function takes care of clearing the console, printing status messages, showing a spinner in the iOS status bar (it’s not network activity, but I like the visual hint), and doing the image processing; the final image is shown and also set to the clipboard. Through a series of text inputs, the script asks me if I want to create an image with default placement, or, if I want to control it myself. If I choose option 2 (control), it asks me if I want the first or second image to be on the left. In case I want the second image, it uses inverted coordinates for pasting.

Allow me to explain some parts more in detail. Once collected from the clipboard and turned over to PIL, screenshots need to be resized to fit inside the 746x650 image I want as final result. To do so, my main function uses resize (line 15/16) at 366x650 with ANTIALIAS, a high-quality downsampling filter provided by PIL. The process will be a little slower, but the image will be resized maintaining a higher quality.

Line 8 creates a 746x650 white background; the main function pastes the screenshots onto the background, offsetting the second one by 14 pixels so to leave a white strip in the middle. Line 19 shows the final image in Pythonista’s console.

When you show() an image in the console, you can tap on it to Copy it or Save it, just like any other image on iOS. However, you can also set the image back to the clipboard automatically, so it’ll be ready for pasting in, say, a Mail message. Line 22 uses clipboard.set_image with format and jpeg_quality parameters to put the image as a JPEG saved at 80% of quality in the clipboard.

Once set up, you can create a Home screen shortcut for the script; select two iPhone screenshots from the clipboard, tap on the shortcut, and after a few seconds you’ll have a composited image ready in your clipboard.

This script has enabled me to a) save precious minutes, b) be faster and more efficient, and c) uninstall Photoshop Touch, which I was using solely to create this kind of screenshots. To manipulate iPhone screenshots on my iPad, I rely on Photo Stream for screenshots already in iCloud; when I’m on the go, I move screenshots between devices using Scotty.

- Download

iPad Screenshot to JPEG

I have an iPad 3. Whenever I take a screenshot, iOS saves a 2048x1536 .png file weighing at least 1 MB. I want that screenshot to become a lighter JPEG saved at 80% of the original quality.

import clipboard
import Image

image = clipboard.get_image()

background = Image.new('RGBA', (2048,1536), (255, 255, 255, 255))

background.paste(image,(0,0))

clipboard.set_image((background), format='jpeg', jpeg_quality=0.80)

Essentially, a simple adaptation of the screenshots script that could be done better (I’m not sure I even need to paste the image first), but still gets the job done.

- Download

Upload Screenshot and Read Text File from Dropbox

For MacStories image uploads, our Don Southard has set up a script that watches a Dropbox folder for images, and uploads every new image to our CDN. Every uploaded image returns a URL that is appended to a text file also in Dropbox. This allows every member of the team to simply drop images in Dropbox and get a CDN URL back after a few seconds; it is very convenient on the Mac thanks to the Dropbox app, but I wanted to automate the process on iOS as well. With input from Ole, I set up a script that uploads an image and waits 15 seconds before reading the last line of our .txt file from Dropbox.

First, create a Dropbox app and make sure to save a script that lets you log into Dropbox and prints your account info. I’m using this version provided on the Pythonista forums.

The script imports get_client and the account information from keychain.

from dropboxlogin import get_client
dropbox_client = get_client()
import clipboard
import keychain
import console
import time
import httplib
from io import BytesIO
import webbrowser

img = clipboard.get_image()
titles = console.input_alert('Image Upload', 'Enter your image name below')
buffer = BytesIO()
img.save(buffer, 'PNG')
buffer.seek(0)
response = dropbox_client.put_file('/LOLwat/Photos/upload-unedited/' + titles + '.png', buffer)

print "uploaded:", response

time.sleep(15) # delay for 15 seconds

console.clear()


def main():
	dropbox_client = get_client()
	f, meta = dropbox_client.get_file_and_metadata('/SecretLocation/Photos/upload-unedited/LeSecret.txt')
	content = f.read() # can use readlines here to get list of lines
	print 'file content:'
	print '=' * 40
	print content
	print '=' * 40
	print 'metadata:'
	print meta
	last = content.splitlines()[-1]
	clipboard.set(last)
	console.clear()
	print last
	webbrowser.open(last)
	
if __name__ == '__main__':
	main()

Line 11 gets an image from the clipboard, lets me enter a name with input_alert, and saves it as .png in a temporary buffer provided by BytesIO. Using the Dropbox command put_file, it uploads the file to a specific directory in my Dropbox.

Line 18 prints a response from Dropbox, then waits 15 seconds (my Mac mini usually takes 10 seconds to process an image) and the console is cleared. Last, dropbox_client reads a text file, and specifically the last line (the URL that was just appended) using splitlines[-1]. The URL is printed, set to the clipboard, and opened in Pythonista’s web browser for extra confirmation.

I’ve put this in my Actions menu and I use it after generating images with the scripts described above.

- Download

Images HTML

As I mentioned above, once I’ve written a post as Markdown and converted it to HTML, I need to insert images in the post. With Dropbox I can easily send an image from the clipboard to our CDN - but what about getting the proper HTML code inserted in the final post? Theoretically, I could use the built-in uploader of apps like Poster and Posts, paste an image’s URL, and adjust the way I want an image to be displayed. However, I like to have HTML for images inserted just right for MacStories, with more control over image alignment and title.

The script I use for this takes a URL in the clipboard and builds a series of HTML strings around it. I've embedded this script as image so its HTML tags wouldn't create issues with this page.

On line 9, the script asks for text to use for the image’s title and alt attributes in the final string. Said string, aptly named final, is simply a concatenation of various HTML tags and elements I have manually entered. Keep in mind double quotes need to be escaped with the \ character, as shown with aligncenter in the string.

Notably, the script uses console.input_alert to pop up native iOS alert boxes asking for user input.

Once constructed, the string is put back onto the clipboard, ready for pasting in a text editor.

- Download

Copy Webpage Title

I often need to grab a URL’s title when writing blog posts, saving bookmarks, or tweeting links. For those times when this kind of workflow isn’t implemented in another script (as you’ll see below), I have a standalone version that, given a URL in the clipboard, prints the title and URL so I can freely copy them from the console.

import urllib
import clipboard
import bs4
import console

link = clipboard.get()

console.show_activity()

soup = bs4.BeautifulSoup(urllib.urlopen(link))
pageTitle = soup.title.string +' '+ link

console.hide_activity()

console.clear()

print pageTitle

- Download

Download in iCab

iCab is a third-party iOS browser that comes with rich support for x-callback-url. The following script uses iCab’s download action to start downloading a link provided by the clipboard.

import webbrowser
import urllib
import clipboard

base = 'x-icabmobile://x-callback-url/download?url='
url = clipboard.get()
url = urllib.quote(url, safe='')
webbrowser.open(base + url)

This comes particularly in handy as a Home screen icon: copy link, tap the icon, start a download.

- Download

Bonus: Here’s a bookmarklet to send a URL to iCab, which will start downloading it.

javascript:window.location='x-icabmobile://x-callback-url/download?url='+encodeURIComponent(document.location.href);

Convert Twitter URLs

Previously detailed in this post, I’ve put together a script to turn Twitter.com URLs into Tweetbot-specific links that will open a tweet with Tweetbot’s detail view.

import clipboard
import console
import webbrowser

mytext = clipboard.get()
mytext = mytext.replace('https://twitter.com/', 'tweetbot://')
mytext = mytext.replace('statuses', 'status')
mytext = mytext.replace('http://twitter.com/', 'tweetbot://')
mytext = mytext.replace('http://mobile.twitter.com/', 'tweetbot://')
mytext = mytext.replace('https://mobile.twitter.com/', 'tweetbot://')

console.clear()
print mytext

clipboard.set(mytext)

webbrowser.open(mytext)

I use this to share links to tweets with my team, as we all use Tweetbot on our devices.

- Download

Percent Encode

This one percent-encodes text from the iOS clipboard.

import clipboard
import urllib

s = clipboard.get()

s = s.encode('utf-8')
s = urllib.quote(s, safe='')

print s

- Download

Post to Pinboard

When I’m not writing, I’m usually reading. And if I’m reading an interesting article I found on the Internet, there’s a high chance it’ll be bookmarked on Pinboard. Unfortunately, there aren’t fast and intuitive ways to save Pinboard bookmarks on iOS: there’s no iPad version of Pinbook yet, and using the bookmarklet works, but it’s not a great experience.

Annoyed by the cumbersome process of carefully tapping on text fields in a bookmarklet and the lack of simultaneous Instapaper & Pinboard saving in Tweetbot, I built a Python script that saves a URL to my Pinboard account in a few seconds. It works with a bookmarklet or the system clipboard.

import console
console.show_activity()
import urllib
from urllib import urlencode
import bs4
import requests
import webbrowser
import sys
import sound
sound.load_effect('Powerup_2')
import keychain
import clipboard
console.hide_activity()

numArgs = len(sys.argv)

if numArgs < 2:
	url = clipboard.get()

	console.show_activity()

	soup = bs4.BeautifulSoup(urllib.urlopen(url))
	title = soup.title.string
        text = title.encode('utf-8')

	console.hide_activity()

else:

	text = sys.argv[1]
	url = sys.argv[2]

PASSWORD = keychain.get_password('pinboard','ticci')
USER = 'ticci'

tags = console.input_alert('Tags', 'Enter your tags below')

console.show_activity()

query = {'url': url,
         'description': text,
         'tags': tags}
query_string = urlencode(query)

pinboard_url = 'https://api.pinboard.in/v1/posts/add?' + query_string

r = requests.get(pinboard_url, auth=(USER, PASSWORD))

console.clear()

if r.status_code != 200:
	print 'Could not post:', r.text
	
elif r.status_code == 200:
	tags = tags.split(' ')
	tags = ','.join(tags)
	sound.play_effect('Powerup_2')
	print 'Link saved successfully'
	print text
	print "tags: " + tags

Bookmarklet:

javascript:window.location='pythonista://Pinb?action=run&argv='+encodeURIComponent(document.title)+'&argv='+encodeURIComponent(document.location.href);

With the bookmarklet, a webpage’s URL and title are sent to Pythonista via argv. If a link is in the clipboard, the webpage title is fetched using bs4. The script checks if less than two argv are sent, and in that case it proceeds to get URL and title from the clipboard.

The bookmarklet scenario is the more frequent one as I tend to browse on my iOS devices using Safari (because of its superior performance to third-party browsers); however, I find it convenient to also be able to copy a URL from any app and run the script.

The bookmarklet is fairly simple: it composes a Pythonista URL scheme string using two argv parameters – the title and the URL. The title portion is required as it’s what Pinboard will use for the name of a bookmark; in the Pinboard API, it’s called description to maintain compatibility with the legacy Delicious API.

The script begins by assigning sys.argv[1] and sys.argv[2] to title and URL, respectively. On line 10, it pre-loads a sound effect it’ll use at the end if a bookmark has been successfully added to Pinboard. Line 33/34 detemines the password and username to contact the Pinboard API with a GET request; the password has been previously saved to Pythonista’s secure keychain, so I don’t have to show it as string in the script. Line 36 brings up an alert to enter space-separated tags.

Line 40–43 creates the final query string by combining URL, description, and tags and by running urlencode on it. Following Ole’s suggestions, the Pinboard API is contacted using requests on line 47. A spinner is shown in the iOS status bar to indicate the script is working in line 2 (before importing the modules), line 20, and line 38.

The final portion of the script uses an if statement to print an error if the Pinboard response code is different than 200. If the GET request is completed without error, the script prints a message with the title of the bookmark and tags assigned to it (the sound is also played). Previously space-separated tags are split by individual words and joined using commas in a single string.

If you’re running the script with a URL in the clipboard, the only difference is how it uses clipboard.get() to grab a URL and runs bs4.BeautifulSoup(urllib.urlopen()) to get a webpage name as string (a spinner is shown in the status bar during this process).

Because of the check for argv on line 17, the same script works regardless of bookmarklet or clipboard usage; if you’ve copied a URL to the clipboard, I recommend adding this script to the Home screen.

- Download

Link in Due

Due is a nice reminder app for iOS and OS X with support for iCloud and Dropbox sync, as well as x-callback-url for creating reminders from other apps. I like to save links as “quick reminders” to check out in a few minutes (or hours) in Due, which happens to have a feature to open URLs contained in a reminder upon hitting the “Done” button. Like the Pinboard one, this script works with a bookmarklet or the system clipboard. If you use the bookmarklet, it grabs a link’s URL and title from Safari ; if you’ve copied a link to the clipboard, it fetches the webpage’s title using bs4.

import console
console.show_activity()
import clipboard
import webbrowser
import urllib
from urllib import urlencode
import bs4
import sys

numArgs = len(sys.argv)

addnew = 'due://x-callback-url/add?title='

addtime = '&secslater='

console.hide_activity()

if numArgs < 2:
	
	console.show_activity()

	url = clipboard.get()

	soup = bs4.BeautifulSoup(urllib.urlopen(url))
	newlink = (soup.title.string + ' ' + url).encode('utf-8')

	console.hide_activity()

else:

	title = sys.argv[1]
	url = sys.argv[2]
	newlink = title + ' ' + url

newtask = console.input_alert('What is this?', 'Type your reminder below')

newtime = console.input_alert('When?', '3600 for 1 hour, 1800 for 30 minutes')

console.hide_activity()

text = newtask + ' - ' + newlink

encoded = urllib.quote(text, safe='')

err_handler = '&x-source=Source%20App&x-error=pythonista://&x-success=' + url

openDue = addnew + encoded + addtime + newtime + err_handler

console.clear()

webbrowser.open(openDue)

Bookmarklet:

javascript:window.location='pythonista://Due?action=run&argv='+encodeURIComponent(document.title)+'&argv='+encodeURIComponent(document.location.href);

Title and URL are represented by argv sent by the bookmarklet. The script revolves around the Due URL scheme, which is based on x-callback-url and is split up in multiple parts to allow me to enter a description and “due time” using console.input_alert in line 35 and 37.

Line 41 combines my description of the reminder with the title and URL using a dash as separator between the two; line 43 percent-encodes the reminder’s name as requested by Due. Last, line 45 specifies how to handle errors in Due: thanks to x-callback-url, Due can go back to another app if the user cancels the action of adding a reminder. Unfortunately, the friendly name for x-source isn’t dynamic – meaning, Due can’t display a different dialog depending on whether the user cancels the action or completes it. So, I chose to display a general “Return to Source App” dialog that returns to Pythonista in case of error (user canceled the reminder) or goes back to the link opened by the bookmarklet if the reminder is added.[13]

Line 47 composes the final URL to open in Due using all the pieces created and encoded in the script, and the last line uses webbrowser to launch Due.

I’ve been using Due to save quick reminders and I like the automation provided by this script, which I’ve added to my Home screen. In using it, I’ve noticed I’d like Pythonista to offer an option to open the iOS numeric keyboard when I need to enter numbers in input_alert.

- Download

Bonus: Here’s a bookmarklet to send a link (URL + title) to Due without Pythonista (you’ll have to enter any additional text and alarms manually). It also works on the Mac.

javascript:window.location='due://x-callback-url/add?title='%20+%20%20encodeURIComponent(document.title)+%20'%20'+%20encodeURIComponent(document.location.href)+'&x-source=Source%20App&x-error=due://x-callback-url/&x-success='+encodeURIComponent(document.location.href);

Sequential Task in Due

Inspired by Sean Korzdorfer, I’ve created a script that adds a reminder including an additional URL to create a sequential reminder in Due upon completing the first one.

import clipboard
import console
import urllib
import webbrowser

addnew = 'due://x-callback-url/add?title='

addtime = '&secslater='

newtask = console.input_alert('First Task', 'Type your reminder below')

newtime = console.input_alert('When?', '3600 for 1 hour, 1800 for 30 minutes, 300 for 5')

seqtask = console.input_alert('What next?', 'Type your reminder below')

seqtime = console.input_alert('Second Task?', '3600 for 1 hour, 1800 for 30 minutes, 300 for 5')

secondR = urllib.quote(seqtask, safe='')

newlink = 'due://x-callback-url/add?title=' + secondR + '&secslater=' + seqtime

encoded = newtask + ' ' + newlink

text = urllib.quote(encoded, safe='')

openDue = addnew + text + addtime + newtime

webbrowser.open(openDue)

Similar to first Due script, I’ve noticed spaces in the sequential reminder weren’t being encoded properly at the end; I’ve thus added an additional percent-encoding for seqtask that ensures spaces are preserved for the sequential reminder’s name.

This script can come in handy for reminders that need to be executed one after the other such as “do laundry” and “fold laundry”. When completing the first reminder, Due will ask you to launch the embedded Due URL scheme, which will create the second reminder without leaving the app.

- Download

Drafts

I use Agile Tortoise’s Drafts with a variety of scripts that, on my remote Mac mini, process text coming from email or text files and add it to other text files or OmniFocus. I also use Drafts to send text to other services such as Twitter and Evernote, and I’m a fan of the custom email actions added in the latest update to the app.

The script I use lets me send text to Drafts quickly. It grabs a webpage’s selection and URL if sent by a bookmarklet; if not, it pops up a console alert allowing me to enter text manually and send it to Drafts.

import sys
import webbrowser
import console
import urllib

numArgs = len(sys.argv)
base = 'drafts://x-callback-url/create?text='

if numArgs == 3:

	clip = sys.argv[2]
	link = sys.argv[1]
	
	if clip =="":
	
		text = link
		text = urllib.quote(text, safe='')
	
	else:
	
		text = clip + "\n" + "\n" + "From: " + link
	
		text = urllib.quote(text, safe='')
	
else:
	
	text = console.input_alert('Drafts', 'Enter your draft below')
	text = urllib.quote(text, safe='')
	
webbrowser.open(base + text)

Bookmarklet:

javascript:window.location='pythonista://Drafts?action=run&argv='+encodeURIComponent(location.href)+'&argv='+encodeURIComponent(window.getSelection());

On line 9, the script checks for the number of argv sent by the bookmarklet: if there are three and if the selection is empty, the text is prepared as a single string consisting of the URL. If there are three but the selection is not empty – meaning, the bookmarklet was sent with text selected in the browser – text is made of the selection, an empty line, and the URL. If there are no arguments (script wasn’t triggered by a bookmarklet), an input_alert lets me write a note manually. In all three cases urllib.quote encodes text for Drafts. The base Drafts x-callback-url is opened by webbrowser alongside text on line 30.

For me, this is the fastest way to send a URL or Safari’s selection and URL to Drafts, so I can forward them to other services configured with the app.

- Download

Append to Notesy

I keep a Scratchpad.txt file in my Dropbox where I append bits of text and information from my Mac and iOS devices. I’ll describe my workflow for this file in a future article. As an alternative to the Drafts script to append text “remotely”, I’ve set up a script based on Notesy’s local URL scheme that appends text like I want to: with a date string and the information copied from the clipboard directly below it.

Like the Pinboard, Due, and Drafts scripts, there’s an option to use this with a bookmarklet in Safari or your iOS browser of choice. The bookmarklet checks for text selected in the current tab and sends that selection to Pythonista to process it for Notesy. Due to the nature of iOS’ Safari, this only works on the iPad, where bookmarklets can be tapped in the browser’s toolbar so window.getSelection() can be preserved while tapping. With JavaScript, I can send the original page’s URL as argv to the script: this makes it possible to “round-trip” back to Safari after the text from a webpage has been clipped to Notesy. Unfortunately, due to a bug in the current version of Notesy, a “?” character is appended to a URL configured with x-success; the Notesy team is aware of the bug and working on a fix.

import console
console.show_activity()
import webbrowser
import urllib
import clipboard
import datetime
import sys

today = datetime.datetime.now()
base = 'notesy://x-callback-url/append?name=Scratchpad.txt'
file = '&text='

console.hide_activity()

numArgs = len(sys.argv)

if numArgs == 3:

	clip = sys.argv[2]
	link = sys.argv[1]
	
	actions = '&x-success=' + link + '&x-error=notesy://'
	
	if clip =="":
	
		text = "\n" + '_' + today.strftime("%Y-%m-%d") + '_' + "\n" + link
		text = urllib.quote(text, safe='')
	
	else:
	
		text = "\n" + '_' + today.strftime("%Y-%m-%d") + '_' + "\n" + clip + "\n" + "\n" + "From: " + link
		text = urllib.quote(text, safe='')

else:

	url = clipboard.get()
	clip = url.encode('utf-8')
	actions = '&x-success=notesy://&x-error=notesy://'
	text = "\n" + '_' + today.strftime("%Y-%m-%d") + '_' + "\n" + clip
	text = urllib.quote(text, safe='')

webbrowser.open(base + actions + file + text)

Bookmarklet:

javascript:window.location='pythonista://Notesy?action=run&argv='+encodeURIComponent(location.href)+'&argv='+encodeURIComponent(window.getSelection());

I’ve got to thank Gabe for suggesting the datetime module. Line 9 specifies what “today” is, and base creates the URL for appending text to a specific file in Notesy (using x-callback-url). When appending text, a new line is added with \n then the date is inserted using the format I prefer with strftime. Another new line is added, and a percent-encoded string is appended underneath the date. With x-callback-url, the script returns to Pythonista if it succeeds, or stays in Notesy in case of error. As usual, app URL schemes are launched from Pythonista using webbrowser.

Like Drafts, the script checks for an empty selection in Safari. If the clip argument is an empty string, only the URL is appended to Notesy’s text file.

This is yet another example of the powerful URL scheme provided by Notesy. I would like to see an x-callback-url parameter to make sure Notesy initiates a sync before and after appending the text and switching back to Pythonista, so I can make sure the text I appended is also synced to all my Dropbox clients (and to avoid conflicted copies).

- Download

OmniFocus Task for Drafts

My last script is another take on the bookmarklet/clipboard combination for Drafts that is formatted for OmniFocus task importing via email. It is primarily intended to quickly add web URLs to OmniFocus using Drafts’ email actions.

import console
console.show_activity()
import webbrowser
import urllib
import bs4
import sys

addDrafts = 'drafts://x-callback-url/create?text='

console.hide_activity()

numArgs = len(sys.argv)

if numArgs == 3:

	clip = sys.argv[2]
	link = sys.argv[1]
	
	if clip =="":
	
		text = link + ' ' + '@Links'
		text = urllib.quote(text, safe='')
		openDrafts = addDrafts + text
	
	else:
	
		text = link + ' ' + '@Links' + '\n' + clip
		text = urllib.quote(text, safe='')
		openDrafts = addDrafts + text

openDrafts = addDrafts + text

webbrowser.open(openDrafts)

Bookmarklet:

javascript:window.location='pythonista://OFLink2Drafts?action=run&argv='+encodeURIComponent(location.href)+'&argv='+encodeURIComponent(window.getSelection());

The script is essentially the same of the regular Drafts one, excepts it uses OmniFocus’ email syntax for contexts and notes; it works with the bookmarklet, and the browser selection is added after a new line so OmniFocus will see it as a note for a new task. Line 21/27 adds a “@Links” string that will identify the task as belonging to the “Links” context in OmniFocus.

- Download

iOS Automation

The scripts above are just an example of how my limited knowledge of Python allowed me to radically improve my iOS workflow thanks to the power of Pythonista.

I’ve been relying on these scripts for a few months now, and I’ve been able to get work done on iOS just like I do on my Mac. However, while the end result may be the same (articles get published and notes get saved), the setup is entirely different.

I don’t think iOS automation will ever be exactly like OS X automation. Pythonista has a series of limitations that, in comparison, running Python on OS X or any other desktop operating system doesn’t have. Hardcore Python users may not like this. At the same time, though, I also find working in Pythonista more intuitive, safer, and, ultimately, easier. Why shouldn’t I “compromise” if the end result is the same but I actually get more joy out of the experience?

I believe that, going forward, Pythonista and other similar apps will show a new kind of “scripting” and task automation built around the core strenghts of iOS. As we’ve seen, x-callback-url is a standard that leverages a part of iOS – URL schemes – to achieve simple, user-friendly and URL-based inter-app communication that can be used in a variety of ways. Looking ahead, there’s a chance rumored features such as XPC will bring more Mac-like functionalities to iOS, but developers will still find new ways to make iOS more powerful without giving up on positive aspects such as increased security and the simplicity of the app model.

We’re in the early stages of iOS automation, but Pythonista, Launch Center Pro, and x-callback-url are indicating the path other developers should follow.

Pythonista shows that it’s possible to be a power user on iOS while playing by Apple’s rules.


  1. Splitting text items at a specific delimiter takes two lines in Python. Don’t get me started on AppleScript Text Item Delimiters.  ↩
  2. My town, Viterbo, is (surprisingly) testing DC-HSDPA wireless networking with TIM, the carrier I chose for all my devices. With the iPad 3 and iPhone 5, I usually get between 10–15 Mbits (download) and 3–6 Mbits (upload) here in Viterbo; it’s faster than my home connection with Fastweb. When not on DC-HSDPA, the iPhone 5 still manages to find good 3G signal (2–3 bars) in areas where previously the iPhone 4S went to “No Service”.  ↩
  3. Actually, no, I did.  ↩
  4. More about this here. In short, I care about having proper title attributes when converting Markdown links to HTML.  ↩
  5. Font options are also available for the output of the interactive prompt.  ↩
  6. If you want, you can pair an external keyboard with Pythonista via Bluetooth. Ole Zorn implemented some shortcuts for the code editor exclusively for external keyboards. As for touch, I love how iOS lets you select an entire line by tapping with two fingers.  ↩
  7. The exporting options offered by Pythonista are very comprehensive. You can export Python scripts as Xcode Projects to build apps on the desktop, even selecting parameters like “Targeted Devices” and “Home Button” behavior beforehand, from Pythonista. I’ll cover this in a future post. To share scripts as scripts, you can send them via email, open in other apps (such as Textastic), or share on GitHub. Gists can be private or anonymous.  ↩
  8. If you select a term that hasn’t got a “best match”, Pythonista’s Quick Help will display possible search results for your query.  ↩
  9. If you delete the built-in examples (like I did), you can always restore them in the Settings. The fruit game is my favorite so far. Crazy to think it’s actually made in Python, on a phone.  ↩
  10. Check out the documentation for an example on how to play specific notes with the Piano sound.  ↩
  11. Not to mention using the URL scheme in combination with x-callback-url and multiple iOS apps that support the standard.  ↩
  12. Yes, I have tried Launch+.  ↩
  13. My reasoning for x-success and x-error is the following: go back to Pythonista if the user cancels or the reminder couldn’t be added; go back to the URL after adding the reminder so the user can close the tab (or re-confirm he wanted to really add that link).  ↩

Send Multiple Tasks To OmniFocus Mail Drop At Once With Drafts and Pythonista

$
0
0

Send Multiple Tasks To OmniFocus Mail Drop At Once With Drafts and Pythonista

Nice workflow by Nathan Henrie to send multiple tasks to OmniFocus at once using Mail Drop, Drafts, and Pythonista:

I’ve also recently started playing with Pythonista, and I came across a Python script written by the dev himself that creates a little SMTP server and sends email directly from Pythonista. Between the two, I found it pretty easy — even for a beginner like me — to put together a combined Drafts / Pythonista workflow that makes for a superior way to import a bunch of tasks to OmniFocus at once (aka “brain dump”).

The Python part is based on the same script I covered in November to send emails through Pythonista; Nathan added a clever Drafts integration by splitting multiple lines (from the draft) into separate email messages sent to your Mail Drop address. Make sure to check out his video to see the workflow in action; I have started using it myself and I like how fast tasks go from Drafts onto OmniFocus via email (I have configured the script with my Gmail address using 2-step verification).

I have become a big fan of OmniFocus Mail Drop. It’s been extremely fast and reliable in my experience, and it works well with Drafts’ email actions.

A Better Chrome To Safari Bookmarklet

$
0
0

pythonista

In January, I tried to put together a bookmarklet to send the webpage currently open in Google Chrome for iOS to Apple's Safari. That turned out to be a surprisingly complex effort as Google didn't think offering an “Open In Safari” option would be a good idea, and the app's URL scheme produced some interesting results when opening and closing Chrome.

I was reminded of the bookmarklet this morning by reader @CNWLshadow, and I realized that I never posted the solution I settled with. It consists of a browser bookmarklet and a Pythonista script, and it works with just one tap.

With Pythonista 1.3, developer Ole Zorn added the possibility to open links specifically in Safari by prefixing them with safari-, therefore allowing me to create a simple bookmarklet that would:

  • Take the current URL open in Chrome;
  • Give it to Pythonista;
  • Open it in Safari.

The bookmarklet is extremely basic. It assumes a script called “ToSafari” is available in the root of Pythonista, and it sends an argument for the webpage that is currently open. You can copy the code below and add it to Chrome as a bookmark.

javascript:window.location='pythonista://ToSafari?action=run&argv='+encodeURIComponent(document.location.href);

The script is simple as well. It starts by counting the arguments sent to the script: if less than two, it will print that no URL was received; else, it will take the URL and send it to Safari by prefixing it with safari-.

# Receives a URL via JS bookmarklet and sends it to Safari
# Bookmarklet: javascript:window.location='pythonista://ToSafari?action=run&argv='+encodeURIComponent(document.location.href);
# Assumes script is in the root of Pythonista and called 'ToSafari'


import sys
import webbrowser

numArgs = len(sys.argv)

if numArgs < 2:
    print 'No URL was received'
    
else:
    url = sys.argv[1]

    webbrowser.open('safari-' + url)

Once set up, the script takes around 6 seconds to be executed on my iPad mini from a cold start (Pythonista force-quit from the multitasking tray), 1 second if Pythonista is in the background. While it isn't the best solution, I think this is an acceptable compromise to get a working Chrome-to-Safari bookmarklet that doesn't require any further manual operation.

Resolve Short URLs with Pythonista on iOS

$
0
0

Clean URLs

I don’t like it when third-party apps or services force me to share links to articles or webpages using their own custom shortened links. I understand the appeal of personalized short domains – after all, we tweet mcstr.net links with the @macstoriesnet account – as they can provide analytics to track clicks, can save characters, and, at least in theory, they “look cool”. However, I’ve been long considering the idea of dropping our mcstr.net links, but I think the issue is worse (and more annoying) for apps and services that don’t tweet links to their own content (like we do) but that override others’ links with different domains. An example is Pocket, which gives you the clean, original URL when you choose the “Copy Link” action from the sharing menu, but that instead returns pocket.co links when sending text to Drafts (which I do often). I’ve grown tired of this practice (in Pocket and other services), and I’ve put together a workflow based on a Python script that allows me to easily resolve short links without having to open the browser and tap on multiple menus.

As I briefly mentioned in a footnote in May, my problem with short URLs was that Flipboard’s “Flip It” bookmarklet can’t fetch URLs behind pocket.co links. In trying to minimize friction as much as possible, I wanted to have a solution that allowed me to send a link from Pocket to Drafts without having to hit “Copy Link” first; then, from Drafts I could launch an action to save a link in Flipboard, but in order to resolve the shortened link I’d have to put a script in the middle. The script I’ve created receives a URL, tries to fetch the real URL of a webpage, and returns a redirect-free URL that I can send to Flipboard, knowing that the service’s web interface will recognize it and add it to my magazine.

The script is short and I’m using it with Pythonista on iOS, but it can be tweaked with minor modifications and ported to OS X if you want to run it in apps like Keyboard Maestro or Alfred. In my workflow, when I’m done reading an article in Pocket, I tap “Send to Drafts”, then the “Clean URL” action, and in a matter of seconds I’m taken from Drafts, to Pythonista, then to Flipboard’s sharing page in Google Chrome, and, last, back to Pocket, where I can archive the article I just read and shared.

Script and quick demo video below.

import re
import string
import console
import urllib
import sys
import clipboard
import webbrowser
 
console.clear()

# Count arguments passed to script, if less than 2 run regex against clipboard
numArgs = len(sys.argv)
 
if numArgs == 2:
	redirect = sys.argv[1]
elif numArgs < 2:
	redirect = clipboard.get()
	
# Provided by Peter Hansen on StackOverflow:
# http://stackoverflow.com/questions/1986059/grubers-url-regular-expression-in-python/1986151#1986151
pat = r'\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^%s\s]|/)))'
pat = pat % re.escape(string.punctuation)
 

match = re.findall(pat, redirect)
 
if match:
	for x in match:
		console.show_activity()
		# Get the first match without redirects
		cleaned = urllib.urlopen(x[0]).geturl()
		final = urllib.quote(cleaned, safe='')
		clipboard.set(cleaned)
		console.hide_activity()
		webbrowser.open('googlechrome-x-callback://x-callback-url/open/?url=https%3A%2F%2Fshare.flipboard.com%2Fflipit%2Fload%3Fv%3D1.0%26url%3D' + final + '&x-success=pocket://&x-source=Pocket')
elif not match:
	console.alert('No match found')

The script essentially relies on the geturl() method of urllib.urlopen() to follow all redirects of a link and return the real URL of a page. As written in the Python documentation:

The geturl() method returns the real URL of the page. In some cases, the HTTP server redirects a client to another URL. The urlopen() function handles this transparently, but in some cases the caller needs to know which URL the client was redirected to. The geturl() method can be used to get at this redirected URL.

Line 12 counts the arguments that have been passed to the script: if less than 2, the script assumes a link may be in the clipboard; if 2 arguments are found, sys.argv[1] is identified as the URL to fetch. I do this because I want to be able to send text from Drafts to Pythonista, and, as I detailed in my original review, Pythonista’s URL scheme supports arguments, which make it easy to set up the kind of check that I have in my script.

Clean URLs

You can launch this script from apps like Launch Center Pro or iCab, but here’s what my Drafts action looks like:

pythonista://CleanURL?action=run&argv=[[draft]]

Lines 21–22 use a regular expression by Peter Hansen (posted in reply to a question about Gruber’s regex in Python) that scans received text for a match; the match is, as you’d imagine, a web URL. Because Pocket sends both an article’s title and short link to Drafts on a single line, we can’t use Drafts’ line tags to easily separate the title from the URL, and the best way to do that is a regex that returns matches of a URL in a string. You can use any regex for matching URLs – Hansen’s worked fine for me and I like its simplicity.

Line 25 runs the regex with re.findall() and lines 27–37 contain the actual URL-fetching mechanism inside an if statement that checks whether there is a match in the first place. Line 31 gets the first match returned by re.findall, fetches its real URL, and percent-encodes it using urllib.quote with no default safe character. The system clipboard is then set to the newly-acquired URL on line 33.

Now, the script could be used like this (get a URL, fetch its real version, set it to the clipboard) without caring about Flipboard’s web interface at all. But because I do a lot of gaming-related reading in Pocket and I have a Flipboard magazine I try to update regularly, I added a line to the script that launches Google Chrome, opens the Flipboard sharing page, and passes the non-Pocket URL fetched by the script.

Clean URLs

Using the webbrowser module and Chrome’s callback system, the script launches Chrome, which will display the Flipboard magazine popup pre-filled with my URL and a custom back button that will take me back to Pocket once the URL has been added to Flipboard. A nice thing about Flipboard’s share UI is that it doesn’t navigate to another page when you add a URL, so the Pocket button will always remain visible.

I realize that not everyone may need a script that resolves a short link and returns the clean, original version; however, I still think it’s pretty incredible that I can run a Python script on my phone to save a few seconds every day, which add up to several minutes every year that I can spend doing better and more productive things than tapping on browser menus – such as spending more time at the beach with my girlfriend. I may not be as adventurous as Dr. Drang, solving a client’s problems with a Python program from a gas station parking lot, but, even from the comfort of my couch, I’m quite fond of the solution I’ve put together to more easily share Pocket links to Flipboard.

Check Dev Center Status From iOS with Pythonista

Fixing iOS Screenshot Status Bars with Python

$
0
0

Dr. Drang:

On last week’s episode of The Prompt, Federico went off on a rant about ugly iOS screenshots. He wasn’t complaining about the apps themselves being ugly, he was chastising those of us who post screenshots with status bars showing inconsistent times, signal strengths, and battery levels. And Lord help you if your battery icon is in the red.

His recommendation was Status Magic, a Mac app that cleans up the status bars in your iOS screenshots and makes them uniform. It looks like a nice app, but my thoughts gravitated toward a script using the Python Imaging Library. Why would I write a script when an inexpensive app is available?

Fixing iOS status bars is one of the reasons I need to use my Mac with Status Magic because there is no similar app on iOS. I am playing around with Dr. Drang’s script, which can be easily adapted to Pythonista and integrated with the app’s photos module for Camera Roll integration. Putting together status bar replacement images that match Apple’s ones is a bit of work (it’s tricky to get the fonts right, but now I’m trying this) and they won’t produce good results with blurred status bars, but those are the same inconveniences that iOS 7 brought to Status Magic anyway.

I’m looking forward to seeing what tweaks and improvements the Doctor will make to his script. Once I have a good solution for Pythonista (which I already use to combine screenshots on iOS), my iOS writing and editing workflow for text and screenshots will be largely similar to the OS X one (I still need a good uploader for Cloud Files and an iOS version of this).

∞ Read this on MacStories


Automate iOS Contacts, Location Services, and Open In Menu with Pythonista 1.4

$
0
0

Pythonista 1.4

Pythonista is the app that changed my iOS workflow a year ago. A Python interpreter with native access to iOS system features like photos, URLs, and interface elements, Pythonista allowed me to convert the scripts and macros that I was using on OS X to the iPad, automating iOS in better and sometimes unexpected ways. Pythonista eventually led to Editorial, also developed by Ole Zorn, which changed the way I write and work on my iPad every day.

Pythonista 1.4, available today on the App Store, is the biggest update to Zorn’s app to date. It includes a new UI for iOS 7 (the app is also iOS 7-only starting today), new modules and enhancements to existing ones, and, more importantly, it doubles down on iOS integration by bringing native support for contacts, location, and Open In.

New UI

Pythonista primarily consists of a script editor and a console view that don’t look out of place on iOS 7, so Zorn overhauled the rest of the app’s interface to make it fit with Apple’s new aesthetic. In short: glossy toolbars are gone, the icons (including the app’s one) have been redesigned, and white dominates the UI with light blue accents for buttons and other interactive elements.

The layout of Pythonista has stayed the same, with scripts listed alongside folders in a sidebar on the left, the script editor in the middle, and the console panel on the right.

Open In

I’m not a fan of Apple’s Open In menu for iOS apps: it creates duplicate files, it’s redundant, and iOS’ inter-app communication needs go beyond a way to send a copy of a file across apps. However, Open In is (mostly) everything we have on iOS, and Zorn decided to properly support it in Pythonista 1.4. The result is intriguing.

The premise is that Pythonista can receive files sent through the Open In menu from other apps. Any app that can send an image (like Skitch or iPhoto), a text file (Byword), a PDF document (PDF Expert), or any other file with the native Open In menu can now send it to Pythonista. By default, Pythonista will add a received file to an Inbox folder in the sidebar and show it with QuickLook.

This is the first big addition. Non-Python files can be viewed in Pythonista with the native iOS QuickLook previewer (the same that is used in Apple apps like Mail and Messages) and plain text files can be created and edited directly in Pythonista. While it was possible to call local files in Pythonista scripts before, putting them into the app’s Documents was a cumbersome process, as it required downloading them from the web with a script or using utilities like iExplorer on a Mac to transfer files via USB. With Open In and a native preview, you can now easily send files to Pythonista without needing a computer. But that’s not the best part.

Pythonista 1.4

The Open In menu can be automated and scripted with Pythonista 1.4. In the app’s settings, you can choose a script to automatically run when a file is received through Open In, and, in the script, you can reference the file’s path and sender app’s bundle ID as command line arguments. This is a huge addition for automated iOS workflows and chaining apps together – it means that you no longer need to manually pick files because you can use Open In as a script handler in Pythonista. You can run specific scripts automatically depending on the app that sent a file while also using that file without ever needing to manually pick it.

I have enhanced several of my scripts and workflows with Open In integration, cutting down time required to choose scripts to run and files to use. Hence, rather than a bland textual description, let’s play by example.

Take, for instance, my “DropPics” script, which uses the dropbox module to put an image in your Public folder in Dropbox and create an old-style public URL that is set to the clipboard and opened in Safari:

from dropboxlogin import get_client
dropbox_client = get_client()
import keychain
import console
import time
import httplib
from io import BytesIO
import datetime
import webbrowser
import urllib
import clipboard
 
today = datetime.datetime.now()
 
console.clear()
 
def DropPic(img):
	titles = console.input_alert('Image Upload', 'Enter your image name below')
	console.show_activity()
	buffer = BytesIO()
	img.save(buffer, 'JPEG')
	buffer.seek(0)
	imgname = today.strftime("%Y-%m-%d-at-%H-%M-%S") + '-' + titles + '.jpeg'
	response = dropbox_client.put_file('/Public/' + imgname, buffer)
	console.hide_activity()
	encoded = urllib.quote(imgname, safe='')
	final = 'http://dl.dropbox.com/u/YOURDROPBOXUSERID/' + encoded
	clipboard.set(final)
	browser = 'safari-' + final
	webbrowser.open(browser)
	
if __name__ == '__main__':
  import photos
  img = photos.pick_image()
  console.clear()
  DropPic(img)

The script’s main purpose – taking an image file and putting it in a Dropbox folder – is a function, which means it can be used by other scripts in Pythonista.

Now comes the meaty part. Here’s the script, called “Open In”, that I set to run automatically every time a file is received via Open In from Pythonista:

import console
import sys
import clipboard

sender = sys.argv[2]
#print sys.argv[2]
if 'com.evernote.Skitch' in sender:
	from DropPics import DropPic
	import Image
	opened = Image.open(sys.argv[1], 'r')
	DropPic(opened)
elif 'com.metaclassy.bywordios' in sender:
	import markdown2
	text = open(sys.argv[1], 'r')
	clipboard.set(markdown2.markdown(text.read(), extras=["header-ids", "footnotes", "fenced-code-blocks"]))
elif 'com.getdropbox.Dropbox' in sender:
	import editor
	text = open(sys.argv[1], 'r')
	title = console.input_alert('New Script Name', 'Choose a name for your new script file.')
	editor.make_new_file(title, text.read())
	editor.reload_files()

This script is a handler for files and scripts: it doesn’t do anything on its own and it is, basically, a bridge for Open In.

When Pythonista receives a file via Open In, it saves two bits of information as command line arguments: the path to the new file (now inside the Inbox folder) and the bundle ID of the sender app; these will be, respectively, sys.argv[1] and sys.argv[2]. Bundle IDs, which should be unique to each iOS app, have a reverse-domain style name, such as “com.apple.mobilemail”, which is the bundle ID for Apple’s Mail app.

The Open In script does a series of checks to perform a different action or script depending on the app that sent a file. On line 5, the sender app’s bundle ID is saved as sender, which is the variable that three subsequent conditional blocks will check against.

The first one is run if an image was sent from Skitch. I like Skitch a lot, and I use it to annotate screenshots of beta apps that I’ll later send to developers for feedback. I was constantly saving and uploading screenshots from Skitch to Dropbox; Open In support in Pythonista allows me to make the entire process automated with just a few lines of code.

If “com.evernote.Skitch” is contained in the sender app’s name, Open In will hand off the received file to DropPics, which will upload it to Dropbox and automatically copy the shared URL and open the uploaded image in Safari (lines 10 and 11 open the image file and call the DropPic() function).

The process takes a few seconds to complete and everything is automated: once set up, I don’t have to fiddle with the script, I don’t have to save and pick the image – my handler script takes care of understanding where the file came from and giving it to another script. It almost seems unfair that this can be done on an iOS device.

The second conditional block is run if the sender app isn’t Skitch, but Byword. In that case, Pythonista will know that a text file has been sent, likely formatted in Markdown because that’s how I write in Byword. The markdown2 module is imported, the text file is opened, and the clipboard is automatically set to the HTML output of the text file. That’s four lines of code and it is only run if Byword was the sender app.

The last block runs if the sender app is Dropbox and I use it to create new scripts in Pythonista. Zorn’s app can’t sync its script library across the iPhone and iPad, and I was constantly saving scripts as text files to Dropbox with Drafts, opening them on a second device to copy the text, open Pythonista, and create a new script there as well.

With Open In, every time Pythonista sees that Dropbox sent a file it will read that file’s text and create a new script in the library with it. Lines 20 and 21 create a new file (with a title I manually type in an alert dialog) and reload Pythonista’s sidebar to show it. While I still need to manually upload scripts to Dropbox if I want to share them to a second device, Open In lets me remove the steps needed to manually copy text and create a file.

Here’s a video showing the workflow in action with Skitch and Dropbox.

A quick tip: if you want to know what an app’s bundle ID is, uncomment line 6 and send a file to Pythonista. The sender’s bundle ID will be printed in the console.

There are many possibilities opened by Pythonista’s integration with Open In, received files, and bundle IDs. You could offer multiple choices for the same app – e.g. do you want to upload this Skitch screenshot to Dropbox or manipulate it in Pythonista? You could check for the content or name of a file and run conditional blocks based on those variables – for Python scripts from Dropbox, do this, but for photos, perform this other action. You could build scripts to extract annotations from PDF documents and send them over email, or you could automate the process of saving photos to Evernote by using Open In and the (now better documented) evernote module. Within the limitations of iOS (namely, the Open In menu can’t send multiple files at once), Pythonista’s support for this feature is pretty fantastic and versatile.

Location Module

My readers know that I’ve longed wished for a scriptable iOS app with support for location data from iOS’ GPS module. Pythonista 1.4 gets just that with an aptly-named location module, which provides native access to geo-location services on iOS for addresses and coordinates.

Being able to fetch a device’s current location in a script is a powerful addition that comes with technical concerns: constantly firing up an iPhone’s GPS will consume battery. Thankfully, Zorn built the location module with functions aimed to fetch data and stop the location service soon after that data is saved; every time you’ll be using location, try to put location.get_location between location.start_updates() and location.stop_updates() so that Pythonista will trigger the GPS and turn it off after a few seconds. You’ll see that Pythonista is fetching your current location with the usual GPS icon in the iOS status and, of course, just like any other iOS app, Pythonista will ask for your permission before attempting to use any function of the location module.

Zorn’s implementation of location in Pythonista uses dictionaries for addresses and coordinates, which can be geo-coded (a readable address is turned into coordinates with latitute and longitude keys) or reverse geo-coded (coordinates are turned into a dictionary of possible readable addresses interpreted by iOS). The accuracy of the location module for your area depends entirely on iOS – Pythonista simply “Python-ifies” what iOS already does behind the scenes.

I’ve made an example script for what I’ve always wanted to be able to do in my scripts: fetch my location (coordinates) and present it as a human-readable address alongside a timestamp. The script, called “GetLocation”, can be used as a module in other scripts by calling the getLocation() function.

import console

def getLocation():
	import location
	import datetime
	import time
	"Returns current location and timestamp in readable format"
	location.start_updates()
	time.sleep(3)
	current = location.get_location()
	location.stop_updates()
	address = location.reverse_geocode({'latitude': current['latitude'], 'longitude': current['longitude']})
	loctime = address[0]['Street'] + ', ' + address[0]['City'] + ', ' + address[0]['Country'] + ' on ' + datetime.datetime.fromtimestamp(int(current['timestamp'])).strftime('%A, %d %B %Y at %-I:%M %p')
	return loctime
	
if __name__ == '__main__':
	print getLocation()

Line 8 turns on the GPS, and line 9 pauses the script for three seconds to allow Pythonista to fetch the current location; line 10 saves the current coordinates to a variable, and the GPS is turned off on line 11. Lines 12 and 13 take care of reverse geo-coding the location and constructing a variable that contains the street name, city, country and timestamp based on strftime. The end result is printed in the console.

To demonstrate the usefulness of getLocation, I tweaked a script that Zorn made to showcase Pythonista’s integration with the Camera Roll and Evernote to create a new note with a picture and location + timestamp information in Evernote. As with previous Evernote scripts, you’ll need a developer token to use this in Pythonista.

Pythonista 1.4

Mine is a simple tweak that imports getLocation (from the “GetLocation” script) and that inserts it as a string in a new Evernote note. What’s cool about Zorn’s script is that it properly attaches photos as Evernote resources to a note, which means that they’ll get a thumbnail preview in the app’s note list and they will be tappable in the note editor (for markup and other options).

Like Open In, support for iOS’ geo-location services creates interesting opportunities for Pythonista users. Whether you want to append a location string to a text log, send coordinates to other apps, or email readable addresses to a contact without having to copy and paste anything, Pythonista’s location module is capable and easy to use.

Other Improvements

There are several other changes and enhancements in Pythonista 1.4 worth mentioning.

On the iPad, the app supports external keyboard shortcuts. They are:

  • ⌘R to run a script;
  • ⌘Space to toggle the console;
  • ⌘F to toggle search;
  • ⌘K to clear the console output.

As with other third-party iPad apps, shortcuts are only available in areas of the app where the software keyboard would otherwise be shown.

The webbrowser module has received new functions to add a URL to Reading List and check whether a URL can be launched. The latter, webbrowser.can_open() is especially handy for checking if a third-party app with a registered URL scheme can be opened by Pythonista.

Here’s a simple script called “ReadLater” that, given a URL in the system clipboard, will add it to Safari’s Reading List and that can be easily triggered from a Launch Center Pro action.

import webbrowser
import clipboard

URL = clipboard.get()
webbrowser.add_to_reading_list(URL)

print 'URL saved to Reading List'

I don’t use contact management apps on my iOS devices and most of my contact-related shortcuts live in Launch Center Pro, but Pythonista 1.4 also comes with full support for iOS Contacts. I’m mentioning it in this section because I haven’t found a particular use for it, but this is a terrific addition for users who deal with contacts on their devices and wanted a way to automate the Address Book: you can add and remove individual contacts and groups, do searches, read notes and labels, and even add contacts to existing groups without ever using the Contacts app. Zorn created Person objects in Python to represent people in your contacts list, giving you access to each field associated with them like email, address, birthday, names, and more. It’s a truly impressive implementation and you can read more in the app’s docs.

If you’ve ever wished for a script that talked to you, there is a new text to speech module you can play with.

Pythonista 1.4

I’m happy to tell Dr. Drang that photos.save_image() now has the same behavior as the tap & hold menu on images shown in the console: it’ll save screenshots losslessly. I’ll (finally) write about my new scripts to handle screenshot generation and status bar cleaning in the next few weeks.

The app’s documentation and release notes mention every other minor improvement or fix in Pythonista 1.4. Notably: local sound effects can be played with parameters for pitch and volume; there’s a new option in the Settings to always clear the console before running a script; the auto-completion bubble can be long-tapped on the iPad to view documentation before accepting a suggestion; and, the action menu is directly editable without opening the Settings.

Automating iOS

The whole idea of being able to script and automate iOS still makes some people uneasy, but the truth is – Pythonista users have been able to speed up their iOS workflows for over a year without making too much of a fuss about it. Like it or not, parts of the same Python automation that’s been possible on OS X for years with the Terminal or apps like Keyboard Maestro are now available on iOS. Several aspects are different; others are more limited because of iOS; and yet there are features, like a unified image picker or GPS module, that make automating iOS easier and more fun. For a power user, it’s comforting to know that Python scripts and workflows can be used on iOS when an iPad is the only device available.

Pythonista has always been about automating iOS, leveraging Python to access system functionalities like the Camera, UIKit, web views, and more. With version 1.4, Zorn continues down the road of iOS automation with powerful new integrations for Contacts, Location, and Open In – all while making the app ready for iOS 7 and fixing annoyances and problems of the old Pythonista.

Support for Open In is a particularly handy enhancement in that, in spite of its flaws, it makes dealing with apps and files a much easier and streamlined process. I’m still tweaking my Open In script, trying to understand how I can make tedious tasks faster and invisible to me; it’ll be interesting to see what the Pythonista community will come up with.

Pythonista 1.4 is a great update and it’s available on the App Store.

My iOS Screenshot Generation Workflow

$
0
0

Screenshots

I didn’t think that complaining about iOS status bars on The Prompt would result in Dr. Drang going on a vision quest to produce better screenshots with Python. But I’m glad that I took the time to point out my dislike for messy status bars, because it led the good Doctor to work on some great scripts to automate the entire process with Python, which are compatible with Pythonista for iOS.

I waited to share my workflow for automated screenshot cleaning/generation because I wanted to see where Dr. Drang would end up with his script, Cleanbar. Now that he appears to have settled on a solution that requires standalone image files to act as partial status bar replacements, I think it’s the right time for me to share how I produce iPhone and iPad screenshots for MacStories.

The first step is to set up Cleanbar. I don’t need to repeat what Dr. Drang already explained, but to sum up: grab a black status bar, crop it to get two files similar to Drang’s, then run a script to pack those images as strings. Once set up, you’ll be able to a) use Cleanbar to clean single images picked from the Camera Roll with Pythonista and b) integrate it as cleanbar in other scripts to clean status bars programmatically.

As for my needs:

  • I usually need to combine two iPhone screenshots side-by-side in a single image as you can see in most reviews on this site;
  • I may or may not need to clean their status bars;
  • I don’t have to combine iPad screenshots. I only need to resize them and I may or may not have to clean their status bars;
  • Occasionally, I want to produce a banner with three screenshots (like this one), which can have original or cleaned status bars;
  • Sometimes, I only need to clean one screenshot out of two;
  • I always need to upload the final image to a Dropbox folder, which is monitored by Hazel.

And, because I’m not an animal, I wanted to automate all of this. The scripts that you’ll find below are the result of late night tweaking and lots of tests; they probably aren’t the most elegant or “Pythonic” way to handle this kind of image generation, but they work for me and they make me save several minutes every day. I haven’t been generating review screenshots manually in months, and they’re more flexible than my old workflow based on Keyboard Maestro.

CombineScreens

The main script that I use on a daily basis, CombineScreens lets me clean and combine multiple screenshots in a single image. The status bar cleaning is optional, and the script can handle any number of screenshots, although I rarely use it to generate images containing more than two.

With CombineScreens I can combine two screenshots and get this:

Screenshots

But with status bar cleaning, get this:

Screenshots

Here’s the script:

import Image
import photos
from Cleanbar import cleanbar
import console

# Ask if you want to clean status bars before combining screenshots
mode = console.alert('Create or Clean', 'Select a mode below.', 'Create Now', 'Clean First')

def CombineScreens():
	# Distance from left side of final image
	base = 0
	# Pixels between screenshots in final image
	offset = 14
	# Number of screenshots to combine
	total = 2
	# iPhone 5 resolution
	height= 1136
	width = (640*total) + ((offset*total)-offset)
	# Create image background
	background = Image.new('RGB', (width,height), 'white')
	for i in range(total):
		screenshot = photos.pick_image(show_albums=True)
		if mode == 1:
			background.paste(screenshot,(base,0))
		elif mode == 2:
			cleanbar(screenshot)
			background.paste(screenshot,(base,0))
		base = base + screenshot.size[0] + offset
	background.show()
	# Upload to Dropbox folder
	from WorkPics import WorkPic
	WorkPic(background)

if __name__ == '__main__':
	CombineScreens()

Line 7 asks if you want to clean the status bars of the screenshots or if you want to generate a composite image without cleaning. Line 9 defines the CombineScreens function that can also be called from other scripts and that handles image generation and status bar cleaning.

As MacStories readers know, the final images that I use for reviews have a narrow white separator between screenshots. In the script, this is handled by line 11 and line 13, which define the base distance from the left side of the image for the first screenshot (0 pixels) and the offset for the following screenshots (14 pixels from the preceding screenshot). The value of 14 pixels is completely arbitrary – I picked it because I liked it. You can change it to whatever you want.

Same goes for line 15: it defaults to 2, which is the total number of screenshots I typically need to combine, but it’ll work for any number greater than that. I tried with 3, 6, and even 10 – you’ll end up with a larger image containing lots of screenshots. The script won’t judge.

Screenshots

I debated whether I needed the script to find out the height and width of screenshots passed to it, but I decided that, in my case, I’ll always be throwing iPhone 5 screenshots (1136×640 pixels) at it; therefore, lines 17–18 come with hardcoded integers for height and width – which are also responsible for defining size properties of the final image.

Line 18 multiplies the width of a single screenshot by the number of total screenshots and adds a single offset of 14 pixels (for each screenshots minus the first one) to create a background for the screenshots. If you need to deal with iPhone 4/4s screenshots, you can change the width and height values to what you need. Finally, line 20 creates the image with a white background where screenshots will be pasted upon; if you want a different background color (the one that will be shown between screenshots), change the color name to something else.

Lines 21–29 do the actual screenshot cleaning and combining. On line 21, a loop asks to pick a file for each screenshot (as assigned to the total variable beforehand); if you chose to combine screenshots without cleaning, they will be pasted on the white background. If, on the other hand, you wanted to clean screenshots, Drang’s cleanbar will run on each screenshot and clean its status bar.

The trick to paste multiple screenshots with a 14-pixel separator between them happens on line 28. The base variable needed to be 0 for the first screenshot, but screenshots after that have to be pasted one after the other while keeping a separator. For each screenshot in the loop, the previous base value, the width of the screenshot the loop just processed, and the offset are added to base. This ensures that the first screenshot in the loop will be pasted at the leftmost side of the white background, while the position of others will be incremented by the loop for each screenshot. Line 29 shows the final image in the Console, allowing you to tap & hold it to copy or save it to the Camera Roll as a PNG file.

Lines 31–32 upload the image to a Dropbox folder I configured in the WorkPics script, which is a variation of the DropPics script that I covered when Pythonista 1.4 was released. For context, here’s WorkPics:

from dropboxlogin import get_client
dropbox_client = get_client()
import keychain
import console
import time
import httplib
from io import BytesIO
import datetime
import webbrowser
import urllib
import clipboard
 
today = datetime.datetime.now()
 
def WorkPic(img):
	titles = console.input_alert('Image Upload', 'Enter your image name below')
	console.show_activity()
	buffer = BytesIO()
	img.save(buffer, 'JPEG', quality=100)
	buffer.seek(0)
	imgname = today.strftime("%Y-%m-%d-at-%H-%M-%S") + '-' + titles + '.jpeg'
	response = dropbox_client.put_file('/MacStories_Team/Photos/Ticci/upload-unedited/' + imgname, buffer)
	console.hide_activity()
	print 'Image Uploaded'
	
if __name__ == '__main__':
  import photos
  img = photos.pick_image()
  WorkPic(img)

Note how the file is renamed using a timestamp and how line 19 saves the PNG into a JPEG at full quality. You can change the Dropbox path on line 22 to whatever you need.

I’m happy with how CombineScreens turned out. I like its flexibility, it’s fast, and it allows me to combine multiple screenshots together in just a few seconds without needing a Mac. More importantly, it’s a nice improvement to what I had in November 2012.

Combine 3

Screenshots

I don’t use this script often, but, when I do, the result is pretty sweet. You can see the kind of banner image that Combine 3 creates in posts like this or this.

import Image
import photos
import console
import ImageOps

# Pick screenshots to combine
screenshot1 = photos.pick_image(show_albums=True)
screenshot2 = photos.pick_image(show_albums=True)
screenshot3 = photos.pick_image(show_albums=True)

mode = console.alert('Create or Clean', 'Select a mode below.', 'Create Now', 'Clean First')

if mode == 2:
	from Cleanbar import cleanbar
	cleanbar(screenshot1)
	cleanbar(screenshot2)
	cleanbar(screenshot3)

# Creates final image
console.clear()
print "Creating final image..."
background = Image.new('RGBA', (866,600), (255, 255, 255, 255))
file1 = screenshot1.resize((250,444),Image.ANTIALIAS)
file2 = screenshot2.resize((320,568),Image.ANTIALIAS)
file3 = screenshot3.resize((250,444),Image.ANTIALIAS)

file1 = ImageOps.expand(file1,border=1,fill='gray')
file2 = ImageOps.expand(file2,border=1,fill='gray')
file3 = ImageOps.expand(file3,border=1,fill='gray')

background.paste(file1,(10,77))
background.paste(file2,(272,15))
background.paste(file3,(604,77))

console.hide_activity()
background.show()
print "\n\n Image created"

The script is fairly straightforward. Lines 7–9 get three screenshots from the Camera Roll; lines 13–17 run an optional cleanbar like CombineScreens; lines 22–36 create the final image by resizing screenshots, adding a gray border to them, and pasting them on a white background.

Some points worth noting: the screenshot in the middle is larger than the other two but all of them are resized with Image.ANTIALIAS. The 1-pixel gray border is added through ImageOps and you can change the fill color to anything you want.

Clean iPad

I don’t need to combine iPad screenshots. Hence, my script simply asks how many screenshots I want to resize in a row, and it offers to clean their status bars with cleanbar.

import photos
import Image
import console
from Cleanbar import cleanbar

number = console.input_alert('How many images?')
mode = console.alert('Create or Clean', 'Select a mode below.', 'Create Now', 'Clean First')

for i in range(int(number)):
	screenshot = photos.pick_image(show_albums=True)
	if mode == 1:
		final = screenshot.resize((1024,768),Image.ANTIALIAS)
	elif mode == 2:
		cleanbar(screenshot)
		final = screenshot.resize((1024,768),Image.ANTIALIAS)
	final.show()
	from WorkPics import WorkPic
	WorkPic(final)

The script uses a loop (lines 9–18) to pick screenshots (based on a number entered in line 6), resize them, clean them, and upload them with WorkPics.

Notes

Dr. Drang’s Cleanbar has been a huge help to achieve the status bar cleaning workflow I wanted. It’s not perfect (it picks the color of status bar icons programmatically, so it can’t account for developers who specifically choose one color over another for aesthetic reasons), but it’s the best solution I’ve found to date. Cleanbar works with solid status bars, black and white versions, and it can also fill icons for status bars with translucencies or gradients. The script that I use the most, CombineScreens, handles screenshot selection, status bar cleaning, image generation, and uploading in a function that does everything I need and that can be reused in other scripts.

Like most automation tricks, there’s a lesson to be learned about fiddling. I tweaked my scripts until I was happy with them and I waited for Dr. Drang to find what he thought was the best solution for the job – I invested time in scripts that, now that they’re set up, don’t require me to manage them or fiddle with them anymore. They’ll simply allow me to save time every day and they’ll work both on the iPhone and iPad. Combining screenshots and cleaning status bars are minor annoyances, and I’m glad that I can get rid of them directly on iOS without a computer. Seven years later, this is what you can do on a phone.

Pythonista 1.5: Custom Interfaces, matplotlib, and No More “Open In”

$
0
0

Pythonista 1.5, the latest version of Ole Zorn's Python interpreter for iOS, has been released today on the App Store, bringing new modules, native integrations, UI refinements, and the removal of the Open In feature to comply with Apple's App Store guidelines. Pythonista 1.5 is another fantastic update to one of the most powerful and flexible iOS apps ever made, and it follows in the footsteps of Editorial 1.1, released last month.

No More "Open In"

In November 2013, Zorn released Pythonista 1.4, which, alongside support for iOS Contacts and location settings introduced a scriptable Open In menu that allowed Pythonista to receive files sent from other apps and read the bundle identifier of the app that sent a file to Pythonista.

As I explained in my article, the addition was twofold. First, the Open In menu itself behaved like any other instance of the Open In feature for iOS -- it allowed Pythonista to receive a copy of a file sent from another app:

The premise is that Pythonista can receive files sent through the Open In menu from other apps. Any app that can send an image (like Skitch or iPhoto), a text file (Byword), a PDF document (PDF Expert), or any other file with the native Open In menu can now send it to Pythonista. By default, Pythonista will add a received file to an Inbox folder in the sidebar and show it with QuickLook.

Zorn's version of Open In, however, was clever as it also allowed Pythonista to see the bundle ID of the sender app, which opened up many possibilities for scripted Open In-based communications between apps:

The Open In menu can be automated and scripted with Pythonista 1.4. In the app’s settings, you can choose a script to automatically run when a file is received through Open In, and, in the script, you can reference the file’s path and sender app’s bundle ID as command line arguments. This is a huge addition for automated iOS workflows and chaining apps together – it means that you no longer need to manually pick files because you can use Open In as a script handler in Pythonista. You can run specific scripts automatically depending on the app that sent a file while also using that file without ever needing to manually pick it.

Pythonista's Open In menu was a great addition to the app and a nice way to get around the feature's limitations by creating scripts that behaved differently depending on the app that sent a file to Pythonista. I had scripts that launched an upload routine for files sent from Skitch, and others that posted to WordPress if the file had been sent by a text editor.

Last week, Apple's review team got in touch with Zorn and asked him to submit an update to the app that removed the possibility to "import executable code from other sources". Therefore, Pythonista 1.5 no longer has support for Open In as, in theory, it could be used to import .py files that contain executable Python code.

While I won't dwell on Apple's retrograde stance for third-party programming apps on the App Store in 2014, I think it's important to stress that Pythonista's Open In menu never allowed users to inadvertently execute code stored in other apps. Users had to deliberately open a file, tap Open In, and choose Pythonista as the destination app for the file. User intent was always a key element of the Open In menu, but, clearly, just being able to import files hasn't gone well with Apple seven months after the release of Pythonista 1.4.

Ultimately, the Open In menu was useful, but it's a price I'll gladly pay to continue having Pythonista on the App Store. It removed a lot of friction from processing files with Pythonista, but I can live without it.1

Custom Interfaces

Like Editorial 1.1, Pythonista 1.5 can create custom interfaces using a ui module that can be combined with Python scripts. Modelled after Apple's UIKit, Zorn's ui module is consistent across Editorial and Pythonista, and it lets you design interfaces that are native to iOS with elements such as popovers, sheets, labels, views, buttons, and more.

Because the module is the same, I can include excerpts from my Editorial 1.1 review and point you to the relevant section for more information:

Modelled after Apple's UIKit, ui isn't a complete wrapper for Apple's framework, but it provides basic components for building native user interfaces and it simplifies some aspects of UIKit to make it easy to integrate custom interfaces with Editorial's actions and scripts. Essentially, you can design your own visual workflows and widgets using native iOS UI elements and interact with them using data and actions from Editorial's workflow system. In a way, it's HyperCard for the modern iOS productivity scene, and it's an impressive piece of work by Zorn.

There's an amazing depth to the ui module and the way it's been translated to Python, but it's meant for advanced users who are fluent in Python and know the basics of UIKit. I've tried to read through and learn from the ui documentation, but it's too much for me. While many features and settings for native GUIs are only exposed through Python right now, I've been absolutely fine using what I believe is the truly important, most user-friendly aspect of all this: the visual UI Editor.

The big difference between the ui module in Editorial and Pythonista's version is that Pythonista doesn't have a visual workflow system, which means that every action in a custom interface will have to be called via Python. While Pythonista does have the same UI Editor found in the Editorial with the same settings and management of attributes and views, there is no sub-workflow system to assign actions to specific UI elements. You'll always have to write the code, design the UI (either in the editor or also via code), and then connect the two.

This makes Pythonista's ui module undoubtedly more difficult to approach than Editorial's, but Pythonista is, after all, a Python interpreter for programmers, not a visual workflow app. The relationship between custom UIs and Python scripts will enable advanced users to come up with powerful ideas for the app: beginners will likely stay away from Pythonista's UI Editor, but the interplay with Python is what, I believe, will drive more Python users to Pythonista.

Even without visual workflows, creating custom interfaces for scripts that are better suited for Pythonista isn't that difficult. The redesigned documentation of Pythonista 1.5 makes it easy to read through and understand how custom UIs are called in Python, and the only extra step will be to learn how to manually show UIs and set attributes for their views within a script. It's a moderately steep learning curve, but doable and fun.

Here's an example of a custom interface in Pythonista that lets me turn an image URL from the clipboard into an actual image in my photo library. In the UI Editor, I put together a simple sheet interface that has an empty ImageView and a button.

In Python, I import the clipboard, set up the UI, and define a function to save an image from URL into my local iOS library.

import clipboard
import urllib
import Image
import photos
import cStringIO
import ui

def button_tapped(sender):
	file = Image.open(cStringIO.StringIO(urllib.urlopen(URL).read()))
	photos.save_image(file)
	view.close()

URL = clipboard.get()
view = ui.load_view()
try:
	image = view['imageview1'].load_from_url(URL)
	button = view['button1']
	button.action = button_tapped
	view.present()
except ValueError:
	import console
	console.alert('No Valid URL In The Clipboard')

There are two important blocks in the script -- lines 8-11 and lines 15-19. With a URL variable saved on line 13, line 14 loads the interface file created in the UI Editor, and line 15 tries to set the interface's ImageView to the image URL in the clipboard. If the URL is valid, it will result in a custom UI like this:

What's important about the try block is that, if the clipboard contains text that isn't a URL (which can't be loaded by line 16), an exception will be raised and the script will inform you with an alert dialog that you're trying to load an image from a URL that isn't actually a URL.

Lines 8-11, on the other hand, define the function that will run upon tapping the Save button in the interface. The image URL will be read by Pythonista and written to a file, which will then be saved to the local photo library, closing the custom interface.

This is a basic example of a custom UI that contains a view and a button performing a specific task with a visual preview; because this is Pythonista, you can launch the script from other apps such as Launch Center Pro, automating the process of saving images from URLs without using Safari.

As I wrote when Editorial 1.1 came out, the possibilities opened by custom interfaces connected to Python scripts and native iOS integrations are essentially endless -- and I can't imagine what Zorn will add with the new technologies coming in iOS 8. The ui module in Pythonista is made for advanced users who don't need the visual actions of Editorial, and my same thoughts from Editorial 1.1 apply here. I can't wait to see what Pythonista users create.2

The New Photo Picker

One of my most used Pythonista system integrations -- the native photo picker -- has received several interesting updates in version 1.5 that make it even easier to import your local images into Python scripts.

Alongside new arguments to return raw image data (useful when writing images to disk) and metadata, the photo picker now lets you pick multiple images at once without copying them from the Photos app first.

This is a major improvement for my screenshot generation workflow, as it allows me to skip Apple's Photos app entirely and pick all the screenshots I want to clean directly in Pythonista -- without repeat loops. Here's an updated version of my CombineScreens script.

import Image
import photos
from Cleanbar import cleanbar
import console

# Ask if you want to clean status bars before combining screenshots
mode = console.alert('Create or Clean', 'Select a mode below.', 'Create Now', 'Clean First')

def CombineScreens():
	# Distance from left side of final image
	base = 0
	# Pixels between screenshots in final image
	offset = 14
	# Number of screenshots to combine
	screenshot = photos.pick_image(show_albums=True, multi=True)
	total = len(screenshot)
	# iPhone 5 resolution
	height= 1136
	width = (640*total) + ((offset*total)-offset)
	# Create image background
	background = Image.new('RGB', (width,height), 'white')
	for file in screenshot:
		if mode == 1:
			background.paste(file,(base,0))
		elif mode == 2:
			cleanbar(file)
			background.paste(file,(base,0))
		base = base + file.size[0] + offset
		background.show()
	# Upload to Dropbox folder
	from WorkPics import WorkPic
	WorkPic(background)

if __name__ == '__main__':
	CombineScreens()

I like to use photos.pick_image(show_albums=True, multi=True) in all my scripts that have a photo picker, so I can view all my iOS albums and pick multiple photos in a single action. When picking multiple images, remember that Pythonista will process them from oldest to newest (left to right) based on their position in the photo library.

matplotlib

Pythonista 1.5 also comes with matplotlib, the popular 2D plotting library for turning data sets into charts and other types of visualizations through Python. Alongside NumPy (which has been added to Pythonista as well), matplotlib was one of the most requested libraries by Python users, and it's now available inside the iOS app.

While I don't need matplotlib's features on a daily basis, I thought it'd be fun to learn the basics of the library through a simple chart that plots the number of MacStories articles per month over time. I've only briefly experimented with the functionalities provided by matplotlib on iOS, and I'm including my script as an example of what can be done with just a few lines of code.

Given a string of text in the following format copied in the clipboard, where a tab character separates months and number of posts...

...generated from an Editorial workflow that counts all MacStories posts from the past five years, the following script generates a chart using matplotlib, NumPy, and the xkcd style for matplotlib:

import matplotlib.pyplot as plt
import numpy as np
import clipboard

data = clipboard.get()

lines = data.splitlines()
months = []
posts = []

for line in lines:
	month = line.split('\t')[0]
	post = line.split('\t')[1]
	months.append(month)
	posts.append(post)
	
x = len(months)
x = range(x)
plt.xkcd()
plt.annotate('When I got cancer', xy=(30,100), arrowprops=dict(arrowstyle='->'), xytext=(15,70))
plt.annotate('Kicked its ass', xy=(47,140), arrowprops=dict(arrowstyle='->'), xytext=(35,160))
plt.annotate('iOS 7', xy=(53,140), arrowprops=dict(arrowstyle='->'), xytext=(50,160))
plt.title('MacStories Posts Per Month')
plt.plot(x, posts)
plt.xticks(np.arange(min(x), max(x)+1, 6.0), months[::6], rotation=40, size='xx-small')
plt.show()

The end result looks like this:

Note how matplotlib makes it easy to add annotations, define arrow styles, and set appearance settings for fonts used in labels. Once Pythonista shows the final output, I can simply tap & hold the chart in the Console and save it to my photo library.

Like I said, I've only scratched the surface of what matplotlib makes possible without using dedicated apps, but I want to spend some quality time learning how I could use the library to automate static charts we need for MacStories articles. The addition of matplotlib and NumPy should make iOS an even more palatable portable Python environment for programmers.

Wrap Up

As Pythonista continues to raise the bar of what a programming app can do within the current limitations of iOS 7, Ole Zorn will need to find a balance between advanced features and what Apple thinks is okay for the platform.

With Pythonista 1.5, the removal of the Open In menu is a small compromise, instrumental in keeping the app on the Store and updating it with custom interfaces, matplotlib, NumPy, and plenty of other additions and refinements. While most of my text automation has moved to Editorial, I still rely on Pythonista for some key tasks in my iOS workflow, and I'm looking forward to seeing what Zorn will come up with for iOS 8.

Pythonista 1.5 is available on the App Store.


  1. Xcode export has also been removed from Pythonista 1.5; Zorn has a solution to create Xcode projects from Pythonista files on OS X. 

  2. Zorn's built-in examples for a calculator, color mixer, and drawing "app" are very cool. 

Pythonista 2.0 Brings Action Extension, iPad Pro Support, Code Editor Improvements, and More

$
0
0

Back in the Fall of 2012, a few months after I had taken it upon myself to start moving all my work from OS X to iOS, I came across Ole Zorn's Pythonista. A Python interpreter for iPhone and iPad that could natively integrate with iOS system features, Pythonista opened up a new world to me, demonstrating how I could automate tedious tasks on iOS devices via scripting. Since then, other apps have come along and shown how iOS tasks can be automated with visual interfaces and pre-packaged actions (above all, Workflow and Launch Center Pro), but Pythonista is, in many ways, the crown jewel of iOS automation and scripting for advanced users.

There's nothing quite like Pythonista on iOS. As I've documented over the past three years, Ole Zorn has slowly but steadily extended the app's capabilities with native ties to iOS interfaces via a UIKit bridge, support for location and the Reminders database, and even matplotlib and motion sensors. As it stands today, Pythonista is, by far, the richest and most powerful scripting app to integrate with native iOS features. Despite the variety of options now available for iOS automation and the continued evolution of iOS that cut down the number of tasks I need to automate (case in point: Split View and using two apps at once), I love keeping Pythonista around for key aspects of my workflow that can't be automated in any other way.

For the past several months, I've been using version 2.0 of Pythonista on my iPhone and iPad, which, after a few rejections from Apple, has been approved and is launching today on the App Store. A free update for existing customers, Pythonista 2.0 brings a refreshed UI, support for the iPad Pro, new modules, and, more importantly, a redesigned code editor and an action extension.

Behind the scenes, Pythonista 2.0 has played an essential role in helping me assemble my reviews of iOS 9 and the iPad Pro, with an action extension I rely upon for all my image uploads, OCR, text statistics, and more.

Editor

After more than a year without updates on the App Store, the biggest upgrade in Pythonista 2.0 is support for iOS 9 and the latest iOS screen sizes. Pythonista 2.0 runs properly on the iPad Pro1 with no upscaling, but it doesn't support iOS 9 multitasking; this means you won't be able to edit or run scripts while using the app next to something else.

You can, however, use the app itself in a split mode with the code editor on the left and the console/documentation on the right, which can be useful to write code and look up functions or output without having to swipe on the screen to change views.

Pythonista's new split mode for the code editor.

Pythonista's new split mode for the code editor.

From an editing standpoint, the most notable change is support for tabs to quickly switch between multiple scripts. Just as the console can be divided into multiple tabs for documentation and the web browser, the code editor can now host different scripts so you can move between them while working on a larger project. A tab can be opened by pressing the '+' button in the top toolbar, or you can swipe left on a file in the left sidebar and choose to open it in a new tab.2

Opening new editor tabs in Pythonista 2.0.

Opening new editor tabs in Pythonista 2.0.

In my case, this has been useful to compare different versions of the same script and console output, as well as to work on scripts that imported functions from other scripts stored in sub-folders. If you're running large codebases in Pythonista on iOS, tabs are a handy timesaver.

Generally speaking, Pythonista's code editor has been polished and updated with tons of smaller improvements in this releases. Themes have been refined and extended with more choice and they look better now. The contextual sharing menu (top right) has a new look and it's easier to create actions based on scripts that act on what you're currently editing. When you select something in the editor, a new 'Highlights' option of the copy & paste menu lets you show all entries of the same selection in a script, so you can see where, say, you have to fix a mistake or where a variable is repeated.

Perhaps my favorite touch, though, is support for color previews through a new popup panel. When the cursor is in a color string (either a color value or something like 'red'), the associated color will be previewed in a smaller popup next to it; tap it, and you'll be able to tweak the color and insert its HEX or RGB value back into the script, replacing the color that was automatically selected by the cursor.

Extension

With version 1.5 released in June 2014 and no updates since, Pythonista missed out on the entire iOS 8 extension craze – which Workflow cleverly capitalized on with its powerful action extension. With version 2.0, Zorn is opening up Pythonista to integration with other apps with its own action extension that can be used to run Python scripts anywhere on iOS. I've been using the extension for two key scripts on my iPad and iPhone for the past few months, and it has allowed me to save hours I would have spent doing the same tasks manually each time.

The new Pythonista extension.

The new Pythonista extension.

Pythonista's extension is powered by the appex module, which grants the extension access to input from the system share sheet. While you can run Python scripts from the extension without any appex functions (for instance, if you just want to run a script to access data from the clipboard, you won't need the dedicated appex module), it's only with appex that you'll be able to read input from the share sheet and do something with it. The Pythonista extension works with four data types from the share sheet: text, URLs, images, and VCards. There's also a function to check whether a script is being run from the extension and perform different actions if necessary.

The main idea is that, by integrating the extension with other apps, you'll get the ability to run scripts that act on data shared by apps, such as links and images. While not as advanced as Workflow's automation environment3, Pythonista's extension will let you enjoy the full benefits of Python, bringing, for example, the power of scripts to Apple's Photos app or Safari.

First, you'll have to configure the extension to your needs. The extension is organized in two areas: at the top, you'll find buttons to view and edit all your scripts directly from the share sheet or open an interactive scratchpad and console. I haven't used these much, but it's pretty amazing that you can edit scripts from the extension itself without having to open Pythonista at all.

In the top left, an Edit button will enable you to add shortcuts for existing scripts, which can have a custom icon with a glyph and color of your choice. It's important to remember that scripts can only be pinned to the extension this way – you won't find a "make this script available in the action extension" option in the Pythonista app. If you want, you can also run scripts with custom arguments.

Once your script shortcuts are set up in the extension, you're off to the races. To give you an idea of how I've been using the Pythonista extension in my iOS workflow, I've included some examples below.

Uploading Images

A few readers have asked me to share my image optimization and upload setup after I struggled for weeks to find an ideal solution. Since early 2015, I've been uploading all MacStories images to Kraken for lossy conversion: Kraken works great for us because it's cheap, its lossy algorithm is comparable to the excellent ImageOptim (if not better) and it directly integrates with Rackspace Cloud Files, which we use to serve images via CDN.

Since last year, I've been optimizing and uploading all my images from the iPhone and iPad with a single script run from the Photos app thanks to the Pythonista extension. Because I already use Photos to manage all my photos and screenshots, Pythonista has enabled me to automate what would take ages with a web app (there are no iOS apps that can do what I need natively) – from an app I'm familiar with. This setup is only possible with Pythonista and it is, frankly, amazing for my needs – I've uploaded thousands of images with it. My iOS 9 review, my coverage of the iPad Pro, all my app reviews – if you've seen a screenshot from me on MacStories, it came from the Pythonista extension.

Here are the scripts I use to upload an existing image from Photos to Kraken with lossless settings, and then convert to lossy and mirror to Rackspace Cloud Files:

UploadKraken (from extension)

# coding: utf-8
import requests
import json
import appex
import datetime
import clipboard
import keychain
import photos
import console

timestamp = datetime.datetime.now()
name = timestamp.strftime("%Y-%m-%d-%H%M%S") + '.jpeg'

apiKey = 'YOUR_API_KEY'
apiSecret = 'YOUR_API_SECRET'

params = {
    'auth': {
        'api_key': apiKey,
        'api_secret': apiSecret
    },
    'wait': True,
        "convert": {
    "format": "jpeg"
  }
}

data = json.dumps(params)

if appex.is_running_extension() is True:
    image = appex.get_image_data()
else:
    image = photos.pick_image(original=True, raw_data=True)

print 'Uploading to Kraken...'
console.show_activity()

request = requests.post(
    url = 'http://api.kraken.io/v1/upload',
    files = { 'name': (name, image)},
    data = { 'data': data }
)

response = json.loads(str(request.content))

if (response['success']):
    console.hud_alert('Lossless image uploaded to Kraken.', 'success')
else:
    print 'Fail. Error message: %s ' % response['error']

from Kraken import kraken
final = kraken(response['kraked_url'])

clipboard.set(final)
import urllib, cStringIO, Image

file = cStringIO.StringIO(urllib.urlopen(final).read())
img = Image.open(file)
img.show()

Kraken (as function)

import urllib
import urllib2
import json
import console
import keychain

apiKey = 'YOUR_API_KEY'
apiSecret = 'YOUR_API_SECRET'
rackspaceApi = 'YOUR_RACKSPACE_API_KEY'

def kraken(image_link):
    params = {
    'auth': {
        'api_key': apiKey,
        'api_secret': apiSecret
    },
    'url': image_link,
    'wait': True,
    "cf_store": {
    "user": "macstories",
    "key": rackspaceApi,
    "container": "Kraken",
    "ssl": True
  },
    'lossy': True # Set lossy upload because UploadKraken.py doesn't upload as lossy first. This lets you compare image savings with the original image later.
}

    url = 'https://api.kraken.io/v1/url'

    console.hud_alert('Uploading lossy image to Kraken...', 'success')
    data = json.dumps(params)
    request = urllib2.Request(url, data)
    response = urllib2.urlopen(request)
    jsonRes = json.loads(str(response.read()))

    if (jsonRes['success']):
        print 'Success. Saved ' + str((jsonRes['original_size']-jsonRes['kraked_size'])/1000) + 'kb in total.' + '\n' + 'Optimized image URL: ' + jsonRes['kraked_url']
        return jsonRes['kraked_url']
    else:
        print 'Fail. Error message: %s ' % jsonRes['error']


def main():
    # If not called by other script, get image link from iOS clipboard.
    import clipboard
    image_link = clipboard.get()
    final = kraken(image_link)
    clipboard.set(final)

if __name__ == '__main__':
    main()

As far as the extension is concerned, the key part is lines 30-33 in the first script. Here, the script checks whether it's running from the extension or outside of it (in the main app); if it's running in the extension, it reads raw image data for a single image shared via the share sheet, which is used to prepare the initial upload to Kraken. If the script is not running in the extension (it returns False after the check), then a native image picker is presented, which also returns raw image data (as byte string) after picking an image.

Here's a video of UploadKraken script running in the extension (recorded with Vidyo, may it rest in peace).

As you can see, the extension is able to show console output for a successful upload (though it can't display the network activity spinner of line 36), it shows calculated image savings as returned by Kraken, and, at the end, it presents a preview of the final image upload as fetched from Rackspace Cloud Files.

This script, run with this setup from the extension, is how I've been uploading images used in MacStories articles for almost a year. And since the iPad gained Split View with iOS 9, it became even more convenient for me to double check screenshots, pick the ones I like, and upload them one by one for use in a blog post.

In fact, take a look at how I do it with the video below:

While I'm editing in 1Writer, I can bring up the Photos app in Split View, find the screenshot I need, and start the uploading process with the Pythonista extension. When it's done (it can take a while for heavy screenshots), it'll copy the final image URL in the clipboard so I can find the point in the document where I want to use the image and paste it in.

As someone who's been looking for the perfect image optimization and uploading workflow for years, the combination of Kraken and Pythonista on iOS has saved me time, energy, and, ultimately, money. I love everything about this script and how it runs in the extension.

Automated OCR

The other script I'm regularly using through the action extension is based on Microsoft's Project Oxford, and it lets me perform OCR on images and obtain extracted plain text in seconds.

Writing about apps and the App Store often, I find myself having to take screenshots of the App Store app on iOS, where descriptions for featured sections and pages are not selectable as text. Every time Apple launches a new collection or feature on the App Store, I have to take a screenshot and extract text from it with OCR if I want to quote that text on MacStories. Occasionally, the same problem occurs with webpages or apps outside of the App Store – just as an example, last week I had to extract text from a textshot shared by Jack Dorsey on Twitter.

Over the years, I've used a variety of OCR-powered apps to grab text from images, such as Prizmo. Dedicated apps are great if you routinely have to scan images and grab text from them; however, I may need to extract text from a screenshot once or twice a month, and when I do, it's usually a time-sensitive news piece that I want to publish quickly. Thus, I wanted the process to be fast, automated, and customized to my needs.

Project Oxford is one of the many cool projects by Microsoft these days. Free to use within certain limits right now, among its APIs there's a Computer Vision one with an OCR mode that, as Microsoft describes it, "detects text in an image and extracts the recognized characters into a machine-usable character stream". Using Microsoft's sample code and my own code for image uploads to Kraken, I put together an action extension script that, given an image shared via the share sheet, uploads everything to Project Oxford's servers and returns text in a few seconds.

Here's what the script looks like:

import httplib, urllib, base64, clipboard, json, requests, datetime, appex, photos, console, dialogs

timestamp = datetime.datetime.now()
name = timestamp.strftime("%Y-%m-%d-%H%M%S") + '.jpeg'

apiKey = 'YOUR_API_KEY'
apiSecret = 'YOUR_API_SECRET'

params = {
    'auth': {
        'api_key': apiKey,
        'api_secret': apiSecret
    },
    'wait': True,
        "convert": {
    "format": "jpeg"
  }
}

data = json.dumps(params)

if appex.is_running_extension() is True:
    image = appex.get_image_data()
else:
    image = photos.pick_image(original=True, raw_data=True)

print 'Uploading to Kraken...'
console.show_activity()

request = requests.post(
    url = 'http://api.kraken.io/v1/upload',
    files = { 'name': (name, image)},
    data = { 'data': data }
)

response = json.loads(str(request.content))

if (response['success']):
    console.hud_alert('Lossless image uploaded to Kraken.', 'success')
    final = response['kraked_url']
else:
    print 'Fail. Error message: %s ' % response['error']

urlOcr = '/vision/v1/ocr'

headers = {
    # Request headers
    'User-Agent': 'python',
    'Ocp-Apim-Subscription-Key': 'YOUR_PROJECT_OXFORD_KEY',
}

params2 = urllib.urlencode({
    # Request parameters
    'language': 'en',
    'detectOrientation': 'true',
})

body = {
    "Url": final,
}

print 'Performing OCR...'

body = json.dumps(body)

conn = httplib.HTTPSConnection('api.projectoxford.ai')
conn.request("POST", urlOcr, body, headers)
response = conn.getresponse()
back = response.read()
conn.close()

print 'OCR successfully performed.'
data = json.loads(back)

s = ''

for item in data["regions"]:
    for line in item["lines"]:
        for word in line["words"]:
            s += ' ' + word["text"]

print s
dialogs.share_text(s)

And here's the script in action on my iPad:

The first half of the script (lines 1-42) is my Kraken script adapted to put an image on Kraken's CDN and return its public URL. Project Oxford requires an image to be available at a URL so it can fetch it and analyze text contained inside it; for convenience, I use Kraken without mirroring to Cloud Files because I don't need these images to be in the same CDN where I keep images for MacStories. Like with the Kraken script, you'll need a Project Oxford API key to run this with your credentials.

The actual processing happens in lines 66-70, where the script posts the image URL to Project Oxford and gets a response back. In my experience, Project Oxford's OCR API has been extremely fast with images where text is laid out with good contrast against its background – often faster than native apps which perform OCR locally on an iOS device. Results are also accurate 90% of the time – I'm impressed with Microsoft's OCR and I wouldn't be surprised to see more developers implementing it soon in their own apps.

In lines 73-83, the script assembles text from the response by combining words from each line from each region of text found in an image. Finally, extracted text is printed in the console and passed to the iOS share sheet via the dialogs module.

This script is another example of the flexibility of Pythonista's new extension. With two taps, I can run an OCR script for any image in the Photos app, allowing me to extract text via an extension that lives alongside my photos, without forcing me to open Pythonista or another separate app.

Converting HTML to Markdown

The third script I've been using both via the action extension and in combination with Workflow lets me convert HTML text to Markdown via html2text.

# coding: utf-8
import sys
import html2text
import clipboard
import webbrowser
import appex
import console

h = html2text.HTML2Text()
h.body_width = 0


if appex.is_running_extension() is True:
    text = appex.get_text()
    converted = h.handle(text)
    clipboard.set(converted)
    console.hud_alert('HTML Converted to Markdown', 'success')
else:
    webpage = clipboard.get()
    text = sys.argv[1]
    converted = h.handle(text)
    clipboard.set('> ' + converted)
    webbrowser.open('safari-' + webpage)

I use this script in two ways. If I want to create a new linked post for MacStories, I run this workflow to get text selected from a webpage as HTML, send it to Pythonista to turn it into Markdown, return to the original webpage and create a linked post draft in 1Writer. This has been working well for me – now, I create every linked post with this Workflow-Pythonista combination. Even better: thanks to iOS 9.2, action extensions based on webpage selections work in both Safari and Safari View Controller.

If I don't need to create a linked post, I can run the same script to convert HTML text passed from the Pythonista extension to Markdown and copy it to the clipboard. This is useful because apps tend to only offer Markdown to HTML conversion; with the extension, I can enjoy the power of html2text anywhere, bringing Python to any app that doesn't offer the functionality I'd like it to have.

Pythonista 2.0

There are dozens of other new features in Pythonista 2.0 I didn't cover. The scene module has been completely revamped and it now allows the creation of even more complex games and 2D animations that can even utilize OpenGL fragment shaders. HTML, CSS, JavaScript, and Markdown files are syntax-highlighted in the editor and most files support Quick Look previews, while .zip archives can be extracted and a new Template menu lets you quickly create different types of files or import images from Photos. For advanced users, it's now possible to take a look at the pure Python code of the Standard Library directly in Pythonista, and there's a whole new traceback navigator to understand errors in scripts.

Three years later and now at version 2.0, Pythonista is still the app testing the unexplored possibilities of iOS. Nothing else on the platform gets close to the raw power and professional feature set of Pythonista, which has been skillfully engineered to work for Python novices like me interested in basic automation but also longtime Python coders, educators, developers who want to prototype their UIs quickly with Python, or kids learning their way around programming with games and animations.

Pythonista embodies the very idea of a post-PC app: reimagined for touch, deeply integrated with the mobile OS, and just as capable as traditional desktop alternatives – if not more powerful because of its relationship with iOS.

Pythonista 2.0 is a fantastic update, with a few omissions I'd like to see rectified in the future. Split View and Slide Over should be enabled to run Pythonista alongside other apps in some way4; the action extension should support more types of content with new functions, most notably webpage selections in Safari and documents shared by apps; eventually, it'd be interesting to see what a widget or a custom keyboard could do to extend Pythonista beyond the app and the action extension.

Pythonista 2.0 is an amazing programming app uniquely designed for iOS. For the work that I do on my iPad and iPhone, I need the combination of Workflow and Pythonista now more than ever. When the visual automation of Workflow doesn't cut it, or when I find myself wishing for more control and personalization, I know I can fire up Pythonista and build whatever I want with it. Pythonista is the kind of app that Apple should celebrate and welcome on the platform with open arms.

Pythonista 2.0 is available on the App Store at $9.99.


  1. The iPad Pro's software keyboard shines in apps like Pythonista where easier access to special characters and numbers can save a lot of time during editing. 
  2. I would like to have both split mode for the editor and this tab behavior for files in Zorn's other app, Editorial for iOS. 
  3. Workflow's action extension truly is on another level now: it works with any file type and it can be integrated with hundreds of existing actions. Also, Pythonista's extension can't launch the web browser (or other URL schemes) from the action extension, but Workflow's can. 
  4. Essentially, Zorn made the call of disabling Split View to prevent rotation issues for game development in Pythonista. I understand the reasoning behind it, but as someone who doesn't develop games in Pythonista, I can only imagine how nice it'd be to run the app next to Safari and StackOverflow threads. 

Pythonista 3.2 Syncs Scripts with iCloud, Supports Open-in-Place via iOS 11’s Files App

$
0
0

For a long time, Apple's App Store review guidelines prohibited apps from downloading executable code from the Internet. The company's original stance resulted in IDEs that couldn't sync scripts and programs across multiple devices – a serious limitation for the emergent movement of programmers embracing the iPad Pro as a portable workstation.

Fortunately, Apple started relaxing their rules earlier this year, allowing "apps designed to teach, develop, or test executable code" to download and run code. Pythonista, the popular Python IDE for iOS (and one of the best pro apps for iOS, period), has been updated this week with the ability to sync scripts with iCloud and edit external scripts in-place using Files' document picker.

As someone who used Pythonista heavily for years and remembers previous rejections based on old App Store guidelines, this is fantastic news. I moved my existing script library to iCloud, which means all my code is now shared between the iPhone and iPad – no workarounds required. Pythonista now supports the iPhone X and drag and drop for importing scripts, but, even better, the app can open scripts and edit them in-place (saving changes back to the original location) just by opening them with the built-in Files picker. This feature makes it possible to, say, use Pythonista as an editor for script files stored in GitHub repositories and managed by Working Copy – all entirely on iOS, and natively integrated with Files.

Version 3.2 of Pythonista gets rid of the most annoying limitations imposed by the old Apple, another sign that the company's approach to professional iOS software has changed over the years. While I don't use Pythonista nearly as much as I did a few years ago (you can imagine why), I plan on playing around with Pythonista 3.2 over the next couple of weeks.

→ Source: itunes.apple.com

Viewing all 20 articles
Browse latest View live