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
22 This makes upgrades easier, since you can overwrite the wmiirc script with a
23 newer version while all your settings are preserved in
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).
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
37 to install wmiirc and the standard plugin.
38 Were any further step necessary, it'd be noted in README.upgrade.
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
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
82 # override position only
83 use_bar_applet("somebody@example.com:cool-bar-applet", 50)
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)
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.
100 ---------------------
102 Some plugins can be configured via the plugin_config hash.
104 #{WMIIRC_CONFIG_FILE}
107 Read standard-plugin.rb to see the options accepted by the standard
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
147 use_binding("my-email@address.com:do-magic")
148 use_bar_applet("my-email@addres.com:applet")
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.
163 Mauricio Fernandez <mfp@acm.org> http://eigenclass.org
167 END
{ "wmiirc #{Process.pid} finishing, $! is #{$!.inspect}" }
170 unless File
.exist
?(WMIIRC_CONFIG_FILE
)
171 File
.open(WMIIRC_CONFIG_FILE
, "w"){|f
| f
.puts
DATA.read
}
173 IO
.popen("xmessage -file -", "w"){|f
| f
.puts WMIIRC_HELP_MESSAGE
; f
.close_write
}
177 LOGGER
.info
"Loading standard plugin"
179 load File
.join(WMIIRC_PLUGIN_DIR
, "standard-plugin.rb"), true
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}"
190 # see if the standard plugin is available
191 if Plugin
.registered_plugins
["standard"].empty
?
193 IO
.popen("xmessage -file -", "w") do |f
|
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.
205 load WMIIRC_CONFIG_FILE
207 Thread
.abort_on_exception
= false
210 LOGGER
.info
"Running START_PROGS:"
211 if defined? START_PROGS
212 START_PROGS
.each
{|line
| LOGGER
.info
"Executing #{line}"; system(line
)}
216 LOGGER
.info
"Executing main loop..."
217 WMII
::Configuration.last_instance
.main_loop
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
230 STDOUT.reopen(logfile
)
231 STDERR.reopen(logfile
)
232 LOGGER
= Logger
.new(STDERR)
234 LOGGER
.level
= Logger
::INFO
236 # IXP extension not ready for prime-time
237 $pure_ruby_ixp_needed = !USE_IXP_EXTENSION
239 $
:.unshift
ENV["HOME"] + "/.wmii-3/ext/IXP"
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
248 if $pure_ruby_ixp_needed
250 IXPError
= Class
.new(StandardError
)
251 BrokenPipeError
= Class
.new(IXPError
)
252 BusyError
= Class
.new(IXPError
)
254 def initialize(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
}
266 `wmiir -a #{@address} read #{file}`
270 system("wmiir -a #{@address} remove #{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}"
281 is
.fcntl Fcntl
::F_SETFD, Fcntl
::FD_CLOEXEC
291 end # if $pure_ruby_ixp_needed
296 def initialize(address
)
301 %w
[write read remove create
].each
do |meth
|
302 define_method(meth
) do |*args
|
305 @client.__send__(meth
, *args
)
306 rescue IXP
::BrokenPipeError, SystemCallError
307 LOGGER
.debug
"Restablishing connection..."
309 retry if (max_attempts
-= 1) > 0
316 def foreach(file
, &block
)
317 @client.foreach(file
, &block
)
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
331 MOUSE_BUTTON_LEFT
= 1
332 MOUSE_BUTTON_MIDDLE
= 2
333 MOUSE_BUTTON_RIGHT
= 3
335 MOUSE_SCROLL_DOWN
= 5
337 module ConfigurationHelper
338 def def_conf_var(*names
)
340 define_method(name
) do |*val
|
342 when nil: instance_variable_get("@#{name}")
343 else instance_variable_set("@#{name}", val
[0])
349 def def_wmii_var(*names
, &block
)
351 define_method(name
) do |*val
|
353 when nil: @ixp_conn.read
"/def/#{name}"
355 @ixp_conn.write
"/def/#{name}", val
[0]
356 block
.call val
[0] if block
365 attr_reader
:registered_plugins
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
)
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
400 extend ConfigurationHelper
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
)
433 DEFAULT_EVENTS
= %w
[BarClick ClientClick ClientFocus CreateClient Key Bye Starting
]
434 attr_reader
:plugin_config, :prev_view
439 def initialize(type
, &block
)
444 def call(*a
); @block.call(*a
) end
447 class KeyHandler
< EventHandler
450 def initialize(key
, &block
)
456 class PluginConfigHash
458 @opts = Hash
.new
{|h
,k
| h
[k
] = {} }
461 def [](x
); @opts[x
] end
462 def []=(x
, val
); @opts[x
] = val
end
466 attr_reader
:last_instance
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."
481 @ixp_conn = IXP
::Client.new
ENV["WMII_ADDRESS"]
482 rescue SystemCallError
483 LOGGER
.info
"IXP server not up yet, waiting..."
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
492 Timeout
.timeout(0.5) do
494 quittxt
= @ixp_conn.read
"/bar/QUIT/data" rescue ""
496 LOGGER
.debug
"Previous wmiirc process signalled termination."
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
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)
560 @ixp_conn.remove(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
)
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
)
590 LOGGER
.error
"Ignoring illegal binding name #{binding_name}."
593 namespace
, name
= md
.captures
594 if (plugins
= Plugin
.registered_plugins
[namespace
]).empty
?
595 LOGGER
.error
"Unknown plugin #{namespace}"
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}."
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
)
617 LOGGER
.error
"Ignoring illegal applet name #{bar_applet_name}."
620 namespace
, name
= md
.captures
621 if (plugins
= Plugin
.registered_plugins
[namespace
]).empty
?
622 LOGGER
.error
"Unknown plugin #{namespace}"
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}."
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
)
649 LOGGER
.error
"Ignoring illegal feature name #{feature_name}."
652 namespace
, name
= md
.captures
653 if (plugins
= Plugin
.registered_plugins
[namespace
]).empty
?
654 LOGGER
.error
"Unknown plugin #{namespace}"
658 features
= plugins
.inject([]){|s
,x
| s
+ [x
.features
[name
]]}.compact
660 LOGGER
.error
"Feature #{name} not found in #{namespace}."
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}"
673 def register(type
, param1
= nil, param2
= nil, &block
)
676 handler
= EventHandler
.new(type
, &block
)
678 handler
= EventHandler
.new(type
) do |*args
|
679 if param1
=== args
[0] && (!param2
|| param2
=== args
[1])
684 (@procs[type
] ||= []) << handler
686 update_custom_handlers_matcher
691 def unregister(handler
)
694 @procs["Key"][handler
.key
].delete handler
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")
708 @ixp_conn.write
"/event", "Bye\n"
709 register_key_bindings
713 times
.unshift Time
.new
715 if times
[4] && times
[0] - times
[4] < 0.1
717 LOGGER
.error
"wmiiwm seems to be gone, leaving"
721 LOGGER
.debug
"Opening /event"
722 IXP
::Client.new(ENV["WMII_ADDRESS"]).foreach("/event") do |line
|
724 LOGGER
.debug
"Got #{line.inspect}" if 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
)}
731 @procs["Key"][$1].each
{|x
| x
.call(self, x
)} if @procs["Key"].has_key
?($1)
732 when /^Bye|^Starting/
733 LOGGER
.info
"Cleanup..."
736 create
"/bar/QUIT" rescue nil
737 write
"/bar/QUIT/data", "QUIT"
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
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."
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
)
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
794 @children << fork(&b
)
795 Process
.detach
@children.last
796 LOGGER
.debug
"clean_fork(): #{@children.last}"
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
|
807 Process
.kill("TERM", pid
)
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
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
835 def initialize(wmiiconfig
, name
)
836 @wmiiconfig = wmiiconfig
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).
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
|
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
886 yield self, BarButton
.new(self, name
)
891 tags
= tags
.split(/\+/) unless Array
=== tags
892 tags
.map
{|x
| x
.chomp
}.sort
.uniq
.compact
.grep(/./).join("+")
896 @ixp_conn.read("/tags").split(/\n/).grep(/./)
899 def views_intellisort
900 views
.sort
.sort_by
do |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
]
915 @ixp_conn.read("/view/name").chomp
918 def view_history_forward
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
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
968 views
.index(curr_view
)
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]]
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
)
1011 rd
.fcntl Fcntl
::F_SETFD, Fcntl
::FD_CLOEXEC
1012 wr
.fcntl Fcntl
::F_SETFD, Fcntl
::FD_CLOEXEC
1016 chosen
= IO
.popen("wmiimenu", "r+") do |f
|
1017 f
.puts options
unless options
.nil?
1021 LOGGER
.debug
"wmiimenu(#{chosen.inspect}) finished"
1025 rescue Exception
# catch EPIPE etc.
1027 yield chosen
if block_given
?
1033 def rd
.value
; ret
= (gets
||"").chomp
; close
; ret
end
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
)
1045 def c
.===(*x
); call(*x
) end
1057 # {{{ ======== ruby-wmii CONFIGURATION BEGINS HERE ==============
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
1067 xsetroot -solid '#333333'
1070 # {{{ WM CONFIGURATION
1071 WMII
::Configuration.define
do
1074 selcolors
'#FFFFFF #248047 #147027'
1075 normcolors
'#4D4E4F #DDDDAA #FFFFCC'
1091 # Translate the following names in the on_key and use_binding definitions.
1092 key_subs
:MODKEY => :Mod1,
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
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
1117 # Uncomment and change to override default on_click actions for the status
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
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}"
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|
1157 # if selection && !selection.empty?
1158 # selection = CGI.escape(selection.join(" "))
1160 # selection = CGI.escape(%!#{`wmiipsel`.strip}!)
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}' &"
1168 # "foo" => lambda do |wmii, *args|
1169 # IO.popen("xmessage -file -", "w"){|f| f.puts "Args: #{args.inspect}"; f.close_write }
1173 #{{{ Import bindings and bar applets
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"
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'
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
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
1242 # :open_urls => lambda do |wmii,bms|
1243 # term = wmii.plugin_config["standard"]["x-terminal-emulator"] || "xterm"
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' &"
1252 # :get_title => lambda do |wmii,uri|
1254 # title = "#{uri.user}@" + title unless uri.user.nil?
1255 # title << ":#{uri.port.to_s}" unless uri.port.nil?
1260 # :open_urls => lambda do |wmii,bms|
1262 # path = URI.unescape(bm[:uri].path)
1263 # LOGGER.info "Opening #{path} with xpdf."
1264 # system "wmiisetsid xpdf '#{path}' &"
1267 # :get_title => lambda do |wmii,uri|
1268 # fname = File.basename(URI.unescape(uri.to_s)).gsub(/\.\S+$/,"")
1269 # [fname, fname.downcase, fname.capitalize]
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"))
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 ==============