hookapp 0.0.7 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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