booker 1.2.1 → 2.0.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.
data/lib/booker.rb CHANGED
@@ -1,423 +1,14 @@
1
- # parse booker's command line args
2
- require "yaml"
3
- require "find"
4
- require "json"
5
- require "shellwords"
6
- require "fileutils"
7
- require_relative "bookmarks"
8
- require_relative "config"
9
- require_relative "consts"
10
-
11
- # get booker opening command
12
- class Booker
13
- @version = "1.2.1"
14
- @@version = @version
15
-
16
- class << self
17
- attr_reader :version
18
- end
19
-
20
- include Browser
21
-
22
- def initialize(args)
23
- parse args
24
- end
25
-
26
- def parse(args)
27
- # no args given, show interactive bookmark selector
28
- show_bookmarks if args.none?
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
43
- end
44
-
45
- # open all bookmarks first
46
- unless bookmark_ids.empty?
47
- open_bookmark bookmark_ids
48
- end
49
-
50
- # then handle remaining args
51
- unless other_args.empty?
52
- if other_args.length == 1 && domain.match(other_args.first)
53
- # single website URL
54
- puts "opening website: ".grn + other_args.first
55
- openweb(prep(other_args.first))
56
- else
57
- # search for the rest
58
- open_search(other_args.join(" ").strip)
59
- end
60
- end
61
- end
62
-
63
- def pexit(msg, sig)
64
- puts msg
65
- exit sig
66
- end
67
-
68
- def helper
69
- pexit HELP_BANNER, 0
70
- end
71
-
72
- def version
73
- pexit @@version, 0
74
- end
75
-
76
- def openweb(url)
77
- # Pass URL directly to browser without invoking shell
78
- # This avoids issues with special characters like parentheses
79
- browser_cmd = browse.strip
80
-
81
- # Redirect stdout/stderr to suppress GTK warnings
82
- success = system(browser_cmd, url, out: "/dev/null", err: "/dev/null")
83
-
84
- unless success
85
- puts "Warning: ".yel + "Failed to open URL (exit code: #{$?.exitstatus})"
86
- end
87
- end
88
-
89
- # an array of ints, as bookmark ids
90
- def open_bookmark(bm)
91
- id = bm.shift
92
- url = Bookmarks.new.bookmark_url(id)
93
- pexit "Failure:".red + " bookmark #{id} not found", 1 if url.nil?
94
- puts "opening bookmark ".grn + url
95
- openweb(url) # No wrap() needed - system() handles it
96
- open_bookmark bm unless bm.empty?
97
- end
98
-
99
- def open_search(term)
100
- puts "searching ".grn + term
101
- search = BConfig.new.searcher
102
- term = term.tr(" ", "+")
103
- openweb(search + term) # No shell escape needed - it's a URL
104
- end
105
-
106
- def show_bookmarks
107
- puts "Bookmarks:".grn + " (usage: booker <id> or booker <search>)"
108
- puts ""
109
-
110
- bm = Bookmarks.new("") # Get all bookmarks
111
- allurls = bm.instance_variable_get(:@allurls)
112
-
113
- if allurls.empty?
114
- puts "No bookmarks found.".red
115
- puts "Run: ".yel + "booker --install bookmarks".cyan
116
- exit 0
117
- end
118
-
119
- # Calculate responsive column widths
120
- term_width = `tput cols`.to_i
121
- term_width = 100 if term_width == 0 # fallback
122
-
123
- id_width = 10
124
- remaining = term_width - id_width - 3 # 3 spaces between columns
125
-
126
- folder_width = [remaining * 0.20, 15].max.to_i
127
- title_width = [remaining * 0.30, 20].max.to_i
128
- url_width = [remaining * 0.50, 30].max.to_i
129
-
130
- # Display bookmarks in a readable format
131
- allurls.each do |bookmark|
132
- id_str = bookmark.id.to_s.window(id_width).grn
133
-
134
- # Clean up folder display
135
- folder = bookmark.folder.gsub(/^\|/, "") # Remove leading |
136
- folder = (folder == "/") ? "[root]" : folder.chomp("/") # Show [root] for top-level
137
- folder_str = folder.window(folder_width).blu
138
-
139
- title_str = bookmark.title.window(title_width).yel
140
- url_str = bookmark.url.window(url_width)
141
-
142
- puts "#{id_str} #{folder_str} #{title_str} #{url_str}"
143
- end
144
-
145
- puts ""
146
- puts "Found #{allurls.length} bookmarks".grn
147
- puts ""
148
- puts "Examples:".yel
149
- puts " #{"booker #{allurls.first.id}".ljust(20).cyan} # Open first bookmark"
150
- puts " #{"booker github".ljust(20).cyan} # Search bookmarks for 'github'"
151
- puts " #{"booker --help".ljust(20).cyan} # Show help"
152
-
153
- exit 0
154
- end
155
-
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
- def install(args)
207
- target = args.shift
208
- exit 0 if target.nil?
209
-
210
- if /comp/i.match?(target) # completion installation
211
- install_completion
212
- elsif /book/i.match?(target) # bookmarks installation
213
- install_bookmarks
214
- elsif /conf/i.match?(target) # default config file generation
215
- install_config
216
- else # unknown argument passed into install
217
- pexit "Failure: ".red + "unknown installation option (#{target})", 1
218
- end
219
-
220
- install(args) # recurse til done
221
- end
222
-
223
- def install_completion
224
- # check if zsh is even installed for this user
225
- begin
226
- fpath = `zsh -c 'echo $fpath'`.split(" ")
227
- rescue
228
- pexit "Failure: ".red + "zsh is probably not installed, could not find $fpath", 1
229
- end
230
-
231
- # Try user-writable directories first, then system directories
232
- user_home = ENV["HOME"]
233
- writable_dirs = fpath.select do |fp|
234
- fp.start_with?(user_home) && File.directory?(fp) && File.writable?(fp)
235
- end
236
-
237
- # If no user-writable directories, try to create one
238
- if writable_dirs.empty?
239
- user_completion_dir = File.join(user_home, ".zsh", "completion")
240
- begin
241
- FileUtils.mkdir_p(user_completion_dir)
242
- writable_dirs << user_completion_dir
243
- puts "Created user completion directory: #{user_completion_dir}".yel
244
-
245
- # Auto-configure .zshrc if it exists
246
- zshrc = File.join(user_home, ".zshrc")
247
- if File.exist?(zshrc)
248
- zshrc_content = File.read(zshrc)
249
- if zshrc_content.include?(".zsh/completion")
250
- puts "~/.zshrc already configured".grn
251
- else
252
- File.open(zshrc, "a") do |f|
253
- f.puts "\n# Booker completion"
254
- f.puts "fpath=(~/.zsh/completion $fpath)"
255
- f.puts "autoload -Uz compinit && compinit"
256
- end
257
- puts "Added completion to ~/.zshrc".grn
258
- puts "Run: ".yel + "source ~/.zshrc".cyan + " to activate"
259
- end
260
- else
261
- puts "Add this to your ~/.zshrc: ".yel + "fpath=(~/.zsh/completion $fpath)".cyan
262
- end
263
- rescue => e
264
- # Couldn't create user dir, try system dirs as fallback
265
- end
266
- end
267
-
268
- # Try writable directories first, then all directories as fallback
269
- dirs_to_try = writable_dirs + fpath.reject { |fp| writable_dirs.include?(fp) }
270
-
271
- success = false
272
- dirs_to_try.each do |fp|
273
- next unless File.directory?(fp)
274
-
275
- begin
276
- completion_file = File.join(fp, "_booker")
277
- File.write(completion_file, COMPLETION)
278
- system "zsh -c 'autoload -U _booker'"
279
- puts "Success: ".grn + "installed zsh autocompletion in #{fp}"
280
- success = true
281
- break
282
- rescue => e
283
- # Try next directory silently
284
- end
285
- end
286
-
287
- unless success
288
- puts "Warning: ".yel + "Could not install ZSH completion to any directory in $fpath"
289
- puts "Try manually: ".grn + "mkdir -p ~/.zsh/completion && booker --install completion"
290
- end
291
- end
292
-
293
- def install_bookmarks
294
- # locate bookmarks file, show user, write to config?
295
- puts "searching for chrome and firefox bookmarks..."
296
- begin
297
- bms = [] # look for bookmarks with type info
298
-
299
- # Search for Chrome bookmarks
300
- ["Library/Application Support/Google/Chrome",
301
- "AppData/Local/Google/Chrome/User Data/Default",
302
- ".config/chromium/Default",
303
- ".config/google-chrome/Default",
304
- "snap/chromium/common/chromium",
305
- "snap/chromium/current/.config/chromium"].each do |f|
306
- home = File.join(ENV["HOME"], f)
307
- next if !FileTest.directory?(home)
308
- Find.find(home) do |file|
309
- if /chrom.*bookmarks/i.match?(file)
310
- bms << {path: file, type: :chrome}
311
- end
312
- end
313
- end
314
-
315
- # Search for Firefox bookmarks
316
- firefox_paths = [
317
- ".mozilla/firefox", # Linux
318
- "snap/firefox/common/.mozilla/firefox", # Linux (snap)
319
- "Library/Application Support/Firefox", # macOS
320
- "AppData/Roaming/Mozilla/Firefox" # Windows
321
- ]
322
-
323
- firefox_paths.each do |f|
324
- firefox_base = File.join(ENV["HOME"], f)
325
- next if !FileTest.directory?(firefox_base)
326
-
327
- profiles_ini = File.join(firefox_base, "profiles.ini")
328
- next if !File.exist?(profiles_ini)
329
-
330
- # Parse profiles.ini to find Firefox profiles
331
- parse_firefox_profiles(profiles_ini, firefox_base).each do |db_path|
332
- bms << {path: db_path, type: :firefox}
333
- end
334
- end
335
-
336
- if bms.empty? # no bookmarks found
337
- puts "Failure: ".red + "bookmarks file could not be found."
338
- raise
339
- elsif bms.length == 1
340
- # Auto-select if only one source found
341
- selected = bms.first[:path]
342
- type_label = (bms.first[:type] == :chrome) ? "[Chrome]" : "[Firefox]"
343
- puts "Found bookmark source: #{type_label} #{selected}".yel
344
- puts "Selected: ".yel + selected
345
- BConfig.new.write(:bookmarks, selected)
346
- puts "Success: ".grn + "config file updated with your bookmarks"
347
- else # have user select a file
348
- puts "select bookmarks source: "
349
-
350
- # Offer "ALL" as first option if multiple sources found
351
- puts "0".grn + " - " + "[ALL SOURCES]".cyan + " (search across all browsers)"
352
- offset = 1
353
-
354
- 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]
357
- end
358
-
359
- input = gets
360
- raise "No input provided" if input.nil?
361
- selection = input.chomp.to_i
362
-
363
- if selection == 0
364
- # User selected "ALL" - save array of all paths
365
- all_paths = bms.map { |bm| bm[:path] }
366
- puts "Selected: ".yel + "All sources (#{bms.length} bookmark files)"
367
- BConfig.new.write(:bookmarks, all_paths)
368
- puts "Success: ".grn + "config file updated to search all bookmark sources"
369
- else
370
- # User selected single source
371
- actual_index = selection - 1
372
- selected = bms[actual_index][:path]
373
- puts "Selected: ".yel + selected
374
- BConfig.new.write(:bookmarks, selected)
375
- puts "Success: ".grn + "config file updated with your bookmarks"
376
- end
377
- end
378
- rescue => e
379
- puts e.message
380
- pexit "Failure: ".red + "could not add bookmarks to config file ~/.booker", 1
381
- end
382
- end
383
-
384
- def parse_firefox_profiles(ini_path, firefox_base)
385
- profiles = []
386
- current_profile = {}
387
-
388
- File.readlines(ini_path).each do |line|
389
- line = line.strip
390
-
391
- # New profile section
392
- if line.start_with?("[Profile")
393
- # Save previous profile if it had a path
394
- if current_profile[:path]
395
- db_path = File.join(firefox_base, current_profile[:path], "places.sqlite")
396
- profiles << db_path if File.exist?(db_path)
397
- end
398
- current_profile = {}
399
-
400
- # Parse key=value pairs
401
- elsif line.include?("=")
402
- key, value = line.split("=", 2).map(&:strip)
403
- current_profile[:path] = value if key == "Path"
404
- current_profile[:name] = value if key == "Name"
405
- end
406
- end
407
-
408
- # Don't forget the last profile
409
- if current_profile[:path]
410
- db_path = File.join(firefox_base, current_profile[:path], "places.sqlite")
411
- profiles << db_path if File.exist?(db_path)
412
- end
413
-
414
- profiles
415
- end
416
-
417
- def install_config
418
- BConfig.new.write
419
- puts "Success: ".grn + "example config file written to ~/.booker"
420
- rescue
421
- pexit "Failure: ".red + "could not write example config file to ~/.booker", 1
422
- end
423
- end
1
+ # frozen_string_literal: true
2
+
3
+ # booker - search, browse and open browser bookmarks from the command line
4
+ #
5
+ # Load order matters in one place: Bookmarks::BROWSERS names the parser classes
6
+ # while its class body runs, so parsers has to come first.
7
+
8
+ require_relative "booker/version"
9
+ require_relative "booker/output"
10
+ require_relative "booker/config"
11
+ require_relative "booker/parsers"
12
+ require_relative "booker/bookmarks"
13
+ require_relative "booker/picker"
14
+ require_relative "booker/cli"
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: 2.0.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
@@ -16,47 +15,55 @@ dependencies:
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: '2.7'
18
+ version: '2.21'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
- version: '2.7'
25
+ version: '2.21'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: sqlite3
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - "~>"
32
31
  - !ruby/object:Gem::Version
33
- version: '1.7'
32
+ version: '2.9'
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: 2.9.5
34
36
  type: :runtime
35
37
  prerelease: false
36
38
  version_requirements: !ruby/object:Gem::Requirement
37
39
  requirements:
38
40
  - - "~>"
39
41
  - !ruby/object:Gem::Version
40
- version: '1.7'
42
+ version: '2.9'
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: 2.9.5
41
46
  - !ruby/object:Gem::Dependency
42
- name: rspec
47
+ name: rexml
43
48
  requirement: !ruby/object:Gem::Requirement
44
49
  requirements:
45
50
  - - "~>"
46
51
  - !ruby/object:Gem::Version
47
- version: '3.13'
48
- type: :development
52
+ version: '3.4'
53
+ type: :runtime
49
54
  prerelease: false
50
55
  version_requirements: !ruby/object:Gem::Requirement
51
56
  requirements:
52
57
  - - "~>"
53
58
  - !ruby/object:Gem::Version
54
- version: '3.13'
59
+ version: '3.4'
55
60
  description: |2
56
61
  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.
62
+ Chromium, Firefox, and Safari. With fzf installed, booker and booker
63
+ <search> open a fuzzy picker over the matches - nothing to install and the
64
+ same in every shell. Zsh, bash, and fish users can also add tab completion
65
+ through bookmark matches. Can open websites directly or search with your
66
+ preferred search engine.
60
67
  email: jeremywrnr@gmail.com
61
68
  executables:
62
69
  - booker
@@ -64,15 +71,26 @@ extensions: []
64
71
  extra_rdoc_files: []
65
72
  files:
66
73
  - bin/booker
74
+ - completions/_booker
75
+ - completions/booker.bash
76
+ - completions/booker.fish
67
77
  - lib/booker.rb
68
- - lib/bookmarks.rb
69
- - lib/config.rb
70
- - lib/consts.rb
71
- homepage: http://github.com/jeremywrnr/booker
78
+ - lib/booker/bookmarks.rb
79
+ - lib/booker/cli.rb
80
+ - lib/booker/config.rb
81
+ - lib/booker/installer.rb
82
+ - lib/booker/output.rb
83
+ - lib/booker/parsers.rb
84
+ - lib/booker/picker.rb
85
+ - lib/booker/version.rb
86
+ homepage: https://github.com/jeremywrnr/booker
72
87
  licenses:
73
88
  - MIT
74
- metadata: {}
75
- post_install_message: 'To add zsh completion run: booker --install'
89
+ metadata:
90
+ source_code_uri: https://github.com/jeremywrnr/booker
91
+ bug_tracker_uri: https://github.com/jeremywrnr/booker/issues
92
+ changelog_uri: https://github.com/jeremywrnr/booker/blob/main/CHANGELOG.md
93
+ rubygems_mfa_required: 'true'
76
94
  rdoc_options: []
77
95
  require_paths:
78
96
  - lib
@@ -80,15 +98,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
80
98
  requirements:
81
99
  - - ">="
82
100
  - !ruby/object:Gem::Version
83
- version: '0'
101
+ version: '3.2'
84
102
  required_rubygems_version: !ruby/object:Gem::Requirement
85
103
  requirements:
86
104
  - - ">="
87
105
  - !ruby/object:Gem::Version
88
106
  version: '0'
89
107
  requirements: []
90
- rubygems_version: 3.4.20
91
- signing_key:
108
+ rubygems_version: 4.0.18
92
109
  specification_version: 4
93
- summary: CLI bookmark manager for Chrome and Firefox
110
+ summary: CLI bookmark manager for Chrome, Firefox, and Safari
94
111
  test_files: []