├── .env ├── .gitignore ├── LICENCE.txt ├── PasteboardTypes.js ├── PathsToPasteboard.js ├── PerformService.js ├── README.md ├── WaitForPasteboard.js ├── go.mod ├── go.sum ├── icon.png ├── icons.afdesign ├── icons ├── error.png ├── icon-large.png ├── icon.png ├── update-available.png └── warning.png ├── info.plist ├── magefile.go ├── main.go └── modd.conf /.env: -------------------------------------------------------------------------------- 1 | # When sourced, creates an Alfred-like environment needed by modd 2 | 3 | # getvar | Read a value from info.plist 4 | getvar() { 5 | local v="$1" 6 | /usr/libexec/PlistBuddy -c "Print :$v" info.plist 7 | } 8 | 9 | export alfred_workflow_bundleid=$( getvar "bundleid" ) 10 | export alfred_workflow_version=$( getvar "version" ) 11 | export alfred_workflow_name=$( getvar "name" ) 12 | 13 | export alfred_workflow_cache="${HOME}/Library/Caches/com.runningwithcrayons.Alfred/Workflow Data/${alfred_workflow_bundleid}" 14 | export alfred_workflow_data="${HOME}/Library/Application Support/Alfred/Workflow Data/${alfred_workflow_bundleid}" 15 | export alfred_version="4.1" 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Dean Jackson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /PasteboardTypes.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/osascript -l JavaScript 2 | 3 | /* 4 | Copyright (c) 2020 Dean Jackson 5 | MIT Licence applies http://opensource.org/licenses/MIT 6 | Created on 2020-08-01 7 | 8 | Returns type(s) of pasteboard items. 9 | */ 10 | 11 | ObjC.import('Cocoa') 12 | 13 | const pboard = $.NSPasteboard.generalPasteboard 14 | 15 | function pboardTypes() { 16 | let types = [] 17 | ObjC.unwrap(pboard.types).forEach(t => { 18 | let s = ObjC.unwrap(t) 19 | if (s.startsWith('dyn.')) return 20 | console.log(`[pboard] type=${s}`) 21 | types.push(s) 22 | }) 23 | 24 | return types 25 | } 26 | 27 | function run() { 28 | return JSON.stringify(pboardTypes()) 29 | } -------------------------------------------------------------------------------- /PathsToPasteboard.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/osascript -l JavaScript 2 | 3 | /* 4 | Copyright (c) 2020 Dean Jackson 5 | MIT Licence applies http://opensource.org/licenses/MIT 6 | Created on 2020-08-01 7 | 8 | Puts filepaths passed as ARGV onto general pasteboard. 9 | */ 10 | 11 | ObjC.import('Cocoa') 12 | 13 | const pboard = $.NSPasteboard.generalPasteboard 14 | 15 | function run(paths) { 16 | console.log('.') 17 | console.log('/--------- INPUT FILES ---------\\') 18 | let arr = $.NSMutableArray.alloc.init, 19 | paths.forEach(p => { 20 | let url = $.NSURL.fileURLWithPath(p) 21 | arr.addObject(url.absoluteURL) 22 | console.log(ObjC.unwrap(url.absoluteString)) 23 | }) 24 | console.log('\\--------- INPUT FILES ---------/') 25 | pboard.clearContents 26 | pboard.writeObjects(arr) 27 | return JSON.stringify({ 28 | alfredworkflow: { 29 | variables: { 30 | PBOARD_TYPES: 'public.file-url', 31 | CLIPBOARD: paths.join('\n') 32 | } 33 | } 34 | }) 35 | } -------------------------------------------------------------------------------- /PerformService.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/osascript -l JavaScript 2 | 3 | /* 4 | Copyright (c) 2020 Dean Jackson 5 | MIT Licence applies http://opensource.org/licenses/MIT 6 | Created on 2020-08-01 7 | 8 | Runs macOS service on contents of general pasteboard. 9 | 10 | Name of service is passed as $1 11 | */ 12 | 13 | ObjC.import('Cocoa') 14 | 15 | function run(argv) { 16 | let service = argv[0] 17 | console.log(`performing service "${service}" ...`) 18 | let ret = $.NSPerformService(service, $.NSPasteboard.generalPasteboard) 19 | if (!ret) return `Service “${service}” failed` 20 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | grey cog in grey ring 4 |
5 | 6 | macOS Services for Alfred 7 | ========================= 8 | 9 | Run macOS services via Alfred 4+. 10 | 11 | This workflow can execute macOS services on the clipboard contents, current selection or files (via an Alfred File Action). 12 | 13 | 14 | Installation 15 | ------------ 16 | 17 | Download the latest version of the workflow from the [releases page][releases], then double-click the `macOS-Services-X.X.X.alfredworkflow` file to install. 18 | 19 | 20 | Usage 21 | ----- 22 | 23 | When run via keyword (`services` by default), you can choose from services that are applicable to the current contents of the general pasteboard. There is also a Hotkey to run the workflow with the current pasteboard. 24 | 25 | Alternatively, you can call the workflow via its File Action (called "macOS Services") to run a service with the selected files, or use the second Hotkey to run the workflow on the current macOS selection. 26 | 27 | Finally, you can assign your own Hotkeys to specific services (though this only works with the pasteboard contents). See the red EXAMPLE element in the workflow (which calls the "New TextEdit Window Containing Selection" service). 28 | 29 | 30 | Licensing & thanks 31 | ------------------ 32 | 33 | - The workflow is released under the [MIT licence][mit]. 34 | - It is heavily based on the [AwGo][awgo] library, also MIT licensed. 35 | - The workflow icons are based on [Font Awesome][awesome], released under the [Creative Commons Attribution 4.0 licence][ccby40]. 36 | 37 | [releases]: https://github.com/deanishe/alfred-services/releases/latest 38 | [mit]: LICENCE.txt 39 | [awesome]: https://github.com/FortAwesome/Font-Awesome 40 | [ccby40]: https://creativecommons.org/licenses/by/4.0/legalcode 41 | [awgo]: https://github.com/deanishe/awgo 42 | -------------------------------------------------------------------------------- /WaitForPasteboard.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/osascript -l JavaScript 2 | 3 | /* 4 | Copyright (c) 2021 Dean Jackson 5 | MIT Licence applies http://opensource.org/licenses/MIT 6 | Created on 2021-07-07 7 | 8 | Trigger ⌘C and wait for pasteboard to change. 9 | */ 10 | 11 | ObjC.import('Cocoa') 12 | ObjC.import('Carbon') 13 | 14 | 15 | function commandC() { 16 | let source = $.CGEventSourceCreate($.kCGEventSourceStateCombinedSessionState); 17 | 18 | let copyCommandDown = $.CGEventCreateKeyboardEvent(source, $.kVK_ANSI_C, true); 19 | $.CGEventSetFlags(copyCommandDown, $.kCGEventFlagMaskCommand); 20 | let copyCommandUp = $.CGEventCreateKeyboardEvent(source, $.kVK_ANSI_C, false); 21 | 22 | $.CGEventPost($.kCGAnnotatedSessionEventTap, copyCommandDown); 23 | $.CGEventPost($.kCGAnnotatedSessionEventTap, copyCommandUp); 24 | } 25 | 26 | 27 | function run() { 28 | const pboard = $.NSPasteboard.generalPasteboard, 29 | value = 'net.deanishe.alfred.macos-services', 30 | type = 'public.utf8-plain-text' 31 | 32 | // put a sentinel value on the clipboard 33 | pboard.clearContents 34 | pboard.setStringForType('', 'org.nspasteboard.ConcealedType') 35 | pboard.setStringForType(value, type) 36 | 37 | commandC() 38 | 39 | // wait up to 2 secs for sentinel value to be replaced with new 40 | // clipboard contents from ⌘C 41 | for (let i = 0; i < 200; i++) { 42 | let v = ObjC.unwrap(pboard.stringForType(type)) 43 | if (v != value) { 44 | return 45 | } 46 | delay(0.01) 47 | } 48 | console.log('pasteboard was not populated within 2 seconds') 49 | return JSON.stringify({'alfredworkflow': {'variables': {'copy_failed': true}}}) 50 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.deanishe.net/alfred-services 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/deanishe/awgo v0.26.2 7 | github.com/magefile/mage v1.10.0 8 | github.com/pkg/errors v0.9.1 9 | howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY= 2 | github.com/bmatcuk/doublestar v1.3.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/deanishe/awgo v0.26.2 h1:eDzriTPO1mcv+Z/9JsIyHWX/sQCsYGV4sh7aFpzvc8I= 8 | github.com/deanishe/awgo v0.26.2/go.mod h1:Qen3509y1/sj7a5syefWc6FQHO1LE/tyoTyN9jRv1vU= 9 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 10 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g= 16 | github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= 17 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 18 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 19 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 20 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 21 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 22 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 23 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 24 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 25 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 26 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 27 | go.deanishe.net/env v0.5.1 h1:WiOncK5uJj8Um57Vj2dc1bq1lMN7fgRag9up7I3LZy0= 28 | go.deanishe.net/env v0.5.1/go.mod h1:ihEYfDm0K0hq3f5ACTCQDrMTWxH9fTiA1lh1i0aMqm0= 29 | go.deanishe.net/fuzzy v1.0.0 h1:3Qp6PCX0DLb9z03b5OHwAGsbRSkgJpSLncsiDdXDt4Y= 30 | go.deanishe.net/fuzzy v1.0.0/go.mod h1:2yEEMfG7jWgT1s5EO0TteVWmx2MXFBRMr5cMm84bQNY= 31 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 32 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 33 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 36 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 37 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 38 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 39 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 40 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 41 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 42 | howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 h1:AQkaJpH+/FmqRjmXZPELom5zIERYZfwTjnHpfoVMQEc= 43 | howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= 44 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- 1 | icons/icon.png -------------------------------------------------------------------------------- /icons.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-services/9bef78c74c83ab421edba08f2951327372903c8d/icons.afdesign -------------------------------------------------------------------------------- /icons/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-services/9bef78c74c83ab421edba08f2951327372903c8d/icons/error.png -------------------------------------------------------------------------------- /icons/icon-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-services/9bef78c74c83ab421edba08f2951327372903c8d/icons/icon-large.png -------------------------------------------------------------------------------- /icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-services/9bef78c74c83ab421edba08f2951327372903c8d/icons/icon.png -------------------------------------------------------------------------------- /icons/update-available.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-services/9bef78c74c83ab421edba08f2951327372903c8d/icons/update-available.png -------------------------------------------------------------------------------- /icons/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanishe/alfred-services/9bef78c74c83ab421edba08f2951327372903c8d/icons/warning.png -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | net.deanishe.alfred.macos-services 7 | connections 8 | 9 | 054DBBD0-6438-4558-8385-F9007F9CAAB4 10 | 11 | 12 | destinationuid 13 | 206FE67F-4309-49B3-ADC4-72B0FD32FB61 14 | modifiers 15 | 0 16 | modifiersubtext 17 | 18 | vitoclose 19 | 20 | 21 | 22 | 206FE67F-4309-49B3-ADC4-72B0FD32FB61 23 | 24 | 25 | destinationuid 26 | D2617139-82A7-4991-B735-173585DE2CAA 27 | modifiers 28 | 0 29 | modifiersubtext 30 | 31 | vitoclose 32 | 33 | 34 | 35 | 4A95E744-11F9-4E06-ADE7-226C6470BD04 36 | 37 | 38 | destinationuid 39 | 6DAAB434-9A13-4929-87FB-A8480CA81ADF 40 | modifiers 41 | 0 42 | modifiersubtext 43 | 44 | vitoclose 45 | 46 | 47 | 48 | 53E8AF64-5368-48AC-92A5-393C41E2FD5F 49 | 50 | 51 | destinationuid 52 | D211F7C8-DC82-4BA5-A6AD-487F0631B08C 53 | modifiers 54 | 0 55 | modifiersubtext 56 | 57 | sourceoutputuid 58 | CE3C53CE-A1F5-4EC5-91CD-CD176F3D3735 59 | vitoclose 60 | 61 | 62 | 63 | destinationuid 64 | 206FE67F-4309-49B3-ADC4-72B0FD32FB61 65 | modifiers 66 | 0 67 | modifiersubtext 68 | 69 | vitoclose 70 | 71 | 72 | 73 | 6DAAB434-9A13-4929-87FB-A8480CA81ADF 74 | 75 | 76 | destinationuid 77 | D2E4E44D-F9CF-41D5-8048-49027A23DF14 78 | modifiers 79 | 0 80 | modifiersubtext 81 | 82 | vitoclose 83 | 84 | 85 | 86 | 7DC37085-E798-44A2-B48A-18C2D615567F 87 | 88 | 89 | destinationuid 90 | 8489026D-97A0-40DD-AE02-94349B682DBE 91 | modifiers 92 | 0 93 | modifiersubtext 94 | 95 | vitoclose 96 | 97 | 98 | 99 | 8489026D-97A0-40DD-AE02-94349B682DBE 100 | 101 | 102 | destinationuid 103 | BD18AA3C-3DA9-4BA2-8F8E-3DD4736BDDF0 104 | modifiers 105 | 0 106 | modifiersubtext 107 | 108 | vitoclose 109 | 110 | 111 | 112 | B23AEA1C-49B7-4BD4-B519-E59BC7445EA5 113 | 114 | 115 | destinationuid 116 | BD18AA3C-3DA9-4BA2-8F8E-3DD4736BDDF0 117 | modifiers 118 | 0 119 | modifiersubtext 120 | 121 | vitoclose 122 | 123 | 124 | 125 | BD18AA3C-3DA9-4BA2-8F8E-3DD4736BDDF0 126 | 127 | 128 | destinationuid 129 | D025E895-3B47-4A1D-A37A-7A57A0A532C7 130 | modifiers 131 | 0 132 | modifiersubtext 133 | 134 | vitoclose 135 | 136 | 137 | 138 | D025E895-3B47-4A1D-A37A-7A57A0A532C7 139 | 140 | 141 | destinationuid 142 | 53E8AF64-5368-48AC-92A5-393C41E2FD5F 143 | modifiers 144 | 0 145 | modifiersubtext 146 | 147 | vitoclose 148 | 149 | 150 | 151 | D2E4E44D-F9CF-41D5-8048-49027A23DF14 152 | 153 | 154 | destinationuid 155 | 6920921E-E46C-46D4-8115-DB670DB13467 156 | modifiers 157 | 0 158 | modifiersubtext 159 | 160 | sourceoutputuid 161 | 0E0BF794-D94E-4DF2-A33B-26D5A934ACAF 162 | vitoclose 163 | 164 | 165 | 166 | destinationuid 167 | BD18AA3C-3DA9-4BA2-8F8E-3DD4736BDDF0 168 | modifiers 169 | 0 170 | modifiersubtext 171 | 172 | vitoclose 173 | 174 | 175 | 176 | 177 | createdby 178 | Dean Jackson <deanishe@deanishe.net> 179 | description 180 | Run macOS Services from Alfred 181 | disabled 182 | 183 | name 184 | macOS Services 185 | objects 186 | 187 | 188 | config 189 | 190 | lastpathcomponent 191 | 192 | onlyshowifquerypopulated 193 | 194 | removeextension 195 | 196 | text 197 | Couldn't get current selection from active application 198 | title 199 | Nothing Selected 200 | 201 | type 202 | alfred.workflow.output.notification 203 | uid 204 | 6920921E-E46C-46D4-8115-DB670DB13467 205 | version 206 | 1 207 | 208 | 209 | config 210 | 211 | action 212 | 0 213 | argument 214 | 0 215 | focusedappvariable 216 | 217 | focusedappvariablename 218 | 219 | hotkey 220 | 44 221 | hotmod 222 | 1441792 223 | hotstring 224 | / 225 | leftcursor 226 | 227 | modsmode 228 | 0 229 | relatedAppsMode 230 | 0 231 | 232 | type 233 | alfred.workflow.trigger.hotkey 234 | uid 235 | 4A95E744-11F9-4E06-ADE7-226C6470BD04 236 | version 237 | 2 238 | 239 | 240 | config 241 | 242 | concurrently 243 | 244 | escaping 245 | 0 246 | script 247 | 248 | scriptargtype 249 | 1 250 | scriptfile 251 | WaitForPasteboard.js 252 | type 253 | 8 254 | 255 | type 256 | alfred.workflow.action.script 257 | uid 258 | 6DAAB434-9A13-4929-87FB-A8480CA81ADF 259 | version 260 | 2 261 | 262 | 263 | config 264 | 265 | conditions 266 | 267 | 268 | inputstring 269 | {var:copy_failed} 270 | matchcasesensitive 271 | 272 | matchmode 273 | 1 274 | matchstring 275 | 276 | outputlabel 277 | Copy Failed 278 | uid 279 | 0E0BF794-D94E-4DF2-A33B-26D5A934ACAF 280 | 281 | 282 | elselabel 283 | else 284 | 285 | type 286 | alfred.workflow.utility.conditional 287 | uid 288 | D2E4E44D-F9CF-41D5-8048-49027A23DF14 289 | version 290 | 1 291 | 292 | 293 | config 294 | 295 | path 296 | 297 | 298 | type 299 | alfred.workflow.action.revealfile 300 | uid 301 | D211F7C8-DC82-4BA5-A6AD-487F0631B08C 302 | version 303 | 1 304 | 305 | 306 | config 307 | 308 | alfredfiltersresults 309 | 310 | alfredfiltersresultsmatchmode 311 | 0 312 | argumenttreatemptyqueryasnil 313 | 314 | argumenttrimmode 315 | 0 316 | argumenttype 317 | 1 318 | escaping 319 | 102 320 | keyword 321 | services 322 | queuedelaycustom 323 | 3 324 | queuedelayimmediatelyinitially 325 | 326 | queuedelaymode 327 | 0 328 | queuemode 329 | 1 330 | runningsubtext 331 | Loading applicable services… 332 | script 333 | ./alfred-services "$1" 334 | scriptargtype 335 | 1 336 | scriptfile 337 | 338 | subtext 339 | Execute macOS Service 340 | title 341 | macOS Services 342 | type 343 | 5 344 | withspace 345 | 346 | 347 | type 348 | alfred.workflow.input.scriptfilter 349 | uid 350 | D025E895-3B47-4A1D-A37A-7A57A0A532C7 351 | version 352 | 3 353 | 354 | 355 | config 356 | 357 | action 358 | 0 359 | argument 360 | 0 361 | focusedappvariable 362 | 363 | focusedappvariablename 364 | 365 | hotkey 366 | 44 367 | hotmod 368 | 1703936 369 | hotstring 370 | / 371 | leftcursor 372 | 373 | modsmode 374 | 0 375 | relatedAppsMode 376 | 0 377 | 378 | type 379 | alfred.workflow.trigger.hotkey 380 | uid 381 | B23AEA1C-49B7-4BD4-B519-E59BC7445EA5 382 | version 383 | 2 384 | 385 | 386 | config 387 | 388 | conditions 389 | 390 | 391 | inputstring 392 | {var:reveal} 393 | matchcasesensitive 394 | 395 | matchmode 396 | 1 397 | matchstring 398 | 399 | outputlabel 400 | reveal service 401 | uid 402 | CE3C53CE-A1F5-4EC5-91CD-CD176F3D3735 403 | 404 | 405 | elselabel 406 | run service 407 | 408 | type 409 | alfred.workflow.utility.conditional 410 | uid 411 | 53E8AF64-5368-48AC-92A5-393C41E2FD5F 412 | version 413 | 1 414 | 415 | 416 | config 417 | 418 | argument 419 | . 420 | /--------- INPUT VARIABLES ---------\ 421 | query={query} 422 | variables={allvars} 423 | \--------- INPUT VARIABLES ---------/ 424 | cleardebuggertext 425 | 426 | processoutputs 427 | 428 | 429 | type 430 | alfred.workflow.utility.debug 431 | uid 432 | BD18AA3C-3DA9-4BA2-8F8E-3DD4736BDDF0 433 | version 434 | 1 435 | 436 | 437 | config 438 | 439 | concurrently 440 | 441 | escaping 442 | 0 443 | script 444 | 445 | scriptargtype 446 | 1 447 | scriptfile 448 | PerformService.js 449 | type 450 | 8 451 | 452 | type 453 | alfred.workflow.action.script 454 | uid 455 | 206FE67F-4309-49B3-ADC4-72B0FD32FB61 456 | version 457 | 2 458 | 459 | 460 | config 461 | 462 | lastpathcomponent 463 | 464 | onlyshowifquerypopulated 465 | 466 | removeextension 467 | 468 | text 469 | {query} 470 | title 471 | 💀 Service Error 💀 472 | 473 | type 474 | alfred.workflow.output.notification 475 | uid 476 | D2617139-82A7-4991-B735-173585DE2CAA 477 | version 478 | 1 479 | 480 | 481 | config 482 | 483 | action 484 | 0 485 | argument 486 | 3 487 | argumenttext 488 | New TextEdit Window Containing Selection 489 | focusedappvariable 490 | 491 | focusedappvariablename 492 | 493 | hotkey 494 | 122 495 | hotmod 496 | 10223616 497 | hotstring 498 | F1 499 | leftcursor 500 | 501 | modsmode 502 | 0 503 | relatedAppsMode 504 | 0 505 | 506 | type 507 | alfred.workflow.trigger.hotkey 508 | uid 509 | 054DBBD0-6438-4558-8385-F9007F9CAAB4 510 | version 511 | 2 512 | 513 | 514 | config 515 | 516 | concurrently 517 | 518 | escaping 519 | 102 520 | script 521 | ./alfred-services -files $@ 522 | scriptargtype 523 | 1 524 | scriptfile 525 | PathsToPasteboard.js 526 | type 527 | 8 528 | 529 | type 530 | alfred.workflow.action.script 531 | uid 532 | 8489026D-97A0-40DD-AE02-94349B682DBE 533 | version 534 | 2 535 | 536 | 537 | config 538 | 539 | acceptsmulti 540 | 1 541 | filetypes 542 | 543 | name 544 | macOS Services 545 | 546 | type 547 | alfred.workflow.trigger.action 548 | uid 549 | 7DC37085-E798-44A2-B48A-18C2D615567F 550 | version 551 | 1 552 | 553 | 554 | readme 555 | macOS Services for Alfred 556 | ========================= 557 | 558 | Run macOS services via Alfred. 559 | 560 | This workflow can execute macOS services on the clipboard contents, current selection or files (via an Alfred File Action). 561 | 562 | 563 | Usage 564 | ----- 565 | 566 | When run via keyword (`services` by default), you can choose from services that are applicable to the current contents of the general pasteboard. There is also a Hotkey to run the workflow with the current pasteboard. 567 | 568 | Alternatively, you can call the workflow via its File Action (called "macOS Services") to run a service with the selected files, or use the second Hotkey to run the workflow on the current macOS selection. 569 | 570 | Finally, you can assign your own Hotkeys to specific services (though this only works with the pasteboard contents). 571 | uidata 572 | 573 | 054DBBD0-6438-4558-8385-F9007F9CAAB4 574 | 575 | colorindex 576 | 1 577 | note 578 | EXAMPLE 579 | 580 | Assign a Hotkey to a specific service (called on current clipboard contents) 581 | xpos 582 | 625 583 | ypos 584 | 385 585 | 586 | 206FE67F-4309-49B3-ADC4-72B0FD32FB61 587 | 588 | note 589 | Execute service 590 | xpos 591 | 1000 592 | ypos 593 | 280 594 | 595 | 4A95E744-11F9-4E06-ADE7-226C6470BD04 596 | 597 | note 598 | Run with current selection 599 | xpos 600 | 45 601 | ypos 602 | 30 603 | 604 | 53E8AF64-5368-48AC-92A5-393C41E2FD5F 605 | 606 | xpos 607 | 835 608 | ypos 609 | 225 610 | 611 | 6920921E-E46C-46D4-8115-DB670DB13467 612 | 613 | xpos 614 | 625 615 | ypos 616 | 15 617 | 618 | 6DAAB434-9A13-4929-87FB-A8480CA81ADF 619 | 620 | note 621 | Simulate ⌘C to get current selection and wait for clipboard to populate 622 | xpos 623 | 210 624 | ypos 625 | 30 626 | 627 | 7DC37085-E798-44A2-B48A-18C2D615567F 628 | 629 | note 630 | Run services on files 631 | xpos 632 | 45 633 | ypos 634 | 385 635 | 636 | 8489026D-97A0-40DD-AE02-94349B682DBE 637 | 638 | note 639 | Put files on pasteboard 640 | xpos 641 | 210 642 | ypos 643 | 385 644 | 645 | B23AEA1C-49B7-4BD4-B519-E59BC7445EA5 646 | 647 | note 648 | Run with clipboard contents 649 | xpos 650 | 45 651 | ypos 652 | 205 653 | 654 | BD18AA3C-3DA9-4BA2-8F8E-3DD4736BDDF0 655 | 656 | xpos 657 | 530 658 | ypos 659 | 235 660 | 661 | D025E895-3B47-4A1D-A37A-7A57A0A532C7 662 | 663 | note 664 | Filter macOS services 665 | xpos 666 | 625 667 | ypos 668 | 205 669 | 670 | D211F7C8-DC82-4BA5-A6AD-487F0631B08C 671 | 672 | xpos 673 | 1000 674 | ypos 675 | 140 676 | 677 | D2617139-82A7-4991-B735-173585DE2CAA 678 | 679 | note 680 | Show error message 681 | xpos 682 | 1170 683 | ypos 684 | 280 685 | 686 | D2E4E44D-F9CF-41D5-8048-49027A23DF14 687 | 688 | xpos 689 | 365 690 | ypos 691 | 50 692 | 693 | 694 | version 695 | 0.2.0 696 | webaddress 697 | 698 | 699 | 700 | -------------------------------------------------------------------------------- /magefile.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 Dean Jackson 2 | // MIT Licence applies http://opensource.org/licenses/MIT 3 | 4 | // +build mage 5 | 6 | package main 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | "path/filepath" 12 | 13 | "github.com/deanishe/awgo/util" 14 | "github.com/deanishe/awgo/util/build" 15 | "github.com/magefile/mage/mg" 16 | "github.com/magefile/mage/sh" 17 | ) 18 | 19 | // Default target to run when none is specified 20 | // If not set, running mage will list available targets 21 | // var Default = Build 22 | 23 | var ( 24 | info *build.Info 25 | env map[string]string 26 | ldflags string 27 | workDir string 28 | buildDir = "./build" 29 | distDir = "./dist" 30 | iconsDir = "./icons" 31 | ) 32 | 33 | var ( 34 | green = "03ae03" 35 | blue = "5484f3" 36 | red = "b00000" 37 | yellow = "f8ac30" 38 | ) 39 | 40 | func init() { 41 | var err error 42 | if info, err = build.NewInfo(); err != nil { 43 | panic(err) 44 | } 45 | if workDir, err = os.Getwd(); err != nil { 46 | panic(err) 47 | } 48 | env = info.Env() 49 | env["API_KEY"] = os.Getenv("GOODREADS_API_KEY") 50 | env["API_SECRET"] = os.Getenv("GOODREADS_API_SECRET") 51 | env["VERSION"] = info.Version 52 | env["PKG"] = "main" 53 | ldflags = `-X "$PKG.version=$VERSION"` 54 | } 55 | 56 | func mod(args ...string) error { 57 | argv := append([]string{"mod"}, args...) 58 | return sh.RunWith(env, "go", argv...) 59 | } 60 | 61 | // Aliases are mage command aliases. 62 | var Aliases = map[string]interface{}{ 63 | "b": Build, 64 | "c": Clean, 65 | "d": Dist, 66 | "l": Link, 67 | } 68 | 69 | // Build builds workflow in ./build 70 | func Build() error { 71 | mg.Deps(cleanBuild) 72 | // mg.Deps(Deps) 73 | fmt.Println("building ...") 74 | 75 | err := sh.RunWith(env, 76 | "go", "build", 77 | // "-tags", "$TAGS", 78 | "-ldflags", ldflags, 79 | "-o", "./build/alfred-services", 80 | ".", 81 | ) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | globs := build.Globs( 87 | "*.png", 88 | "info.plist", 89 | "*.html", 90 | "README.md", 91 | "LICENCE.txt", 92 | "icons/*.png", 93 | "*.js", 94 | ) 95 | 96 | return build.SymlinkGlobs(buildDir, globs...) 97 | } 98 | 99 | // Run run workflow 100 | func Run() error { 101 | mg.Deps(Build) 102 | fmt.Println("running ...") 103 | return sh.RunWith(env, buildDir+"/alfred-services", "-h") 104 | } 105 | 106 | // Dist build an .alfredworkflow file in ./dist 107 | func Dist() error { 108 | mg.SerialDeps(Clean, Build) 109 | p, err := build.Export(buildDir, distDir) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | fmt.Printf("built workflow file %q\n", p) 115 | return nil 116 | } 117 | 118 | // Config display configuration 119 | func Config() { 120 | fmt.Println(" Workflow name:", info.Name) 121 | fmt.Println(" Bundle ID:", info.BundleID) 122 | fmt.Println(" Workflow version:", info.Version) 123 | fmt.Println(" Preferences file:", info.AlfredPrefsBundle) 124 | fmt.Println(" Sync folder:", info.AlfredSyncDir) 125 | fmt.Println("Workflow directory:", info.AlfredWorkflowDir) 126 | fmt.Println(" Data directory:", info.DataDir) 127 | fmt.Println(" Cache directory:", info.CacheDir) 128 | } 129 | 130 | // Link symlinks ./build directory to Alfred's workflow directory. 131 | func Link() error { 132 | mg.Deps(Build) 133 | 134 | fmt.Println("linking ./build to workflow directory ...") 135 | target := filepath.Join(info.AlfredWorkflowDir, info.BundleID) 136 | // fmt.Printf("target: %s\n", target) 137 | 138 | if util.PathExists(target) { 139 | fmt.Println("removing existing workflow ...") 140 | } 141 | // try to remove it anyway, as dangling symlinks register as existing 142 | if err := os.RemoveAll(target); err != nil && !os.IsNotExist(err) { 143 | return err 144 | } 145 | 146 | src, err := filepath.Abs(buildDir) 147 | if err != nil { 148 | return err 149 | } 150 | return build.Symlink(target, src, true) 151 | } 152 | 153 | // Deps ensure dependencies 154 | func Deps() error { 155 | mg.Deps(cleanDeps) 156 | fmt.Println("downloading deps ...") 157 | return mod("download") 158 | } 159 | 160 | // Vendor copy dependencies to ./vendor 161 | func Vendor() error { 162 | mg.Deps(Deps) 163 | fmt.Println("vendoring deps ...") 164 | return mod("vendor") 165 | } 166 | 167 | // Clean remove build files 168 | func Clean() { 169 | fmt.Println("cleaning ...") 170 | mg.Deps(cleanBuild, cleanMage, cleanDeps) 171 | } 172 | 173 | func cleanDeps() error { 174 | return mod("tidy", "-v") 175 | } 176 | 177 | // remove & recreate directory 178 | func cleanDir(name string) error { 179 | if err := sh.Rm(name); err != nil { 180 | return err 181 | } 182 | return os.MkdirAll(name, 0755) 183 | } 184 | 185 | /* 186 | func cleanDir(name string, exclude ...string) error { 187 | 188 | if _, err := os.Stat(name); err != nil { 189 | return nil 190 | } 191 | 192 | infos, err := ioutil.ReadDir(name) 193 | if err != nil { 194 | return err 195 | } 196 | for _, fi := range infos { 197 | 198 | var match bool 199 | for _, glob := range exclude { 200 | if match, err = doublestar.Match(glob, fi.Name()); err != nil { 201 | return err 202 | } else if match { 203 | break 204 | } 205 | } 206 | 207 | if match { 208 | fmt.Printf("excluded: %s\n", fi.Name()) 209 | continue 210 | } 211 | 212 | p := filepath.Join(name, fi.Name()) 213 | if err := os.RemoveAll(p); err != nil { 214 | return err 215 | } 216 | } 217 | return nil 218 | } 219 | */ 220 | 221 | func cleanBuild() error { 222 | return cleanDir(buildDir) 223 | } 224 | 225 | func cleanMage() error { 226 | return sh.Run("mage", "-clean") 227 | } 228 | 229 | // CleanIcons delete all generated icons from ./icons 230 | func CleanIcons() error { 231 | return cleanDir(iconsDir) 232 | } 233 | 234 | // func gitVersion() string { 235 | // s, _ := sh.Output("git", "describe", "--tags", "--always", "--abbrev=10") 236 | // return s 237 | // } 238 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 Dean Jackson 2 | // MIT Licence applies http://opensource.org/licenses/MIT 3 | // Created on 2020-08-01 4 | 5 | package main 6 | 7 | import ( 8 | "encoding/json" 9 | "flag" 10 | "fmt" 11 | "io/ioutil" 12 | "log" 13 | "os" 14 | "os/exec" 15 | "regexp" 16 | "sort" 17 | "strings" 18 | 19 | aw "github.com/deanishe/awgo" 20 | "github.com/deanishe/awgo/update" 21 | "github.com/deanishe/awgo/util" 22 | "github.com/pkg/errors" 23 | "howett.net/plist" 24 | ) 25 | 26 | const ( 27 | // workflow's GitHub repo (for updates) 28 | repo = "deanishe/alfred-services" 29 | helpURL = "https://github.com/deanishe/alfred-services/issues" 30 | // property list containing list of services 31 | servicesList = "${HOME}/Library/Caches/com.apple.nsservicescache.plist" 32 | // properly list containing enabled information 33 | pbsList = "${HOME}/Library/Preferences/pbs.plist" 34 | ) 35 | 36 | var ( 37 | iconUpdateAvailable = &aw.Icon{Value: "icons/update-available.png"} 38 | iconError = &aw.Icon{Value: "icons/error.png"} 39 | iconWarning = &aw.Icon{Value: "icons/warning.png"} 40 | ) 41 | 42 | var ( 43 | wf *aw.Workflow 44 | 45 | fs = flag.NewFlagSet("alfred-services", flag.ExitOnError) 46 | flagHelp = fs.Bool("h", false, "show this message and exit") 47 | flagUpdate = fs.Bool("update", false, "check for newer version of the workflow") 48 | ) 49 | 50 | // Service is a macOS service. 51 | type Service struct { 52 | Name string // name of service 53 | Types []string // supported pasteboard types 54 | AppName string // name of app service belongs to (optional) 55 | AppPath string // path of application that defines service 56 | Disabled bool // whether service is disabled 57 | } 58 | 59 | // Title returns a more readable name. 60 | func (s Service) Title() string { 61 | // Safari action has this weird name. Replace it with something better. 62 | if s.Name == "Search With %WebSearchProvider@" { 63 | return "Search Web" 64 | } 65 | return s.Name 66 | } 67 | 68 | // UID returns a unique ID for Service. 69 | func (s Service) UID() string { return s.AppPath + " - " + s.Name } 70 | 71 | // Icon returns a workflow icon for the service. 72 | func (s Service) Icon() *aw.Icon { return &aw.Icon{Value: s.AppPath, Type: aw.IconTypeFileIcon} } 73 | 74 | // Supports returns true if this service supports any of the given types. 75 | func (s Service) Supports(types []string) bool { 76 | for _, t1 := range s.Types { 77 | for _, t2 := range types { 78 | if t1 == t2 { 79 | return true 80 | } 81 | } 82 | } 83 | return false 84 | } 85 | 86 | // ByName sorts services by name. 87 | type ByName []Service 88 | 89 | // Implement sort.Interface 90 | func (s ByName) Len() int { return len(s) } 91 | func (s ByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 92 | func (s ByName) Less(i, j int) bool { return s[i].Name < s[j].Name } 93 | 94 | var rxServiceName = regexp.MustCompile(`^(\S+) - (.+) - (\S+)$`) 95 | 96 | // read services from property list. 97 | func loadServices() ([]Service, error) { 98 | var ( 99 | disabled = map[string]bool{} 100 | services []Service 101 | path string 102 | data []byte 103 | err error 104 | ) 105 | // extract enabled/disabled info from pbs.plist 106 | path = os.ExpandEnv(pbsList) 107 | if data, err = ioutil.ReadFile(path); err != nil { 108 | return nil, errors.Wrap(err, "read pbs.plist") 109 | } 110 | v1 := struct { 111 | Services map[string]struct { 112 | Modes struct { 113 | ContextMenu bool `plist:"ContextMenu"` 114 | ServicesMenu bool `plist:"ServicesMenu"` 115 | TouchBar bool `plist:"TouchBar"` 116 | } `plist:"presentation_modes"` 117 | } `plist:"NSServicesStatus"` 118 | }{} 119 | if _, err = plist.Unmarshal(data, &v1); err != nil { 120 | return nil, errors.Wrap(err, "unmarshal pbs list") 121 | } 122 | for key, modes := range v1.Services { 123 | m := rxServiceName.FindStringSubmatch(key) 124 | if m == nil { 125 | log.Printf("[WARNING] could not parse pbs service name: %s", key) 126 | continue 127 | } 128 | if !modes.Modes.ContextMenu && !modes.Modes.ServicesMenu && !modes.Modes.TouchBar { 129 | disabled[m[2]] = true 130 | } 131 | } 132 | 133 | // extract main services metadata 134 | path = os.ExpandEnv(servicesList) 135 | if data, err = ioutil.ReadFile(path); err != nil { 136 | return nil, errors.Wrap(err, "read services list") 137 | } 138 | v2 := struct { 139 | Apps map[string]struct { 140 | BundleID string `plist:"bundle_id"` 141 | Name string `plist:"name"` 142 | Services []struct { 143 | Menu struct { 144 | Name string `plist:"default"` 145 | } `plist:"NSMenuItem"` 146 | Types []string `plist:"NSSendTypes"` 147 | } `plist:"service_dicts"` 148 | } `plist:"ServicesCache"` 149 | }{} 150 | 151 | if _, err = plist.Unmarshal(data, &v2); err != nil { 152 | return nil, errors.Wrap(err, "unmarshal services list") 153 | } 154 | 155 | for path, app := range v2.Apps { 156 | for _, v := range app.Services { 157 | services = append(services, Service{ 158 | Name: v.Menu.Name, 159 | Types: v.Types, 160 | AppName: app.Name, 161 | AppPath: path, 162 | Disabled: disabled[v.Menu.Name], 163 | }) 164 | } 165 | } 166 | 167 | sort.Sort(ByName(services)) 168 | 169 | return services, nil 170 | } 171 | 172 | // get clipboard data types via environment variable or script 173 | func pasteboardTypes() []string { 174 | if s := os.Getenv("PBOARD_TYPES"); s != "" { 175 | return strings.Split(s, "|") 176 | } 177 | 178 | var types []string 179 | data, err := util.Run("./PasteboardTypes.js") 180 | checkErr(err) 181 | checkErr(json.Unmarshal(data, &types)) 182 | 183 | wf.Var("PBOARD_TYPES", strings.Join(types, "|")) 184 | return types 185 | } 186 | 187 | // get contents of clipboard via environment variable or pbpaste 188 | func clipboardContents() string { 189 | if s := os.Getenv("CLIPBOARD"); s != "" { 190 | return s 191 | } 192 | 193 | data, err := util.RunCmd(exec.Command("/usr/bin/pbpaste", "-Prefer", "txt")) 194 | checkErr(err) 195 | s := string(data) 196 | wf.Var("CLIPBOARD", s) 197 | return s 198 | } 199 | 200 | func init() { 201 | aw.IconError = iconError 202 | aw.IconWarning = iconWarning 203 | 204 | wf = aw.New(update.GitHub(repo), aw.HelpURL(helpURL)) 205 | 206 | fs.SetOutput(os.Stderr) 207 | } 208 | 209 | func usage() { 210 | fmt.Fprint(fs.Output(), `alfred-services (-files|-services) [input...] 211 | 212 | Alfred workflow to run macOS services 213 | 214 | `) 215 | fs.PrintDefaults() 216 | } 217 | 218 | func run() { 219 | checkErr(fs.Parse(wf.Args())) 220 | 221 | if *flagHelp { 222 | usage() 223 | return 224 | } 225 | 226 | if *flagUpdate { 227 | wf.Configure(aw.TextErrors(true)) 228 | log.Printf("checking for update ...") 229 | checkErr(wf.CheckForUpdate()) 230 | return 231 | } 232 | 233 | var ( 234 | query = fs.Arg(0) 235 | clipboard string 236 | types []string 237 | allServices []Service 238 | services []Service 239 | err error 240 | ) 241 | 242 | log.Printf("query=%q", query) 243 | 244 | // check updates 245 | if query == "" && wf.UpdateAvailable() { 246 | wf.Configure(aw.SuppressUIDs(true)) 247 | wf.NewItem("Update Available"). 248 | Subtitle("⇥ or ↩ to update workflow"). 249 | Autocomplete("workflow:update"). 250 | Valid(false). 251 | Icon(iconUpdateAvailable) 252 | } 253 | 254 | if wf.UpdateCheckDue() { 255 | if !wf.IsRunning("update") { 256 | checkErr(wf.RunInBackground("update", exec.Command(os.Args[0], "-update"))) 257 | } 258 | } 259 | 260 | // show list of services 261 | types = pasteboardTypes() 262 | if len(types) == 0 { 263 | wf.Warn("No Data on Pasteboard", "") 264 | return 265 | } 266 | 267 | for _, s := range types { 268 | log.Printf("[pasteboard] type=%q", s) 269 | } 270 | 271 | allServices, err = loadServices() 272 | checkErr(err) 273 | log.Printf("%d total service(s)", len(allServices)) 274 | 275 | for _, s := range allServices { 276 | if !s.Disabled && s.Supports(types) { 277 | services = append(services, s) 278 | } 279 | } 280 | log.Printf("%d enabled service(s) support current pasteboard types", len(services)) 281 | if len(services) == 0 { 282 | wf.Warn("No Matching Services", "No services support the current data") 283 | return 284 | } 285 | 286 | clipboard = clipboardContents() 287 | 288 | for _, s := range services { 289 | it := wf.NewItem(s.Title()). 290 | Subtitle(s.AppName). 291 | Arg(s.Name). 292 | UID(s.UID()). 293 | Valid(true). 294 | Largetype(clipboard). 295 | Icon(s.Icon()) 296 | 297 | it.NewModifier(aw.ModCmd). 298 | Subtitle("Reveal "+s.AppPath). 299 | Arg(s.AppPath). 300 | Var("reveal", "true") 301 | } 302 | 303 | if query != "" { 304 | wf.Filter(query) 305 | } 306 | 307 | wf.WarnEmpty("No Matching Services", "Try a different query?") 308 | wf.SendFeedback() 309 | } 310 | 311 | func main() { 312 | wf.Run(run) 313 | } 314 | 315 | func checkErr(err error) { 316 | if err != nil { 317 | panic(err) 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /modd.conf: -------------------------------------------------------------------------------- 1 | 2 | magefile.go 3 | magefile_*.go { 4 | prep: " 5 | # verifying magefile 6 | mage -l 7 | " 8 | } 9 | 10 | modd.conf 11 | **/*.go 12 | !mage_*.go 13 | !vendor/** { 14 | prep: " 15 | # run unit tests 16 | go test -v @dirmods \ 17 | && mage -v run 18 | " 19 | } 20 | 21 | modd.conf 22 | *.js 23 | icons/* { 24 | prep: " 25 | # build workflow 26 | mage -v build 27 | " 28 | } --------------------------------------------------------------------------------