├── .gitignore ├── README.md └── TE.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Convert TextExpander snippets to Keyboard Maestro macros 2 | 3 | Simple batch convert of all your Snippets to Macros. 4 | 5 | ### Usage 6 | 7 | Download the script and read the comments and make any changed before running the script with 8 | 9 | `$ python TE.py` 10 | 11 | ### Notes 12 | 13 | This script will copy the snippets as they are, but doesn't convert any of the fancy features (fillPopups etc) from TextExpander automatically. 14 | 15 | So if TextExpander you have a snippet where it prompts you with a dropdown lise so: 16 | 17 | `@include breakpoint(%fillpopup:name=size:$small:$medium:default=$large:$xlarge%) {%|}` 18 | 19 | Then that will be what Keyboard Maestro will paste (or type depending on your settings). 20 | 21 | The fillpop won't work, so be sure to go through manually if you have fancy snippets. 22 | 23 | ## Insert cursor here 24 | 25 | Included a find and replace for plaintext snippets allowing the "place cursor here" to work in Keyboard Maestro. The string in TextExpander to place cursor is `%|` and in Keyboard Maestro it is `%|%` 26 | 27 | Although the find and replace would work with the other snippet types it woudln't work if you opt to have the snippet typed rather than pasted. 28 | 29 | If you don't then replace 30 | 31 | `'Text': text,` 32 | 33 | with 34 | 35 | `'Text': text.replace("%|", "%|%", 1),` 36 | 37 | You may want to do a find and replace for `%clipboard` replace with `%PastClipboard%1%` if you use "insert clipboard". 38 | 39 | You can also drag your Settings.textexpandersettings file into Sublime and do a find and replace on the whole folder to replace TextExpander variables with the equivalent for Keyboard Maestro. 40 | 41 | 42 | -------------------------------------------------------------------------------- /TE.py: -------------------------------------------------------------------------------- 1 | import plistlib 2 | import os 3 | import glob 4 | 5 | ''' 6 | This script will parse through all group_*.xml files within your TextExpander folder. 7 | Anything marked as Plain Text, Shell Script or JavaScript should be converted into 8 | Keyboard Maestro groups with the same title and abbreviation. 9 | 10 | All new KM Macro files will be saved to the Desktop. 11 | 12 | ''' 13 | 14 | # Modify this area to customize how the script will run 15 | 16 | # Change this path to where ever your TextExander Settings live 17 | HOME = os.path.expanduser('~') 18 | TEXTEXPANDER_PATH = HOME + '/Dropbox/TextExpander/Settings.textexpandersettings' 19 | SAVE_PATH = HOME + '/Desktop/TextExpander_to_KeyboardMaestro' 20 | 21 | # Change this if you'd like to change your snippets when importing to Keyboard Maestro 22 | # If your snippet is ttest, you can make it ;;ttest by changing the variable to ';;' 23 | OPTIONAL_NEW_PREFIX = '' 24 | 25 | # Change this if you want the snippet to inserted by typing or pasting 26 | # Remember it MUST be 'paste' or 'type' or the script will fail 27 | PASTE_OR_TYPE = 'paste' # 'type' 28 | 29 | 30 | 31 | 32 | ############ 33 | 34 | # Edit below at your own risk 35 | 36 | ############ 37 | 38 | snippet_types = { 39 | 'plaintext': 0, 40 | 'applescript': 2, 41 | 'shell': 3, 42 | 'javascript': 4, 43 | } 44 | 45 | snippet_types_to_values = dict((value, key) for key, value in snippet_types.iteritems()) 46 | 47 | 48 | class KeyboardMaestroMacros(object): 49 | @classmethod 50 | def macro_by_name(cls, macro_name, group_name, name, text, abbreviation): 51 | return getattr(cls, macro_name)(group_name, name, text, abbreviation) 52 | 53 | @staticmethod 54 | def javascript(group_name, name, text, abbreviation): 55 | return { 56 | 'Activate': 'Normal', 57 | 'CreationDate': 0.0, 58 | 'IsActive': True, 59 | 'Macros': [ 60 | {'Actions': [ 61 | {'DisplayKind': KeyboardMaestroMacros._paste_or_type(), 62 | 'IncludeStdErr': True, 63 | 'IsActive': True, 64 | 'IsDisclosed': True, 65 | 'MacroActionType': 'ExecuteJavaScriptForAutomation', 66 | 'Path': '', 67 | 'Text': text, 68 | 'TimeOutAbortsMacro': True, 69 | 'TrimResults': True, 70 | 'TrimResultsNew': True, 71 | 'UseText': True}, { 72 | 'IsActive': True, 73 | 'IsDisclosed': True, 74 | 'MacroActionType': 'DeletePastClipboard', 75 | 'PastExpression': '0'} 76 | ], 77 | 'CreationDate': 482018934.65354, 78 | 'IsActive': True, 79 | 'ModificationDate': 482018953.856014, 80 | 'Name': name, 81 | 'Triggers': [{ 82 | 'Case': 'Exact', 83 | 'DiacriticalsMatter': True, 84 | 'MacroTriggerType': 'TypedString', 85 | 'OnlyAfterWordBreak': False, 86 | 'SimulateDeletes': True, 87 | 'TypedString': KeyboardMaestroMacros._abbreviation(abbreviation)}]} 88 | ], 89 | 'Name': 'Snippet - %s' % group_name, 90 | } 91 | 92 | @staticmethod 93 | def applescript(group_name, name, text, abbreviation): 94 | return { 95 | 'Activate': 'Normal', 96 | 'CreationDate': 0.0, 97 | 'IsActive': True, 98 | 'Macros': [ 99 | {'Actions': [ 100 | {'DisplayKind': KeyboardMaestroMacros._paste_or_type(), 101 | 'IncludeStdErr': True, 102 | 'IsActive': True, 103 | 'IsDisclosed': True, 104 | 'MacroActionType': 'ExecuteAppleScript', 105 | 'Path': '', 106 | 'Text': text, 107 | 'TimeOutAbortsMacro': True, 108 | 'TrimResults': True, 109 | 'TrimResultsNew': True, 110 | 'UseText': True}, { 111 | 'IsActive': True, 112 | 'IsDisclosed': True, 113 | 'MacroActionType': 'DeletePastClipboard', 114 | 'PastExpression': '0'} 115 | ], 116 | 'CreationDate': 482018934.65354, 117 | 'IsActive': True, 118 | 'ModificationDate': 482018953.856014, 119 | 'Name': name, 120 | 'Triggers': [{ 121 | 'Case': 'Exact', 122 | 'DiacriticalsMatter': True, 123 | 'MacroTriggerType': 'TypedString', 124 | 'OnlyAfterWordBreak': False, 125 | 'SimulateDeletes': True, 126 | 'TypedString': KeyboardMaestroMacros._abbreviation(abbreviation)}]} 127 | ], 128 | 'Name': 'Snippet - %s' % group_name, 129 | } 130 | 131 | @staticmethod 132 | def plaintext(group_name, name, text, abbreviation): 133 | return { 134 | 'Activate': 'Normal', 135 | 'CreationDate': 0.0, 136 | 'IsActive': True, 137 | 'Macros': [{'Actions': [ 138 | { 139 | 'Action': KeyboardMaestroMacros._paste_or_type('plaintext'), 140 | 'IsActive': True, 141 | 'IsDisclosed': True, 142 | 'MacroActionType': 'InsertText', 143 | 'Paste': True, 144 | 'Text': text.replace("%|", "%|%", 1)}, { 145 | 'IsActive': True, 146 | 'IsDisclosed': True, 147 | 'MacroActionType': 'DeletePastClipboard', 148 | 'PastExpression': '0' 149 | }], 150 | 'CreationDate': 0.0, 151 | 'IsActive': True, 152 | 'ModificationDate': 482031702.132113, 153 | 'Name': name, 154 | 'Triggers': [{ 155 | 'Case': 'Exact', 156 | 'DiacriticalsMatter': True, 157 | 'MacroTriggerType': 'TypedString', 158 | 'OnlyAfterWordBreak': False, 159 | 'SimulateDeletes': True, 160 | 'TypedString': KeyboardMaestroMacros._abbreviation(abbreviation)}], 161 | }], 162 | 'Name': 'Snippet - %s' % group_name, 163 | } 164 | 165 | @staticmethod 166 | def shell(group_name, name, text, abbreviation): 167 | return { 168 | 'Activate': 'Normal', 169 | 'CreationDate': 0.0, 170 | 'IsActive': True, 171 | 'Macros': [{ 172 | 'Actions': [{ 173 | 'DisplayKind': KeyboardMaestroMacros._paste_or_type(), 174 | 'IncludeStdErr': True, 175 | 'IsActive': True, 176 | 'IsDisclosed': True, 177 | 'MacroActionType': 'ExecuteShellScript', 178 | 'Path': '', 179 | 'Text': text, 180 | 'TimeOutAbortsMacro': True, 181 | 'TrimResults': True, 182 | 'TrimResultsNew': True, 183 | 'UseText': True}, 184 | {'IsActive': True, 185 | 'IsDisclosed': True, 186 | 'MacroActionType': 'DeletePastClipboard', 187 | 'PastExpression': '0'}], 188 | 'CreationDate': 482018896.698121, 189 | 'IsActive': True, 190 | 'ModificationDate': 482020783.300151, 191 | 'Name': name, 192 | 'Triggers': [{ 193 | 'Case': 'Exact', 194 | 'DiacriticalsMatter': True, 195 | 'MacroTriggerType': 'TypedString', 196 | 'OnlyAfterWordBreak': False, 197 | 'SimulateDeletes': True, 198 | 'TypedString': KeyboardMaestroMacros._abbreviation(abbreviation)}], 199 | }], 200 | 'Name': 'Snippet - %s' % group_name, 201 | } 202 | 203 | @staticmethod 204 | def _abbreviation(name): 205 | return OPTIONAL_NEW_PREFIX + name 206 | 207 | @staticmethod 208 | def _paste_or_type(snippet_type=None): 209 | value = { 210 | 'paste': "Pasting", 211 | 'type': "Typing" 212 | } 213 | if snippet_type == 'plaintext': 214 | return "By%s" % value[PASTE_OR_TYPE] 215 | else: 216 | return value[PASTE_OR_TYPE] 217 | 218 | 219 | def parse_textexpander(): 220 | ''' 221 | Each TextExpander group is its own file starting with the file name 'group_'. 222 | 223 | Example snippet dictionary 224 | { 225 | 'abbreviation': '.bimg', 226 | 'abbreviationMode': 0, 227 | 'creationDate': datetime.datetime(2013, 5, 19, 19, 42, 16), 228 | 'label': '', 229 | 'modificationDate': datetime.datetime(2015, 1, 10, 20, 19, 59), 230 | 'plainText': 'some text, 231 | 'snippetType': 3, 232 | 'uuidString': '100F8D1F-A2D1-4313-8B55-EFD504AE7894' 233 | } 234 | 235 | Return a list of dictionaries where the keys are the name of the group 236 | ''' 237 | to_ret = {} 238 | 239 | # Let's get all the xml group files in the directory 240 | xml_files = [f for f in glob.glob(TEXTEXPANDER_PATH + "/*.xml") 241 | if f.startswith(TEXTEXPANDER_PATH + "/group_")] 242 | 243 | for xml_file in xml_files: 244 | pl = plistlib.readPlist(xml_file) 245 | if pl['name'] not in to_ret: 246 | to_ret[pl['name']] = [] 247 | for snippet in pl['snippetPlists']: 248 | if snippet['snippetType'] in snippet_types.values(): 249 | to_ret[pl['name']].append(snippet) 250 | return to_ret 251 | 252 | 253 | def main(): 254 | text_expanders = parse_textexpander() 255 | for group, text_expander in text_expanders.iteritems(): 256 | macros_to_create = [] 257 | for snippet in text_expander: 258 | macros_to_create.append( 259 | KeyboardMaestroMacros.macro_by_name(snippet_types_to_values[snippet['snippetType']], 260 | group, 261 | snippet['label'], 262 | snippet['plainText'], 263 | snippet['abbreviation']) 264 | ) 265 | 266 | # Create a new folder on the desktop to put the macros 267 | if not os.path.exists(SAVE_PATH): 268 | os.mkdir(SAVE_PATH) 269 | # Save the macros 270 | with open(SAVE_PATH + '/%s.kmmacros' % group, 'w') as f: 271 | f.write(plistlib.writePlistToString(macros_to_create)) 272 | 273 | if __name__ == '__main__': 274 | main() 275 | --------------------------------------------------------------------------------