hookapp 0.0.7 → 2.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,508 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shellwords'
4
+ require 'uri'
5
+ # Hook.app functions
6
+ class HookApp
7
+ # Create a single regex for validation of an
8
+ # array by first char or full match.
9
+ def format_regex(options)
10
+ fmt_rx_array = []
11
+ options.map {|fmt| fmt_rx_array.push(fmt.sub(/^(.)(.*?)$/, '\1(\2)?')) }
12
+ Regexp.new("^(#{fmt_rx_array.join('|')})$",'i')
13
+ end
14
+
15
+ # Check if format fully matches or matches the first
16
+ # character of available options.
17
+ # Return full valid format or nil
18
+ def validate_format(fmt, options)
19
+ valid_format_rx = options.map { |format| format.sub(/^(.)(.*)$/, '^\1(\2)?$') }
20
+ valid_format = nil
21
+ valid_format_rx.each_with_index do |rx, i|
22
+ cmp = Regexp.new(rx, 'i')
23
+ next unless fmt =~ cmp
24
+
25
+ valid_format = options[i]
26
+ break
27
+ end
28
+ valid_format
29
+ end
30
+
31
+ # Get a Hook bookmark for file or URL. Return bookmark hash.
32
+ def bookmark_for(url)
33
+ url.valid_hook!
34
+ raise "Invalid target: #{url}" unless url
35
+
36
+ begin
37
+ mark = `osascript <<'APPLESCRIPT'
38
+ tell application "Hook"
39
+ set _hook to make bookmark with data "#{url}"
40
+ if _hook is missing value
41
+ return ""
42
+ else
43
+ return name of _hook & "||" & address of _hook & "||" & path of _hook
44
+ end if
45
+ end tell
46
+ APPLESCRIPT`.strip
47
+ rescue p => e
48
+ raise e
49
+ end
50
+
51
+ raise "Error getting bookmark for #{url}" if mark.empty?
52
+ mark.split_hook
53
+ end
54
+
55
+ # Get bookmarks hooked to file or URL. Return array of bookmark hashes.
56
+ def get_hooks(url)
57
+ url.valid_hook!
58
+ raise "Invalid target: #{url}" unless url
59
+
60
+ hooks = `osascript <<'APPLESCRIPT'
61
+ tell application "Hook"
62
+ set _mark to make bookmark with data "#{url}"
63
+ if _mark is missing value
64
+ return ""
65
+ end if
66
+ set _hooks to hooked bookmarks of _mark
67
+ set _out to {}
68
+ repeat with _hook in _hooks
69
+ set _out to _out & (name of _hook & "||" & address of _hook & "||" & path of _hook)
70
+ end repeat
71
+ set {astid, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "^^"}
72
+ set _output to _out as string
73
+ set AppleScript's text item delimiters to astid
74
+ return _output
75
+ end tell
76
+ APPLESCRIPT`.strip
77
+ hooks.split_hooks
78
+ end
79
+
80
+ # Get a bookmark from the foreground document of specified app.
81
+ def bookmark_from_app(app, opts)
82
+ mark = `osascript <<'APPLESCRIPT'
83
+ tell application "System Events" to set front_app to name of first application process whose frontmost is true
84
+ tell application "#{app}" to activate
85
+ delay 2
86
+ tell application "Hook"
87
+ set _hook to (bookmark from active window)
88
+ set _output to (name of _hook & "||" & address of _hook & "||" & path of _hook)
89
+ end tell
90
+ tell application front_app to activate
91
+ return _output
92
+ APPLESCRIPT`.strip.split_hook
93
+ title = mark[:name].empty? ? "#{app.cap} link" : mark[:name]
94
+ output = opts[:markdown] ? "[#{title}](#{mark[:url]})" : mark[:url]
95
+
96
+ if opts[:copy]
97
+ "Copied Markdown link for '#{title}' to clipboard" if output.clip
98
+ else
99
+ output
100
+ end
101
+ end
102
+
103
+ # Search boomark names/titles. Return array of bookmark hashes.
104
+ def search_name(search)
105
+ `osascript <<'APPLESCRIPT'
106
+ set searchString to "#{search.strip}"
107
+ tell application "Hook"
108
+ set _marks to every bookmark whose name contains searchString
109
+ set _out to {}
110
+ repeat with _hook in _marks
111
+ set _out to _out & (name of _hook & "||" & address of _hook & "||" & path of _hook)
112
+ end repeat
113
+ set {astid, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "^^"}
114
+ set _output to _out as string
115
+ set AppleScript's text item delimiters to astid
116
+ return _output
117
+ end tell
118
+ APPLESCRIPT`.strip.split_hooks
119
+ end
120
+
121
+ # Search bookmark paths and addresses. Return array of bookmark hashes.
122
+ def search_path_or_address(search)
123
+ `osascript <<'APPLESCRIPT'
124
+ set searchString to "#{search.strip}"
125
+ tell application "Hook"
126
+ set _marks to every bookmark whose path contains searchString or address contains searchString
127
+ set _out to {}
128
+ repeat with _hook in _marks
129
+ set _out to _out & (name of _hook & "||" & address of _hook & "||" & path of _hook)
130
+ end repeat
131
+ set {astid, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "^^"}
132
+ set _output to _out as string
133
+ set AppleScript's text item delimiters to astid
134
+ return _output
135
+ end tell
136
+ APPLESCRIPT`.strip.split_hooks
137
+ end
138
+
139
+ # Get all known bookmarks. Return array of bookmark hashes.
140
+ def all_bookmarks
141
+ `osascript <<'APPLESCRIPT'
142
+ tell application "Hook"
143
+ set _marks to every bookmark
144
+ set _out to {}
145
+ repeat with _hook in _marks
146
+ set _out to _out & (name of _hook & "||" & address of _hook & "||" & path of _hook)
147
+ end repeat
148
+ set {astid, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "^^"}
149
+ set _output to _out as string
150
+ set AppleScript's text item delimiters to astid
151
+ return _output
152
+ end tell
153
+ APPLESCRIPT`.strip.split_hooks
154
+ end
155
+
156
+ # Search bookmarks, using both names and addresses unless options contain ":names_only".
157
+ # Return results as formatted list.
158
+ def search_bookmarks(search, opts)
159
+ unless search.nil? || search.empty?
160
+ result = search_name(search)
161
+ unless opts[:names_only]
162
+ more_results = search_path_or_address(search)
163
+ result = result.concat(more_results).uniq
164
+ end
165
+ else
166
+ result = all_bookmarks
167
+ end
168
+
169
+ separator = opts[:format] == 'paths' && opts[:null_separator] ? "\0" : "\n"
170
+
171
+ output = output_array(result, opts)
172
+
173
+ if opts[:format] =~ /^v/
174
+ "Search results for: #{search}\n---------\n" + output.join("\n") if output
175
+ else
176
+ output.join(separator) if output
177
+ end
178
+ end
179
+
180
+ # Create a bookmark for specified file/url and copy to the clipboard.
181
+ def clip_bookmark(url, opts)
182
+ mark = bookmark_for(url)
183
+ copy_bookmark(mark[:name], mark[:url], opts)
184
+ end
185
+
186
+ # Create a bookmark from specified title and url and copy to the clipboard.
187
+ def copy_bookmark(title, url, opts)
188
+ raise "No URL found" if url.empty?
189
+ title = title.empty? ? 'No title' : title
190
+ output = opts[:markdown] ? "[#{title}](#{url})" : url
191
+ output.clip
192
+ %(Copied #{opts[:markdown] ? 'Markdown link' : 'Hook URL'} for '#{title}' to clipboard)
193
+ end
194
+
195
+ # Generate a menu of available hooks for selecting one or more hooks to operate on.
196
+ # Revamped to use `fzf`, which is embedded as `lib/helpers/fuzzyfilefinder` to avoid any conflicts.
197
+ # Allows multiple selections with tab key, and type-ahead fuzzy filtering of results.
198
+ def select_hook(marks)
199
+ # intpad = marks.length.to_s.length + 1
200
+ # marks.each_with_index do |mark, i|
201
+ # STDERR.printf "%#{intpad}d) %s\n", i + 1, mark[:name]
202
+ # end
203
+ # STDERR.print 'Open which bookmark: '
204
+ # sel = STDIN.gets.strip.to_i
205
+ # raise 'Invalid selection' unless sel.positive? && sel <= marks.length
206
+
207
+ # marks[sel - 1]
208
+
209
+ options = marks.map {|mark|
210
+ if mark[:name]
211
+ id = mark[:name]
212
+ elsif mark[:path]
213
+ id = mark[:path]
214
+ elsif mark[:url]
215
+ id = mark[:url]
216
+ else
217
+ return false
218
+ end
219
+
220
+ if mark[:path]
221
+ loc = File.dirname(mark[:path])
222
+ elsif mark[:url]
223
+ url = URI.parse(mark[:url])
224
+ id = mark[:url]
225
+ loc = url.scheme + " - " + url.hostname
226
+ else
227
+ loc = ""
228
+ end
229
+
230
+ "#{id}\t#{mark[:path]}\t#{mark[:url]}\t#{loc}"
231
+ }.delete_if { |mark| !mark }
232
+
233
+ raise "Error processing available hooks" if options.empty?
234
+
235
+ args = ['--layout=reverse-list',
236
+ '--header="esc: cancel, tab: multi-select, return: open > "',
237
+ '--prompt=" Select hooks > "',
238
+ '--multi',
239
+ '--tabstop=4',
240
+ '--delimiter="\t"',
241
+ '--with-nth=1,4',
242
+ '--height=60%',
243
+ '--min-height=10'
244
+ ]
245
+
246
+ fzf = File.join(File.dirname(__FILE__), '../helpers/fuzzyfilefinder')
247
+
248
+ sel = `echo #{Shellwords.escape(options.join("\n"))} | '#{fzf}' #{args.join(' ')}`.chomp
249
+ res = sel.split(/\n/).map { |s|
250
+ ps = s.split(/\t/)
251
+ { name: ps[0], path: ps[1], url: ps[2] }
252
+ }
253
+
254
+ if res.size == 0
255
+ raise 'Cancelled (empty response)'
256
+ end
257
+
258
+ res
259
+ end
260
+
261
+ # Open the Hook GUI for browsing/performing actions on a file or url
262
+ def open_gui(url)
263
+ result = `osascript <<'APPLESCRIPT'
264
+ tell application "Hook"
265
+ set _mark to make bookmark with data "#{url.valid_hook}"
266
+ if _mark is missing value
267
+ return "Failed to create bookmark for #{url}"
268
+ else
269
+ invoke on _mark
270
+ return ""
271
+ end if
272
+ end tell
273
+ APPLESCRIPT`.strip
274
+ raise result unless result.empty?
275
+ end
276
+
277
+ # Select from a menu of available hooks and open using macOS `open`.
278
+ def open_linked(url)
279
+ marks = get_hooks(url)
280
+ if marks.empty?
281
+ warn "No hooks found for #{url}"
282
+ else
283
+ res = select_hook(marks)
284
+ res.each {|mark|
285
+ `open '#{mark[:url]}'`
286
+ }
287
+ end
288
+ end
289
+
290
+ # Link 2 or more files/urls with bi-directional hooks.
291
+ def link_files(args)
292
+ target = args.pop
293
+ target.valid_hook!
294
+ raise "Invalid target: #{target}" unless target
295
+
296
+ args.each do |file|
297
+ file.valid_hook!
298
+ raise "Invalid target: #{file}" unless file
299
+
300
+ puts "Linking #{file} and #{target}..."
301
+ `osascript <<'APPLESCRIPT'
302
+ tell application "Hook"
303
+ set _mark1 to make bookmark with data "#{file}"
304
+ set _mark2 to make bookmark with data "#{target}"
305
+ hook _mark1 and _mark2
306
+ return true
307
+ end tell
308
+ APPLESCRIPT`
309
+ end
310
+ "Linked #{args.length} files to #{target}"
311
+ end
312
+
313
+ # Copy all hooks from source file to target file
314
+ def clone_hooks(args)
315
+ target = args.pop.valid_hook
316
+ source = args[0].valid_hook
317
+
318
+ if target && source
319
+ hooks = get_hooks(source)
320
+ hooks.each do |hook|
321
+ `osascript <<'APPLESCRIPT'
322
+ tell application "Hook"
323
+ set _mark1 to make bookmark with data "#{hook[:url]}"
324
+ set _mark2 to make bookmark with data "#{target}"
325
+ hook _mark1 and _mark2
326
+ return true
327
+ end tell
328
+ APPLESCRIPT`
329
+ end
330
+ "Hooks from #{source} cloned to #{target}"
331
+ else
332
+ raise 'Invalid file specified'
333
+ end
334
+ end
335
+
336
+ # Delete all hooked files/urls from target file
337
+ def delete_all_hooks(url, force: false)
338
+ unless force
339
+ STDERR.print "Are you sure you want to delete ALL hooks from #{url} (y/N)? "
340
+ res = STDIN.gets.strip
341
+ end
342
+
343
+ if res =~ /^y/i || force
344
+ get_hooks(url).each do |hook|
345
+ `osascript <<'APPLESCRIPT'
346
+ tell application "Hook"
347
+ set _mark1 to make bookmark with data "#{hook[:url]}"
348
+ set _mark2 to make bookmark with data "#{url}"
349
+ unhook _mark1 and _mark2
350
+ return true
351
+ end tell
352
+ APPLESCRIPT`
353
+ end
354
+ "Removed all hooks from #{url}"
355
+ end
356
+ end
357
+
358
+ # Delete hooks between two files/urls
359
+ def delete_hooks(args, opts)
360
+ urls = args.map(&:valid_hook).delete_if { |url| !url }
361
+ output = []
362
+ if opts[:all]
363
+ urls.each_with_index do |url, i|
364
+ raise "Invalid target: #{args[i]}" unless url
365
+
366
+ output.push(delete_all_hooks(url, force: opts[:force]))
367
+ end
368
+ return output.join("\n")
369
+ end
370
+
371
+ if urls.length == 2
372
+ source = urls[0]
373
+ target = urls[1]
374
+ `osascript <<'APPLESCRIPT'
375
+ tell application "Hook"
376
+ set _mark1 to make bookmark with data "#{source}"
377
+ set _mark2 to make bookmark with data "#{target}"
378
+ unhook _mark1 and _mark2
379
+ return true
380
+ end tell
381
+ APPLESCRIPT`
382
+ "Hook removed between #{source} and #{target}"
383
+ else
384
+ raise 'Invalid number of URLs or files specified'
385
+ end
386
+ end
387
+
388
+ # Create bi-directional links between every file/url in the list of arguments
389
+ def link_all(args)
390
+ args.each do |file|
391
+ source = file.valid_hook
392
+ link_to = args.dup.map(&:valid_hook).reject { |url| url == source }
393
+ link_to.each do |url|
394
+ `osascript <<'APPLESCRIPT'
395
+ tell application "Hook"
396
+ set _mark1 to make bookmark with data "#{source}"
397
+ set _mark2 to make bookmark with data "#{url}"
398
+ hook _mark1 and _mark2
399
+ return true
400
+ end tell
401
+ APPLESCRIPT`
402
+ end
403
+ end
404
+ "Linked #{args.length} files to each other"
405
+ end
406
+
407
+ # Get a list of all hooks on a file/url.
408
+ def linked_bookmarks(args, opts)
409
+ result = []
410
+
411
+ separator = args.length == 1 && opts[:format] == 'paths' && opts[:null_separator] ? "\0" : "\n"
412
+
413
+ if args.nil? || args.empty?
414
+ result = output_array(all_bookmarks, opts)
415
+ else
416
+ args.each do |url|
417
+ source_mark = bookmark_for(url)
418
+ filename = source_mark[:name]
419
+
420
+ case opts[:format]
421
+ when /^m/
422
+ filename = "[#{source_mark[:name]}](#{source_mark[:url]})"
423
+ filename += " <file://#{CGI.escape(source_mark[:path])}>" if source_mark[:path]
424
+ when /^p/
425
+ filename = "File: #{source_mark[:name]}"
426
+ filename += " (#{source_mark[:path]})" if source_mark[:path]
427
+ when /^h/
428
+ filename = "File: #{source_mark[:name]}"
429
+ filename += " (#{source_mark[:url]})" if source_mark[:url]
430
+ else
431
+ filename = "Bookmarks attached to #{source_mark[:path] || source_mark[:url]}"
432
+ end
433
+
434
+ hooks_arr = get_hooks(url)
435
+
436
+ output = output_array(hooks_arr, opts)
437
+ result.push({ file: filename, links: output.join(separator) }) if output
438
+ end
439
+
440
+
441
+ if result.length > 1 || opts[:format] == 'verbose'
442
+ result.map! do |res|
443
+ "#{res[:file]}\n\n#{res[:links]}\n"
444
+ end
445
+ else
446
+ result.map! do |res|
447
+ res[:links]
448
+ end
449
+ end
450
+ end
451
+ result.join(separator)
452
+ end
453
+
454
+ # Output an array of hooks in the given format.
455
+ def output_array(hooks_arr, opts)
456
+ if !hooks_arr.empty?
457
+ hooks_arr.reject! { |h| h[:path].nil? || h[:path] == '' } if opts[:files_only]
458
+
459
+ output = []
460
+
461
+ case opts[:format]
462
+ when /^m/
463
+ hooks_arr.each do |h|
464
+ if h[:name].empty?
465
+ title = h[:url]
466
+ else
467
+ title = h[:name]
468
+ end
469
+ output.push("- [#{title}](#{h[:url]})")
470
+ end
471
+ when /^p/
472
+ hooks_arr.each do |h|
473
+ output.push(h[:path].nil? ? h[:url] : h[:path])
474
+ end
475
+ when /^h/
476
+ hooks_arr.each do |h|
477
+ output.push(h[:url])
478
+ end
479
+ else
480
+ hooks_arr.each do |h|
481
+ output.push("Title: #{h[:name]}\nPath: #{h[:path]}\nAddress: #{h[:url]}\n---------------------")
482
+ end
483
+ end
484
+ else
485
+ warn 'No bookmarks'
486
+ end
487
+
488
+ output
489
+ end
490
+
491
+ def encode(string)
492
+ result = `osascript <<'APPLESCRIPT'
493
+ tell application "Hook"
494
+ percent encode "#{string.escape_quotes}"
495
+ end tell
496
+ APPLESCRIPT`.strip.gsub(/'/,'%27')
497
+ print result
498
+ end
499
+
500
+ def decode(string)
501
+ result = `osascript <<'APPLESCRIPT'
502
+ tell application "Hook"
503
+ percent decode "#{string.escape_quotes}"
504
+ end tell
505
+ APPLESCRIPT`.strip
506
+ print result
507
+ end
508
+ end