1 # Copyright (c) 2006 Mauricio Fernandez <mfp@acm.org>
2 # http://eigenclass.org/hiki.rb?wmii+ruby
3 # Licensed under the same terms as Ruby (see LICENSE).
5 # ===========================================================================
7 #{{{ Core bindings and applets defined as the "standard" plugin
9 # This file must be placed in $HOME/.wmii-3/plugins.
11 # It will be overwritten when upgrading ruby-wmii, so modifications will be
14 # If you want to change a standard binding/applet, either:
15 # * send a patch to <mfp@acm.org> (put wmii in the subject)
16 # * copy the relevant code to another file under $HOME/.wmii-3/plugins
17 # and edit as needed. Don't forget to change the namespace, e.g.
18 # Plugin.define "my-address@isp.com" do
19 # .... paste the code here & change it ...
21 # you'll also have to change wmiirc-config.rb to import the new
22 # bindings/applets. For example, if you want to use new definitions of
23 # the "retag" binding and the "volume" applet, comment
25 # use_bar_applet "volume"
26 # in the from "standard" do .... end area, and add
27 # from "my-address@isp.com" do
29 # use_bar_applet "volume"
32 # Read the top of wmiirc or type Alt-a -> config-help for more information on
35 # The standard plugin is updated regularly with new functionality and
36 # *bugfixes*. You cannot benefit from them automatically on upgrade if you
37 # copy the code and modify the definitions under your own namespace, though.
38 # ===========================================================================
40 Plugin.define "standard" do
41 author '"Mauricio Fernandez" <mfp@acm.org>'
44 bar_applet("volume", 990) do |wmii, bar|
45 mixers = wmii.plugin_config["standard:volume"]["mixer"] || ["Master"]
46 mixers = [mixers] if !(Array === mixers)
47 update_volume = lambda do |increment|
52 sign = increment < 0 ? "-" : "+"
54 mixers.reverse.each do |mixer| # show status of first mixer in list
55 status = `amixer set "#{mixer},0" #{increment.abs}#{sign}`
57 volume = status [/\[(\d+%)\]/]
58 volume = "OFF" if status[/\[off\]/]
59 bar.data = "VOL #{volume}"
61 Thread.new{ loop { update_volume[0]; sleep 10 } }
63 term = wmii.plugin_config["standard"]["x-terminal-emulator"] || "xterm"
64 bar.on_click(MOUSE_SCROLL_UP){ update_volume[+1] }
65 bar.on_click(MOUSE_SCROLL_DOWN){ update_volume[-1] }
66 bar.on_click(MOUSE_BUTTON_LEFT) do
67 handler = wmii.on_createclient do |cid|
68 wmii.write("/view/sel/sel/ctl", "sendto 0")
69 wmii.write("/view/sel/sel/geom", "0 100 east south-100")
70 wmii.unregister handler
72 system "wmiisetsid #{term} -e alsamixer &"
74 bar.on_click(MOUSE_BUTTON_RIGHT) do
75 action = case `amixer get "#{mixers.first},0"`
76 when /\[off\]/: 'unmute'
77 when /\[on\]/ : 'mute'
79 mixers.each do |mixer|
80 `amixer set "#{mixer},0" #{action}`
86 #{{{ Modal keybindings: raw/normal modes.
87 bar_applet("mode", 980) do |wmii, bar|
90 keys = wmii.plugin_config["standard:mode"]["mode_toggle_keys"] || ["MODKEY2-space"]
91 h = wmii.on_key(*keys) do
94 wmii.write("/def/keys", saved_keys)
97 LOGGER.info "Entering NORMAL mode."
99 saved_keys = wmii.read("/def/keys")
100 wmii.write("/def/keys", h.key)
103 LOGGER.info "Entering RAW mode."
109 #{{{ Selection + temporary view
110 bar_applet("temporary-selection", 985) do |wmii, bar|
111 wmii.on_clientfocus do |cid|
112 view = wmii.curr_view
113 if wmii.read("/client/#{cid}/tags").split(/\+/).include? "#{view}:tmp"
119 wmii.on_key(*([wmii.plugin_config["standard:temporary-selection"]["select-keys"] ||
120 ["MODKEY-x"]]).flatten) do |wmii, |
121 old_tags = wmii.curr_client_tags
122 case curr = wmii.curr_view
124 wmii.retag_curr_client("-#{curr}")
126 tmpview = "#{curr}:tmp"
127 if old_tags.include? tmpview
128 wmii.retag_curr_client("-#{tmpview}")
131 wmii.retag_curr_client("+#{tmpview}")
136 wmii.on_key(*([wmii.plugin_config["standard:temporary-selection"]["destroy-keys"] ||
137 ["MODKEY-Shift-x"]]).flatten) do |wmii, |
138 ids = lambda{|txt| txt.to_a.map{|x| x.split(/\s+/).last}.select{|x| /\A\d+\z/ =~ x} }
139 view = wmii.curr_view.gsub(/:tmp/,"")
141 ids[wmii.read("/client")].each do |cid|
142 ctags_file = "/client/#{cid}/tags"
143 old_tags = wmii.read(ctags_file)
144 new_tags = wmii.normalize(old_tags.split(/\+/) - ["#{view}:tmp"])
145 wmii.write(ctags_file, new_tags) if old_tags != new_tags
152 bar_applet("dict", 880, "DICT") do |wmii, bar|
153 dict_ask_and_define = lambda do
155 wmii.wmiimenu([]) do |phrase|
156 system "dcop kdict KDictIface definePhrase '#{phrase}'"
157 end.value # block until we get the word
158 wmii.set_curr_view "dict" unless wmii.curr_view == "dict"
161 wmii.on_key("MODKEY-Control-d"){ dict_ask_and_define.call }
162 bar.on_click(MOUSE_BUTTON_LEFT){ dict_ask_and_define.call }
163 bar.on_click(MOUSE_BUTTON_RIGHT){ wmii.set_curr_view "dict" }
166 # {{{ Battery monitor
167 # Originally by Wael Nasreddine <wael@phoenixlinux.org>.
168 bar_applet("battery-monitor", 950) do |wmii, bar|
169 statefile = wmii.plugin_config["standard:battery-monitor"]["statefile"] ||
170 '/proc/acpi/battery/BAT0/state'
171 infofile = wmii.plugin_config["standard:battery-monitor"]["infofile"] ||
172 '/proc/acpi/battery/BAT0/info'
173 low = wmii.plugin_config["standard:battery-monitor"]["low"] || 5
174 low_action = wmii.plugin_config["standard:battery-monitor"]["low-action"] ||
175 'echo "Low battery" | xmessage -center -buttons quit:0 -default quit -file -'
176 critical = wmii.plugin_config["standard:battery-monitor"]["critical"] || 1
177 critical_action = wmii.plugin_config["standard:battery-monitor"]["critical-action"] ||
178 'echo "Critical battery" | xmessage -center -buttons quit:0 -default quit -file -'
180 warned_critical = false
183 batt = IO.readlines(statefile)
184 battinfo = IO.readlines(infofile)
185 battpresent = battinfo[0].gsub(/.*:\s*/,'').chomp
186 if battpresent == "yes"
187 batt_percent = ((batt[4].gsub(/.*:\s*/,'').chomp.chomp("mAh").to_f / battinfo[2].gsub(/.*:\s*/,'').chomp.chomp(" mAh").to_f ) * 100).to_i
188 batt_state = batt[2].gsub(/.*:\s*/,'').chomp
189 # Take action in case battery is low/critical
190 if batt_state == "discharging" && batt_percent <= critical
191 unless warned_critical
192 LOGGER.info "Warning about critical battery."
193 system("wmiisetsid #{critical_action} &")
194 warned_critical = true
196 elsif batt_state == "discharging" && batt_percent <= low
198 LOGGER.info "Warning about low battery."
199 system("wmiisetsid #{low_action} &")
204 warned_critical = false
206 # If percent is 100 and state is discharging then
207 # the battery is full and not discharging.
208 batt_state = "=" if batt_state == "charged" || ( batt_state == "discharging" && batt_percent >= 97 )
209 batt_state = "^" if batt_state == "charging"
210 batt_state = "v" if batt_state == "discharging"
211 text = "#{batt_state} #{batt_percent} #{batt_state}"
222 # Originally by Wael Nasreddine <wael@phoenixlinux.org>.
223 bar_applet("mpd", 100) do |wmii, bar|
225 mpd_do_action = lambda do |action, *args|
229 r = mpd.__send__(action, *args)
230 LOGGER.info "MPD #{action}"
238 update_bar = lambda do
239 mpdserv_status = mpdserv.status["state"]
241 when 'play' : text = ">>: "; show_info = true
242 when 'pause': text = "||: "; show_info = true
243 else show_info = false
246 title = mpdserv.strf("%t")[0..(wmii.plugin_config["standard:mpd"]["title_maxlen"] || -1)]
247 author = mpdserv.strf("%a")[0..(wmii.plugin_config["standard:mpd"]["author_maxlen"] || -1)]
248 bar.data = text + "#{author} - #{title} " + mpdserv.strf("(%e/%l)")
249 else # Player is stopped or connection not yet initialized...
250 bar.data = "[]: NOT PLAYING"
253 # Initialize MPD status
255 loop{ begin; update_bar.call; rescue Exception; end; sleep 1 }
257 bar.on_click(MOUSE_SCROLL_UP) { mpd_do_action[:previous] }
258 bar.on_click(MOUSE_SCROLL_DOWN){ mpd_do_action[:next] }
259 bar.on_click(MOUSE_BUTTON_LEFT) do
263 mpdserv_status = mpd.status
267 case mpdserv_status["state"]
268 when "play": mpd_do_action[:pause]
269 when "pause", "stop" : mpd_do_action[:play]
273 bar.on_click(MOUSE_BUTTON_RIGHT) do
274 mpd_handle = wmii.on_createclient do |cid|
275 wmii.write("/view/sel/sel/ctl", "sendto 0")
276 wmii.write("/view/sel/sel/geom", "400 0 center+200 south")
277 wmii.unregister mpd_handle
279 wmii.write("/view/ctl", "select toggle")
280 term = wmii.plugin_config["standard"]["x-terminal-emulator"] || "xterm"
281 system "wmiisetsid #{term} -e ncmpc &"
286 bar_applet("cpuinfo", 800) do |wmii, bar|
289 cpuinfo = IO.readlines("/proc/cpuinfo").grep(/MHz/).first.split[-1].sub(/\..*$/,'')
290 bar.data = cpuinfo.chomp + " Mhz"
297 bar_applet("status", 0, "STATUS BAR --- init") do |wmii, bar|
299 text_proc = wmii.plugin_config["standard:status"]["text_proc"]
302 Thread.new{ loop { currload = `uptime`.chomp.sub(/.*: /,"").gsub(/,/,""); sleep 10 } }
303 text_proc = lambda { "#{Time.new.strftime("%a %b %d %I:%M %p")} #{currload}" }
306 bar.data = text_proc.call
307 sleep(wmii.plugin_config["standard:status"]["refresh_period"] || 1)
311 xmessagebox = "xmessage -center -buttons quit:0 -default quit -file -"
312 term = wmii.plugin_config["standard"]["x-terminal-emulator"] || "xterm"
313 fl = lambda{ wmii.write "/view/ctl", "select 0" }
314 toggle_fl = lambda{ sleep 2; wmii.write "/view/ctl", "select toggle" }
315 left_action = wmii.plugin_config["standard:status"]["left_click_action"] ||
316 lambda { fl[]; system "tzwatch | wmiisetsid #{xmessagebox} &"; toggle_fl[] }
317 right_action = wmii.plugin_config["standard:status"]["right_click_action"] ||
318 lambda { fl[]; system "ncal -y | wmiisetsid #{xmessagebox} &"; toggle_fl[] }
319 middle_action = wmii.plugin_config["standard:status"]["middle_click_action"] ||
320 lambda { fl[]; system "wmiisetsid #{term} -e top &"; toggle_fl[] }
321 bar.on_click do |name, button|
322 current = wmii.curr_view_index
324 when MOUSE_BUTTON_LEFT: left_action.call if left_action
325 when MOUSE_BUTTON_MIDDLE: middle_action.call if middle_action
326 when MOUSE_BUTTON_RIGHT: right_action.call if right_action
328 wmii.set_curr_view(wmii.views[wmii.curr_view_index-1] || wmii.views[-1])
329 when MOUSE_SCROLL_DOWN
330 wmii.set_curr_view(wmii.views[wmii.curr_view_index+1] || wmii.views[0])
335 binding("dict-lookup", "MODKEY-Control-d") do |wmii,|
337 wmii.wmiimenu([]) do |phrase|
338 system "dict #{phrase} > /tmp/dict.txt || ( zenity --error --text=\"dict returned $?\"; exit 1;) && zenity --text-info --filename=/tmp/dict.txt "
339 end.value # block until we get the word
340 wmii.set_curr_view "dict" unless wmii.curr_view == "dict"
344 binding("lock-x", "MODKEY-Control-l") do |wmii,|
345 system "gnome-screensaver-command -l"
348 #{{{ actions (internal and WMIIRC_HOME/*) (w/ history)
349 standard_internal_actions = {
350 "browser" => lambda do |wmii, *selection|
351 selection = selection[0]
352 selection ||= `wmiipsel`.strip
353 case browser = ENV["BROWSER"]
354 when nil: system "wmiisetsid /etc/alternatives/x-www-browser '#{selection}' &"
355 else system "wmiisetsid #{browser} '#{selection}' &"
358 "google" => lambda do |wmii, *selection|
360 if selection && !selection.empty?
361 selection = CGI.escape(selection.join(" "))
363 selection = CGI.escape(%!#{`wmiipsel`.strip}!)
365 url = "http://www.google.com/search?q=#{selection}"
366 case browser = ENV["BROWSER"]
367 when nil: system "wmiisetsid /etc/alternatives/x-www-browser '#{url}' &"
368 else system "wmiisetsid #{browser} '#{url}' &"
371 "screenshot" => lambda do |wmii, *base|
372 fname = (base[0] || "screenshot") + "000"
373 fname.succ! while File.exist?(File.join(ENV["HOME"], "tmp", "#{fname}.png"))
374 system("import -window root ~/tmp/#{fname}.png &")
376 "rename-view" => lambda do |wmii, *args|
377 unless /./ =~ (new_name = args[0].to_s)
378 new_name = wmii.wmiimenu([]).value # blocking, OK
381 wmii.read("/client").each do |line|
382 cid = line.split(/\s+/).last
383 wmii.write("/client/#{cid}/tags",
384 wmii.read("/client/#{cid}/tags").gsub(/\b#{Regexp.escape(old)}\b/, new_name))
388 "quit" => lambda do |wmii|
389 wmii.write "/event", "Bye"
390 wmii.write "/ctl", "quit"
392 "config-help" => lambda do |wmii|
393 IO.popen("xmessage -file -", "w"){|f| f.puts WMIIRC_HELP_MESSAGE; f.close_write }
397 def_settings "actions/{internal,history,history_size}" do |wmii|
398 wmii.plugin_config["standard:actions"]["internal"] ||= {}
399 wmii.plugin_config["standard:actions"]["history"] = []
400 wmii.plugin_config["standard:actions"]["history_size"] = 5
403 binding("execute-action", "MODKEY-Shift-a") do |wmii,|
404 internal_actions = standard_internal_actions.merge(wmii.plugin_config["standard:actions"]["internal"])
405 history_size = wmii.plugin_config["standard:actions"]["history_size"]
406 remembered = wmii.plugin_config["standard:actions"]["history"]
407 actions = (wmii.action_list + internal_actions.keys.map{|x| x.to_s}).sort.uniq
408 internal_actions.each_pair{|name, action| actions.delete(name) unless action }
409 list = remembered + actions
410 result = wmii.wmiimenu(list) do |choice|
411 choices = choice.split(/\s+/)
413 if internal_actions.has_key? cmd
414 internal_actions[cmd].call(wmii, *choices[1..-1]) if internal_actions[cmd]
416 system("wmiisetsid #{WMIIRC_HOME}/#{choice} &") if /^\s*$/ !~ choice
419 # use result.value to record the choice in the current process
421 if cmd = result.value.split(/\s+/).first
422 remembered.delete cmd
423 remembered.unshift cmd
424 LOGGER.debug "plugin/actions: history #{remembered.inspect}"
425 remembered.replace remembered[0, history_size]
431 standard_programs = {
432 "programs" => lambda do
433 # Get the standard list of executable files in your $PATH
434 @__program_list_last_update ||= Time.at(0)
435 @__program_list ||= {}
436 path_glob = ENV["PATH"].gsub(/,/, '\\,').tr(":",",")
437 return @__program_list if Time.new - @__program_list_last_update < 3600
439 @__program_list_last_update = Time.new
440 Dir.glob("{#{path_glob}}/*").select do |fname|
441 File.file?(fname) && File.executable?(fname)
442 end.map{|fname| File.basename(fname)}.uniq.each{|v| @__program_list[v] = v}
447 def_settings "programs/histories" do |wmii|
448 wmii.plugin_config["standard:programs"]["histories"] = {}
451 def run_program(wmii, *lists)
453 history_name = lists.map{|h| h.keys}.flatten.sort.join("-")
454 lists.each{|list| list.each_value{|x| programs.merge!(x.call) } }
455 programs.delete_if { |k,v| k =~ /^\s*$/ }
456 histories = wmii.plugin_config["standard:programs"]["histories"]
457 history = histories[history_name] ||= []
458 choice = wmii.wmiimenu(history + programs.keys.sort).value
461 s.sub!(/\s+\S+$/, '') until programs.has_key?(s) || s.empty? || !s[/\s/]
462 cmd, args = (programs[s] || s), choice.sub(s, '').strip
463 LOGGER.info "Executing #{choice}"
464 system("wmiisetsid #{cmd} #{args} &")
465 return [choice, history_name]
471 def_settings "programs/{history_size,histories}" do |wmii|
472 wmii.plugin_config["standard:programs"]["history_size"] = 5
473 wmii.plugin_config['standard:programs']['histories'] = Hash.new{|h,k| h[k] = []}
476 def record_history(wmii, entry, history_name)
477 history_size = wmii.plugin_config["standard:programs"]["history_size"]
478 histories = wmii.plugin_config['standard:programs']['histories']
479 history = wmii.plugin_config['standard:programs']['histories'][history_name]
481 history.unshift entry
482 LOGGER.debug "plugin/programs: history #{entry.inspect}"
483 history.replace history[0, history_size]
486 def_settings "programs/lists" do |wmii|
487 wmii.plugin_config["standard:programs"]["lists"] ||= {}
490 #{{{ programs (w/ history)
491 binding("execute-program", "MODKEY-Shift-p") do |wmii,|
493 entry, history_name = run_program(wmii, standard_programs, wmii.plugin_config["standard:programs"]["lists"])
494 record_history(wmii, entry, history_name) if entry
498 # {{{ Run program with given tag
499 binding("execute-program-with-tag", "MODKEY-Control-y") do |wmii,|
501 result = wmii.wmiimenu(wmii.views_intellisort) do |tag|
505 choice, history_name = run_program(wmii, standard_programs,
506 wmii.plugin_config["standard:programs"]["lists"])
507 Marshal.dump([choice, history_name], wr)
509 Marshal.dump(nil, wr)
516 # using result.value to perform the view switch in the current process so
517 # that the transition table can be updated
522 handler = wmii.on_createclient do |cid|
523 LOGGER.info "Moving #{cid} to #{tag}"
524 wmii.write("/client/#{cid}/tags", tag)
525 wmii.unregister handler
529 choice, history_name = Marshal.load(rd.read)
531 record_history(wmii, choice, history_name)
532 else # empty string, spaces only, etc
533 wmii.unregister handler
541 #{{{ Either move to the given numeric tag, if it exists, or to the
542 # (N-last_numeric_tag)th non-numeric tag.
543 # e.g. views 1 3 4 mail web
549 binding("numeric-jump-#{key}", "MODKEY-#{key}") do |wmii,|
550 all_views = wmii.views
551 num_tags = all_views.grep(/^\d+$/)
552 nkey = (key - 1) % 10
553 if num_tags.include?(key.to_s)
555 elsif nkey >= (prev_index = (num_tags.last || 0).to_i)
556 non_num_tags = all_views - num_tags
557 wmii.view non_num_tags[nkey - prev_index]
562 #{{{ Move to given view, with intelligent history
563 binding("tag-jump", "MODKEY-t") do |wmii,|
565 # do it this way so the current process can update the transition table
566 wmii.view wmii.wmiimenu(wmii.views_intellisort - [wmii.curr_view]).value
570 binding("retag", "MODKEY-Shift-t") do |wmii,|
571 wmii.wmiimenu(wmii.views_intellisort){|new_tag| wmii.retag_curr_client(new_tag) }
573 binding("retag-jump", "MODKEY-Shift-r") do |wmii,|
575 wmii.wmiimenu(wmii.views_intellisort) do |new_tag|
577 wmii.retag_curr_client(new_tag)
585 wmii.view new_tag[/(?![+-]).*/]
589 binding("namespace-retag", "MODKEY2-Shift-t") do |wmii,|
590 wmii.wmiimenu(wmii.views){|new_tag| wmii.retag_curr_client_ns(new_tag) }
592 binding("namespace-retag-jump", "MODKEY2-Shift-r") do |wmii,|
594 result = wmii.wmiimenu(wmii.views) do |new_tag|
596 wmii.retag_curr_client_ns(new_tag)
604 wmii.view "#{wmii.curr_view[/[^:]+/]}:#{subtag[/(?![+-]).*/]}"
608 (('a'..'z').to_a+('0'..'9').to_a).each do |key|
609 binding("letter-jump-#{key}", "MODKEY2-#{key}") do |wmii,|
610 unless wmii.curr_view[0,1] == key
611 wmii.view wmii.views_intellisort.find{|x| x[0,1] == key }
615 # Retag as specified numeric tag if it exists, or
616 # (N-last_numeric_tag)th non-numeric tag.
618 binding("numeric-retag-#{key}", "MODKEY-Shift-#{key}") do |wmii,|
619 all_views = wmii.views
620 num_tags = all_views.grep(/^\d+$/)
621 curr_tags = wmii.curr_client_tags
622 nkey = (key - 1) % 10
623 if num_tags.include? key.to_s or key > all_views.size
624 new_tags = curr_tags.reject{|x| /^\d+$/=~ x } + [key.to_s]
625 elsif nkey >= (prev_index = (num_tags.last || 0).to_i)
626 non_num_tags = all_views - num_tags
627 new_tags = non_num_tags[nkey - prev_index]
631 LOGGER.info "Retagging #{curr_tags.inspect} => #{new_tags.inspect}"
632 wmii.set_curr_client_tags(new_tags)
635 # Retag current client using tag starting with key.
636 # Only the current view is replaced.
637 (('a'..'z').to_a+('0'..'9').to_a).each do |key|
638 binding("letter-retag-#{key}", "MODKEY2-Shift-#{key}") do |wmii,|
639 unless wmii.curr_view[0,1] == key
640 curr_tags = wmii.curr_client_tags
641 new_view = wmii.views_intellisort.find{|x| x[0,1] == key }
643 new_tags = curr_tags.reject {|view| view == new_view }
644 curr_index = new_tags.index wmii.curr_view
646 LOGGER.error "curr_index is nil in letter-retag"
649 new_tags[curr_index] = new_view
650 LOGGER.info "Retagging #{curr_tags.inspect} => #{new_tags.inspect}"
651 wmii.set_curr_client_tags(new_tags)
658 binding("history-move-forward", "MODKEY-plus"){|wmii,| wmii.view_history_forward }
659 binding("history-move-back", "MODKEY-minus"){|wmii,| wmii.view_history_back }
660 binding("move-prev", "MODKEY-Control-UP", "MODKEY-comma") do |wmii,|
661 wmii.view wmii.views[wmii.curr_view_index-1] || wmii.views[-1]
663 binding("move-next", "MODKEY-Control-DOWN", "MODKEY-period") do |wmii,|
664 wmii.view wmii.views[wmii.curr_view_index+1] || wmii.views[0]
666 move_within_namespace = lambda do |wmii, offset|
667 namespace = wmii.curr_view[/([^:]+)/]
668 candidate_views = wmii.views.grep(/#{Regexp.escape(namespace)}\b/)
669 dest = candidate_views[candidate_views.index(wmii.curr_view) + offset]
670 dest ||= (offset > 0) ? candidate_views[0] : candidate_views[-1]
673 binding("namespace-move-prev", "MODKEY2-Shift-UP", "MODKEY2-comma") do |wmii,|
674 move_within_namespace.call(wmii, -1)
676 binding("namespace-move-next", "MODKEY2-Shift-DOWN", "MODKEY2-period") do |wmii,|
677 move_within_namespace.call(wmii, +1)
682 #{{{ Bookmark manager
683 # Defines the following bindings:
684 # bookmark take current X11 primary selection (with wmiipsel), ask
685 # for description (suggests the page title). You can
686 # append tags to the description:
687 # Page about foo :foo :ruby :bar
688 # tags the bookmark as :foo, :ruby and :bar, and sets the
689 # description to "Page about foo"
690 # bookmark-open ask for a bookmark and open it on a new browser window
691 # The possible completions are shown as you type text from
692 # the description. You can refine the selection
693 # successively, entering a bit (e.g. a word) at a time
694 # (append a space if you don't want the first suggestion
695 # to be taken). You can also use any number of the following
696 # conditions (possible completions will only be shown
697 # after you press enter in that case):
699 # :tag only bookmarks tagged with :tag
700 # ~t regexp bookmarks whose description matches regexp
701 # ~u regexp bookmarks whose URL matches regexp
702 # ~p regexp bookmarks whose protocol matches regexp
703 # ~d 2001 bookmarks defined/last used in 2001
704 # ~d jan bookmarks defined/last used in January
705 # ~d >4d bookmarks defined/last used over 4 days ago
706 # ~d >4m 4 months ago
707 # ~d <4d bookmarks defined/last used less than 4 days ago
708 # ~d <4m 4 months ago
709 # ~d q1 bookmarks defined/last used in the first quarter
714 # returns all bookmarks with "eigen" in the description or the
715 # URL, tagged as :ruby, used/defined in the last 3 months
717 # There are also some commands that apply to the current list
718 # (they will only be recognized as commands if you don't enter
719 # anything else on the same line):
721 # !o open all the bookmarks
725 # :blog<enter> to select all the bookmarks tagged as :blog
726 # !o<enter> to open them all
729 class BookmarkManager
730 Bookmark = Struct.new(:description, :url, :tags, :date)
732 def initialize(filename)
735 @deleted_bookmarks = {}
742 IO.foreach(@filename) do |line|
744 desc, url, tags, date = line.chomp.split(/\t/).map{|x| x.strip}
745 if @bookmarks[url].nil? && !@deleted_bookmarks[url]
746 tags = (tags || "").split(/\s/)
748 date = Time.rfc822(date)
752 bm = Bookmark.new(desc, url, tags, date)
755 LOGGER.warn "Loading bookmark #{url.inspect}: already loaded, skipping."
758 # keep parsing other lines
764 # Returns the bookmark corresponding to the given URL, or nil.
766 self.load unless @loaded
770 # Returns true if it was a new bookmark, false if a bookmark
771 # with the same URL exists.
772 # If a bookmark with the same URL already exists it is replaced.
773 def add_bookmark(desc, url, tags, date)
774 self.load unless @loaded
775 ret = @bookmarks.include? url
776 bm = Bookmark.new(desc, url, tags, date)
778 @deleted_bookmarks.delete(url)
782 # Remove bookmark matching the given URL if it exists.
783 # Returns true if bookmark was removed otherwise returns false.
784 def remove_bookmark(url)
785 @deleted_bookmarks[url] = @bookmarks[url] if @bookmarks.has_key?(url)
786 not @bookmarks.delete(url).nil?
790 self.load unless @loaded
794 # Saves the bookmarks to the specified filename. It tries to merge them with
795 # the list currently stored in the file. Bookmarks present in the later but
796 # missing in +self+ which were not explicitly deleted will also be added to the
798 def save!(destination = @filename)
800 tmpfile = @filename + "_tmp_#{Process.pid}"
801 File.open(tmpfile, "a") do |f|
802 @bookmarks.values.sort_by{|bm| bm.date}.reverse_each do |bm|
803 f.puts [bm.description, bm.url, bm.tags.join(" "), bm.date.rfc822].join("\t")
807 File.rename(tmpfile, destination) # atomic if on the same FS and fleh
812 IO.foreach(@filename) do |line|
813 desc, url, tags, date = line.chomp.split(/\t/).map{|x| x.strip}
814 unless @deleted_bookmarks[url] ||
816 @bookmarks[url].date >= date)
817 tags = (tags || "").split(/\s/)
819 date = Time.rfc822(date)
823 add_bookmark(desc, url, tags, date)
832 fh = File.open(@filename + ".lock", "a")
833 fh.flock(File::LOCK_EX)
834 @mutex.synchronize{ yield(self) }
836 fh.flock(File::LOCK_UN)
841 def satisfy_date_condition?(bookmark, condition)
844 when /^q1$/i : date.month >= 12 || date.month <= 4
845 when /^q2$/i : date.month >= 3 && date.month <= 7
846 when /^q3$/i : date.month >= 6 && date.month <= 10
847 when /^q4$/i : date.month >= 9 || date.month <= 1
848 when /^\d+$/ : date.year == condition.to_i
849 when /^\w+$/ : date.month - 1 == Time::RFC2822_MONTH_NAME.index(condition.capitalize)
850 when /^([><])(\d+)([md])/
851 sign, units, type = $1, $2.to_i, $3
852 multiplier = 3600 * 24
853 multiplier *= 30.4375 if type == 'm'
855 when '<': Time.new - date <= units * multiplier
856 when '>': Time.new - date >= units * multiplier
860 private :satisfy_date_condition?
862 def refine_selection(expression, choices=self.bookmarks)
863 expression = expression.strip
864 pieces = expression.split(/\s+/)
866 option_needed = false
869 when true: criteria.last << " #{x}"; option_needed = false
870 when false: criteria << x; option_needed = true if /^~\w/ =~ x
873 choices.select do |bm|
874 criteria.all? do |criterion|
876 when /~t\s+(\S+)/: Regexp.new($1) =~ bm.description
877 when /~u\s+(\S+)/: Regexp.new($1) =~ bm.url
878 when /~p\s+(\S+)/: Regexp.new($1) =~ bm.url[%r{^(\S+?):/},1]
879 when /~d\s+(\S+)/: satisfy_date_condition?(bm, $1)
880 when /:.+$/ : bm.tags.include?(criterion)
881 else bm.description.index(criterion) or bm.url.index(criterion)
888 BOOKMARK_FILE = File.join(ENV["HOME"], ".wmii-3", "bookmarks.txt")
889 BOOKMARK_REMOTE_FILE = BOOKMARK_FILE + ".remote"
890 BOOKMARK_AGENT = 'ruby-wmii #{WMIIRC_VERSION} (#{WMIIRC_RELEASE_DATE})'
892 MISSING_DELICIOUS_AUTH_MSG = <<EOF
893 Missing del.icio.us user/password.
894 You must set them in your wmiirc-config.rb as follows:
896 plugin_config["standard:bookmark"]["del.icio.us-user"] = 'username'
897 plugin_config["standard:bookmark"]["del.icio.us-password"] = 'password'
906 require 'resolv-replace'
908 Socket.do_not_reverse_lookup = true
910 Plugin.define "standard" do
911 author '"Mauricio Fernandez" <mfp@acm.org>'
913 DELICIOUS_ENCODING = 'UTF-8'
915 def perform_delicious_request(request, user, pass)
916 delicious_address = Resolv.new.getaddress 'api.del.icio.us'
917 https = Net::HTTP.new(delicious_address, 443)
921 req = Net::HTTP::Get.new(request, 'User-Agent' => BOOKMARK_AGENT)
922 req.basic_auth(user, pass)
923 https.request(req).body
927 def push_delicious_bookmark(bookmark, user, pass, shared = false, encoding = nil)
928 LOGGER.debug "Pushing to del.icio.us: #{bookmark.inspect}"
929 desc = encoding.nil? ? bookmark.description :
930 Iconv.conv(DELICIOUS_ENCODING, encoding, bookmark.description)
931 req_url = '/v1/posts/add?'
932 req_url << "url=#{CGI.escape(bookmark.url)}"
933 req_url << ";description=#{CGI.escape(desc)}"
934 tags = bookmark.tags.map{|x| x.gsub(/^:/,'')}.join(' ')
935 req_url << ";tags=#{CGI.escape(tags)}"
936 req_url << ";replace=yes;shared=#{shared ? 'yes' : 'no'}"
937 date = bookmark.date.clone.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
938 req_url << ";dt=#{CGI.escape(date)}"
940 perform_delicious_request(req_url, user, pass)
943 def delete_delicious_bookmark(url, user, pass)
944 LOGGER.debug "Deleting from del.icio.us: #{url.inspect}"
945 req_url = '/v1/posts/delete?'
946 req_url << "url=#{CGI.escape(url)}"
948 perform_delicious_request(req_url, user, pass)
951 def download_delicious_bookmarks(user, pass, mode = :full, encoding = nil)
952 require 'rexml/document'
955 LOGGER.debug "Downloading del.icio.us bookmarks, #{mode} mode"
958 req = '/v1/posts/all'
960 req = '/v1/posts/recent?count=100'
962 xml = perform_delicious_request(req, user, pass)
964 elements = REXML::Document.new(xml).elements
966 elements.each("posts/post") do |el|
968 desc, url, tags, time = %w[description href tag time].map{|x| el.attributes[x]}
969 desc = Iconv.conv(encoding, DELICIOUS_ENCODING, desc) unless encoding.nil?
970 desc = desc.gsub(/\s+/, " ").strip
971 year, month, day, hour, min, sec, = ParseDate.parsedate(time)
972 date = Time.utc(year, month, day, hour, min, sec, 0)
974 if tags == 'system:unfiled'
977 tags = tags.split(/\s+/).map{|x| ":#{x}"}
980 ret << [desc, url, tags, date]
981 rescue Iconv::IllegalSequence
982 LOGGER.error "download_delicious_bookmarks, #{url}: iconv error #{$!.class}."
984 LOGGER.error "download_delicious_bookmarks, #{url}: #{$!}."
991 def get_delicious_update_time(user, pass)
992 require 'rexml/document'
995 LOGGER.debug "Getting del.icio.us last update time"
997 req = '/v1/posts/update'
998 xml = perform_delicious_request(req, user, pass)
999 time = REXML::Document.new(xml).elements['update'].attributes['time']
1000 year, month, day, hour, min, sec, = ParseDate.parsedate(time)
1001 Time.utc(year, month, day, hour, min, sec, 0)
1004 def sync_delicious_bookmarks(wmii, mode, last_update_time = nil)
1006 config = wmii.plugin_config["standard:bookmark"]
1007 user = config["del.icio.us-user"]
1008 pass = config["del.icio.us-password"]
1009 shared = config.has_key?("del.icio.us-share") ? config["del.icio.us-share"] : false
1010 encoding = config["encoding"]
1012 if "#{user}".empty? or "#{pass}".empty?
1013 raise MISSING_DELICIOUS_AUTH_MSG
1016 LOGGER.info "Sync with del.icio.us, in #{mode} mode, last update time #{last_update_time ? last_update_time : "unknown"}."
1018 bm_manager = BookmarkManager.new(BOOKMARK_FILE)
1019 prev_bm_manager = BookmarkManager.new(BOOKMARK_REMOTE_FILE)
1020 unless File.exist?(BOOKMARK_REMOTE_FILE)
1021 # This is the first time we sync with del.icio.us.
1022 # Download all bookmarks.
1023 download_delicious_bookmarks(user, pass, :full, encoding).each do |desc, url, tags, date|
1024 prev_bm_manager.add_bookmark(desc, url, tags, date)
1026 prev_bm_manager.transaction { |bmanager| bmanager.save! }
1027 last_update_time = Time.now
1030 # Form lists of local bookmark changes.
1032 if mode == :bidirectional
1033 bm_manager.bookmarks.reject{|bm| bm.url !~ %r[^(http|https|ftp)://]}.each do |bm|
1034 if prev_bm_manager[bm.url].nil?
1035 local_changes[bm.url] = {:bm => bm, :change => :add}
1036 elsif prev_bm_manager[bm.url].description != bm.description ||
1037 prev_bm_manager[bm.url].tags.sort != bm.tags.sort
1038 local_changes[bm.url] = {:bm => bm, :change => :modify}
1041 prev_bm_manager.bookmarks.reject{|bm| bm.url !~ %r[^(http|https|ftp)://]}.each do |bm|
1042 if bm_manager[bm.url].nil?
1043 local_changes[bm.url] = {:change => :remove}
1048 # Form lists of remote bookmark changes.
1050 if last_update_time.nil? ||
1051 last_update_time < get_delicious_update_time(user, pass)
1052 # The last update time is unknown or
1053 # the remote bookarks were updated since last sync
1054 remote_bookmarks = {}
1055 download_delicious_bookmarks(user, pass, :full, encoding).each do |desc, url, tags, date|
1056 remote_bookmarks[url] = BookmarkManager::Bookmark.new(desc, url, tags, date)
1059 remote_bookmarks.values.each do |bm|
1060 if prev_bm_manager[bm.url].nil?
1061 remote_changes[bm.url] = {:bm => bm, :change => :add}
1062 elsif prev_bm_manager[bm.url].description != bm.description ||
1063 prev_bm_manager[bm.url].tags.sort != bm.tags.sort
1064 remote_changes[bm.url] = {:bm => bm, :change => :modify}
1067 prev_bm_manager.bookmarks.reject{|bm| bm.url !~ %r[^(http|https|ftp)://]}.each do |bm|
1068 if remote_bookmarks[bm.url].nil?
1069 remote_changes[bm.url] = {:change => :remove}
1074 (local_changes.keys + remote_changes.keys).uniq.each do |url|
1075 local_change = local_changes[url].nil? ? :none : local_changes[url][:change]
1076 remote_change = remote_changes[url].nil? ? :none : remote_changes[url][:change]
1078 if local_change == :none ||
1079 (remote_change == :remove && local_change != :add)
1080 # If no local changes - propagate remote changes.
1081 # If bookmark was deleted remotely, delete it locally
1082 # even if it was modified locally.
1083 LOGGER.debug "Bookmark #{url.inspect}: no local changes, propagating remote changes #{remote_change.inspect}."
1086 bm = remote_changes[url][:bm]
1087 bm_manager.add_bookmark(bm.description, bm.url, bm.tags, bm.date)
1089 bm_manager.remove_bookmark(url) unless local_change == :remove
1091 LOGGER.error "sync_delicious_bookmarks: unknown change type #{remote_change.inspect}."
1093 elsif remote_change == :none ||
1094 (local_change == :remove && remote_change != :add)
1095 # If no remote changes - propagate local changes.
1096 # If bookmark was deleted locally, delete it remotely
1097 # even if it was modified remotely.
1098 LOGGER.debug "Bookmark #{url.inspect}: no remote changes, propagating local changes #{local_change.inspect}."
1101 bm = local_changes[url][:bm]
1102 push_delicious_bookmark(bm, user, pass, shared, encoding)
1104 delete_delicious_bookmark(url, user, pass) unless remote_change == :remove
1106 LOGGER.error "sync_delicious_bookmarks: unknown change type #{local_change.inspect}."
1109 # If bookmark was modified locally and remotely,
1110 # propagate the newest version.
1111 local_bm = local_changes[url][:bm]
1112 remote_bm = remote_changes[url][:bm]
1113 if local_bm.date <= remote_bm.date
1114 LOGGER.debug "Bookmark #{url.inspect}: remote changes are newer, propagating remote changes #{remote_change}."
1115 bm_manager.add_bookmark(remote_bm.description,
1120 LOGGER.debug "Bookmark #{url.inspect}: local changes are newer, propagating local changes #{local_change}."
1121 push_delicious_bookmark(local_bm, user, pass, shared, encoding)
1125 bm_manager.transaction {|bmanager| bmanager.save!}
1126 prev_bm_manager.transaction {FileUtils.cp BOOKMARK_FILE, BOOKMARK_REMOTE_FILE}
1128 LOGGER.info "Done importing bookmarks from del.icio.us."
1131 def bookmark_url(wmii,url)
1138 url = URI.escape url
1141 LOGGER.error "Failed to bookmark #{URI.unescape(url)}: invalid URI."
1144 bookmark_protocols = wmii.plugin_config["standard:bookmark"]["protocols"]
1145 protocol_desc = bookmark_protocols[uri.scheme]
1146 if protocol_desc.nil?
1147 user_specified_proto = wmii.wmiimenu(bookmark_protocols.keys.sort).value
1148 user_specified_proto.strip!
1149 unless user_specified_proto.empty?
1150 uri.scheme = user_specified_proto
1151 # reparse the url after scheme change
1152 uri = URI.parse uri.to_s
1153 protocol_desc = bookmark_protocols[uri.scheme]
1156 unless protocol_desc.nil?
1157 # If url path is empty, set it to '/' to avoid duplication after
1158 # sync with del.icio.us.
1159 uri.path = '/' if uri.path.empty?
1160 title_variants = nil
1161 unless protocol_desc[:get_title].nil?
1164 title_variants = protocol_desc[:get_title].call(wmii,uri)
1166 rescue Timeout::Error
1167 LOGGER.warn "get_title timeout for URL #{uri.to_s}."
1169 LOGGER.warn "get_title exception for URL #{uri.to_s}: #{$!}"
1172 wmii.wmiimenu(title_variants) do |choice|
1173 tags = choice[/\s+(:\S+\s*)+$/] || ""
1174 description = choice[0..-1-tags.size].strip
1175 if description =~ /\S/
1176 bm_manager = BookmarkManager.new(BOOKMARK_FILE)
1177 LOGGER.info "Bookmarking #{uri.to_s}: #{description.inspect}, tags #{tags.inspect}"
1178 bm_manager.add_bookmark(description, uri.to_s, tags.strip.split(/\s+/), Time.new)
1179 bm_manager.transaction{|bm| bm.save!}
1183 LOGGER.error "Failed to bookmark #{uri.to_s}: unknown protocol #{uri.scheme.inspect}."
1187 def_settings("actions/internal") do |wmii|
1188 hash = wmii.plugin_config["standard:actions"]["internal"] ||= {}
1190 import_lambda = lambda do
1191 info = download_delicious_bookmarks(wmii.plugin_config["standard:bookmark"]["del.icio.us-user"],
1192 wmii.plugin_config["standard:bookmark"]["del.icio.us-password"],
1194 bm = BookmarkManager.new(BOOKMARK_FILE)
1195 info.each{|desc, url, tags, date| bm.add_bookmark(desc, url, tags, date) }
1197 File.delete(BOOKMARK_REMOTE_FILE) rescue nil
1198 prevbm = BookmarkManager.new(BOOKMARK_REMOTE_FILE)
1199 info.each{|desc, url, tags, date| prevbm.add_bookmark(desc, url, tags, date) }
1202 hash.update("del.icio.us-import" => import_lambda)
1207 "del.icio.us-sync" => lambda do
1208 mode = wmii.plugin_config["standard:bookmark"]["del.icio.us-mode"] || :unidirectional
1209 sync_delicious_bookmarks(wmii, mode)
1211 "bookmark-add" => lambda do |wmii|
1212 # if xclip is not available, the first one will be empty
1213 options = [`xclip -o -selection clipboard`, `wmiipsel`].reject {|x| x.strip.empty?}
1214 wmii.wmiimenu(options) do |url|
1215 unless url.strip.empty?
1216 url = "file://#{url}" unless url[%r{\A\w+:/}]
1217 bookmark_url(wmii, url)
1221 "bookmark-delete" => lambda do |wmii|
1222 bm_manager = BookmarkManager.new(BOOKMARK_FILE)
1223 delete_bookmark = lambda do |bm|
1224 if bm_manager.remove_bookmark bm.url
1225 bm_manager.transaction{ |bmanager| bmanager.save! }
1226 LOGGER.info "Delete bookmark #{bm.description.inspect} -> #{bm.url}."
1228 LOGGER.info "Could not delete bookmark #{bm.description.inspect} -> #{bm.url}."
1231 refine_choices = lambda do |bookmarks|
1232 options = bookmarks.sort_by{|x| x.description}.map do |x|
1233 "#{x.description} : #{x.url}"
1235 wmii.wmiimenu(options) do |condition|
1236 condition = condition.strip
1237 unless condition.empty?
1238 if condition == "!o"
1239 if bookmarks.size <=
1240 (limit = wmii.plugin_config["standard:bookmark"]["multiple-delete-limit"])
1241 bookmarks.each do |bm|
1242 delete_bookmark.call(bm)
1245 LOGGER.error "Tried to delete #{bookmarks.size} bookmarks at a time."
1246 LOGGER.error "Refusing since it's over multiple-delete-limit (#{limit})."
1248 elsif bm = bm_manager[condition[/ : (\S+)$/,1]]
1249 delete_bookmark.call(bm)
1251 choices = bm_manager.refine_selection(condition, bookmarks)
1252 refine_choices.call(choices) unless choices.empty?
1257 refine_choices.call(bm_manager.bookmarks)
1259 "bookmark-edit" => lambda do |wmii|
1260 bm_manager = BookmarkManager.new(BOOKMARK_FILE)
1261 refine_choices = lambda do |bookmarks|
1262 options = bookmarks.sort_by{|x| x.description}.map do |x|
1263 "#{x.description} : #{x.url}"
1265 wmii.wmiimenu(options) do |condition|
1266 condition = condition.strip
1267 unless condition.empty?
1268 if bm = bm_manager[condition[/ : (\S+)$/,1]]
1269 title_variants = [bm.description]
1271 (bm.description + ' ' + bm.tags.join(' ')) unless bm.tags.empty?
1272 wmii.wmiimenu(title_variants) do |choice|
1273 tags = choice[/\s+(:\S+\s*)+$/] || ""
1274 description = choice[0..-1-tags.size].strip
1275 if description =~ /\S/
1276 LOGGER.info "Edited bookmark #{description.inspect} -> #{bm.url}."
1277 bm_manager.add_bookmark(description, bm.url, tags.strip.split(/\s+/), Time.new)
1278 bm_manager.transaction{ |bmanager| bmanager.save! }
1282 choices = bm_manager.refine_selection(condition, bookmarks)
1283 refine_choices.call(choices) unless choices.empty?
1288 refine_choices.call(bm_manager.bookmarks)
1293 def_settings("bookmark/multiple-open") do |wmii|
1294 wmii.plugin_config["standard:bookmark"]["multiple-open-limit"] = 10
1297 def_settings("bookmark/multiple-delete") do |wmii|
1298 wmii.plugin_config["standard:bookmark"]["multiple-delete-limit"] = 30
1301 def_settings("bookmark/del.icio.us importer") do |wmii|
1302 wmii.plugin_config["standard:bookmark"]["refresh_period"] = 30
1304 sleep 20 # time to get the wmiirc-config.rb loaded
1305 if wmii.plugin_config["standard:bookmark"]["del.icio.us-user"] and
1306 wmii.plugin_config["standard:bookmark"]["del.icio.us-password"]
1308 mode = wmii.plugin_config["standard:bookmark"]["del.icio.us-mode"] || :unidirectional
1309 last_update_time = nil
1312 sync_delicious_bookmarks(wmii, mode, last_update_time)
1313 last_update_time = Time.now
1315 LOGGER.error "Error while sync'ing bookmarks."
1316 LOGGER.error $!.exception
1319 sleep(60 * wmii.plugin_config["standard:bookmark"]["refresh_period"])
1325 standard_bookmark_protocols = {
1327 :open_urls => lambda do |wmii,bms|
1328 browser = ENV["BROWSER"] || '/etc/alternatives/x-www-browser'
1329 urls = bms.map{|bm| bm[:uri].to_s}.join "' '"
1330 system "wmiisetsid #{browser} '#{urls}' &"
1332 :get_title => lambda do |wmii,uri|
1333 resolved_uri = uri.clone
1334 resolved_uri.host = Resolv.new.getaddress resolved_uri.host
1335 contents = open(resolved_uri.to_s, "Host" => uri.host, "User-Agent" => BOOKMARK_AGENT){|f| f.read}
1336 title = CGI.unescapeHTML((contents[%r{title>(.*)</title>}im, 1] || "").strip).gsub(/&[^;]+;/, "")
1337 title.gsub!(/\s+/, " ")
1338 [title, title.downcase, title.capitalize]
1342 :open_urls => lambda do |wmii,bms|
1343 term = wmii.plugin_config["standard"]["x-terminal-emulator"] || "xterm"
1347 ssh_host = "#{uri.user}@" + ssh_host unless uri.user.nil?
1348 ssh_port = "-p #{uri.port}" unless uri.port.nil?
1349 system "wmiisetsid #{term} -T '#{bm[:bm].url}' -e 'ssh #{ssh_host} #{ssh_port} || read' &"
1352 :get_title => lambda do |wmii,uri|
1354 title = "#{uri.user}@" + title unless uri.user.nil?
1355 title << ":#{uri.port.to_s}" unless uri.port.nil?
1360 standard_bookmark_protocols['https'] = standard_bookmark_protocols['http']
1361 standard_bookmark_protocols['ftp'] =
1363 :open_urls => standard_bookmark_protocols['http'][:open_urls],
1364 :get_title => standard_bookmark_protocols['ssh'][:get_title]
1368 def_settings("bookmark/protocols") do |wmii|
1369 wmii.plugin_config["standard:bookmark"]["protocols"] ||= {}
1370 wmii.plugin_config["standard:bookmark"]["protocols"] =
1371 standard_bookmark_protocols.merge wmii.plugin_config["standard:bookmark"]["protocols"]
1374 binding("bookmark", "MODKEY-Shift-b") do |wmii,|
1376 url = `wmiipsel`.strip
1377 url = "file://#{url}" unless url[%r{\A\w+:/}]
1378 bookmark_url(wmii, url)
1382 binding("bookmark-open", "MODKEY-Shift-Control-b") do |wmii,|
1383 bm_manager = BookmarkManager.new(BOOKMARK_FILE)
1384 open_bookmark = lambda do |bms|
1385 bookmark_protocols = wmii.plugin_config["standard:bookmark"]["protocols"]
1388 LOGGER.debug "Opening bookmark #{bm.description.inspect} -> #{bm.url}."
1390 uri = URI.parse bm.url
1392 LOGGER.error "Failed to open #{bm.url}: invalid URI. Corrupted bookmarks.txt?"
1395 protocol_desc = bookmark_protocols[uri.scheme]
1396 unless protocol_desc.nil?
1397 bm_hash[protocol_desc] = (bm_hash[protocol_desc].to_a << {:bm => bm, :uri => uri})
1399 LOGGER.error "Failed to open #{bm.url}: unknown protocol #{uri.scheme.inspect}."
1402 bm_hash.each do |proto,bms|
1403 proto[:open_urls].call(wmii,bms)
1406 refine_choices = lambda do |bookmarks|
1407 options = bookmarks.sort_by{|x| x.description}.map do |x|
1408 "#{x.description} : #{x.url}"
1410 wmii.wmiimenu(options) do |condition|
1411 condition = condition.strip
1412 unless condition.empty?
1413 if condition == "!o"
1414 if bookmarks.size <=
1415 (limit = wmii.plugin_config["standard:bookmark"]["multiple-open-limit"])
1416 bookmarks.each do |bm|
1419 bm_manager.transaction{|bm| bm.save!}
1420 open_bookmark.call(bookmarks)
1422 LOGGER.error "Tried to open #{bookmarks.size} bookmarks at a time."
1423 LOGGER.error "Refusing since it's over multiple-open-limit (#{limit})."
1425 elsif bm = bm_manager[condition[/ : (\S+)$/,1]]
1427 bm_manager.transaction{ bm_manager.save! }
1428 open_bookmark.call([bm])
1430 choices = bm_manager.refine_selection(condition, bookmarks)
1431 refine_choices.call(choices) unless choices.empty?
1436 refine_choices.call(bm_manager.bookmarks)