booker 1.2.1 → 1.3.0
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.
- checksums.yaml +4 -4
- data/lib/booker.rb +144 -84
- data/lib/bookmarks.rb +122 -11
- data/lib/config.rb +4 -1
- data/lib/consts.rb +2 -17
- metadata +36 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 072d423925a81f2c79870c6019e99615906fdca3391ce795abc80bac49e4688b
|
|
4
|
+
data.tar.gz: e01d49d23c0f36282e387c7b2391c225dde73c0aaa5d6106765296e6f0b333e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5facfef0a12be688be4960b4f76f982765870239a43ca1ea1197fcad4c8225d33bea1e14521bd556219d5b6b0ae51f0083ece20f405f53d9fec438e62f69722
|
|
7
|
+
data.tar.gz: 2b01bbb94d2a50d124b31fd42289f4d2ab24155d333fccc0c96b14f0cc2898dd7823d8cba881f5eaa48b778d13df7c4c48a50f9e79420ce9b166919ce5e3f56d
|
data/lib/booker.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
require "yaml"
|
|
3
3
|
require "find"
|
|
4
4
|
require "json"
|
|
5
|
+
require "optparse"
|
|
5
6
|
require "shellwords"
|
|
6
7
|
require "fileutils"
|
|
7
8
|
require_relative "bookmarks"
|
|
@@ -10,7 +11,7 @@ require_relative "consts"
|
|
|
10
11
|
|
|
11
12
|
# get booker opening command
|
|
12
13
|
class Booker
|
|
13
|
-
@version = "1.
|
|
14
|
+
@version = "1.3.0"
|
|
14
15
|
@@version = @version
|
|
15
16
|
|
|
16
17
|
class << self
|
|
@@ -24,53 +25,70 @@ class Booker
|
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def parse(args)
|
|
27
|
-
# no args given, show interactive bookmark selector
|
|
28
28
|
show_bookmarks if args.none?
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# separate bookmark IDs from other args
|
|
34
|
-
bookmark_ids = []
|
|
35
|
-
other_args = []
|
|
36
|
-
|
|
37
|
-
args.each do |arg|
|
|
38
|
-
if /^[0-9_]+$/.match?(arg) # bookmark ID (digits or underscore)
|
|
39
|
-
bookmark_ids << arg
|
|
40
|
-
else
|
|
41
|
-
other_args << arg
|
|
42
|
-
end
|
|
30
|
+
if args.first&.start_with?("-")
|
|
31
|
+
dispatch_option(args)
|
|
32
|
+
exit 0
|
|
43
33
|
end
|
|
44
34
|
|
|
45
|
-
|
|
46
|
-
unless bookmark_ids.empty?
|
|
47
|
-
open_bookmark bookmark_ids
|
|
48
|
-
end
|
|
35
|
+
bookmark_ids, other_args = args.partition { |a| /^[0-9_]+$/.match?(a) }
|
|
36
|
+
open_bookmark(bookmark_ids) unless bookmark_ids.empty?
|
|
49
37
|
|
|
50
|
-
# then handle remaining args
|
|
51
38
|
unless other_args.empty?
|
|
52
39
|
if other_args.length == 1 && domain.match(other_args.first)
|
|
53
|
-
# single website URL
|
|
54
40
|
puts "opening website: ".grn + other_args.first
|
|
55
41
|
openweb(prep(other_args.first))
|
|
56
42
|
else
|
|
57
|
-
# search for the rest
|
|
58
43
|
open_search(other_args.join(" ").strip)
|
|
59
44
|
end
|
|
60
45
|
end
|
|
61
46
|
end
|
|
62
47
|
|
|
63
|
-
def
|
|
64
|
-
|
|
65
|
-
|
|
48
|
+
def option_parser
|
|
49
|
+
@option_parser ||= OptionParser.new do |opts|
|
|
50
|
+
opts.banner = "Usage: booker [options] [arguments]"
|
|
51
|
+
opts.separator ""
|
|
52
|
+
opts.separator "Main options:"
|
|
53
|
+
opts.on("-b", "--bookmark", "explicitly open bookmark") { @mode = :bookmark }
|
|
54
|
+
opts.on("-i", "--install", "install: all|bookmarks|completion|config|safari") { @mode = :install }
|
|
55
|
+
opts.on("-s", "--search", "explicitly search arguments") { @mode = :search }
|
|
56
|
+
opts.separator ""
|
|
57
|
+
opts.separator "Other options:"
|
|
58
|
+
opts.on("-c", "--complete", "show tab completions") { @mode = :complete }
|
|
59
|
+
opts.on("-v", "--version", "print version") {
|
|
60
|
+
puts @@version
|
|
61
|
+
exit 0
|
|
62
|
+
}
|
|
63
|
+
opts.on_tail("-h", "--help", "show help") {
|
|
64
|
+
puts opts
|
|
65
|
+
exit 0
|
|
66
|
+
}
|
|
67
|
+
end
|
|
66
68
|
end
|
|
67
69
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
+
def dispatch_option(args)
|
|
71
|
+
option_parser.parse!(args)
|
|
72
|
+
|
|
73
|
+
case @mode
|
|
74
|
+
when :bookmark
|
|
75
|
+
pexit "Error: ".red + "booker --bookmark expects bookmark id", 1 if args.empty?
|
|
76
|
+
open_bookmark(args)
|
|
77
|
+
when :install
|
|
78
|
+
args.empty? ? install(%w[completion config bookmarks]) : install(args)
|
|
79
|
+
when :search
|
|
80
|
+
pexit "Error: ".red + "--search requires an argument", 1 if args.empty?
|
|
81
|
+
open_search(args.join(" "))
|
|
82
|
+
when :complete
|
|
83
|
+
Bookmarks.new(args.join(" ")).autocomplete
|
|
84
|
+
end
|
|
85
|
+
rescue OptionParser::InvalidOption => e
|
|
86
|
+
pexit "Error: ".red + e.message, 1
|
|
70
87
|
end
|
|
71
88
|
|
|
72
|
-
def
|
|
73
|
-
|
|
89
|
+
def pexit(msg, sig)
|
|
90
|
+
puts msg
|
|
91
|
+
exit sig
|
|
74
92
|
end
|
|
75
93
|
|
|
76
94
|
def openweb(url)
|
|
@@ -153,66 +171,24 @@ class Booker
|
|
|
153
171
|
exit 0
|
|
154
172
|
end
|
|
155
173
|
|
|
156
|
-
# parse and execute any command line options
|
|
157
|
-
def parse_opt(args)
|
|
158
|
-
valid_opts = %w[--version -v --install -i --help -h
|
|
159
|
-
--complete -c --bookmark -b --search -s]
|
|
160
|
-
|
|
161
|
-
nextarg = args.shift
|
|
162
|
-
errormsg = "Error: ".red + "unrecognized option #{nextarg}"
|
|
163
|
-
pexit errormsg, 1 if !(valid_opts.include? nextarg)
|
|
164
|
-
|
|
165
|
-
# forced bookmarking
|
|
166
|
-
if nextarg == "--bookmark" || nextarg == "-b"
|
|
167
|
-
if args.first.nil?
|
|
168
|
-
pexit "Error: ".red + "booker --bookmark expects bookmark id", 1
|
|
169
|
-
else
|
|
170
|
-
open_bookmark args
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# autocompletion
|
|
175
|
-
if nextarg == "--complete" || nextarg == "-c"
|
|
176
|
-
allargs = args.join(" ")
|
|
177
|
-
bm = Bookmarks.new(allargs)
|
|
178
|
-
bm.autocomplete
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# installation
|
|
182
|
-
if nextarg == "--install" || nextarg == "-i"
|
|
183
|
-
if !args.empty?
|
|
184
|
-
install(args)
|
|
185
|
-
else # do everything
|
|
186
|
-
install(%w[completion config bookmarks])
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# forced searching
|
|
191
|
-
if nextarg == "--search" || nextarg == "-s"
|
|
192
|
-
pexit "--search requires an argument", 1 if args.empty?
|
|
193
|
-
allargs = args.join(" ")
|
|
194
|
-
open_search allargs
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# print version information
|
|
198
|
-
version if nextarg == "--version" || nextarg == "-v"
|
|
199
|
-
|
|
200
|
-
# needs some help
|
|
201
|
-
helper if nextarg == "--help" || nextarg == "-h"
|
|
202
|
-
|
|
203
|
-
exit 0 # dont parse_arg
|
|
204
|
-
end # parse_opt
|
|
205
|
-
|
|
206
174
|
def install(args)
|
|
207
175
|
target = args.shift
|
|
208
176
|
exit 0 if target.nil?
|
|
209
177
|
|
|
178
|
+
# 'all' expands to the full install list (including opt-in safari)
|
|
179
|
+
if /^all$/i.match?(target)
|
|
180
|
+
args = %w[completion config bookmarks safari] + args
|
|
181
|
+
target = args.shift
|
|
182
|
+
end
|
|
183
|
+
|
|
210
184
|
if /comp/i.match?(target) # completion installation
|
|
211
185
|
install_completion
|
|
212
186
|
elsif /book/i.match?(target) # bookmarks installation
|
|
213
187
|
install_bookmarks
|
|
214
188
|
elsif /conf/i.match?(target) # default config file generation
|
|
215
189
|
install_config
|
|
190
|
+
elsif /safari/i.match?(target) # opt-in Safari FDA setup (macOS only)
|
|
191
|
+
install_safari
|
|
216
192
|
else # unknown argument passed into install
|
|
217
193
|
pexit "Failure: ".red + "unknown installation option (#{target})", 1
|
|
218
194
|
end
|
|
@@ -292,7 +268,7 @@ class Booker
|
|
|
292
268
|
|
|
293
269
|
def install_bookmarks
|
|
294
270
|
# locate bookmarks file, show user, write to config?
|
|
295
|
-
puts "searching for
|
|
271
|
+
puts "searching for browser bookmarks..."
|
|
296
272
|
begin
|
|
297
273
|
bms = [] # look for bookmarks with type info
|
|
298
274
|
|
|
@@ -333,14 +309,17 @@ class Booker
|
|
|
333
309
|
end
|
|
334
310
|
end
|
|
335
311
|
|
|
312
|
+
# Search for Safari bookmarks (macOS only)
|
|
313
|
+
safari_path = File.join(ENV["HOME"], "Library/Safari/Bookmarks.plist")
|
|
314
|
+
bms << {path: safari_path, type: :safari} if File.exist?(safari_path)
|
|
315
|
+
|
|
336
316
|
if bms.empty? # no bookmarks found
|
|
337
317
|
puts "Failure: ".red + "bookmarks file could not be found."
|
|
338
318
|
raise
|
|
339
319
|
elsif bms.length == 1
|
|
340
320
|
# Auto-select if only one source found
|
|
341
321
|
selected = bms.first[:path]
|
|
342
|
-
|
|
343
|
-
puts "Found bookmark source: #{type_label} #{selected}".yel
|
|
322
|
+
puts "Found bookmark source: #{bookmark_type_label(bms.first[:type])} #{selected}".yel
|
|
344
323
|
puts "Selected: ".yel + selected
|
|
345
324
|
BConfig.new.write(:bookmarks, selected)
|
|
346
325
|
puts "Success: ".grn + "config file updated with your bookmarks"
|
|
@@ -352,8 +331,7 @@ class Booker
|
|
|
352
331
|
offset = 1
|
|
353
332
|
|
|
354
333
|
bms.each_with_index do |bm, i|
|
|
355
|
-
|
|
356
|
-
puts (i + offset).to_s.grn + " - " + type_label + " " + bm[:path]
|
|
334
|
+
puts (i + offset).to_s.grn + " - " + bookmark_type_label(bm[:type], color: true) + " " + bm[:path]
|
|
357
335
|
end
|
|
358
336
|
|
|
359
337
|
input = gets
|
|
@@ -381,6 +359,17 @@ class Booker
|
|
|
381
359
|
end
|
|
382
360
|
end
|
|
383
361
|
|
|
362
|
+
def bookmark_type_label(type, color: false)
|
|
363
|
+
label = {chrome: "[Chrome]", firefox: "[Firefox]", safari: "[Safari]"}[type] || "[?]"
|
|
364
|
+
return label unless color
|
|
365
|
+
case type
|
|
366
|
+
when :chrome then label.yel
|
|
367
|
+
when :firefox then label.blu
|
|
368
|
+
when :safari then label.cyan
|
|
369
|
+
else label
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
384
373
|
def parse_firefox_profiles(ini_path, firefox_base)
|
|
385
374
|
profiles = []
|
|
386
375
|
current_profile = {}
|
|
@@ -420,4 +409,75 @@ class Booker
|
|
|
420
409
|
rescue
|
|
421
410
|
pexit "Failure: ".red + "could not write example config file to ~/.booker", 1
|
|
422
411
|
end
|
|
412
|
+
|
|
413
|
+
# Opt-in Safari setup: walks through granting Full Disk Access so booker
|
|
414
|
+
# can read ~/Library/Safari/Bookmarks.plist. Not included in the default
|
|
415
|
+
# --install flow because it requires a TCC permission grant.
|
|
416
|
+
def install_safari
|
|
417
|
+
plist = File.join(ENV["HOME"], "Library/Safari/Bookmarks.plist")
|
|
418
|
+
fda_url = "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
|
|
419
|
+
|
|
420
|
+
unless RUBY_PLATFORM.include?("darwin")
|
|
421
|
+
puts "Skip: ".yel + "Safari support is macOS-only."
|
|
422
|
+
return
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
unless File.exist?(plist)
|
|
426
|
+
puts "Skip: ".yel + "Safari bookmarks not found at #{plist}"
|
|
427
|
+
puts "Hint: ".grn + "launch Safari at least once, then re-run."
|
|
428
|
+
return
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
if safari_readable?(plist)
|
|
432
|
+
puts "PASS: ".grn + "Safari bookmarks are already readable."
|
|
433
|
+
return
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
puts "Safari stores bookmarks at:"
|
|
437
|
+
puts " #{plist}".cyan
|
|
438
|
+
puts
|
|
439
|
+
puts "That file is protected by macOS TCC. Grant Full Disk Access to one of:"
|
|
440
|
+
puts
|
|
441
|
+
puts " [A] ".cyan + "Your terminal app".yel + " (simplest; inherited by any tool)"
|
|
442
|
+
puts " [B] ".cyan + "/usr/bin/plutil only".yel + " (narrower scope)"
|
|
443
|
+
puts
|
|
444
|
+
print "Pick [A] or [B] (default A): "
|
|
445
|
+
$stdout.flush
|
|
446
|
+
choice = $stdin.gets.to_s.strip.upcase
|
|
447
|
+
choice = "A" if choice.empty?
|
|
448
|
+
pexit "Error: ".red + "invalid choice.", 1 unless %w[A B].include?(choice)
|
|
449
|
+
|
|
450
|
+
puts
|
|
451
|
+
puts "Opening the Full Disk Access pane..."
|
|
452
|
+
system("open", fda_url)
|
|
453
|
+
puts
|
|
454
|
+
|
|
455
|
+
puts "In the pane that just opened:"
|
|
456
|
+
if choice == "A"
|
|
457
|
+
puts " 1. Click ".yel + "+".cyan + ", add your terminal app from /Applications"
|
|
458
|
+
puts " 2. Toggle it ".yel + "on".cyan
|
|
459
|
+
puts " 3. ".yel + "Fully quit".cyan + " the terminal (Cmd+Q), reopen it,"
|
|
460
|
+
puts " and re-run ".yel + "booker --install safari".cyan + " to verify."
|
|
461
|
+
else
|
|
462
|
+
puts " 1. Click ".yel + "+".cyan
|
|
463
|
+
puts " 2. Press ".yel + "Cmd+Shift+G".cyan + " (opens 'Go to Folder')".yel
|
|
464
|
+
puts " 3. Type ".yel + "/usr/bin/plutil".cyan + " and press Return".yel
|
|
465
|
+
puts " 4. Click ".yel + "Open".cyan + ", then toggle it ".yel + "on".cyan
|
|
466
|
+
puts
|
|
467
|
+
print "Press Return once you've added plutil and toggled it on... "
|
|
468
|
+
$stdout.flush
|
|
469
|
+
$stdin.gets
|
|
470
|
+
|
|
471
|
+
if safari_readable?(plist)
|
|
472
|
+
puts "PASS: ".grn + "Safari bookmarks are now readable."
|
|
473
|
+
else
|
|
474
|
+
puts "FAIL: ".red + "still can't read the bookmarks file."
|
|
475
|
+
puts "Try fully quitting the terminal (Cmd+Q) and re-running."
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def safari_readable?(plist)
|
|
481
|
+
system("plutil", "-lint", "-s", plist, out: File::NULL, err: File::NULL)
|
|
482
|
+
end
|
|
423
483
|
end
|
data/lib/bookmarks.rb
CHANGED
|
@@ -216,31 +216,138 @@ class FirefoxBookmarkParser < BookmarkParser
|
|
|
216
216
|
end
|
|
217
217
|
end
|
|
218
218
|
|
|
219
|
+
# Safari bookmark parser (binary plist format)
|
|
220
|
+
class SafariBookmarkParser < BookmarkParser
|
|
221
|
+
def parse
|
|
222
|
+
xml = convert_plist_to_xml(@file_path)
|
|
223
|
+
return if xml.nil?
|
|
224
|
+
|
|
225
|
+
begin
|
|
226
|
+
require "rexml/document"
|
|
227
|
+
doc = REXML::Document.new(xml)
|
|
228
|
+
plist_el = doc.root
|
|
229
|
+
root_node = plist_el.elements.to_a.first
|
|
230
|
+
root = parse_node(root_node)
|
|
231
|
+
rescue => e
|
|
232
|
+
puts "Warning: ".yel + "Could not parse Safari bookmarks: #{e.message}"
|
|
233
|
+
return
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
walk(root, "|")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
private
|
|
240
|
+
|
|
241
|
+
def convert_plist_to_xml(path)
|
|
242
|
+
contents = File.read(path)
|
|
243
|
+
# Already an XML plist (fixtures, non-macOS). Binary plists need plutil
|
|
244
|
+
# (macOS-only); the JSON converter rejects <data>/<date> fields, so xml1.
|
|
245
|
+
return contents if contents.start_with?("<?xml") || contents.include?("<plist")
|
|
246
|
+
|
|
247
|
+
output = IO.popen(["plutil", "-convert", "xml1", "-o", "-", path], err: File::NULL, &:read)
|
|
248
|
+
return output if $?.success?
|
|
249
|
+
|
|
250
|
+
puts "Warning: ".yel + "Could not read Safari bookmarks file."
|
|
251
|
+
puts "Suggest: ".grn + "grant terminal 'Full Disk Access' in System Settings"
|
|
252
|
+
nil
|
|
253
|
+
rescue Errno::ENOENT
|
|
254
|
+
puts "Warning: ".yel + "Safari bookmarks file not found."
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def parse_node(el)
|
|
259
|
+
case el.name
|
|
260
|
+
when "dict"
|
|
261
|
+
result = {}
|
|
262
|
+
children = el.elements.to_a
|
|
263
|
+
children.each_slice(2) do |key_el, val_el|
|
|
264
|
+
next if key_el.nil? || val_el.nil?
|
|
265
|
+
result[key_el.text.to_s] = parse_node(val_el)
|
|
266
|
+
end
|
|
267
|
+
result
|
|
268
|
+
when "array"
|
|
269
|
+
el.elements.map { |c| parse_node(c) }
|
|
270
|
+
when "string"
|
|
271
|
+
el.text.to_s
|
|
272
|
+
when "integer"
|
|
273
|
+
el.text.to_i
|
|
274
|
+
when "real"
|
|
275
|
+
el.text.to_f
|
|
276
|
+
when "true"
|
|
277
|
+
true
|
|
278
|
+
when "false"
|
|
279
|
+
false
|
|
280
|
+
when "data", "date"
|
|
281
|
+
el.text.to_s
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def walk(node, folder_title)
|
|
286
|
+
children = node["Children"]
|
|
287
|
+
return unless children.is_a?(Array)
|
|
288
|
+
|
|
289
|
+
children.each do |child|
|
|
290
|
+
case child["WebBookmarkType"]
|
|
291
|
+
when "WebBookmarkTypeLeaf"
|
|
292
|
+
parse_leaf(folder_title, child)
|
|
293
|
+
when "WebBookmarkTypeList"
|
|
294
|
+
name = child["Title"].to_s
|
|
295
|
+
sub_title = folder_title + name.gsub(/[:,'"]/, "-").downcase + "/"
|
|
296
|
+
walk(child, sub_title)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def parse_leaf(folder, leaf)
|
|
302
|
+
uri = leaf["URIDictionary"] || {}
|
|
303
|
+
title = (uri["title"] || "").to_s
|
|
304
|
+
url = (leaf["URLString"] || "").to_s
|
|
305
|
+
id = (leaf["WebBookmarkUUID"] || "").to_s
|
|
306
|
+
|
|
307
|
+
values = [folder, title, url, id]
|
|
308
|
+
if matches_search?(values)
|
|
309
|
+
@allurls << Bookmark.new(folder, title, url, id)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
219
314
|
# Main Bookmarks facade class
|
|
220
315
|
class Bookmarks
|
|
316
|
+
PARSER_SOURCE = {
|
|
317
|
+
ChromeBookmarkParser => "chrome",
|
|
318
|
+
FirefoxBookmarkParser => "firefox",
|
|
319
|
+
SafariBookmarkParser => "safari"
|
|
320
|
+
}
|
|
321
|
+
|
|
221
322
|
def initialize(search_term = "")
|
|
222
323
|
@conf = BConfig.new
|
|
223
|
-
file_paths = @conf.bookmarks
|
|
324
|
+
file_paths = @conf.bookmarks
|
|
224
325
|
@allurls = []
|
|
326
|
+
seen = {}
|
|
327
|
+
|
|
328
|
+
loaded_sources = file_paths.count { |p| p && File.exist?(p) }
|
|
329
|
+
@multi_source = loaded_sources > 1
|
|
225
330
|
|
|
226
|
-
# Parse bookmarks from all sources
|
|
227
331
|
file_paths.each_with_index do |file_path, source_index|
|
|
228
332
|
next unless file_path && File.exist?(file_path)
|
|
229
333
|
|
|
230
334
|
parser_class = detect_parser(file_path)
|
|
335
|
+
source = PARSER_SOURCE[parser_class]
|
|
231
336
|
parser = parser_class.new(file_path, search_term)
|
|
232
337
|
parser.parse
|
|
233
338
|
|
|
234
|
-
# Merge results, prefixing IDs to ensure uniqueness across sources
|
|
235
339
|
parser.results.each do |bookmark|
|
|
236
|
-
|
|
237
|
-
|
|
340
|
+
key = [bookmark.title, bookmark.url]
|
|
341
|
+
next if seen[key]
|
|
342
|
+
seen[key] = true
|
|
343
|
+
|
|
344
|
+
@allurls << Bookmark.new(
|
|
238
345
|
bookmark.folder,
|
|
239
346
|
bookmark.title,
|
|
240
347
|
bookmark.url,
|
|
241
|
-
"#{source_index}_#{bookmark.id}"
|
|
348
|
+
"#{source_index}_#{bookmark.id}",
|
|
349
|
+
source
|
|
242
350
|
)
|
|
243
|
-
@allurls << unique_bookmark
|
|
244
351
|
end
|
|
245
352
|
end
|
|
246
353
|
end
|
|
@@ -260,8 +367,9 @@ class Bookmarks
|
|
|
260
367
|
folder = url.folder.gsub(/^\|/, "")
|
|
261
368
|
folder = (folder == "/") ? "[root]" : folder.chomp("/")
|
|
262
369
|
|
|
263
|
-
# Format: folder | title
|
|
264
|
-
|
|
370
|
+
# Format: [source] folder | title (source only when multi-source)
|
|
371
|
+
prefix = (@multi_source && url.source) ? "[#{url.source}] " : ""
|
|
372
|
+
name = prefix + folder + " | " + url.title.gsub(/[^a-z0-9\-\/_ ]/i, "")
|
|
265
373
|
name.squeeze!("-")
|
|
266
374
|
name.squeeze!(" ")
|
|
267
375
|
# Use half terminal width for name to leave room for URL
|
|
@@ -290,6 +398,8 @@ class Bookmarks
|
|
|
290
398
|
def detect_parser(file_path)
|
|
291
399
|
if file_path&.end_with?(".sqlite")
|
|
292
400
|
FirefoxBookmarkParser
|
|
401
|
+
elsif file_path&.end_with?(".plist")
|
|
402
|
+
SafariBookmarkParser
|
|
293
403
|
else
|
|
294
404
|
ChromeBookmarkParser
|
|
295
405
|
end
|
|
@@ -314,11 +424,12 @@ end
|
|
|
314
424
|
|
|
315
425
|
# clean bookmark title, set attrs
|
|
316
426
|
class Bookmark
|
|
317
|
-
attr_reader :title, :folder, :url, :id
|
|
318
|
-
def initialize(f, t, u, id)
|
|
427
|
+
attr_reader :title, :folder, :url, :id, :source
|
|
428
|
+
def initialize(f, t, u, id, source = nil)
|
|
319
429
|
@title = t.gsub(/[:'"+]/, " ").downcase
|
|
320
430
|
@folder = f
|
|
321
431
|
@url = u
|
|
322
432
|
@id = id
|
|
433
|
+
@source = source
|
|
323
434
|
end
|
|
324
435
|
end
|
data/lib/config.rb
CHANGED
|
@@ -77,7 +77,7 @@ class BConfig
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
def detect_default_bookmarks
|
|
80
|
-
# Return ALL available bookmark sources (
|
|
80
|
+
# Return ALL available bookmark sources (Chrome, Firefox, Safari)
|
|
81
81
|
all_sources = discover_all_bookmark_sources
|
|
82
82
|
|
|
83
83
|
# If we found sources, return them all; otherwise return a default single path
|
|
@@ -140,6 +140,9 @@ class BConfig
|
|
|
140
140
|
end
|
|
141
141
|
end
|
|
142
142
|
|
|
143
|
+
safari_path = HOME + "/Library/Safari/Bookmarks.plist"
|
|
144
|
+
sources << safari_path if File.exist?(safari_path)
|
|
145
|
+
|
|
143
146
|
sources.uniq
|
|
144
147
|
end
|
|
145
148
|
|
data/lib/consts.rb
CHANGED
|
@@ -2,21 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
# todo - wrap in module
|
|
4
4
|
|
|
5
|
-
HELP_BANNER = <<~EOS
|
|
6
|
-
Open browser:
|
|
7
|
-
$ booker [option] [arguments]
|
|
8
|
-
|
|
9
|
-
Main options:
|
|
10
|
-
--bookmark, -b: explicity open bookmark
|
|
11
|
-
--install, -i: install [bookmarks, completion, config]
|
|
12
|
-
--search, -s: explicity search arguments
|
|
13
|
-
|
|
14
|
-
Others:
|
|
15
|
-
--complete, -c: show tab completions
|
|
16
|
-
--version, -v: print version
|
|
17
|
-
--help, -h: show help
|
|
18
|
-
EOS
|
|
19
|
-
|
|
20
5
|
DEF_CONFIG = <<~EOS
|
|
21
6
|
---
|
|
22
7
|
:searcher: https://google.com/search?q=
|
|
@@ -78,8 +63,8 @@ COMPLETION = <<~EOS
|
|
|
78
63
|
'(--bookmark)--bookmark[do bookmark completion]'\
|
|
79
64
|
'(-c)-c[show bookmark completions]'\
|
|
80
65
|
'(--complete)--complete[show bookmark completions]'\
|
|
81
|
-
'(-i)-i[perform installations (bookmarks, completion, config)]'\
|
|
82
|
-
'(--install)--install[perform installations (bookmarks, completion, config)]'\
|
|
66
|
+
'(-i)-i[perform installations (all, bookmarks, completion, config, safari)]'\
|
|
67
|
+
'(--install)--install[perform installations (all, bookmarks, completion, config, safari)]'\
|
|
83
68
|
'(-s)-s[search google for...]'\
|
|
84
69
|
'(--search)--search[search google for...]'\
|
|
85
70
|
'(-h)-h[show booker help]'\
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: booker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeremy Warner
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: json
|
|
@@ -38,6 +37,20 @@ dependencies:
|
|
|
38
37
|
- - "~>"
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
39
|
version: '1.7'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rexml
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.2'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.2'
|
|
41
54
|
- !ruby/object:Gem::Dependency
|
|
42
55
|
name: rspec
|
|
43
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,11 +65,26 @@ dependencies:
|
|
|
52
65
|
- - "~>"
|
|
53
66
|
- !ruby/object:Gem::Version
|
|
54
67
|
version: '3.13'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: standard
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.0'
|
|
55
82
|
description: |2
|
|
56
83
|
Search, browse, and open bookmarks from the command line. Supports Chrome,
|
|
57
|
-
Chromium, and
|
|
58
|
-
keyword. ZSH users get tab completion through bookmark matches.
|
|
59
|
-
open websites directly or search with your preferred search
|
|
84
|
+
Chromium, Firefox, and Safari. Browse all bookmarks interactively, or
|
|
85
|
+
search by keyword. ZSH users get tab completion through bookmark matches.
|
|
86
|
+
Can also open websites directly or search with your preferred search
|
|
87
|
+
engine.
|
|
60
88
|
email: jeremywrnr@gmail.com
|
|
61
89
|
executables:
|
|
62
90
|
- booker
|
|
@@ -87,8 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
87
115
|
- !ruby/object:Gem::Version
|
|
88
116
|
version: '0'
|
|
89
117
|
requirements: []
|
|
90
|
-
rubygems_version:
|
|
91
|
-
signing_key:
|
|
118
|
+
rubygems_version: 4.0.8
|
|
92
119
|
specification_version: 4
|
|
93
|
-
summary: CLI bookmark manager for Chrome and
|
|
120
|
+
summary: CLI bookmark manager for Chrome, Firefox, and Safari
|
|
94
121
|
test_files: []
|