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 +4 -4
- data/lib/booker/cli.rb +14 -8
- data/lib/booker/config.rb +20 -2
- data/lib/booker/installer.rb +9 -1
- data/lib/booker/parsers.rb +32 -13
- data/lib/booker/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a0f32b2a81f8c03acdc8c56ce6f9554a1229aced8a731d103fbfd53f1d03b5d3
|
|
4
|
+
data.tar.gz: 2d0386dcc31468311faaac5e5ec749d19623ad40f9f6b083a86b77cd9632be01
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
BOOKMARK_ID = /\A
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
|
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
|
data/lib/booker/installer.rb
CHANGED
|
@@ -290,7 +290,13 @@ module Booker
|
|
|
290
290
|
|
|
291
291
|
input = gets
|
|
292
292
|
raise "No input provided" if input.nil?
|
|
293
|
-
|
|
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
|
data/lib/booker/parsers.rb
CHANGED
|
@@ -50,22 +50,30 @@ module Booker
|
|
|
50
50
|
class Chrome < Base
|
|
51
51
|
def parse
|
|
52
52
|
begin
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
152
|
-
|
|
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 =
|
|
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.
|
data/lib/booker/version.rb
CHANGED