Sync macOS Shortcuts to Alfred Snippets

Python script to sync macOS/iOS system shortcuts to an Alfred snippet collection.

The script syncs your macOS shortcuts as defined in “System Preferences > Keyboard > Text” to an Alfred snippets collection.

As the script syncs macOS snippets to Alfred, it will remove all other snippets, so don't put anything else in that collection.

Why?

To have your iOS snippets work properly on your Mac. macOS snippets don't work everywhere and aren't as simple to use as Alfred ones.

Usage

You can run the script from wherever, but unless you run it from your Alfred snippets directory, you'll at least need to set the SNIPPETS_DIR environent variable (see below), so Alfred sees the snippets.

Configuration

The script has two options, set by environment variables:

SNIPPET_DIR (default: .)
Directory your Alfred snippets are in. In most cases, this should be the path to the snippets subdirectory of your Alfred.alfredpreferences bundle. The default location (i.e. you aren't syncing your Alfred preferences) would be ~/Library/Application Support/Alfred/Alfred.alfredpreferences/snippets.
COLLECTION_NAME (default: macOS)
Name of Alfred snippet collection to sync macOS shortcuts to

The script will create Alfred snippets in directory $SNIPPET_DIR/$COLLECTION_NAME.

shortcuts2alfred.py
view raw
#!/usr/bin/python
# encoding: utf-8
#
# Copyright (c) 2020 Dean Jackson <deanishe@deanishe.net>
# MIT Licence. See http://opensource.org/licenses/MIT
#
# Created on 2020-06-22
#

"""Convert macOS text shortcuts to Alfred snippets."""

from __future__ import print_function, absolute_import

from collections import namedtuple
import csv
from io import BytesIO
import json
import os
from os.path import expanduser, join, realpath
from subprocess import check_output
import sys


# directory to save snippets to
SNIPPET_DIR = os.getenv('SNIPPET_DIR') or '.'
COLLECTION_NAME = os.getenv('COLLECTION_NAME') or 'macOS'


DBPATH = expanduser('~/Library/KeyboardServices/TextReplacements.db')
QUERY = """
select ZUNIQUENAME, ZSHORTCUT, ZPHRASE
    from ZTEXTREPLACEMENTENTRY
    where ZWASDELETED = 0;
"""


Shortcut = namedtuple('Shortcut', 'uid keyword snippet')


def log(s, *args, **kwargs):
    """Log to STDERR."""
    if args:
        s = s % args
    elif kwargs:
        s = s % kwargs

    print(s, file=sys.stderr)


def load_shortcuts():
    """Read shortcuts from system SQLite database."""
    output = check_output(['/usr/bin/sqlite3', '-csv', DBPATH, QUERY])

    reader = csv.reader(BytesIO(output), delimiter=',', quotechar='"')
    shortcuts = []
    for row in reader:
        if len(row) == 3:
            sc = Shortcut(*[s.decode('utf-8') for s in row])
            if sc.keyword == sc.snippet:  # ignore do-nothing shortcuts
                continue
            shortcuts.append(sc)

    return shortcuts


def shortcut_to_snippet(shortcut):
    """Create Alfred snippet dict from macOS shortcut."""
    return {
        'alfredsnippet': {
            'snippet': shortcut.snippet,
            'uid': shortcut.uid,
            'name': shortcut.keyword,
            'keyword': shortcut.keyword,
        }
    }


def safename(s):
    """Make filesystem-safe name."""
    for c in ('/', ':'):
        s = s.replace(c, '-')
    return s


def export_shortcuts(shortcuts, dirpath):
    """Save macOS shortcuts to directory as Alfred snippets."""
    log('exporting snippets to %r ...', dirpath)
    if not os.path.exists(dirpath):
        os.makedirs(dirpath, 0700)

    # remove existing snippets
    for name in os.listdir(dirpath):
        if name.endswith('.json'):
            os.unlink(os.path.join(dirpath, name))

    for i, sc in enumerate(shortcuts):
        name = u'%s [%s].json' % (safename(sc.keyword), sc.uid)
        path = join(dirpath, name.encode('utf-8'))
        log('[%d/%d] saving snippet %r to %r ...', i+1, len(shortcuts), sc.keyword, path)
        with open(path, 'wb') as fp:
            json.dump(shortcut_to_snippet(sc), fp, indent=2, separators=(',', ': '))


def main():
    """Run script."""
    shortcuts = load_shortcuts()
    log('loaded %d macOS shortcut(s)', len(shortcuts))
    dirpath = realpath(expanduser(join(SNIPPET_DIR, COLLECTION_NAME)))
    export_shortcuts(shortcuts, dirpath)


if __name__ == '__main__':
    main()

Inspired by this thread on the Alfred forums.

Downloads

Typo
3f1f2ae