use monospace font for comments
[srid.dotfiles.git] / dot-wmii-3 / plugins / standard-plugin.rb
blobac24a403b44d9a5ee0e1bf29c6f81697c5a298e0
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
8
9 # This file must be placed in $HOME/.wmii-3/plugins.
10
11 # It will be overwritten when upgrading ruby-wmii, so modifications will be
12 # lost!
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 ...
20 #     end
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 
24 #       use_binding "retag"
25 #       use_bar_applet "volume"
26 #   in the   from "standard" do .... end   area, and add
27 #     from "my-address@isp.com" do
28 #       use_binding "retag"
29 #       use_bar_applet "volume"
30 #     end
32 # Read the top of wmiirc or type Alt-a -> config-help for more information on
33 # plugins.
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>'
43   #{{{ Volume control
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|
48       if mixers.empty?
49         bar.data = "VOL OFF"
50         return
51       end
52       sign = increment < 0 ? "-" : "+"
53       status = ''
54       mixers.reverse.each do |mixer| # show status of first mixer in list
55         status = `amixer set "#{mixer},0" #{increment.abs}#{sign}`
56       end
57       volume = status [/\[(\d+%)\]/]
58       volume = "OFF" if status[/\[off\]/]
59       bar.data = "VOL #{volume}"
60     end
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
71       end
72       system "wmiisetsid #{term} -e alsamixer &"
73     end
74     bar.on_click(MOUSE_BUTTON_RIGHT) do
75       action = case `amixer get "#{mixers.first},0"`
76       when /\[off\]/: 'unmute'
77       when /\[on\]/ : 'mute'
78       end
79       mixers.each do |mixer|
80         `amixer set "#{mixer},0" #{action}`
81       end
82       update_volume[0]
83     end
84   end
86   #{{{ Modal keybindings: raw/normal modes.
87   bar_applet("mode", 980) do |wmii, bar|
88     raw_mode = false
89     saved_keys = nil
90     keys = wmii.plugin_config["standard:mode"]["mode_toggle_keys"] || ["MODKEY2-space"]
91     h = wmii.on_key(*keys) do
92       case raw_mode
93       when true
94         wmii.write("/def/keys", saved_keys)
95         raw_mode = false
96         bar.data = "-N-"
97         LOGGER.info "Entering NORMAL mode."
98       when false
99         saved_keys = wmii.read("/def/keys")
100         wmii.write("/def/keys", h.key)
101         raw_mode = true
102         bar.data = "-R-"
103         LOGGER.info "Entering RAW mode."
104       end
105     end
106     bar.data = "-N-"
107   end
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"
114         bar.data = "SEL"
115       else
116         bar.data = "   "
117       end
118     end
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
123       when /:tmp/
124         wmii.retag_curr_client("-#{curr}")
125       else
126         tmpview = "#{curr}:tmp"
127         if old_tags.include? tmpview
128           wmii.retag_curr_client("-#{tmpview}")
129           bar.data = "   "
130         else
131           wmii.retag_curr_client("+#{tmpview}")
132           bar.data = "SEL"
133         end
134       end
135     end
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/,"")
140       wmii.view view
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
146       end
147       bar.data = "   "
148     end
149   end
151 # {{{ Dictionary
152   bar_applet("dict", 880, "DICT") do |wmii, bar|
153     dict_ask_and_define = lambda do
154       Thread.new 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"
159       end
160     end
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" }
164   end
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 -'
179     warned_low = false
180     warned_critical = false
181     Thread.new do
182       loop do
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
195             end
196           elsif batt_state == "discharging" && batt_percent <= low
197             unless warned_low
198               LOGGER.info "Warning about low battery."
199               system("wmiisetsid #{low_action} &")
200               warned_low = true
201             end
202           else
203             warned_low = false
204             warned_critical = false
205           end
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}"
212           bar.data = text
213         else
214           bar.data = "N/A"
215         end
216         sleep 2
217       end
218     end
219   end
221   # {{{ MPD Bar
222   # Originally  by Wael Nasreddine <wael@phoenixlinux.org>.
223   bar_applet("mpd", 100) do |wmii, bar|
224     require 'mpd'
225     mpd_do_action = lambda do |action, *args|
226       Thread.new do
227         begin
228           mpd = MPD.new
229           r = mpd.__send__(action, *args)
230           LOGGER.info "MPD #{action}"
231           r
232         ensure
233           mpd.close
234         end
235       end
236     end
237     mpdserv = MPD.new
238     update_bar = lambda do
239       mpdserv_status = mpdserv.status["state"]
240       case mpdserv_status
241       when 'play' : text = ">>: "; show_info = true
242       when 'pause': text = "||: "; show_info = true
243       else show_info = false
244       end
245       if show_info
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"
251       end
252     end
253     # Initialize MPD status
254     Thread.new do
255       loop{ begin; update_bar.call; rescue Exception; end; sleep 1 }
256     end
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
260       Thread.new do
261         begin
262           mpd = MPD.new
263           mpdserv_status = mpd.status
264         ensure 
265           mpd.close rescue nil
266         end
267         case mpdserv_status["state"]
268         when "play":           mpd_do_action[:pause]
269         when "pause", "stop" : mpd_do_action[:play]
270         end
271       end
272     end
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
278       end
279       wmii.write("/view/ctl", "select toggle")
280       term = wmii.plugin_config["standard"]["x-terminal-emulator"] || "xterm"
281       system "wmiisetsid #{term} -e ncmpc &"
282     end
283   end
285 # # {{{ CPU info
286   bar_applet("cpuinfo", 800) do |wmii, bar|
287     Thread.new do
288       loop do
289         cpuinfo = IO.readlines("/proc/cpuinfo").grep(/MHz/).first.split[-1].sub(/\..*$/,'')
290         bar.data = cpuinfo.chomp + " Mhz"
291         sleep 5
292       end
293     end
294   end
296 # {{{ Status bar
297   bar_applet("status", 0, "STATUS BAR --- init") do |wmii, bar|
298     Thread.new do
299       text_proc = wmii.plugin_config["standard:status"]["text_proc"]
300       unless text_proc
301         currload = nil
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}" }
304       end
305       loop do
306         bar.data = text_proc.call
307         sleep(wmii.plugin_config["standard:status"]["refresh_period"] || 1)
308       end
309     end
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
323       case button.to_i
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
327       when MOUSE_SCROLL_UP
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])
331       end
332     end
333   end
335   binding("dict-lookup", "MODKEY-Control-d") do |wmii,|
336     Thread.new do
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"
341     end
342   end
344   binding("lock-x", "MODKEY-Control-l") do |wmii,|
345     system "gnome-screensaver-command -l"
346   end
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}' &"
356       end
357     end,
358     "google" => lambda do |wmii, *selection|
359       require 'cgi'
360       if selection && !selection.empty?
361         selection = CGI.escape(selection.join(" "))
362       else
363         selection = CGI.escape(%!#{`wmiipsel`.strip}!)
364       end
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}' &"
369       end
370     end,
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 &")
375     end,
376     "rename-view" => lambda do |wmii, *args|
377       unless /./ =~ (new_name = args[0].to_s)
378         new_name = wmii.wmiimenu([]).value  # blocking, OK
379       end
380       old = wmii.curr_view
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))
385       end
386       wmii.view new_name
387     end,
388     "quit" => lambda do |wmii|
389       wmii.write "/event", "Bye"
390       wmii.write "/ctl", "quit"
391     end,
392     "config-help" => lambda do |wmii|
393       IO.popen("xmessage -file -", "w"){|f| f.puts WMIIRC_HELP_MESSAGE; f.close_write }
394     end
395   }
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
401   end
402   
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+/)
412       cmd = choices.first
413       if internal_actions.has_key? cmd
414         internal_actions[cmd].call(wmii, *choices[1..-1]) if internal_actions[cmd]
415       else
416         system("wmiisetsid #{WMIIRC_HOME}/#{choice} &") if /^\s*$/ !~ choice
417       end
418     end
419     # use result.value to record the choice in the current process
420     Thread.new do
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]
426       end
427     end
428   end
430 #{{{ program lists
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}
443       @__program_list
444     end
445   }
447   def_settings "programs/histories" do |wmii|
448     wmii.plugin_config["standard:programs"]["histories"] = {}
449   end
451   def run_program(wmii, *lists)
452     programs = {}
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
459     if /\S/ =~ choice
460       s = choice.clone
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]
466     else
467       return nil
468     end
469   end
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] = []}
474   end
475   
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]
480     history.delete entry
481     history.unshift entry
482     LOGGER.debug "plugin/programs: history #{entry.inspect}"
483     history.replace history[0, history_size]
484   end
486   def_settings "programs/lists" do |wmii|
487     wmii.plugin_config["standard:programs"]["lists"] ||= {}
488   end
490 #{{{ programs (w/ history)
491   binding("execute-program", "MODKEY-Shift-p") do |wmii,|
492     Thread.new do 
493       entry, history_name = run_program(wmii, standard_programs, wmii.plugin_config["standard:programs"]["lists"])
494       record_history(wmii, entry, history_name) if entry
495     end
496   end
498 # {{{ Run program with given tag
499   binding("execute-program-with-tag", "MODKEY-Control-y") do |wmii,|
500     rd, wr = IO.pipe
501     result = wmii.wmiimenu(wmii.views_intellisort) do |tag|
502       begin
503         rd.close
504         if /^\s*$/ !~ tag
505           choice, history_name = run_program(wmii, standard_programs, 
506                                              wmii.plugin_config["standard:programs"]["lists"])
507           Marshal.dump([choice, history_name], wr)
508         else
509           Marshal.dump(nil, wr)
510         end
511       ensure
512         wr.close rescue nil
513       end
514     end
515     wr.close
516     # using result.value to perform the view switch in the current process so
517     # that the transition table can be updated
518     Thread.new do 
519       begin
520         tag = result.value
521         if /^\s*$/ !~ tag
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
526             wmii.view tag
527           end
528         end
529         choice, history_name = Marshal.load(rd.read)
530         if choice
531           record_history(wmii, choice, history_name)
532         else # empty string, spaces only, etc
533           wmii.unregister handler
534         end
535       ensure
536         rd.close rescue nil
537       end
538     end
539   end
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
544 #    ALT-1  => 1
545 #    ALT-3  => 3
546 #    ALT-5 => mail
547 #    ALT-6 => web
548   (0..9).each do |key|
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)
554         wmii.view(key)
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]
558       end
559     end
560   end
562 #{{{ Move to given view, with intelligent history
563   binding("tag-jump", "MODKEY-t") do |wmii,|
564     Thread.new do
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
567     end
568   end
570   binding("retag", "MODKEY-Shift-t") do |wmii,|
571     wmii.wmiimenu(wmii.views_intellisort){|new_tag| wmii.retag_curr_client(new_tag) }
572   end
573   binding("retag-jump", "MODKEY-Shift-r") do |wmii,|
574     rd, wr = IO.pipe
575     wmii.wmiimenu(wmii.views_intellisort) do |new_tag|
576       rd.close
577       wmii.retag_curr_client(new_tag)
578       wr.puts new_tag
579       wr.close
580     end
581     wr.close
582     Thread.new do
583       new_tag = rd.gets
584       rd.close
585       wmii.view new_tag[/(?![+-]).*/]
586     end
587   end
589   binding("namespace-retag", "MODKEY2-Shift-t") do |wmii,|
590     wmii.wmiimenu(wmii.views){|new_tag| wmii.retag_curr_client_ns(new_tag) }
591   end
592   binding("namespace-retag-jump", "MODKEY2-Shift-r") do |wmii,|
593     rd, wr = IO.pipe
594     result = wmii.wmiimenu(wmii.views) do |new_tag|
595       rd.close
596       wmii.retag_curr_client_ns(new_tag)
597       wr.puts new_tag
598       wr.close
599     end
600     wr.close
601     Thread.new do
602       subtag = rd.gets
603       rd.close
604       wmii.view "#{wmii.curr_view[/[^:]+/]}:#{subtag[/(?![+-]).*/]}"
605     end
606   end
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 }
612       end
613     end
614   end
615 # Retag as specified numeric tag if it exists, or 
616 # (N-last_numeric_tag)th non-numeric tag.
617   (0..9).each do |key|
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]
628       else
629         break
630       end
631       LOGGER.info "Retagging #{curr_tags.inspect} => #{new_tags.inspect}"
632       wmii.set_curr_client_tags(new_tags)
633     end
634   end
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 }
642         unless new_view.nil?
643           new_tags = curr_tags.reject {|view| view == new_view }
644           curr_index = new_tags.index wmii.curr_view
645           if curr_index.nil?
646             LOGGER.error "curr_index is nil in letter-retag"
647             return
648           end
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)
652         end
653       end
654     end
655   end
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]
662   end
663   binding("move-next", "MODKEY-Control-DOWN", "MODKEY-period") do |wmii,|
664     wmii.view  wmii.views[wmii.curr_view_index+1] || wmii.views[0]
665   end
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]
671     wmii.view dest
672   end
673   binding("namespace-move-prev", "MODKEY2-Shift-UP", "MODKEY2-comma") do |wmii,|
674     move_within_namespace.call(wmii, -1)
675   end
676   binding("namespace-move-next", "MODKEY2-Shift-DOWN", "MODKEY2-period") do |wmii,|
677     move_within_namespace.call(wmii, +1)
678   end
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
710 #                             (q1..q4)
711 #                 
712 #                 Example:
713 #                   eigen :ruby ~d <3m
714 #                 returns all bookmarks with "eigen" in the description or the
715 #                 URL, tagged as :ruby, used/defined in the last 3 months
716 #                 
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
723 #                 Usage example:
724 #                 
725 #                  :blog<enter>    to select all the bookmarks tagged as :blog
726 #                  !o<enter>       to open them all
727 require 'time'
728 require 'thread'
729 class BookmarkManager
730   Bookmark = Struct.new(:description, :url, :tags, :date)
732   def initialize(filename)
733     @filename = filename
734     @bookmarks = {}
735     @deleted_bookmarks = {}
736     @loaded = false
737     @mutex = Mutex.new
738   end
740   def load
741     @bookmarks.clear
742     IO.foreach(@filename) do |line|
743       begin
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/)
747           begin
748             date = Time.rfc822(date)
749           rescue
750             date = Time.new
751           end
752           bm = Bookmark.new(desc, url, tags, date)
753           @bookmarks[url] = bm
754         else
755           LOGGER.warn "Loading bookmark #{url.inspect}: already loaded, skipping."
756         end
757       rescue Exception
758         # keep parsing other lines
759       end
760     end rescue nil
761     @loaded = true
762   end
764   # Returns the bookmark corresponding to the given URL, or nil.
765   def [](url)
766     self.load unless @loaded
767     @bookmarks[url]
768   end
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)
777     @bookmarks[url] = bm
778     @deleted_bookmarks.delete(url)
779     not ret
780   end
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?
787   end
789   def bookmarks
790     self.load unless @loaded
791     @bookmarks.values
792   end
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
797   # list and saved.
798   def save!(destination = @filename)
799       merge!
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")
804         end
805         f.sync
806       end
807       File.rename(tmpfile, destination) # atomic if on the same FS and fleh
808       self.load
809   end
810   
811   def merge!
812     IO.foreach(@filename) do |line|
813       desc, url, tags, date = line.chomp.split(/\t/).map{|x| x.strip}
814       unless @deleted_bookmarks[url] ||
815           (@bookmarks[url] &&
816            @bookmarks[url].date >= date)
817         tags = (tags || "").split(/\s/)
818         begin
819           date = Time.rfc822(date)
820         rescue
821           date = Time.new
822         end
823         add_bookmark(desc, url, tags, date)
824       end
825     end rescue nil
826   end
827   private :merge!
830   def transaction
831     begin
832       fh = File.open(@filename + ".lock", "a")
833       fh.flock(File::LOCK_EX)
834       @mutex.synchronize{ yield(self) }
835     ensure
836       fh.flock(File::LOCK_UN)
837       fh.close
838     end
839   end
841   def satisfy_date_condition?(bookmark, condition)
842     date = bookmark.date
843     case 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'
854       case sign
855       when '<':  Time.new - date <= units * multiplier
856       when '>':  Time.new - date >= units * multiplier
857       end
858     end
859   end
860   private :satisfy_date_condition?
861   
862   def refine_selection(expression, choices=self.bookmarks)
863     expression = expression.strip
864     pieces = expression.split(/\s+/)
865     criteria = []
866     option_needed = false
867     pieces.each do |x|
868       case option_needed
869       when true:    criteria.last << " #{x}"; option_needed = false
870       when false:   criteria << x; option_needed = true if /^~\w/ =~ x 
871       end
872     end
873     choices.select do |bm|
874       criteria.all? do |criterion|
875         case 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)
882         end
883       end
884     end
885   end
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'
900 require 'uri'
901 require 'open-uri'
902 require 'net/http'
903 require 'net/https'
904 require 'cgi'
905 require 'iconv'
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)
918     https.use_ssl = true
920     xml = https.start do
921       req = Net::HTTP::Get.new(request, 'User-Agent' => BOOKMARK_AGENT)
922       req.basic_auth(user, pass)
923       https.request(req).body
924     end
925   end
926   
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)
941   end
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)
949   end
951   def download_delicious_bookmarks(user, pass, mode = :full, encoding = nil)
952     require 'rexml/document'
953     require 'parsedate'
954     require 'time'
955     LOGGER.debug "Downloading del.icio.us bookmarks, #{mode} mode"
956     case mode
957     when :full
958       req = '/v1/posts/all'
959     when :partial
960       req = '/v1/posts/recent?count=100'
961     end
962     xml = perform_delicious_request(req, user, pass)
964     elements = REXML::Document.new(xml).elements
965     ret = []
966     elements.each("posts/post") do |el|
967       begin
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)
973         date.localtime
974         if tags == 'system:unfiled'
975           tags = []
976         else
977           tags = tags.split(/\s+/).map{|x| ":#{x}"}
978         end
980         ret << [desc, url, tags, date]
981       rescue Iconv::IllegalSequence
982         LOGGER.error "download_delicious_bookmarks, #{url}: iconv error #{$!.class}."
983       rescue
984         LOGGER.error "download_delicious_bookmarks, #{url}: #{$!}."
985       end
986     end
988     ret
989   end
991   def get_delicious_update_time(user, pass)
992     require 'rexml/document'
993     require 'parsedate'
994     require 'time'
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)
1002   end
1004   def sync_delicious_bookmarks(wmii, mode, last_update_time = nil)
1005     require 'fileutils'
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
1014     end
1015     
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)
1025       end
1026       prev_bm_manager.transaction { |bmanager| bmanager.save! }
1027       last_update_time = Time.now
1028     end
1030     # Form lists of local bookmark changes.
1031     local_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}
1039         end
1040       end
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}
1044         end
1045       end
1046     end
1048     # Form lists of remote bookmark changes.
1049     remote_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)
1057       end
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}
1065         end
1066       end
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}
1070         end
1071       end
1072     end
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}."
1084         case remote_change
1085         when :modify, :add
1086           bm = remote_changes[url][:bm]
1087           bm_manager.add_bookmark(bm.description, bm.url, bm.tags, bm.date)
1088         when :remove
1089           bm_manager.remove_bookmark(url) unless local_change == :remove
1090         else
1091           LOGGER.error "sync_delicious_bookmarks: unknown change type #{remote_change.inspect}."
1092         end
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}."
1099         case local_change
1100         when :modify, :add
1101           bm = local_changes[url][:bm]
1102           push_delicious_bookmark(bm, user, pass, shared, encoding)
1103         when :remove
1104           delete_delicious_bookmark(url, user, pass) unless remote_change == :remove
1105         else
1106           LOGGER.error "sync_delicious_bookmarks: unknown change type #{local_change.inspect}."
1107         end
1108       else
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,
1116                                   remote_bm.url,
1117                                   remote_bm.tags,
1118                                   remote_bm.date)
1119         else
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)
1122         end
1123       end
1124     end
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."
1129   end
1131   def bookmark_url(wmii,url)
1132     escaped = false
1133     begin
1134       uri = URI.parse url
1135     rescue
1136       unless escaped
1137         escaped = true
1138         url = URI.escape url
1139         retry
1140       end
1141       LOGGER.error "Failed to bookmark #{URI.unescape(url)}: invalid URI."
1142       return
1143     end
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]
1154       end
1155     end
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?
1162         begin
1163           timeout 5 do
1164             title_variants = protocol_desc[:get_title].call(wmii,uri)
1165           end
1166         rescue Timeout::Error
1167           LOGGER.warn "get_title timeout for URL #{uri.to_s}."
1168         rescue
1169           LOGGER.warn "get_title exception for URL #{uri.to_s}: #{$!}"
1170         end
1171       end
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!}
1180         end
1181       end
1182     else
1183       LOGGER.error "Failed to bookmark #{uri.to_s}: unknown protocol #{uri.scheme.inspect}."
1184     end
1185   end
1186   
1187   def_settings("actions/internal") do |wmii|
1188     hash = wmii.plugin_config["standard:actions"]["internal"] ||= {}
1189 =begin
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"],
1193                                           :full)
1194       bm = BookmarkManager.new(BOOKMARK_FILE)
1195       info.each{|desc, url, tags, date| bm.add_bookmark(desc, url, tags, date) }
1196       bm.save!
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) }
1200       prevbm.save!
1201     end
1202     hash.update("del.icio.us-import" => import_lambda)
1203 =end
1205     hash.update(
1206     {
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)
1210       end,
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)
1218           end
1219         end
1220       end,
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}."
1227           else
1228             LOGGER.info "Could not delete bookmark #{bm.description.inspect} -> #{bm.url}."
1229           end
1230         end
1231         refine_choices = lambda do |bookmarks|
1232           options = bookmarks.sort_by{|x| x.description}.map do |x| 
1233             "#{x.description} : #{x.url}"
1234           end
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)
1243                   end
1244                 else
1245                   LOGGER.error "Tried to delete #{bookmarks.size} bookmarks at a time."
1246                   LOGGER.error "Refusing since it's over multiple-delete-limit (#{limit})."
1247                 end
1248               elsif bm = bm_manager[condition[/ : (\S+)$/,1]]
1249                 delete_bookmark.call(bm)
1250               else
1251                 choices = bm_manager.refine_selection(condition, bookmarks)
1252                 refine_choices.call(choices) unless choices.empty?
1253               end
1254             end
1255           end
1256         end
1257         refine_choices.call(bm_manager.bookmarks)
1258       end,
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}"
1264           end
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]
1270                 title_variants <<
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! }
1279                   end
1280                 end
1281               else
1282                 choices = bm_manager.refine_selection(condition, bookmarks)
1283                 refine_choices.call(choices) unless choices.empty?
1284               end
1285             end
1286           end
1287         end
1288         refine_choices.call(bm_manager.bookmarks)
1289       end
1290     })
1291   end
1293   def_settings("bookmark/multiple-open") do |wmii|
1294     wmii.plugin_config["standard:bookmark"]["multiple-open-limit"] = 10
1295   end
1297   def_settings("bookmark/multiple-delete") do |wmii|
1298     wmii.plugin_config["standard:bookmark"]["multiple-delete-limit"] = 30
1299   end
1301   def_settings("bookmark/del.icio.us importer") do |wmii|
1302     wmii.plugin_config["standard:bookmark"]["refresh_period"] = 30
1303     Thread.new do
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
1310         loop do
1311           begin
1312             sync_delicious_bookmarks(wmii, mode, last_update_time)
1313             last_update_time = Time.now
1314           rescue Exception
1315             LOGGER.error "Error while sync'ing bookmarks."
1316             LOGGER.error $!.exception
1317             puts $!.backtrace
1318           end
1319           sleep(60 * wmii.plugin_config["standard:bookmark"]["refresh_period"])
1320         end
1321       end
1322     end
1323   end
1325   standard_bookmark_protocols = {
1326     'http' => {
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}' &"
1331       end,
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]
1339       end
1340     },
1341     'ssh' => {
1342       :open_urls => lambda do |wmii,bms|
1343         term = wmii.plugin_config["standard"]["x-terminal-emulator"] || "xterm"
1344         bms.each do |bm|
1345           uri = bm[:uri]
1346           ssh_host = uri.host
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' &"
1350         end
1351       end,
1352       :get_title => lambda do |wmii,uri|
1353         title = uri.host
1354         title = "#{uri.user}@" + title unless uri.user.nil?
1355         title << ":#{uri.port.to_s}" unless uri.port.nil?
1356         title
1357       end
1358     }
1359   }
1360   standard_bookmark_protocols['https'] = standard_bookmark_protocols['http']
1361   standard_bookmark_protocols['ftp'] =
1362     {
1363       :open_urls => standard_bookmark_protocols['http'][:open_urls],
1364       :get_title => standard_bookmark_protocols['ssh'][:get_title]
1365     }
1366   
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"]
1372   end
1374   binding("bookmark", "MODKEY-Shift-b") do |wmii,|
1375     Thread.new do
1376       url = `wmiipsel`.strip
1377       url = "file://#{url}" unless url[%r{\A\w+:/}]
1378       bookmark_url(wmii, url)
1379     end
1380   end
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"]
1386       bm_hash = {}
1387       bms.each do |bm|
1388         LOGGER.debug "Opening bookmark #{bm.description.inspect} -> #{bm.url}."
1389         begin
1390           uri = URI.parse bm.url
1391         rescue
1392           LOGGER.error "Failed to open #{bm.url}: invalid URI. Corrupted bookmarks.txt?"
1393           return
1394         end
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})
1398         else
1399           LOGGER.error "Failed to open #{bm.url}: unknown protocol #{uri.scheme.inspect}."
1400         end
1401       end
1402       bm_hash.each do |proto,bms|
1403         proto[:open_urls].call(wmii,bms)
1404       end
1405     end
1406     refine_choices = lambda do |bookmarks|
1407       options = bookmarks.sort_by{|x| x.description}.map do |x| 
1408         "#{x.description} : #{x.url}"
1409       end
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|
1417                 bm.date = Time.new
1418               end
1419               bm_manager.transaction{|bm| bm.save!}
1420               open_bookmark.call(bookmarks)
1421             else
1422               LOGGER.error "Tried to open #{bookmarks.size} bookmarks at a time."
1423               LOGGER.error "Refusing since it's over multiple-open-limit (#{limit})."
1424             end
1425           elsif bm = bm_manager[condition[/ : (\S+)$/,1]]
1426             bm.date = Time.new
1427             bm_manager.transaction{ bm_manager.save! }
1428             open_bookmark.call([bm])
1429           else
1430             choices = bm_manager.refine_selection(condition, bookmarks)
1431             refine_choices.call(choices) unless choices.empty?
1432           end
1433         end
1434       end
1435     end
1436     refine_choices.call(bm_manager.bookmarks)
1437   end