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.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/lib/booker.rb +144 -84
  3. data/lib/bookmarks.rb +122 -11
  4. data/lib/config.rb +4 -1
  5. data/lib/consts.rb +2 -17
  6. metadata +36 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7951b5a4c5802dd8cd01774cd94908d9ba86aa9e1f75db34d1a59b7eed3e66ca
4
- data.tar.gz: b46ae04d09adc03b98346a5d9b502bc617179a266282a68fe7f9709c4e928ade
3
+ metadata.gz: 072d423925a81f2c79870c6019e99615906fdca3391ce795abc80bac49e4688b
4
+ data.tar.gz: e01d49d23c0f36282e387c7b2391c225dde73c0aaa5d6106765296e6f0b333e2
5
5
  SHA512:
6
- metadata.gz: c9c0e5d85e201beeeb7ca9c091f3cfeeaa2c27da3200726f783f662cc87fb51a5007a51b54999b9d6442b102b1be6fea80b4b4c33d84af015705e35fd28b69f5
7
- data.tar.gz: 656c7251b8e6b03d1e1a9a60823504cbdea2ec8c076c9afb0700895ae7db70fb89dc85338bad4d97750815b987188495df686d22e69efbbfb2993db19c90efd2
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.2.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
- # if arg starts with hyphen, parse option
31
- parse_opt args if /^-.*/.match?(args.first)
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
- # open all bookmarks first
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 pexit(msg, sig)
64
- puts msg
65
- exit sig
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 helper
69
- pexit HELP_BANNER, 0
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 version
73
- pexit @@version, 0
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 chrome and firefox bookmarks..."
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
- type_label = (bms.first[:type] == :chrome) ? "[Chrome]" : "[Firefox]"
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
- type_label = (bm[:type] == :chrome) ? "[Chrome]".yel : "[Firefox]".blu
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 # Now returns an array
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
- # Create unique ID: source_index + original_id
237
- unique_bookmark = Bookmark.new(
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
- name = folder + " |" + url.title.gsub(/[^a-z0-9\-\/_ ]/i, "")
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 (both Chrome and Firefox)
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.2.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: 2026-02-09 00:00:00.000000000 Z
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 Firefox. Browse all bookmarks interactively, or search by
58
- keyword. ZSH users get tab completion through bookmark matches. Can also
59
- open websites directly or search with your preferred search engine.
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: 3.4.20
91
- signing_key:
118
+ rubygems_version: 4.0.8
92
119
  specification_version: 4
93
- summary: CLI bookmark manager for Chrome and Firefox
120
+ summary: CLI bookmark manager for Chrome, Firefox, and Safari
94
121
  test_files: []