diff --git a/.gitignore b/.gitignore index 3bdfd76..d14f96d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ ._* *.py[cod] *~ +pipe.alfredworkflow diff --git a/LICENSE b/LICENSE index da56212..3ca5e73 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 Robin Breathe +Copyright (c) 2013-2017 Robin Breathe Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..491d1d6 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +all: + zip -j9 --filesync pipe.alfredworkflow *.{json,plist,png,py} diff --git a/README.md b/README.md index 0c1c617..f1d82d5 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,48 @@ -# pipe workflow by isometry +# pipe transformation workflow for Alfred -A workflow for [Alfred](http://www.alfredapp.com/) to transform the currently selected text or the contents of the clipboard by passing it through an arbitrary shell one-liner. +An [Alfred](http://www.alfredapp.com/) workflow enabling easy transformation of the current contents of the clipboard by piping through arbitrary shell one-liners. ## Requirements - [Alfred](http://www.alfredapp.com/) (version 2.0+) - The [Alfred Powerpack](http://www.alfredapp.com/powerpack/). -- [pipe.alfredworkflow](https://raw.github.com/isometry/alfredworkflows/master/pipe.alfredworkflow) ## Usage -(Optional) assign hotkeys for the two Hotkey handlers in the workflow. I recommend `Cmd+Shift+|` and `Cmd+Ctrl+\`, respectively. +Trigger the workflow by hotkey or keyword (default=`|`, override with the `keyword` variable) followed by an arbitrarily simple or complex shell one-liner to transform the contents of the clipboard in-place; optionally use the `Cmd`-modifier to immediately paste the results into the foreground app. -Two actions are available, both taking an arbitrarily complex shell pipe as their argument: +### Examples -1. triggered by the first hotkey or by the `|` or `pipe` keywords, will transform the clipboard in-place by passing its contents through the pipe given as argument. -2. triggered by the second hotkey, will transform the currently selected text in-place by passing its contents through the pipe given as argument. +- Transform to UPPERCASE: `| perl -nle 'print uc'` or `| tr a-z A-Z` +- Base64 encode: `| base64` +- Base64 decode: `| base64 --decode` +- Top 10 unique lines with counts: `| sort | uniq -c | sort -rn | head -10` -A number of built-in pipelines are [included](https://raw.github.com/isometry/alfredworkflows/net.isometry.alfred.pipe/builtins.json), and custom aliases can also be defined with the following syntax: +### Built-ins -`| alias NAME=PIPE | LINE@@` +A number of example pipelines (including those above) are [built-in](https://github.com/isometry/alfred-pipe/raw/master/builtins.json). -`| alias tac=sed '1!G;h;$!d'@@` +Built-ins can be disabled en-mass by setting the `load_builtins` variable to any value other than `yes`. -## Contributions & Thanks +### Aliases -- ctwise +To save repetitive typing, custom aliases can be defined with the following syntax: + +`| alias NAME=PIPE | LINE @@@` + +The trailing `@@@` (override with the `alias_terminator` variable) terminates the alias definition and causes it to be saved. -## License +#### Examples -(The MIT License) +- `| alias tac=sed '1!G;h;$!d' @@@` +- `| alias top10=sort | uniq -c | sort -rn | head -10 @@@` -Copyright (c) 2013 Robin Breathe +#### Alias removal -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: +Any custom alias can be removed with: -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +`| alias NAME=@@@` -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +## Contributions & Thanks + +- ctwise diff --git a/alfred.py b/alfred.py index 6e71f13..fe484a8 100644 --- a/alfred.py +++ b/alfred.py @@ -64,8 +64,8 @@ def unescape(query, characters=None): def work(volatile): path = { - True: os.getenv('alfred_workflow_cache'), - False: os.getenv('alfred_workflow_data') + True: os.getenv('alfred_workflow_cache', os.getenv('TMPDIR', '/tmp')), + False: os.getenv('alfred_workflow_data', os.getenv('HOME', '/tmp')) }[bool(volatile)] return _create(os.path.expanduser(path)) diff --git a/builtins.json b/builtins.json index 5567007..8a8481e 100644 --- a/builtins.json +++ b/builtins.json @@ -1,6 +1,6 @@ { - "openssl base64 -d": "decode base64", - "openssl base64 -e": "encode base64", + "base64": "encode base64", + "base64 --decode": "decode base64", "openssl enc -d -base64 -aes256 -k X": "AES-256 decrypt with passphrase 'X'", "openssl enc -e -base64 -aes256 -k X": "AES-256 encrypt with passphrase 'X'", "openssl x509 -noout -fingerprint": "x509 fingerprint", diff --git a/info.plist b/info.plist index 8e09a81..02d78e8 100644 --- a/info.plist +++ b/info.plist @@ -4,31 +4,47 @@ bundleid net.isometry.alfred.pipe + category + Tools connections - 64D17591-D71C-421B-B701-91E99A3C0318 + 16039760-F173-4AB8-9C73-DA7401D5DE23 destinationuid - D029EE08-8642-404B-83FE-BB5DFEE9C6B6 + 1D296119-4F9D-4FDC-9777-1DA7CF655D19 modifiers 0 modifiersubtext + vitoclose + - 74681B4B-CBE4-403F-BDF7-39F4D2319ABD + 1D296119-4F9D-4FDC-9777-1DA7CF655D19 destinationuid - 16039760-F173-4AB8-9C73-DA7401D5DE23 + 9B8A5712-D806-47E3-9045-EC31AB0C6485 + modifiers + 0 + modifiersubtext + + vitoclose + + + + destinationuid + CB1F4AD1-CC6A-4CE9-9953-5BA92C4B2634 modifiers 0 modifiersubtext + vitoclose + - 880D9394-A59A-4BC1-BB46-01461CEFEE80 + 74681B4B-CBE4-403F-BDF7-39F4D2319ABD destinationuid @@ -37,20 +53,34 @@ 0 modifiersubtext + vitoclose + + + + destinationuid + D4DF549F-4556-4AA6-BF1A-159154EB3051 + modifiers + 1048576 + modifiersubtext + Paste on completion + vitoclose + - CCEE8787-6024-46B4-B155-240F86561281 + 9B8A5712-D806-47E3-9045-EC31AB0C6485 destinationuid - E8AC122D-DD51-491F-A734-C89446EB1924 + 8E5E1774-FB5B-47C7-A0E0-AE958708254F modifiers 0 modifiersubtext + vitoclose + - D029EE08-8642-404B-83FE-BB5DFEE9C6B6 + CB1F4AD1-CC6A-4CE9-9953-5BA92C4B2634 destinationuid @@ -59,6 +89,21 @@ 0 modifiersubtext + vitoclose + + + + D4DF549F-4556-4AA6-BF1A-159154EB3051 + + + destinationuid + 16039760-F173-4AB8-9C73-DA7401D5DE23 + modifiers + 0 + modifiersubtext + + vitoclose + D637E280-521E-432F-85C1-3F58E064A66E @@ -70,6 +115,8 @@ 0 modifiersubtext + vitoclose + @@ -83,6 +130,42 @@ pipe objects + + config + + autopaste + + clipboardtext + {query} + transient + + + type + alfred.workflow.output.clipboard + uid + 8E5E1774-FB5B-47C7-A0E0-AE958708254F + version + 2 + + + config + + inputstring + {var:paste} + matchcasesensitive + + matchmode + 1 + matchstring + 1 + + type + alfred.workflow.utility.filter + uid + 9B8A5712-D806-47E3-9045-EC31AB0C6485 + version + 1 + config @@ -90,38 +173,67 @@ 0 argument 0 + focusedappvariable + + focusedappvariablename + hotkey - 42 + 11 hotmod - 1179648 + 1310720 hotstring - | + X leftcursor modsmode 0 + relatedAppsMode + 0 type alfred.workflow.trigger.hotkey uid D637E280-521E-432F-85C1-3F58E064A66E + version + 2 config + alfredfiltersresults + + alfredfiltersresultsmatchmode + 0 + argumenttrimmode + 0 argumenttype 0 escaping 0 keyword - | + {var:keyword} + queuedelaycustom + 1 + queuedelayimmediatelyinitially + + queuedelaymode + 0 + queuemode + 1 + runningsubtext + Processing… script - from pipe import complete -print complete("""{query}""") + + scriptargtype + 1 + scriptfile + pipe.py + subtext + Enter one-liner or alias title Pipe clipboard through one-liner type - 3 + 8 withspace @@ -129,192 +241,212 @@ print complete("""{query}""") alfred.workflow.input.scriptfilter uid 74681B4B-CBE4-403F-BDF7-39F4D2319ABD + version + 2 config - autopaste - - clipboardtext - {query} - - type - alfred.workflow.output.clipboard - uid - 0FCF109F-A181-4D53-A4DB-DB54DA7440B8 - - - config - + concurrently + escaping 0 script pbpaste | {query} + scriptargtype + 0 + scriptfile + type 5 type alfred.workflow.action.script uid - D029EE08-8642-404B-83FE-BB5DFEE9C6B6 + 16039760-F173-4AB8-9C73-DA7401D5DE23 + version + 2 config - - action - 0 - argument - 1 - hotkey - 42 - hotmod - 1310720 - hotstring - \ - leftcursor - - modsmode - 0 - + type - alfred.workflow.trigger.hotkey + alfred.workflow.utility.junction uid - CCEE8787-6024-46B4-B155-240F86561281 + 1D296119-4F9D-4FDC-9777-1DA7CF655D19 + version + 1 config - applescript - on alfred_script(q) - tell application "Alfred 2" to search "|| " -end alfred_script - cachescript + autopaste + clipboardtext + {query} + transient + type - alfred.workflow.action.applescript + alfred.workflow.output.clipboard uid - E8AC122D-DD51-491F-A734-C89446EB1924 + 0FCF109F-A181-4D53-A4DB-DB54DA7440B8 + version + 2 config - argumenttype - 0 - escaping - 0 - keyword - || - script - from pipe import complete -print complete("""{query}""") - title - Pipe clipboard through one-liner and paste - type - 3 - withspace - + argument + {query} + variables + + paste + 1 + type - alfred.workflow.input.scriptfilter + alfred.workflow.utility.argument uid - 64D17591-D71C-421B-B701-91E99A3C0318 + D4DF549F-4556-4AA6-BF1A-159154EB3051 + version + 1 config - argumenttype - 0 - escaping - 0 - keyword - pipe - script - from pipe import complete -print complete("""{query}""") - title - Pipe clipboard through one-liner - type - 3 - withspace + inputstring + {var:paste} + matchcasesensitive - - type - alfred.workflow.input.scriptfilter - uid - 880D9394-A59A-4BC1-BB46-01461CEFEE80 - - - config - - escaping + matchmode 0 - script - pbpaste | {query} | pbcopy - type - 5 + matchstring + 1 type - alfred.workflow.action.script + alfred.workflow.utility.filter uid - 16039760-F173-4AB8-9C73-DA7401D5DE23 + CB1F4AD1-CC6A-4CE9-9953-5BA92C4B2634 + version + 1 readme - Usage: + Trigger the workflow by hotkey or keyword (default=`|`, override with the `keyword` variable) followed by an arbitrarily simple or complex shell one-liner to transform the contents of the clipboard in-place; optionally use the `Cmd`-modifier to immediately paste the results into the foreground app. + +Examples: + +Uppercase: `| perl -nle 'print uc'` +base64 encode: `| openssl base64 -e` +base64 decode: `| openssl base64 -d` + +A number of example pipelines (including those above) are [built-in](https://github.com/isometry/alfred-pipe/raw/master/builtins.json). + +To save repetitive typing, custom aliases can be defined with the following syntax: -# Sort the contents of the clipboard, counting duplicates -| sort | uniq -c +`| alias NAME=PIPE | LINE@@@` -| …your one-liner of choice. +The trailing `@@@` (override with `alias_terminator` variable) terminates the alias definition and causes it to be saved. + +For example: + +`| alias tac=sed '1!G;h;$!d'@@@` (reverse line-by-line) + +Any custom aliases can be deleted with: + +`| alias NAME=@@@` + +Built-ins can be disabled en-mass by setting the `load_builtins` variable to any value other than `yes`. uidata 0FCF109F-A181-4D53-A4DB-DB54DA7440B8 + xpos + 740 ypos - 420 + 140 16039760-F173-4AB8-9C73-DA7401D5DE23 + note + Run the one-liner + xpos + 430 ypos - 60 + 80 - 64D17591-D71C-421B-B701-91E99A3C0318 + 1D296119-4F9D-4FDC-9777-1DA7CF655D19 + xpos + 580 ypos - 420 + 110 74681B4B-CBE4-403F-BDF7-39F4D2319ABD + note + Prompt for one-liner or alias + xpos + 190 ypos - 60 + 80 - 880D9394-A59A-4BC1-BB46-01461CEFEE80 + 8E5E1774-FB5B-47C7-A0E0-AE958708254F + xpos + 740 ypos - 180 + 20 - CCEE8787-6024-46B4-B155-240F86561281 + 9B8A5712-D806-47E3-9045-EC31AB0C6485 + note + paste!=1 + xpos + 660 ypos - 300 + 50 - D029EE08-8642-404B-83FE-BB5DFEE9C6B6 + CB1F4AD1-CC6A-4CE9-9953-5BA92C4B2634 + note + paste==1 + xpos + 660 ypos - 420 + 170 - D637E280-521E-432F-85C1-3F58E064A66E + D4DF549F-4556-4AA6-BF1A-159154EB3051 + note + paste=1 + xpos + 350 ypos - 60 + 160 - E8AC122D-DD51-491F-A734-C89446EB1924 + D637E280-521E-432F-85C1-3F58E064A66E + xpos + 30 ypos - 300 + 80 + variables + + alias_terminator + @@@ + keyword + | + load_builtins + yes + max_results + 9 + + version + 1.1 webaddress https://github.com/isometry/alfredworkflows/tree/master/net.isometry.alfred.pipe diff --git a/pipe.py b/pipe.py index 7ae68c7..754eaec 100755 --- a/pipe.py +++ b/pipe.py @@ -1,111 +1,133 @@ +#!/usr/bin/env python2.7 #-*- coding: utf-8 -*- -# pipe.alfredworkflow, v1.0 -# Robin Breathe, 2013 +# pipe.alfredworkflow, v1.1 +# Robin Breathe, 2013-2017 + +from __future__ import unicode_literals +from __future__ import print_function import alfred -import json +import json, sys, os from fnmatch import fnmatch from os import path from time import strftime -_MAX_RESULTS=9 -_ALIASES_FILE=u'aliases.json' -_BUILTINS_FILE=u'builtins.json' -_TIMESTAMP=u'%Y-%m-%d @ %H:%M' +DEFAULT_MAX_RESULTS=9 +DEFAULT_ALIAS_TERMINATOR="@@@" +ALIASES_FILE="aliases.json" +BUILTINS_FILE="builtins.json" -def fetch_aliases(_path=_ALIASES_FILE): +def fetch_aliases(_path): file = path.join(alfred.work(volatile=False), _path) if not path.isfile(file): return {} return json.load(open(file, 'r')) -def write_aliases(_dict, _path=_ALIASES_FILE): +def write_aliases(_dict, _path): file = path.join(alfred.work(volatile=False), _path) json.dump(_dict, open(file, 'w'), indent=4, separators=(',', ': ')) -def define_alias(_dict, definition): - if u'=' in definition: - (alias, pipe) = definition.split(u'=', 1) +def define_alias(_dict, definition, alias_file): + if '=' in definition: + (alias, pipe) = definition.split('=', 1) else: - (alias, pipe) = (definition, u'') + (alias, pipe) = (definition, '') + + terminator = os.getenv("alias_terminator", DEFAULT_ALIAS_TERMINATOR); if not alias: return alfred.xml([alfred.Item( - attributes = {'uid': u'pipe:help', 'valid': u'no'}, - title = u"alias NAME=VALUE", - subtitle = u'Terminate VALUE with @@ to save', - icon = u'icon.png' + attributes = {'valid': 'no'}, + title = "alias NAME=ARBITRARY-ONE-LINER", + subtitle = "Terminate ONE-LINER with '{0}' to save or 'NAME={0}' to delete alias".format(terminator), + icon = 'icon.png' + )]) + + if pipe and pipe == terminator: + _dict.pop(alias, None) + write_aliases(_dict, alias_file) + return alfred.xml([alfred.Item( + attributes = {'valid': 'no', 'autocomplete': ''}, + title = "alias {0}={1}".format(alias, pipe), + subtitle = 'Alias deleted! TAB to continue', + icon = 'icon.png' )]) - if pipe and pipe.endswith('@@'): - pipe = pipe[:-2] + + if pipe and pipe.endswith(terminator): + pipe = pipe[:-len(terminator)] _dict[alias] = pipe - write_aliases(_dict) + write_aliases(_dict, alias_file) return alfred.xml([alfred.Item( - attributes = {'uid': u'pipe:{}'.format(pipe) , 'valid': u'no', 'autocomplete': alias}, - title = u"alias {}={}".format(alias, pipe), - subtitle = u'Alias saved! TAB to continue', - icon = u'icon.png' + attributes = {'valid': 'no', 'autocomplete': alias}, + title = "alias {0}={1}".format(alias, pipe), + subtitle = 'Alias saved! TAB to continue', + icon = 'icon.png' )]) return alfred.xml([alfred.Item( - attributes = {'uid': u'pipe:{}'.format(pipe) , 'valid': u'no'}, - title = u"alias {}={}".format(alias, pipe or 'VALUE'), - subtitle = u'Terminate with @@ to save', - icon = u'icon.png' + attributes = {'valid': 'no'}, + title = "alias {0}={1}".format(alias, pipe), + subtitle = 'Terminate with {0} to save'.format(terminator), + icon = 'icon.png' )]) def exact_alias(_dict, query): pipe = _dict[query] return alfred.xml([alfred.Item( - attributes = {'uid': u'pipe:{}'.format(pipe), 'arg': pipe}, + attributes = {'uid': 'pipe:{}'.format(pipe), 'arg': pipe}, title = pipe, - subtitle = u'(expanded alias)', - icon = u'icon.png' + subtitle = '(expanded alias)', + icon = 'icon.png' )]) def match_aliases(_dict, query): results = [] for (alias, pipe) in _dict.iteritems(): - if (pipe != query) and fnmatch(alias, u'{}*'.format(query)): + if (pipe != query) and fnmatch(alias, '{}*'.format(query)): results.append(alfred.Item( - attributes = {'uid': u'pipe:{}'.format(pipe) , 'arg': pipe, 'autocomplete': pipe}, + attributes = {'uid': 'pipe:{}'.format(pipe) , 'arg': pipe, 'autocomplete': pipe}, title = pipe, - subtitle = u'(alias: {})'.format(alias), - icon = u'icon.png' + subtitle = '(alias: {})'.format(alias), + icon = 'icon.png' )) return results -def fetch_builtins(_path=_BUILTINS_FILE): +def fetch_builtins(_path): return json.load(open(_path, 'r')) def match_builtins(_dict, query): results = [] for (pipe, desc) in _dict.iteritems(): - if fnmatch(pipe, u'*{}*'.format(query)) or fnmatch(desc, u'*{}*'.format(query)): + if fnmatch(pipe, '*{}*'.format(query)) or fnmatch(desc, '*{}*'.format(query)): results.append(alfred.Item( - attributes = {'uid': u'pipe:{}'.format(pipe) , 'arg': pipe, 'autocomplete': pipe}, + attributes = {'uid': 'pipe:{}'.format(pipe) , 'arg': pipe, 'autocomplete': pipe}, title = pipe, - subtitle = u'(builtin: {})'.format(desc), - icon = u'icon.png' + subtitle = '(builtin: {})'.format(desc), + icon = 'icon.png' )) return results def verbatim(query): return alfred.Item( - attributes = {'uid': u'pipe:{}'.format(query), 'arg': query}, + attributes = {'uid': 'pipe:{}'.format(query), 'arg': query}, title = query, subtitle = None, - icon = u'icon.png' + icon = 'icon.png' ) -def complete(query, maxresults=_MAX_RESULTS): - aliases = fetch_aliases() - builtins = fetch_builtins() +def complete(): + query = sys.argv[1] + + max_results = int(os.getenv('max_results', DEFAULT_MAX_RESULTS)) + load_builtins = bool(os.getenv('builtins_file', "yes") == "yes") + + aliases = fetch_aliases(ALIASES_FILE) + builtins = load_builtins and fetch_builtins(BUILTINS_FILE) or {} if query.startswith('alias '): - return define_alias(aliases, query[6:]) + return define_alias(aliases, query[6:], ALIASES_FILE) results = [] @@ -118,4 +140,7 @@ def complete(query, maxresults=_MAX_RESULTS): ): results.extend(matches) - return alfred.xml(results, maxresults=maxresults) + return alfred.xml(results, maxresults=max_results) + +if __name__ == '__main__': + print(complete())