a bookmark id is "_". safari uses a uuid where
chrome and firefox number theirs, so the second half is anything id-shaped
rather than digits; bare digits stay valid for single-source configs
written before the prefix existed. completion inserts urls now, but ids
are still first class - --bookmark takes them, the picker returns them
/\A(?:\d+_[a-z0-9-]+|[0-9_]+)\z/i
ID_WIDTH =
what is left of the terminal once the id column and the single spaces
between columns are spoken for, shared out by proportion with a floor
apiece so a narrow window still shows something of every field
# File 'lib/booker/cli.rb', line 21definitialize(args)parseargsend
Instance Method Details
#column_widths ⇒ Object
212
213
214
215
# File 'lib/booker/cli.rb', line 212defcolumn_widthsremaining=Term.width-ID_WIDTH-3SHARES.transform_values{|share,floor|(remaining*share).clamp(floor..).to_i}end
# File 'lib/booker/cli.rb', line 80defdispatch_option(args)option_parser.parse!(args)case@modewhen:bookmarkpexit"Error: ".red+"booker --bookmark expects bookmark id"ifargs.empty?open_bookmark(args)when:installinstaller.install(args.empty??%w[completionconfigbookmarks]:args)when:searchpexit"Error: ".red+"--search requires an argument"ifargs.empty?open_search(args.join(""))when:list# `booker --list github` narrows the table, rather than silently
# dropping the term and printing everything
show_bookmarks(args.join(""),force_table:true)when:completeBookmarks.new(args.join("")).autocompletewhen:complete_rawBookmarks.new(args.join("")).autocomplete_rawendrescueOptionParser::InvalidOption=>epexit"Error: ".red+e.messageend
#installer ⇒ Object
105
106
107
108
109
110
111
112
# File 'lib/booker/cli.rb', line 105definstaller# the installer pulls in find, fileutils and open3 and is reachable only
# through --install, so a plain search - and every completion subshell -
# loaded all of it to never call any of it
require_relative"installer"@installer||=Installer.newend
#open_bookmark(bm, store = nil) ⇒ Object
bookmark ids, opened in order. parsing every source costs hundreds of
milliseconds, so a caller that already has the set - the picker does -
passes it in, and it is searched once for the list rather than once per id
135
136
137
138
139
140
141
142
143
144
# File 'lib/booker/cli.rb', line 135defopen_bookmark(bm,store=nil)store||=Bookmarks.newbm.eachdo|id|url=store.bookmark_url(id)pexit"Failure:".red+" bookmark #{id} not found"ifurl.nil?puts"opening bookmark ".grn+urlopenweb(url)# no shell quoting needed - system() handles it
endend
#open_search(term) ⇒ Object
146
147
148
149
150
151
# File 'lib/booker/cli.rb', line 146defopen_search(term)puts"searching ".grn+termsearch=Config.default.searcherterm=term.tr("","+")openweb(search+term)# No shell escape needed - it's a URL
end
# File 'lib/booker/cli.rb', line 114defopenweb(url)# Pass URL directly to browser without invoking shell
# This avoids issues with special characters like parentheses
browser_cmd=browse.strip# Redirect stdout/stderr to suppress GTK warnings
success=system(browser_cmd,url,out:File::NULL,err:File::NULL)unlesssuccess# system reports a missing binary as nil rather than false, but the
# forked child still exited - with 127, the shell convention for execvp
# failures - so there is a status either way. the &. is only for nothing
# in this process having spawned a child yet
code=Process.last_status&.exitstatusputs"Warning: ".yel+"Failed to open URL (exit code: #{code})"endend
# File 'lib/booker/cli.rb', line 51defoption_parser# required here rather than at the top of the file: optparse is only
# reachable through an argument starting with "-", so `booker <term>` and
# every tab press paid to load it without ever building one
require"optparse"@option_parser||=OptionParser.newdo|opts|opts.banner="Usage: booker [options] [arguments]"opts.separator""opts.separator"Main options:"opts.on("-b","--bookmark","explicitly open bookmark"){@mode=:bookmark}opts.on("-i","--install","install: all|bookmarks|completion|config|safari"){@mode=:install}opts.on("-s","--search","explicitly search arguments"){@mode=:search}opts.on("-l","--list","print the bookmark table, skipping the picker"){@mode=:list}opts.separator""opts.separator"Other options:"opts.on("-c","--complete","show tab completions"){@mode=:complete}opts.on("--complete-raw","tab completions, tab separated (for shell scripts)"){@mode=:complete_raw}opts.on("-v","--version","print version"){putsVERSIONexit0}opts.on_tail("-h","--help","show help"){putsoptsexit0}endend
# File 'lib/booker/cli.rb', line 25defparse(args)show_bookmarksifargs.none?ifargs.first&.start_with?("-")dispatch_option(args)exit0endbookmark_ids,other_args=args.partition{|a|BOOKMARK_ID.match?(a)}open_bookmark(bookmark_ids)unlessbookmark_ids.empty?unlessother_args.empty?# every argument being a url means they all came from tab completion,
# which inserts one per bookmark - open the lot. one non-url word makes
# the whole line a search, so a phrase containing a domain still is one
ifother_args.all?{|arg|domain.match?(arg)}other_args.eachdo|site|puts"opening website: ".grn+siteopenweb(prep(site))endelsepick_or_search(other_args.join("").strip)endendend
#pick_and_open(bookmarks) ⇒ Object
offer the bookmarks as a choice and open what comes back. either way this
ends the run: cancelling the picker is a decision, not a fallthrough to
whatever the caller would have done instead
168
169
170
171
172
173
# File 'lib/booker/cli.rb', line 168defpick_and_open(bookmarks)id=Picker.select(bookmarks.rows)exit0ifid.nil?open_bookmark([id],bookmarks)exit0end
#pick_or_search(term) ⇒ Object
a term matching bookmarks is offered as a choice; one matching nothing is
a search, exactly as it always was. cancelling the picker is a decision
rather than a fallthrough, and booker -s <term> still demands a search
156
157
158
159
160
161
162
163
# File 'lib/booker/cli.rb', line 156defpick_or_search(term)ifPicker.enabled?bookmarks=Bookmarks.new(term)pick_and_open(bookmarks)unlessbookmarks.allurls.empty?endopen_search(term)end
#row(bookmark, widths) ⇒ Object
217
218
219
220
221
222
223
224
# File 'lib/booker/cli.rb', line 217defrow(bookmark,widths)[bookmark.id.to_s.window(ID_WIDTH).grn,bookmark.display_folder.window(widths[:folder]).blu,bookmark.title.window(widths[:title]).yel,bookmark.url.window(widths[:url])].join("")end
# File 'lib/booker/cli.rb', line 175defshow_bookmarks(term="",force_table:false)bookmarks=Bookmarks.new(term)allurls=bookmarks.allurlsifallurls.empty?puts"No bookmarks found.".redputs"Run: ".yel+"booker --install bookmarks".cyanexit0end# the picker owns the screen when there is one, so nothing is printed
# above it. --list forces the table back for anyone who wants it
pick_and_open(bookmarks)if!force_table&&Picker.enabled?puts"Bookmarks:".grn+" (usage: booker <id> or booker <search>)"puts""widths=column_widthsallurls.each{|bookmark|putsrow(bookmark,widths)}puts""puts"Found #{allurls.length} bookmarks".grnputs""puts"Examples:".yelputs"#{"booker #{allurls.first.id}".ljust(20).cyan} # Open first bookmark"puts"#{"booker github".ljust(20).cyan} # Search bookmarks for 'github'"puts"#{"booker --help".ljust(20).cyan} # Show help"exit0end