├── .gitignore
├── 976D393D-46DE-45CD-B8BF-083333650B0D.png
├── LICENSE.txt
├── README.md
├── getKMdata.py
├── icon.png
├── imgs
├── packal-searchclips.gif
├── searchclips.gif
└── searchvars.gif
├── info.plist
├── var_viewer.applescript
├── var_viewer.scptd
└── Contents
│ ├── Info.plist
│ ├── Resources
│ ├── Script Libraries
│ │ └── Dialog Toolkit Plus.scptd
│ │ │ └── Contents
│ │ │ ├── Info.plist
│ │ │ ├── Resources
│ │ │ ├── ScriptingDictionary.sdef
│ │ │ ├── Scripts
│ │ │ │ └── main.recover.rtf
│ │ │ └── description.rtfd
│ │ │ │ └── TXT.rtf
│ │ │ └── Script Debugger.plist
│ ├── Scripts
│ │ └── main.recover.rtf
│ └── description.rtfd
│ │ └── TXT.rtf
│ └── Script Debugger.plist
└── workflow
├── .alfredversionchecked
├── Notify.tgz
├── __init__.py
├── __init__.pyc
├── background.py
├── background.pyc
├── notify.py
├── update.py
├── update.pyc
├── util.py
├── util.pyc
├── version
├── web.py
├── web.pyc
├── workflow.py
├── workflow.pyc
├── workflow3.py
└── workflow3.pyc
/.gitignore:
--------------------------------------------------------------------------------
1 | #############
2 | # Mac Files #
3 | #############
4 | .DS_Store
5 | .Trashes
6 | .Spotlight-V100
7 | .idea
8 |
9 | *.scpt
10 |
--------------------------------------------------------------------------------
/976D393D-46DE-45CD-B8BF-083333650B0D.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/976D393D-46DE-45CD-B8BF-083333650B0D.png
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Kevin Funderburg
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Search Keyboard Maestro Variables/Clipboards
6 |
7 | Search all Keyboard Maestro variables and named clipboards in [Alfred][alfredapp].
8 | * * *
9 | 
10 | * * *
11 | 
12 | * * *
13 |
14 | ## Features
15 |
16 | - Search all variables to: view, edit, or delete
17 |
18 | - Search and view all named clipboard contents:
19 |
20 | - **When searching variables**:
21 | - Press ↩︎ to open variable in an editor window.
22 | - _Editor window actions_:
23 | - _Edit_ the value of the variable, then click `OK` (or ↩︎ if value is not multiple lines) to update variable in Keyboard Maestro
24 | - _Delete_ variable by clicking `Delete` (or ⌘D)
25 | - Press ⌘↩︎ to delete variable from Alfred window.
26 | - _NOTE:_ If the value of the variable is < 100 characters, it will display the value of the variable as the subtitle of search result
27 | - **When searching clipboards**:
28 | - Press ↩︎ to display clipboard in window.
29 |
30 | ## Installation
31 |
32 | Download [the latest release][gh-releases] and double-click the file to install in Alfred.
33 |
34 | ## Usage
35 |
36 | The two main keywords are `kmv` & `kmc`:
37 |
38 | - `kmv []` - Search all variables
39 | - ↩︎ or ⌘NUM — Open variable in editor window.
40 | - ⌘↩︎ — Delete variable in Keyboard Maestro
41 |
42 | - `kmc []` — Search all named clipboards in Keyboard Maestro.
43 | - ↩︎ or ⌘NUM — Display clipboard in window.
44 |
45 | ## Configuration
46 |
47 | ## Licensing & thanks
48 |
49 | This workflow is released under the [MIT Licence][mit].
50 |
51 | This workflow uses on the wonderful library [alfred-workflow](https://github.com/deanishe/alfred-workflow) by [@deanishe](https://github.com/deanishe).
52 |
53 | ## Changelog
54 |
55 | - v1.0.0
56 | - First public release
57 |
58 | [alfredapp]: https://www.alfredapp.com/
59 | [gh-releases]: https://github.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/releases/latest
60 | [mit]: https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/master/LICENCE.txt
61 |
--------------------------------------------------------------------------------
/getKMdata.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # encoding: utf-8
3 |
4 | from __future__ import unicode_literals
5 |
6 | import os
7 | import argparse
8 | import plistlib
9 | import sys
10 | import sqlite3
11 | from sqlite3 import Error
12 |
13 | from workflow import Workflow3, ICON_INFO, ICON_WARNING, ICON_ERROR
14 |
15 | KM_APP_SUPPORT = os.path.expanduser("~/Library/Application Support/Keyboard Maestro/")
16 | KM_APP_RESOURCES = "/System/Volumes/Data/Applications/Keyboard Maestro.app/Contents/Resources/"
17 |
18 | VARS_DB = KM_APP_SUPPORT + "Keyboard Maestro Variables.sqlite"
19 | CLIPS_PLIST = KM_APP_SUPPORT + "Keyboard Maestro Clipboards.plist"
20 | ICON_KM_VAR = KM_APP_RESOURCES + "Variable.icns"
21 | ICON_KM_CLIP = KM_APP_RESOURCES + "ClipboardIcon.icns"
22 |
23 | wf = None
24 | log = None
25 |
26 |
27 | # noinspection PyProtectedMember
28 | def main(wf):
29 | parser = argparse.ArgumentParser()
30 | parser.add_argument('-v', dest='vars', action='store_true')
31 | parser.add_argument('-c', dest='clips', action='store_true')
32 | parser.add_argument('query', nargs='?', default=None)
33 | args = parser.parse_args(wf.args)
34 |
35 | if args.vars:
36 | sql = "SELECT name, value FROM variables WHERE value IS NOT '%Delete%';"
37 |
38 | # create a database connection
39 | conn = create_connection(VARS_DB)
40 | with conn:
41 | log.info("query: " + sql)
42 | cur = conn.cursor()
43 | cur.execute(sql)
44 | rows = cur.fetchall()
45 | for row in rows:
46 | name = row[0]
47 | value = row[1]
48 | if len(value) < 100:
49 | sub = value
50 | else:
51 | sub = 'press ↩︎ to view in window'
52 |
53 | it = wf.add_item(uid=value,
54 | title=name,
55 | subtitle=sub,
56 | arg=[name,value],
57 | autocomplete=name,
58 | valid=True,
59 | icon=ICON_KM_VAR,
60 | icontype="filepath",
61 | quicklookurl=value)
62 | it.add_modifier('cmd', subtitle="delete '" + name + "'", arg=[name,value], valid=True)
63 |
64 | elif args.clips:
65 | clips_pl = plistlib.readPlist(CLIPS_PLIST)
66 | for clip in clips_pl:
67 | name = clip['Name']
68 | uid = clip['UID']
69 | it = wf.add_item(uid=uid,
70 | title=name,
71 | subtitle='press ↩︎ to view',
72 | arg=[name, uid],
73 | autocomplete=name,
74 | valid=True,
75 | icon=ICON_KM_CLIP,
76 | icontype="filepath",
77 | quicklookurl=ICON_KM_CLIP)
78 |
79 | if len(wf._items) == 0:
80 | wf.add_item('No items found', icon=ICON_WARNING)
81 |
82 | wf.send_feedback()
83 |
84 |
85 | def create_connection(db_file):
86 | """ create a database connection to the SQLite database
87 | specified by the db_file
88 | :param db_file: database file
89 | :return: Connection object or None
90 | """
91 | conn = None
92 | try:
93 | conn = sqlite3.connect(db_file)
94 | except Error as e:
95 | print(e)
96 |
97 | return conn
98 |
99 |
100 | if __name__ == '__main__':
101 | wf = Workflow3()
102 | log = wf.logger
103 | sys.exit(wf.run(main))
104 |
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/icon.png
--------------------------------------------------------------------------------
/imgs/packal-searchclips.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/imgs/packal-searchclips.gif
--------------------------------------------------------------------------------
/imgs/searchclips.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/imgs/searchclips.gif
--------------------------------------------------------------------------------
/imgs/searchvars.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/imgs/searchvars.gif
--------------------------------------------------------------------------------
/info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | bundleid
6 | com.kfunderburg.alfred-searchKMvariables
7 | category
8 | Tools
9 | connections
10 |
11 | 976D393D-46DE-45CD-B8BF-083333650B0D
12 |
13 |
14 | destinationuid
15 | 368744CA-9CE8-423A-A6F9-7AE591B10450
16 | modifiers
17 | 0
18 | modifiersubtext
19 |
20 | vitoclose
21 |
22 |
23 |
24 | F465E1CF-1023-4618-98DD-140098F68A71
25 |
26 |
27 | destinationuid
28 | 69E79431-C1D2-4F73-AE71-F8C90760C9B9
29 | modifiers
30 | 0
31 | modifiersubtext
32 |
33 | vitoclose
34 |
35 |
36 |
37 | destinationuid
38 | DF78BC78-5104-4673-80E7-BC04093A2DE9
39 | modifiers
40 | 1048576
41 | modifiersubtext
42 |
43 | vitoclose
44 |
45 |
46 |
47 |
48 | createdby
49 | Kevin Funderburg
50 | description
51 | search and view all the variables stored in Keyboard Maestro
52 | disabled
53 |
54 | name
55 | Keyboard Maestro - Search Variables
56 | objects
57 |
58 |
59 | config
60 |
61 | alfredfiltersresults
62 |
63 | alfredfiltersresultsmatchmode
64 | 0
65 | argumenttreatemptyqueryasnil
66 |
67 | argumenttrimmode
68 | 0
69 | argumenttype
70 | 1
71 | escaping
72 | 102
73 | keyword
74 | kmv
75 | queuedelaycustom
76 | 3
77 | queuedelayimmediatelyinitially
78 |
79 | queuedelaymode
80 | 0
81 | queuemode
82 | 1
83 | runningsubtext
84 | getting variables...
85 | script
86 | /usr/bin/python getKMdata.py -v
87 | scriptargtype
88 | 1
89 | scriptfile
90 |
91 | subtext
92 | type 'kmv' to view variables
93 | title
94 | view value of Keyboard Maestro variables
95 | type
96 | 5
97 | withspace
98 |
99 |
100 | type
101 | alfred.workflow.input.scriptfilter
102 | uid
103 | F465E1CF-1023-4618-98DD-140098F68A71
104 | version
105 | 3
106 |
107 |
108 | config
109 |
110 | concurrently
111 |
112 | escaping
113 | 102
114 | script
115 | osascript var_viewer.scptd "$1" "$2"
116 | scriptargtype
117 | 1
118 | scriptfile
119 |
120 | type
121 | 0
122 |
123 | type
124 | alfred.workflow.action.script
125 | uid
126 | 69E79431-C1D2-4F73-AE71-F8C90760C9B9
127 | version
128 | 2
129 |
130 |
131 | config
132 |
133 | concurrently
134 |
135 | escaping
136 | 68
137 | script
138 | use scripting additions
139 |
140 | on run argv
141 | set {_name, _val} to argv
142 |
143 | tell application "Keyboard Maestro Engine"
144 | setvariable _name to "%Delete%"
145 | end tell
146 |
147 | display notification ("'" & _name & "' deleted")
148 | end run
149 | scriptargtype
150 | 1
151 | scriptfile
152 |
153 | type
154 | 6
155 |
156 | type
157 | alfred.workflow.action.script
158 | uid
159 | DF78BC78-5104-4673-80E7-BC04093A2DE9
160 | version
161 | 2
162 |
163 |
164 | config
165 |
166 | alfredfiltersresults
167 |
168 | alfredfiltersresultsmatchmode
169 | 0
170 | argumenttreatemptyqueryasnil
171 |
172 | argumenttrimmode
173 | 0
174 | argumenttype
175 | 1
176 | escaping
177 | 102
178 | keyword
179 | kmc
180 | queuedelaycustom
181 | 3
182 | queuedelayimmediatelyinitially
183 |
184 | queuedelaymode
185 | 0
186 | queuemode
187 | 1
188 | runningsubtext
189 | getting clipboards..
190 | script
191 | /usr/bin/python getKMdata.py -c
192 | scriptargtype
193 | 1
194 | scriptfile
195 |
196 | subtext
197 | type 'kmc' to view clipboards
198 | title
199 | view Keyboard Maestro clipboards
200 | type
201 | 5
202 | withspace
203 |
204 |
205 | type
206 | alfred.workflow.input.scriptfilter
207 | uid
208 | 976D393D-46DE-45CD-B8BF-083333650B0D
209 | version
210 | 3
211 |
212 |
213 | config
214 |
215 | concurrently
216 |
217 | escaping
218 | 68
219 | script
220 | on run argv
221 | set {cname, cuid} to argv
222 | log "cname: " & cname & return & "cuid: " & cuid
223 | displayClip(cname, cuid)
224 | end run
225 |
226 |
227 | on displayClip(_name, _id)
set scpt to "<dict>
228 | <key>MacroActionType</key>
229 | <string>DisplayImage</string>
230 | <key>TargetNamedClipboardRedundantDisplayName</key>
231 | <string>" & _name & "</string>
232 | <key>TargetNamedClipboardUID</key>
233 | <string>" & _id & "</string>
234 | <key>TargetUseNamedClipboard</key>
235 | <true/>
236 | </dict>"
tell application "Keyboard Maestro Engine" to do script scpt
end displayClip
237 | scriptargtype
238 | 1
239 | scriptfile
240 |
241 | type
242 | 6
243 |
244 | type
245 | alfred.workflow.action.script
246 | uid
247 | 368744CA-9CE8-423A-A6F9-7AE591B10450
248 | version
249 | 2
250 |
251 |
252 | readme
253 | Search all Keyboard Maestro variables and clipboards.
254 | uidata
255 |
256 | 368744CA-9CE8-423A-A6F9-7AE591B10450
257 |
258 | xpos
259 | 235
260 | ypos
261 | 285
262 |
263 | 69E79431-C1D2-4F73-AE71-F8C90760C9B9
264 |
265 | note
266 | open in editor
267 | xpos
268 | 235
269 | ypos
270 | 35
271 |
272 | 976D393D-46DE-45CD-B8BF-083333650B0D
273 |
274 | colorindex
275 | 9
276 | note
277 | search clipboards
278 | xpos
279 | 10
280 | ypos
281 | 285
282 |
283 | DF78BC78-5104-4673-80E7-BC04093A2DE9
284 |
285 | note
286 | delete var
287 | xpos
288 | 235
289 | ypos
290 | 155
291 |
292 | F465E1CF-1023-4618-98DD-140098F68A71
293 |
294 | colorindex
295 | 1
296 | note
297 | search variables
298 | xpos
299 | 10
300 | ypos
301 | 35
302 |
303 |
304 | variablesdontexport
305 |
306 | version
307 | 1.0.0
308 | webaddress
309 | https://github.com/kevin-funderburg/alfred-search-keyboard-maestro-vars
310 |
311 |
312 |
--------------------------------------------------------------------------------
/var_viewer.applescript:
--------------------------------------------------------------------------------
1 | --
2 | -- Created by: Kevin Funderburg
3 | -- Created on: 9/11/20
4 | --
5 | -- Copyright © 2020 funderburg, All Rights Reserved
6 | --
7 |
8 | use AppleScript version "2.4" -- Yosemite (10.10) or later
9 | use framework "Foundation"
10 | use framework "AppKit"
11 | use scripting additions
12 | use script "Dialog Toolkit Plus"
13 | property ICON_KM : "/System/Volumes/Data/Applications/Keyboard Maestro.app/Contents/Resources/Variable.icns"
14 |
15 | on run argv
16 |
17 | if class of argv = script then
18 | set argv to {"_path", "System/Volumes/Data/Applications/Keyboard Maestro.app/Contents/Resources/Variable.icn"}
19 | end if
20 | set {_name, val} to argv
21 | set {width, height} to screensize()
22 | set maxFieldHeight to height * 0.75
23 | set lineCount to count of paragraphs of val
24 | set fieldHeight to 15 * lineCount
25 | if fieldHeight > maxFieldHeight then
26 | set fieldHeight to maxFieldHeight
27 | end if
28 | set maxWidth to 550
29 | set minWidth to 400
30 | set accWidth to getLongestLine(val)
31 | set accWidth to (accWidth / 1.5) * 10
32 | if accWidth > maxWidth then
33 | set accWidth to maxWidth
34 | else if accWidth < minWidth then
35 | set accWidth to minWidth
36 | end if
37 | set accViewWidth to accWidth
38 | set {theButtons, minWidth} to ¬
39 | create buttons {"Cancel", "Delete", "OK"} ¬
40 | button keys {"", "d", ""} ¬
41 | default button 3 ¬
42 | cancel button 1
43 | if minWidth > accViewWidth then set accViewWidth to minWidth -- make sure buttons fit
44 | set {theField, theTop} to ¬
45 | create field (val) ¬
46 | placeholder text ("Enter your text here") ¬
47 | bottom 0 ¬
48 | field width accViewWidth ¬
49 | extra height fieldHeight ¬
50 | with accepts linebreak and tab
51 |
52 | set {regLabel, theTop} to ¬
53 | create label ("OK:" & tab & tab & "Dismiss or set variable to new value" & return & ¬
54 | "Delete:" & tab & "Delete variable from Keyboard Maestro (⌘D)" & return & ¬
55 | "Cancel:" & tab & "⎋") ¬
56 | bottom theTop + 10 ¬
57 | max width accViewWidth ¬
58 | left inset 75 ¬
59 | control size regular size ¬
60 | aligns left aligned
61 |
62 | set {theRule, theTop} to ¬
63 | create rule (theTop + 12) ¬
64 | left inset 75 ¬
65 | rule width accViewWidth - 60
66 |
67 | set {boldLabel, theTop} to ¬
68 | create label ("View or edit the Keyboard Maestro variable here.") ¬
69 | bottom theTop + 10 ¬
70 | max width accViewWidth ¬
71 | left inset 75 ¬
72 | control size regular size ¬
73 | aligns left aligned ¬
74 | with bold type
75 | set {imgView, theTop} to ¬
76 | create image view (ICON_KM) ¬
77 | left inset 10 ¬
78 | bottom theTop - 50 ¬
79 | view width 50 ¬
80 | view height 50 ¬
81 | scale image scale proportionally ¬
82 | align image top left aligned
83 | set {buttonName, controlsResults} to ¬
84 | display enhanced window ("Keyboard Maestro Variable Viewer") ¬
85 | acc view width accViewWidth ¬
86 | acc view height theTop + 5 ¬
87 | acc view controls {theField, regLabel, theRule, boldLabel, imgView} ¬
88 | buttons theButtons ¬
89 | active field theField ¬
90 | initial position {0, 0} ¬
91 | with align cancel button
92 |
93 | if buttonName = "OK" then
94 | set newVal to item 1 of controlsResults
95 | if newVal ≠ val then
96 | tell application "Keyboard Maestro Engine"
97 | setvariable _name to newVal
98 | end tell
99 | display notification ("'" & _name & "' updated to " & newVal)
100 | end if
101 | end if
102 | end run
103 |
104 | on getLongestLine(txt)
105 | set max to 0
106 | repeat with p in paragraphs of txt
107 | if (count of characters of contents of p) > max then
108 | set max to count of characters of contents of p
109 | end if
110 | end repeat
111 | return max
112 | end getLongestLine
113 |
114 | on screensize()
115 | set theScreen to current application's NSScreen's mainScreen()
116 | set theFrame to theScreen's visibleFrame()
117 | set width to item 1 of item 2 of theFrame
118 | set height to item 2 of item 2 of theFrame
119 | return {width, height}
120 | end screensize
121 |
122 |
--------------------------------------------------------------------------------
/var_viewer.scptd/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleIconFile
6 | applet
7 | CFBundleIdentifier
8 | com.kfunderburg.km-var-viewer
9 | CFBundleVersion
10 | 101
11 | NSHumanReadableCopyright
12 | Copyright © 2020 funderburg, All Rights Reserved
13 |
14 |
15 |
--------------------------------------------------------------------------------
/var_viewer.scptd/Contents/Resources/Script Libraries/Dialog Toolkit Plus.scptd/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleIconFile
6 | applet
7 | CFBundleIdentifier
8 | au.com.myriad-com.lib.Dialog-Toolkit-Plus
9 | CFBundleShortVersionString
10 | 1.1.2
11 | CFBundleVersion
12 | 5
13 | LSMinimumSystemVersion
14 | 10.10.0
15 | NSHumanReadableCopyright
16 | Copyright © 2014-19 Myriad Communications, All Rights Reserved
17 | OSAAppleScriptObjCEnabled
18 |
19 | OSAScriptingDefinition
20 | ScriptingDictionary
21 | TemplateForCFBundleIdentifier
22 | au.com.myriad-com.lib.Dialog-Toolkit2
23 | WindowState
24 |
25 | bundleDividerCollapsed
26 |
27 | bundlePositionOfDivider
28 | 697
29 | dividerCollapsed
30 |
31 | eventLogLevel
32 | -1
33 | name
34 | ScriptWindowState
35 | positionOfDivider
36 | 609
37 | savedFrame
38 | 186 393 996 860 0 0 2560 1417
39 | selectedTab
40 | log
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/var_viewer.scptd/Contents/Resources/Script Libraries/Dialog Toolkit Plus.scptd/Contents/Resources/ScriptingDictionary.sdef:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
--------------------------------------------------------------------------------
/var_viewer.scptd/Contents/Resources/Script Libraries/Dialog Toolkit Plus.scptd/Contents/Resources/description.rtfd/TXT.rtf:
--------------------------------------------------------------------------------
1 | {\rtf1\ansi\ansicpg1252\cocoartf1671\cocoasubrtf500
2 | {\fonttbl}
3 | {\colortbl;\red255\green255\blue255;}
4 | {\*\expandedcolortbl;;}
5 | }
--------------------------------------------------------------------------------
/var_viewer.scptd/Contents/Resources/Script Libraries/Dialog Toolkit Plus.scptd/Contents/Script Debugger.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SD4Version
6 | 7.0.8
7 | SD5ConversionOffered
8 |
9 | SDAddUsedLibrariesOnExport
10 | 0
11 | SDAutoIncBuildNumber
12 |
13 | SDBreakOnExceptions
14 |
15 | SDBreakpointsEnabled
16 |
17 | SDBundleVersion
18 | 2.0
19 | SDCocoaTermsInProperties
20 |
21 | SDCodesignOnlyOnExport
22 |
23 | SDFolds
24 | YnBsaXN0MDDUAQIDBAUGFBVYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0
25 | b3ASAAGGoKMHCA1VJG51bGzSCQoLDFpOUy5vYmplY3RzViRjbGFzc6CAAtIODxAR
26 | WiRjbGFzc25hbWVYJGNsYXNzZXNeTlNNdXRhYmxlQXJyYXmjEBITV05TQXJyYXlY
27 | TlNPYmplY3RfEA9OU0tleWVkQXJjaGl2ZXLRFhdUcm9vdIABCBEaIy0yNztBRlFY
28 | WVtga3SDh4+Yqq2yAAAAAAAAAQEAAAAAAAAAGAAAAAAAAAAAAAAAAAAAALQ=
29 | SDHideCodeFolding
30 |
31 | SDPersistentGlobals
32 |
33 | SDWindowState
34 |
35 | eventLogWindow
36 |
37 | resultWindow
38 |
39 | traceDelay
40 | 0.80000001192092896
41 | viewControllers
42 |
43 | D475926E-89D8-42B3-826C-C4BAD24DDA3C-75749-0003A038B6B37AC8
44 |
45 | debuggerViewState
46 |
47 | expressionsState
48 |
49 | dividerPos
50 | 150
51 | scope2.changedItems
52 |
53 | scope2.emptyElements
54 |
55 | scope2.filledElements
56 |
57 | scope2.props
58 |
59 | scope2.unchangedItems
60 |
61 | showScopeBar
62 |
63 |
64 | heights4
65 |
66 | positions
67 |
68 | 3
69 |
70 | 463
71 | 586
72 | 231
73 |
74 | 5
75 |
76 | 247
77 | 100
78 | 313
79 | 80
80 | 124
81 |
82 |
83 |
84 | resultState
85 |
86 | explorerState
87 |
88 | dividerPos
89 | 150
90 | scope2.changedItems
91 |
92 | scope2.emptyElements
93 |
94 | scope2.filledElements
95 |
96 | scope2.props
97 |
98 | scope2.unchangedItems
99 |
100 | showScopeBar
101 |
102 |
103 | modes
104 |
105 | mode.best
106 |
107 | positions2
108 |
109 | positions
110 |
111 |
112 | prettyPrint
113 |
114 |
115 | stackVisible
116 |
117 | variablesState
118 |
119 | dividerPos
120 | 150
121 | scope2.changedItems
122 |
123 | scope2.emptyElements
124 |
125 | scope2.filledElements
126 |
127 | scope2.props
128 |
129 | scope2.unchangedItems
130 |
131 | showScopeBar
132 |
133 |
134 |
135 | eventLogHeight
136 | 174.5
137 | inspectorsViewState
138 |
139 | applications
140 |
141 | dividerPos2
142 | 144
143 | searchFlags
144 | 167784446
145 |
146 | clippings
147 |
148 | heights3
149 |
150 | positions
151 |
152 | 3
153 |
154 | 320
155 | 292
156 | 289
157 |
158 |
159 |
160 | tellContext
161 |
162 | explorer
163 |
164 | dividerPos
165 | 291
166 | scope2.changedItems
167 |
168 | scope2.emptyElements
169 |
170 | scope2.filledElements
171 |
172 | scope2.props
173 |
174 | scope2.unchangedItems
175 |
176 | showScopeBar
177 |
178 |
179 |
180 |
181 | mainSplitPosition
182 | 440
183 | modeIdentifier
184 | tab.rsrcs
185 | resourcesViewState
186 |
187 | heights2
188 |
189 | positions
190 |
191 | 2
192 |
193 | 430
194 | 863
195 |
196 |
197 |
198 | resources
199 |
200 | expanded
201 |
202 | selection
203 |
204 |
205 |
206 | scriptViewState
207 |
208 | editorState
209 |
210 | LNSSelection
211 |
212 |
213 | length
214 | 0
215 | location
216 | 3757
217 |
218 |
219 | LNSTextViewShowInvisibles
220 |
221 | LNSTextViewShowSpaces
222 |
223 | LNSTextViewShowTabStops
224 |
225 | LNSTextViewWrapLines
226 |
227 | LNSVisibleRect
228 | {{0, 1311}, {765, 1276}}
229 | ShowLineNumbers
230 |
231 | showCodeCoverage
232 |
233 |
234 | instanceID
235 | 2DEF509D-FD5A-4E64-B753-D60FFF4CB31F-52917-0000901E0F89C2D1
236 | resultBarVisible
237 |
238 | showProgressBar
239 |
240 |
241 | showEventLog
242 |
243 | tabHeights
244 |
245 | tab.bps
246 | 433
247 | tab.debug
248 | 439
249 | tab.rsrcs
250 | 439
251 |
252 | tabsHidden
253 |
254 | windowControllerID
255 | D475926E-89D8-42B3-826C-C4BAD24DDA3C-75749-0003A038B6B37AC8
256 | windowFrame
257 | 1018 53 1271 1364 0 0 2560 1417
258 |
259 |
260 | windowControllers
261 |
262 |
263 | controllerClass
264 | ScriptWindowController
265 | controllerID
266 | D475926E-89D8-42B3-826C-C4BAD24DDA3C-75749-0003A038B6B37AC8
267 | controllerState
268 |
269 | miniaturized
270 |
271 | windowFrame
272 | 1018 53 1271 1364 0 0 2560 1417
273 |
274 |
275 |
276 |
277 |
278 |
279 |
--------------------------------------------------------------------------------
/var_viewer.scptd/Contents/Resources/Scripts/main.recover.rtf:
--------------------------------------------------------------------------------
1 | {\rtf1\ansi\ansicpg1252\cocoartf2513
2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 FiraCode-Retina;\f1\fnil\fcharset0 FiraCode-Bold;\f2\fnil\fcharset0 FiraCode-Medium;
3 | \f3\fnil\fcharset0 Menlo-Bold;}
4 | {\colortbl;\red255\green255\blue255;\red102\green102\blue102;\red0\green0\blue0;\red251\green2\blue128;
5 | \red0\green128\blue255;\red219\green112\blue9;\red64\green128\blue2;\red0\green0\blue255;\red251\green2\blue7;
6 | \red43\green131\blue159;\red15\green128\blue255;\red128\green0\blue255;\red68\green21\blue176;\red20\green135\blue173;
7 | }
8 | {\*\expandedcolortbl;;\csgenericrgb\c40000\c40000\c40000;\csgenericrgb\c0\c0\c0;\csgenericrgb\c98604\c821\c50192;
9 | \csgenericrgb\c0\c50196\c100000;\csgenericrgb\c86026\c44067\c3627;\csgenericrgb\c25098\c50194\c896;\csgenericrgb\c0\c0\c100000;\csgenericrgb\c98624\c711\c2742;
10 | \csgenericrgb\c16899\c51199\c62499;\csgenericrgb\c5951\c50193\c99845;\csgenericrgb\c50197\c148\c99850;\csgenericrgb\c26552\c8261\c69158;\csgenericrgb\c7950\c52904\c67881;
11 | }
12 | \deftab480
13 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
14 |
15 | \f0\fs26 \cf2 --\cf3 \
16 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
17 | \cf2 -- Created by: Kevin Funderburg\cf3 \
18 | \cf2 -- Created on: 9/11/20\cf3 \
19 | \cf2 --\cf3 \
20 | \cf2 -- Copyright \'a9 2020 funderburg, All Rights Reserved\cf3 \
21 | \cf2 --\cf3 \
22 | \
23 |
24 | \f1\b use
25 | \f0\b0
26 | \f2 \cf4 AppleScript
27 | \f0 \cf3 \cf5 version\cf3 "2.4" \cf2 -- Yosemite (10.10) or later\cf3 \
28 |
29 | \f1\b use
30 | \f0\b0
31 | \f2 \cf6 framework
32 | \f0 \cf3 "Foundation"\
33 |
34 | \f1\b use
35 | \f0\b0
36 | \f2 \cf6 framework
37 | \f0 \cf3 "AppKit"\
38 |
39 | \f1\b use
40 | \f0\b0
41 | \f2 \cf6 scripting additions
42 | \f0 \cf3 \
43 |
44 | \f1\b use
45 | \f0\b0
46 | \f2 \cf6 script
47 | \f0 \cf3 "Dialog Toolkit Plus"\
48 |
49 | \f1\b property
50 | \f0\b0 \cf7 ICON_KM\cf3 : "/System/Volumes/Data/Applications/Keyboard Maestro.app/Contents/Resources/Variable.icns"\
51 | \
52 |
53 | \f1\b on
54 | \f0\b0
55 | \f1\b \cf8 run
56 | \f0\b0 \cf3 \cf7 argv\cf3 \
57 | \
58 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
59 | \cf3
60 | \f1\b if
61 | \f0\b0
62 | \f2 \cf6 class
63 | \f0 \cf3
64 | \f1\b of
65 | \f0\b0 \cf7 argv\cf3 =
66 | \f2 \cf6 script
67 | \f0 \cf3
68 | \f1\b then
69 | \f0\b0 \cf2 -- for testing\cf3 \
70 |
71 | \f1\b set
72 | \f0\b0 \cf7 argv\cf3
73 | \f1\b to
74 | \f0\b0 \{"_path", "System/Volumes/Data/Applications/Keyboard Maestro.app/Contents/Resources/Variable.icn"\}\
75 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
76 | \cf3
77 | \f1\b end
78 | \f0\b0
79 | \f1\b if
80 | \f0\b0 \
81 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
82 | \cf3
83 | \f1\b set
84 | \f0\b0 \{\cf7 _name\cf3 , \cf7 val\cf3 \}
85 | \f1\b to
86 | \f0\b0 \cf7 argv\cf3 \
87 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
88 | \cf3 \
89 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
90 | \cf3
91 | \f1\b set
92 | \f0\b0 \{\cf7 width\cf3 , \cf7 height\cf3 \}
93 | \f1\b to
94 | \f0\b0 \cf9 screensize\cf3 ()\
95 |
96 | \f1\b set
97 | \f0\b0 \cf7 maxFieldHeight\cf3
98 | \f1\b to
99 | \f0\b0 \cf7 height\cf3 * \cf10 0.75\cf3 \
100 |
101 | \f1\b set
102 | \f0\b0 \cf7 lineCount\cf3
103 | \f1\b to
104 | \f0\b0
105 | \f1\b \cf8 count
106 | \f0\b0 \cf3
107 | \f1\b of
108 | \f0\b0
109 | \f2 \cf6 paragraphs
110 | \f0 \cf3
111 | \f1\b of
112 | \f0\b0 \cf7 val\cf3 \
113 |
114 | \f1\b set
115 | \f0\b0 \cf7 fieldHeight\cf3
116 | \f1\b to
117 | \f0\b0 \cf10 15\cf3 * \cf7 lineCount\cf3 \
118 |
119 | \f1\b if
120 | \f0\b0 \cf7 fieldHeight\cf3 > \cf7 maxFieldHeight\cf3
121 | \f1\b then
122 | \f0\b0
123 | \f1\b set
124 | \f0\b0 \cf7 fieldHeight\cf3
125 | \f1\b to
126 | \f0\b0 \cf7 maxFieldHeight\cf3 \
127 |
128 | \f1\b set
129 | \f0\b0 \cf7 maxWidth\cf3
130 | \f1\b to
131 | \f0\b0 \cf10 550\cf3 \
132 |
133 | \f1\b set
134 | \f0\b0 \cf7 minWidth\cf3
135 | \f1\b to
136 | \f0\b0 \cf10 400\cf3 \
137 |
138 | \f1\b set
139 | \f0\b0 \cf7 accWidth\cf3
140 | \f1\b to
141 | \f0\b0 \cf9 getLongestLine\cf3 (\cf7 val\cf3 )\
142 |
143 | \f1\b set
144 | \f0\b0 \cf7 accWidth\cf3
145 | \f1\b to
146 | \f0\b0 (\cf7 accWidth\cf3 / \cf10 1.5\cf3 ) * \cf10 10\cf3 \
147 |
148 | \f1\b if
149 | \f0\b0 \cf7 accWidth\cf3 > \cf7 maxWidth\cf3
150 | \f1\b then
151 | \f0\b0 \
152 |
153 | \f1\b set
154 | \f0\b0 \cf7 accWidth\cf3
155 | \f1\b to
156 | \f0\b0 \cf7 maxWidth\cf3 \
157 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
158 | \cf3
159 | \f1\b else
160 | \f0\b0
161 | \f1\b if
162 | \f0\b0 \cf7 accWidth\cf3 < \cf7 minWidth\cf3
163 | \f1\b then
164 | \f0\b0 \
165 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
166 | \cf3
167 | \f1\b set
168 | \f0\b0 \cf7 accWidth\cf3
169 | \f1\b to
170 | \f0\b0 \cf7 minWidth\cf3 \
171 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
172 | \cf3
173 | \f1\b end
174 | \f0\b0
175 | \f1\b if
176 | \f0\b0 \
177 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
178 | \cf3
179 | \f1\b set
180 | \f0\b0 \cf7 accViewWidth\cf3
181 | \f1\b to
182 | \f0\b0 \cf7 accWidth\cf3 \
183 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
184 | \cf3 \
185 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
186 | \cf3
187 | \f1\b set
188 | \f0\b0 \{\cf7 theButtons\cf3 , \cf7 minWidth\cf3 \}
189 | \f1\b to
190 | \f0\b0 \'ac\
191 |
192 | \f2 \cf11 create buttons
193 | \f0 \cf3 \{"Cancel", "Delete", "OK"\} \'ac\
194 | \cf12 button keys\cf3 \{"", "d", ""\} \'ac\
195 | \cf12 default button\cf3 \cf10 3\cf3 \'ac\
196 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
197 | \cf3 \cf12 cancel button\cf3 \cf10 1\cf3 \
198 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
199 | \cf3
200 | \f1\b if
201 | \f0\b0 \cf7 minWidth\cf3 > \cf7 accViewWidth\cf3
202 | \f1\b then
203 | \f0\b0
204 | \f1\b set
205 | \f0\b0 \cf7 accViewWidth\cf3
206 | \f1\b to
207 | \f0\b0 \cf7 minWidth\cf3 \cf2 -- make sure buttons fit\cf3 \
208 |
209 | \f1\b if
210 | \f0\b0 \cf7 lineCount\cf3 > \cf10 1\cf3
211 | \f1\b then
212 | \f0\b0 \
213 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
214 | \cf3
215 | \f1\b set
216 | \f0\b0 \{\cf7 theField\cf3 , \cf7 theTop\cf3 \}
217 | \f1\b to
218 | \f0\b0 \'ac\
219 |
220 | \f2 \cf11 create field
221 | \f0 \cf3 (\cf7 val\cf3 ) \'ac\
222 | \cf12 placeholder text\cf3 ("Enter your text here") \'ac\
223 | \cf12 bottom\cf3 \cf10 0\cf3 \'ac\
224 | \cf12 field width\cf3 \cf7 accViewWidth\cf3 \'ac\
225 | \cf12 extra height\cf3 \cf7 fieldHeight\cf3 \'ac\
226 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
227 | \cf3
228 | \f1\b with
229 | \f0\b0 \cf12 accepts linebreak and tab\cf3 \
230 |
231 | \f1\b else
232 | \f0\b0 \
233 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
234 | \cf3
235 | \f1\b set
236 | \f0\b0 \{\cf7 theField\cf3 , \cf7 theTop\cf3 \}
237 | \f1\b to
238 | \f0\b0 \'ac\
239 |
240 | \f2 \cf11 create field
241 | \f0 \cf3 (\cf7 val\cf3 ) \'ac\
242 | \cf12 placeholder text\cf3 ("Enter your text here") \'ac\
243 | \cf12 bottom\cf3 \cf10 0\cf3 \'ac\
244 | \cf12 field width\cf3 \cf7 accViewWidth\cf3 \'ac\
245 | \cf12 extra height\cf3 \cf7 fieldHeight\cf3 \'ac\
246 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
247 | \cf3
248 | \f1\b without
249 | \f0\b0 \cf12 accepts linebreak and tab\cf3 \
250 |
251 | \f1\b end
252 | \f0\b0
253 | \f1\b if
254 | \f0\b0 \
255 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
256 | \cf3
257 | \f1\b set
258 | \f0\b0 \{\cf7 regLabel\cf3 , \cf7 theTop\cf3 \}
259 | \f1\b to
260 | \f0\b0 \'ac\
261 |
262 | \f2 \cf11 create label
263 | \f0 \cf3 ("OK:" &
264 | \f2 \cf4 tab
265 | \f0 \cf3 &
266 | \f2 \cf4 tab
267 | \f0 \cf3 & "Dismiss or set variable to new value" &
268 | \f2 \cf4 return
269 | \f0 \cf3 & \'ac\
270 | "Delete:" &
271 | \f2 \cf4 tab
272 | \f0 \cf3 & "Delete variable from Keyboard Maestro (
273 | \f3\b \uc0\u8984
274 | \f0\b0 D)" &
275 | \f2 \cf4 return
276 | \f0 \cf3 & \'ac\
277 | "Cancel:" &
278 | \f2 \cf4 tab
279 | \f0 \cf3 & "
280 | \f3\b \uc0\u9099
281 | \f0\b0 ") \'ac\
282 | \cf12 bottom\cf3 \cf7 theTop\cf3 + \cf10 10\cf3 \'ac\
283 | \cf12 max width\cf3 \cf7 accViewWidth\cf3 \'ac\
284 | \cf12 left inset\cf3 \cf10 75\cf3 \'ac\
285 | \cf12 control size\cf3 \cf13 regular size\cf3 \'ac\
286 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
287 | \cf3 \cf12 aligns\cf3 \cf13 left aligned\cf3 \
288 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
289 | \cf3
290 | \f1\b set
291 | \f0\b0 \{\cf7 theRule\cf3 , \cf7 theTop\cf3 \}
292 | \f1\b to
293 | \f0\b0 \'ac\
294 |
295 | \f2 \cf11 create rule
296 | \f0 \cf3 (\cf7 theTop\cf3 + \cf10 12\cf3 ) \'ac\
297 | \cf12 left inset\cf3 \cf10 75\cf3 \'ac\
298 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
299 | \cf3 \cf12 rule width\cf3 \cf7 accViewWidth\cf3 - \cf10 60\cf3 \
300 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
301 | \cf3
302 | \f1\b set
303 | \f0\b0 \{\cf7 boldLabel\cf3 , \cf7 theTop\cf3 \}
304 | \f1\b to
305 | \f0\b0 \'ac\
306 |
307 | \f2 \cf11 create label
308 | \f0 \cf3 ("View or edit the Keyboard Maestro variable here.") \'ac\
309 | \cf12 bottom\cf3 \cf7 theTop\cf3 + \cf10 10\cf3 \'ac\
310 | \cf12 max width\cf3 \cf7 accViewWidth\cf3 \'ac\
311 | \cf12 left inset\cf3 \cf10 75\cf3 \'ac\
312 | \cf12 control size\cf3 \cf13 regular size\cf3 \'ac\
313 | \cf12 aligns\cf3 \cf13 left aligned\cf3 \'ac\
314 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
315 | \cf3
316 | \f1\b with
317 | \f0\b0 \cf12 bold type\cf3 \
318 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
319 | \cf3
320 | \f1\b set
321 | \f0\b0 \{\cf7 imgView\cf3 , \cf7 theTop\cf3 \}
322 | \f1\b to
323 | \f0\b0 \'ac\
324 |
325 | \f2 \cf11 create image view
326 | \f0 \cf3 (\cf7 ICON_KM\cf3 ) \'ac\
327 | \cf12 left inset\cf3 \cf10 10\cf3 \'ac\
328 | \cf12 bottom\cf3 \cf7 theTop\cf3 - \cf10 50\cf3 \'ac\
329 | \cf12 view width\cf3 \cf10 50\cf3 \'ac\
330 | \cf12 view height\cf3 \cf10 50\cf3 \'ac\
331 | \cf12 scale image\cf3 \cf13 scale proportionally\cf3 \'ac\
332 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
333 | \cf3 \cf12 align image\cf3 \cf13 top left aligned\cf3 \
334 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
335 | \cf3
336 | \f1\b set
337 | \f0\b0 \{\cf7 buttonName\cf3 , \cf7 controlsResults\cf3 \}
338 | \f1\b to
339 | \f0\b0 \'ac\
340 |
341 | \f2 \cf11 display enhanced window
342 | \f0 \cf3 ("Keyboard Maestro Variable Viewer") \'ac\
343 | \cf12 acc view width\cf3 \cf7 accViewWidth\cf3 \'ac\
344 | \cf12 acc view height\cf3 \cf7 theTop\cf3 + \cf10 5\cf3 \'ac\
345 | \cf12 acc view controls\cf3 \{\cf7 theField\cf3 , \cf7 regLabel\cf3 , \cf7 theRule\cf3 , \cf7 boldLabel\cf3 , \cf7 imgView\cf3 \} \'ac\
346 | \cf12 buttons\cf3 \cf7 theButtons\cf3 \'ac\
347 | \cf12 active field\cf3 \cf7 theField\cf3 \'ac\
348 | \cf12 initial position\cf3 \{\} \'ac\
349 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
350 | \cf3
351 | \f1\b with
352 | \f0\b0 \cf12 align cancel button\cf3 \
353 | \
354 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
355 | \cf3
356 | \f1\b if
357 | \f0\b0 \cf7 buttonName\cf3 = "OK"
358 | \f1\b then
359 | \f0\b0 \
360 |
361 | \f1\b set
362 | \f0\b0 \cf7 newVal\cf3
363 | \f1\b to
364 | \f0\b0
365 | \f2 \cf6 item
366 | \f0 \cf3 \cf10 1\cf3
367 | \f1\b of
368 | \f0\b0 \cf7 controlsResults\cf3 \
369 |
370 | \f1\b if
371 | \f0\b0 \cf7 newVal\cf3 \uc0\u8800 \cf7 val\cf3
372 | \f1\b then
373 | \f0\b0 \
374 |
375 | \f1\b tell
376 | \f0\b0
377 | \f2 \cf6 application
378 | \f0 \cf3 "Keyboard Maestro Engine"
379 | \f1\b to
380 | \f0\b0
381 | \f1\b \cf8 setvariable
382 | \f0\b0 \cf3 \cf7 _name\cf3 \cf5 to\cf3 \cf7 newVal\cf3 \
383 |
384 | \f2 \cf11 display notification
385 | \f0 \cf3 ("'" & \cf7 _name\cf3 & "' updated to " & \cf7 newVal\cf3 )\
386 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
387 | \cf3
388 | \f1\b end
389 | \f0\b0
390 | \f1\b if
391 | \f0\b0 \
392 |
393 | \f1\b end
394 | \f0\b0
395 | \f1\b if
396 | \f0\b0 \
397 | \
398 |
399 | \f1\b end
400 | \f0\b0
401 | \f1\b \cf8 run
402 | \f0\b0 \cf3 \
403 | \
404 |
405 | \f1\b on
406 | \f0\b0 \cf9 getLongestLine\cf3 (\cf7 txt\cf3 )\
407 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
408 | \cf3
409 | \f1\b set
410 | \f0\b0 \cf7 max\cf3
411 | \f1\b to
412 | \f0\b0 \cf10 0\cf3 \
413 |
414 | \f1\b repeat
415 | \f0\b0
416 | \f1\b with
417 | \f0\b0 \cf7 p\cf3
418 | \f1\b in
419 | \f0\b0
420 | \f2 \cf6 paragraphs
421 | \f0 \cf3
422 | \f1\b of
423 | \f0\b0 \cf7 txt\cf3 \
424 |
425 | \f1\b if
426 | \f0\b0 (
427 | \f1\b \cf8 count
428 | \f0\b0 \cf3
429 | \f1\b of
430 | \f0\b0
431 | \f2 \cf6 characters
432 | \f0 \cf3
433 | \f1\b of
434 | \f0\b0
435 | \f2 \cf4 contents
436 | \f0 \cf3
437 | \f1\b of
438 | \f0\b0 \cf7 p\cf3 ) > \cf7 max\cf3
439 | \f1\b then
440 | \f0\b0 \
441 |
442 | \f1\b set
443 | \f0\b0 \cf7 max\cf3
444 | \f1\b to
445 | \f0\b0
446 | \f1\b \cf8 count
447 | \f0\b0 \cf3
448 | \f1\b of
449 | \f0\b0
450 | \f2 \cf6 characters
451 | \f0 \cf3
452 | \f1\b of
453 | \f0\b0
454 | \f2 \cf4 contents
455 | \f0 \cf3
456 | \f1\b of
457 | \f0\b0 \cf7 p\cf3 \
458 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
459 | \cf3
460 | \f1\b end
461 | \f0\b0
462 | \f1\b if
463 | \f0\b0 \
464 |
465 | \f1\b end
466 | \f0\b0
467 | \f1\b repeat
468 | \f0\b0 \
469 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
470 | \cf3
471 | \f1\b return
472 | \f0\b0 \cf7 max\cf3 \
473 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
474 |
475 | \f1\b \cf3 end
476 | \f0\b0 \cf9 getLongestLine\cf3 \
477 | \
478 |
479 | \f1\b on
480 | \f0\b0 \cf9 screensize\cf3 ()\
481 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
482 | \cf3
483 | \f1\b set
484 | \f0\b0 \cf7 theScreen\cf3
485 | \f1\b to
486 | \f0\b0 \cf14 current application's\cf3 \cf7 NSScreen's\cf3 \cf9 mainScreen\cf3 ()\
487 |
488 | \f1\b set
489 | \f0\b0 \cf7 theFrame\cf3
490 | \f1\b to
491 | \f0\b0 \cf7 theScreen's\cf3 \cf9 visibleFrame\cf3 ()\
492 |
493 | \f1\b set
494 | \f0\b0 \cf7 width\cf3
495 | \f1\b to
496 | \f0\b0
497 | \f2 \cf6 item
498 | \f0 \cf3 \cf10 1\cf3
499 | \f1\b of
500 | \f0\b0
501 | \f2 \cf6 item
502 | \f0 \cf3 \cf10 2\cf3
503 | \f1\b of
504 | \f0\b0 \cf7 theFrame\cf3 \
505 |
506 | \f1\b set
507 | \f0\b0 \cf7 height\cf3
508 | \f1\b to
509 | \f0\b0
510 | \f2 \cf6 item
511 | \f0 \cf3 \cf10 2\cf3
512 | \f1\b of
513 | \f0\b0
514 | \f2 \cf6 item
515 | \f0 \cf3 \cf10 2\cf3
516 | \f1\b of
517 | \f0\b0 \cf7 theFrame\cf3 \
518 |
519 | \f1\b return
520 | \f0\b0 \{\cf7 width\cf3 , \cf7 height\cf3 \}\
521 | \pard\pardeftab480\slleading40\pardirnatural\partightenfactor0
522 |
523 | \f1\b \cf3 end
524 | \f0\b0 \cf9 screensize\cf3 \
525 | }
--------------------------------------------------------------------------------
/var_viewer.scptd/Contents/Resources/description.rtfd/TXT.rtf:
--------------------------------------------------------------------------------
1 | {\rtf1\ansi\ansicpg1252\cocoartf2513
2 | \cocoatextscaling0\cocoaplatform0{\fonttbl}
3 | {\colortbl;\red255\green255\blue255;}
4 | {\*\expandedcolortbl;;}
5 | }
--------------------------------------------------------------------------------
/var_viewer.scptd/Contents/Script Debugger.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SD4Version
6 | 7.0.12
7 | SD5ConversionOffered
8 |
9 | SDAddUsedLibrariesOnExport
10 | 0
11 | SDAutoIncBuildNumber
12 |
13 | SDBreakOnExceptions
14 |
15 | SDBreakpointsEnabled
16 |
17 | SDBundleVersion
18 | 2.0
19 | SDCocoaTermsInProperties
20 |
21 | SDCodesignOnlyOnExport
22 |
23 | SDFolds
24 | YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVj
25 | dHMSAAGGoF8QD05TS2V5ZWRBcmNoaXZlctEICVRyb290gAGjCwwRVSRudWxs0g0O
26 | DxBaTlMub2JqZWN0c1YkY2xhc3OggALSEhMUFVokY2xhc3NuYW1lWCRjbGFzc2Vz
27 | Xk5TTXV0YWJsZUFycmF5oxQWF1dOU0FycmF5WE5TT2JqZWN0CBEaJCkyN0lMUVNX
28 | XWJtdHV3fIeQn6OrAAAAAAAAAQEAAAAAAAAAGAAAAAAAAAAAAAAAAAAAALQ=
29 | SDHideCodeFolding
30 |
31 | SDPersistentGlobals
32 |
33 | SDWindowState
34 |
35 | eventLogWindow
36 |
37 | resultWindow
38 |
39 | traceDelay
40 | 0.80000001192092896
41 | viewControllers
42 |
43 | A3630FCB-B1E9-405D-BF78-9101D438F6C6-526-000003905843201B
44 |
45 | debuggerViewState
46 |
47 | expressionsState
48 |
49 | dividerPos
50 | 150
51 | scope2.changedItems
52 |
53 | scope2.emptyElements
54 |
55 | scope2.filledElements
56 |
57 | scope2.props
58 |
59 | scope2.unchangedItems
60 |
61 | showScopeBar
62 |
63 |
64 | heights4
65 |
66 | positions
67 |
68 | 3
69 |
70 | 312
71 | 396
72 | 155
73 |
74 | 5
75 |
76 | 280
77 | 54
78 | 325
79 | 81
80 | 88
81 |
82 |
83 |
84 | resultState
85 |
86 | explorerState
87 |
88 | dividerPos
89 | 150
90 | scope2.changedItems
91 |
92 | scope2.emptyElements
93 |
94 | scope2.filledElements
95 |
96 | scope2.props
97 |
98 | scope2.unchangedItems
99 |
100 | showScopeBar
101 |
102 |
103 | modes
104 |
105 | mode.best
106 |
107 | positions2
108 |
109 | positions
110 |
111 |
112 | prettyPrint
113 |
114 | sourceState
115 |
116 | text
117 |
118 | LNSSelection
119 |
120 |
121 | length
122 | 0
123 | location
124 | 16
125 |
126 |
127 | LNSTextViewShowInvisibles
128 |
129 | LNSTextViewShowSpaces
130 |
131 | LNSTextViewShowTabStops
132 |
133 | LNSTextViewWrapLines
134 |
135 | LNSVisibleRect
136 | {{0, 0}, {424, 250.5}}
137 |
138 |
139 | textState
140 |
141 | text
142 |
143 | LNSSelection
144 |
145 |
146 | length
147 | 0
148 | location
149 | 85
150 |
151 |
152 | LNSTextViewShowInvisibles
153 |
154 | LNSTextViewShowSpaces
155 |
156 | LNSTextViewShowTabStops
157 |
158 | LNSTextViewWrapLines
159 |
160 | LNSVisibleRect
161 | {{0, 0}, {424, 256.5}}
162 |
163 |
164 |
165 | stackVisible
166 |
167 | variablesState
168 |
169 | dividerPos
170 | 150
171 | scope2.changedItems
172 |
173 | scope2.emptyElements
174 |
175 | scope2.filledElements
176 |
177 | scope2.props
178 |
179 | scope2.unchangedItems
180 |
181 | showScopeBar
182 |
183 |
184 |
185 | eventLogHeight
186 | 174.5
187 | eventLogViewState
188 |
189 | format
190 | EventLogScopeBar_Source
191 | resultState
192 |
193 | modes
194 |
195 | mode.best
196 |
197 | positions2
198 |
199 | positions
200 |
201 |
202 | prettyPrint
203 |
204 |
205 | resultViewerWidth
206 | 410
207 | scopeVisible
208 |
209 | showResultViewer
210 |
211 |
212 | inspectorsViewState
213 |
214 | applications
215 |
216 | dividerPos2
217 | 144
218 | searchFlags
219 | 167784446
220 |
221 | clippings
222 |
223 | heights3
224 |
225 | positions
226 |
227 | 3
228 |
229 | 85
230 | 147
231 | 669
232 |
233 |
234 |
235 | tellContext
236 |
237 | explorer
238 |
239 | dividerPos
240 | 291
241 | scope2.changedItems
242 |
243 | scope2.emptyElements
244 |
245 | scope2.filledElements
246 |
247 | scope2.props
248 |
249 | scope2.unchangedItems
250 |
251 | showScopeBar
252 |
253 |
254 |
255 |
256 | mainSplitPosition
257 | 440
258 | modeIdentifier
259 | tab.debug
260 | resourcesViewState
261 |
262 | heights2
263 |
264 | positions
265 |
266 | 2
267 |
268 | 252
269 | 613
270 |
271 |
272 |
273 | resources
274 |
275 | expanded
276 |
277 | /Users/kevinfunderburg/Dropbox/Library/Application Support/Alfred/Alfred.alfredpreferences/workflows/user.workflow.A9B9BCB0-3BB9-4B1C-AC17-14B74DA87BE0/var_viewer.scptd/Contents/Resources/Script Libraries
278 |
279 | selection
280 |
281 |
282 |
283 | scriptViewState
284 |
285 | editorState
286 |
287 | LNSSelection
288 |
289 |
290 | length
291 | 0
292 | location
293 | 907
294 |
295 |
296 | LNSTextViewShowInvisibles
297 |
298 | LNSTextViewShowSpaces
299 |
300 | LNSTextViewShowTabStops
301 |
302 | LNSTextViewWrapLines
303 |
304 | LNSVisibleRect
305 | {{0, 515}, {786, 694.5}}
306 | ShowLineNumbers
307 |
308 | showCodeCoverage
309 |
310 |
311 | instanceID
312 | 4D62FCD7-592D-4ECC-BF06-C698BEA4D19A-2196-00000F39DA4B05C8
313 | resultBarVisible
314 |
315 | showProgressBar
316 |
317 |
318 | showEventLog
319 |
320 | tabHeights
321 |
322 | tab.bps
323 | 433
324 | tab.debug
325 | 439
326 | tab.rsrcs
327 | 433
328 |
329 | tabsHidden
330 |
331 | windowControllerID
332 | A3630FCB-B1E9-405D-BF78-9101D438F6C6-526-000003905843201B
333 | windowFrame
334 | 408 62 1272 965 0 0 1680 1027
335 |
336 |
337 | windowControllers
338 |
339 |
340 | controllerClass
341 | ScriptWindowController
342 | controllerID
343 | A3630FCB-B1E9-405D-BF78-9101D438F6C6-526-000003905843201B
344 | controllerState
345 |
346 | miniaturized
347 |
348 | windowFrame
349 | 408 62 1272 965 0 0 1680 1027
350 |
351 |
352 |
353 |
354 |
355 |
356 |
--------------------------------------------------------------------------------
/workflow/.alfredversionchecked:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/workflow/.alfredversionchecked
--------------------------------------------------------------------------------
/workflow/Notify.tgz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/workflow/Notify.tgz
--------------------------------------------------------------------------------
/workflow/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 | #
4 | # Copyright (c) 2014 Dean Jackson
5 | #
6 | # MIT Licence. See http://opensource.org/licenses/MIT
7 | #
8 | # Created on 2014-02-15
9 | #
10 |
11 | """A helper library for `Alfred `_ workflows."""
12 |
13 | import os
14 |
15 | # Workflow objects
16 | from .workflow import Workflow, manager
17 | from .workflow3 import Variables, Workflow3
18 |
19 | # Exceptions
20 | from .workflow import PasswordNotFound, KeychainError
21 |
22 | # Icons
23 | from .workflow import (
24 | ICON_ACCOUNT,
25 | ICON_BURN,
26 | ICON_CLOCK,
27 | ICON_COLOR,
28 | ICON_COLOUR,
29 | ICON_EJECT,
30 | ICON_ERROR,
31 | ICON_FAVORITE,
32 | ICON_FAVOURITE,
33 | ICON_GROUP,
34 | ICON_HELP,
35 | ICON_HOME,
36 | ICON_INFO,
37 | ICON_NETWORK,
38 | ICON_NOTE,
39 | ICON_SETTINGS,
40 | ICON_SWIRL,
41 | ICON_SWITCH,
42 | ICON_SYNC,
43 | ICON_TRASH,
44 | ICON_USER,
45 | ICON_WARNING,
46 | ICON_WEB,
47 | )
48 |
49 | # Filter matching rules
50 | from .workflow import (
51 | MATCH_ALL,
52 | MATCH_ALLCHARS,
53 | MATCH_ATOM,
54 | MATCH_CAPITALS,
55 | MATCH_INITIALS,
56 | MATCH_INITIALS_CONTAIN,
57 | MATCH_INITIALS_STARTSWITH,
58 | MATCH_STARTSWITH,
59 | MATCH_SUBSTRING,
60 | )
61 |
62 |
63 | __title__ = 'Alfred-Workflow'
64 | __version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read()
65 | __author__ = 'Dean Jackson'
66 | __licence__ = 'MIT'
67 | __copyright__ = 'Copyright 2014-2019 Dean Jackson'
68 |
69 | __all__ = [
70 | 'Variables',
71 | 'Workflow',
72 | 'Workflow3',
73 | 'manager',
74 | 'PasswordNotFound',
75 | 'KeychainError',
76 | 'ICON_ACCOUNT',
77 | 'ICON_BURN',
78 | 'ICON_CLOCK',
79 | 'ICON_COLOR',
80 | 'ICON_COLOUR',
81 | 'ICON_EJECT',
82 | 'ICON_ERROR',
83 | 'ICON_FAVORITE',
84 | 'ICON_FAVOURITE',
85 | 'ICON_GROUP',
86 | 'ICON_HELP',
87 | 'ICON_HOME',
88 | 'ICON_INFO',
89 | 'ICON_NETWORK',
90 | 'ICON_NOTE',
91 | 'ICON_SETTINGS',
92 | 'ICON_SWIRL',
93 | 'ICON_SWITCH',
94 | 'ICON_SYNC',
95 | 'ICON_TRASH',
96 | 'ICON_USER',
97 | 'ICON_WARNING',
98 | 'ICON_WEB',
99 | 'MATCH_ALL',
100 | 'MATCH_ALLCHARS',
101 | 'MATCH_ATOM',
102 | 'MATCH_CAPITALS',
103 | 'MATCH_INITIALS',
104 | 'MATCH_INITIALS_CONTAIN',
105 | 'MATCH_INITIALS_STARTSWITH',
106 | 'MATCH_STARTSWITH',
107 | 'MATCH_SUBSTRING',
108 | ]
109 |
--------------------------------------------------------------------------------
/workflow/__init__.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/workflow/__init__.pyc
--------------------------------------------------------------------------------
/workflow/background.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 | #
4 | # Copyright (c) 2014 deanishe@deanishe.net
5 | #
6 | # MIT Licence. See http://opensource.org/licenses/MIT
7 | #
8 | # Created on 2014-04-06
9 | #
10 |
11 | """This module provides an API to run commands in background processes.
12 |
13 | Combine with the :ref:`caching API ` to work from cached data
14 | while you fetch fresh data in the background.
15 |
16 | See :ref:`the User Manual ` for more information
17 | and examples.
18 | """
19 |
20 | from __future__ import print_function, unicode_literals
21 |
22 | import signal
23 | import sys
24 | import os
25 | import subprocess
26 | import pickle
27 |
28 | from workflow import Workflow
29 |
30 | __all__ = ['is_running', 'run_in_background']
31 |
32 | _wf = None
33 |
34 |
35 | def wf():
36 | global _wf
37 | if _wf is None:
38 | _wf = Workflow()
39 | return _wf
40 |
41 |
42 | def _log():
43 | return wf().logger
44 |
45 |
46 | def _arg_cache(name):
47 | """Return path to pickle cache file for arguments.
48 |
49 | :param name: name of task
50 | :type name: ``unicode``
51 | :returns: Path to cache file
52 | :rtype: ``unicode`` filepath
53 |
54 | """
55 | return wf().cachefile(name + '.argcache')
56 |
57 |
58 | def _pid_file(name):
59 | """Return path to PID file for ``name``.
60 |
61 | :param name: name of task
62 | :type name: ``unicode``
63 | :returns: Path to PID file for task
64 | :rtype: ``unicode`` filepath
65 |
66 | """
67 | return wf().cachefile(name + '.pid')
68 |
69 |
70 | def _process_exists(pid):
71 | """Check if a process with PID ``pid`` exists.
72 |
73 | :param pid: PID to check
74 | :type pid: ``int``
75 | :returns: ``True`` if process exists, else ``False``
76 | :rtype: ``Boolean``
77 |
78 | """
79 | try:
80 | os.kill(pid, 0)
81 | except OSError: # not running
82 | return False
83 | return True
84 |
85 |
86 | def _job_pid(name):
87 | """Get PID of job or `None` if job does not exist.
88 |
89 | Args:
90 | name (str): Name of job.
91 |
92 | Returns:
93 | int: PID of job process (or `None` if job doesn't exist).
94 | """
95 | pidfile = _pid_file(name)
96 | if not os.path.exists(pidfile):
97 | return
98 |
99 | with open(pidfile, 'rb') as fp:
100 | pid = int(fp.read())
101 |
102 | if _process_exists(pid):
103 | return pid
104 |
105 | try:
106 | os.unlink(pidfile)
107 | except Exception: # pragma: no cover
108 | pass
109 |
110 |
111 | def is_running(name):
112 | """Test whether task ``name`` is currently running.
113 |
114 | :param name: name of task
115 | :type name: unicode
116 | :returns: ``True`` if task with name ``name`` is running, else ``False``
117 | :rtype: bool
118 |
119 | """
120 | if _job_pid(name) is not None:
121 | return True
122 |
123 | return False
124 |
125 |
126 | def _background(pidfile, stdin='/dev/null', stdout='/dev/null',
127 | stderr='/dev/null'): # pragma: no cover
128 | """Fork the current process into a background daemon.
129 |
130 | :param pidfile: file to write PID of daemon process to.
131 | :type pidfile: filepath
132 | :param stdin: where to read input
133 | :type stdin: filepath
134 | :param stdout: where to write stdout output
135 | :type stdout: filepath
136 | :param stderr: where to write stderr output
137 | :type stderr: filepath
138 |
139 | """
140 | def _fork_and_exit_parent(errmsg, wait=False, write=False):
141 | try:
142 | pid = os.fork()
143 | if pid > 0:
144 | if write: # write PID of child process to `pidfile`
145 | tmp = pidfile + '.tmp'
146 | with open(tmp, 'wb') as fp:
147 | fp.write(str(pid))
148 | os.rename(tmp, pidfile)
149 | if wait: # wait for child process to exit
150 | os.waitpid(pid, 0)
151 | os._exit(0)
152 | except OSError as err:
153 | _log().critical('%s: (%d) %s', errmsg, err.errno, err.strerror)
154 | raise err
155 |
156 | # Do first fork and wait for second fork to finish.
157 | _fork_and_exit_parent('fork #1 failed', wait=True)
158 |
159 | # Decouple from parent environment.
160 | os.chdir(wf().workflowdir)
161 | os.setsid()
162 |
163 | # Do second fork and write PID to pidfile.
164 | _fork_and_exit_parent('fork #2 failed', write=True)
165 |
166 | # Now I am a daemon!
167 | # Redirect standard file descriptors.
168 | si = open(stdin, 'r', 0)
169 | so = open(stdout, 'a+', 0)
170 | se = open(stderr, 'a+', 0)
171 | if hasattr(sys.stdin, 'fileno'):
172 | os.dup2(si.fileno(), sys.stdin.fileno())
173 | if hasattr(sys.stdout, 'fileno'):
174 | os.dup2(so.fileno(), sys.stdout.fileno())
175 | if hasattr(sys.stderr, 'fileno'):
176 | os.dup2(se.fileno(), sys.stderr.fileno())
177 |
178 |
179 | def kill(name, sig=signal.SIGTERM):
180 | """Send a signal to job ``name`` via :func:`os.kill`.
181 |
182 | .. versionadded:: 1.29
183 |
184 | Args:
185 | name (str): Name of the job
186 | sig (int, optional): Signal to send (default: SIGTERM)
187 |
188 | Returns:
189 | bool: `False` if job isn't running, `True` if signal was sent.
190 | """
191 | pid = _job_pid(name)
192 | if pid is None:
193 | return False
194 |
195 | os.kill(pid, sig)
196 | return True
197 |
198 |
199 | def run_in_background(name, args, **kwargs):
200 | r"""Cache arguments then call this script again via :func:`subprocess.call`.
201 |
202 | :param name: name of job
203 | :type name: unicode
204 | :param args: arguments passed as first argument to :func:`subprocess.call`
205 | :param \**kwargs: keyword arguments to :func:`subprocess.call`
206 | :returns: exit code of sub-process
207 | :rtype: int
208 |
209 | When you call this function, it caches its arguments and then calls
210 | ``background.py`` in a subprocess. The Python subprocess will load the
211 | cached arguments, fork into the background, and then run the command you
212 | specified.
213 |
214 | This function will return as soon as the ``background.py`` subprocess has
215 | forked, returning the exit code of *that* process (i.e. not of the command
216 | you're trying to run).
217 |
218 | If that process fails, an error will be written to the log file.
219 |
220 | If a process is already running under the same name, this function will
221 | return immediately and will not run the specified command.
222 |
223 | """
224 | if is_running(name):
225 | _log().info('[%s] job already running', name)
226 | return
227 |
228 | argcache = _arg_cache(name)
229 |
230 | # Cache arguments
231 | with open(argcache, 'wb') as fp:
232 | pickle.dump({'args': args, 'kwargs': kwargs}, fp)
233 | _log().debug('[%s] command cached: %s', name, argcache)
234 |
235 | # Call this script
236 | cmd = ['/usr/bin/python', __file__, name]
237 | _log().debug('[%s] passing job to background runner: %r', name, cmd)
238 | retcode = subprocess.call(cmd)
239 |
240 | if retcode: # pragma: no cover
241 | _log().error('[%s] background runner failed with %d', name, retcode)
242 | else:
243 | _log().debug('[%s] background job started', name)
244 |
245 | return retcode
246 |
247 |
248 | def main(wf): # pragma: no cover
249 | """Run command in a background process.
250 |
251 | Load cached arguments, fork into background, then call
252 | :meth:`subprocess.call` with cached arguments.
253 |
254 | """
255 | log = wf.logger
256 | name = wf.args[0]
257 | argcache = _arg_cache(name)
258 | if not os.path.exists(argcache):
259 | msg = '[{0}] command cache not found: {1}'.format(name, argcache)
260 | log.critical(msg)
261 | raise IOError(msg)
262 |
263 | # Fork to background and run command
264 | pidfile = _pid_file(name)
265 | _background(pidfile)
266 |
267 | # Load cached arguments
268 | with open(argcache, 'rb') as fp:
269 | data = pickle.load(fp)
270 |
271 | # Cached arguments
272 | args = data['args']
273 | kwargs = data['kwargs']
274 |
275 | # Delete argument cache file
276 | os.unlink(argcache)
277 |
278 | try:
279 | # Run the command
280 | log.debug('[%s] running command: %r', name, args)
281 |
282 | retcode = subprocess.call(args, **kwargs)
283 |
284 | if retcode:
285 | log.error('[%s] command failed with status %d', name, retcode)
286 | finally:
287 | os.unlink(pidfile)
288 |
289 | log.debug('[%s] job complete', name)
290 |
291 |
292 | if __name__ == '__main__': # pragma: no cover
293 | wf().run(main)
294 |
--------------------------------------------------------------------------------
/workflow/background.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/workflow/background.pyc
--------------------------------------------------------------------------------
/workflow/notify.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 | #
4 | # Copyright (c) 2015 deanishe@deanishe.net
5 | #
6 | # MIT Licence. See http://opensource.org/licenses/MIT
7 | #
8 | # Created on 2015-11-26
9 | #
10 |
11 | # TODO: Exclude this module from test and code coverage in py2.6
12 |
13 | """
14 | Post notifications via the macOS Notification Center.
15 |
16 | This feature is only available on Mountain Lion (10.8) and later.
17 | It will silently fail on older systems.
18 |
19 | The main API is a single function, :func:`~workflow.notify.notify`.
20 |
21 | It works by copying a simple application to your workflow's data
22 | directory. It replaces the application's icon with your workflow's
23 | icon and then calls the application to post notifications.
24 | """
25 |
26 | from __future__ import print_function, unicode_literals
27 |
28 | import os
29 | import plistlib
30 | import shutil
31 | import subprocess
32 | import sys
33 | import tarfile
34 | import tempfile
35 | import uuid
36 |
37 | import workflow
38 |
39 |
40 | _wf = None
41 | _log = None
42 |
43 |
44 | #: Available system sounds from System Preferences > Sound > Sound Effects
45 | SOUNDS = (
46 | 'Basso',
47 | 'Blow',
48 | 'Bottle',
49 | 'Frog',
50 | 'Funk',
51 | 'Glass',
52 | 'Hero',
53 | 'Morse',
54 | 'Ping',
55 | 'Pop',
56 | 'Purr',
57 | 'Sosumi',
58 | 'Submarine',
59 | 'Tink',
60 | )
61 |
62 |
63 | def wf():
64 | """Return Workflow object for this module.
65 |
66 | Returns:
67 | workflow.Workflow: Workflow object for current workflow.
68 | """
69 | global _wf
70 | if _wf is None:
71 | _wf = workflow.Workflow()
72 | return _wf
73 |
74 |
75 | def log():
76 | """Return logger for this module.
77 |
78 | Returns:
79 | logging.Logger: Logger for this module.
80 | """
81 | global _log
82 | if _log is None:
83 | _log = wf().logger
84 | return _log
85 |
86 |
87 | def notifier_program():
88 | """Return path to notifier applet executable.
89 |
90 | Returns:
91 | unicode: Path to Notify.app ``applet`` executable.
92 | """
93 | return wf().datafile('Notify.app/Contents/MacOS/applet')
94 |
95 |
96 | def notifier_icon_path():
97 | """Return path to icon file in installed Notify.app.
98 |
99 | Returns:
100 | unicode: Path to ``applet.icns`` within the app bundle.
101 | """
102 | return wf().datafile('Notify.app/Contents/Resources/applet.icns')
103 |
104 |
105 | def install_notifier():
106 | """Extract ``Notify.app`` from the workflow to data directory.
107 |
108 | Changes the bundle ID of the installed app and gives it the
109 | workflow's icon.
110 | """
111 | archive = os.path.join(os.path.dirname(__file__), 'Notify.tgz')
112 | destdir = wf().datadir
113 | app_path = os.path.join(destdir, 'Notify.app')
114 | n = notifier_program()
115 | log().debug('installing Notify.app to %r ...', destdir)
116 | # z = zipfile.ZipFile(archive, 'r')
117 | # z.extractall(destdir)
118 | tgz = tarfile.open(archive, 'r:gz')
119 | tgz.extractall(destdir)
120 | assert os.path.exists(n), \
121 | 'Notify.app could not be installed in %s' % destdir
122 |
123 | # Replace applet icon
124 | icon = notifier_icon_path()
125 | workflow_icon = wf().workflowfile('icon.png')
126 | if os.path.exists(icon):
127 | os.unlink(icon)
128 |
129 | png_to_icns(workflow_icon, icon)
130 |
131 | # Set file icon
132 | # PyObjC isn't available for 2.6, so this is 2.7 only. Actually,
133 | # none of this code will "work" on pre-10.8 systems. Let it run
134 | # until I figure out a better way of excluding this module
135 | # from coverage in py2.6.
136 | if sys.version_info >= (2, 7): # pragma: no cover
137 | from AppKit import NSWorkspace, NSImage
138 |
139 | ws = NSWorkspace.sharedWorkspace()
140 | img = NSImage.alloc().init()
141 | img.initWithContentsOfFile_(icon)
142 | ws.setIcon_forFile_options_(img, app_path, 0)
143 |
144 | # Change bundle ID of installed app
145 | ip_path = os.path.join(app_path, 'Contents/Info.plist')
146 | bundle_id = '{0}.{1}'.format(wf().bundleid, uuid.uuid4().hex)
147 | data = plistlib.readPlist(ip_path)
148 | log().debug('changing bundle ID to %r', bundle_id)
149 | data['CFBundleIdentifier'] = bundle_id
150 | plistlib.writePlist(data, ip_path)
151 |
152 |
153 | def validate_sound(sound):
154 | """Coerce ``sound`` to valid sound name.
155 |
156 | Returns ``None`` for invalid sounds. Sound names can be found
157 | in ``System Preferences > Sound > Sound Effects``.
158 |
159 | Args:
160 | sound (str): Name of system sound.
161 |
162 | Returns:
163 | str: Proper name of sound or ``None``.
164 | """
165 | if not sound:
166 | return None
167 |
168 | # Case-insensitive comparison of `sound`
169 | if sound.lower() in [s.lower() for s in SOUNDS]:
170 | # Title-case is correct for all system sounds as of macOS 10.11
171 | return sound.title()
172 | return None
173 |
174 |
175 | def notify(title='', text='', sound=None):
176 | """Post notification via Notify.app helper.
177 |
178 | Args:
179 | title (str, optional): Notification title.
180 | text (str, optional): Notification body text.
181 | sound (str, optional): Name of sound to play.
182 |
183 | Raises:
184 | ValueError: Raised if both ``title`` and ``text`` are empty.
185 |
186 | Returns:
187 | bool: ``True`` if notification was posted, else ``False``.
188 | """
189 | if title == text == '':
190 | raise ValueError('Empty notification')
191 |
192 | sound = validate_sound(sound) or ''
193 |
194 | n = notifier_program()
195 |
196 | if not os.path.exists(n):
197 | install_notifier()
198 |
199 | env = os.environ.copy()
200 | enc = 'utf-8'
201 | env['NOTIFY_TITLE'] = title.encode(enc)
202 | env['NOTIFY_MESSAGE'] = text.encode(enc)
203 | env['NOTIFY_SOUND'] = sound.encode(enc)
204 | cmd = [n]
205 | retcode = subprocess.call(cmd, env=env)
206 | if retcode == 0:
207 | return True
208 |
209 | log().error('Notify.app exited with status {0}.'.format(retcode))
210 | return False
211 |
212 |
213 | def convert_image(inpath, outpath, size):
214 | """Convert an image file using ``sips``.
215 |
216 | Args:
217 | inpath (str): Path of source file.
218 | outpath (str): Path to destination file.
219 | size (int): Width and height of destination image in pixels.
220 |
221 | Raises:
222 | RuntimeError: Raised if ``sips`` exits with non-zero status.
223 | """
224 | cmd = [
225 | b'sips',
226 | b'-z', str(size), str(size),
227 | inpath,
228 | b'--out', outpath]
229 | # log().debug(cmd)
230 | with open(os.devnull, 'w') as pipe:
231 | retcode = subprocess.call(cmd, stdout=pipe, stderr=subprocess.STDOUT)
232 |
233 | if retcode != 0:
234 | raise RuntimeError('sips exited with %d' % retcode)
235 |
236 |
237 | def png_to_icns(png_path, icns_path):
238 | """Convert PNG file to ICNS using ``iconutil``.
239 |
240 | Create an iconset from the source PNG file. Generate PNG files
241 | in each size required by macOS, then call ``iconutil`` to turn
242 | them into a single ICNS file.
243 |
244 | Args:
245 | png_path (str): Path to source PNG file.
246 | icns_path (str): Path to destination ICNS file.
247 |
248 | Raises:
249 | RuntimeError: Raised if ``iconutil`` or ``sips`` fail.
250 | """
251 | tempdir = tempfile.mkdtemp(prefix='aw-', dir=wf().datadir)
252 |
253 | try:
254 | iconset = os.path.join(tempdir, 'Icon.iconset')
255 |
256 | assert not os.path.exists(iconset), \
257 | 'iconset already exists: ' + iconset
258 | os.makedirs(iconset)
259 |
260 | # Copy source icon to icon set and generate all the other
261 | # sizes needed
262 | configs = []
263 | for i in (16, 32, 128, 256, 512):
264 | configs.append(('icon_{0}x{0}.png'.format(i), i))
265 | configs.append((('icon_{0}x{0}@2x.png'.format(i), i * 2)))
266 |
267 | shutil.copy(png_path, os.path.join(iconset, 'icon_256x256.png'))
268 | shutil.copy(png_path, os.path.join(iconset, 'icon_128x128@2x.png'))
269 |
270 | for name, size in configs:
271 | outpath = os.path.join(iconset, name)
272 | if os.path.exists(outpath):
273 | continue
274 | convert_image(png_path, outpath, size)
275 |
276 | cmd = [
277 | b'iconutil',
278 | b'-c', b'icns',
279 | b'-o', icns_path,
280 | iconset]
281 |
282 | retcode = subprocess.call(cmd)
283 | if retcode != 0:
284 | raise RuntimeError('iconset exited with %d' % retcode)
285 |
286 | assert os.path.exists(icns_path), \
287 | 'generated ICNS file not found: ' + repr(icns_path)
288 | finally:
289 | try:
290 | shutil.rmtree(tempdir)
291 | except OSError: # pragma: no cover
292 | pass
293 |
294 |
295 | if __name__ == '__main__': # pragma: nocover
296 | # Simple command-line script to test module with
297 | # This won't work on 2.6, as `argparse` isn't available
298 | # by default.
299 | import argparse
300 |
301 | from unicodedata import normalize
302 |
303 | def ustr(s):
304 | """Coerce `s` to normalised Unicode."""
305 | return normalize('NFD', s.decode('utf-8'))
306 |
307 | p = argparse.ArgumentParser()
308 | p.add_argument('-p', '--png', help="PNG image to convert to ICNS.")
309 | p.add_argument('-l', '--list-sounds', help="Show available sounds.",
310 | action='store_true')
311 | p.add_argument('-t', '--title',
312 | help="Notification title.", type=ustr,
313 | default='')
314 | p.add_argument('-s', '--sound', type=ustr,
315 | help="Optional notification sound.", default='')
316 | p.add_argument('text', type=ustr,
317 | help="Notification body text.", default='', nargs='?')
318 | o = p.parse_args()
319 |
320 | # List available sounds
321 | if o.list_sounds:
322 | for sound in SOUNDS:
323 | print(sound)
324 | sys.exit(0)
325 |
326 | # Convert PNG to ICNS
327 | if o.png:
328 | icns = os.path.join(
329 | os.path.dirname(o.png),
330 | os.path.splitext(os.path.basename(o.png))[0] + '.icns')
331 |
332 | print('converting {0!r} to {1!r} ...'.format(o.png, icns),
333 | file=sys.stderr)
334 |
335 | assert not os.path.exists(icns), \
336 | 'destination file already exists: ' + icns
337 |
338 | png_to_icns(o.png, icns)
339 | sys.exit(0)
340 |
341 | # Post notification
342 | if o.title == o.text == '':
343 | print('ERROR: empty notification.', file=sys.stderr)
344 | sys.exit(1)
345 | else:
346 | notify(o.title, o.text, o.sound)
347 |
--------------------------------------------------------------------------------
/workflow/update.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 | #
4 | # Copyright (c) 2014 Fabio Niephaus ,
5 | # Dean Jackson
6 | #
7 | # MIT Licence. See http://opensource.org/licenses/MIT
8 | #
9 | # Created on 2014-08-16
10 | #
11 |
12 | """Self-updating from GitHub.
13 |
14 | .. versionadded:: 1.9
15 |
16 | .. note::
17 |
18 | This module is not intended to be used directly. Automatic updates
19 | are controlled by the ``update_settings`` :class:`dict` passed to
20 | :class:`~workflow.workflow.Workflow` objects.
21 |
22 | """
23 |
24 | from __future__ import print_function, unicode_literals
25 |
26 | from collections import defaultdict
27 | from functools import total_ordering
28 | import json
29 | import os
30 | import tempfile
31 | import re
32 | import subprocess
33 |
34 | import workflow
35 | import web
36 |
37 | # __all__ = []
38 |
39 |
40 | RELEASES_BASE = 'https://api.github.com/repos/{}/releases'
41 | match_workflow = re.compile(r'\.alfred(\d+)?workflow$').search
42 |
43 | _wf = None
44 |
45 |
46 | def wf():
47 | """Lazy `Workflow` object."""
48 | global _wf
49 | if _wf is None:
50 | _wf = workflow.Workflow()
51 | return _wf
52 |
53 |
54 | @total_ordering
55 | class Download(object):
56 | """A workflow file that is available for download.
57 |
58 | .. versionadded: 1.37
59 |
60 | Attributes:
61 | url (str): URL of workflow file.
62 | filename (str): Filename of workflow file.
63 | version (Version): Semantic version of workflow.
64 | prerelease (bool): Whether version is a pre-release.
65 | alfred_version (Version): Minimum compatible version
66 | of Alfred.
67 |
68 | """
69 |
70 | @classmethod
71 | def from_dict(cls, d):
72 | """Create a `Download` from a `dict`."""
73 | return cls(url=d['url'], filename=d['filename'],
74 | version=Version(d['version']),
75 | prerelease=d['prerelease'])
76 |
77 | @classmethod
78 | def from_releases(cls, js):
79 | """Extract downloads from GitHub releases.
80 |
81 | Searches releases with semantic tags for assets with
82 | file extension .alfredworkflow or .alfredXworkflow where
83 | X is a number.
84 |
85 | Files are returned sorted by latest version first. Any
86 | releases containing multiple files with the same (workflow)
87 | extension are rejected as ambiguous.
88 |
89 | Args:
90 | js (str): JSON response from GitHub's releases endpoint.
91 |
92 | Returns:
93 | list: Sequence of `Download`.
94 | """
95 | releases = json.loads(js)
96 | downloads = []
97 | for release in releases:
98 | tag = release['tag_name']
99 | dupes = defaultdict(int)
100 | try:
101 | version = Version(tag)
102 | except ValueError as err:
103 | wf().logger.debug('ignored release: bad version "%s": %s',
104 | tag, err)
105 | continue
106 |
107 | dls = []
108 | for asset in release.get('assets', []):
109 | url = asset.get('browser_download_url')
110 | filename = os.path.basename(url)
111 | m = match_workflow(filename)
112 | if not m:
113 | wf().logger.debug('unwanted file: %s', filename)
114 | continue
115 |
116 | ext = m.group(0)
117 | dupes[ext] = dupes[ext] + 1
118 | dls.append(Download(url, filename, version,
119 | release['prerelease']))
120 |
121 | valid = True
122 | for ext, n in dupes.items():
123 | if n > 1:
124 | wf().logger.debug('ignored release "%s": multiple assets '
125 | 'with extension "%s"', tag, ext)
126 | valid = False
127 | break
128 |
129 | if valid:
130 | downloads.extend(dls)
131 |
132 | downloads.sort(reverse=True)
133 | return downloads
134 |
135 | def __init__(self, url, filename, version, prerelease=False):
136 | """Create a new Download.
137 |
138 | Args:
139 | url (str): URL of workflow file.
140 | filename (str): Filename of workflow file.
141 | version (Version): Version of workflow.
142 | prerelease (bool, optional): Whether version is
143 | pre-release. Defaults to False.
144 |
145 | """
146 | if isinstance(version, basestring):
147 | version = Version(version)
148 |
149 | self.url = url
150 | self.filename = filename
151 | self.version = version
152 | self.prerelease = prerelease
153 |
154 | @property
155 | def alfred_version(self):
156 | """Minimum Alfred version based on filename extension."""
157 | m = match_workflow(self.filename)
158 | if not m or not m.group(1):
159 | return Version('0')
160 | return Version(m.group(1))
161 |
162 | @property
163 | def dict(self):
164 | """Convert `Download` to `dict`."""
165 | return dict(url=self.url, filename=self.filename,
166 | version=str(self.version), prerelease=self.prerelease)
167 |
168 | def __str__(self):
169 | """Format `Download` for printing."""
170 | u = ('Download(url={dl.url!r}, '
171 | 'filename={dl.filename!r}, '
172 | 'version={dl.version!r}, '
173 | 'prerelease={dl.prerelease!r})'.format(dl=self))
174 |
175 | return u.encode('utf-8')
176 |
177 | def __repr__(self):
178 | """Code-like representation of `Download`."""
179 | return str(self)
180 |
181 | def __eq__(self, other):
182 | """Compare Downloads based on version numbers."""
183 | if self.url != other.url \
184 | or self.filename != other.filename \
185 | or self.version != other.version \
186 | or self.prerelease != other.prerelease:
187 | return False
188 | return True
189 |
190 | def __ne__(self, other):
191 | """Compare Downloads based on version numbers."""
192 | return not self.__eq__(other)
193 |
194 | def __lt__(self, other):
195 | """Compare Downloads based on version numbers."""
196 | if self.version != other.version:
197 | return self.version < other.version
198 | return self.alfred_version < other.alfred_version
199 |
200 |
201 | class Version(object):
202 | """Mostly semantic versioning.
203 |
204 | The main difference to proper :ref:`semantic versioning `
205 | is that this implementation doesn't require a minor or patch version.
206 |
207 | Version strings may also be prefixed with "v", e.g.:
208 |
209 | >>> v = Version('v1.1.1')
210 | >>> v.tuple
211 | (1, 1, 1, '')
212 |
213 | >>> v = Version('2.0')
214 | >>> v.tuple
215 | (2, 0, 0, '')
216 |
217 | >>> Version('3.1-beta').tuple
218 | (3, 1, 0, 'beta')
219 |
220 | >>> Version('1.0.1') > Version('0.0.1')
221 | True
222 | """
223 |
224 | #: Match version and pre-release/build information in version strings
225 | match_version = re.compile(r'([0-9\.]+)(.+)?').match
226 |
227 | def __init__(self, vstr):
228 | """Create new `Version` object.
229 |
230 | Args:
231 | vstr (basestring): Semantic version string.
232 | """
233 | if not vstr:
234 | raise ValueError('invalid version number: {!r}'.format(vstr))
235 |
236 | self.vstr = vstr
237 | self.major = 0
238 | self.minor = 0
239 | self.patch = 0
240 | self.suffix = ''
241 | self.build = ''
242 | self._parse(vstr)
243 |
244 | def _parse(self, vstr):
245 | if vstr.startswith('v'):
246 | m = self.match_version(vstr[1:])
247 | else:
248 | m = self.match_version(vstr)
249 | if not m:
250 | raise ValueError('invalid version number: {!r}'.format(vstr))
251 |
252 | version, suffix = m.groups()
253 | parts = self._parse_dotted_string(version)
254 | self.major = parts.pop(0)
255 | if len(parts):
256 | self.minor = parts.pop(0)
257 | if len(parts):
258 | self.patch = parts.pop(0)
259 | if not len(parts) == 0:
260 | raise ValueError('version number too long: {!r}'.format(vstr))
261 |
262 | if suffix:
263 | # Build info
264 | idx = suffix.find('+')
265 | if idx > -1:
266 | self.build = suffix[idx+1:]
267 | suffix = suffix[:idx]
268 | if suffix:
269 | if not suffix.startswith('-'):
270 | raise ValueError(
271 | 'suffix must start with - : {0}'.format(suffix))
272 | self.suffix = suffix[1:]
273 |
274 | # wf().logger.debug('version str `{}` -> {}'.format(vstr, repr(self)))
275 |
276 | def _parse_dotted_string(self, s):
277 | """Parse string ``s`` into list of ints and strings."""
278 | parsed = []
279 | parts = s.split('.')
280 | for p in parts:
281 | if p.isdigit():
282 | p = int(p)
283 | parsed.append(p)
284 | return parsed
285 |
286 | @property
287 | def tuple(self):
288 | """Version number as a tuple of major, minor, patch, pre-release."""
289 | return (self.major, self.minor, self.patch, self.suffix)
290 |
291 | def __lt__(self, other):
292 | """Implement comparison."""
293 | if not isinstance(other, Version):
294 | raise ValueError('not a Version instance: {0!r}'.format(other))
295 | t = self.tuple[:3]
296 | o = other.tuple[:3]
297 | if t < o:
298 | return True
299 | if t == o: # We need to compare suffixes
300 | if self.suffix and not other.suffix:
301 | return True
302 | if other.suffix and not self.suffix:
303 | return False
304 | return self._parse_dotted_string(self.suffix) \
305 | < self._parse_dotted_string(other.suffix)
306 | # t > o
307 | return False
308 |
309 | def __eq__(self, other):
310 | """Implement comparison."""
311 | if not isinstance(other, Version):
312 | raise ValueError('not a Version instance: {0!r}'.format(other))
313 | return self.tuple == other.tuple
314 |
315 | def __ne__(self, other):
316 | """Implement comparison."""
317 | return not self.__eq__(other)
318 |
319 | def __gt__(self, other):
320 | """Implement comparison."""
321 | if not isinstance(other, Version):
322 | raise ValueError('not a Version instance: {0!r}'.format(other))
323 | return other.__lt__(self)
324 |
325 | def __le__(self, other):
326 | """Implement comparison."""
327 | if not isinstance(other, Version):
328 | raise ValueError('not a Version instance: {0!r}'.format(other))
329 | return not other.__lt__(self)
330 |
331 | def __ge__(self, other):
332 | """Implement comparison."""
333 | return not self.__lt__(other)
334 |
335 | def __str__(self):
336 | """Return semantic version string."""
337 | vstr = '{0}.{1}.{2}'.format(self.major, self.minor, self.patch)
338 | if self.suffix:
339 | vstr = '{0}-{1}'.format(vstr, self.suffix)
340 | if self.build:
341 | vstr = '{0}+{1}'.format(vstr, self.build)
342 | return vstr
343 |
344 | def __repr__(self):
345 | """Return 'code' representation of `Version`."""
346 | return "Version('{0}')".format(str(self))
347 |
348 |
349 | def retrieve_download(dl):
350 | """Saves a download to a temporary file and returns path.
351 |
352 | .. versionadded: 1.37
353 |
354 | Args:
355 | url (unicode): URL to .alfredworkflow file in GitHub repo
356 |
357 | Returns:
358 | unicode: path to downloaded file
359 |
360 | """
361 | if not match_workflow(dl.filename):
362 | raise ValueError('attachment not a workflow: ' + dl.filename)
363 |
364 | path = os.path.join(tempfile.gettempdir(), dl.filename)
365 | wf().logger.debug('downloading update from '
366 | '%r to %r ...', dl.url, path)
367 |
368 | r = web.get(dl.url)
369 | r.raise_for_status()
370 |
371 | r.save_to_path(path)
372 |
373 | return path
374 |
375 |
376 | def build_api_url(repo):
377 | """Generate releases URL from GitHub repo.
378 |
379 | Args:
380 | repo (unicode): Repo name in form ``username/repo``
381 |
382 | Returns:
383 | unicode: URL to the API endpoint for the repo's releases
384 |
385 | """
386 | if len(repo.split('/')) != 2:
387 | raise ValueError('invalid GitHub repo: {!r}'.format(repo))
388 |
389 | return RELEASES_BASE.format(repo)
390 |
391 |
392 | def get_downloads(repo):
393 | """Load available ``Download``s for GitHub repo.
394 |
395 | .. versionadded: 1.37
396 |
397 | Args:
398 | repo (unicode): GitHub repo to load releases for.
399 |
400 | Returns:
401 | list: Sequence of `Download` contained in GitHub releases.
402 | """
403 | url = build_api_url(repo)
404 |
405 | def _fetch():
406 | wf().logger.info('retrieving releases for %r ...', repo)
407 | r = web.get(url)
408 | r.raise_for_status()
409 | return r.content
410 |
411 | key = 'github-releases-' + repo.replace('/', '-')
412 | js = wf().cached_data(key, _fetch, max_age=60)
413 |
414 | return Download.from_releases(js)
415 |
416 |
417 | def latest_download(dls, alfred_version=None, prereleases=False):
418 | """Return newest `Download`."""
419 | alfred_version = alfred_version or os.getenv('alfred_version')
420 | version = None
421 | if alfred_version:
422 | version = Version(alfred_version)
423 |
424 | dls.sort(reverse=True)
425 | for dl in dls:
426 | if dl.prerelease and not prereleases:
427 | wf().logger.debug('ignored prerelease: %s', dl.version)
428 | continue
429 | if version and dl.alfred_version > version:
430 | wf().logger.debug('ignored incompatible (%s > %s): %s',
431 | dl.alfred_version, version, dl.filename)
432 | continue
433 |
434 | wf().logger.debug('latest version: %s (%s)', dl.version, dl.filename)
435 | return dl
436 |
437 | return None
438 |
439 |
440 | def check_update(repo, current_version, prereleases=False,
441 | alfred_version=None):
442 | """Check whether a newer release is available on GitHub.
443 |
444 | Args:
445 | repo (unicode): ``username/repo`` for workflow's GitHub repo
446 | current_version (unicode): the currently installed version of the
447 | workflow. :ref:`Semantic versioning ` is required.
448 | prereleases (bool): Whether to include pre-releases.
449 | alfred_version (unicode): version of currently-running Alfred.
450 | if empty, defaults to ``$alfred_version`` environment variable.
451 |
452 | Returns:
453 | bool: ``True`` if an update is available, else ``False``
454 |
455 | If an update is available, its version number and download URL will
456 | be cached.
457 |
458 | """
459 | key = '__workflow_latest_version'
460 | # data stored when no update is available
461 | no_update = {
462 | 'available': False,
463 | 'download': None,
464 | 'version': None,
465 | }
466 | current = Version(current_version)
467 |
468 | dls = get_downloads(repo)
469 | if not len(dls):
470 | wf().logger.warning('no valid downloads for %s', repo)
471 | wf().cache_data(key, no_update)
472 | return False
473 |
474 | wf().logger.info('%d download(s) for %s', len(dls), repo)
475 |
476 | dl = latest_download(dls, alfred_version, prereleases)
477 |
478 | if not dl:
479 | wf().logger.warning('no compatible downloads for %s', repo)
480 | wf().cache_data(key, no_update)
481 | return False
482 |
483 | wf().logger.debug('latest=%r, installed=%r', dl.version, current)
484 |
485 | if dl.version > current:
486 | wf().cache_data(key, {
487 | 'version': str(dl.version),
488 | 'download': dl.dict,
489 | 'available': True,
490 | })
491 | return True
492 |
493 | wf().cache_data(key, no_update)
494 | return False
495 |
496 |
497 | def install_update():
498 | """If a newer release is available, download and install it.
499 |
500 | :returns: ``True`` if an update is installed, else ``False``
501 |
502 | """
503 | key = '__workflow_latest_version'
504 | # data stored when no update is available
505 | no_update = {
506 | 'available': False,
507 | 'download': None,
508 | 'version': None,
509 | }
510 | status = wf().cached_data(key, max_age=0)
511 |
512 | if not status or not status.get('available'):
513 | wf().logger.info('no update available')
514 | return False
515 |
516 | dl = status.get('download')
517 | if not dl:
518 | wf().logger.info('no download information')
519 | return False
520 |
521 | path = retrieve_download(Download.from_dict(dl))
522 |
523 | wf().logger.info('installing updated workflow ...')
524 | subprocess.call(['open', path])
525 |
526 | wf().cache_data(key, no_update)
527 | return True
528 |
529 |
530 | if __name__ == '__main__': # pragma: nocover
531 | import sys
532 |
533 | prereleases = False
534 |
535 | def show_help(status=0):
536 | """Print help message."""
537 | print('usage: update.py (check|install) '
538 | '[--prereleases] ')
539 | sys.exit(status)
540 |
541 | argv = sys.argv[:]
542 | if '-h' in argv or '--help' in argv:
543 | show_help()
544 |
545 | if '--prereleases' in argv:
546 | argv.remove('--prereleases')
547 | prereleases = True
548 |
549 | if len(argv) != 4:
550 | show_help(1)
551 |
552 | action = argv[1]
553 | repo = argv[2]
554 | version = argv[3]
555 |
556 | try:
557 |
558 | if action == 'check':
559 | check_update(repo, version, prereleases)
560 | elif action == 'install':
561 | install_update()
562 | else:
563 | show_help(1)
564 |
565 | except Exception as err: # ensure traceback is in log file
566 | wf().logger.exception(err)
567 | raise err
568 |
--------------------------------------------------------------------------------
/workflow/update.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/workflow/update.pyc
--------------------------------------------------------------------------------
/workflow/util.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # encoding: utf-8
3 | #
4 | # Copyright (c) 2017 Dean Jackson
5 | #
6 | # MIT Licence. See http://opensource.org/licenses/MIT
7 | #
8 | # Created on 2017-12-17
9 | #
10 |
11 | """A selection of helper functions useful for building workflows."""
12 |
13 | from __future__ import print_function, absolute_import
14 |
15 | import atexit
16 | from collections import namedtuple
17 | from contextlib import contextmanager
18 | import errno
19 | import fcntl
20 | import functools
21 | import json
22 | import os
23 | import signal
24 | import subprocess
25 | import sys
26 | from threading import Event
27 | import time
28 |
29 | # JXA scripts to call Alfred's API via the Scripting Bridge
30 | # {app} is automatically replaced with "Alfred 3" or
31 | # "com.runningwithcrayons.Alfred" depending on version.
32 | #
33 | # Open Alfred in search (regular) mode
34 | JXA_SEARCH = "Application({app}).search({arg});"
35 | # Open Alfred's File Actions on an argument
36 | JXA_ACTION = "Application({app}).action({arg});"
37 | # Open Alfred's navigation mode at path
38 | JXA_BROWSE = "Application({app}).browse({arg});"
39 | # Set the specified theme
40 | JXA_SET_THEME = "Application({app}).setTheme({arg});"
41 | # Call an External Trigger
42 | JXA_TRIGGER = "Application({app}).runTrigger({arg}, {opts});"
43 | # Save a variable to the workflow configuration sheet/info.plist
44 | JXA_SET_CONFIG = "Application({app}).setConfiguration({arg}, {opts});"
45 | # Delete a variable from the workflow configuration sheet/info.plist
46 | JXA_UNSET_CONFIG = "Application({app}).removeConfiguration({arg}, {opts});"
47 |
48 |
49 | class AcquisitionError(Exception):
50 | """Raised if a lock cannot be acquired."""
51 |
52 |
53 | AppInfo = namedtuple('AppInfo', ['name', 'path', 'bundleid'])
54 | """Information about an installed application.
55 |
56 | Returned by :func:`appinfo`. All attributes are Unicode.
57 |
58 | .. py:attribute:: name
59 |
60 | Name of the application, e.g. ``u'Safari'``.
61 |
62 | .. py:attribute:: path
63 |
64 | Path to the application bundle, e.g. ``u'/Applications/Safari.app'``.
65 |
66 | .. py:attribute:: bundleid
67 |
68 | Application's bundle ID, e.g. ``u'com.apple.Safari'``.
69 |
70 | """
71 |
72 |
73 | def jxa_app_name():
74 | """Return name of application to call currently running Alfred.
75 |
76 | .. versionadded: 1.37
77 |
78 | Returns 'Alfred 3' or 'com.runningwithcrayons.Alfred' depending
79 | on which version of Alfred is running.
80 |
81 | This name is suitable for use with ``Application(name)`` in JXA.
82 |
83 | Returns:
84 | unicode: Application name or ID.
85 |
86 | """
87 | if os.getenv('alfred_version', '').startswith('3'):
88 | # Alfred 3
89 | return u'Alfred 3'
90 | # Alfred 4+
91 | return u'com.runningwithcrayons.Alfred'
92 |
93 |
94 | def unicodify(s, encoding='utf-8', norm=None):
95 | """Ensure string is Unicode.
96 |
97 | .. versionadded:: 1.31
98 |
99 | Decode encoded strings using ``encoding`` and normalise Unicode
100 | to form ``norm`` if specified.
101 |
102 | Args:
103 | s (str): String to decode. May also be Unicode.
104 | encoding (str, optional): Encoding to use on bytestrings.
105 | norm (None, optional): Normalisation form to apply to Unicode string.
106 |
107 | Returns:
108 | unicode: Decoded, optionally normalised, Unicode string.
109 |
110 | """
111 | if not isinstance(s, unicode):
112 | s = unicode(s, encoding)
113 |
114 | if norm:
115 | from unicodedata import normalize
116 | s = normalize(norm, s)
117 |
118 | return s
119 |
120 |
121 | def utf8ify(s):
122 | """Ensure string is a bytestring.
123 |
124 | .. versionadded:: 1.31
125 |
126 | Returns `str` objects unchanced, encodes `unicode` objects to
127 | UTF-8, and calls :func:`str` on anything else.
128 |
129 | Args:
130 | s (object): A Python object
131 |
132 | Returns:
133 | str: UTF-8 string or string representation of s.
134 |
135 | """
136 | if isinstance(s, str):
137 | return s
138 |
139 | if isinstance(s, unicode):
140 | return s.encode('utf-8')
141 |
142 | return str(s)
143 |
144 |
145 | def applescriptify(s):
146 | """Escape string for insertion into an AppleScript string.
147 |
148 | .. versionadded:: 1.31
149 |
150 | Replaces ``"`` with `"& quote &"`. Use this function if you want
151 |
152 | to insert a string into an AppleScript script:
153 | >>> query = 'g "python" test'
154 | >>> applescriptify(query)
155 | 'g " & quote & "python" & quote & "test'
156 |
157 | Args:
158 | s (unicode): Unicode string to escape.
159 |
160 | Returns:
161 | unicode: Escaped string
162 |
163 | """
164 | return s.replace(u'"', u'" & quote & "')
165 |
166 |
167 | def run_command(cmd, **kwargs):
168 | """Run a command and return the output.
169 |
170 | .. versionadded:: 1.31
171 |
172 | A thin wrapper around :func:`subprocess.check_output` that ensures
173 | all arguments are encoded to UTF-8 first.
174 |
175 | Args:
176 | cmd (list): Command arguments to pass to ``check_output``.
177 | **kwargs: Keyword arguments to pass to ``check_output``.
178 |
179 | Returns:
180 | str: Output returned by ``check_output``.
181 |
182 | """
183 | cmd = [utf8ify(s) for s in cmd]
184 | return subprocess.check_output(cmd, **kwargs)
185 |
186 |
187 | def run_applescript(script, *args, **kwargs):
188 | """Execute an AppleScript script and return its output.
189 |
190 | .. versionadded:: 1.31
191 |
192 | Run AppleScript either by filepath or code. If ``script`` is a valid
193 | filepath, that script will be run, otherwise ``script`` is treated
194 | as code.
195 |
196 | Args:
197 | script (str, optional): Filepath of script or code to run.
198 | *args: Optional command-line arguments to pass to the script.
199 | **kwargs: Pass ``lang`` to run a language other than AppleScript.
200 |
201 | Returns:
202 | str: Output of run command.
203 |
204 | """
205 | lang = 'AppleScript'
206 | if 'lang' in kwargs:
207 | lang = kwargs['lang']
208 | del kwargs['lang']
209 |
210 | cmd = ['/usr/bin/osascript', '-l', lang]
211 |
212 | if os.path.exists(script):
213 | cmd += [script]
214 | else:
215 | cmd += ['-e', script]
216 |
217 | cmd.extend(args)
218 |
219 | return run_command(cmd, **kwargs)
220 |
221 |
222 | def run_jxa(script, *args):
223 | """Execute a JXA script and return its output.
224 |
225 | .. versionadded:: 1.31
226 |
227 | Wrapper around :func:`run_applescript` that passes ``lang=JavaScript``.
228 |
229 | Args:
230 | script (str): Filepath of script or code to run.
231 | *args: Optional command-line arguments to pass to script.
232 |
233 | Returns:
234 | str: Output of script.
235 |
236 | """
237 | return run_applescript(script, *args, lang='JavaScript')
238 |
239 |
240 | def run_trigger(name, bundleid=None, arg=None):
241 | """Call an Alfred External Trigger.
242 |
243 | .. versionadded:: 1.31
244 |
245 | If ``bundleid`` is not specified, reads the bundle ID of the current
246 | workflow from Alfred's environment variables.
247 |
248 | Args:
249 | name (str): Name of External Trigger to call.
250 | bundleid (str, optional): Bundle ID of workflow trigger belongs to.
251 | arg (str, optional): Argument to pass to trigger.
252 |
253 | """
254 | bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
255 | appname = jxa_app_name()
256 | opts = {'inWorkflow': bundleid}
257 | if arg:
258 | opts['withArgument'] = arg
259 |
260 | script = JXA_TRIGGER.format(app=json.dumps(appname),
261 | arg=json.dumps(name),
262 | opts=json.dumps(opts, sort_keys=True))
263 |
264 | run_applescript(script, lang='JavaScript')
265 |
266 |
267 | def set_config(name, value, bundleid=None, exportable=False):
268 | """Set a workflow variable in ``info.plist``.
269 |
270 | .. versionadded:: 1.33
271 |
272 | Args:
273 | name (str): Name of variable to set.
274 | value (str): Value to set variable to.
275 | bundleid (str, optional): Bundle ID of workflow variable belongs to.
276 | exportable (bool, optional): Whether variable should be marked
277 | as exportable (Don't Export checkbox).
278 |
279 | """
280 | bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
281 | appname = jxa_app_name()
282 | opts = {
283 | 'toValue': value,
284 | 'inWorkflow': bundleid,
285 | 'exportable': exportable,
286 | }
287 |
288 | script = JXA_SET_CONFIG.format(app=json.dumps(appname),
289 | arg=json.dumps(name),
290 | opts=json.dumps(opts, sort_keys=True))
291 |
292 | run_applescript(script, lang='JavaScript')
293 |
294 |
295 | def unset_config(name, bundleid=None):
296 | """Delete a workflow variable from ``info.plist``.
297 |
298 | .. versionadded:: 1.33
299 |
300 | Args:
301 | name (str): Name of variable to delete.
302 | bundleid (str, optional): Bundle ID of workflow variable belongs to.
303 |
304 | """
305 | bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
306 | appname = jxa_app_name()
307 | opts = {'inWorkflow': bundleid}
308 |
309 | script = JXA_UNSET_CONFIG.format(app=json.dumps(appname),
310 | arg=json.dumps(name),
311 | opts=json.dumps(opts, sort_keys=True))
312 |
313 | run_applescript(script, lang='JavaScript')
314 |
315 |
316 | def appinfo(name):
317 | """Get information about an installed application.
318 |
319 | .. versionadded:: 1.31
320 |
321 | Args:
322 | name (str): Name of application to look up.
323 |
324 | Returns:
325 | AppInfo: :class:`AppInfo` tuple or ``None`` if app isn't found.
326 |
327 | """
328 | cmd = ['mdfind', '-onlyin', '/Applications',
329 | '-onlyin', os.path.expanduser('~/Applications'),
330 | '(kMDItemContentTypeTree == com.apple.application &&'
331 | '(kMDItemDisplayName == "{0}" || kMDItemFSName == "{0}.app"))'
332 | .format(name)]
333 |
334 | output = run_command(cmd).strip()
335 | if not output:
336 | return None
337 |
338 | path = output.split('\n')[0]
339 |
340 | cmd = ['mdls', '-raw', '-name', 'kMDItemCFBundleIdentifier', path]
341 | bid = run_command(cmd).strip()
342 | if not bid: # pragma: no cover
343 | return None
344 |
345 | return AppInfo(unicodify(name), unicodify(path), unicodify(bid))
346 |
347 |
348 | @contextmanager
349 | def atomic_writer(fpath, mode):
350 | """Atomic file writer.
351 |
352 | .. versionadded:: 1.12
353 |
354 | Context manager that ensures the file is only written if the write
355 | succeeds. The data is first written to a temporary file.
356 |
357 | :param fpath: path of file to write to.
358 | :type fpath: ``unicode``
359 | :param mode: sames as for :func:`open`
360 | :type mode: string
361 |
362 | """
363 | suffix = '.{}.tmp'.format(os.getpid())
364 | temppath = fpath + suffix
365 | with open(temppath, mode) as fp:
366 | try:
367 | yield fp
368 | os.rename(temppath, fpath)
369 | finally:
370 | try:
371 | os.remove(temppath)
372 | except (OSError, IOError):
373 | pass
374 |
375 |
376 | class LockFile(object):
377 | """Context manager to protect filepaths with lockfiles.
378 |
379 | .. versionadded:: 1.13
380 |
381 | Creates a lockfile alongside ``protected_path``. Other ``LockFile``
382 | instances will refuse to lock the same path.
383 |
384 | >>> path = '/path/to/file'
385 | >>> with LockFile(path):
386 | >>> with open(path, 'wb') as fp:
387 | >>> fp.write(data)
388 |
389 | Args:
390 | protected_path (unicode): File to protect with a lockfile
391 | timeout (float, optional): Raises an :class:`AcquisitionError`
392 | if lock cannot be acquired within this number of seconds.
393 | If ``timeout`` is 0 (the default), wait forever.
394 | delay (float, optional): How often to check (in seconds) if
395 | lock has been released.
396 |
397 | Attributes:
398 | delay (float): How often to check (in seconds) whether the lock
399 | can be acquired.
400 | lockfile (unicode): Path of the lockfile.
401 | timeout (float): How long to wait to acquire the lock.
402 |
403 | """
404 |
405 | def __init__(self, protected_path, timeout=0.0, delay=0.05):
406 | """Create new :class:`LockFile` object."""
407 | self.lockfile = protected_path + '.lock'
408 | self._lockfile = None
409 | self.timeout = timeout
410 | self.delay = delay
411 | self._lock = Event()
412 | atexit.register(self.release)
413 |
414 | @property
415 | def locked(self):
416 | """``True`` if file is locked by this instance."""
417 | return self._lock.is_set()
418 |
419 | def acquire(self, blocking=True):
420 | """Acquire the lock if possible.
421 |
422 | If the lock is in use and ``blocking`` is ``False``, return
423 | ``False``.
424 |
425 | Otherwise, check every :attr:`delay` seconds until it acquires
426 | lock or exceeds attr:`timeout` and raises an :class:`AcquisitionError`.
427 |
428 | """
429 | if self.locked and not blocking:
430 | return False
431 |
432 | start = time.time()
433 | while True:
434 | # Raise error if we've been waiting too long to acquire the lock
435 | if self.timeout and (time.time() - start) >= self.timeout:
436 | raise AcquisitionError('lock acquisition timed out')
437 |
438 | # If already locked, wait then try again
439 | if self.locked:
440 | time.sleep(self.delay)
441 | continue
442 |
443 | # Create in append mode so we don't lose any contents
444 | if self._lockfile is None:
445 | self._lockfile = open(self.lockfile, 'a')
446 |
447 | # Try to acquire the lock
448 | try:
449 | fcntl.lockf(self._lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
450 | self._lock.set()
451 | break
452 | except IOError as err: # pragma: no cover
453 | if err.errno not in (errno.EACCES, errno.EAGAIN):
454 | raise
455 |
456 | # Don't try again
457 | if not blocking: # pragma: no cover
458 | return False
459 |
460 | # Wait, then try again
461 | time.sleep(self.delay)
462 |
463 | return True
464 |
465 | def release(self):
466 | """Release the lock by deleting `self.lockfile`."""
467 | if not self._lock.is_set():
468 | return False
469 |
470 | try:
471 | fcntl.lockf(self._lockfile, fcntl.LOCK_UN)
472 | except IOError: # pragma: no cover
473 | pass
474 | finally:
475 | self._lock.clear()
476 | self._lockfile = None
477 | try:
478 | os.unlink(self.lockfile)
479 | except (IOError, OSError): # pragma: no cover
480 | pass
481 |
482 | return True
483 |
484 | def __enter__(self):
485 | """Acquire lock."""
486 | self.acquire()
487 | return self
488 |
489 | def __exit__(self, typ, value, traceback):
490 | """Release lock."""
491 | self.release()
492 |
493 | def __del__(self):
494 | """Clear up `self.lockfile`."""
495 | self.release() # pragma: no cover
496 |
497 |
498 | class uninterruptible(object):
499 | """Decorator that postpones SIGTERM until wrapped function returns.
500 |
501 | .. versionadded:: 1.12
502 |
503 | .. important:: This decorator is NOT thread-safe.
504 |
505 | As of version 2.7, Alfred allows Script Filters to be killed. If
506 | your workflow is killed in the middle of critical code (e.g.
507 | writing data to disk), this may corrupt your workflow's data.
508 |
509 | Use this decorator to wrap critical functions that *must* complete.
510 | If the script is killed while a wrapped function is executing,
511 | the SIGTERM will be caught and handled after your function has
512 | finished executing.
513 |
514 | Alfred-Workflow uses this internally to ensure its settings, data
515 | and cache writes complete.
516 |
517 | """
518 |
519 | def __init__(self, func, class_name=''):
520 | """Decorate `func`."""
521 | self.func = func
522 | functools.update_wrapper(self, func)
523 | self._caught_signal = None
524 |
525 | def signal_handler(self, signum, frame):
526 | """Called when process receives SIGTERM."""
527 | self._caught_signal = (signum, frame)
528 |
529 | def __call__(self, *args, **kwargs):
530 | """Trap ``SIGTERM`` and call wrapped function."""
531 | self._caught_signal = None
532 | # Register handler for SIGTERM, then call `self.func`
533 | self.old_signal_handler = signal.getsignal(signal.SIGTERM)
534 | signal.signal(signal.SIGTERM, self.signal_handler)
535 |
536 | self.func(*args, **kwargs)
537 |
538 | # Restore old signal handler
539 | signal.signal(signal.SIGTERM, self.old_signal_handler)
540 |
541 | # Handle any signal caught during execution
542 | if self._caught_signal is not None:
543 | signum, frame = self._caught_signal
544 | if callable(self.old_signal_handler):
545 | self.old_signal_handler(signum, frame)
546 | elif self.old_signal_handler == signal.SIG_DFL:
547 | sys.exit(0)
548 |
549 | def __get__(self, obj=None, klass=None):
550 | """Decorator API."""
551 | return self.__class__(self.func.__get__(obj, klass),
552 | klass.__name__)
553 |
--------------------------------------------------------------------------------
/workflow/util.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/workflow/util.pyc
--------------------------------------------------------------------------------
/workflow/version:
--------------------------------------------------------------------------------
1 | 1.37.1
--------------------------------------------------------------------------------
/workflow/web.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | #
3 | # Copyright (c) 2014 Dean Jackson
4 | #
5 | # MIT Licence. See http://opensource.org/licenses/MIT
6 | #
7 | # Created on 2014-02-15
8 | #
9 |
10 | """Lightweight HTTP library with a requests-like interface."""
11 |
12 | import codecs
13 | import json
14 | import mimetypes
15 | import os
16 | import random
17 | import re
18 | import socket
19 | import string
20 | import unicodedata
21 | import urllib
22 | import urllib2
23 | import urlparse
24 | import zlib
25 |
26 |
27 | USER_AGENT = u'Alfred-Workflow/1.36 (+http://www.deanishe.net/alfred-workflow)'
28 |
29 | # Valid characters for multipart form data boundaries
30 | BOUNDARY_CHARS = string.digits + string.ascii_letters
31 |
32 | # HTTP response codes
33 | RESPONSES = {
34 | 100: 'Continue',
35 | 101: 'Switching Protocols',
36 | 200: 'OK',
37 | 201: 'Created',
38 | 202: 'Accepted',
39 | 203: 'Non-Authoritative Information',
40 | 204: 'No Content',
41 | 205: 'Reset Content',
42 | 206: 'Partial Content',
43 | 300: 'Multiple Choices',
44 | 301: 'Moved Permanently',
45 | 302: 'Found',
46 | 303: 'See Other',
47 | 304: 'Not Modified',
48 | 305: 'Use Proxy',
49 | 307: 'Temporary Redirect',
50 | 400: 'Bad Request',
51 | 401: 'Unauthorized',
52 | 402: 'Payment Required',
53 | 403: 'Forbidden',
54 | 404: 'Not Found',
55 | 405: 'Method Not Allowed',
56 | 406: 'Not Acceptable',
57 | 407: 'Proxy Authentication Required',
58 | 408: 'Request Timeout',
59 | 409: 'Conflict',
60 | 410: 'Gone',
61 | 411: 'Length Required',
62 | 412: 'Precondition Failed',
63 | 413: 'Request Entity Too Large',
64 | 414: 'Request-URI Too Long',
65 | 415: 'Unsupported Media Type',
66 | 416: 'Requested Range Not Satisfiable',
67 | 417: 'Expectation Failed',
68 | 500: 'Internal Server Error',
69 | 501: 'Not Implemented',
70 | 502: 'Bad Gateway',
71 | 503: 'Service Unavailable',
72 | 504: 'Gateway Timeout',
73 | 505: 'HTTP Version Not Supported'
74 | }
75 |
76 |
77 | def str_dict(dic):
78 | """Convert keys and values in ``dic`` into UTF-8-encoded :class:`str`.
79 |
80 | :param dic: Mapping of Unicode strings
81 | :type dic: dict
82 | :returns: Dictionary containing only UTF-8 strings
83 | :rtype: dict
84 |
85 | """
86 | if isinstance(dic, CaseInsensitiveDictionary):
87 | dic2 = CaseInsensitiveDictionary()
88 | else:
89 | dic2 = {}
90 | for k, v in dic.items():
91 | if isinstance(k, unicode):
92 | k = k.encode('utf-8')
93 | if isinstance(v, unicode):
94 | v = v.encode('utf-8')
95 | dic2[k] = v
96 | return dic2
97 |
98 |
99 | class NoRedirectHandler(urllib2.HTTPRedirectHandler):
100 | """Prevent redirections."""
101 |
102 | def redirect_request(self, *args):
103 | """Ignore redirect."""
104 | return None
105 |
106 |
107 | # Adapted from https://gist.github.com/babakness/3901174
108 | class CaseInsensitiveDictionary(dict):
109 | """Dictionary with caseless key search.
110 |
111 | Enables case insensitive searching while preserving case sensitivity
112 | when keys are listed, ie, via keys() or items() methods.
113 |
114 | Works by storing a lowercase version of the key as the new key and
115 | stores the original key-value pair as the key's value
116 | (values become dictionaries).
117 |
118 | """
119 |
120 | def __init__(self, initval=None):
121 | """Create new case-insensitive dictionary."""
122 | if isinstance(initval, dict):
123 | for key, value in initval.iteritems():
124 | self.__setitem__(key, value)
125 |
126 | elif isinstance(initval, list):
127 | for (key, value) in initval:
128 | self.__setitem__(key, value)
129 |
130 | def __contains__(self, key):
131 | return dict.__contains__(self, key.lower())
132 |
133 | def __getitem__(self, key):
134 | return dict.__getitem__(self, key.lower())['val']
135 |
136 | def __setitem__(self, key, value):
137 | return dict.__setitem__(self, key.lower(), {'key': key, 'val': value})
138 |
139 | def get(self, key, default=None):
140 | """Return value for case-insensitive key or default."""
141 | try:
142 | v = dict.__getitem__(self, key.lower())
143 | except KeyError:
144 | return default
145 | else:
146 | return v['val']
147 |
148 | def update(self, other):
149 | """Update values from other ``dict``."""
150 | for k, v in other.items():
151 | self[k] = v
152 |
153 | def items(self):
154 | """Return ``(key, value)`` pairs."""
155 | return [(v['key'], v['val']) for v in dict.itervalues(self)]
156 |
157 | def keys(self):
158 | """Return original keys."""
159 | return [v['key'] for v in dict.itervalues(self)]
160 |
161 | def values(self):
162 | """Return all values."""
163 | return [v['val'] for v in dict.itervalues(self)]
164 |
165 | def iteritems(self):
166 | """Iterate over ``(key, value)`` pairs."""
167 | for v in dict.itervalues(self):
168 | yield v['key'], v['val']
169 |
170 | def iterkeys(self):
171 | """Iterate over original keys."""
172 | for v in dict.itervalues(self):
173 | yield v['key']
174 |
175 | def itervalues(self):
176 | """Interate over values."""
177 | for v in dict.itervalues(self):
178 | yield v['val']
179 |
180 |
181 | class Response(object):
182 | """
183 | Returned by :func:`request` / :func:`get` / :func:`post` functions.
184 |
185 | Simplified version of the ``Response`` object in the ``requests`` library.
186 |
187 | >>> r = request('http://www.google.com')
188 | >>> r.status_code
189 | 200
190 | >>> r.encoding
191 | ISO-8859-1
192 | >>> r.content # bytes
193 | ...
194 | >>> r.text # unicode, decoded according to charset in HTTP header/meta tag
195 | u' ...'
196 | >>> r.json() # content parsed as JSON
197 |
198 | """
199 |
200 | def __init__(self, request, stream=False):
201 | """Call `request` with :mod:`urllib2` and process results.
202 |
203 | :param request: :class:`urllib2.Request` instance
204 | :param stream: Whether to stream response or retrieve it all at once
205 | :type stream: bool
206 |
207 | """
208 | self.request = request
209 | self._stream = stream
210 | self.url = None
211 | self.raw = None
212 | self._encoding = None
213 | self.error = None
214 | self.status_code = None
215 | self.reason = None
216 | self.headers = CaseInsensitiveDictionary()
217 | self._content = None
218 | self._content_loaded = False
219 | self._gzipped = False
220 |
221 | # Execute query
222 | try:
223 | self.raw = urllib2.urlopen(request)
224 | except urllib2.HTTPError as err:
225 | self.error = err
226 | try:
227 | self.url = err.geturl()
228 | # sometimes (e.g. when authentication fails)
229 | # urllib can't get a URL from an HTTPError
230 | # This behaviour changes across Python versions,
231 | # so no test cover (it isn't important).
232 | except AttributeError: # pragma: no cover
233 | pass
234 | self.status_code = err.code
235 | else:
236 | self.status_code = self.raw.getcode()
237 | self.url = self.raw.geturl()
238 | self.reason = RESPONSES.get(self.status_code)
239 |
240 | # Parse additional info if request succeeded
241 | if not self.error:
242 | headers = self.raw.info()
243 | self.transfer_encoding = headers.getencoding()
244 | self.mimetype = headers.gettype()
245 | for key in headers.keys():
246 | self.headers[key.lower()] = headers.get(key)
247 |
248 | # Is content gzipped?
249 | # Transfer-Encoding appears to not be used in the wild
250 | # (contrary to the HTTP standard), but no harm in testing
251 | # for it
252 | if 'gzip' in headers.get('content-encoding', '') or \
253 | 'gzip' in headers.get('transfer-encoding', ''):
254 | self._gzipped = True
255 |
256 | @property
257 | def stream(self):
258 | """Whether response is streamed.
259 |
260 | Returns:
261 | bool: `True` if response is streamed.
262 |
263 | """
264 | return self._stream
265 |
266 | @stream.setter
267 | def stream(self, value):
268 | if self._content_loaded:
269 | raise RuntimeError("`content` has already been read from "
270 | "this Response.")
271 |
272 | self._stream = value
273 |
274 | def json(self):
275 | """Decode response contents as JSON.
276 |
277 | :returns: object decoded from JSON
278 | :rtype: list, dict or unicode
279 |
280 | """
281 | return json.loads(self.content, self.encoding or 'utf-8')
282 |
283 | @property
284 | def encoding(self):
285 | """Text encoding of document or ``None``.
286 |
287 | :returns: Text encoding if found.
288 | :rtype: str or ``None``
289 |
290 | """
291 | if not self._encoding:
292 | self._encoding = self._get_encoding()
293 |
294 | return self._encoding
295 |
296 | @property
297 | def content(self):
298 | """Raw content of response (i.e. bytes).
299 |
300 | :returns: Body of HTTP response
301 | :rtype: str
302 |
303 | """
304 | if not self._content:
305 |
306 | # Decompress gzipped content
307 | if self._gzipped:
308 | decoder = zlib.decompressobj(16 + zlib.MAX_WBITS)
309 | self._content = decoder.decompress(self.raw.read())
310 |
311 | else:
312 | self._content = self.raw.read()
313 |
314 | self._content_loaded = True
315 |
316 | return self._content
317 |
318 | @property
319 | def text(self):
320 | """Unicode-decoded content of response body.
321 |
322 | If no encoding can be determined from HTTP headers or the content
323 | itself, the encoded response body will be returned instead.
324 |
325 | :returns: Body of HTTP response
326 | :rtype: unicode or str
327 |
328 | """
329 | if self.encoding:
330 | return unicodedata.normalize('NFC', unicode(self.content,
331 | self.encoding))
332 | return self.content
333 |
334 | def iter_content(self, chunk_size=4096, decode_unicode=False):
335 | """Iterate over response data.
336 |
337 | .. versionadded:: 1.6
338 |
339 | :param chunk_size: Number of bytes to read into memory
340 | :type chunk_size: int
341 | :param decode_unicode: Decode to Unicode using detected encoding
342 | :type decode_unicode: bool
343 | :returns: iterator
344 |
345 | """
346 | if not self.stream:
347 | raise RuntimeError("You cannot call `iter_content` on a "
348 | "Response unless you passed `stream=True`"
349 | " to `get()`/`post()`/`request()`.")
350 |
351 | if self._content_loaded:
352 | raise RuntimeError(
353 | "`content` has already been read from this Response.")
354 |
355 | def decode_stream(iterator, r):
356 | dec = codecs.getincrementaldecoder(r.encoding)(errors='replace')
357 |
358 | for chunk in iterator:
359 | data = dec.decode(chunk)
360 | if data:
361 | yield data
362 |
363 | data = dec.decode(b'', final=True)
364 | if data: # pragma: no cover
365 | yield data
366 |
367 | def generate():
368 | if self._gzipped:
369 | decoder = zlib.decompressobj(16 + zlib.MAX_WBITS)
370 |
371 | while True:
372 | chunk = self.raw.read(chunk_size)
373 | if not chunk:
374 | break
375 |
376 | if self._gzipped:
377 | chunk = decoder.decompress(chunk)
378 |
379 | yield chunk
380 |
381 | chunks = generate()
382 |
383 | if decode_unicode and self.encoding:
384 | chunks = decode_stream(chunks, self)
385 |
386 | return chunks
387 |
388 | def save_to_path(self, filepath):
389 | """Save retrieved data to file at ``filepath``.
390 |
391 | .. versionadded: 1.9.6
392 |
393 | :param filepath: Path to save retrieved data.
394 |
395 | """
396 | filepath = os.path.abspath(filepath)
397 | dirname = os.path.dirname(filepath)
398 | if not os.path.exists(dirname):
399 | os.makedirs(dirname)
400 |
401 | self.stream = True
402 |
403 | with open(filepath, 'wb') as fileobj:
404 | for data in self.iter_content():
405 | fileobj.write(data)
406 |
407 | def raise_for_status(self):
408 | """Raise stored error if one occurred.
409 |
410 | error will be instance of :class:`urllib2.HTTPError`
411 | """
412 | if self.error is not None:
413 | raise self.error
414 | return
415 |
416 | def _get_encoding(self):
417 | """Get encoding from HTTP headers or content.
418 |
419 | :returns: encoding or `None`
420 | :rtype: unicode or ``None``
421 |
422 | """
423 | headers = self.raw.info()
424 | encoding = None
425 |
426 | if headers.getparam('charset'):
427 | encoding = headers.getparam('charset')
428 |
429 | # HTTP Content-Type header
430 | for param in headers.getplist():
431 | if param.startswith('charset='):
432 | encoding = param[8:]
433 | break
434 |
435 | if not self.stream: # Try sniffing response content
436 | # Encoding declared in document should override HTTP headers
437 | if self.mimetype == 'text/html': # sniff HTML headers
438 | m = re.search(r"""""",
439 | self.content)
440 | if m:
441 | encoding = m.group(1)
442 |
443 | elif ((self.mimetype.startswith('application/')
444 | or self.mimetype.startswith('text/'))
445 | and 'xml' in self.mimetype):
446 | m = re.search(r"""]*\?>""",
447 | self.content)
448 | if m:
449 | encoding = m.group(1)
450 |
451 | # Format defaults
452 | if self.mimetype == 'application/json' and not encoding:
453 | # The default encoding for JSON
454 | encoding = 'utf-8'
455 |
456 | elif self.mimetype == 'application/xml' and not encoding:
457 | # The default for 'application/xml'
458 | encoding = 'utf-8'
459 |
460 | if encoding:
461 | encoding = encoding.lower()
462 |
463 | return encoding
464 |
465 |
466 | def request(method, url, params=None, data=None, headers=None, cookies=None,
467 | files=None, auth=None, timeout=60, allow_redirects=False,
468 | stream=False):
469 | """Initiate an HTTP(S) request. Returns :class:`Response` object.
470 |
471 | :param method: 'GET' or 'POST'
472 | :type method: unicode
473 | :param url: URL to open
474 | :type url: unicode
475 | :param params: mapping of URL parameters
476 | :type params: dict
477 | :param data: mapping of form data ``{'field_name': 'value'}`` or
478 | :class:`str`
479 | :type data: dict or str
480 | :param headers: HTTP headers
481 | :type headers: dict
482 | :param cookies: cookies to send to server
483 | :type cookies: dict
484 | :param files: files to upload (see below).
485 | :type files: dict
486 | :param auth: username, password
487 | :type auth: tuple
488 | :param timeout: connection timeout limit in seconds
489 | :type timeout: int
490 | :param allow_redirects: follow redirections
491 | :type allow_redirects: bool
492 | :param stream: Stream content instead of fetching it all at once.
493 | :type stream: bool
494 | :returns: Response object
495 | :rtype: :class:`Response`
496 |
497 |
498 | The ``files`` argument is a dictionary::
499 |
500 | {'fieldname' : { 'filename': 'blah.txt',
501 | 'content': '',
502 | 'mimetype': 'text/plain'}
503 | }
504 |
505 | * ``fieldname`` is the name of the field in the HTML form.
506 | * ``mimetype`` is optional. If not provided, :mod:`mimetypes` will
507 | be used to guess the mimetype, or ``application/octet-stream``
508 | will be used.
509 |
510 | """
511 | # TODO: cookies
512 | socket.setdefaulttimeout(timeout)
513 |
514 | # Default handlers
515 | openers = []
516 |
517 | if not allow_redirects:
518 | openers.append(NoRedirectHandler())
519 |
520 | if auth is not None: # Add authorisation handler
521 | username, password = auth
522 | password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
523 | password_manager.add_password(None, url, username, password)
524 | auth_manager = urllib2.HTTPBasicAuthHandler(password_manager)
525 | openers.append(auth_manager)
526 |
527 | # Install our custom chain of openers
528 | opener = urllib2.build_opener(*openers)
529 | urllib2.install_opener(opener)
530 |
531 | if not headers:
532 | headers = CaseInsensitiveDictionary()
533 | else:
534 | headers = CaseInsensitiveDictionary(headers)
535 |
536 | if 'user-agent' not in headers:
537 | headers['user-agent'] = USER_AGENT
538 |
539 | # Accept gzip-encoded content
540 | encodings = [s.strip() for s in
541 | headers.get('accept-encoding', '').split(',')]
542 | if 'gzip' not in encodings:
543 | encodings.append('gzip')
544 |
545 | headers['accept-encoding'] = ', '.join(encodings)
546 |
547 | # Force POST by providing an empty data string
548 | if method == 'POST' and not data:
549 | data = ''
550 |
551 | if files:
552 | if not data:
553 | data = {}
554 | new_headers, data = encode_multipart_formdata(data, files)
555 | headers.update(new_headers)
556 | elif data and isinstance(data, dict):
557 | data = urllib.urlencode(str_dict(data))
558 |
559 | # Make sure everything is encoded text
560 | headers = str_dict(headers)
561 |
562 | if isinstance(url, unicode):
563 | url = url.encode('utf-8')
564 |
565 | if params: # GET args (POST args are handled in encode_multipart_formdata)
566 |
567 | scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
568 |
569 | if query: # Combine query string and `params`
570 | url_params = urlparse.parse_qs(query)
571 | # `params` take precedence over URL query string
572 | url_params.update(params)
573 | params = url_params
574 |
575 | query = urllib.urlencode(str_dict(params), doseq=True)
576 | url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
577 |
578 | req = urllib2.Request(url, data, headers)
579 | return Response(req, stream)
580 |
581 |
582 | def get(url, params=None, headers=None, cookies=None, auth=None,
583 | timeout=60, allow_redirects=True, stream=False):
584 | """Initiate a GET request. Arguments as for :func:`request`.
585 |
586 | :returns: :class:`Response` instance
587 |
588 | """
589 | return request('GET', url, params, headers=headers, cookies=cookies,
590 | auth=auth, timeout=timeout, allow_redirects=allow_redirects,
591 | stream=stream)
592 |
593 |
594 | def post(url, params=None, data=None, headers=None, cookies=None, files=None,
595 | auth=None, timeout=60, allow_redirects=False, stream=False):
596 | """Initiate a POST request. Arguments as for :func:`request`.
597 |
598 | :returns: :class:`Response` instance
599 |
600 | """
601 | return request('POST', url, params, data, headers, cookies, files, auth,
602 | timeout, allow_redirects, stream)
603 |
604 |
605 | def encode_multipart_formdata(fields, files):
606 | """Encode form data (``fields``) and ``files`` for POST request.
607 |
608 | :param fields: mapping of ``{name : value}`` pairs for normal form fields.
609 | :type fields: dict
610 | :param files: dictionary of fieldnames/files elements for file data.
611 | See below for details.
612 | :type files: dict of :class:`dict`
613 | :returns: ``(headers, body)`` ``headers`` is a
614 | :class:`dict` of HTTP headers
615 | :rtype: 2-tuple ``(dict, str)``
616 |
617 | The ``files`` argument is a dictionary::
618 |
619 | {'fieldname' : { 'filename': 'blah.txt',
620 | 'content': '',
621 | 'mimetype': 'text/plain'}
622 | }
623 |
624 | - ``fieldname`` is the name of the field in the HTML form.
625 | - ``mimetype`` is optional. If not provided, :mod:`mimetypes` will
626 | be used to guess the mimetype, or ``application/octet-stream``
627 | will be used.
628 |
629 | """
630 | def get_content_type(filename):
631 | """Return or guess mimetype of ``filename``.
632 |
633 | :param filename: filename of file
634 | :type filename: unicode/str
635 | :returns: mime-type, e.g. ``text/html``
636 | :rtype: str
637 |
638 | """
639 | return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
640 |
641 | boundary = '-----' + ''.join(random.choice(BOUNDARY_CHARS)
642 | for i in range(30))
643 | CRLF = '\r\n'
644 | output = []
645 |
646 | # Normal form fields
647 | for (name, value) in fields.items():
648 | if isinstance(name, unicode):
649 | name = name.encode('utf-8')
650 | if isinstance(value, unicode):
651 | value = value.encode('utf-8')
652 | output.append('--' + boundary)
653 | output.append('Content-Disposition: form-data; name="%s"' % name)
654 | output.append('')
655 | output.append(value)
656 |
657 | # Files to upload
658 | for name, d in files.items():
659 | filename = d[u'filename']
660 | content = d[u'content']
661 | if u'mimetype' in d:
662 | mimetype = d[u'mimetype']
663 | else:
664 | mimetype = get_content_type(filename)
665 | if isinstance(name, unicode):
666 | name = name.encode('utf-8')
667 | if isinstance(filename, unicode):
668 | filename = filename.encode('utf-8')
669 | if isinstance(mimetype, unicode):
670 | mimetype = mimetype.encode('utf-8')
671 | output.append('--' + boundary)
672 | output.append('Content-Disposition: form-data; '
673 | 'name="%s"; filename="%s"' % (name, filename))
674 | output.append('Content-Type: %s' % mimetype)
675 | output.append('')
676 | output.append(content)
677 |
678 | output.append('--' + boundary + '--')
679 | output.append('')
680 | body = CRLF.join(output)
681 | headers = {
682 | 'Content-Type': 'multipart/form-data; boundary=%s' % boundary,
683 | 'Content-Length': str(len(body)),
684 | }
685 | return (headers, body)
686 |
--------------------------------------------------------------------------------
/workflow/web.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/workflow/web.pyc
--------------------------------------------------------------------------------
/workflow/workflow.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/workflow/workflow.pyc
--------------------------------------------------------------------------------
/workflow/workflow3.py:
--------------------------------------------------------------------------------
1 | # encoding: utf-8
2 | #
3 | # Copyright (c) 2016 Dean Jackson
4 | #
5 | # MIT Licence. See http://opensource.org/licenses/MIT
6 | #
7 | # Created on 2016-06-25
8 | #
9 |
10 | """An Alfred 3+ version of :class:`~workflow.Workflow`.
11 |
12 | :class:`~workflow.Workflow3` supports new features, such as
13 | setting :ref:`workflow-variables` and
14 | :class:`the more advanced modifiers ` supported by Alfred 3+.
15 |
16 | In order for the feedback mechanism to work correctly, it's important
17 | to create :class:`Item3` and :class:`Modifier` objects via the
18 | :meth:`Workflow3.add_item()` and :meth:`Item3.add_modifier()` methods
19 | respectively. If you instantiate :class:`Item3` or :class:`Modifier`
20 | objects directly, the current :class:`Workflow3` object won't be aware
21 | of them, and they won't be sent to Alfred when you call
22 | :meth:`Workflow3.send_feedback()`.
23 |
24 | """
25 |
26 | from __future__ import print_function, unicode_literals, absolute_import
27 |
28 | import json
29 | import os
30 | import sys
31 |
32 | from .workflow import ICON_WARNING, Workflow
33 |
34 |
35 | class Variables(dict):
36 | """Workflow variables for Run Script actions.
37 |
38 | .. versionadded: 1.26
39 |
40 | This class allows you to set workflow variables from
41 | Run Script actions.
42 |
43 | It is a subclass of :class:`dict`.
44 |
45 | >>> v = Variables(username='deanishe', password='hunter2')
46 | >>> v.arg = u'output value'
47 | >>> print(v)
48 |
49 | See :ref:`variables-run-script` in the User Guide for more
50 | information.
51 |
52 | Args:
53 | arg (unicode, optional): Main output/``{query}``.
54 | **variables: Workflow variables to set.
55 |
56 |
57 | Attributes:
58 | arg (unicode): Output value (``{query}``).
59 | config (dict): Configuration for downstream workflow element.
60 |
61 | """
62 |
63 | def __init__(self, arg=None, **variables):
64 | """Create a new `Variables` object."""
65 | self.arg = arg
66 | self.config = {}
67 | super(Variables, self).__init__(**variables)
68 |
69 | @property
70 | def obj(self):
71 | """Return ``alfredworkflow`` `dict`."""
72 | o = {}
73 | if self:
74 | d2 = {}
75 | for k, v in self.items():
76 | d2[k] = v
77 | o['variables'] = d2
78 |
79 | if self.config:
80 | o['config'] = self.config
81 |
82 | if self.arg is not None:
83 | o['arg'] = self.arg
84 |
85 | return {'alfredworkflow': o}
86 |
87 | def __unicode__(self):
88 | """Convert to ``alfredworkflow`` JSON object.
89 |
90 | Returns:
91 | unicode: ``alfredworkflow`` JSON object
92 |
93 | """
94 | if not self and not self.config:
95 | if self.arg:
96 | return self.arg
97 | else:
98 | return u''
99 |
100 | return json.dumps(self.obj)
101 |
102 | def __str__(self):
103 | """Convert to ``alfredworkflow`` JSON object.
104 |
105 | Returns:
106 | str: UTF-8 encoded ``alfredworkflow`` JSON object
107 |
108 | """
109 | return unicode(self).encode('utf-8')
110 |
111 |
112 | class Modifier(object):
113 | """Modify :class:`Item3` arg/icon/variables when modifier key is pressed.
114 |
115 | Don't use this class directly (as it won't be associated with any
116 | :class:`Item3`), but rather use :meth:`Item3.add_modifier()`
117 | to add modifiers to results.
118 |
119 | >>> it = wf.add_item('Title', 'Subtitle', valid=True)
120 | >>> it.setvar('name', 'default')
121 | >>> m = it.add_modifier('cmd')
122 | >>> m.setvar('name', 'alternate')
123 |
124 | See :ref:`workflow-variables` in the User Guide for more information
125 | and :ref:`example usage `.
126 |
127 | Args:
128 | key (unicode): Modifier key, e.g. ``"cmd"``, ``"alt"`` etc.
129 | subtitle (unicode, optional): Override default subtitle.
130 | arg (unicode, optional): Argument to pass for this modifier.
131 | valid (bool, optional): Override item's validity.
132 | icon (unicode, optional): Filepath/UTI of icon to use
133 | icontype (unicode, optional): Type of icon. See
134 | :meth:`Workflow.add_item() `
135 | for valid values.
136 |
137 | Attributes:
138 | arg (unicode): Arg to pass to following action.
139 | config (dict): Configuration for a downstream element, such as
140 | a File Filter.
141 | icon (unicode): Filepath/UTI of icon.
142 | icontype (unicode): Type of icon. See
143 | :meth:`Workflow.add_item() `
144 | for valid values.
145 | key (unicode): Modifier key (see above).
146 | subtitle (unicode): Override item subtitle.
147 | valid (bool): Override item validity.
148 | variables (dict): Workflow variables set by this modifier.
149 |
150 | """
151 |
152 | def __init__(self, key, subtitle=None, arg=None, valid=None, icon=None,
153 | icontype=None):
154 | """Create a new :class:`Modifier`.
155 |
156 | Don't use this class directly (as it won't be associated with any
157 | :class:`Item3`), but rather use :meth:`Item3.add_modifier()`
158 | to add modifiers to results.
159 |
160 | Args:
161 | key (unicode): Modifier key, e.g. ``"cmd"``, ``"alt"`` etc.
162 | subtitle (unicode, optional): Override default subtitle.
163 | arg (unicode, optional): Argument to pass for this modifier.
164 | valid (bool, optional): Override item's validity.
165 | icon (unicode, optional): Filepath/UTI of icon to use
166 | icontype (unicode, optional): Type of icon. See
167 | :meth:`Workflow.add_item() `
168 | for valid values.
169 |
170 | """
171 | self.key = key
172 | self.subtitle = subtitle
173 | self.arg = arg
174 | self.valid = valid
175 | self.icon = icon
176 | self.icontype = icontype
177 |
178 | self.config = {}
179 | self.variables = {}
180 |
181 | def setvar(self, name, value):
182 | """Set a workflow variable for this Item.
183 |
184 | Args:
185 | name (unicode): Name of variable.
186 | value (unicode): Value of variable.
187 |
188 | """
189 | self.variables[name] = value
190 |
191 | def getvar(self, name, default=None):
192 | """Return value of workflow variable for ``name`` or ``default``.
193 |
194 | Args:
195 | name (unicode): Variable name.
196 | default (None, optional): Value to return if variable is unset.
197 |
198 | Returns:
199 | unicode or ``default``: Value of variable if set or ``default``.
200 |
201 | """
202 | return self.variables.get(name, default)
203 |
204 | @property
205 | def obj(self):
206 | """Modifier formatted for JSON serialization for Alfred 3.
207 |
208 | Returns:
209 | dict: Modifier for serializing to JSON.
210 |
211 | """
212 | o = {}
213 |
214 | if self.subtitle is not None:
215 | o['subtitle'] = self.subtitle
216 |
217 | if self.arg is not None:
218 | o['arg'] = self.arg
219 |
220 | if self.valid is not None:
221 | o['valid'] = self.valid
222 |
223 | if self.variables:
224 | o['variables'] = self.variables
225 |
226 | if self.config:
227 | o['config'] = self.config
228 |
229 | icon = self._icon()
230 | if icon:
231 | o['icon'] = icon
232 |
233 | return o
234 |
235 | def _icon(self):
236 | """Return `icon` object for item.
237 |
238 | Returns:
239 | dict: Mapping for item `icon` (may be empty).
240 |
241 | """
242 | icon = {}
243 | if self.icon is not None:
244 | icon['path'] = self.icon
245 |
246 | if self.icontype is not None:
247 | icon['type'] = self.icontype
248 |
249 | return icon
250 |
251 |
252 | class Item3(object):
253 | """Represents a feedback item for Alfred 3+.
254 |
255 | Generates Alfred-compliant JSON for a single item.
256 |
257 | Don't use this class directly (as it then won't be associated with
258 | any :class:`Workflow3 ` object), but rather use
259 | :meth:`Workflow3.add_item() `.
260 | See :meth:`~workflow.Workflow3.add_item` for details of arguments.
261 |
262 | """
263 |
264 | def __init__(self, title, subtitle='', arg=None, autocomplete=None,
265 | match=None, valid=False, uid=None, icon=None, icontype=None,
266 | type=None, largetext=None, copytext=None, quicklookurl=None):
267 | """Create a new :class:`Item3` object.
268 |
269 | Use same arguments as for
270 | :class:`Workflow.Item `.
271 |
272 | Argument ``subtitle_modifiers`` is not supported.
273 |
274 | """
275 | self.title = title
276 | self.subtitle = subtitle
277 | self.arg = arg
278 | self.autocomplete = autocomplete
279 | self.match = match
280 | self.valid = valid
281 | self.uid = uid
282 | self.icon = icon
283 | self.icontype = icontype
284 | self.type = type
285 | self.quicklookurl = quicklookurl
286 | self.largetext = largetext
287 | self.copytext = copytext
288 |
289 | self.modifiers = {}
290 |
291 | self.config = {}
292 | self.variables = {}
293 |
294 | def setvar(self, name, value):
295 | """Set a workflow variable for this Item.
296 |
297 | Args:
298 | name (unicode): Name of variable.
299 | value (unicode): Value of variable.
300 |
301 | """
302 | self.variables[name] = value
303 |
304 | def getvar(self, name, default=None):
305 | """Return value of workflow variable for ``name`` or ``default``.
306 |
307 | Args:
308 | name (unicode): Variable name.
309 | default (None, optional): Value to return if variable is unset.
310 |
311 | Returns:
312 | unicode or ``default``: Value of variable if set or ``default``.
313 |
314 | """
315 | return self.variables.get(name, default)
316 |
317 | def add_modifier(self, key, subtitle=None, arg=None, valid=None, icon=None,
318 | icontype=None):
319 | """Add alternative values for a modifier key.
320 |
321 | Args:
322 | key (unicode): Modifier key, e.g. ``"cmd"`` or ``"alt"``
323 | subtitle (unicode, optional): Override item subtitle.
324 | arg (unicode, optional): Input for following action.
325 | valid (bool, optional): Override item validity.
326 | icon (unicode, optional): Filepath/UTI of icon.
327 | icontype (unicode, optional): Type of icon. See
328 | :meth:`Workflow.add_item() `
329 | for valid values.
330 |
331 | Returns:
332 | Modifier: Configured :class:`Modifier`.
333 |
334 | """
335 | mod = Modifier(key, subtitle, arg, valid, icon, icontype)
336 |
337 | # Add Item variables to Modifier
338 | mod.variables.update(self.variables)
339 |
340 | self.modifiers[key] = mod
341 |
342 | return mod
343 |
344 | @property
345 | def obj(self):
346 | """Item formatted for JSON serialization.
347 |
348 | Returns:
349 | dict: Data suitable for Alfred 3 feedback.
350 |
351 | """
352 | # Required values
353 | o = {
354 | 'title': self.title,
355 | 'subtitle': self.subtitle,
356 | 'valid': self.valid,
357 | }
358 |
359 | # Optional values
360 | if self.arg is not None:
361 | o['arg'] = self.arg
362 |
363 | if self.autocomplete is not None:
364 | o['autocomplete'] = self.autocomplete
365 |
366 | if self.match is not None:
367 | o['match'] = self.match
368 |
369 | if self.uid is not None:
370 | o['uid'] = self.uid
371 |
372 | if self.type is not None:
373 | o['type'] = self.type
374 |
375 | if self.quicklookurl is not None:
376 | o['quicklookurl'] = self.quicklookurl
377 |
378 | if self.variables:
379 | o['variables'] = self.variables
380 |
381 | if self.config:
382 | o['config'] = self.config
383 |
384 | # Largetype and copytext
385 | text = self._text()
386 | if text:
387 | o['text'] = text
388 |
389 | icon = self._icon()
390 | if icon:
391 | o['icon'] = icon
392 |
393 | # Modifiers
394 | mods = self._modifiers()
395 | if mods:
396 | o['mods'] = mods
397 |
398 | return o
399 |
400 | def _icon(self):
401 | """Return `icon` object for item.
402 |
403 | Returns:
404 | dict: Mapping for item `icon` (may be empty).
405 |
406 | """
407 | icon = {}
408 | if self.icon is not None:
409 | icon['path'] = self.icon
410 |
411 | if self.icontype is not None:
412 | icon['type'] = self.icontype
413 |
414 | return icon
415 |
416 | def _text(self):
417 | """Return `largetext` and `copytext` object for item.
418 |
419 | Returns:
420 | dict: `text` mapping (may be empty)
421 |
422 | """
423 | text = {}
424 | if self.largetext is not None:
425 | text['largetype'] = self.largetext
426 |
427 | if self.copytext is not None:
428 | text['copy'] = self.copytext
429 |
430 | return text
431 |
432 | def _modifiers(self):
433 | """Build `mods` dictionary for JSON feedback.
434 |
435 | Returns:
436 | dict: Modifier mapping or `None`.
437 |
438 | """
439 | if self.modifiers:
440 | mods = {}
441 | for k, mod in self.modifiers.items():
442 | mods[k] = mod.obj
443 |
444 | return mods
445 |
446 | return None
447 |
448 |
449 | class Workflow3(Workflow):
450 | """Workflow class that generates Alfred 3+ feedback.
451 |
452 | It is a subclass of :class:`~workflow.Workflow` and most of its
453 | methods are documented there.
454 |
455 | Attributes:
456 | item_class (class): Class used to generate feedback items.
457 | variables (dict): Top level workflow variables.
458 |
459 | """
460 |
461 | item_class = Item3
462 |
463 | def __init__(self, **kwargs):
464 | """Create a new :class:`Workflow3` object.
465 |
466 | See :class:`~workflow.Workflow` for documentation.
467 |
468 | """
469 | Workflow.__init__(self, **kwargs)
470 | self.variables = {}
471 | self._rerun = 0
472 | # Get session ID from environment if present
473 | self._session_id = os.getenv('_WF_SESSION_ID') or None
474 | if self._session_id:
475 | self.setvar('_WF_SESSION_ID', self._session_id)
476 |
477 | @property
478 | def _default_cachedir(self):
479 | """Alfred 4's default cache directory."""
480 | return os.path.join(
481 | os.path.expanduser(
482 | '~/Library/Caches/com.runningwithcrayons.Alfred/'
483 | 'Workflow Data/'),
484 | self.bundleid)
485 |
486 | @property
487 | def _default_datadir(self):
488 | """Alfred 4's default data directory."""
489 | return os.path.join(os.path.expanduser(
490 | '~/Library/Application Support/Alfred/Workflow Data/'),
491 | self.bundleid)
492 |
493 | @property
494 | def rerun(self):
495 | """How often (in seconds) Alfred should re-run the Script Filter."""
496 | return self._rerun
497 |
498 | @rerun.setter
499 | def rerun(self, seconds):
500 | """Interval at which Alfred should re-run the Script Filter.
501 |
502 | Args:
503 | seconds (int): Interval between runs.
504 | """
505 | self._rerun = seconds
506 |
507 | @property
508 | def session_id(self):
509 | """A unique session ID every time the user uses the workflow.
510 |
511 | .. versionadded:: 1.25
512 |
513 | The session ID persists while the user is using this workflow.
514 | It expires when the user runs a different workflow or closes
515 | Alfred.
516 |
517 | """
518 | if not self._session_id:
519 | from uuid import uuid4
520 | self._session_id = uuid4().hex
521 | self.setvar('_WF_SESSION_ID', self._session_id)
522 |
523 | return self._session_id
524 |
525 | def setvar(self, name, value, persist=False):
526 | """Set a "global" workflow variable.
527 |
528 | .. versionchanged:: 1.33
529 |
530 | These variables are always passed to downstream workflow objects.
531 |
532 | If you have set :attr:`rerun`, these variables are also passed
533 | back to the script when Alfred runs it again.
534 |
535 | Args:
536 | name (unicode): Name of variable.
537 | value (unicode): Value of variable.
538 | persist (bool, optional): Also save variable to ``info.plist``?
539 |
540 | """
541 | self.variables[name] = value
542 | if persist:
543 | from .util import set_config
544 | set_config(name, value, self.bundleid)
545 | self.logger.debug('saved variable %r with value %r to info.plist',
546 | name, value)
547 |
548 | def getvar(self, name, default=None):
549 | """Return value of workflow variable for ``name`` or ``default``.
550 |
551 | Args:
552 | name (unicode): Variable name.
553 | default (None, optional): Value to return if variable is unset.
554 |
555 | Returns:
556 | unicode or ``default``: Value of variable if set or ``default``.
557 |
558 | """
559 | return self.variables.get(name, default)
560 |
561 | def add_item(self, title, subtitle='', arg=None, autocomplete=None,
562 | valid=False, uid=None, icon=None, icontype=None, type=None,
563 | largetext=None, copytext=None, quicklookurl=None, match=None):
564 | """Add an item to be output to Alfred.
565 |
566 | Args:
567 | match (unicode, optional): If you have "Alfred filters results"
568 | turned on for your Script Filter, Alfred (version 3.5 and
569 | above) will filter against this field, not ``title``.
570 |
571 | See :meth:`Workflow.add_item() ` for
572 | the main documentation and other parameters.
573 |
574 | The key difference is that this method does not support the
575 | ``modifier_subtitles`` argument. Use the :meth:`~Item3.add_modifier()`
576 | method instead on the returned item instead.
577 |
578 | Returns:
579 | Item3: Alfred feedback item.
580 |
581 | """
582 | item = self.item_class(title, subtitle, arg, autocomplete,
583 | match, valid, uid, icon, icontype, type,
584 | largetext, copytext, quicklookurl)
585 |
586 | # Add variables to child item
587 | item.variables.update(self.variables)
588 |
589 | self._items.append(item)
590 | return item
591 |
592 | @property
593 | def _session_prefix(self):
594 | """Filename prefix for current session."""
595 | return '_wfsess-{0}-'.format(self.session_id)
596 |
597 | def _mk_session_name(self, name):
598 | """New cache name/key based on session ID."""
599 | return self._session_prefix + name
600 |
601 | def cache_data(self, name, data, session=False):
602 | """Cache API with session-scoped expiry.
603 |
604 | .. versionadded:: 1.25
605 |
606 | Args:
607 | name (str): Cache key
608 | data (object): Data to cache
609 | session (bool, optional): Whether to scope the cache
610 | to the current session.
611 |
612 | ``name`` and ``data`` are the same as for the
613 | :meth:`~workflow.Workflow.cache_data` method on
614 | :class:`~workflow.Workflow`.
615 |
616 | If ``session`` is ``True``, then ``name`` is prefixed
617 | with :attr:`session_id`.
618 |
619 | """
620 | if session:
621 | name = self._mk_session_name(name)
622 |
623 | return super(Workflow3, self).cache_data(name, data)
624 |
625 | def cached_data(self, name, data_func=None, max_age=60, session=False):
626 | """Cache API with session-scoped expiry.
627 |
628 | .. versionadded:: 1.25
629 |
630 | Args:
631 | name (str): Cache key
632 | data_func (callable): Callable that returns fresh data. It
633 | is called if the cache has expired or doesn't exist.
634 | max_age (int): Maximum allowable age of cache in seconds.
635 | session (bool, optional): Whether to scope the cache
636 | to the current session.
637 |
638 | ``name``, ``data_func`` and ``max_age`` are the same as for the
639 | :meth:`~workflow.Workflow.cached_data` method on
640 | :class:`~workflow.Workflow`.
641 |
642 | If ``session`` is ``True``, then ``name`` is prefixed
643 | with :attr:`session_id`.
644 |
645 | """
646 | if session:
647 | name = self._mk_session_name(name)
648 |
649 | return super(Workflow3, self).cached_data(name, data_func, max_age)
650 |
651 | def clear_session_cache(self, current=False):
652 | """Remove session data from the cache.
653 |
654 | .. versionadded:: 1.25
655 | .. versionchanged:: 1.27
656 |
657 | By default, data belonging to the current session won't be
658 | deleted. Set ``current=True`` to also clear current session.
659 |
660 | Args:
661 | current (bool, optional): If ``True``, also remove data for
662 | current session.
663 |
664 | """
665 | def _is_session_file(filename):
666 | if current:
667 | return filename.startswith('_wfsess-')
668 | return filename.startswith('_wfsess-') \
669 | and not filename.startswith(self._session_prefix)
670 |
671 | self.clear_cache(_is_session_file)
672 |
673 | @property
674 | def obj(self):
675 | """Feedback formatted for JSON serialization.
676 |
677 | Returns:
678 | dict: Data suitable for Alfred 3 feedback.
679 |
680 | """
681 | items = []
682 | for item in self._items:
683 | items.append(item.obj)
684 |
685 | o = {'items': items}
686 | if self.variables:
687 | o['variables'] = self.variables
688 | if self.rerun:
689 | o['rerun'] = self.rerun
690 | return o
691 |
692 | def warn_empty(self, title, subtitle=u'', icon=None):
693 | """Add a warning to feedback if there are no items.
694 |
695 | .. versionadded:: 1.31
696 |
697 | Add a "warning" item to Alfred feedback if no other items
698 | have been added. This is a handy shortcut to prevent Alfred
699 | from showing its fallback searches, which is does if no
700 | items are returned.
701 |
702 | Args:
703 | title (unicode): Title of feedback item.
704 | subtitle (unicode, optional): Subtitle of feedback item.
705 | icon (str, optional): Icon for feedback item. If not
706 | specified, ``ICON_WARNING`` is used.
707 |
708 | Returns:
709 | Item3: Newly-created item.
710 |
711 | """
712 | if len(self._items):
713 | return
714 |
715 | icon = icon or ICON_WARNING
716 | return self.add_item(title, subtitle, icon=icon)
717 |
718 | def send_feedback(self):
719 | """Print stored items to console/Alfred as JSON."""
720 | json.dump(self.obj, sys.stdout)
721 | sys.stdout.flush()
722 |
--------------------------------------------------------------------------------
/workflow/workflow3.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kevin-funderburg/alfred-search-keyboard-maestro-vars/a996043d8ed6076b10f245a5747d0b921c11dae7/workflow/workflow3.pyc
--------------------------------------------------------------------------------