booker 1.1.0 → 1.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.
Files changed (6) hide show
  1. checksums.yaml +4 -4
  2. data/lib/booker.rb +288 -74
  3. data/lib/bookmarks.rb +244 -65
  4. data/lib/config.rb +99 -26
  5. data/lib/consts.rb +103 -92
  6. metadata +23 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93b6a3c6bf829119ae61f5f00420929df7826d9755fe455d2e96512a0750ab49
4
- data.tar.gz: 0a66a91c861b95e091441dfb5006033e8181a67aae1bed35e7e909efe66f5d6f
3
+ metadata.gz: fc8148125669f4128e710ef78210347f90caf80f5753ffbacf48f0b78af6f851
4
+ data.tar.gz: 1deea2d2a8f289eaefb670c77cfe5a2a8696c2dcaafcbb96b990eda0e8269384
5
5
  SHA512:
6
- metadata.gz: 94db993a684f9b7e5cc06f308975054c3af63224a9b26082253ec3e0c227522aeb9ad948ab0c800b901ce8d32629fa98e8596daf635cdfc0935bf8329b1f04ba
7
- data.tar.gz: cab7a43c4b8a251761b91194657e3526e9a2f806041f72a55c52ea545557814e4117b13cb52bc2ab0a1a059e68821d989b470c9a7e1e4954c54bbd2282cd9940
6
+ metadata.gz: b769f210cd3153dafe3240119044e742765bc260afcbf963daec319c135975799376446198fb9cecc68a3ad277ffb43fe33bc3529ad8af62a79cf6403ecfc577
7
+ data.tar.gz: b2c40993f4f25aac98a5a2549eba4f933e31d504c1f6f85245b075eda2ba2072f26154a97fded5dc2e429738f127fc90617d7d6f52b0ba35b1e17c5a333f6443
data/lib/booker.rb CHANGED
@@ -1,16 +1,16 @@
1
1
  # parse booker's command line args
2
- require 'yaml'
3
- require 'find'
4
- require 'json'
5
- require 'shellwords'
6
- require_relative 'bookmarks'
7
- require_relative 'config'
8
- require_relative 'consts'
9
-
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
10
 
11
11
  # get booker opening command
12
12
  class Booker
13
- @version = "1.1.0"
13
+ @version = "1.2.0"
14
14
  @@version = @version
15
15
 
16
16
  class << self
@@ -18,27 +18,45 @@ class Booker
18
18
  end
19
19
 
20
20
  include Browser
21
+
21
22
  def initialize(args)
22
23
  parse args
23
24
  end
24
25
 
25
26
  def parse(args)
26
- # no args given, show help
27
- helper if args.none?
27
+ # no args given, show interactive bookmark selector
28
+ show_bookmarks if args.none?
28
29
 
29
30
  # if arg starts with hyphen, parse option
30
- parse_opt args if /^-.*/.match(args.first)
31
+ parse_opt args if /^-.*/.match?(args.first)
31
32
 
32
- # interpret command
33
- browsearg = args.first
33
+ # separate bookmark IDs from other args
34
+ bookmark_ids = []
35
+ other_args = []
34
36
 
35
- if browsearg.match(/^[0-9]/) # bookmark
36
- open_bookmark args
37
- elsif domain.match(browsearg) # website
38
- puts 'opening website: ' + browsearg
39
- openweb(prep(browsearg))
40
- else
41
- open_search(args.join(' ').strip)
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: " + 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
42
60
  end
43
61
  end
44
62
 
@@ -56,7 +74,16 @@ class Booker
56
74
  end
57
75
 
58
76
  def openweb(url)
59
- system(browse + wrap(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
60
87
  end
61
88
 
62
89
  # an array of ints, as bookmark ids
@@ -64,64 +91,114 @@ class Booker
64
91
  id = bm.shift
65
92
  url = Bookmarks.new.bookmark_url(id)
66
93
  pexit "Failure:".red + " bookmark #{id} not found", 1 if url.nil?
67
- puts 'opening bookmark ' + url + '...'
68
- openweb(wrap(url))
94
+ puts "opening bookmark " + url + "..."
95
+ openweb(url) # No wrap() needed - system() handles it
69
96
  open_bookmark bm unless bm.empty?
70
97
  end
71
98
 
72
99
  def open_search(term)
73
- puts 'searching ' + term + '...'
100
+ puts "searching " + term + "..."
74
101
  search = BConfig.new.searcher
75
- term = term.gsub(' ', '+')
76
- openweb(search + Shellwords.escape(term))
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
77
154
  end
78
155
 
79
156
  # parse and execute any command line options
80
157
  def parse_opt(args)
81
- valid_opts = %w{--version -v --install -i --help -h
82
- --complete -c --bookmark -b --search -s}
158
+ valid_opts = %w[--version -v --install -i --help -h
159
+ --complete -c --bookmark -b --search -s]
83
160
 
84
161
  nextarg = args.shift
85
- errormsg = 'Error: '.red + "unrecognized option #{nextarg}"
86
- pexit errormsg, 1 if ! (valid_opts.include? nextarg)
162
+ errormsg = "Error: ".red + "unrecognized option #{nextarg}"
163
+ pexit errormsg, 1 if !(valid_opts.include? nextarg)
87
164
 
88
165
  # forced bookmarking
89
- if nextarg == '--bookmark' || nextarg == '-b'
166
+ if nextarg == "--bookmark" || nextarg == "-b"
90
167
  if args.first.nil?
91
- pexit 'Error: '.red + 'booker --bookmark expects bookmark id', 1
168
+ pexit "Error: ".red + "booker --bookmark expects bookmark id", 1
92
169
  else
93
170
  open_bookmark args
94
171
  end
95
172
  end
96
173
 
97
174
  # autocompletion
98
- if nextarg == '--complete' || nextarg == '-c'
99
- allargs = args.join(' ')
175
+ if nextarg == "--complete" || nextarg == "-c"
176
+ allargs = args.join(" ")
100
177
  bm = Bookmarks.new(allargs)
101
178
  bm.autocomplete
102
179
  end
103
180
 
104
181
  # installation
105
- if nextarg == '--install' || nextarg == '-i'
182
+ if nextarg == "--install" || nextarg == "-i"
106
183
  if !args.empty?
107
184
  install(args)
108
185
  else # do everything
109
- install(%w{completion config bookmarks})
186
+ install(%w[completion config bookmarks])
110
187
  end
111
188
  end
112
189
 
113
190
  # forced searching
114
- if nextarg == '--search' || nextarg == '-s'
115
- pexit '--search requires an argument', 1 if args.empty?
116
- allargs = args.join(' ')
191
+ if nextarg == "--search" || nextarg == "-s"
192
+ pexit "--search requires an argument", 1 if args.empty?
193
+ allargs = args.join(" ")
117
194
  open_search allargs
118
195
  end
119
196
 
120
197
  # print version information
121
- version if nextarg == '--version' || nextarg == '-v'
198
+ version if nextarg == "--version" || nextarg == "-v"
122
199
 
123
200
  # needs some help
124
- helper if nextarg == '--help' || nextarg == '-h'
201
+ helper if nextarg == "--help" || nextarg == "-h"
125
202
 
126
203
  exit 0 # dont parse_arg
127
204
  end # parse_opt
@@ -130,11 +207,11 @@ class Booker
130
207
  target = args.shift
131
208
  exit 0 if target.nil?
132
209
 
133
- if /comp/i.match(target) # completion installation
210
+ if /comp/i.match?(target) # completion installation
134
211
  install_completion
135
- elsif /book/i.match(target) # bookmarks installation
212
+ elsif /book/i.match?(target) # bookmarks installation
136
213
  install_bookmarks
137
- elsif /conf/i.match(target) # default config file generation
214
+ elsif /conf/i.match?(target) # default config file generation
138
215
  install_config
139
216
  else # unknown argument passed into install
140
217
  pexit "Failure: ".red + "unknown installation option (#{target})", 1
@@ -146,64 +223,201 @@ class Booker
146
223
  def install_completion
147
224
  # check if zsh is even installed for this user
148
225
  begin
149
- fpath = `zsh -c 'echo $fpath'`.split(' ')
226
+ fpath = `zsh -c 'echo $fpath'`.split(" ")
150
227
  rescue
151
228
  pexit "Failure: ".red + "zsh is probably not installed, could not find $fpath", 1
152
229
  end
153
230
 
154
- # determine where to install completion function
155
- fpath.each do |fp|
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")
156
240
  begin
157
- File.open(fp + "/_booker", 'w') {|f| f.write(COMPLETION) }
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)
158
278
  system "zsh -c 'autoload -U _booker'"
159
279
  puts "Success: ".grn + "installed zsh autocompletion in #{fp}"
160
- break # if this works, don't try anymore
161
- rescue
162
- puts "Failure: ".red + "could not write ZSH completion _booker script to $fpath (#{fp})"
280
+ success = true
281
+ break
282
+ rescue => e
283
+ # Try next directory silently
163
284
  end
164
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
165
291
  end
166
292
 
167
293
  def install_bookmarks
168
294
  # locate bookmarks file, show user, write to config?
169
- puts 'searching for chrome bookmarks...'
295
+ puts "searching for chrome and firefox bookmarks..."
170
296
  begin
171
- bms = [] # look for bookmarks
172
- [ '/Library/Application Support/Google/Chrome',
173
- '/AppData/Local/Google/Chrome/User Data/Default',
174
- '/.config/chromium/Default/',
175
- '/.config/google-chrome/Default/',
176
- ].each do |f|
177
- home = File.join(ENV['HOME'], f)
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)
178
307
  next if !FileTest.directory?(home)
179
308
  Find.find(home) do |file|
180
- bms << file if /chrom.*bookmarks/i.match 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}
181
333
  end
182
334
  end
183
335
 
184
336
  if bms.empty? # no bookmarks found
185
- puts "Failure: ".red + 'bookmarks file could not be found.'
337
+ puts "Failure: ".red + "bookmarks file could not be found."
186
338
  raise
187
- else # have user select a file
188
- puts 'select bookmarks file: '
189
- bms.each_with_index {|bm, i| puts i.to_s.grn + " - " + bm }
190
- selected = bms[gets.chomp.to_i]
191
- puts 'Selected: '.yel + selected
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
192
345
  BConfig.new.write(:bookmarks, selected)
193
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
194
377
  end
195
- rescue StandardError => e
378
+ rescue => e
196
379
  puts e.message
197
380
  pexit "Failure: ".red + "could not add bookmarks to config file ~/.booker", 1
198
381
  end
199
382
  end
200
383
 
201
- def install_config
202
- begin
203
- BConfig.new.write
204
- puts "Success: ".grn + "example config file written to ~/.booker"
205
- rescue
206
- pexit "Failure: ".red + "could not write example config file to ~/.booker", 1
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)
207
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
208
422
  end
209
423
  end