Class: Booker::Installer
- Inherits:
-
Object
- Object
- Booker::Installer
- Includes:
- Output
- Defined in:
- lib/booker/installer.rb
Constant Summary collapse
- SHELLS =
shells we ship completion for, and where those scripts live
%w[zsh bash fish].freeze
- ZSHRC_LINES =
what ~/.zshrc needs for a script in ~/.zsh/completion to be found
[ "fpath=(~/.zsh/completion $fpath)", "autoload -Uz compinit && compinit" ].freeze
- COMPLETIONS_DIR =
File.("../../completions", __dir__).freeze
- BASH_COMPLETION_MARKERS =
if any of these exist, bash-completion is installed and will autoload our script out of the XDG user directory
[ "/usr/share/bash-completion/bash_completion", "/etc/bash_completion", "/usr/local/etc/profile.d/bash_completion.sh", "/opt/homebrew/etc/profile.d/bash_completion.sh" ].freeze
- ALL =
'all' expands to the full install list (including opt-in safari)
%w[completion config bookmarks safari].freeze
Instance Method Summary collapse
-
#append_once(rcfile, lines, marker: lines, label: rcfile) ⇒ Object
append to an rc file once - install is expected to be re-runnable without stacking up duplicates.
- #bash_completion_present? ⇒ Boolean
-
#bookmark_type_label(type, color: false) ⇒ Object
the label and its color come off the browser table, so a fourth browser is a row there rather than two more branches here.
-
#clear_zsh_compdump ⇒ Object
compinit caches what it found in $fpath, so a shell started against an old dump keeps describing the completion booker just replaced.
-
#completion_script(name) ⇒ Object
read one of the shipped completion scripts (completions/ sits next to lib/, and is listed in the gemspec so it ships with the gem).
- #home ⇒ Object
- #install(args) ⇒ Object
-
#install_bookmarks ⇒ Object
locate bookmarks files, show the user, write the choice to the config.
-
#install_completion ⇒ Object
install completion for every supported shell present on this machine, so a zsh user who also drops into bash gets both without running install twice.
-
#install_completion_bash ⇒ Object
bash-completion (when installed) autoloads from the XDG user directory.
-
#install_completion_fish ⇒ Object
fish autoloads anything in its completions directory, so there is no rc file to edit here.
-
#install_completion_for(shell) ⇒ Object
every shell in SHELLS has an install_completion_
, so adding a fourth means adding the script, the method, and the SHELLS entry - nothing here. - #install_completion_zsh ⇒ Object
- #install_config ⇒ Object
- #install_one(target) ⇒ Object
-
#install_safari ⇒ Object
Opt-in Safari setup: walks through granting Full Disk Access so booker can read ~/Library/Safari/Bookmarks.plist.
- #safari_readable?(plist) ⇒ Boolean
- #save_bookmarks(selected, message) ⇒ Object
-
#shell_present?(shell) ⇒ Boolean
Picker.which, not
sh -c "command -v": a fork per shell to answer a PATH question booker already knows how to answer in process. - #xdg_config_home ⇒ Object
- #xdg_data_home ⇒ Object
Methods included from Output
Instance Method Details
#append_once(rcfile, lines, marker: lines, label: rcfile) ⇒ Object
append to an rc file once - install is expected to be re-runnable without
stacking up duplicates. marker is what counts as "already there" when
that is narrower than what gets written; label is the name shown
231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/booker/installer.rb', line 231 def append_once(rcfile, lines, marker: lines, label: rcfile) if File.exist?(rcfile) && File.read(rcfile).include?(marker) puts "#{label} already configured".grn return end File.open(rcfile, "a") do |f| f.puts "\n# Booker completion" f.puts lines end puts "Added completion to #{label}".grn yield if block_given? end |
#bash_completion_present? ⇒ Boolean
224 225 226 |
# File 'lib/booker/installer.rb', line 224 def bash_completion_present? BASH_COMPLETION_MARKERS.any? { |marker| File.exist?(marker) } end |
#bookmark_type_label(type, color: false) ⇒ Object
the label and its color come off the browser table, so a fourth browser is a row there rather than two more branches here
311 312 313 314 315 316 |
# File 'lib/booker/installer.rb', line 311 def bookmark_type_label(type, color: false) browser = Bookmarks::BROWSERS[type] return "[?]" if browser.nil? color ? Colors.paint(browser[:label], browser[:color]) : browser[:label] end |
#clear_zsh_compdump ⇒ Object
compinit caches what it found in $fpath, so a shell started against an old dump keeps describing the completion booker just replaced. dropping the cache - zsh rebuilds it on the next start - is how a new script reaches the next shell. nothing helps the shell you ran this from, which autoloaded _booker for the life of the session, hence the unfunction advice
158 159 160 161 162 163 164 165 166 167 168 169 170 |
# File 'lib/booker/installer.rb', line 158 def clear_zsh_compdump base = ENV["ZDOTDIR"] || home dumps = Dir.glob(File.join(base, ".zcompdump*")) return if dumps.empty? dumps.each do |dump| File.delete(dump) rescue # a dump we cannot remove is not worth failing an install over end puts "Refreshed: ".grn + "zsh completion cache (rebuilds on next shell)" end |
#completion_script(name) ⇒ Object
read one of the shipped completion scripts (completions/ sits next to lib/, and is listed in the gemspec so it ships with the gem)
214 215 216 217 218 |
# File 'lib/booker/installer.rb', line 214 def completion_script(name) File.read(File.join(COMPLETIONS_DIR, name)) rescue Errno::ENOENT pexit "Failure: ".red + "completion script #{name} missing from #{COMPLETIONS_DIR}" end |
#home ⇒ Object
245 246 247 |
# File 'lib/booker/installer.rb', line 245 def home ENV["HOME"] || "/usr/local" end |
#install(args) ⇒ Object
39 40 41 42 |
# File 'lib/booker/installer.rb', line 39 def install(args) args.flat_map { |target| /^all$/i.match?(target) ? ALL : target } .each { |target| install_one(target) } end |
#install_bookmarks ⇒ Object
locate bookmarks files, show the user, write the choice to the config. the search is Sources.discover, the same code auto-detection falls back on, so this offers exactly what booker would find on its own
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/booker/installer.rb', line 260 def install_bookmarks puts "searching for browser bookmarks..." begin bms = Sources.discover if bms.empty? # no bookmarks found puts "Failure: ".red + "bookmarks file could not be found." raise elsif bms.length == 1 # Auto-select if only one source found selected = bms.first puts "Found bookmark source: #{bookmark_type_label(Bookmarks.source_for(selected))} #{selected}".yel save_bookmarks(selected, "config file updated with your bookmarks") else # have user select a file puts "select bookmarks source: " # Offer "ALL" as first option if multiple sources found puts "0".grn + " - " + "[ALL SOURCES]".cyan + " (search across all browsers)" bms.each_with_index do |path, i| label = bookmark_type_label(Bookmarks.source_for(path), color: true) puts (i + 1).to_s.grn + " - " + label + " " + path end input = gets raise "No input provided" if input.nil? selection = input.chomp.to_i if selection == 0 # User selected "ALL" - save array of all paths puts "Selected: ".yel + "All sources (#{bms.length} bookmark files)" Config.default.write(:bookmarks, bms) puts "Success: ".grn + "config file updated to search all bookmark sources" else save_bookmarks(bms[selection - 1], "config file updated with your bookmarks") end end rescue => e puts e. pexit "Failure: ".red + "could not add bookmarks to config file ~/.booker" end end |
#install_completion ⇒ Object
install completion for every supported shell present on this machine, so a zsh user who also drops into bash gets both without running install twice
62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/booker/installer.rb', line 62 def install_completion found, missing = SHELLS.partition { |shell| shell_present?(shell) } if found.empty? puts "Warning: ".yel + "no supported shell found (#{SHELLS.join(", ")})" return end found.each { |shell| install_completion_for(shell) } puts "Skip: ".yel + "#{missing.join(", ")} not installed" unless missing.empty? end |
#install_completion_bash ⇒ Object
bash-completion (when installed) autoloads from the XDG user directory. Without it there is nothing doing the loading, so fall back to a plain directory plus a source line in ~/.bashrc.
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/booker/installer.rb', line 175 def install_completion_bash autoloaded = bash_completion_present? dir = if autoloaded File.join(xdg_data_home, "bash-completion", "completions") else File.join(home, ".bash_completion.d") end FileUtils.mkdir_p(dir) File.write(File.join(dir, "booker"), completion_script("booker.bash")) puts "Success: ".grn + "installed bash completion in #{dir}" # with bash-completion installed there is nothing left to wire up unless autoloaded append_once( File.join(home, ".bashrc"), "[ -f ~/.bash_completion.d/booker ] && . ~/.bash_completion.d/booker" ) end puts "Run: ".yel + "source ~/.bashrc".cyan + " to activate" rescue => e puts "Warning: ".yel + "could not install bash completion (#{e.})" end |
#install_completion_fish ⇒ Object
fish autoloads anything in its completions directory, so there is no rc file to edit here
202 203 204 205 206 207 208 209 210 |
# File 'lib/booker/installer.rb', line 202 def install_completion_fish dir = File.join(xdg_config_home, "fish", "completions") FileUtils.mkdir_p(dir) File.write(File.join(dir, "booker.fish"), completion_script("booker.fish")) puts "Success: ".grn + "installed fish completion in #{dir}" puts "Run: ".yel + "exec fish".cyan + " to activate" rescue => e puts "Warning: ".yel + "could not install fish completion (#{e.})" end |
#install_completion_for(shell) ⇒ Object
every shell in SHELLS has an install_completion_
77 78 79 |
# File 'lib/booker/installer.rb', line 77 def install_completion_for(shell) send("install_completion_#{shell}") end |
#install_completion_zsh ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/booker/installer.rb', line 81 def install_completion_zsh # check if zsh is even installed for this user - capture3 raises # Errno::ENOENT when it is not, same as the backtick this replaces begin out, _err, _status = Open3.capture3("zsh", "-c", "echo $fpath") fpath = out.split(" ") rescue pexit "Failure: ".red + "zsh is probably not installed, could not find $fpath" end # Try user-writable directories first, then system directories user_home = home writable_dirs = fpath.select do |fp| fp.start_with?(user_home) && File.directory?(fp) && File.writable?(fp) end # If no user-writable directories, try to create one if writable_dirs.empty? user_completion_dir = File.join(user_home, ".zsh", "completion") begin # mkdir_p is happy either way, but saying "Created" every time a # developer reinstalls is a small lie about what just happened existed = File.directory?(user_completion_dir) FileUtils.mkdir_p(user_completion_dir) writable_dirs << user_completion_dir puts "Created user completion directory: #{user_completion_dir}".yel unless existed # Auto-configure .zshrc if it exists zshrc = File.join(user_home, ".zshrc") if File.exist?(zshrc) # the marker is the directory, not the whole block: a zshrc that # already puts it on $fpath is configured, however it spelled the # compinit call next to it append_once(zshrc, ZSHRC_LINES, marker: ".zsh/completion", label: "~/.zshrc") do puts "Run: ".yel + "source ~/.zshrc".cyan + " to activate" end else puts "Add this to your ~/.zshrc: ".yel + "fpath=(~/.zsh/completion $fpath)".cyan end rescue # Couldn't create user dir, try system dirs as fallback end end # Try writable directories first, then all directories as fallback dirs_to_try = writable_dirs + fpath.reject { |fp| writable_dirs.include?(fp) } success = false dirs_to_try.each do |fp| next unless File.directory?(fp) begin # nothing is autoloaded here on purpose: `zsh -c 'autoload -U # _booker'` loaded it into a subshell that exited on the next line. # clear_zsh_compdump below is what reaches a new shell File.write(File.join(fp, "_booker"), completion_script("_booker")) puts "Success: ".grn + "installed zsh autocompletion in #{fp}" success = true break rescue # Try next directory silently end end if success clear_zsh_compdump else puts "Warning: ".yel + "Could not install ZSH completion to any directory in $fpath" puts "Try manually: ".grn + "mkdir -p ~/.zsh/completion && booker --install zsh" end end |
#install_config ⇒ Object
318 319 320 321 322 323 |
# File 'lib/booker/installer.rb', line 318 def install_config Config.default.write puts "Success: ".grn + "example config file written to ~/.booker" rescue pexit "Failure: ".red + "could not write example config file to ~/.booker" end |
#install_one(target) ⇒ Object
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/booker/installer.rb', line 44 def install_one(target) if /comp/i.match?(target) # completion for every shell on this machine install_completion elsif (shell = SHELLS.find { |s| target.downcase.include?(s) }) install_completion_for(shell) # completion for one named shell elsif /book/i.match?(target) # bookmarks installation install_bookmarks elsif /conf/i.match?(target) # default config file generation install_config elsif /safari/i.match?(target) # opt-in Safari FDA setup (macOS only) install_safari else # unknown argument passed into install pexit "Failure: ".red + "unknown installation option (#{target})" end end |
#install_safari ⇒ Object
Opt-in Safari setup: walks through granting Full Disk Access so booker can read ~/Library/Safari/Bookmarks.plist. Not included in the default --install flow because it requires a TCC permission grant.
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 |
# File 'lib/booker/installer.rb', line 328 def install_safari plist = File.join(home, "Library/Safari/Bookmarks.plist") fda_url = "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles" unless RUBY_PLATFORM.include?("darwin") puts "Skip: ".yel + "Safari support is macOS-only." return end unless File.exist?(plist) puts "Skip: ".yel + "Safari bookmarks not found at #{plist}" puts "Hint: ".grn + "launch Safari at least once, then re-run." return end if safari_readable?(plist) puts "PASS: ".grn + "Safari bookmarks are already readable." return end puts "Safari stores bookmarks at:" puts " #{plist}".cyan puts puts "That file is protected by macOS TCC. Grant Full Disk Access to one of:" puts puts " [A] ".cyan + "Your terminal app".yel + " (simplest; inherited by any tool)" puts " [B] ".cyan + "/usr/bin/plutil only".yel + " (narrower scope)" puts print "Pick [A] or [B] (default A): " $stdout.flush choice = $stdin.gets.to_s.strip.upcase choice = "A" if choice.empty? pexit "Error: ".red + "invalid choice." unless %w[A B].include?(choice) puts puts "Opening the Full Disk Access pane..." system("open", fda_url) puts puts "In the pane that just opened:" if choice == "A" puts " 1. Click ".yel + "+".cyan + ", add your terminal app from /Applications" puts " 2. Toggle it ".yel + "on".cyan puts " 3. ".yel + "Fully quit".cyan + " the terminal (Cmd+Q), reopen it," puts " and re-run ".yel + "booker --install safari".cyan + " to verify." else puts " 1. Click ".yel + "+".cyan puts " 2. Press ".yel + "Cmd+Shift+G".cyan + " (opens 'Go to Folder')".yel puts " 3. Type ".yel + "/usr/bin/plutil".cyan + " and press Return".yel puts " 4. Click ".yel + "Open".cyan + ", then toggle it ".yel + "on".cyan puts print "Press Return once you've added plutil and toggled it on... " $stdout.flush $stdin.gets if safari_readable?(plist) puts "PASS: ".grn + "Safari bookmarks are now readable." else puts "FAIL: ".red + "still can't read the bookmarks file." puts "Try fully quitting the terminal (Cmd+Q) and re-running." end end end |
#safari_readable?(plist) ⇒ Boolean
392 393 394 |
# File 'lib/booker/installer.rb', line 392 def safari_readable?(plist) system("plutil", "-lint", "-s", plist, out: File::NULL, err: File::NULL) end |
#save_bookmarks(selected, message) ⇒ Object
303 304 305 306 307 |
# File 'lib/booker/installer.rb', line 303 def save_bookmarks(selected, ) puts "Selected: ".yel + selected Config.default.write(:bookmarks, selected) puts "Success: ".grn + end |
#shell_present?(shell) ⇒ Boolean
Picker.which, not sh -c "command -v": a fork per shell to answer a PATH
question booker already knows how to answer in process
222 |
# File 'lib/booker/installer.rb', line 222 def shell_present?(shell) = !Picker.which(shell).nil? |
#xdg_config_home ⇒ Object
253 254 255 |
# File 'lib/booker/installer.rb', line 253 def xdg_config_home ENV["XDG_CONFIG_HOME"] || File.join(home, ".config") end |
#xdg_data_home ⇒ Object
249 250 251 |
# File 'lib/booker/installer.rb', line 249 def xdg_data_home ENV["XDG_DATA_HOME"] || File.join(home, ".local", "share") end |