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,|
336 LOGGER.debug "dict-lookup called!!!"
338 wmii.wmiimenu([]) do |phrase|
339 system "dict #{phrase} > /tmp/dict.txt | zenity --text-info --filename=/tmp/dict.txt"
340 end.value # block until we get the word
341 wmii.set_curr_view "dict" unless wmii.curr_view == "dict"
345 #{{{ actions (internal and WMIIRC_HOME/*) (w/ history)
346 standard_internal_actions = {
347 "browser" => lambda do |wmii, *selection|
348 selection = selection[0]
349 selection ||= `wmiipsel`.strip
350 case browser = ENV["BROWSER"]
351 when nil: system "wmiisetsid /etc/alternatives/x-www-browser '#{selection}' &"
352 else system "wmiisetsid #{browser} '#{selection}' &"
355 "google" => lambda do |wmii, *selection|
357 if selection && !selection.empty?
358 selection = CGI.escape(selection.join(" "))
360 selection = CGI.escape(%!#{`wmiipsel`.strip}!)
362 url = "http://www.google.com/search?q=#{selection}"
363 case browser = ENV["BROWSER"]
364 when nil: system "wmiisetsid /etc/alternatives/x-www-browser '#{url}' &"
365 else system "wmiisetsid #{browser} '#{url}' &"
368 "screenshot" => lambda do |wmii, *base|
369 fname = (base[0] || "screenshot") + "000"
370 fname.succ! while File.exist?(File.join(ENV["HOME"], "tmp", "#{fname}.png"))
371 system("import -window root ~/tmp/#{fname}.png &")
373 "rename-view" => lambda do |wmii, *args|
374 unless /./ =~ (new_name = args[0].to_s)
375 new_name = wmii.wmiimenu([]).value # blocking, OK
378 wmii.read("/client").each do |line|
379 cid = line.split(/\s+/).last
380 wmii.write("/client/#{cid}/tags",
381 wmii.read("/client/#{cid}/tags").gsub(/\b#{Regexp.escape(old)}\b/, new_name))
385 "quit" => lambda do |wmii|
386 wmii.write "/event", "Bye"
387 wmii.write "/ctl", "quit"
389 "config-help" => lambda do |wmii|
390 IO.popen("xmessage -file -", "w"){|f| f.puts WMIIRC_HELP_MESSAGE; f.close_write }
394 def_settings "actions/{internal,history,history_size}" do |wmii|
395 wmii.plugin_config["standard:actions"]["internal"] ||= {}
396 wmii.plugin_config["standard:actions"]["history"] = []
397 wmii.plugin_config["standard:actions"]["history_size"] = 5
400 binding("execute-action", "MODKEY-Shift-a") do |wmii,|
401 internal_actions = standard_internal_actions.merge(wmii.plugin_config["standard:actions"]["internal"])
402 history_size = wmii.plugin_config["standard:actions"]["history_size"]
403 remembered = wmii.plugin_config["standard:actions"]["history"]
404 actions = (wmii.action_list + internal_actions.keys.map{|x| x.to_s}).sort.uniq
405 internal_actions.each_pair{|name, action| actions.delete(name) unless action }
406 list = remembered + actions
407 result = wmii.wmiimenu(list) do |choice|
408 choices = choice.split(/\s+/)
410 if internal_actions.has_key? cmd
411 internal_actions[cmd].call(wmii, *choices[1..-1]) if internal_actions[cmd]
413 system("wmiisetsid #{WMIIRC_HOME}/#{choice} &") if /^\s*$/ !~ choice
416 # use result.value to record the choice in the current process
418 if cmd = result.value.split(/\s+/).first
419 remembered.delete cmd
420 remembered.unshift cmd
421 LOGGER.debug "plugin/actions: history #{remembered.inspect}"
422 remembered.replace remembered[0, history_size]
428 standard_programs = {
429 "programs" => lambda do
430 # Get the standard list of executable files in your $PATH
431 @__program_list_last_update ||= Time.at(0)
432 @__program_list ||= {}
433 path_glob = ENV["PATH"].gsub(/,/, '\\,').tr(":",",")
434 return @__program_list if Time.new - @__program_list_last_update < 3600
436 @__program_list_last_update = Time.new
437 Dir.glob("{#{path_glob}}/*").select do |fname|
438 File.file?(fname) && File.executable?(fname)
439 end.map{|fname| File.basename(fname)}.uniq.each{|v| @__program_list[v] = v}
444 def_settings "programs/histories" do |wmii|
445 wmii.plugin_config["standard:programs"]["histories"] = {}
448 def run_program(wmii, *lists)
450 history_name = lists.map{|h| h.keys}.flatten.sort.join("-")
451 lists.each{|list| list.each_value{|x| programs.merge!(x.call) } }
452 programs.delete_if { |k,v| k =~ /^\s*$/ }
453 histories = wmii.plugin_config["standard:programs"]["histories"]
454 history = histories[history_name] ||= []
455 choice = wmii.wmiimenu(history + programs.keys.sort).value
458 s.sub!(/\s+\S+$/, '') until programs.has_key?(s) || s.empty? || !s[/\s/]
459 cmd, args = (programs[s] || s), choice.sub(s, '').strip
460 LOGGER.info "Executing #{choice}"
461 system("wmiisetsid #{cmd} #{args} &")
462 return [choice, history_name]
468 def_settings "programs/{history_size,histories}" do |wmii|
469 wmii.plugin_config["standard:programs"]["history_size"] = 5
470 wmii.plugin_config['standard:programs']['histories'] = Hash.new{|h,k| h[k] = []}
473 def record_history(wmii, entry, history_name)
474 history_size = wmii.plugin_config["standard:programs"]["history_size"]
475 histories = wmii.plugin_config['standard:programs']['histories']
476 history = wmii.plugin_config['standard:programs']['histories'][history_name]
478 history.unshift entry
479 LOGGER.debug "plugin/programs: history #{entry.inspect}"
480 history.replace history[0, history_size]
483 def_settings "programs/lists" do |wmii|
484 wmii.plugin_config["standard:programs"]["lists"] ||= {}
487 #{{{ programs (w/ history)
488 binding("execute-program", "MODKEY-Shift-p") do |wmii,|
490 entry, history_name = run_program(wmii, standard_programs, wmii.plugin_config["standard:programs"]["lists"])
491 record_history(wmii, entry, history_name) if entry
495 # {{{ Run program with given tag
496 binding("execute-program-with-tag", "MODKEY-Control-y") do |wmii,|
498 result = wmii.wmiimenu(wmii.views_intellisort) do |tag|
502 choice, history_name = run_program(wmii, standard_programs,
503 wmii.plugin_config["standard:programs"]["lists"])
504 Marshal.dump([choice, history_name], wr)
506 Marshal.dump(nil, wr)
513 # using result.value to perform the view switch in the current process so
514 # that the transition table can be updated
519 handler = wmii.on_createclient do |cid|
520 LOGGER.info "Moving #{cid} to #{tag}"
521 wmii.write("/client/#{cid}/tags", tag)
522 wmii.unregister handler
526 choice, history_name = Marshal.load(rd.read)
528 record_history(wmii, choice, history_name)
529 else # empty string, spaces only, etc
530 wmii.unregister handler
538 #{{{ Either move to the given numeric tag, if it exists, or to the
539 # (N-last_numeric_tag)th non-numeric tag.
540 # e.g. views 1 3 4 mail web
546 binding("numeric-jump-#{key}", "MODKEY-#{key}") do |wmii,|
547 all_views = wmii.views
548 num_tags = all_views.grep(/^\d+$/)
549 nkey = (key - 1) % 10
550 if num_tags.include?(key.to_s)
552 elsif nkey >= (prev_index = (num_tags.last || 0).to_i)
553 non_num_tags = all_views - num_tags
554 wmii.view non_num_tags[nkey - prev_index]
559 #{{{ Move to given view, with intelligent history
560 binding("tag-jump", "MODKEY-t") do |wmii,|
562 # do it this way so the current process can update the transition table
563 wmii.view wmii.wmiimenu(wmii.views_intellisort - [wmii.curr_view]).value
567 binding("retag", "MODKEY-Shift-t") do |wmii,|
568 wmii.wmiimenu(wmii.views_intellisort){|new_tag| wmii.retag_curr_client(new_tag) }
570 binding("retag-jump", "MODKEY-Shift-r") do |wmii,|
572 wmii.wmiimenu(wmii.views_intellisort) do |new_tag|
574 wmii.retag_curr_client(new_tag)
582 wmii.view new_tag[/(?![+-]).*/]
586 binding("namespace-retag", "MODKEY2-Shift-t") do |wmii,|
587 wmii.wmiimenu(wmii.views){|new_tag| wmii.retag_curr_client_ns(new_tag) }
589 binding("namespace-retag-jump", "MODKEY2-Shift-r") do |wmii,|
591 result = wmii.wmiimenu(wmii.views) do |new_tag|
593 wmii.retag_curr_client_ns(new_tag)
601 wmii.view "#{wmii.curr_view[/[^:]+/]}:#{subtag[/(?![+-]).*/]}"
605 (('a'..'z').to_a+('0'..'9').to_a).each do |key|
606 binding("letter-jump-#{key}", "MODKEY2-#{key}") do |wmii,|
607 unless wmii.curr_view[0,1] == key
608 wmii.view wmii.views_intellisort.find{|x| x[0,1] == key }
612 # Retag as specified numeric tag if it exists, or
613 # (N-last_numeric_tag)th non-numeric tag.
615 binding("numeric-retag-#{key}", "MODKEY-Shift-#{key}") do |wmii,|
616 all_views = wmii.views
617 num_tags = all_views.grep(/^\d+$/)
618 curr_tags = wmii.curr_client_tags
619 nkey = (key - 1) % 10
620 if num_tags.include? key.to_s or key > all_views.size
621 new_tags = curr_tags.reject{|x| /^\d+$/=~ x } + [key.to_s]
622 elsif nkey >= (prev_index = (num_tags.last || 0).to_i)
623 non_num_tags = all_views - num_tags
624 new_tags = non_num_tags[nkey - prev_index]
628 LOGGER.info "Retagging #{curr_tags.inspect} => #{new_tags.inspect}"
629 wmii.set_curr_client_tags(new_tags)
632 # Retag current client using tag starting with key.
633 # Only the current view is replaced.
634 (('a'..'z').to_a+('0'..'9').to_a).each do |key|
635 binding("letter-retag-#{key}", "MODKEY2-Shift-#{key}") do |wmii,|
636 unless wmii.curr_view[0,1] == key
637 curr_tags = wmii.curr_client_tags
638 new_view = wmii.views_intellisort.find{|x| x[0,1] == key }
640 new_tags = curr_tags.reject {|view| view == new_view }
641 curr_index = new_tags.index wmii.curr_view
643 LOGGER.error "curr_index is nil in letter-retag"
646 new_tags[curr_index] = new_view
647 LOGGER.info "Retagging #{curr_tags.inspect} => #{new_tags.inspect}"
648 wmii.set_curr_client_tags(new_tags)
655 binding("history-move-forward", "MODKEY-plus"){|wmii,| wmii.view_history_forward }
656 binding("history-move-back", "MODKEY-minus"){|wmii,| wmii.view_history_back }
657 binding("move-prev", "MODKEY-Control-UP", "MODKEY-comma") do |wmii,|
658 wmii.view wmii.views[wmii.curr_view_index-1] || wmii.views[-1]
660 binding("move-next", "MODKEY-Control-DOWN", "MODKEY-period") do |wmii,|
661 wmii.view wmii.views[wmii.curr_view_index+1] || wmii.views[0]
663 move_within_namespace = lambda do |wmii, offset|
664 namespace = wmii.curr_view[/([^:]+)/]
665 candidate_views = wmii.views.grep(/#{Regexp.escape(namespace)}\b/)
666 dest = candidate_views[candidate_views.index(wmii.curr_view) + offset]
667 dest ||= (offset > 0) ? candidate_views[0] : candidate_views[-1]
670 binding("namespace-move-prev", "MODKEY2-Shift-UP", "MODKEY2-comma") do |wmii,|
671 move_within_namespace.call(wmii, -1)
673 binding("namespace-move-next", "MODKEY2-Shift-DOWN", "MODKEY2-period") do |wmii,|
674 move_within_namespace.call(wmii, +1)
679 #{{{ Bookmark manager
680 # Defines the following bindings:
681 # bookmark take current X11 primary selection (with wmiipsel), ask
682 # for description (suggests the page title). You can
683 # append tags to the description:
684 # Page about foo :foo :ruby :bar
685 # tags the bookmark as :foo, :ruby and :bar, and sets the
686 # description to "Page about foo"
687 # bookmark-open ask for a bookmark and open it on a new browser window
688 # The possible completions are shown as you type text from
689 # the description. You can refine the selection
690 # successively, entering a bit (e.g. a word) at a time
691 # (append a space if you don't want the first suggestion
692 # to be taken). You can also use any number of the following
693 # conditions (possible completions will only be shown
694 # after you press enter in that case):
696 # :tag only bookmarks tagged with :tag
697 # ~t regexp bookmarks whose description matches regexp
698 # ~u regexp bookmarks whose URL matches regexp
699 # ~p regexp bookmarks whose protocol matches regexp
700 # ~d 2001 bookmarks defined/last used in 2001
701 # ~d jan bookmarks defined/last used in January
702 # ~d >4d bookmarks defined/last used over 4 days ago
703 # ~d >4m 4 months ago
704 # ~d <4d bookmarks defined/last used less than 4 days ago
705 # ~d <4m 4 months ago
706 # ~d q1 bookmarks defined/last used in the first quarter
711 # returns all bookmarks with "eigen" in the description or the
712 # URL, tagged as :ruby, used/defined in the last 3 months
714 # There are also some commands that apply to the current list
715 # (they will only be recognized as commands if you don't enter
716 # anything else on the same line):
718 # !o open all the bookmarks
722 # :blog<enter> to select all the bookmarks tagged as :blog
723 # !o<enter> to open them all
726 class BookmarkManager
727 Bookmark = Struct.new(:description, :url, :tags, :date)
729 def initialize(filename)
732 @deleted_bookmarks = {}
739 IO.foreach(@filename) do |line|
741 desc, url, tags, date = line.chomp.split(/\t/).map{|x| x.strip}
742 if @bookmarks[url].nil? && !@deleted_bookmarks[url]
743 tags = (tags || "").split(/\s/)
745 date = Time.rfc822(date)
749 bm = Bookmark.new(desc, url, tags, date)
752 LOGGER.warn "Loading bookmark #{url.inspect}: already loaded, skipping."
755 # keep parsing other lines
761 # Returns the bookmark corresponding to the given URL, or nil.
763 self.load unless @loaded
767 # Returns true if it was a new bookmark, false if a bookmark
768 # with the same URL exists.
769 # If a bookmark with the same URL already exists it is replaced.
770 def add_bookmark(desc, url, tags, date)
771 self.load unless @loaded
772 ret = @bookmarks.include? url
773 bm = Bookmark.new(desc, url, tags, date)
775 @deleted_bookmarks.delete(url)
779 # Remove bookmark matching the given URL if it exists.
780 # Returns true if bookmark was removed otherwise returns false.
781 def remove_bookmark(url)
782 @deleted_bookmarks[url] = @bookmarks[url] if @bookmarks.has_key?(url)
783 not @bookmarks.delete(url).nil?
787 self.load unless @loaded
791 # Saves the bookmarks to the specified filename. It tries to merge them with
792 # the list currently stored in the file. Bookmarks present in the later but
793 # missing in +self+ which were not explicitly deleted will also be added to the
795 def save!(destination = @filename)
797 tmpfile = @filename + "_tmp_#{Process.pid}"
798 File.open(tmpfile, "a") do |f|
799 @bookmarks.values.sort_by{|bm| bm.date}.reverse_each do |bm|
800 f.puts [bm.description, bm.url, bm.tags.join(" "), bm.date.rfc822].join("\t")
804 File.rename(tmpfile, destination) # atomic if on the same FS and fleh
809 IO.foreach(@filename) do |line|
810 desc, url, tags, date = line.chomp.split(/\t/).map{|x| x.strip}
811 unless @deleted_bookmarks[url] ||
813 @bookmarks[url].date >= date)
814 tags = (tags || "").split(/\s/)
816 date = Time.rfc822(date)
820 add_bookmark(desc, url, tags, date)
829 fh = File.open(@filename + ".lock", "a")
830 fh.flock(File::LOCK_EX)
831 @mutex.synchronize{ yield(self) }
833 fh.flock(File::LOCK_UN)
838 def satisfy_date_condition?(bookmark, condition)
841 when /^q1$/i : date.month >= 12 || date.month <= 4
842 when /^q2$/i : date.month >= 3 && date.month <= 7
843 when /^q3$/i : date.month >= 6 && date.month <= 10
844 when /^q4$/i : date.month >= 9 || date.month <= 1
845 when /^\d+$/ : date.year == condition.to_i
846 when /^\w+$/ : date.month - 1 == Time::RFC2822_MONTH_NAME.index(condition.capitalize)
847 when /^([><])(\d+)([md])/
848 sign, units, type = $1, $2.to_i, $3
849 multiplier = 3600 * 24
850 multiplier *= 30.4375 if type == 'm'
852 when '<': Time.new - date <= units * multiplier
853 when '>': Time.new - date >= units * multiplier
857 private :satisfy_date_condition?
859 def refine_selection(expression, choices=self.bookmarks)
860 expression = expression.strip
861 pieces = expression.split(/\s+/)
863 option_needed = false
866 when true: criteria.last << " #{x}"; option_needed = false
867 when false: criteria << x; option_needed = true if /^~\w/ =~ x
870 choices.select do |bm|
871 criteria.all? do |criterion|
873 when /~t\s+(\S+)/: Regexp.new($1) =~ bm.description
874 when /~u\s+(\S+)/: Regexp.new($1) =~ bm.url
875 when /~p\s+(\S+)/: Regexp.new($1) =~ bm.url[%r{^(\S+?):/},1]
876 when /~d\s+(\S+)/: satisfy_date_condition?(bm, $1)
877 when /:.+$/ : bm.tags.include?(criterion)
878 else bm.description.index(criterion) or bm.url.index(criterion)
885 BOOKMARK_FILE = File.join(ENV["HOME"], ".wmii-3", "bookmarks.txt")
886 BOOKMARK_REMOTE_FILE = BOOKMARK_FILE + ".remote"
887 BOOKMARK_AGENT = 'ruby-wmii #{WMIIRC_VERSION} (#{WMIIRC_RELEASE_DATE})'
889 MISSING_DELICIOUS_AUTH_MSG = <<EOF
890 Missing del.icio.us user/password.
891 You must set them in your wmiirc-config.rb as follows:
893 plugin_config["standard:bookmark"]["del.icio.us-user"] = 'username'
894 plugin_config["standard:bookmark"]["del.icio.us-password"] = 'password'
903 require 'resolv-replace'
905 Socket.do_not_reverse_lookup = true
907 Plugin.define "standard" do
908 author '"Mauricio Fernandez" <mfp@acm.org>'
910 DELICIOUS_ENCODING = 'UTF-8'
912 def perform_delicious_request(request, user, pass)
913 delicious_address = Resolv.new.getaddress 'api.del.icio.us'
914 https = Net::HTTP.new(delicious_address, 443)
918 req = Net::HTTP::Get.new(request, 'User-Agent' => BOOKMARK_AGENT)
919 req.basic_auth(user, pass)
920 https.request(req).body
924 def push_delicious_bookmark(bookmark, user, pass, shared = false, encoding = nil)
925 LOGGER.debug "Pushing to del.icio.us: #{bookmark.inspect}"
926 desc = encoding.nil? ? bookmark.description :
927 Iconv.conv(DELICIOUS_ENCODING, encoding, bookmark.description)
928 req_url = '/v1/posts/add?'
929 req_url << "url=#{CGI.escape(bookmark.url)}"
930 req_url << ";description=#{CGI.escape(desc)}"
931 tags = bookmark.tags.map{|x| x.gsub(/^:/,'')}.join(' ')
932 req_url << ";tags=#{CGI.escape(tags)}"
933 req_url << ";replace=yes;shared=#{shared ? 'yes' : 'no'}"
934 date = bookmark.date.clone.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
935 req_url << ";dt=#{CGI.escape(date)}"
937 perform_delicious_request(req_url, user, pass)
940 def delete_delicious_bookmark(url, user, pass)
941 LOGGER.debug "Deleting from del.icio.us: #{url.inspect}"
942 req_url = '/v1/posts/delete?'
943 req_url << "url=#{CGI.escape(url)}"
945 perform_delicious_request(req_url, user, pass)
948 def download_delicious_bookmarks(user, pass, mode = :full, encoding = nil)
949 require 'rexml/document'
952 LOGGER.debug "Downloading del.icio.us bookmarks, #{mode} mode"
955 req = '/v1/posts/all'
957 req = '/v1/posts/recent?count=100'
959 xml = perform_delicious_request(req, user, pass)
961 elements = REXML::Document.new(xml).elements
963 elements.each("posts/post") do |el|
965 desc, url, tags, time = %w[description href tag time].map{|x| el.attributes[x]}
966 desc = Iconv.conv(encoding, DELICIOUS_ENCODING, desc) unless encoding.nil?
967 desc = desc.gsub(/\s+/, " ").strip
968 year, month, day, hour, min, sec, = ParseDate.parsedate(time)
969 date = Time.utc(year, month, day, hour, min, sec, 0)
971 if tags == 'system:unfiled'
974 tags = tags.split(/\s+/).map{|x| ":#{x}"}
977 ret << [desc, url, tags, date]
978 rescue Iconv::IllegalSequence
979 LOGGER.error "download_delicious_bookmarks, #{url}: iconv error #{$!.class}."
981 LOGGER.error "download_delicious_bookmarks, #{url}: #{$!}."
988 def get_delicious_update_time(user, pass)
989 require 'rexml/document'
992 LOGGER.debug "Getting del.icio.us last update time"
994 req = '/v1/posts/update'
995 xml = perform_delicious_request(req, user, pass)
996 time = REXML::Document.new(xml).elements['update'].attributes['time']
997 year, month, day, hour, min, sec, = ParseDate.parsedate(time)
998 Time.utc(year, month, day, hour, min, sec, 0)
1001 def sync_delicious_bookmarks(wmii, mode, last_update_time = nil)
1003 config = wmii.plugin_config["standard:bookmark"]
1004 user = config["del.icio.us-user"]
1005 pass = config["del.icio.us-password"]
1006 shared = config.has_key?("del.icio.us-share") ? config["del.icio.us-share"] : false
1007 encoding = config["encoding"]
1009 if "#{user}".empty? or "#{pass}".empty?
1010 raise MISSING_DELICIOUS_AUTH_MSG
1013 LOGGER.info "Sync with del.icio.us, in #{mode} mode, last update time #{last_update_time ? last_update_time : "unknown"}."
1015 bm_manager = BookmarkManager.new(BOOKMARK_FILE)
1016 prev_bm_manager = BookmarkManager.new(BOOKMARK_REMOTE_FILE)
1017 unless File.exist?(BOOKMARK_REMOTE_FILE)
1018 # This is the first time we sync with del.icio.us.
1019 # Download all bookmarks.
1020 download_delicious_bookmarks(user, pass, :full, encoding).each do |desc, url, tags, date|
1021 prev_bm_manager.add_bookmark(desc, url, tags, date)
1023 prev_bm_manager.transaction { |bmanager| bmanager.save! }
1024 last_update_time = Time.now
1027 # Form lists of local bookmark changes.
1029 if mode == :bidirectional
1030 bm_manager.bookmarks.reject{|bm| bm.url !~ %r[^(http|https|ftp)://]}.each do |bm|
1031 if prev_bm_manager[bm.url].nil?
1032 local_changes[bm.url] = {:bm => bm, :change => :add}
1033 elsif prev_bm_manager[bm.url].description != bm.description ||
1034 prev_bm_manager[bm.url].tags.sort != bm.tags.sort
1035 local_changes[bm.url] = {:bm => bm, :change => :modify}
1038 prev_bm_manager.bookmarks.reject{|bm| bm.url !~ %r[^(http|https|ftp)://]}.each do |bm|
1039 if bm_manager[bm.url].nil?
1040 local_changes[bm.url] = {:change => :remove}
1045 # Form lists of remote bookmark changes.
1047 if last_update_time.nil? ||
1048 last_update_time < get_delicious_update_time(user, pass)
1049 # The last update time is unknown or
1050 # the remote bookarks were updated since last sync
1051 remote_bookmarks = {}
1052 download_delicious_bookmarks(user, pass, :full, encoding).each do |desc, url, tags, date|
1053 remote_bookmarks[url] = BookmarkManager::Bookmark.new(desc, url, tags, date)
1056 remote_bookmarks.values.each do |bm|
1057 if prev_bm_manager[bm.url].nil?
1058 remote_changes[bm.url] = {:bm => bm, :change => :add}
1059 elsif prev_bm_manager[bm.url].description != bm.description ||
1060 prev_bm_manager[bm.url].tags.sort != bm.tags.sort
1061 remote_changes[bm.url] = {:bm => bm, :change => :modify}
1064 prev_bm_manager.bookmarks.reject{|bm| bm.url !~ %r[^(http|https|ftp)://]}.each do |bm|
1065 if remote_bookmarks[bm.url].nil?
1066 remote_changes[bm.url] = {:change => :remove}
1071 (local_changes.keys + remote_changes.keys).uniq.each do |url|
1072 local_change = local_changes[url].nil? ? :none : local_changes[url][:change]
1073 remote_change = remote_changes[url].nil? ? :none : remote_changes[url][:change]
1075 if local_change == :none ||
1076 (remote_change == :remove && local_change != :add)
1077 # If no local changes - propagate remote changes.
1078 # If bookmark was deleted remotely, delete it locally
1079 # even if it was modified locally.
1080 LOGGER.debug "Bookmark #{url.inspect}: no local changes, propagating remote changes #{remote_change.inspect}."
1083 bm = remote_changes[url][:bm]
1084 bm_manager.add_bookmark(bm.description, bm.url, bm.tags, bm.date)
1086 bm_manager.remove_bookmark(url) unless local_change == :remove
1088 LOGGER.error "sync_delicious_bookmarks: unknown change type #{remote_change.inspect}."
1090 elsif remote_change == :none ||
1091 (local_change == :remove && remote_change != :add)
1092 # If no remote changes - propagate local changes.
1093 # If bookmark was deleted locally, delete it remotely
1094 # even if it was modified remotely.
1095 LOGGER.debug "Bookmark #{url.inspect}: no remote changes, propagating local changes #{local_change.inspect}."
1098 bm = local_changes[url][:bm]
1099 push_delicious_bookmark(bm, user, pass, shared, encoding)
1101 delete_delicious_bookmark(url, user, pass) unless remote_change == :remove
1103 LOGGER.error "sync_delicious_bookmarks: unknown change type #{local_change.inspect}."
1106 # If bookmark was modified locally and remotely,
1107 # propagate the newest version.
1108 local_bm = local_changes[url][:bm]
1109 remote_bm = remote_changes[url][:bm]
1110 if local_bm.date <= remote_bm.date
1111 LOGGER.debug "Bookmark #{url.inspect}: remote changes are newer, propagating remote changes #{remote_change}."
1112 bm_manager.add_bookmark(remote_bm.description,
1117 LOGGER.debug "Bookmark #{url.inspect}: local changes are newer, propagating local changes #{local_change}."
1118 push_delicious_bookmark(local_bm, user, pass, shared, encoding)
1122 bm_manager.transaction {|bmanager| bmanager.save!}
1123 prev_bm_manager.transaction {FileUtils.cp BOOKMARK_FILE, BOOKMARK_REMOTE_FILE}
1125 LOGGER.info "Done importing bookmarks from del.icio.us."
1128 def bookmark_url(wmii,url)
1135 url = URI.escape url
1138 LOGGER.error "Failed to bookmark #{URI.unescape(url)}: invalid URI."
1141 bookmark_protocols = wmii.plugin_config["standard:bookmark"]["protocols"]
1142 protocol_desc = bookmark_protocols[uri.scheme]
1143 if protocol_desc.nil?
1144 user_specified_proto = wmii.wmiimenu(bookmark_protocols.keys.sort).value
1145 user_specified_proto.strip!
1146 unless user_specified_proto.empty?
1147 uri.scheme = user_specified_proto
1148 # reparse the url after scheme change
1149 uri = URI.parse uri.to_s
1150 protocol_desc = bookmark_protocols[uri.scheme]
1153 unless protocol_desc.nil?
1154 # If url path is empty, set it to '/' to avoid duplication after
1155 # sync with del.icio.us.
1156 uri.path = '/' if uri.path.empty?
1157 title_variants = nil
1158 unless protocol_desc[:get_title].nil?
1161 title_variants = protocol_desc[:get_title].call(wmii,uri)
1163 rescue Timeout::Error
1164 LOGGER.warn "get_title timeout for URL #{uri.to_s}."
1166 LOGGER.warn "get_title exception for URL #{uri.to_s}: #{$!}"
1169 wmii.wmiimenu(title_variants) do |choice|
1170 tags = choice[/\s+(:\S+\s*)+$/] || ""
1171 description = choice[0..-1-tags.size].strip
1172 if description =~ /\S/
1173 bm_manager = BookmarkManager.new(BOOKMARK_FILE)
1174 LOGGER.info "Bookmarking #{uri.to_s}: #{description.inspect}, tags #{tags.inspect}"
1175 bm_manager.add_bookmark(description, uri.to_s, tags.strip.split(/\s+/), Time.new)
1176 bm_manager.transaction{|bm| bm.save!}
1180 LOGGER.error "Failed to bookmark #{uri.to_s}: unknown protocol #{uri.scheme.inspect}."
1184 def_settings("actions/internal") do |wmii|
1185 hash = wmii.plugin_config["standard:actions"]["internal"] ||= {}
1187 import_lambda = lambda do
1188 info = download_delicious_bookmarks(wmii.plugin_config["standard:bookmark"]["del.icio.us-user"],
1189 wmii.plugin_config["standard:bookmark"]["del.icio.us-password"],
1191 bm = BookmarkManager.new(BOOKMARK_FILE)
1192 info.each{|desc, url, tags, date| bm.add_bookmark(desc, url, tags, date) }
1194 File.delete(BOOKMARK_REMOTE_FILE) rescue nil
1195 prevbm = BookmarkManager.new(BOOKMARK_REMOTE_FILE)
1196 info.each{|desc, url, tags, date| prevbm.add_bookmark(desc, url, tags, date) }
1199 hash.update("del.icio.us-import" => import_lambda)
1204 "del.icio.us-sync" => lambda do
1205 mode = wmii.plugin_config["standard:bookmark"]["del.icio.us-mode"] || :unidirectional
1206 sync_delicious_bookmarks(wmii, mode)
1208 "bookmark-add" => lambda do |wmii|
1209 # if xclip is not available, the first one will be empty
1210 options = [`xclip -o -selection clipboard`, `wmiipsel`].reject {|x| x.strip.empty?}
1211 wmii.wmiimenu(options) do |url|
1212 unless url.strip.empty?
1213 url = "file://#{url}" unless url[%r{\A\w+:/}]
1214 bookmark_url(wmii, url)
1218 "bookmark-delete" => lambda do |wmii|
1219 bm_manager = BookmarkManager.new(BOOKMARK_FILE)
1220 delete_bookmark = lambda do |bm|
1221 if bm_manager.remove_bookmark bm.url
1222 bm_manager.transaction{ |bmanager| bmanager.save! }
1223 LOGGER.info "Delete bookmark #{bm.description.inspect} -> #{bm.url}."
1225 LOGGER.info "Could not delete bookmark #{bm.description.inspect} -> #{bm.url}."
1228 refine_choices = lambda do |bookmarks|
1229 options = bookmarks.sort_by{|x| x.description}.map do |x|
1230 "#{x.description} : #{x.url}"
1232 wmii.wmiimenu(options) do |condition|
1233 condition = condition.strip
1234 unless condition.empty?
1235 if condition == "!o"
1236 if bookmarks.size <=
1237 (limit = wmii.plugin_config["standard:bookmark"]["multiple-delete-limit"])
1238 bookmarks.each do |bm|
1239 delete_bookmark.call(bm)
1242 LOGGER.error "Tried to delete #{bookmarks.size} bookmarks at a time."
1243 LOGGER.error "Refusing since it's over multiple-delete-limit (#{limit})."
1245 elsif bm = bm_manager[condition[/ : (\S+)$/,1]]
1246 delete_bookmark.call(bm)
1248 choices = bm_manager.refine_selection(condition, bookmarks)
1249 refine_choices.call(choices) unless choices.empty?
1254 refine_choices.call(bm_manager.bookmarks)
1256 "bookmark-edit" => lambda do |wmii|
1257 bm_manager = BookmarkManager.new(BOOKMARK_FILE)
1258 refine_choices = lambda do |bookmarks|
1259 options = bookmarks.sort_by{|x| x.description}.map do |x|
1260 "#{x.description} : #{x.url}"
1262 wmii.wmiimenu(options) do |condition|
1263 condition = condition.strip
1264 unless condition.empty?
1265 if bm = bm_manager[condition[/ : (\S+)$/,1]]
1266 title_variants = [bm.description]
1268 (bm.description + ' ' + bm.tags.join(' ')) unless bm.tags.empty?
1269 wmii.wmiimenu(title_variants) do |choice|
1270 tags = choice[/\s+(:\S+\s*)+$/] || ""
1271 description = choice[0..-1-tags.size].strip
1272 if description =~ /\S/
1273 LOGGER.info "Edited bookmark #{description.inspect} -> #{bm.url}."
1274 bm_manager.add_bookmark(description, bm.url, tags.strip.split(/\s+/), Time.new)
1275 bm_manager.transaction{ |bmanager| bmanager.save! }
1279 choices = bm_manager.refine_selection(condition, bookmarks)
1280 refine_choices.call(choices) unless choices.empty?
1285 refine_choices.call(bm_manager.bookmarks)
1290 def_settings("bookmark/multiple-open") do |wmii|
1291 wmii.plugin_config["standard:bookmark"]["multiple-open-limit"] = 10
1294 def_settings("bookmark/multiple-delete") do |wmii|
1295 wmii.plugin_config["standard:bookmark"]["multiple-delete-limit"] = 30
1298 def_settings("bookmark/del.icio.us importer") do |wmii|
1299 wmii.plugin_config["standard:bookmark"]["refresh_period"] = 30
1301 sleep 20 # time to get the wmiirc-config.rb loaded
1302 if wmii.plugin_config["standard:bookmark"]["del.icio.us-user"] and
1303 wmii.plugin_config["standard:bookmark"]["del.icio.us-password"]
1305 mode = wmii.plugin_config["standard:bookmark"]["del.icio.us-mode"] || :unidirectional
1306 last_update_time = nil
1309 sync_delicious_bookmarks(wmii, mode, last_update_time)
1310 last_update_time = Time.now
1312 LOGGER.error "Error while sync'ing bookmarks."
1313 LOGGER.error $!.exception
1316 sleep(60 * wmii.plugin_config["standard:bookmark"]["refresh_period"])
1322 standard_bookmark_protocols = {
1324 :open_urls => lambda do |wmii,bms|
1325 browser = ENV["BROWSER"] || '/etc/alternatives/x-www-browser'
1326 urls = bms.map{|bm| bm[:uri].to_s}.join "' '"
1327 system "wmiisetsid #{browser} '#{urls}' &"
1329 :get_title => lambda do |wmii,uri|
1330 resolved_uri = uri.clone
1331 resolved_uri.host = Resolv.new.getaddress resolved_uri.host
1332 contents = open(resolved_uri.to_s, "Host" => uri.host, "User-Agent" => BOOKMARK_AGENT){|f| f.read}
1333 title = CGI.unescapeHTML((contents[%r{title>(.*)</title>}im, 1] || "").strip).gsub(/&[^;]+;/, "")
1334 title.gsub!(/\s+/, " ")
1335 [title, title.downcase, title.capitalize]
1339 :open_urls => lambda do |wmii,bms|
1340 term = wmii.plugin_config["standard"]["x-terminal-emulator"] || "xterm"
1344 ssh_host = "#{uri.user}@" + ssh_host unless uri.user.nil?
1345 ssh_port = "-p #{uri.port}" unless uri.port.nil?
1346 system "wmiisetsid #{term} -T '#{bm[:bm].url}' -e 'ssh #{ssh_host} #{ssh_port} || read' &"
1349 :get_title => lambda do |wmii,uri|
1351 title = "#{uri.user}@" + title unless uri.user.nil?
1352 title << ":#{uri.port.to_s}" unless uri.port.nil?
1357 standard_bookmark_protocols['https'] = standard_bookmark_protocols['http']
1358 standard_bookmark_protocols['ftp'] =
1360 :open_urls => standard_bookmark_protocols['http'][:open_urls],
1361 :get_title => standard_bookmark_protocols['ssh'][:get_title]
1365 def_settings("bookmark/protocols") do |wmii|
1366 wmii.plugin_config["standard:bookmark"]["protocols"] ||= {}
1367 wmii.plugin_config["standard:bookmark"]["protocols"] =
1368 standard_bookmark_protocols.merge wmii.plugin_config["standard:bookmark"]["protocols"]
1371 binding("bookmark", "MODKEY-Shift-b") do |wmii,|
1373 url = `wmiipsel`.strip
1374 url = "file://#{url}" unless url[%r{\A\w+:/}]
1375 bookmark_url(wmii, url)
1379 binding("bookmark-open", "MODKEY-Shift-Control-b") do |wmii,|
1380 bm_manager = BookmarkManager.new(BOOKMARK_FILE)
1381 open_bookmark = lambda do |bms|
1382 bookmark_protocols = wmii.plugin_config["standard:bookmark"]["protocols"]
1385 LOGGER.debug "Opening bookmark #{bm.description.inspect} -> #{bm.url}."
1387 uri = URI.parse bm.url
1389 LOGGER.error "Failed to open #{bm.url}: invalid URI. Corrupted bookmarks.txt?"
1392 protocol_desc = bookmark_protocols[uri.scheme]
1393 unless protocol_desc.nil?
1394 bm_hash[protocol_desc] = (bm_hash[protocol_desc].to_a << {:bm => bm, :uri => uri})
1396 LOGGER.error "Failed to open #{bm.url}: unknown protocol #{uri.scheme.inspect}."
1399 bm_hash.each do |proto,bms|
1400 proto[:open_urls].call(wmii,bms)
1403 refine_choices = lambda do |bookmarks|
1404 options = bookmarks.sort_by{|x| x.description}.map do |x|
1405 "#{x.description} : #{x.url}"
1407 wmii.wmiimenu(options) do |condition|
1408 condition = condition.strip
1409 unless condition.empty?
1410 if condition == "!o"
1411 if bookmarks.size <=
1412 (limit = wmii.plugin_config["standard:bookmark"]["multiple-open-limit"])
1413 bookmarks.each do |bm|
1416 bm_manager.transaction{|bm| bm.save!}
1417 open_bookmark.call(bookmarks)
1419 LOGGER.error "Tried to open #{bookmarks.size} bookmarks at a time."
1420 LOGGER.error "Refusing since it's over multiple-open-limit (#{limit})."
1422 elsif bm = bm_manager[condition[/ : (\S+)$/,1]]
1424 bm_manager.transaction{ bm_manager.save! }
1425 open_bookmark.call([bm])
1427 choices = bm_manager.refine_selection(condition, bookmarks)
1428 refine_choices.call(choices) unless choices.empty?
1433 refine_choices.call(bm_manager.bookmarks)