Scripting the wmii-3 window manager with Ruby
UPDATE Much improved script released.
It's been over one week since the release of wmii-3, the dynamic window manager thats boasts
Wimp (Windows, Icons, Menus and Pointing device) is dead!
It took me one day to grow accustomed to the new tag-based workspaces, and a couple to stabilize my setup.
wmii ships with a few shell scripts that control the behavior of the WM: key bindings, menu bar, default options... I was more than willing to rewrite them in Ruby in order to get rid of that echo, sed, awk, etc. mess. In the process I abstracted things a little bit, making it easier to handle the events reported by the WM.
I can now define basic keybindings as follows:
on_key("#{MODKEY}-#{LEFT}"){ write "/view/ctl", "select prev" } on_key("#{MODKEY}-#{RIGHT}"){ write "/view/ctl", "select next" } on_key("#{MODKEY}-#{DOWN}"){ write "/view/sel/ctl", "select next" } on_key("#{MODKEY}-#{UP}"){ write "/view/sel/ctl", "select prev" }
Other events are easily defined too. This is my "volume applet":
on_barclick(volume_name, MOUSE_SCROLL_UP){ update_volume[+1] } on_barclick(volume_name, MOUSE_SCROLL_DOWN){ update_volume[-1] }
The good thing about using Ruby is that adding extra logic isn't a PITA, as it is in shell script, at least for me. The following snippet allows me to retag the current client partially, replacing only the numerical tag and leaving the other ones untouched:
(0..9).each do |key| on_key("#{MODKEY}-Shift-#{key}") do oldtags = read("/view/sel/sel/tags").split(/\+/).reject{|x| /^\d+$/ =~ x} write "/view/sel/sel/tags", (oldtags + [key]).join("+") end end
Finally, here's something I'd been wanting for some time but never got to implement before: a global keybinding for quick dictionary lookup without ever using the pointing device.
dict_name = "80_dict" setup_bar(dict_name, WMII_NORMCOLORS, "DICT") dict_ask_and_define = lambda do fork do phrase = `wmiimenu < /dev/null` system "dcop kdict KDictIface definePhrase '#{phrase}'" write "/ctl", "view dict" end end on_key("#{MODKEY}-Control-d"){ dict_ask_and_define.call } on_barclick(dict_name, MOUSE_BUTTON_LEFT){ dict_ask_and_define.call } on_barclick(dict_name, MOUSE_BUTTON_RIGHT){ write "/ctl", "view dict" }
I guess this makes me a weirdo (maybe even more so amongst Rubyists, which would often kill for a Mac --- their third or fourth one) but I much prefer this to Exposé+Dock+Dashboard and all that eye candy (not that I've used it that much, but more than it took me to like wmii --- maybe the learning curve is too steep? :-P).
Code
The script is quite long --- much longer than the original shell script (of course, it also does much more). But I find it's way easier to extend, especially if I want stateful event handling and non-trivial logic.
I used to have a Ruby extension to use the IXP IPC, but I haven't recompiled it against the latest libixp and the wmiir tool does the work nicely (if slowly) for now:
#!/usr/bin/env ruby require 'logger' logfile = File.open(File.join(ENV["HOME"], ".wmii-3", "wmiirc.log"), "a") logfile.sync = true STDOUT.reopen(logfile) STDERR.reopen(logfile) LOGGER = Logger.new(STDERR) LOGGER.info "wmiirc init" WMIIRC_HOME = ENV["HOME"] + "/.wmii-3" class EventReactor class Handler attr_reader :type def initialize(type, &block) @type = type @block = block end def call(*a) @block.call(*a) end end class KeyHandler < Handler attr_reader :key def initialize(key, &block) @key = key super("Key", &block) end end def initialize(&block) @event_source = open("|wmiir read /event") @procs = {"BarClick" => [], "ClientClick" => [], "ClientFocus" => [], "CreateClient" => [], "Key" => Hash.new{|h,k| h[k] = []}} @children = [@event_source.pid] # reset key bindings IO.popen("wmiir write /def/keys", "w"){|io| io.puts } instance_eval(&block) if block_given? end def register(type, param1 = nil, param2 = nil, &block) raise "Unknown type" unless @procs.has_key?(type) case param1 when nil handler = Handler.new(type, &block) else handler = Handler.new(type) do |*args| if param1 === args[0] && (!param2 || param2 === args[1]) block.call(*args) end end end @procs[type] << handler handler end def unregister(handler) case handler when KeyHandler @procs["Key"][handler.key] else @procs[handler.type].delete handler end end def main_loop # register key bindings IO.popen("wmiir write /def/keys", "w"){|io| io.puts @procs["Key"].keys } # wait for events while line = @event_source.gets LOGGER.debug "Got #{line.inspect} from #{@event_source.pid}" case line when /(BarClick|ClientClick)\s+(\S+)\s+(\S+)$/ @procs[$1].each{|x| x.call($2, $3.to_i)} when /(ClientFocus|CreateClient)\s+(\S+)/ @procs[$1].each{|x| x.call($2.to_i)} when /Key (\S+)$/ @procs["Key"][$1].each{|x| x.call(x)} if @procs["Key"].has_key?($1) when /Bye/ break end end end # not worth meta-programming def on_barclick(name = nil, button = nil, &b) register("BarClick", name, button, &b) end def on_clientclick(client = nil, button = nil, &b) register("ClientClick", client, button, &b) end def on_clientfocus(client = nil, &b); register("ClientFocus", client, &b) end def on_createclient(client = nil, &b); register("CreateClient", client, &b) end def on_key(key, &block) handler = KeyHandler.new("Key", &block) @procs["Key"][key] << handler handler end def clean_fork(&b) @children << fork(&b) LOGGER.info "clean_fork(): #{@children.last}" end def cleanup LOGGER.info "This is #{Process.pid}, killing #{@children.inspect}" @children.each do |pid| begin Process.kill("TERM", pid) rescue Exception end end end end module WMII MOUSE_BUTTON_LEFT = 1 MOUSE_BUTTON_MIDDLE = 2 MOUSE_BUTTON_RIGHT = 3 MOUSE_SCROLL_UP = 4 MOUSE_SCROLL_DOWN = 5 def write(file, contents) IO.popen("wmiir write #{file}", "w"){|io| io.print contents.to_s } true rescue Errno::EPIPE return false end def read(file) `wmiir read #{file}` end def remove(file) system("wmiir remove #{file}") end def create(file) system("wmiir create #{file}") end def setup_bar(name, colors, data = "") remove "/bar/#{name}" create "/bar/#{name}" write "/bar/#{name}/colors", colors write "/bar/#{name}/data", data.chomp end def view(viewname) write "/ctl", "view #{viewname.to_s.chomp}" end def wmiimenu(options, &block) fork do chosen = IO.popen("wmiimenu", "r+") do |f| f.puts options f.close_write f.read end.chomp yield chosen if block_given? LOGGER.debug "wmiimenu(#{chosen.inspect}) finished" end end def program_list @__program_list_last_update ||= Time.at(0) @__program_list ||= [] path_glob = ENV["PATH"].gsub(/,/, '\\,').tr(":",",") return @__program_list if Time.new - @__program_list_last_update < 3600 @__program_list_last_update = Time.new @__program_list = Dir.glob("{#{path_glob}}/*").select do |fname| File.file?(fname) && File.executable?(fname) end.map{|fname| File.basename(fname)}.sort.uniq end def action_list Dir["#{ENV["HOME"]}/.wmii-3/*"].select do |f| File.file?(f) && File.executable?(f) end.map{|f| File.basename(f)}.sort.uniq end def condition(&block) c = lambda(&block) def c.===(*x); call(*x) end c end end include WMII LOGGER.info "loading configuration" # {{{ ======== CONFIGURATION BEGINS HERE ============== MODKEY = "Mod1" UP = "k" DOWN = "j" LEFT = "h" RIGHT = "l" WMII_FONT = 'fixed' #WMII_SELCOLORS = '#ffffff #285577 #4c7899' #WMII_NORMCOLORS = '#222222 #eeeeee #666666' # dark background #WMII_NORMCOLORS ='#e0e0e0 #0a0a0a #202020' #WMII_FONT='-adobe-helvetica-bold-r-normal-*-12-120-75-75-p-70-iso8859-1' #WMII_SELCOLORS='#ffffff #dd0fdd #6600ff' #WMII_NORMCOLORS='#ffddff #000000 #dd0fdd' WMII_SELCOLORS='#ffffff #4682b4 #cccccc' WMII_NORMCOLORS='#ffffff #36648b #909090' XMESSAGEBOX = "xmessage -center -buttons quit:0 -default quit -file -" write "/event", "Bye\n" # {{{ EventReactor reactor = EventReactor.new do |e| # {{{ WM CONFIGURATION write "/def/border", 1 write "/def/font", WMII_FONT write "/def/selcolors", WMII_SELCOLORS write "/def/normcolors", WMII_NORMCOLORS write "/def/colmode", "default" write "/def/colwidth", 0 write "/def/rules", <<EOF /Kdict.*/ -> dict /XMMS.*/ -> ~ /Gimp.*/ -> ~ /MPlayer.*/ -> ~ /XForm.*/ -> ~ /.*/ -> ! /.*/ -> 1 EOF # {{{ Signal we're about to start to the previous wmiirc process, or wait # until the IXP connection is established loop{ write "/event", "Starting" and break } # {{{ MOD-Control-y prefix to create in new view e.on_key "#{MODKEY}-Control-y" do handler = on_createclient do |cid| last_tag = read("/tags").select{|x| /^[0-9]+$/ =~ x}.sort.last new_tag = last_tag.to_i + 1 old_tags = read("/client/#{cid}/tags").split(/\+/).reject{|x| /^[0-9]+/ =~ x} write("/client/#{cid}/tags", (old_tags + [new_tag]).join("+")) e.unregister handler write("/ctl", "view #{new_tag}") end system("wmiisetsid `wmiimenu < /tmp/.wmiimenu.$USER.proglist` &") end # {{{ Volume control volume_name = "99_volume" setup_bar(volume_name, WMII_NORMCOLORS) update_volume = lambda do |increment| sign = increment < 0 ? "-" : "+" volume = `amixer set PCM,0 #{increment.abs}#{sign}`[/\[(\d+%)\]/] write("/bar/#{volume_name}/data", "VOL #{volume}") end Thread.new{ loop { update_volume[0]; sleep 10 } } on_barclick(volume_name, MOUSE_SCROLL_UP){ update_volume[+1] } on_barclick(volume_name, MOUSE_SCROLL_DOWN){ update_volume[-1] } # {{{ Dictionary dict_name = "80_dict" setup_bar(dict_name, WMII_NORMCOLORS, "DICT") dict_ask_and_define = lambda do fork do phrase = `wmiimenu < /dev/null` system "dcop kdict KDictIface definePhrase '#{phrase}'" write "/ctl", "view dict" end end on_key("#{MODKEY}-Control-d"){ dict_ask_and_define.call } on_barclick(dict_name, MOUSE_BUTTON_LEFT){ dict_ask_and_define.call } on_barclick(dict_name, MOUSE_BUTTON_RIGHT){ write "/ctl", "view dict" } # {{{ Status bar status_name = "00_status" setup_bar(status_name, WMII_NORMCOLORS, "STATUS BAR --- init") clean_fork do sleep 2 loop do currload = `uptime`.sub(/.*: /,"").gsub(/,/,"") text= "#{Time.new.strftime("%d/%m/%Y %X %Z")} #{currload}" write("/bar/#{status_name}/data", text.chomp) sleep 1 end end on_barclick(status_name) do |name, button| views = read("/tags").map{|x| x.chomp} current = views.index read("/view/name") case button.to_i when MOUSE_BUTTON_LEFT: system "tzwatch | #{XMESSAGEBOX} &" when MOUSE_BUTTON_MIDDLE: system "x-terminal-emulator -C top &" when MOUSE_BUTTON_RIGHT: system "ncal -y | #{XMESSAGEBOX} &" when MOUSE_SCROLL_UP: write "/ctl", "view #{views[current-1] || views[-1]}" when MOUSE_SCROLL_DOWN: write "/ctl", "view #{views[current+1] || views[0]}" end end # {{{ Click on view bars all_but_status = /^(?!#{Regexp.escape(status_name)})/ on_barclick(all_but_status, MOUSE_BUTTON_LEFT){|name,| view name} on_barclick(all_but_status, MOUSE_BUTTON_RIGHT){|name,| view name} # {{{ Tag all konqueror instances as 'konqueror' e.on_createclient(condition{|c| /^Konqueror:/ =~ read("/client/#{c}/class")}) do |cid| write("/client/#{cid}/tags", (read("/client/#{cid}/tags").split(/\+/) + ["konqueror"]).join("+")) end # {{{ Key bindings on_key("#{MODKEY}-#{LEFT}"){ write "/view/ctl", "select prev" } on_key("#{MODKEY}-#{RIGHT}"){ write "/view/ctl", "select next" } on_key("#{MODKEY}-#{DOWN}"){ write "/view/sel/ctl", "select next" } on_key("#{MODKEY}-#{UP}"){ write "/view/sel/ctl", "select prev" } on_key("#{MODKEY}-space"){ write "/view/ctl", "select toggle" } on_key("#{MODKEY}-d"){ write "/view/sel/mode", "default" } on_key("#{MODKEY}-s"){ write "/view/sel/mode", "stack" } on_key("#{MODKEY}-m"){ write "/view/sel/mode", "max" } on_key("#{MODKEY}-f"){ write "/view/0/sel/geom", "0 0 east south" } on_key("#{MODKEY}-a") do internal_actions = { "browser" => lambda do selection = `wmiipsel`.strip case browser = ENV["BROWSER"] when nil: system "/etc/alternatives/x-www-browser '#{selection}' &" else system "#{browser} '#{selection}' &" end end, "google" => lambda do require 'cgi' selection = CGI.escape(%!"#{`wmiipsel`.strip}"!) url = "http://www.google.com/search?q=#{selection}" case browser = ENV["BROWSER"] when nil: system "/etc/alternatives/x-www-browser '#{url}' &" else system "#{browser} '#{url}' &" end end } wmiimenu((action_list + internal_actions.keys).sort) do |choice| if internal_actions[choice] internal_actions[choice].call else system("$HOME/.wmii-3/#{choice} &") if /^\s*$/ !~ choice end end end on_key("#{MODKEY}-p") do wmiimenu(program_list){|prog| system("#{prog} &") if /^\s*$/ !~ prog } end on_key("#{MODKEY}-t") do wmiimenu(read("/tags")){|new_view| view(new_view)} end (0..9).each{|key| on_key("#{MODKEY}-#{key}"){ view(key) } } on_key("#{MODKEY}-Return"){ system "x-terminal-emulator &" } on_key("#{MODKEY}-Shift-#{LEFT}"){ write "/view/sel/sel/ctl", "sendto prev" } on_key("#{MODKEY}-Shift-#{RIGHT}"){ write "/view/sel/sel/ctl", "sendto next" } on_key("#{MODKEY}-Shift-#{DOWN}"){ write "/view/sel/sel/ctl", "swap down" } on_key("#{MODKEY}-Shift-#{UP}"){ write "/view/sel/sel/ctl", "swap up" } on_key("#{MODKEY}-Shift-space"){ write "/view/sel/sel/ctl", "sendto toggle" } on_key("#{MODKEY}-Shift-c"){ write "/view/sel/sel/ctl", "kill" } on_key("#{MODKEY}-Shift-t") do wmiimenu(read("/tags")){|new_tags| write "/view/sel/sel/tags", new_tags } end (0..9).each do |key| on_key("#{MODKEY}-Shift-#{key}") do oldtags = read("/view/sel/sel/tags").split(/\+/).reject{|x| /^\d+$/ =~ x} write "/view/sel/sel/tags", (oldtags + [key]).join("+") end end on_key("#{MODKEY}-Control-#{LEFT}"){ write "/view/sel/sel/ctl", "swap prev" } on_key("#{MODKEY}-Control-#{RIGHT}"){ write "/view/sel/sel/ctl", "swap next" } get_view_data = lambda do views = read("/tags").map{|x| x.chomp} current = views.index read("/view/name") [views, current] end on_key("#{MODKEY}-Control-#{DOWN}") do views, current = get_view_data.call view(views[current-1] || views[-1]) end on_key("#{MODKEY}-Control-#{UP}") do views, current = get_view_data.call view(views[current+1] || views[0]) end end # {{{ ==== main loop system "xsetroot -solid '#333333'" system "x-terminal-emulator &" sleep 0.2 system "kdict &" LOGGER.info "Executing main loop..." reactor.main_loop write "/event", "Bye\n" reactor.cleanup LOGGER.info "NORMAL EXIT"
_why bindings - Tom (2006-06-18 (Sun) 13:46:21)
mfp: Actually, yes: http://redhanded.hobix.com/inspect/aFewWmii3Hacks.html
And thanks from me for this code; as it's an excellent starting point for Ruby+WMII-3!
mfp 2006-06-18 (Sun) 17:23:59
yup, saw that
Glad you liked it... there's some more coming :)
I have evolved my wmiirc (I'd added several bindings since I posted this; I saw some of them reimplemented on redhanded --- we're all walking similar paths in the usability space :) and will probably release something tomorrow. I also have an IXP extension built on top of wmii's libixp, but there are still a couple pending issues.
so fantastic - why (2006-06-03 (Sat) 19:40:53)
thanks, mfp. you have me hooked. i've been playing with this all day.
mfp 2006-06-05 (Mon) 15:43:07
Hey _why, came up with any interesting binding? (<- high expectations)
Keyword(s):[ruby] [blog] [frontpage] [wmii] [wm] [script] [wmiirc]
References:[Ruby] [Automagic (generalizable) working set discovery, improved ruby-wmii WM scripting 0.2.1]