removing outdated emacs files (soon to be replaced with recent ones)
[srid.dotfiles.git] / dot-wmii-3 / wmiirc
blob72ce9dded05c5574942692d27c2a014f871b5fe9
1 #!/usr/bin/env ruby
2 # Copyright (c) 2006 Mauricio Fernandez <mfp@acm.org>
3 # http://eigenclass.org/hiki.rb?wmii+ruby
4 # Licensed under the same terms as Ruby (see LICENSE).
6 USE_IXP_EXTENSION = true
7 WMIIRC_VERSION = "0.3.2"
8 WMIIRC_RELEASE_DATE = "unreleased (preliminary/internal)"
9 WMIIRC_HOME = File.join(ENV["HOME"], ".wmii-3")
10 WMIIRC_CONFIG_FILE = File.join(WMIIRC_HOME, "wmiirc-config.rb")
11 WMIIRC_PLUGIN_DIR = File.join(WMIIRC_HOME, "plugins")
13 WMIIRC_HELP_MESSAGE = <<EOF
15 Welcome to ruby-wmii, a Ruby script for configuring and controlling the wmii
16 window manager. This message describes how you can customize and extend
17 ruby-wmii. You can reread it at any time by pressing MODKEY-a and selecting
18 'config-help' (MODKEY is the Alt key by default).
20 Since 0.3.0, ruby-wmii can be customized by editing
21 #{WMIIRC_CONFIG_FILE}
22 This makes upgrades easier, since you can overwrite the wmiirc script with a
23 newer version while all your settings are preserved in
24 #{WMIIRC_CONFIG_FILE}
25 In other words, the main wmiirc script itself shouldn't be modified. Were that
26 necessary, please consider submitting a patch if you think the changes will be
27 of use to other people. You can send your modifications to <mfp@acm.org> (add
28 'wmii' to the subject to make sure it gets through the spam filters).
30 Staying up to date
31 ==================
33 The latest version of ruby-wmii can be obtained from
34 http://eigenclass.org/hiki.rb?wmii+ruby
35 Run the included install.rb script with
36 ruby install.rb
37 to install wmiirc and the standard plugin.
38 Were any further step necessary, it'd be noted in README.upgrade.
40 Using plugins
41 =============
43 ruby-wmii can be extended using third-party plugins comprising keyboard
44 bindings and "bar applets". This involves two steps:
45 1) placing the plugin under #{WMIIRC_PLUGIN_DIR}
46 2) editing #{WMIIRC_CONFIG_FILE} to request that a given plugin be used
48 Each plugin defines a number of key bindings and applets under a namespace.
49 A namespace:name pair refers to a key binding or an applet; this is all you
50 need to reference and use them.
51 Here's a small example of how to use the 'volume' applet and a number of
52 bindings defined under the "standard" namespace
54 from("standard") {
55 use_bar_applet("volume")
56 use_binding("dict-lookup")
57 use_binding("execute-program-with-tag")
58 use_binding("execute-action")
59 use_binding("execute-program")
60 (0..9).each{|k| use_binding("numeric-jump-\#{k}") }
63 This is broadly equivalent to
65 use_bar_applet("standard:volume")
66 use_binding("standard:dict-lookup")
67 use_binding("standard:execute-program-with-tag")
68 use_binding("standard:execute-action")
69 use_binding("standard:execute-program")
70 (0..9).each{|k| use_binding("standard:numeric-jump-\#{k}") }
72 (There's a small difference though: #from uses instance_eval to change self in
73 the block, but this shouldn't matter normally.)
75 Overriding default plugin settings
76 ----------------------------------
78 The default position and the data (label displayed by wmiibar) of each bar
79 applet are specified by the author, but you can override them with something
80 like
82 # override position only
83 use_bar_applet("somebody@example.com:cool-bar-applet", 50)
84 # position and label
85 use_bar_applet("somebody@example.com:cool-bar-applet2", 100, "text to display")
87 the second argument should be an integer between 0 and 999, which will be used
88 to determine the relative position of the applets in use.
90 You can as well override the default key combination for a key binding (allowing
91 you to reuse the action while changing the actual key sequence that triggers it)
92 as follows:
94 use_binding("myfriend@example.com:do-magic", "MODKEY2-m", "MODKEY-Shift-m")
96 This associates the action defined as 'do-magic' under the myfriend@example
97 namespace to both MODKEY2 and MODKEY-Shift-m.
99 Further configuration
100 ---------------------
102 Some plugins can be configured via the plugin_config hash.
103 See the default
104 #{WMIIRC_CONFIG_FILE}
105 for an example.
107 Read standard-plugin.rb to see the options accepted by the standard
108 bindings/applets.
110 Writing your own plugins
111 ========================
113 If you think you've found a particularly useful action or applet, you can
114 distribute it as a plugin which should look like this:
116 Plugin.define("my-email@address.com") do
117 # Use your email address to make sure namespaces are unique.
118 # You can also use my-email@address.com/subspace if you need to
119 # partition your namespace.
120 author '"My Full Name" <my-email-address.com>'
122 bar_applet("applet", 50) do |wmii, bar|
123 bar.on_click(MOUSE_BUTTON_LEFT){ wmii.view "foo" }
124 Thread.new{ loop{ bar.data = `somecmd` }; sleep 2 }
127 binding("do-magic", "MODKEY-Shift-m", "MODKEY2-m") do |wmii, keyhandler|
128 # keyhandler.key is the key that was pressed.
129 # you can unregisted the keyhandler (remove the binding) with
130 # wmii.unregister(keyhandler)
132 wmii.write "/view/sel/mode", "max"
136 The end-user would have to place the above code in a .rb file under
137 $HOME/.wmii-3/plugins
138 and add this to his wmii-config.rb:
140 from("my-email@address.com") {
141 use_binding("do-magic") # optionally rebind it to another key
142 use_bar_applet("applet") # optionally change position and initial label
145 or alternatively
147 use_binding("my-email@address.com:do-magic")
148 use_bar_applet("my-email@addres.com:applet")
151 Further information
152 ===================
154 This should be enough to get you running, but there are still a few things to
155 be learned from ruby-wmii's sources. You can see how some typical tasks are
156 implemented in standard-plugin.rb, which contains the default bindings and
157 applets under the "standard" namespace.
160 Happy hacking,
163 Mauricio Fernandez <mfp@acm.org> http://eigenclass.org
167 END{ "wmiirc #{Process.pid} finishing, $! is #{$!.inspect}" }
169 def run
170 unless File.exist?(WMIIRC_CONFIG_FILE)
171 File.open(WMIIRC_CONFIG_FILE, "w"){|f| f.puts DATA.read }
172 Thread.new do
173 IO.popen("xmessage -file -", "w"){|f| f.puts WMIIRC_HELP_MESSAGE; f.close_write }
177 LOGGER.info "Loading standard plugin"
178 begin
179 load File.join(WMIIRC_PLUGIN_DIR, "standard-plugin.rb"), true
180 rescue LoadError
181 LOGGER.error "standard-plugin.rb not found"
184 Dir["#{WMIIRC_PLUGIN_DIR}/*.rb"].each do |fname|
185 next if File.basename(fname) == "standard-plugin.rb"
186 LOGGER.info "Loading plugin #{fname}"
187 load fname, true
190 # see if the standard plugin is available
191 if Plugin.registered_plugins["standard"].empty?
192 Thread.new do
193 IO.popen("xmessage -file -", "w") do |f|
194 f.puts <<EOF
195 Could not find the standard plugin, so several bindings/applets
196 will be missing. Please reinstall ruby-wmii, or copy
197 standard-plugin.rb (which you will find in the source tarball) to
198 $HOME/.wmii-3/plugins.
200 f.close_write
205 load WMIIRC_CONFIG_FILE
207 Thread.abort_on_exception = false
209 #{{{ Run START_PROGS
210 LOGGER.info "Running START_PROGS:"
211 if defined? START_PROGS
212 START_PROGS.each{|line| LOGGER.info "Executing #{line}"; system(line)}
215 #{{{ Main loop
216 LOGGER.info "Executing main loop..."
217 WMII::Configuration.last_instance.main_loop
221 require 'logger'
222 require 'fcntl'
224 $children = []
225 logfile = File.open(File.join(WMIIRC_HOME, "wmiirc.log"), "a")
226 logfile.fcntl Fcntl::F_SETFD, Fcntl::FD_CLOEXEC
227 DATA.fcntl Fcntl::F_SETFD, Fcntl::FD_CLOEXEC
229 logfile.sync = true
230 STDOUT.reopen(logfile)
231 STDERR.reopen(logfile)
232 LOGGER = Logger.new(STDERR)
233 LOGGER.info "INIT"
234 LOGGER.level = Logger::INFO
236 # IXP extension not ready for prime-time
237 $pure_ruby_ixp_needed = !USE_IXP_EXTENSION
238 begin
239 $:.unshift ENV["HOME"] + "/.wmii-3/ext/IXP"
240 require 'IXP'
241 rescue LoadError
242 LOGGER.debug "Using wmiir"
243 LOGGER.debug "Current dir: #{Dir.pwd}"
244 $pure_ruby_ixp_needed = true
245 end unless $pure_ruby_ixp_needed
247 module IXP
248 if $pure_ruby_ixp_needed
249 OWRITE = 1
250 IXPError = Class.new(StandardError)
251 BrokenPipeError = Class.new(IXPError)
252 BusyError = Class.new(IXPError)
253 class LowLevelClient
254 def initialize(address)
255 @address = address
258 def write(file, contents, mode = nil) # mode ignored
259 IO.popen("wmiir -a #{@address} write #{file}", "w"){|io| io.print contents.to_s; io.close_write }
260 true
261 rescue Errno::EPIPE
262 return false
265 def read(file)
266 `wmiir -a #{@address} read #{file}`
269 def remove(file)
270 system("wmiir -a #{@address} remove #{file}")
273 def create(file)
274 system("wmiir -a #{@address} create #{file}")
277 def foreach(file, &block)
278 open("|wmiir read #{file}") do |is|
279 LOGGER.debug "Executing foreach, using process #{is.pid}"
280 $children << is.pid
281 is.fcntl Fcntl::F_SETFD, Fcntl::FD_CLOEXEC
282 is.each(&block)
287 def fileno
291 end # if $pure_ruby_ixp_needed
293 class Client
294 attr_reader :address
296 def initialize(address)
297 @address = address
298 establish_connection
301 %w[write read remove create].each do |meth|
302 define_method(meth) do |*args|
303 max_attempts = 10
304 begin
305 @client.__send__(meth, *args)
306 rescue IXP::BrokenPipeError, SystemCallError
307 LOGGER.debug "Restablishing connection..."
308 establish_connection
309 retry if (max_attempts -= 1) > 0
310 rescue IXP::IXPError
311 return ""
316 def foreach(file, &block)
317 @client.foreach(file, &block)
320 private
321 def establish_connection
322 LOGGER.debug "Connecting to #{address}"
323 @client = LowLevelClient.new(address.clone)
324 LOGGER.debug "Connection established (fd #{@client.fileno})" if @client.fileno
329 require 'timeout'
330 module WMII
331 MOUSE_BUTTON_LEFT = 1
332 MOUSE_BUTTON_MIDDLE = 2
333 MOUSE_BUTTON_RIGHT = 3
334 MOUSE_SCROLL_UP = 4
335 MOUSE_SCROLL_DOWN = 5
337 module ConfigurationHelper
338 def def_conf_var(*names)
339 names.each do |name|
340 define_method(name) do |*val|
341 case val[0]
342 when nil: instance_variable_get("@#{name}")
343 else instance_variable_set("@#{name}", val[0])
349 def def_wmii_var(*names, &block)
350 names.each do |name|
351 define_method(name) do |*val|
352 case val[0]
353 when nil: @ixp_conn.read "/def/#{name}"
354 else
355 @ixp_conn.write "/def/#{name}", val[0]
356 block.call val[0] if block
363 class Plugin
364 class << self
365 attr_reader :registered_plugins
366 private :new
368 @registered_plugins = Hash.new{|h,namespace| h[namespace] = []}
370 PluginClashError = Class.new(StandardError)
371 KeyBindingClashError = Class.new(PluginClashError)
372 BarAppletClashError = Class.new(PluginClashError)
373 FeatureClashError = Class.new(PluginClashError)
374 SettingsClashError = Class.new(PluginClashError)
376 KeyBinding = Struct.new(:keys, :block)
377 BarApplet = Struct.new(:position, :initial_data, :block)
378 Feature = Struct.new(:block)
379 Settings = Struct.new(:block)
381 def self.define(namespace, &block)
382 r = new(namespace)
383 r.instance_eval(&block)
384 Plugin.registered_plugins[namespace] << r
388 attr_reader :bindings
389 attr_reader :bar_applets
390 attr_reader :features
391 attr_reader :settings
392 def initialize(namespace)
393 @namespace = namespace
394 @bindings = {}
395 @bar_applets = {}
396 @features = {}
397 @settings = {}
400 extend ConfigurationHelper
401 def_conf_var :author
403 def binding(name, *suggested_bindings, &block)
404 if @bindings.has_key?(name)
405 raise KeyBindingClashError, "Binding #{name} defined twice in namespace #{@namespace.inspect}"
407 @bindings[name] = KeyBinding.new(suggested_bindings, block)
410 def bar_applet(name, position, initial_data = "", &block)
411 if @bar_applets.has_key?(name)
412 raise BarAppletClashError, "Bar applet #{name} defined twice in namespace #{@namespace.inspect}"
414 @bar_applets[name] = BarApplet.new(position, initial_data, block)
417 def feature(name, &block)
418 if @features.has_key?(name)
419 raise FeatureClashError, "Feature #{name} defined twice in namespace #{@namespace.inspect}"
421 @features[name] = Feature.new(block)
424 def def_settings(name, &block)
425 if @settings.has_key?(name)
426 raise SettingsClashError, "Settings #{name} defined twice in namespace #{@namespace.inspect}"
428 @settings[name] = Settings.new(block)
432 class Configuration
433 DEFAULT_EVENTS = %w[BarClick ClientClick ClientFocus CreateClient Key Bye Starting]
434 attr_reader :plugin_config, :prev_view
436 class EventHandler
437 attr_reader :type
439 def initialize(type, &block)
440 @type = type
441 @block = block
444 def call(*a); @block.call(*a) end
447 class KeyHandler < EventHandler
448 attr_reader :key
450 def initialize(key, &block)
451 @key = key
452 super("Key", &block)
456 class PluginConfigHash
457 def initialize
458 @opts = Hash.new{|h,k| h[k] = {} }
461 def [](x); @opts[x] end
462 def []=(x, val); @opts[x] = val end
465 class << self;
466 attr_reader :last_instance
467 def define(*a, &b)
468 r = new(*a, &b)
469 @last_instance = r
473 private :new
476 def initialize(&block)
477 # Signal we're about to start to the previous wmiirc process, or wait
478 # until the IXP server is up
479 LOGGER.info "Waiting for the IXP server."
480 begin
481 @ixp_conn = IXP::Client.new ENV["WMII_ADDRESS"]
482 rescue SystemCallError
483 LOGGER.info "IXP server not up yet, waiting..."
484 sleep 0.2
485 retry
487 LOGGER.info "Killing old wmiirc instances."
488 loop{ @ixp_conn.write "/event", "Starting\n" and break } # standard wmiirc
489 @ixp_conn.write "/event", "Bye\n" # new-style wmiirc
491 begin
492 Timeout.timeout(0.5) do
493 loop do
494 quittxt = @ixp_conn.read "/bar/QUIT/data" rescue ""
495 if quittxt == "QUIT"
496 LOGGER.debug "Previous wmiirc process signalled termination."
497 remove "/bar/QUIT"
498 break
500 sleep 0.1
503 rescue Timeout::Error
504 LOGGER.debug "Previous wmiirc process didn't signal termination."
507 @procs = {"BarClick" => [], "ClientClick" => [],
508 "ClientFocus" => [], "CreateClient" => [],
509 "Key" => Hash.new{|h,k| h[k] = []}}
510 update_custom_handlers_matcher
512 @plugin_config = PluginConfigHash.new
513 @children = $children
514 # reset key bindings
515 LOGGER.info "Resetting key bindings."
516 @ixp_conn.write "/def/keys", "\n"
517 @key_substitutions = {
518 "MODKEY" => "Mod1", "UP" => "k", "DOWN" => "j",
519 "LEFT" => "h", "RIGHT" => "l"
521 @view_history_decay = 0.8
522 @view_history_prev_bias = 0.4
523 @view_transition_table = Hash.new{|h,k| h[k] = Hash.new{|h2,k2| h2[k2] = 0} }
524 @view_transitions = Hash.new{|h,k| h[k] = 0}
525 @prev_view = curr_view
526 @view_history = [curr_view]
527 @view_history_index = 0
529 @managed_bar_applets = []
530 LOGGER.info "Loading configuration"
531 LOGGER.info "Plugin specified settings..."
533 load_settings = lambda do |namespace, settings|
534 settings.each_pair do |name, setting|
535 LOGGER.info "Loading settings #{name} from #{namespace}"
536 setting.block.call(self)
540 # Load from standard first
541 if Plugin.registered_plugins.has_key?("standard")
542 Plugin.registered_plugins["standard"].each{|plugin| load_settings.call("standard", plugin.settings) }
544 Plugin.registered_plugins.each_pair do |namespace, plugins|
545 next if namespace == "standard"
546 plugins.each{|plugin| load_settings.call(namespace, plugin.settings) }
548 instance_eval(&block) if block_given?
551 def write(file, contents)
552 @ixp_conn.write(file, contents.to_s, IXP::OWRITE)
555 def read(file)
556 @ixp_conn.read(file)
559 def remove(file)
560 @ixp_conn.remove(file)
563 def create(file)
564 @ixp_conn.create(file)
567 def update_custom_handlers_matcher
568 @custom_handlers_re = /^#{(@procs.keys - DEFAULT_EVENTS).join("|")}(\s|$)/
571 private :update_custom_handlers_matcher
572 def from(namespace, &block)
573 conf = self
574 Class.new do
575 define_method(:use_binding) do |name, *overriding_keys|
576 conf.use_binding("#{namespace}:#{name}", *overriding_keys)
578 define_method(:use_bar_applet) do |name, *opts|
579 conf.use_bar_applet("#{namespace}:#{name}", *opts)
581 define_method(:use_feature) do |name|
582 conf.use_feature("#{namespace}:#{name}")
584 end.new.instance_eval(&block)
587 def use_binding(binding_name, *overriding_keys)
588 md = /([^:]+):(.+)/.match(binding_name)
589 unless md
590 LOGGER.error "Ignoring illegal binding name #{binding_name}."
591 return
593 namespace, name = md.captures
594 if (plugins = Plugin.registered_plugins[namespace]).empty?
595 LOGGER.error "Unknown plugin #{namespace}"
596 return
598 key_bindings = plugins.inject([]){|s,x| s + [x.bindings[name]]}.compact
599 if key_bindings.empty?
600 LOGGER.error "Key binding #{name} not found in #{namespace}."
601 return
603 if key_bindings.size > 1
604 LOGGER.debug "Key binding #{name} defined more than once in #{namespace}."
605 LOGGER.debug "Keeping last definition."
607 key_binding = key_bindings.last
608 keys, block = key_binding.keys, key_binding.block
609 actual_keys = overriding_keys.empty? ? keys : overriding_keys
610 LOGGER.info "Importing key binding #{namespace}:#{name} as #{actual_keys.join(' ')}"
611 on_key(*actual_keys, &block)
614 def use_bar_applet(bar_applet_name, position = nil, data = nil)
615 md = /([^:]+):(.+)/.match(bar_applet_name)
616 unless md
617 LOGGER.error "Ignoring illegal applet name #{bar_applet_name}."
618 return
620 namespace, name = md.captures
621 if (plugins = Plugin.registered_plugins[namespace]).empty?
622 LOGGER.error "Unknown plugin #{namespace}"
623 return
625 bar_applets = plugins.inject([]){|s,x| s + [x.bar_applets[name]]}.compact
626 if bar_applets.empty?
627 LOGGER.error "Bar applet #{name} not found in #{namespace}."
628 return
630 if bar_applets.size > 1
631 LOGGER.debug "Bar applet #{name} defined more than once in #{namespace}."
632 LOGGER.debug "Keeping last definition."
634 bar_applet = bar_applets.last
636 position = position || bar_applet.position
637 initial_data = data || bar_applet.initial_data
638 block = bar_applet.block
640 LOGGER.info "Importing bar applet #{namespace}:#{name} at position #{position}"
641 setup_bar("%03d_#{name}" % position, normcolors, initial_data, &block)
644 def use_feature(feature_name)
645 LOGGER.info "Use feature: #{feature_name}"
647 md = /([^:]+):(.+)/.match(feature_name)
648 unless md
649 LOGGER.error "Ignoring illegal feature name #{feature_name}."
650 return
652 namespace, name = md.captures
653 if (plugins = Plugin.registered_plugins[namespace]).empty?
654 LOGGER.error "Unknown plugin #{namespace}"
655 return
658 features = plugins.inject([]){|s,x| s + [x.features[name]]}.compact
659 if features.empty?
660 LOGGER.error "Feature #{name} not found in #{namespace}."
661 return
663 if features.size > 1
664 LOGGER.debug "Feature #{name} defined more than once in #{namespace}."
665 LOGGER.debug "Keeping last definition."
667 feature = features.last
668 LOGGER.info "Importing feature #{namespace}:#{name}"
670 feature.block[self]
673 def register(type, param1 = nil, param2 = nil, &block)
674 case param1
675 when nil
676 handler = EventHandler.new(type, &block)
677 else
678 handler = EventHandler.new(type) do |*args|
679 if param1 === args[0] && (!param2 || param2 === args[1])
680 block.call(*args)
684 (@procs[type] ||= []) << handler
686 update_custom_handlers_matcher
688 handler
691 def unregister(handler)
692 case handler
693 when KeyHandler
694 @procs["Key"][handler.key].delete handler
695 else
696 @procs[handler.type].delete handler
698 update_custom_handlers_matcher
702 def register_key_bindings
703 LOGGER.info "Setting /def/keys"
704 @ixp_conn.write "/def/keys", @procs["Key"].keys.join("\n")
707 def main_loop
708 @ixp_conn.write "/event", "Bye\n"
709 register_key_bindings
710 times = []
711 loop do
712 begin
713 times.unshift Time.new
714 times = times[0,5]
715 if times[4] && times[0] - times[4] < 0.1
716 cleanup
717 LOGGER.error "wmiiwm seems to be gone, leaving"
718 exit!
720 # wait for events
721 LOGGER.debug "Opening /event"
722 IXP::Client.new(ENV["WMII_ADDRESS"]).foreach("/event") do |line|
723 begin
724 LOGGER.debug "Got #{line.inspect}" if line
725 case line
726 when /^(BarClick|ClientClick)\s+(\S+)\s+(\S+)$/
727 @procs[$1].each{|x| x.call($2, $3.to_i)}
728 when /^(ClientFocus|CreateClient)\s+(\S+)/
729 @procs[$1].each{|x| x.call($2.to_i)}
730 when /^Key (\S+)$/
731 @procs["Key"][$1].each{|x| x.call(self, x)} if @procs["Key"].has_key?($1)
732 when /^Bye|^Starting/
733 LOGGER.info "Cleanup..."
734 cleanup
735 LOGGER.info "QUIT"
736 create "/bar/QUIT" rescue nil
737 write "/bar/QUIT/data", "QUIT"
738 exit!
739 when @custom_handlers_re
740 event, *args = *line.chomp.split(/\s/)
741 LOGGER.debug "Custom event #{event}"
742 @procs[event].each{|x| x.call(*args)}
744 rescue IOError, SystemCallError
745 # terminate #foreach
746 raise
747 rescue StandardError => e
748 LOGGER.debug e.inspect
749 LOGGER.debug e.message
750 e.backtrace.each{|x| LOGGER.debug x}
753 rescue IXP::BusyError
754 # this is a child process, it must die ASAP
755 LOGGER.debug "child process #{Process.pid} exiting."
756 exit!
757 rescue Exception
758 # ignore
759 LOGGER.debug "Ignoring #{$!.inspect}"
764 # not worth meta-programming
765 def on_barclick(name = nil, button = nil, &b)
766 register("BarClick", name, button, &b)
769 def on_clientclick(client = nil, button = nil, &b)
770 register("ClientClick", client, button, &b)
773 def on_clientfocus(client = nil, &b); register("ClientFocus", client, &b) end
774 def on_createclient(client = nil, &b); register("CreateClient", client, &b) end
776 def on_key(*aliasedkeys, &block)
777 handlers = []
778 aliasedkeys.each do |aliasedkey|
779 key = aliasedkey.clone
780 handler = KeyHandler.new(key, &block)
781 @key_substitutions.sort_by{|kalias, actual| kalias.size}.reverse.each do |kalias, val|
782 key.gsub!(/\b#{Regexp.escape(kalias)}\b/, val)
785 LOGGER.info "Registering #{aliasedkey} => #{key}."
786 @procs["Key"][key] << handler
787 handlers << handler
790 return *handlers
793 def clean_fork(&b)
794 @children << fork(&b)
795 Process.detach @children.last
796 LOGGER.debug "clean_fork(): #{@children.last}"
799 def cleanup
800 LOGGER.info "Removing managed bar applets: #{@managed_bar_applets.join(' ')}"
801 @managed_bar_applets.each do |name|
802 remove "/bar/#{name}" rescue nil
804 LOGGER.debug "This is #{Process.pid}, killing #{@children.inspect}"
805 @children.each do |pid|
806 begin
807 Process.kill("TERM", pid)
808 rescue Exception
813 ####{{{ Configuration methods
814 extend ConfigurationHelper
815 def_conf_var :view_history_decay
816 def_conf_var :view_history_prev_bias
818 def_wmii_var :border, :colmode, :colwidth, :rules, :grabmod
819 def_wmii_var(:selcolors){|colors| ENV["WMII_SELCOLORS"] = colors.to_s }
820 def_wmii_var(:normcolors){|colors| ENV["WMII_NORMCOLORS"] = colors.to_s }
821 def_wmii_var(:font){|font| ENV["WMII_FONT"] = font.to_s }
823 def key_subs(associations)
824 unless Hash === associations
825 raise ArgumentError,
826 "key_subs takes a hash with alias => actual_key associations"
828 associations.each_pair do |key, val|
829 @key_substitutions[key.to_s] = val.to_s
833 class BarButton
834 attr_reader :name
835 def initialize(wmiiconfig, name)
836 @wmiiconfig = wmiiconfig
837 @name = name
840 def on_click(key = nil, &block)
841 raise "block wanted" unless block
842 @wmiiconfig.on_barclick(name, key, &block)
845 def data; @wmiiconfig.read("/bar/#{@name}/data") end
846 def data=(x); @wmiiconfig.write("/bar/#{@name}/data", x) end
848 # Returns a triplet [fgcolor, bgcolor, bordercolor]
849 # where each element is itself a RGB triplet (0 to 255).
850 def colors
851 @wmiiconfig.read("/bar/#{@name}/colors").split(/\s+/).map do |txt|
852 txt.scan(/[a-fA-F0-9]{2}/).map{|hex| hex.to_i(16)}
856 # Takes a triplet [fgcolor, bgcolor, bordercolor]
857 # where each element is itself a RGB triplet (0 to 255).
858 def colors=(col_arrays)
859 col_arrays = col_arrays.map do |r,g,b|
860 [ r < 0 ? 0 : (r > 255 ? 255 : r),
861 g < 0 ? 0 : (g > 255 ? 255 : g),
862 b < 0 ? 0 : (b > 255 ? 255 : b) ]
864 txt = col_arrays.map{|vals| "#" + vals.map{|y| "%02X" % y}.join("")}.join(" ")
865 @wmiiconfig.write("/bar/#{@name}/colors", txt)
868 %w[fgcolor bgcolor bordercolor].each_with_index do |name, idx|
869 define_method(name){ self.colors[idx] }
870 define_method("#{name}=") do |col_arr|
871 old = self.colors
872 old[idx] = col_arr
873 self.colors = old
878 # convenience methods
879 def setup_bar(name, colors = normcolors, data = "", &block)
880 LOGGER.info "Setting up bar applet #{name}"
881 @ixp_conn.create "/bar/#{name}"
882 @ixp_conn.write "/bar/#{name}/colors", colors
883 @ixp_conn.write "/bar/#{name}/data", data.chomp
884 @managed_bar_applets << name
885 if block_given?
886 yield self, BarButton.new(self, name)
890 def normalize(tags)
891 tags = tags.split(/\+/) unless Array === tags
892 tags.map{|x| x.chomp}.sort.uniq.compact.grep(/./).join("+")
895 def views
896 @ixp_conn.read("/tags").split(/\n/).grep(/./)
899 def views_intellisort
900 views.sort.sort_by do |view|
901 curr = curr_view
902 if @view_transition_table[curr].has_key?(view)
903 prob = @view_transition_table[curr][view]
904 prob = [prob, @view_history_prev_bias].max if view == @prev_view
905 [0, - prob, view] # handle ties
906 elsif view == @prev_view
907 [0, -@view_history_prev_bias, view]
908 else
909 [1, 0, view]
914 def curr_view
915 @ixp_conn.read("/view/name").chomp
918 def view_history_forward
919 allviews = views
920 viewname = curr_view
921 loop do
922 @view_history_index = [@view_history.size - 1, @view_history_index + 1].min
923 viewname = @view_history[@view_history_index]
924 break if views.include? viewname || @view_history_index == @view_history.size - 1
926 if viewname != curr_view
927 LOGGER.info("History: forward to #{viewname.inspect}")
928 @ixp_conn.write "/ctl", "view #{viewname}"
932 def view_history_back
933 allviews = views
934 viewname = curr_view
935 loop do
936 @view_history_index = [0, @view_history_index - 1].max
937 viewname = @view_history[@view_history_index]
938 break if views.include? viewname || @view_history_index == 0
940 if viewname != curr_view
941 LOGGER.info("History: back to #{viewname.inspect}")
942 @ixp_conn.write "/ctl", "view #{viewname}"
946 def set_curr_view(viewname)
947 viewname = viewname.to_s.chomp
948 return unless views.include? viewname
949 LOGGER.info("Switching to #{viewname.inspect}")
950 @prev_view = curr_view
951 @view_history[@view_history_index + 1..@view_history.size] = viewname
952 @view_history_index += 1
953 @ixp_conn.write "/ctl", "view #{viewname}"
954 n_trans = (@view_transitions[@prev_view] += 1)
955 table = @view_transition_table[@prev_view]
956 table.each_pair do |key, val|
957 table[key] = val * @view_history_decay * n_trans / (n_trans + 1)
959 table[viewname] += (1.0 - @view_history_decay) + 1.0 / (n_trans + 1)
960 total = table.values.inject{|s,x| s+x}
961 table.each_key{|k| table[k] /= total }
962 LOGGER.debug "Trans. table (order 1): #{@view_transition_table.inspect}"
964 alias_method :curr_view=, :set_curr_view
965 alias_method :view, :set_curr_view
967 def curr_view_index
968 views.index(curr_view)
971 def curr_client_tags
972 @ixp_conn.read("/view/sel/sel/tags").split(/\+/).reject{|x| x.empty?}.compact.uniq.sort
975 def curr_client_tags=(tags)
976 @ixp_conn.write("/view/sel/sel/tags", normalize(tags))
979 alias_method :set_curr_client_tags, :curr_client_tags=
981 def retag_curr_client(new_tag)
982 return if /^\s*$/ =~ new_tag
983 old_tags = curr_client_tags
984 new_tags = case new_tag
985 when /^\+/ : old_tags + [new_tag[1..-1]]
986 when /^-/ : old_tags - [new_tag[1..-1]]
987 else [new_tag]
989 new_tags = normalize(new_tags)
991 LOGGER.info "Retagging #{old_tags.inspect} => #{new_tags.inspect})"
992 set_curr_client_tags new_tags
995 def retag_curr_client_ns(new_tag)
996 return if /^\s*$/ =~ new_tag
998 old_tags = curr_client_tags
999 namespace = old_tags.reject{|x| /^\d+$|:/ =~ x}.last
1000 new_tags = case new_tag
1001 when /^\+/ : old_tags + ["#{namespace}:#{new_tag[1..-1]}"]
1002 when /^\-/ : old_tags - ["#{namespace}:#{new_tag[1..-1]}"]
1003 else ["#{namespace}:#{new_tag}"]
1006 set_curr_client_tags new_tags
1009 def wmiimenu(options = nil, &block)
1010 rd, wr = IO.pipe
1011 rd.fcntl Fcntl::F_SETFD, Fcntl::FD_CLOEXEC
1012 wr.fcntl Fcntl::F_SETFD, Fcntl::FD_CLOEXEC
1013 Thread.new do
1014 pid = fork do
1015 rd.close
1016 chosen = IO.popen("wmiimenu", "r+") do |f|
1017 f.puts options unless options.nil?
1018 f.close_write
1019 f.read
1020 end.chomp
1021 LOGGER.debug "wmiimenu(#{chosen.inspect}) finished"
1022 begin
1023 wr.print chosen
1024 wr.close
1025 rescue Exception # catch EPIPE etc.
1027 yield chosen if block_given?
1028 exit!
1030 Process.wait(pid)
1032 wr.close
1033 def rd.value; ret = (gets||"").chomp; close; ret end
1037 def action_list
1038 Dir["#{WMIIRC_HOME}/*"].select do |f|
1039 File.file?(f) && File.executable?(f)
1040 end.map{|f| File.basename(f)}.sort.uniq
1043 def condition(&block)
1044 c = lambda(&block)
1045 def c.===(*x); call(*x) end
1048 end # Configuration
1050 end # WMII
1052 include WMII
1056 __END__
1057 # {{{ ======== ruby-wmii CONFIGURATION BEGINS HERE ==============
1059 # Set the log level
1060 # It defaults to Logger::INFO.
1061 # Set to Logger::DEBUG for extra verbosity.
1062 #LOGGER.level = Logger::DEBUG
1064 # programs to run when wmiirc starts
1065 # one per line, they're run sequentially right before the main loop begins
1066 START_PROGS = <<EOF
1067 xsetroot -solid '#333333'
1070 # {{{ WM CONFIGURATION
1071 WMII::Configuration.define do
1072 border 1
1073 font "fixed"
1074 selcolors '#FFFFFF #248047 #147027'
1075 normcolors '#4D4E4F #DDDDAA #FFFFCC'
1076 colmode 'default'
1077 colwidth 0
1078 grabmod 'Mod1'
1079 rules <<EOF
1080 /Kdict.*/ -> dict
1081 /XMMS.*/ -> ~
1082 /Gimp.*/ -> ~
1083 /MPlayer.*/ -> ~
1084 /XForm.*/ -> ~
1085 /XSane.*/ -> ~
1086 /fontforge.*/ -> ~
1087 /.*/ -> !
1088 /.*/ -> 1
1091 # Translate the following names in the on_key and use_binding definitions.
1092 key_subs :MODKEY => :Mod1,
1093 :MODKEY2 => :Mod4,
1094 :LEFT => :h,
1095 :RIGHT => :l,
1096 :UP => :k,
1097 :DOWN => :j
1100 # Constant used by the intellisort tag selection mechanism
1101 # set it to 0.0 <= value <= 1.0
1102 # Lower values make recent choices more likely (modified first order
1103 # markovian process with exponential decay):
1104 # 0.0 means that only the last transition counts (all others forgotten)
1105 # 1.0 means that the probabilities aren't biased to make recent choices more
1106 # likely
1107 view_history_decay 0.8
1109 # Favor the view we came from in intellisort.
1110 # 1.0: that view is the first choice
1111 # 0.0: that view comes after all views with non-zero transition probability,
1112 # but before all views we haven't yet jumped to from the current one
1113 view_history_prev_bias 0.4
1115 # {{{ Plugin config
1117 # Uncomment and change to override default on_click actions for the status
1118 # bar
1119 #plugin_config["standard:status"]["left_click_action"] = lambda{ system "xeyes" }
1120 #plugin_config["standard:status"]["right_click_action"] = lambda{ system "xeyes" }
1121 #plugin_config["standard:status"]["middle_click_action"] = lambda{ system "xeyes" }
1123 plugin_config["standard:status"]["refresh_time"] = 1
1125 # Uncomment and change to override default text
1126 #currload = nil
1127 #Thread.new{ loop { currload = `uptime`.chomp.sub(/.*: /,"").gsub(/,/,""); sleep 10 } }
1128 #plugin_config["standard:status"]["text_proc"] = lambda do
1129 # "#{Time.new.strftime("%d/%m/%Y %X %Z")} #{currload}"
1130 #end
1132 plugin_config["standard"]["x-terminal-emulator"] = "x-terminal-emulator"
1134 plugin_config["standard:actions"]["history_size"] = 3 # set to 0 to disable
1135 plugin_config["standard:programs"]["history_size"] = 5 # set to 0 to disable
1137 plugin_config["standard:volume"]["mixer"] = "Master"
1139 plugin_config["standard:mode"]["mode_toggle_keys"] = ["MODKEY2-space"]
1141 plugin_config["standard:battery-monitor"]["statefile"] =
1142 '/proc/acpi/battery/BAT0/state'
1143 plugin_config["standard:battery-monitor"]["infofile"] =
1144 '/proc/acpi/battery/BAT0/info'
1145 plugin_config["standard:battery-monitor"]["low"] = 5
1146 plugin_config["standard:battery-monitor"]["low_action"] =
1147 'echo "Low battery" | xmessage -center -buttons quit:0 -default quit -file -'
1148 plugin_config["standard:battery-monitor"]["critical"] = 1
1149 plugin_config["standard:battery-monitor"]["critical_action"] =
1150 'echo "Critical battery" | xmessage -center -buttons quit:0 -default quit -file -'
1152 # Allows you to override the default internal actions and define new ones:
1153 #plugin_config["standard:actions"]["internal"].update({
1154 # "screenshot" => nil, # remove default screenshot action
1155 # "google" => lambda do |wmii, *selection|
1156 # require 'cgi'
1157 # if selection && !selection.empty?
1158 # selection = CGI.escape(selection.join(" "))
1159 # else
1160 # selection = CGI.escape(%!#{`wmiipsel`.strip}!)
1161 # end
1162 # url = "http://www.google.com/search?q=#{selection}"
1163 # case browser = ENV["BROWSER"]
1164 # when nil: system "wmiisetsid /etc/alternatives/x-www-browser '#{url}' &"
1165 # else system "wmiisetsid #{browser} '#{url}' &"
1166 # end
1167 # end,
1168 # "foo" => lambda do |wmii, *args|
1169 # IO.popen("xmessage -file -", "w"){|f| f.puts "Args: #{args.inspect}"; f.close_write }
1170 # end
1173 #{{{ Import bindings and bar applets
1174 from "standard" do
1175 use_bar_applet "volume", 999
1176 use_bar_applet "mode", 900
1177 use_bar_applet "status", 100
1178 #use_bar_applet "cpuinfo", 150
1179 #use_bar_applet "mpd", 110
1180 use_bar_applet "battery-monitor"
1182 use_binding "dict-lookup"
1183 use_binding "execute-program-with-tag"
1184 use_binding "execute-action"
1185 use_binding "execute-program"
1186 (0..9).each{|k| use_binding "numeric-jump-#{k}" }
1187 use_binding "tag-jump"
1188 use_binding "retag"
1189 use_binding "retag-jump"
1190 use_binding "namespace-retag"
1191 use_binding "namespace-retag-jump"
1192 (('a'..'z').to_a+('0'..'9').to_a).each{|k| use_binding "letter-jump-#{k}" }
1193 (0..9).each{|k| use_binding "numeric-retag-#{k}" }
1194 (('a'..'z').to_a+('0'..'9').to_a).each{|k| use_binding "letter-retag-#{k}" }
1195 use_binding "move-prev"
1196 use_binding "move-next"
1197 use_binding "namespace-move-prev"
1198 use_binding "namespace-move-next"
1199 use_binding "history-move-forward"
1200 use_binding "history-move-back"
1202 use_binding "bookmark"
1203 use_binding "bookmark-open"
1206 # {{{ del.icio.us bookmark import
1207 #plugin_config["standard:bookmark"]["del.icio.us-user"] = 'myusername'
1208 #plugin_config["standard:bookmark"]["del.icio.us-password"] = 'mypass'
1210 ## WORD OF CAUTION!
1211 ## Before setting the sync mode to :bidirectional, make sure
1212 ## that your bookmarks.txt file contains all the bookmarks you want to keep,
1213 ## because all the del.icio.us bookmarks not listed there will be deleted!
1214 ## You can import your del.icio.us bookmarks by setting it to
1215 ## :unidirectional and reloading wmiirc ("ALT-a wmiirc" by default).
1216 ## Allow some time for the bookmarks to be downloaded (wait until you see
1217 ## "Done importing bookmarks from del.icio.us." in
1218 ## $HOME/.wmii-3/wmiirc.log). You can then change the mode to :bidirectional
1219 ## and reload wmiirc. From that point on, the bookmark lists will be
1220 ## synchronized, so local modifications will be propagated to del.icio.us,
1221 ## and if you remove a bookmark locally it will also be deleted on
1222 ## del.icio.us.
1223 #plugin_config["standard:bookmark"]["del.icio.us-mode"] = :bidirectional
1224 #plugin_config["standard:bookmark"]["del.icio.us-share"] = true
1226 ## Sets the encoding used to:
1227 # * store the bookmark descriptions in bookmarks.txt
1228 # * present choices through wmiimenu
1229 # Please make sure your bookmarks.txt uses the appropriate encoding before
1230 # setting the next line. If you had already imported bookmarks from
1231 # del.icio.us, they will be stored UTF-8, so you might want to
1232 # recode utf-8..NEW_ENCODING bookmarks.txt
1234 # If left to nil, bookmarks imported from del.icio.us will be in UTF-8, and
1235 # those created locally will be in the encoding specified by your locale.
1236 #plugin_config["standard:bookmark"]["encoding"] = 'KOI8-R'
1238 # Allows you to override the default bookmark protocols and define new ones:
1239 #plugin_config["standard:bookmark"]["protocols"].update({
1240 # 'http' => nil, # remove default http protocol
1241 # 'ssh' => {
1242 # :open_urls => lambda do |wmii,bms|
1243 # term = wmii.plugin_config["standard"]["x-terminal-emulator"] || "xterm"
1244 # bms.each do |bm|
1245 # uri = bm[:uri]
1246 # ssh_host = uri.host
1247 # ssh_host = "#{uri.user}@" + ssh_host unless uri.user.nil?
1248 # ssh_port = "-p #{uri.port}" unless uri.port.nil?
1249 # system "wmiisetsid #{term} -T '#{bm[:bm].url}' -e 'ssh #{ssh_host} #{ssh_port} || read' &"
1250 # end
1251 # end,
1252 # :get_title => lambda do |wmii,uri|
1253 # title = uri.host
1254 # title = "#{uri.user}@" + title unless uri.user.nil?
1255 # title << ":#{uri.port.to_s}" unless uri.port.nil?
1256 # title
1257 # end
1258 # },
1259 # 'pdf' => {
1260 # :open_urls => lambda do |wmii,bms|
1261 # bms.each do |bm|
1262 # path = URI.unescape(bm[:uri].path)
1263 # LOGGER.info "Opening #{path} with xpdf."
1264 # system "wmiisetsid xpdf '#{path}' &"
1265 # end
1266 # end,
1267 # :get_title => lambda do |wmii,uri|
1268 # fname = File.basename(URI.unescape(uri.to_s)).gsub(/\.\S+$/,"")
1269 # [fname, fname.downcase, fname.capitalize]
1270 # end
1274 # {{{ Click on view bars
1275 on_barclick(/./, MOUSE_BUTTON_LEFT){|name,| view name}
1276 on_barclick(/./, MOUSE_BUTTON_RIGHT){|name,| view name}
1278 # {{{ Tag all browser instances as 'web' in addition to the current tag
1279 # browsers = %w[Firefox Konqueror]
1280 # browser_re = /^#{browsers.join("|")}/
1281 # on_createclient(condition{|c| browser_re =~ read("/client/#{c}/class")}) do |cid|
1282 # write("/client/#{cid}/tags", normalize(read("/client/#{cid}/tags") + "+web"))
1283 # end
1285 #{{{ Simpler key bindings --- not defined in plugins
1286 on_key("MODKEY-LEFT"){ write "/view/ctl", "select prev" }
1287 on_key("MODKEY-RIGHT"){ write "/view/ctl", "select next" }
1288 on_key("MODKEY-DOWN"){ write "/view/sel/ctl", "select next" }
1289 on_key("MODKEY-UP"){ write "/view/sel/ctl", "select prev" }
1290 on_key("MODKEY-space"){ write "/view/ctl", "select toggle" }
1291 on_key("MODKEY-Shift-d"){ write "/view/sel/mode", "default" }
1292 on_key("MODKEY-s"){ write "/view/sel/mode", "stack" }
1293 on_key("MODKEY-Shift-m"){ write "/view/sel/mode", "max" }
1294 on_key("MODKEY-Shift-f"){ write "/view/0/sel/geom", "0 0 east south" }
1295 on_key("MODKEY-i"){ write "/view/sel/sel/geom", "+0 +0 +0 +48" }
1296 on_key("MODKEY-Shift-i"){ write "/view/sel/sel/geom", "+0 +0 +0 -48" }
1297 on_key("MODKEY-Return") do
1298 term = plugin_config["standard"]["x-terminal-emulator"] || "xterm"
1299 system "wmiisetsid #{term} &"
1301 on_key("MODKEY-Shift-LEFT"){ write "/view/sel/sel/ctl", "sendto prev" }
1302 on_key("MODKEY-Shift-RIGHT"){ write "/view/sel/sel/ctl", "sendto next" }
1303 on_key("MODKEY-Shift-DOWN"){ write "/view/sel/sel/ctl", "swap down" }
1304 on_key("MODKEY-Shift-UP"){ write "/view/sel/sel/ctl", "swap up" }
1305 on_key("MODKEY-Shift-space"){ write "/view/sel/sel/ctl", "sendto toggle" }
1306 on_key("MODKEY-Shift-c"){ write "/view/sel/sel/ctl", "kill" }
1307 on_key("MODKEY-r"){ view prev_view }
1308 on_key("MODKEY-Control-LEFT") { write "/view/sel/sel/ctl", "swap prev" }
1309 on_key("MODKEY-Control-RIGHT"){ write "/view/sel/sel/ctl", "swap next" }
1312 # {{{ ======== CONFIGURATION ENDS HERE ==============