LaTeX and Textastic

When your audience is mostly Apple users, the evening after a WWDC keynote address is probably not the best time to post anything that isn’t about the day’s announcements. But I have one more thing to say about writing in LaTeX on the iPad.

A couple of days ago, I described a Drafts action and Shortcut that allow me to compile my LaTeX files on my Mac from my iPad. This is great if I’m writing in Drafts, but what if I’m using another editor? Specifically, what if I’m using Textastic, which has good integration with Dropbox and LaTeX syntax highlighting? If my report has complex formatting, that highlighting makes it nicer to write LaTeX in Textastic than in Drafts.1

Unfortunately, Textastic isn’t scriptable, so I can’t write an action for it the way I could with Drafts. But it does have a way of passing text to a Shortcut, so with a little trickery I can get around its limitations.

Here’s a file in Textastic with the commands needed to compile the LaTeX file over an SSH connection:

SSH build script in Textastic

These are the same commands I used in the PDFLaTeX shortcut from Saturday’s post:

bash:
export PATH=$PATH:/Library/TeX/texbin
cd ~/Dropbox/projects/test/report
latexmk -pdf -interaction=nonstopmode report

The only difference is that the directory (~/Dropbox/projects/test/report) and file name (report.tex, but latexmk doesn’t need the extension) are given explicitly instead of as dictionary look-ups. As you can see, they’re saved in a file named build.sh, but the name doesn’t really matter. And although the commands will be run on a Macintosh, where report.tex is saved, build.sh doesn’t have to be saved there, certainly not in the same folder as the report. It can be a local file on your iPad or saved in the Textastic folder of iCloud. What’s important is that you can put it in a tab in Textastic, because we’re going to send its contents to a Shortcut.

And here’s the Shortcut:

Run As Shell Script shortcut

All it does is log on to my Mac and run the commands that are passed into it as text. That text comes from build.sh.

When I’m writing in Textastic, I keep the LaTeX source for the report in one tab and build.sh in another. To compile my LaTeX file, I tab over to build.sh and tap on the Sharing button in the top right corner. This brings down a menu with options for processing the text in the active file.

Sending text from Textastic to Shortcuts

Most of them are Textastic-specific, but the Open In… option hands the text off to the standard iPad share sheet.

Sending build text to standard Share Sheet

From here, I tap Shortcuts, then Run As Shell Script, and voila!. Shortcuts opens an SSH connection to my Mac, runs the three commands in build.sh, and returns to Textastic. On my Mac is a newly built PDF of the report.

This is not nearly as convenient as pressing ⇧⌘P in Drafts—in fact, it requires five taps to compile and another to get back to the report file. But it is a way to write in Textastic without having to keep a Prompt connection to my Mac as an active app in Split Screen.


  1. I’m hoping Greg Pierce adds a syntax highlighting engine to Drafts that understands LaTeX (and Python and Bash and …). I think it’s on his Someday list, but I don’t know how many other things are ahead of it. 


A little more LaTeX

While I’m on the topic of LaTeX, I might as well throw in another post on how I’m writing my reports for work nowadays. It’s based strongly on TextExpander, because that’s the best way I know to make automations that work on both macOS and iOS.

Although my writing workflow does change occasionally, it tends to stay stable for years at a time. In the mid-90s, after learning HTML, I began using a system based on SGML and troff. That lasted until 2000 or so, when I started writing in LaTeX. After discovering Markdown (and MultiMarkdown), I began using a Markdown-to-LaTeX-to-paper workflow that changed to Markdown-to-LaTeX-to-PDF when my client base got a little more computer savvy. This system served me well for 10–12 years, starting in 2005. I wrote about it here, including a fun diagram.

A few years ago, though, I found myself writing more reports that needed extra care in formatting. I would start by writing in Markdown, as before, but too often had to edit the intermediate LaTeX to get the pages of the PDF looking the way I wanted. I decided it was best to just switch back to writing LaTeX directly. Coincidentally, this happened at about the time I bought my 9.7″ iPad Pro and wanted to start writing on it.

For the bulk of a report, writing in LaTeX isn’t much different from writing in Markdown. You’re basically just writing in plain text, with blank lines separating the paragraphs. The big differences come at the beginning of the text, where LaTeX requires a long header, and in the inclusion of figures, where LaTeX requires a lot of formatting to get the figures where you want them.1

Luckily, this extra writing needed by LaTeX is mostly boilerplate, and there’s a good system for automating the insertion of boilerplate on both the Mac and the iPad: TextExpander. I went through some of my LaTeX source files, pulled out the text fragments that I use frequently or are verbose, and built a set of TextExpander snippets with them. If you’re a LaTeX user, you can download them.

Fair warning: the longest snippet, the one that generates the LaTeX header, won’t work as-is for you because it relies on a style file (not included) that’s heavily customized for my company. But you can edit it to fit your needs.

LaTeX TextExpander snippets

One thing you’ll notice right away is that the abbreviations start with ll. That’s how the whole set is defined.

LaTeX snippet prefix definition

All of my snippets use a double letter prefix on the home row, and ll is a natural for LaTeX.

Many of the snippets are self-explanatory, but some are worth discussing, especially those for figures.

The llfig snippet is sort of the baseline snippet for figures that aren’t special in any particular way. The snippet definition is

\begin{figure}[htbp]
\begin{center}
\includegraphics[width=5.5in]{%clipboard%fillpopup:name=extension:default=.pdf:.jpg%}
\caption{%|}
\label{%clipboard}
\end{center}
\end{figure}

and it looks like this when invoked:

Figure fill-in snippet

It assumes the name of the figure file (sans extension) is on the clipboard and uses that in both the \includegraphics command and as the figure’s label for reference in the text. It provides a popup button to select whether the figure is a PDF or a JPEG, and it puts the cursor between the braces of the \caption command. I used to have a fill-in field for the caption, but I find it generally more convenient to type in a real text editor than in a field in a dialog box.

The [width=5.5in] specifier is something I often change after the fact to make for a better looking page. I don’t define it as a fill-in field because I don’t know the best width until I’ve seen the rendered page.

The llland snippet is specifically for figures that are photographs taken in landscape orientation. It’s similar to the llfig snippet, except the image will always be a JPEG and the size of the graphic is set to a height of 3.25 inches. This height works well for fitting two photos on a page.

\begin{figure}[htbp]
\begin{center}
\includegraphics[height=3.25in]{%clipboard.jpg}
\caption{%| (%clipboard).}
\label{%clipboard}
\end{center}
\end{figure}

Note also that the clipboard, which is supposed to be the filename sans extension when the snippet is invoked, is put in parentheses after the caption. I do this because I usually have dozens of photos associated with a project, and this makes it easier to identify the which photos are in the report.

The llport snippet is for photographs taken in landscape orientation. It’s more complicated than the llland, because I want the caption to show up next to the photo, not below it. It accomplishes this by creating two minipage environments, one on the left for the photo and one on the right for the caption.

\begin{figure}[htbp]
\begin{center}
\begin{minipage}[c]{0.55\linewidth}
\includegraphics[height=4.25in]{%clipboard.jpg}
\end{minipage}\hfill
\begin{minipage}[c]{0.40\linewidth}
\caption{%| (%clipboard).}
\label{%clipboard}
\end{minipage}
\end{center}
\end{figure}

Like the llland snippet, this allows two photos to fit together on the same page. Both can be landscape, both portrait, or one of each.

Example of landscape and portrait photographs

Finally, there is the llsfig snippet, which is just like llfig except that it uses the sidewaysfigure float defined in the rotating package. As you can guess from the name, this meant for large, wide figures that are best displayed rotated 90° on a page of their own.

\begin{sidewaysfigure}
\begin{center}
\includegraphics[width=8in]{%clipboard%fillpopup:name=extension:default=.pdf:.jpg%}
\end{center}
\hspace{1.5in}\parbox{6in}{\caption{%|}\label{%clipboard}}
\end{sidewaysfigure}

I wrote about using sidewaysfigure over a decade ago. Time flies.

All these snippets assume that the clipboard contains the filename without the extension because it’s easier on both platforms to copy the filename without the extension than with it. On a Mac, two slow clicks (not a double click) on a file in the Finder selects the filename without the extension, so a quick ⌘C puts that onto the clipboard. On an iPad, choosing Rename from a file’s popup menu in the Dropbox app puts the cursor just before the dot that starts the extension, so it’s easy to type ⇧⌘← to select and ⌘C to copy. Using the Files app is similar.

There are some compromises in these snippets that I wouldn’t have made back when I did all my writing on Macs. In those days, I would have avoided contaminating the clipboard and used AppleScript instead to get the name of the selected file in the Finder. But I want the portability of the iPad and consistency across platforms, so the small loss in productivity is worth it.


  1. This formatting was also required in my Markdown-LaTeX-PDF workflow, but it was handled by the XSLT that converted from Markdown to LaTeX. 


LaTeX via Drafts

As I said in my last post, I often write reports on both Mac and iPad, switching between the two according to whichever is more convenient to work on. As the writing progresses, the text is kept in both Drafts and a file in Dropbox, which are kept in sync by the Drafts actions described in that post. My reports are written in LaTeX, which have to be compiled into a PDF. Until recently, I did the compilation either directly on a Mac via commands entered in a Terminal window or indirectly on a Mac via commands entered into Prompt, the iOS app that lets me connect to my Macs via SSH. But this week, I created a Drafts Action/Shortcut hybrid that allows me to bypass Prompt and compile LaTeX files from Drafts.

I’ve never set up a similar workflow on my Mac to allow me to compile LaTeX from within BBEdit, because I’ve never seen much advantage in doing so. On a Mac, I can always have a Terminal window ready to run the compilation command. On an iPad, doing the equivalent with Prompt isn’t as easy. Unless Prompt is an active app, it will time out after a few minutes—this is an iOS thing, something the folks at Panic can’t get around. Which means if I’m writing in Drafts (or any editor, for that matter), running the LaTeX compilation command in Prompt is a juggling act. I either have to keep Prompt open as the other app in Split View, which means I have to periodically replace Drafts in Split View with Safari, Dropbox, Books, or whatever apps I’m refering to as I write, or I have to relaunch Prompt, log in, and cd to the right directory every time I want to compile to see how the report looks.

But no more. With the PDFLaTeX action, I can keep Drafts in one pane, the research app in the other, and compile with a menu selection or keystroke combination.

The action has two steps: a Script step that figures out the directory and filename and a Run Shortcut step that sends that information to a Shortcut that connects to my computer and compiles the LaTeX into a PDF.

Here’s the script:

javascript:
 1:  // Get the folder and filename from the dbox line.
 2:  var d = draft.content;
 3:  
 4:  // Regex for fileline.
 5:  var fileRE = /dbox:(.+)$/m;
 6:  
 7:  if (fileRE.test(d)) {
 8:    // Get the path. Add a slash to the front if I forgot.
 9:    var fileline = d.match(fileRE);
10:    var flStart = d.search(fileRE);
11:    var path = fileline[1];
12:    if (path[0] != '/') {
13:      path = '/' + path;
14:      editor.setTextInRange(flStart + 5, 0, '/');
15:    }
16:    // Split the path into directory and filename and make a dictionary.
17:     parts = path.split("/")
18:     var fileinfo = {"directory":"~/Dropbox" + parts.slice(0, -1).join("/"),
19:                 "filename":parts.slice(-1)[0]};
20:    
21:    // Create a template tag from the dictionary.
22:    draft.setTemplateTag("fileinfo", JSON.stringify(fileinfo))
23:   
24:  } else {
25:    alert("No dbox line");
26:  }

Recall that I put a dbox line in my drafts that gives the path to the Dropbox file where the draft is also saved. In a LaTeX file, the dbox line is a comment that looks like this:

% dbox:/projects/test/report/report.tex

Most of the script has been recycled from my earlier script that saves a draft to Dropbox. You can go to that post for the explanation of how the dbox line is parsed. I’ll concentrate here on the new stuff.

Line 17 splits the path into a list of its components: subdirectories and the filename. Lines 18–19 then creates a JSON dictionary. The directory item is the reconstructed path, starting with the Dropbox folder in my home folder. The filename item is exactly what you think it is.

Line 22 creates a template tag named fileinfo for this draft. The value of the tag is the string form of the JSON dictionary created in Lines 18–19. We’ll use this tag in the next step to pass information to a Shortcut.

The Run Shortcut step looks like this:

Run Shortcut step

There’s not much to it. The Shortcut it runs is also named PDFLaTeX, and the text passed to it is the value of the fileinfo tag created in Line 22 of the script above.

Here’s how the PDFLaTeX shortcut is defined:

PDFLaTeX shortcut

The first step parses the text passed to the shortcut into a dictionary. The second step logs into my computer via SSH and runs the following three commands

export PATH=$PATH:/Library/TeX/texbin
cd <Dictionary:directory>
latexmk -pdf -interaction=nonstopmode <Dictionary:filename>

where the things inside the angled bracket are magic variables that extract the given items from the dictionary created in the first step.

For reasons I don’t quite understand, SSHing into my computer through a Shortcut isn’t exactly like doing it via Prompt.1 Most significantly, the PATH environment variable doesn’t include the directory where the various TeX/LaTeX commands are kept. So the purpose of the first line is to add that directory to the PATH.

The second line changes to the directory where the LaTeX file is saved, and the third line does the compilation via latexmk.

The latexmk command is a Perl script that gets installed along with MacTeX. It’s job is to automate the compilation of LaTex files into the final output. If you’re a LaTeX user, you know that generating output often requires two or more steps as bibliographies get built and floating figures move from one page to another; latexmk figures out which commands need to be run and runs them as often as necessary until everything is in its final state. The -pdf option tells latexmk to generate a PDF and the -interaction=nonstopmode option tells it to keep going even if there’s an error in the source files.

I have the action bound to the ⇧⌘P keystroke combination. When I run it, Shortcuts appears on the screen while the shorcut runs. If the compilation is successful, Shortcuts disappears and the screen goes back to Drafts and the research app. If there’s a LaTeX error—a misspelled command or an unclosed brace—Shortcuts stays on the screen displaying the error message.

PDFLaTeX error message

As it stands, the error message displayed is pretty worthless. The full error message from latexmk, which has what we need, is simply too long to fit in the dialog box and the good part gets lost in the truncation. I may try to extract the actual error from the log file and display that instead.

Even with a crummy error message, this is better than juggling apps or relaunching Prompt.


  1. I suspect it has something to do with whether or not I’m logging in with an interactive shell. I could probably avoid this line if I had a better understanding of how and when bash’s various dotfiles are invoked. But the PATH of least resistance (I slay me) was to update the environment variable. 


Drafts and Dropbox redux

About a year ago, I wrote a post about using something I called filelines (akin to Vim’s modelines) and Drafts actions to save drafts to Dropbox. The little two-step action I wrote back then served me well for a long time, but I’ve recently updated it and created a reverse action for pulling files from Dropbox and putting their text into drafts.

The motivation for this is simple: although I do a lot of writing entirely in Drafts, text often needs to be saved in files before it can be used. And with longer pieces, like the reports I do for work, I often switch between working on my iPad and my Macs.1

The key to going back and forth between drafts and files is the fileline, a comment line that tells Drafts where that draft is saved as a file in Dropbox. In LaTeX, a fileline takes this form:

% dbox:/projects/ritchie.condo/report/report.tex

Similarly in Python:

# dbox:/projects/thompson.bridge/analysis/stringer.py

Basically, a fileline is dbox: followed by the POSIX-style path to the file, starting at the root of the Dropbox folder.

The action that saves a draft to the file specified in the fileline has just one step, this script:

javascript:
 1:  // Get Dropbox path from dbox: file line in draft
 2:  // and save the draft to that file.
 3:  
 4:  var d = draft.content;
 5:  
 6:  // Regex for fileline.
 7:  var fileRE = /dbox:(.+)$/m;
 8:  
 9:  if (fileRE.test(d)) {
10:    // Set the path. Add a slash to the front if I forgot.
11:    var fileline = d.match(fileRE);
12:    var flStart = d.search(fileRE);
13:    var path = fileline[1];
14:    if (path[0] != '/') {
15:      path = '/' + path;
16:      editor.setTextInRange(flStart + 5, 0, '/');
17:    }
18:    // Save the draft to the specified file.
19:    var db = Dropbox.create();
20:    var check = db.write(path, draft.content, "overwrite");
21:    if (!check) {
22:      alert("Couldn't save to " + path);
23:    }
24:  } else {
25:    alert("No dbox line");
26:  }

The basics of the script are simple; most of the code is there to handle mistakes I might make in creating the fileline.

Line 4 puts the content of the current draft into the variable d. Line 7 defines the regular expression used to find the fileline and extract the path from it.

The if/else block that starts on Line 9 tests whether there is a fileline in the draft. If there is, it does what we’ll talk about in the next few paragraphs; if not, it puts up an alert box (Line 25) warning me.

Assuming a fileline is present, Lines 11 and 12 get the matching text and its starting location in the draft. The starting location is the character position of the “d” in “dbox.” Line 13 then pulls out the part of the matching text corresponding to the parentheses in the regex and saves it to the path variable.

Lines 14–17 handle the case where I forget that the Dropbox path has to start with a slash. I have trouble remembering this because the path is relative to the Dropbox folder, and relative paths in Unix don’t start with a slash. But the Drafts scripting commands for communicating with Dropbox (which we’ll get to soon) require the leading slash. So if I forget, these lines add the leading slash to the path (Line 15) and add the slash to the fileline (Line 16).

Now we’re in the endgame. Line 19 creates the connection to Dropbox, and Line 20 saves the content of the draft (which may have been edited in Line 16) to the file defined by path. If something bad happens during the save, Line 22 tells me.

This action is called Save to Dropbox Fileline and I’ve given it the keyboard shortcut ⇧⌘S. I suppose I could have gone with just ⌘S, but that seemed presumptuous when I first wrote this action (too important a shortcut for my little script), and I haven’t changed it.

I use the reverse action when I started writing something on my iPad, wrote some more on the Mac, and then want to continue writing on the iPad. When I open the draft on my iPad, it won’t have the additions made on the Mac. But the fileline is there, so I can read in the text of the updated file from Dropbox and replace the draft with it. The action that does it, Reload from Dropbox Fileline (⇧⌘R), has a single step consisting of this script:

javascript:
 1:  // Get Dropbox path from dbox: file line in draft
 2:  // and replace draft with that file.
 3:  
 4:  var d = draft.content;
 5:  
 6:  // Regex for fileline.
 7:  var fileRE = /dbox:(.+)$/m;
 8:  
 9:  if (fileRE.test(d)) {
10:    // Set the path. Add a slash to the front if I forgot.
11:    var fileline = d.match(fileRE);
12:    var path = fileline[1];
13:    if (path[0] != '/') {
14:      path = '/' + path;
15:    }
16:    // Download file and replace the current draft.
17:    var db = Dropbox.create();
18:    var txt = db.read(path);
19:    editor.setText(txt);
20:  } else {
21:    alert("No dbox line");
22:  }

Most of the reloading script is the same as the saving script. The part that handles the possibility of my forgetting to include the leading slash in the path is simpler, because editing the draft to add it is of no value—the draft is going to be overwritten by the contents of the Dropbox file.2

Lines 18 and 19 are new. Line 18 reads the content of the file defined by path into the variable txt. Line 19 then replaces the content of the current draft with txt.

There are, of course, text editors for iOS that handle the syncing of files to Dropbox (or iCloud) automatically. If I could get as much functionality out of them as I can from Drafts, I would switch. But where can I go? I’ve had trouble with persistent scripting bugs in Editorial and it seems like abandonware now. 1Writer sounds like it has good scripting support, but when I tried it a year or two ago it just kept crashing (I probably should give it another look).

At the moment, Drafts is the only text editor I can bend to meet my needs, including reading and writing to files.


  1. The recent release of Drafts for the Mac has certainly been helpful in switching between platforms, but until the Mac Drafts feature set is on par with that of iOS Drafts, I’ll continue to do most of my writing on the Mac in BBEdit. 

  2. Given that I’ve already saved the draft to Dropbox—and my saving script adds the leading slash—the condition tested in Line 13 should never occur. But I’ve kept the test in anyway, because things that should never occur sometimes do.