[project @ sridhar.ratna@gmail.com-20070917031430-uqleoxeqf05ucn4f]
[srid.dotfiles.git] / dot-wmii-3 / plugins / standard-plugin.rb
blob0c69720c001030f15daf3e85ed880e0bbf20ac83
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     LOGGER.debug "dict-lookup called!!!"
337     Thread.new do
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"
342     end
343   end
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}' &"
353       end
354     end,
355     "google" => lambda do |wmii, *selection|
356       require 'cgi'
357       if selection && !selection.empty?
358         selection = CGI.escape(selection.join(" "))
359       else
360         selection = CGI.escape(%!#{`wmiipsel`.strip}!)
361       end
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}' &"
366       end
367     end,
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 &")
372     end,
373     "rename-view" => lambda do |wmii, *args|
374       unless /./ =~ (new_name = args[0].to_s)
375         new_name = wmii.wmiimenu([]).value  # blocking, OK
376       end
377       old = wmii.curr_view
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))
382       end
383       wmii.view new_name
384     end,
385     "quit" => lambda do |wmii|
386       wmii.write "/event", "Bye"
387       wmii.write "/ctl", "quit"
388     end,
389     "config-help" => lambda do |wmii|
390       IO.popen("xmessage -file -", "w"){|f| f.puts WMIIRC_HELP_MESSAGE; f.close_write }
391     end
392   }
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
398   end
399   
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+/)
409       cmd = choices.first
410       if internal_actions.has_key? cmd
411         internal_actions[cmd].call(wmii, *choices[1..-1]) if internal_actions[cmd]
412       else
413         system("wmiisetsid #{WMIIRC_HOME}/#{choice} &") if /^\s*$/ !~ choice
414       end
415     end
416     # use result.value to record the choice in the current process
417     Thread.new do
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]
423       end
424     end
425   end
427 #{{{ program lists
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}
440       @__program_list
441     end
442   }
444   def_settings "programs/histories" do |wmii|
445     wmii.plugin_config["standard:programs"]["histories"] = {}
446   end
448   def run_program(wmii, *lists)
449     programs = {}
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
456     if /\S/ =~ choice
457       s = choice.clone
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]
463     else
464       return nil
465     end
466   end
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] = []}
471   end
472   
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]
477     history.delete entry
478     history.unshift entry
479     LOGGER.debug "plugin/programs: history #{entry.inspect}"
480     history.replace history[0, history_size]
481   end
483   def_settings "programs/lists" do |wmii|
484     wmii.plugin_config["standard:programs"]["lists"] ||= {}
485   end
487 #{{{ programs (w/ history)
488   binding("execute-program", "MODKEY-Shift-p") do |wmii,|
489     Thread.new do 
490       entry, history_name = run_program(wmii, standard_programs, wmii.plugin_config["standard:programs"]["lists"])
491       record_history(wmii, entry, history_name) if entry
492     end
493   end
495 # {{{ Run program with given tag
496   binding("execute-program-with-tag", "MODKEY-Control-y") do |wmii,|
497     rd, wr = IO.pipe
498     result = wmii.wmiimenu(wmii.views_intellisort) do |tag|
499       begin
500         rd.close
501         if /^\s*$/ !~ tag
502           choice, history_name = run_program(wmii, standard_programs, 
503                                              wmii.plugin_config["standard:programs"]["lists"])
504           Marshal.dump([choice, history_name], wr)
505         else
506           Marshal.dump(nil, wr)
507         end
508       ensure
509         wr.close rescue nil
510       end
511     end
512     wr.close
513     # using result.value to perform the view switch in the current process so
514     # that the transition table can be updated
515     Thread.new do 
516       begin
517         tag = result.value
518         if /^\s*$/ !~ tag
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
523             wmii.view tag
524           end
525         end
526         choice, history_name = Marshal.load(rd.read)
527         if choice
528           record_history(wmii, choice, history_name)
529         else # empty string, spaces only, etc
530           wmii.unregister handler
531         end
532       ensure
533         rd.close rescue nil
534       end
535     end
536   end
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
541 #    ALT-1  => 1
542 #    ALT-3  => 3
543 #    ALT-5 => mail
544 #    ALT-6 => web
545   (0..9).each do |key|
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)
551         wmii.view(key)
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]
555       end
556     end
557   end
559 #{{{ Move to given view, with intelligent history
560   binding("tag-jump", "MODKEY-t") do |wmii,|
561     Thread.new do
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
564     end
565   end
567   binding("retag", "MODKEY-Shift-t") do |wmii,|
568     wmii.wmiimenu(wmii.views_intellisort){|new_tag| wmii.retag_curr_client(new_tag) }
569   end
570   binding("retag-jump", "MODKEY-Shift-r") do |wmii,|
571     rd, wr = IO.pipe
572     wmii.wmiimenu(wmii.views_intellisort) do |new_tag|
573       rd.close
574       wmii.retag_curr_client(new_tag)
575       wr.puts new_tag
576       wr.close
577     end
578     wr.close
579     Thread.new do
580       new_tag = rd.gets
581       rd.close
582       wmii.view new_tag[/(?![+-]).*/]
583     end
584   end
586   binding("namespace-retag", "MODKEY2-Shift-t") do |wmii,|
587     wmii.wmiimenu(wmii.views){|new_tag| wmii.retag_curr_client_ns(new_tag) }
588   end
589   binding("namespace-retag-jump", "MODKEY2-Shift-r") do |wmii,|
590     rd, wr = IO.pipe
591     result = wmii.wmiimenu(wmii.views) do |new_tag|
592       rd.close
593       wmii.retag_curr_client_ns(new_tag)
594       wr.puts new_tag
595       wr.close
596     end
597     wr.close
598     Thread.new do
599       subtag = rd.gets
600       rd.close
601       wmii.view "#{wmii.curr_view[/[^:]+/]}:#{subtag[/(?![+-]).*/]}"
602     end
603   end
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 }
609       end
610     end
611   end
612 # Retag as specified numeric tag if it exists, or 
613 # (N-last_numeric_tag)th non-numeric tag.
614   (0..9).each do |key|
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]
625       else
626         break
627       end
628       LOGGER.info "Retagging #{curr_tags.inspect} => #{new_tags.inspect}"
629       wmii.set_curr_client_tags(new_tags)
630     end
631   end
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 }
639         unless new_view.nil?
640           new_tags = curr_tags.reject {|view| view == new_view }
641           curr_index = new_tags.index wmii.curr_view
642           if curr_index.nil?
643             LOGGER.error "curr_index is nil in letter-retag"
644             return
645           end
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)
649         end
650       end
651     end
652   end
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]
659   end
660   binding("move-next", "MODKEY-Control-DOWN", "MODKEY-period") do |wmii,|
661     wmii.view  wmii.views[wmii.curr_view_index+1] || wmii.views[0]
662   end
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]
668     wmii.view dest
669   end
670   binding("namespace-move-prev", "MODKEY2-Shift-UP", "MODKEY2-comma") do |wmii,|
671     move_within_namespace.call(wmii, -1)
672   end
673   binding("namespace-move-next", "MODKEY2-Shift-DOWN", "MODKEY2-period") do |wmii,|
674     move_within_namespace.call(wmii, +1)
675   end
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
707 #                             (q1..q4)
708 #                 
709 #                 Example:
710 #                   eigen :ruby ~d <3m
711 #                 returns all bookmarks with "eigen" in the description or the
712 #                 URL, tagged as :ruby, used/defined in the last 3 months
713 #                 
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
720 #                 Usage example:
721 #                 
722 #                  :blog<enter>    to select all the bookmarks tagged as :blog
723 #                  !o<enter>       to open them all
724 require 'time'
725 require 'thread'
726 class BookmarkManager
727   Bookmark = Struct.new(:description, :url, :tags, :date)
729   def initialize(filename)
730     @filename = filename
731     @bookmarks = {}
732     @deleted_bookmarks = {}
733     @loaded = false
734     @mutex = Mutex.new
735   end
737   def load
738     @bookmarks.clear
739     IO.foreach(@filename) do |line|
740       begin
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/)
744           begin
745             date = Time.rfc822(date)
746           rescue
747             date = Time.new
748           end
749           bm = Bookmark.new(desc, url, tags, date)
750           @bookmarks[url] = bm
751         else
752           LOGGER.warn "Loading bookmark #{url.inspect}: already loaded, skipping."
753         end
754       rescue Exception
755         # keep parsing other lines
756       end
757     end rescue nil
758     @loaded = true
759   end
761   # Returns the bookmark corresponding to the given URL, or nil.
762   def [](url)
763     self.load unless @loaded
764     @bookmarks[url]
765   end
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)
774     @bookmarks[url] = bm
775     @deleted_bookmarks.delete(url)
776     not ret
777   end
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?
784   end
786   def bookmarks
787     self.load unless @loaded
788     @bookmarks.values
789   end
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
794   # list and saved.
795   def save!(destination = @filename)
796       merge!
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")
801         end
802         f.sync
803       end
804       File.rename(tmpfile, destination) # atomic if on the same FS and fleh
805       self.load
806   end
807   
808   def merge!
809     IO.foreach(@filename) do |line|
810       desc, url, tags, date = line.chomp.split(/\t/).map{|x| x.strip}
811       unless @deleted_bookmarks[url] ||
812           (@bookmarks[url] &&
813            @bookmarks[url].date >= date)
814         tags = (tags || "").split(/\s/)
815         begin
816           date = Time.rfc822(date)
817         rescue
818           date = Time.new
819         end
820         add_bookmark(desc, url, tags, date)
821       end
822     end rescue nil
823   end
824   private :merge!
827   def transaction
828     begin
829       fh = File.open(@filename + ".lock", "a")
830       fh.flock(File::LOCK_EX)
831       @mutex.synchronize{ yield(self) }
832     ensure
833       fh.flock(File::LOCK_UN)
834       fh.close
835     end
836   end
838   def satisfy_date_condition?(bookmark, condition)
839     date = bookmark.date
840     case 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'
851       case sign
852       when '<':  Time.new - date <= units * multiplier
853       when '>':  Time.new - date >= units * multiplier
854       end
855     end
856   end
857   private :satisfy_date_condition?
858   
859   def refine_selection(expression, choices=self.bookmarks)
860     expression = expression.strip
861     pieces = expression.split(/\s+/)
862     criteria = []
863     option_needed = false
864     pieces.each do |x|
865       case option_needed
866       when true:    criteria.last << " #{x}"; option_needed = false
867       when false:   criteria << x; option_needed = true if /^~\w/ =~ x 
868       end
869     end
870     choices.select do |bm|
871       criteria.all? do |criterion|
872         case 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)
879         end
880       end
881     end
882   end
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'
897 require 'uri'
898 require 'open-uri'
899 require 'net/http'
900 require 'net/https'
901 require 'cgi'
902 require 'iconv'
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)
915     https.use_ssl = true
917     xml = https.start do
918       req = Net::HTTP::Get.new(request, 'User-Agent' => BOOKMARK_AGENT)
919       req.basic_auth(user, pass)
920       https.request(req).body
921     end
922   end
923   
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)
938   end
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)
946   end
948   def download_delicious_bookmarks(user, pass, mode = :full, encoding = nil)
949     require 'rexml/document'
950     require 'parsedate'
951     require 'time'
952     LOGGER.debug "Downloading del.icio.us bookmarks, #{mode} mode"
953     case mode
954     when :full
955       req = '/v1/posts/all'
956     when :partial
957       req = '/v1/posts/recent?count=100'
958     end
959     xml = perform_delicious_request(req, user, pass)
961     elements = REXML::Document.new(xml).elements
962     ret = []
963     elements.each("posts/post") do |el|
964       begin
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)
970         date.localtime
971         if tags == 'system:unfiled'
972           tags = []
973         else
974           tags = tags.split(/\s+/).map{|x| ":#{x}"}
975         end
977         ret << [desc, url, tags, date]
978       rescue Iconv::IllegalSequence
979         LOGGER.error "download_delicious_bookmarks, #{url}: iconv error #{$!.class}."
980       rescue
981         LOGGER.error "download_delicious_bookmarks, #{url}: #{$!}."
982       end
983     end
985     ret
986   end
988   def get_delicious_update_time(user, pass)
989     require 'rexml/document'
990     require 'parsedate'
991     require 'time'
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)
999   end
1001   def sync_delicious_bookmarks(wmii, mode, last_update_time = nil)
1002     require 'fileutils'
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
1011     end
1012     
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)
1022       end
1023       prev_bm_manager.transaction { |bmanager| bmanager.save! }
1024       last_update_time = Time.now
1025     end
1027     # Form lists of local bookmark changes.
1028     local_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}
1036         end
1037       end
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}
1041         end
1042       end
1043     end
1045     # Form lists of remote bookmark changes.
1046     remote_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)
1054       end
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}
1062         end
1063       end
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}
1067         end
1068       end
1069     end
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}."
1081         case remote_change
1082         when :modify, :add
1083           bm = remote_changes[url][:bm]
1084           bm_manager.add_bookmark(bm.description, bm.url, bm.tags, bm.date)
1085         when :remove
1086           bm_manager.remove_bookmark(url) unless local_change == :remove
1087         else
1088           LOGGER.error "sync_delicious_bookmarks: unknown change type #{remote_change.inspect}."
1089         end
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}."
1096         case local_change
1097         when :modify, :add
1098           bm = local_changes[url][:bm]
1099           push_delicious_bookmark(bm, user, pass, shared, encoding)
1100         when :remove
1101           delete_delicious_bookmark(url, user, pass) unless remote_change == :remove
1102         else
1103           LOGGER.error "sync_delicious_bookmarks: unknown change type #{local_change.inspect}."
1104         end
1105       else
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,
1113                                   remote_bm.url,
1114                                   remote_bm.tags,
1115                                   remote_bm.date)
1116         else
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)
1119         end
1120       end
1121     end
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."
1126   end
1128   def bookmark_url(wmii,url)
1129     escaped = false
1130     begin
1131       uri = URI.parse url
1132     rescue
1133       unless escaped
1134         escaped = true
1135         url = URI.escape url
1136         retry
1137       end
1138       LOGGER.error "Failed to bookmark #{URI.unescape(url)}: invalid URI."
1139       return
1140     end
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]
1151       end
1152     end
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?
1159         begin
1160           timeout 5 do
1161             title_variants = protocol_desc[:get_title].call(wmii,uri)
1162           end
1163         rescue Timeout::Error
1164           LOGGER.warn "get_title timeout for URL #{uri.to_s}."
1165         rescue
1166           LOGGER.warn "get_title exception for URL #{uri.to_s}: #{$!}"
1167         end
1168       end
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!}
1177         end
1178       end
1179     else
1180       LOGGER.error "Failed to bookmark #{uri.to_s}: unknown protocol #{uri.scheme.inspect}."
1181     end
1182   end
1183   
1184   def_settings("actions/internal") do |wmii|
1185     hash = wmii.plugin_config["standard:actions"]["internal"] ||= {}
1186 =begin
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"],
1190                                           :full)
1191       bm = BookmarkManager.new(BOOKMARK_FILE)
1192       info.each{|desc, url, tags, date| bm.add_bookmark(desc, url, tags, date) }
1193       bm.save!
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) }
1197       prevbm.save!
1198     end
1199     hash.update("del.icio.us-import" => import_lambda)
1200 =end
1202     hash.update(
1203     {
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)
1207       end,
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)
1215           end
1216         end
1217       end,
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}."
1224           else
1225             LOGGER.info "Could not delete bookmark #{bm.description.inspect} -> #{bm.url}."
1226           end
1227         end
1228         refine_choices = lambda do |bookmarks|
1229           options = bookmarks.sort_by{|x| x.description}.map do |x| 
1230             "#{x.description} : #{x.url}"
1231           end
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)
1240                   end
1241                 else
1242                   LOGGER.error "Tried to delete #{bookmarks.size} bookmarks at a time."
1243                   LOGGER.error "Refusing since it's over multiple-delete-limit (#{limit})."
1244                 end
1245               elsif bm = bm_manager[condition[/ : (\S+)$/,1]]
1246                 delete_bookmark.call(bm)
1247               else
1248                 choices = bm_manager.refine_selection(condition, bookmarks)
1249                 refine_choices.call(choices) unless choices.empty?
1250               end
1251             end
1252           end
1253         end
1254         refine_choices.call(bm_manager.bookmarks)
1255       end,
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}"
1261           end
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]
1267                 title_variants <<
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! }
1276                   end
1277                 end
1278               else
1279                 choices = bm_manager.refine_selection(condition, bookmarks)
1280                 refine_choices.call(choices) unless choices.empty?
1281               end
1282             end
1283           end
1284         end
1285         refine_choices.call(bm_manager.bookmarks)
1286       end
1287     })
1288   end
1290   def_settings("bookmark/multiple-open") do |wmii|
1291     wmii.plugin_config["standard:bookmark"]["multiple-open-limit"] = 10
1292   end
1294   def_settings("bookmark/multiple-delete") do |wmii|
1295     wmii.plugin_config["standard:bookmark"]["multiple-delete-limit"] = 30
1296   end
1298   def_settings("bookmark/del.icio.us importer") do |wmii|
1299     wmii.plugin_config["standard:bookmark"]["refresh_period"] = 30
1300     Thread.new do
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
1307         loop do
1308           begin
1309             sync_delicious_bookmarks(wmii, mode, last_update_time)
1310             last_update_time = Time.now
1311           rescue Exception
1312             LOGGER.error "Error while sync'ing bookmarks."
1313             LOGGER.error $!.exception
1314             puts $!.backtrace
1315           end
1316           sleep(60 * wmii.plugin_config["standard:bookmark"]["refresh_period"])
1317         end
1318       end
1319     end
1320   end
1322   standard_bookmark_protocols = {
1323     'http' => {
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}' &"
1328       end,
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]
1336       end
1337     },
1338     'ssh' => {
1339       :open_urls => lambda do |wmii,bms|
1340         term = wmii.plugin_config["standard"]["x-terminal-emulator"] || "xterm"
1341         bms.each do |bm|
1342           uri = bm[:uri]
1343           ssh_host = uri.host
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' &"
1347         end
1348       end,
1349       :get_title => lambda do |wmii,uri|
1350         title = uri.host
1351         title = "#{uri.user}@" + title unless uri.user.nil?
1352         title << ":#{uri.port.to_s}" unless uri.port.nil?
1353         title
1354       end
1355     }
1356   }
1357   standard_bookmark_protocols['https'] = standard_bookmark_protocols['http']
1358   standard_bookmark_protocols['ftp'] =
1359     {
1360       :open_urls => standard_bookmark_protocols['http'][:open_urls],
1361       :get_title => standard_bookmark_protocols['ssh'][:get_title]
1362     }
1363   
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"]
1369   end
1371   binding("bookmark", "MODKEY-Shift-b") do |wmii,|
1372     Thread.new do
1373       url = `wmiipsel`.strip
1374       url = "file://#{url}" unless url[%r{\A\w+:/}]
1375       bookmark_url(wmii, url)
1376     end
1377   end
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"]
1383       bm_hash = {}
1384       bms.each do |bm|
1385         LOGGER.debug "Opening bookmark #{bm.description.inspect} -> #{bm.url}."
1386         begin
1387           uri = URI.parse bm.url
1388         rescue
1389           LOGGER.error "Failed to open #{bm.url}: invalid URI. Corrupted bookmarks.txt?"
1390           return
1391         end
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})
1395         else
1396           LOGGER.error "Failed to open #{bm.url}: unknown protocol #{uri.scheme.inspect}."
1397         end
1398       end
1399       bm_hash.each do |proto,bms|
1400         proto[:open_urls].call(wmii,bms)
1401       end
1402     end
1403     refine_choices = lambda do |bookmarks|
1404       options = bookmarks.sort_by{|x| x.description}.map do |x| 
1405         "#{x.description} : #{x.url}"
1406       end
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|
1414                 bm.date = Time.new
1415               end
1416               bm_manager.transaction{|bm| bm.save!}
1417               open_bookmark.call(bookmarks)
1418             else
1419               LOGGER.error "Tried to open #{bookmarks.size} bookmarks at a time."
1420               LOGGER.error "Refusing since it's over multiple-open-limit (#{limit})."
1421             end
1422           elsif bm = bm_manager[condition[/ : (\S+)$/,1]]
1423             bm.date = Time.new
1424             bm_manager.transaction{ bm_manager.save! }
1425             open_bookmark.call([bm])
1426           else
1427             choices = bm_manager.refine_selection(condition, bookmarks)
1428             refine_choices.call(choices) unless choices.empty?
1429           end
1430         end
1431       end
1432     end
1433     refine_choices.call(bm_manager.bookmarks)
1434   end