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 yourAlfred.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
.
#!/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.