booker 2.1.0 → 2.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13dc462a7bedf194daa250fdd6fc0b7bdcc61374873c23fb3a03e48e73c5e287
4
- data.tar.gz: bffeeb80575559e1404dd81543ab5b36a87fe74cb8e3c908495c5afb0196f0cd
3
+ metadata.gz: a0f32b2a81f8c03acdc8c56ce6f9554a1229aced8a731d103fbfd53f1d03b5d3
4
+ data.tar.gz: 2d0386dcc31468311faaac5e5ec749d19623ad40f9f6b083a86b77cd9632be01
5
5
  SHA512:
6
- metadata.gz: 11fc60dbe348abd38052010e3073f7ad524bc81b0aeb1017741f25a1a8b3108654443c91444c40d07d065907f9b59117768ab5f2e9923b3fbd529f335c5082c7
7
- data.tar.gz: 130643b4c4920f03d273cdfde85955295ca6fd701031fffe6228aad52e26e8b5e7f11c784d6ad5f914371785ad321074e155fff2a5eab657030bb45a60671d9c
6
+ metadata.gz: 963773985475ebf1a6e9c2fa340ceb79a607e4dd052e7c0dab1507881c0cd736f881388e7bd0445e8381dd69d561ead3bc14a9d940aed1cd08c07eb3770dc593
7
+ data.tar.gz: 279afd708175be5626eea44dfe3733ad435f4bb527dc4c9995bb97027376df4551b84c464f9d9ac69150cecddeb440b15b3d03f7eb8b1d706ef26589e8d03e4b
data/lib/booker/cli.rb CHANGED
@@ -13,10 +13,10 @@ module Booker
13
13
 
14
14
  # a bookmark id is "<source index>_<browser id>". safari uses a uuid where
15
15
  # chrome and firefox number theirs, so the second half is anything id-shaped
16
- # rather than digits; bare digits stay valid for single-source configs
17
- # written before the prefix existed. completion inserts urls now, but ids
18
- # are still first class - --bookmark takes them, the picker returns them
19
- BOOKMARK_ID = /\A(?:\d+_[a-z0-9-]+|[0-9_]+)\z/i
16
+ # rather than digits. a bare number is a search: `booker 2024` is looking
17
+ # for a year. completion inserts urls now, but ids are still first class -
18
+ # --bookmark takes them, the picker returns them
19
+ BOOKMARK_ID = /\A\d+_[a-z0-9-]+\z/i
20
20
 
21
21
  def initialize(args)
22
22
  parse args
@@ -83,7 +83,9 @@ module Booker
83
83
  case @mode
84
84
  when :bookmark
85
85
  pexit "Error: ".red + "booker --bookmark expects bookmark id" if args.empty?
86
- open_bookmark(args)
86
+ # a bare number is an id from before ids carried their source, when
87
+ # there was only ever source 0
88
+ open_bookmark(args.map { |id| /\A\d+\z/.match?(id) ? "0_#{id}" : id })
87
89
  when :install
88
90
  installer.install(args.empty? ? %w[completion config bookmarks] : args)
89
91
  when :search
@@ -98,7 +100,9 @@ module Booker
98
100
  when :complete_raw
99
101
  Bookmarks.new(args.join(" ")).autocomplete_raw
100
102
  end
101
- rescue OptionParser::InvalidOption => e
103
+ # ParseError rather than InvalidOption: `booker --list=x` is a
104
+ # NeedlessArgument, and every other subclass is just as much a usage error
105
+ rescue OptionParser::ParseError => e
102
106
  pexit "Error: ".red + e.message
103
107
  end
104
108
 
@@ -146,8 +150,10 @@ module Booker
146
150
  def open_search(term)
147
151
  puts "searching ".grn + term
148
152
  search = Config.default.searcher
149
- term = term.tr(" ", "+")
150
- openweb(search + term) # No shell escape needed - it's a URL
153
+ # encoded, not just spaces to "+": a literal + reads back as a space, &
154
+ # ends the query and # starts a fragment, so `c++` searched for "c"
155
+ require "uri"
156
+ openweb(search + URI.encode_www_form_component(term))
151
157
  end
152
158
 
153
159
  # a term matching bookmarks is offered as a choice; one matching nothing is
data/lib/booker/config.rb CHANGED
@@ -57,6 +57,10 @@ module Booker
57
57
 
58
58
  # configuration
59
59
  class Config
60
+ # raised by #write rather than replacing a file that exists but would not
61
+ # parse: the defaults in memory are a fallback, not the user's settings
62
+ class Unreadable < StandardError; end
63
+
60
64
  VALID = [:searcher, :bookmarks, :browser, :picker].freeze
61
65
  HOME = ENV.fetch("HOME", "/usr/local/").freeze
62
66
  YAMLCONF = (HOME + "/.booker.yml").freeze
@@ -87,7 +91,7 @@ module Booker
87
91
 
88
92
  # prune bad config keys
89
93
  @config.each_key do |k|
90
- if !VALID.include? k.to_sym
94
+ if !VALID.include? k
91
95
  warn "Failure:".red + " Bad key found in config file: #{k}"
92
96
  exit 1
93
97
  end
@@ -111,7 +115,15 @@ module Booker
111
115
 
112
116
  def read(file)
113
117
  # #write emits symbol keys, so they have to be permitted on the way back
114
- YAML.safe_load_file(file, permitted_classes: [Symbol])
118
+ conf = YAML.safe_load_file(file, permitted_classes: [Symbol])
119
+ # an empty file is no config at all, which is fine to write over
120
+ return false if conf.nil?
121
+ raise Psych::Exception, "not a mapping" unless conf.is_a?(Hash)
122
+
123
+ # symbols throughout, whatever the file used: `searcher:` is how yaml is
124
+ # usually written, but the readers look up :searcher, so a config spelled
125
+ # the usual way passed validation and then read back as nil
126
+ conf.transform_keys { |k| k.to_s.to_sym }
115
127
  rescue Errno::ENOENT
116
128
  unless @quiet_missing
117
129
  warn "Warning: ".yel +
@@ -123,6 +135,7 @@ module Booker
123
135
  # every psych failure means the same thing here - an unusable config to
124
136
  # fall back from rather than crash on. rescuing SyntaxError alone let
125
137
  # anchors through as an AliasesNotEnabled backtrace under psych 4
138
+ @unreadable = true
126
139
  warn "Warning: ".red +
127
140
  "YAML configuration file could not be read. Using defaults."
128
141
  false
@@ -130,6 +143,11 @@ module Booker
130
143
 
131
144
  # used for creating and updating the default configuration
132
145
  def write(k = nil, v = nil)
146
+ if @unreadable
147
+ raise Unreadable, "#{YAMLCONF} could not be read - fix or remove it, " \
148
+ "it was left as is"
149
+ end
150
+
133
151
  @config[k] = v unless k.nil? # update users yaml config file
134
152
  File.write(YAMLCONF, @config.to_yaml)
135
153
  end
@@ -290,7 +290,13 @@ module Booker
290
290
 
291
291
  input = gets
292
292
  raise "No input provided" if input.nil?
293
- selection = input.chomp.to_i
293
+
294
+ # to_i alone read any typo as 0 - all sources - and a negative number
295
+ # as an index from the end of the list
296
+ selection = Integer(input.strip, 10, exception: false)
297
+ unless selection&.between?(0, bms.length)
298
+ raise "Invalid selection: #{input.strip.inspect}"
299
+ end
294
300
 
295
301
  if selection == 0
296
302
  # User selected "ALL" - save array of all paths
@@ -325,6 +331,8 @@ module Booker
325
331
  def install_config
326
332
  config.write
327
333
  puts "Success: ".grn + "example config file written to ~/.booker"
334
+ rescue Config::Unreadable => e
335
+ pexit "Failure: ".red + e.message
328
336
  rescue
329
337
  pexit "Failure: ".red + "could not write example config file to ~/.booker"
330
338
  end
@@ -50,22 +50,30 @@ module Booker
50
50
  class Chrome < Base
51
51
  def parse
52
52
  begin
53
- local_bookmarks = JSON.parse(File.read(@file_path))
54
- @chrome_bookmarks = local_bookmarks["roots"]["bookmark_bar"]["children"]
53
+ roots = JSON.parse(File.read(@file_path))["roots"].select { |_, r| r.is_a?(Hash) }
55
54
  rescue
56
55
  warn "Warning: ".yel + "Bookmarks file not found or invalid."
57
56
  warn "Suggest: ".grn + "booker --install bookmarks"
58
57
  return
59
58
  end
60
59
 
61
- parse_recursive
60
+ # every root, not just the bar: "other bookmarks" is where chrome files
61
+ # anything saved without picking a folder, and "mobile bookmarks" is
62
+ # what sync brings over from a phone. the bar keeps the bare "|" it
63
+ # always had; the others are folders like any other, named by chrome
64
+ top = Folder.new([], "|")
65
+ roots.each do |key, root|
66
+ if key == "bookmark_bar"
67
+ parse_recursive(Folder.new(root["children"], "|"))
68
+ else
69
+ parse_folder(top, root)
70
+ end
71
+ end
62
72
  end
63
73
 
64
74
  private
65
75
 
66
- def parse_recursive(root = nil)
67
- root = Folder.new(@chrome_bookmarks, "|") if root.nil?
68
-
76
+ def parse_recursive(root)
69
77
  root.json.each { |x| parse_link(root.title, x) }
70
78
  root.json.each { |x| parse_folder(root, x) }
71
79
  end
@@ -143,15 +151,19 @@ module Booker
143
151
  # Recursive CTE query to build folder paths
144
152
  query = <<-SQL
145
153
  WITH RECURSIVE bookmark_tree(id, parent, title, url, path) AS (
146
- -- Start with bookmarks toolbar root
154
+ -- Start with the children of every user-facing root: the menu is
155
+ -- where firefox saves a bookmark by default, so the toolbar alone
156
+ -- missed most of them. toolbar paths stay unprefixed, as before
147
157
  SELECT b.id, b.parent, b.title, p.url,
148
- CAST(b.title as TEXT) as path
158
+ CAST(CASE r.guid
159
+ WHEN 'toolbar_____' THEN b.title
160
+ ELSE COALESCE(r.title, '') || '/' || b.title
161
+ END as TEXT) as path
149
162
  FROM moz_bookmarks b
163
+ INNER JOIN moz_bookmarks r ON b.parent = r.id
150
164
  LEFT JOIN moz_places p ON b.fk = p.id
151
- WHERE b.parent = (
152
- SELECT id FROM moz_bookmarks
153
- WHERE guid = 'toolbar_____'
154
- )
165
+ WHERE r.guid IN
166
+ ('toolbar_____', 'menu________', 'unfiled_____', 'mobile______')
155
167
 
156
168
  UNION ALL
157
169
 
@@ -217,7 +229,14 @@ module Booker
217
229
  # sniff the head rather than reading the whole file: on the binary path
218
230
  # the contents are thrown away and plutil reads it again from disk, and
219
231
  # a real Bookmarks.plist grows with the user's bookmarks
220
- head = File.binread(path, 512).to_s
232
+ head = begin
233
+ File.binread(path, 512).to_s
234
+ rescue Errno::EPERM, Errno::EACCES
235
+ # macOS privacy protection, when this process lacks Full Disk Access.
236
+ # the file is binary on a mac anyway, so leave it to plutil - which
237
+ # may have been granted access on its own (--install safari, [B])
238
+ ""
239
+ end
221
240
 
222
241
  # Already an XML plist (fixtures, non-macOS). Binary plists need plutil
223
242
  # (macOS-only); the JSON converter rejects <data>/<date> fields, so xml1.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Booker
4
- VERSION = "2.1.0"
4
+ VERSION = "2.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: booker
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Warner