Emacs on an iPad

With my iPad having replaced my MacBook, the last thing I was missing was Emacs, which I not only enjoy hacking on, but is also how update my blog and resume! Let’s just say that it’s an integral tool in my life which I wanted access to, and I managed to make it happen.

So first off, you cannot run Emacs directly on an iPad as an app, but can certainly access something else which is running it. In this case a Virtual Private Server (VPS) over SSH, such as one might setup on DigitalOcean. Cloud hosts have plenty of information on how to deploy a VPS, so I won’t repeat it here. These instructions are for an Ubuntu 20.04 machine.

I think the app Blink is more than adequate as a terminal emulator (with 24 bit color support, which is important!) with SSH access and a number of useful supporting tools (like ssh-agent).

I used the app’s settings (either type config or hit CMD-,) to generate a new SSH key pair and add my VPS as a host named vps with its IPv6 address for the host name. Before connecting I first open a tab and start ssh-agent, then I open a second tab and run ssh-add id_rsa (no, not the path to the key, just the key’s default name, which the app’s port of the tools just understands), and finally connect with the forwarded agent via ssh -A vps.

In the app’s keyboard settings I set “Option” to send “Esc” (which is “Alt” for terminals and “Meta” for Emacs, as in the M in M-x). In my system settings I have my hardware keyboard’s “Caps Lock” key set to act as “Control,” but Blink can do this in software as well.

Install the Basics

From here I first installed some helpful tools:

sudo apt install git stow ripgrep aspell htop

Secure it with SSHGuard

Installing SSHGuard is still very useful to mitigate connection attempts to an open VPS:

sudo apt install sshguard
sudo journalctl -u sshguard

Once I saw how many connection attempts were being made (but all failing because I only have key access setup, no passwords), I added a firewall rule on DigitalOcean to deny all incoming traffic except port 22 (SSH) on IPv6. This allows me to access it because I know its IPv6 address, but since that address space is so huge it really can’t be found by bots scanning it.

Add a Swap File

Now before attempting to compile anything from source on a VPS with limited memory, I like to setup a swap file to prevent silly out of memory errors. I think I followed this guide on Linuxize. In short:

sudo swapon --show
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
sudo swapon --show
sudo cp /etc/fstab /etc/fstab.bak
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Build Emacs from Source

Ok, now the fun part: building Emacs 27 from source, for the terminal, with everything we need. First we clone the emacs-27 branch from the GitHub mirror:

git clone -b emacs-27 https://github.com/emacs-mirror/emacs.git
cd emacs

Now we install the myriad of necessary packages (I think this was everything I could find in my bash_history after I got things working). This includes the normal build tools, TeX for the documentation, ncurses since this is a terminal build, GNU TLS for secure connections when installing packages, Jansson JSON library for LSP, XML library, zlib library, and finally the systemd library because we’ll use the unit file to daemonize Emacs.

sudo apt install build-essential autoconf pkg-config texinfo \
     libncurses-dev libgnutls28-dev libjansson-dev \
     libxml2-dev libz-dev libsystemd-dev

Now hopefully you can configure the build. The first step uses autoconf (installed above) to generate the configure file, which is then used to generate the Makefile. The configuration options explicitly disable X since, again, this is a terminal build, enable the features outlined above, and set the image support to be enabled opportunistically instead of required.

./configure --with-x-toolkit=no --with-gnutls --with-json --with-xml2 --with-libsystemd  \
            --with-jpeg=ifavailable --with-png=ifavailable --with-gif=ifavailable \
            --with-tiff=ifavailable --with-xpm=ifavailable

Finally we can launch a parallel build. I like to connect in another Blink tab and launch htop to watch the pretty colors as the core usage is reported. When it’s done, install it!

make -j $(nproc) all info doc
sudo make -j $(nproc) install

Daemonize Emacs

This is why we installed and configured the systemd library support. I always use Emacs in server mode so that I can simply edit files with emacsclient (which I alias to simply e) as fast as vi users. Enable and start the user-scoped unit file that Emacs installed, and check the log to see if it worked. It should start Emacs with --fg-daemon, and hopefully your own init.el is setup such that this works.

systemctl --user enable --now emacs
journalctl --user-unit emacs

Fix the SSH Agent

So one problem shows up when using Emacs as a daemon under systemd: your SSH environment isn’t used which means the forwarded SSH agent cannot be found. I fixed this by adding the following code to my ~/.bashrc to opportunistically find the socket file and set SSH_AUTH_SOCK appropriately.

Note that I don’t actually have this in ~/.bashrc but in an environment setup file which I ensure is always sourced by Bash, that way my interactive shell setup is kept separate. But that’s for another blog post.

# Find SSH agent socket file
ssh_sock_file=$(compgen -G "/tmp/ssh-*/agent.*" | head -n 1)
if [[ -z $SSH_AUTH_SOCK && -S $ssh_sock_file ]]; then
    export SSH_AUTH_SOCK=$ssh_sock_file

Then I used the ever helpful exec-path-from-shell Emacs package to set this environment variable within Emacs when it starts.

(use-package exec-path-from-shell
  :if (daemonp)
  (add-to-list 'exec-path-from-shell-variables "SSH_AUTH_SOCK")

Add 24-bit Support to Terminal

Okay so now we need to setup 24-bit colors because otherwise my favorite Emacs theme, Solarized, has a bright blue background! It’s an ancient “bug” and frankly will never be fixed, but 24-bit terminal support is a viable workaround. Besides, it’s prettier that way.

The official Emacs FAQ has an entry, Colors on a TTY, with detailed instructions to enable “direct color mode” in the terminal. Unfortunately, the instructions as currently posted do not work with Blink, or at least not how I’ve set it up. Interestingly, a prior version of the instructions (where the semicolons were replaced with colons), does work, so that’s what I adapted and used. All this “terminfo” stuff is rather archaic, and I haven’t found a satisfactory answer as to why it’s broken and why what I did worked, so if you have any insight, I’d love to hear it here.

Like in the instructions, create a file named terminfo-custom.src with the following content (where I’ve removed the line breaks and replaced escaped colons \: with semicolons ;, and deleted the repeated colon after “2”):

# See Emacs FAQ "Colors on a TTY" but sub ; for :
xterm-emacs|xterm with 24-bit direct color mode for Emacs,

This is a file with “terminfo” (terminal info) which we then compile to the user terminfo database with tic:

tic -x -o ~/.terminfo terminfo-custom.src

Now we can set TERM=xterm-emacs and launch Emacs with full 24-bit color support, which means the Solarized theme works!

Some keys that won’t work

There are a number of Emacs bindings that simply cannot be typed in a terminal because it’s restricted to the ASCII character set. They all seem to be control prefixes, perhaps it’s something to do with control characters used by terminals. Frankly I don’t know, the implementation of TTYs is arcane to me.

Some of these quite annoying, if you have a fix, please let me know!


In the “Custom Presses” section at the bottom of Blink’s keyboard settings I added “^_” (“Control-Space”) to send exactly that, “Control-Space,” used often in Emacs for marking. This is a bit of a workaround because iPadOS otherwise intercepts that keyboard combination and uselessly opens the Emoji keyboard (or usefully opens your other language’s keyboards), and another workaround is to remove all other keyboards. Unfortunately, even with these workarounds it doesn’t seem to “persist” through key chords so I have to let go of control between keys in a chord. That is, C-SPC C-p with control held prints “p” but C-SPC, release control, C-p works as expected.

Interestingly, C-2 comes through as C-@, one of the alternative default bindings for set-mark-command, which I previously never understood as it was difficult to type. It’s a viable replacement for C-SPC since the shift key does not actually have to be pressed, and it makes me wonder if this was the case with old-school TTYs as well, ergo its existence.

C-x C-;

The key combination C-; is not a valid character in a terminal, so it is baffling that it is used as a default binding. When using Emacs in a terminal typing C-x C-; mis-translates to C-x ; which calls comment-set-column, a confusing situation indeed! Instead of finding a new mapping, I just select the line and use M-;.

C-<1,2,3,...> (any number)

This one sure is awful. Again a prevalent default (used as one option for numeric prefixes), it also drops control and just inserts the digit. Fortunately, M-<1,2,3...> all work as expected.


I have this bound to the ever helpful expand-region command, but it doesn’t work either, the control is dropped.


I’m happily blogging away in Emacs on my iPad now. In fact, two last notes about Blink: it supports SSH tunnels and copying files!

For Hugo, I connect with ssh -A -L 1313:localhost:1313 vps (tunneling my iPad’s port 1313 to the VPS’s port 1313), and then launch hugo server which listens on that port by default. I can then see my blog in Safari at: http://localhost:1313

I’m also successfully hacking on Python with fast LSP support, and even edited my LaTeX resume. You can run link-files in a separate Blink tab which opens a dialog to link an iPad or iCloud folder to the app (like your Desktop). Copying a file using Blink is a bit arduous. The SCP port is actually curl underneath, and using IPv6 with that is tricky. This seemed hacky but worked:

curl --insecure -v -u <VPS username> "scp://[VPS IPv6 address]/~/path/to/file.pdf" -o Desktop/file.pdf

Let me know how your experience goes!