wmiirc-rumai

git clone git://oldgit.suckless.org/wmiirc-rumai/
Log | Files | Refs | README | LICENSE

commit 5051f8d830e933f9d6aa80805783e7c7ea10aefa
parent 122dcdebdffc10c8a795a9587dfd785b8bc5a22e
Author: Suraj N. Kurapati <sunaku@gmail.com>
Date:   Sat,  9 May 2009 23:26:29 -0700

migrate to a YAML-based configuration file

Rumai 3.0.0 is now required

add new functionality from default wmiirc

rename show_menu() to key_menu()

add click_menu() which runs wmii9menu

Button class now shows errors as its label

improve MPD integration as a status bar button

Diffstat:
README | 28+++++++++++++++++++++++++++-
config.rb | 950+++++++++++++++++++++----------------------------------------------------------
config.yaml | 750+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
wmiirc | 181+++++++++++++++++++++----------------------------------------------------------
4 files changed, 1076 insertions(+), 833 deletions(-)

diff --git a/README b/README @@ -1,7 +1,14 @@ -This is my Ruby-based wmii configuration, explained in these articles: +sunaku's Ruby wmiirc +==================== + +This is my wmii configuration, described in these articles: + + http://wmii.suckless.org/alternative_wmiirc_scripts http://snk.tuxfamily.org/lib/rumai/ + http://article.gmane.org/gmane.comp.window-managers.wmii/1704 + http://snk.tuxfamily.org/web/2006-07-01-wmii-3-1-configuration-in-ruby.html Dependencies: @@ -20,3 +27,22 @@ Installation: # enjoy the goods! ~/.wmii-hg/wmiirc + +Documentation: + + # see list of all key bindings + grep 'Mod1.*#' ~/.wmii-hg/config.yaml + + # read the configuration file + less ~/.wmii-hg/config.yaml + +Configuration: + + Edit the configuration file to your liking. + + Run ~/.wmii-hg/wmiirc to apply your changes. + +Questions: + + Send me an e-mail; see LICENSE for my address. + diff --git a/config.rb b/config.rb @@ -1,748 +1,302 @@ -# Ruby-based configuration file for wmii. - -################################################################################ -# GENERAL CONFIGURATION -################################################################################ - -module Key - MOD = 'Mod1' - UP = 't' - DOWN = 'n' - LEFT = 'h' - RIGHT = 's' - - PREFIX = MOD + '-Control-' - FOCUS = PREFIX - SEND = PREFIX + 'm,' - SWAP = PREFIX + 'w,' - ARRANGE = PREFIX + 'z,' - GROUP = PREFIX + 'g,' - VIEW = PREFIX + 'v,' - MENU = PREFIX - EXECUTE = PREFIX -end - -module Mouse - PRIMARY = 1 - MIDDLE = 2 - SECONDARY = 3 - SCROLL_UP = 4 - SCROLL_DOWN = 5 -end - -module Color - { # Color tuples are "<text> <background> <border>" - :NORMCOLORS => NORMAL = '#e0e0e0 #0a0a0a #202020', - :FOCUSCOLORS => FOCUSED = '#ffffff #285577 #4c7899', - :BACKGROUND => BACKGROUND = '#333333', - }.each_pair do |k, v| - ENV["WMII_#{k}"] = v - end -end - -WMII_FONT = '-*-bitstream vera sans mono-medium-r-*-*-16-*-*-*-*-*-*-*' - - -################################################################################ -# DETAILED CONFIGURATION -################################################################################ - -# WM Configuration -fs.ctl.write <<EOF -grabmod #{Key::MOD} -border 2 -font #{WMII_FONT} -focuscolors #{Color::FOCUSED} -normcolors #{Color::NORMAL} -EOF - -# Column Rules -fs.colrules.write <<EOF -/./ -> 50+50 -EOF - -# Tagging Rules -fs.tagrules.write <<EOF -/Deluge/ -> tor -/Buddy List.*/ -> chat -/XChat.*/ -> chat -/Thunderbird.*/ -> mail -/Liferea.*/ -> mail -/Gimp.*/ -> gimp -/xconsole.*/ -> ~ -/alsamixer.*/ -> ~ -/QEMU.*/ -> ~ -/XMMS.*/ -> ~ -/MPlayer.*/ -> ~ -/.*/ -> ! -/.*/ -> 1 -EOF - -# events - event :CreateTag do |tag| - bar = fs.lbar[tag] - bar.create - bar.write "#{Color::NORMAL} #{tag}" - end - - event :DestroyTag do |tag| - fs.lbar[tag].remove - end - - event :FocusTag do |tag| - fs.lbar[tag].write "#{Color::FOCUSED} #{tag}" - end - - event :UnfocusTag do |tag| - btn = fs.lbar[tag] - btn.write "#{Color::NORMAL} #{tag}" if btn.exist? - end +# DSL for wmiirc configuration. +#-- +# Copyright 2006 Suraj N. Kurapati +# See the LICENSE file for details. +#++ - # Urgent 0x36003d4 Client - # UrgentTag Client chat - event :UrgentTag do |what, tag| - btn = fs.lbar[tag] - btn.write "*#{tag}" if btn.exist? - end - - # NotUrgent 0x36003d4 Client - # NotUrgentTag Client chat - event :NotUrgentTag do |what, tag| - btn = fs.lbar[tag] - btn.write tag if btn.exist? - end - - event :LeftBarClick do |button, view_id| - case button.to_i - when Mouse::PRIMARY - focus_view view_id +require 'rubygems' +gem 'rumai', '~> 3' +require 'rumai' - when Mouse::MIDDLE - # add the grouping onto the clicked view - grouping.each do |c| - c.tag view_id - end - - when Mouse::SECONDARY - # remove the grouping from the clicked view - grouping.each do |c| - c.untag view_id - end - end - end +include Rumai - event :ClientClick do |client_id, button| - case button.to_i - when Mouse::SECONDARY - # toggle the clicked client's grouping - Client.toggle_group client_id - end +class Handler < Hash + def initialize + super {|h,k| h[k] = [] } end - # wmii puts subsequent firefox instances on the same - # view as the first instance. bypass this and move - # the newly created firefox to the current view + ## + # If a block is given, registers a handler + # for the given key and returns the handler. # - # this "feature" applies to all programs - # that provide window grouping hints. so - # we'll just have this event handler - # bypass the feature for ALL programs! :-( - event :CreateClient do |id| - c = Client.new(id) - - case c.props.read - when /(Firefox|Gran Paradiso) - Restore Previous Session/ - c.tags = 'web' - when /:(Firefox|Gran Paradiso|jEdit|Epiphany|Thunar)/i - c.tags = curr_tag - c.focus - end - end - -# actions - action :rehash do - @program_menu = find_programs ENV['PATH'].squeeze(':').split(':') - @action_menu = find_programs File.dirname(__FILE__) - end - - action :kill do - fs.ctl.write 'quit' - end - - action :quit do - action :clear - action :kill - end - - action :clear do - # firefox's restore session feature doesn't - # work unless the whole process is killed. - system 'killall firefox firefox-bin thunderbird thunderbird-bin deluge' - - # gnome-panel refuses to die by other means - system 'killall -s TERM gnome-panel' + # Otherwise, executes all handlers registered for the given key. + # + def handle key, *args, &block + if block + self[key] << block - until (clients = Rumai.clients).empty? - clients.each do |c| - begin - c.focus - c.ctl.write :kill - rescue - end + elsif key? key + self[key].each do |block| + block.call(*args) end end - end - - class Button < Thread - ## - # Creates a new button at the given node and updates its label - # according to the given refresh rate (measured in seconds). The - # given block is invoked to calculate the label of the button. - # - # The return value of the given block must be either an array (whose - # first item is a color sequence for the button, and the second is the - # label of the button) or a string containing the label of the button. - # - def initialize fs_bar_node, refresh_rate, &button_label - raise ArgumentError unless block_given? - - super(fs_bar_node) do |b| - b.create unless b.exist? - - while true - ary = Array(button_label.call) - - # provide a default color - unless ary.length > 1 - ary.unshift Color::NORMAL - end - b.write ary.join(' ') - sleep refresh_rate - end - end - end + block end +end - action :status do - if defined? @buttons - @buttons.each {|s| s.kill } - end - - @buttons = [ - Button.new(fs.rbar.clock, 2) do - Time.now.to_s - end, +EVENTS = Handler.new +ACTIONS = Handler.new +KEYS = Handler.new + +## +# When a block is given, registers a handler +# for the given event and returns the handler. +# +# Otherwise, executes all handlers for the given event. +# +def event *a, &b + EVENTS.handle(*a, &b) +end - Button.new(fs.rbar.cpu_load, 5) do - File.read('/proc/loadavg').split[0..2].join(' ') - end, +## +# If a block is given, registers a handler for +# the given action and returns the handler. +# +# Otherwise, executes all handlers for the given action. +# +def action *a, &b + ACTIONS.handle(*a, &b) +end - Button.new(fs.rbar.disk_space, 120) do - rem, use, dir = `df -h ~`.split[-3..-1] - "#{dir} #{use} used #{rem} free" - end, +## +# If a block is given, registers a handler for +# the given keypress and returns the handler. +# +# Otherwise, executes all handlers for the given keypress. +# +def key *a, &b + KEYS.handle(*a, &b) +end - Button.new(fs.rbar.volume, 10) do - refresh_volume_display - end, - ] +## +# Shows a menu (where the user must press keys on their keyboard to +# make a choice) with the given items and returns the chosen item. +# +# If nothing was chosen, then nil is returned. +# +# ==== Parameters +# +# [prompt] +# Instruction on what the user should enter or choose. +# +def key_menu choices, prompt = nil + words = %w[dmenu -b -fn].push(CONFIG['display']['font']) + + words.concat %w[-nf -nb -sf -sb].zip( + [ + CONFIG['display']['color']['normal'], + CONFIG['display']['color']['focus'], + + ].map {|c| c.to_s.split[0,2] }.flatten + + ).flatten + + words.push '-p', prompt if prompt + + command = shell_join(words) + IO.popen(command, 'r+') do |menu| + menu.puts choices + menu.close_write + + choice = menu.read + choice unless choice.empty? end +end -# keyboard shortcuts - # focusing / showing - # focus client at left - key Key::FOCUS + Key::LEFT do - curr_view.ctl.write 'select left' rescue nil - end - - # focus client at right - key Key::FOCUS + Key::RIGHT do - curr_view.ctl.write 'select right' rescue nil - end - - # focus client below - key Key::FOCUS + Key::DOWN do - curr_view.ctl.write 'select down' - end - - # focus client above - key Key::FOCUS + Key::UP do - curr_view.ctl.write 'select up' - end - - # toggle focus between floating area and the columns - key Key::FOCUS + 'space' do - curr_view.ctl.write 'select toggle' - end - - # apply equal-spacing layout to current column - key Key::ARRANGE + 'w' do - curr_area.layout = :default - end - - # apply equal-spacing layout to all columns - key Key::ARRANGE + 'Shift-w' do - curr_view.columns.each do |a| - a.layout = :default - end - end - - # apply stacked layout to currently focused column - key Key::ARRANGE + 'v' do - curr_area.layout = :stack - end - - # apply stacked layout to all columns in current view - key Key::ARRANGE + 'Shift-v' do - curr_view.columns.each do |a| - a.layout = :stack - end - end - - # apply maximized layout to currently focused column - key Key::ARRANGE + 'm' do - curr_area.layout = :max - end - - # apply maximized layout to all columns in current view - key Key::ARRANGE + 'Shift-m' do - curr_view.columns.each do |a| - a.layout = :max - end - end - - # focus the previous view - key Key::FOCUS + 'comma' do - prev_view.focus - end - - # focus the next view - key Key::FOCUS + 'period' do - next_view.focus - end - - # sending / moving - key Key::SEND + Key::LEFT do - grouping.each do |c| - c.send :left rescue nil - end - end - - key Key::SEND + Key::RIGHT do - grouping.each do |c| - c.send :right rescue nil - end - end - - key Key::SEND + Key::DOWN do - grouping.each do |c| - c.send :down rescue nil - end - end - - key Key::SEND + Key::UP do - grouping.each do |c| - c.send :up rescue nil - end - end - - # send all grouped clients from managed to floating area (or vice versa) - key Key::SEND + 'space' do - grouping.each do |c| - c.send :toggle rescue nil - end - end - - # close all grouped clients - key Key::SEND + 'Delete' do - grouping.each do |c| - c.ctl.write 'kill' - end - end - - # swap the currently focused client with the one to its left - key Key::SWAP + Key::LEFT do - curr_client.swap :left rescue nil - end - - # swap the currently focused client with the one to its right - key Key::SWAP + Key::RIGHT do - curr_client.swap :right rescue nil - end +## +# Shows a menu (where the user must click a menu +# item using their mouse to make a choice) with +# the given items and returns the chosen item. +# +# If nothing was chosen, then nil is returned. +# +# ==== Parameters +# +# [choices] +# List of choices to display in the menu. +# +# [initial] +# The choice that should be initially selected. +# +# If this choice is not included in the list +# of cohices, then this item will be made +# into a makeshift title-bar for the menu. +# +def click_menu choices, initial = nil + words = %w[wmii9menu] + + if initial + words << '-i' + + unless choices.include? initial + initial = "<<#{initial}>>:" + words << initial + end + + words << initial + end - # swap the currently focused client with the one below it - key Key::SWAP + Key::DOWN do - curr_client.swap :down rescue nil - end + words.concat choices + command = shell_join(words) - # swap the currently focused client with the one above it - key Key::SWAP + Key::UP do - curr_client.swap :up rescue nil - end + choice = `#{command}`.chomp + choice unless choice.empty? +end - # Changes the tag (according to a menu choice) of each grouped client and - # returns the chosen tag. The +tag -tag idea is from Jonas Pfenniger: - # <http://zimbatm.oree.ch/articles/2006/06/15/wmii-3-and-ruby> - key Key::SEND + 'v' do - choices = tags.map {|t| [t, "+#{t}", "-#{t}"]}.flatten +## +# Joins the given array of words into a properly quoted shell command. +# +def shell_join words + # TODO: properly shell escape these items instead of doing String#inspect + words.map {|c| c.to_s.inspect }.join(' ') +end - if target = show_menu(choices, 'tag as:') - grouping.each do |c| - case target - when /^\+/ - c.tag $' +require 'pathname' - when /^\-/ - c.untag $' +## +# Returns the basenames of executable files present in the given directories. +# +def find_programs *dirs + dirs.flatten. + map {|d| Pathname.new(d).expand_path.children rescue [] }.flatten. + map {|f| f.basename.to_s if f.file? and f.executable? }.compact.uniq.sort +end - else - c.tags = target +## +# A button on a bar. +# +class Button < Thread + ## + # Creates a new button at the given node and updates its label + # according to the given refresh rate (measured in seconds). The + # given block is invoked to calculate the label of the button. + # + # The return value of the given block can be either an + # array (whose first item is a wmii color sequence for the + # button, and the remaining items compose the label of the + # button) or a string containing the label of the button. + # + # If the given block raises a standard exception, then that will be + # rescued and displayed (using error colors) as the button's label. + # + def initialize fs_bar_node, refresh_rate, &button_label + raise ArgumentError, 'block must be given' unless block_given? + + super(fs_bar_node) do |b| + b.create unless b.exist? + + while true + data = + begin + Array(button_label.call) + rescue Exception => e + LOG.error e + [CONFIG['display']['color']['error'], e] end - end - end - end - # zooming / sizing - ZOOMED_SUFFIX = /~(\d+)$/ - - # Sends grouped clients to temporary view. - key Key::PREFIX + 'b' do - clients = grouping - - unless clients.empty? - # determine new view - if curr_tag =~ ZOOMED_SUFFIX - src, num = $`, $1.to_i - dst = "#{src}~#{num+1}" - else - dst = "#{curr_tag}~1" + # provide default color + unless data.first =~ /(?:#[[:xdigit:]]{6} ?){3}/ + data.unshift CONFIG['display']['color']['normal'] end - # add clients to new view - clients.each {|c| c.tag dst } - - # focus new view - v = View.new dst - v.focus - v.arrange_in_grid - - # preserve focus inside new view - clients.first.focus v + b.write data.join(' ') + sleep refresh_rate end end + end - # Sends grouped clients back to their original view. - key Key::PREFIX + 'Shift-b' do - clients = grouping - - unless clients.empty? - src = curr_tag - - if src =~ ZOOMED_SUFFIX - # determine new view - dst = $` - - # remove clients from old view - clients.each do |c| - c.with_tags do - delete src + ## + # Refreshes the label of this button. + # + alias refresh wakeup +end - if empty? - push dst - else - dst = last +require 'yaml' + +## +# Loads the given YAML configuration file. +# +def load_config_file config_file + config_data = YAML.load_file(config_file) + Object.const_set :CONFIG, config_data + + # display + fo = ENV['WMII_FONT'] = CONFIG['display']['font'] + fc = ENV['WMII_FOCUSCOLORS'] = CONFIG['display']['color']['focus'] + nc = ENV['WMII_NORMCOLORS'] = CONFIG['display']['color']['normal'] + + settings = { + 'font' => fo, + 'focuscolors' => fc, + 'normcolors' => nc, + 'border' => CONFIG['display']['border'], + 'bar on' => CONFIG['display']['bar'], + 'colmode' => CONFIG['display']['column']['mode'], + 'grabmod' => CONFIG['control']['grab'], + } + + fs.ctl.write settings.map {|pair| pair.join(' ') }.join("\n") + + system "xsetroot -solid #{CONFIG['display']['background'].inspect} &" + + # column + fs.colrules.write CONFIG['display']['column']['rule'] + + # client + event 'CreateClient' do |client_id| + client = Client.new(client_id) + + if label = client.label.read rescue nil + catch :found do + CONFIG['display']['client'].each do |regexp, target| + if label =~ regexp + client.tags = target + throw :found end end - end - - # focus new view - v = View.new dst - v.focus - - # preserve focus inside new view - clients.first.focus v - end - end - end - - # client grouping - # include/exclude the currently focused client from the grouping - key Key::GROUP + 'g' do - curr_client.toggle_group - end - - # include all clients in the currently focused view into the grouping - key Key::GROUP + 'v' do - curr_view.group - end - - # exclude all clients in the currently focused view from the grouping - key Key::GROUP + 'Shift-v' do - curr_view.ungroup - end - - # include all clients in the currently focused area into the grouping - key Key::GROUP + 'c' do - curr_area.group - end - - # exclude all clients in the currently focused column from the grouping - key Key::GROUP + 'Shift-c' do - curr_area.ungroup - end - - # include all clients in the floating area into the grouping - key Key::GROUP + 'f' do - curr_view.floating_area.group - end - - # exclude all clients in the currently focused column from the grouping - key Key::GROUP + 'Shift-f' do - curr_view.floating_area.ungroup - end - - # include all clients in the managed areas into the grouping - key Key::GROUP + 'm' do - curr_view.columns.each do |c| - c.group - end - end - - # exclude all clients in the managed areas from the grouping - key Key::GROUP + 'Shift-m' do - curr_view.columns.each do |c| - c.ungroup - end - end - - # invert the grouping in the currently focused view - key Key::GROUP + 'i' do - curr_view.toggle_group - end - - # exclude all clients everywhere from the grouping - key Key::GROUP + 'n' do - Rumai.ungroup - end - - # visual arrangement - key Key::ARRANGE + 't' do - curr_view.arrange_as_larswm - end - - key Key::ARRANGE + 'g' do - curr_view.arrange_in_grid - end - - key Key::ARRANGE + 'd' do - curr_view.arrange_in_diamond - end - - # interactive menu - # launch an internal action by choosing from a menu - key Key::MENU + 'i' do - if choice = show_menu(@action_menu + ACTIONS.keys, 'run action:') - unless action choice.to_sym - system choice << '&' - end - end - end - - # launch an external program by choosing from a menu - key Key::MENU + 'e' do - if choice = show_menu(@program_menu, 'run program:') - system choice << '&' - end - end - - # focus any view by choosing from a menu - key Key::MENU + 'u' do - if choice = show_menu(tags, 'show view:') - focus_view choice - end - end - # focus any client by choosing from a menu - key Key::MENU + 'a' do - choices = [] - clients.each_with_index do |c, i| - choices << "%d. [%s] %s" % [i, c[:tags].read, c[:props].read.downcase] - end - - if target = show_menu(choices, 'show client:') - i = target.scan(/\d+/).first.to_i - clients[i].focus - end - end - - # external programs - require 'fileutils' - - # Open a new terminal and set its working directory - # to be the same as the currently focused terminal. - key Key::EXECUTE + 'x' do - c = curr_client - d = File.expand_path(c.label.read.split(' ', 2).last) rescue nil - d = ENV['HOME'] unless File.directory? d.to_s - - FileUtils.cd(d) do - system 'terminal &' - end - end - - key Key::EXECUTE + 'k' do - system 'firefox &' - end - - key Key::EXECUTE + 'j' do - system 'thunar &' - end - - # volume controls - def refresh_volume_display - level = `amixer get Master`.scan(/\d+%/).first - label = "volume #{level}" - - b = Rumai.fs.rbar.volume - b.create unless b.exist? - b.write "#{Color::NORMAL} #{label}" - - label - end - - key(Key::PREFIX + 'Shift-Prior') do - system 'amixer set Master 3dB+' - refresh_volume_display - end - - key(Key::PREFIX + 'Shift-Next') do - system 'amixer set Master 3dB-' - refresh_volume_display - end - - key(Key::PREFIX + 'Shift-Return') do - system 'amixer set Master toggle' - refresh_volume_display - end - - # music controls - print 'connecting to MPD... ', - begin - require 'rubygems' - require 'librmpd' - - @mpd = MPD.new - @mpd.connect(true) # true keeps connection alive - rescue => e - puts e # ignore - end - - key(Key::PREFIX + 'Prior') { @mpd.previous } - key(Key::PREFIX + 'Next') { @mpd.next } - - key Key::PREFIX + 'Return' do # play / pause - if @mpd.stopped? - @mpd.play - else - # toggle play/pause - @mpd.pause = !@mpd.paused? - end - end - - # load an MPD playlist - key(Key::PREFIX + 'Home') do - choices = @mpd.playlists - - if target = show_menu(choices, 'load MPD playlist:') - @mpd.clear - @mpd.load target - @mpd.play + # force client onto current view + client.tags = curr_tag + client.focus + end end end - # add current song to an MPD playlist - key(Key::PREFIX + 'End') do - choices = @mpd.playlists - - if target = show_menu(choices, 'add current song to MPD playlist:') - song = @mpd.current_song - - file = File.join(File.expand_path('~/.mpd/playlists'), target + '.m3u') - list = File.read(file).split(/\r?\n/) rescue [] + # status + action 'status' do + unless defined? @status_button_by_name + @status_button_by_name = {} - list.push song.file - list.uniq! + CONFIG['display']['status'].each do |name, defn| + button = eval "Button.new(Rumai.fs.rbar[#{name.inspect}], #{defn['refresh']}) { #{defn['content']} }", TOPLEVEL_BINDING, "#{config_file}:display:status:#{name}" - File.open(file, 'w') {|f| f.puts list } + @status_button_by_name[name] = button + end end - end + @status_button_by_name.each_value {|b| b.refresh } - # wmii-2 style client detaching - DETACHED_TAG = '|' + end.call - # Detach the current grouping from the current view. - key Key::PREFIX + 'd' do - grouping.each do |c| - c.with_tags do - delete curr_tag - push DETACHED_TAG + ## + # Refreshes the content of the status button with the given name. + # + def status name + if button = @status_button_by_name[name] + button.refresh end end - end - - # Attach the most recently detached client onto the current view. - key Key::PREFIX + 'Shift-d' do - v = View.new DETACHED_TAG - if v.exist? and c = v.clients.last - c.with_tags do - delete DETACHED_TAG - push curr_tag - end + # control + %w[key action event].each do |param| + CONFIG['control'][param].each do |name, code| + eval "#{param}(#{name.inspect}) {|*argv| #{code} }", + TOPLEVEL_BINDING, "#{config_file}:control:#{param}:#{name}" end end - # number keys - 10.times do |i| - # focus the {i}'th view - key Key::FOCUS + i.to_s do - focus_view tags[i - 1] || i - end + # script + eval CONFIG['script'], TOPLEVEL_BINDING, "#{config_file}:script" - # send current grouping to {i}'th view - key Key::SEND + i.to_s do - grouping.each do |c| - c.tags = tags[i - 1] || i - end - end - - # swap current client with the primary client in {i}'th column - key Key::SWAP + i.to_s do - curr_view.ctl.write "swap sel #{i+1}" # XXX: +1 b/c floating area is column 1: until John-Galt fixes this! - end - - # apply grid layout with {i} clients per column - key Key::ARRANGE + i.to_s do - curr_view.arrange_in_grid i - end - end - - # alphabet keys - # focus the view whose name begins with an alphabet key - ('a'..'z').each do |k| - key Key::VIEW + k do - if t = tags.grep(/^#{k}/i).first - focus_view t - end - end - end - -# wallpaper - system "xsetroot -solid #{Color::BACKGROUND.inspect} &" - system 'sh ~/.fehbg &' # set desktop wallpaper - -# bootstrap - action :status - action :rehash +end diff --git a/config.yaml b/config.yaml @@ -0,0 +1,750 @@ +# High-level wmii configuration. +#-- +# Copyright 2006 Suraj N. Kurapati +# See the LICENSE file for details. +#++ + + +## +# Appearance settings. +# +display: + + ## + # Where to display the horizontal bar? + # + # (either "top" or "bottom") + # + bar: bottom + + ## + # The font to use in all text rendered by wmii. + # + font: -*-fixed-medium-r-*-*-18-*-*-*-*-*-*-* + + ## + # Thickness of client border (measured in pixels). + # + border: 1 + + ## + # Number of seconds a notice shoud be displayed. + # + notice: 5 + + ## + # Color schemes for everything drawn by wmii. + # + # <scheme>: "<text> <background> <border>" + # + # You can find more color schemes at: + # + # http://wmii.suckless.org/scripts_n_snips/themes + # + color: + normal: "#c0c0c0 #0a0a0a #202020" + focus: "#ffffff #285577 #4c7899" + error: "#8a1f11 #FBE3E4 #FBC2C4" # from http://www.blueprintcss.org + notice: "#514721 #FFF6BF #FFD324" # from http://www.blueprintcss.org + success: "#264409 #E6EFC2 #C6D880" # from http://www.blueprintcss.org + + ## + # Color of desktop background. + # + background: "#333333" + + ## + # Settings for columns drawn by wmii. + # + # mode: <the default column mode> + # rule: <the wmii "colrules" setting> + # + column: + mode: stack + rule: | + /gimp/ -> 17+83+41 + /.*/ -> 62+38 # Golden Ratio + + ## + # Mapping of clients to views they must appear on: + # + # !ruby/regexp '<client label>' : <tags to apply> + # + client: + !ruby/regexp '/\b(xconsole|alsamixer|XMMS|Sonata)\b/' : 1 + !ruby/regexp '/\b(Buddy List|XChat|WeeChat)\b/' : chat + !ruby/regexp '/\b(Liferea|GMail)\b/' : mail + !ruby/regexp '/\bRestore (Previous )?Session\b/' : web + + ## + # Self-refreshing buttons on the status bar: + # + # <IXP node basename>: + # refresh: <number of seconds to wait before refreshing the content> + # content: <Ruby code whose result is displayed as the content> + # + # You can refresh a particular status button in Ruby using: + # + # status( "IXP node basename" ) + # + status: + clock: + refresh: 5 + content: Time.now.to_s + + cpu_load: + refresh: 10 + content: File.read('/proc/loadavg').split.first(3).unshift('load') + + disk_space: + refresh: 600 # 10 minutes + content: | + free, used, path = `df -h ~`.split.last(3) + [path, used, 'used', free, 'free'] + + volume: + refresh: 60 + content: | + ['vol', `amixer get Master`.scan(/\d+%/).first] + + music: + refresh: 15 + content: | + unless defined? @music + require 'rubygems' + gem 'librmpd', '~> 0.1' + require 'librmpd' + + @music = MPD.new + end + + unless @music.connected? + @music.connect + end + + song_state = (@music.stopped? || @music.paused?) ? '(-)' : '(>)' + + song = @music.current_song + artist = song.artist + title = song.title || (f = song.file and File.basename(f)) + song_name = [artist, title].compact.join(': ') + + [song_state, song_name] + + # TODO: mouse events on this status button + click: + left: + middle: + right: + + scroll: + up: + down: + + +## +# Interaction settings. +# +control: + + ## + # The wmii "grabmod" setting. + # + grab: Mod1 + + ## + # Key bindings. + # + # <key sequence>: <Ruby code to execute> + # + key: + #--------------------------------------------------------------------------- + # focus + #--------------------------------------------------------------------------- + + Mod1-Control-t: | # focus above client + curr_view.select(:up) rescue nil + + Mod1-Control-n: | # focus below client + curr_view.select(:down) rescue nil + + Mod1-Control-h: | # focus left client + curr_view.select(:left) rescue nil + + Mod1-Control-s: | # focus right client + curr_view.select(:right) rescue nil + + Mod1-Control-space: | # focus floating area (toggle) + curr_view.select(:toggle) + + Mod1-Control-comma: | # focus previous view + prev_view.focus + + Mod1-Control-period: | # focus next view + next_view.focus + + # focus the view whose index or name equals the pressed number + Mod1-Control-1: focus_view( tags[0] || 1 ) + Mod1-Control-2: focus_view( tags[1] || 2 ) + Mod1-Control-3: focus_view( tags[2] || 3 ) + Mod1-Control-4: focus_view( tags[3] || 4 ) + Mod1-Control-5: focus_view( tags[4] || 5 ) + Mod1-Control-6: focus_view( tags[5] || 6 ) + Mod1-Control-7: focus_view( tags[6] || 7 ) + Mod1-Control-8: focus_view( tags[7] || 8 ) + Mod1-Control-9: focus_view( tags[8] || 9 ) + Mod1-Control-0: focus_view( tags[9] || 10 ) + + # focus the view whose name begins with the pressed alphabet + Mod1-Control-v,a: t = tags.grep(/^a/i).first and focus_view(t) + Mod1-Control-v,b: t = tags.grep(/^b/i).first and focus_view(t) + Mod1-Control-v,c: t = tags.grep(/^c/i).first and focus_view(t) + Mod1-Control-v,d: t = tags.grep(/^d/i).first and focus_view(t) + Mod1-Control-v,e: t = tags.grep(/^e/i).first and focus_view(t) + Mod1-Control-v,f: t = tags.grep(/^f/i).first and focus_view(t) + Mod1-Control-v,g: t = tags.grep(/^g/i).first and focus_view(t) + Mod1-Control-v,h: t = tags.grep(/^h/i).first and focus_view(t) + Mod1-Control-v,i: t = tags.grep(/^i/i).first and focus_view(t) + Mod1-Control-v,j: t = tags.grep(/^j/i).first and focus_view(t) + Mod1-Control-v,k: t = tags.grep(/^k/i).first and focus_view(t) + Mod1-Control-v,l: t = tags.grep(/^l/i).first and focus_view(t) + Mod1-Control-v,m: t = tags.grep(/^m/i).first and focus_view(t) + Mod1-Control-v,n: t = tags.grep(/^n/i).first and focus_view(t) + Mod1-Control-v,o: t = tags.grep(/^o/i).first and focus_view(t) + Mod1-Control-v,p: t = tags.grep(/^p/i).first and focus_view(t) + Mod1-Control-v,q: t = tags.grep(/^q/i).first and focus_view(t) + Mod1-Control-v,r: t = tags.grep(/^r/i).first and focus_view(t) + Mod1-Control-v,s: t = tags.grep(/^s/i).first and focus_view(t) + Mod1-Control-v,t: t = tags.grep(/^t/i).first and focus_view(t) + Mod1-Control-v,u: t = tags.grep(/^u/i).first and focus_view(t) + Mod1-Control-v,v: t = tags.grep(/^v/i).first and focus_view(t) + Mod1-Control-v,w: t = tags.grep(/^w/i).first and focus_view(t) + Mod1-Control-v,x: t = tags.grep(/^x/i).first and focus_view(t) + Mod1-Control-v,y: t = tags.grep(/^y/i).first and focus_view(t) + Mod1-Control-v,z: t = tags.grep(/^z/i).first and focus_view(t) + + #--------------------------------------------------------------------------- + # move + #--------------------------------------------------------------------------- + + Mod1-Control-m,t: | # move grouping toward the top + grouping.each {|c| c.send(:up) rescue nil } + + Mod1-Control-m,n: | # move grouping toward the bottom + grouping.each {|c| c.send(:down) rescue nil } + + Mod1-Control-m,h: | # move grouping toward the left + grouping.each {|c| c.send(:left) rescue nil } + + Mod1-Control-m,s: | # move grouping toward the right + grouping.each {|c| c.send(:right) rescue nil } + + Mod1-Control-m,space: | # move grouping to floating area (toggle) + grouping.each {|c| c.send(:toggle) rescue nil } + + Mod1-Control-m,v: | # move grouping to chosen view + # + # Changes the tag (according to a menu choice) of each grouped client and + # returns the chosen tag. The +tag -tag idea is from Jonas Pfenniger: + # <http://zimbatm.oree.ch/articles/2006/06/15/wmii-3-and-ruby> + # + choices = tags.map {|t| [t, "+#{t}", "-#{t}"] }.flatten + + if target = key_menu(choices, 'tag as:') + grouping.each do |c| + case target + when /^\+/ then c.tag $' + when /^\-/ then c.untag $' + else c.tags = target + end + end + end + + Mod1-Control-m,Delete: | # kill all clients in grouping + grouping.each {|c| c.kill } + + # move grouping to the view whose index or name equals the pressed number + Mod1-Control-m,1: grouping.each {|c| c.tags = tags[0] || 1 } + Mod1-Control-m,2: grouping.each {|c| c.tags = tags[1] || 2 } + Mod1-Control-m,3: grouping.each {|c| c.tags = tags[2] || 3 } + Mod1-Control-m,4: grouping.each {|c| c.tags = tags[3] || 4 } + Mod1-Control-m,5: grouping.each {|c| c.tags = tags[4] || 5 } + Mod1-Control-m,6: grouping.each {|c| c.tags = tags[5] || 6 } + Mod1-Control-m,7: grouping.each {|c| c.tags = tags[6] || 7 } + Mod1-Control-m,8: grouping.each {|c| c.tags = tags[7] || 8 } + Mod1-Control-m,9: grouping.each {|c| c.tags = tags[8] || 9 } + Mod1-Control-m,0: grouping.each {|c| c.tags = tags[9] || 10 } + + #--------------------------------------------------------------------------- + # swap + #--------------------------------------------------------------------------- + + Mod1-Control-w,t: | # swap with above client + curr_client.swap(:up) rescue nil + + Mod1-Control-w,n: | # swap with below client + curr_client.swap(:down) rescue nil + + Mod1-Control-w,h: | # swap with left client + curr_client.swap(:left) rescue nil + + Mod1-Control-w,s: | # swap with right client + curr_client.swap(:right) rescue nil + + # swap current client with the column whose index equals the pressed number + Mod1-Control-w,1: curr_client.swap 1 + Mod1-Control-w,2: curr_client.swap 2 + Mod1-Control-w,3: curr_client.swap 3 + Mod1-Control-w,4: curr_client.swap 4 + Mod1-Control-w,5: curr_client.swap 5 + Mod1-Control-w,6: curr_client.swap 6 + Mod1-Control-w,7: curr_client.swap 7 + Mod1-Control-w,8: curr_client.swap 8 + Mod1-Control-w,9: curr_client.swap 9 + Mod1-Control-w,0: curr_client.swap 10 + + #--------------------------------------------------------------------------- + # column + #--------------------------------------------------------------------------- + + Mod1-Control-z,w: | # apply equal-spacing layout to current column + curr_area.layout = :default + + Mod1-Control-z,Shift-w: | # apply equal-spacing layout to all columns + curr_view.columns.each do |a| + a.layout = :default + end + + Mod1-Control-z,v: | # apply stacked layout to current column + curr_area.layout = 'stack-max' + + Mod1-Control-z,Shift-v: | # apply stacked layout to all columns + curr_view.columns.each do |a| + a.layout = 'stack-max' + end + + Mod1-Control-z,m: | # apply maximized layout to current column + curr_area.layout = 'stack+max' + + Mod1-Control-z,Shift-m: | # apply maximized layout to all columns + curr_view.columns.each do |a| + a.layout = 'stack+max' + end + + #--------------------------------------------------------------------------- + # group + #--------------------------------------------------------------------------- + + Mod1-Control-g,c: | # add current client to grouping (toggle) + curr_client.group! + + Mod1-Control-g,a: | # add clients in current area to grouping (toggle) + curr_area.group! + + Mod1-Control-g,f: | # add clients in floating area to grouping (toggle) + Area.floating.group! + + Mod1-Control-g,v: | # add clients in current view to grouping (toggle) + curr_view.group! + + Mod1-Control-g,m: | # add clients in managed areas to grouping (toggle) + curr_view.columns.each {|a| a.group! } + + Mod1-Control-g,n: | # remove all clients everywhere from grouping + Rumai.ungroup + + #--------------------------------------------------------------------------- + # detach + #--------------------------------------------------------------------------- + + Mod1-Control-d: | # detach grouping from current view + grouping.each do |c| + c.with_tags do + delete curr_tag + push DETACHED_TAG + end + end + + Mod1-Control-Shift-d: | # attach most recently detached client + v = View.new DETACHED_TAG + + if v.exist? and c = v.clients.last + c.with_tags do + delete DETACHED_TAG + push curr_tag + end + end + + #--------------------------------------------------------------------------- + # zoom + #--------------------------------------------------------------------------- + + Mod1-Control-f: | # zoom client to fullscreen (toggle) + curr_client.fullscreen! + + Mod1-Control-b: | # copy grouping to temporary view + clients = grouping + + unless clients.empty? + # determine new view + if curr_tag =~ ZOOMED_SUFFIX + src, num = $`, $1.to_i + dst = "#{src}~#{num+1}" + else + dst = "#{curr_tag}~1" + end + + # add clients to new view + clients.each {|c| c.tag dst } + + # focus new view + v = View.new dst + v.focus + v.arrange_in_grid + + # propagate focus into new view + clients.first.focus v + end + + Mod1-Control-Shift-b: | # return grouping to original view + clients = grouping + + unless clients.empty? + src = curr_tag + + if src =~ ZOOMED_SUFFIX + # determine new view + dst = $` + + # remove clients from old view + clients.each do |c| + c.with_tags do + delete src + + if empty? + push dst + else + dst = last + end + end + end + + # focus new view + v = View.new dst + v.focus + + # propagate focus into original view + clients.first.focus v + end + end + + #--------------------------------------------------------------------------- + # arrange + #--------------------------------------------------------------------------- + + Mod1-Control-z,t: | # arrange clients in current view like LarsWM does + curr_view.arrange_as_larswm + + Mod1-Control-z,g: | # arrange clients in current view like a grid + curr_view.arrange_in_grid + + Mod1-Control-z,d: | # arrange clients in current view like a diamond + curr_view.arrange_in_diamond + + # apply grid layout with the pressed number of clients per column + Mod1-Control-z,1: curr_view.arrange_in_grid 1 + Mod1-Control-z,2: curr_view.arrange_in_grid 2 + Mod1-Control-z,3: curr_view.arrange_in_grid 3 + Mod1-Control-z,4: curr_view.arrange_in_grid 4 + Mod1-Control-z,5: curr_view.arrange_in_grid 5 + Mod1-Control-z,6: curr_view.arrange_in_grid 6 + Mod1-Control-z,7: curr_view.arrange_in_grid 7 + Mod1-Control-z,8: curr_view.arrange_in_grid 8 + Mod1-Control-z,9: curr_view.arrange_in_grid 9 + Mod1-Control-z,0: curr_view.arrange_in_grid 9999 # make one giant column + + #--------------------------------------------------------------------------- + # menu + #--------------------------------------------------------------------------- + + Mod1-Control-i: | # run internal action chosen from a menu + choices = @action_menu + ACTIONS.keys + + if choice = key_menu(choices, 'run action:') + action(choice) or # action is a Ruby block + system(choice << '&') # action is a program + end + + Mod1-Control-e: | # run external program chosen from a menu + if choice = key_menu(@program_menu, 'run program:') + system(choice << '&') + end + + Mod1-Control-u: | # focus view chosen from a menu + if choice = key_menu(tags, 'show view:') + focus_view choice + end + + Mod1-Control-a: | # focus client chosen from a menu + choices = [] + + clients.each_with_index do |c, i| + choices << "%d. [%s] %s" % [i, c[:tags].read, c[:label].read.downcase] + end + + if target = key_menu(choices, 'show client:') + i = target.scan(/\d+/).first.to_i + clients[i].focus + end + + #--------------------------------------------------------------------------- + # launcher + #--------------------------------------------------------------------------- + + Mod1-Control-x: | # launch a terminal + # + # Launch a new terminal and set its + # working directory to be the same + # as the currently focused terminal. + # + root = ENV['HOME'] + + label = curr_client.label.read rescue '' + label.split(' ').reverse_each do |word| + # + # iterate in reverse order because + # paths are usually at end of label + # + path = File.expand_path(word) + + if File.exist? path + unless File.directory? path + path = File.dirname(path) + end + + root = path + break + end + end + + require 'fileutils' + FileUtils.cd root do + system 'urxvt &' + end + + Mod1-Control-k: | # launch a web browser + system 'firefox &' + + Mod1-Control-j: | # launch a file manager + system 'thunar &' + + Mod1-Control-q: | # launch a note taker + system 'mousepad &' + + #--------------------------------------------------------------------------- + # music + #--------------------------------------------------------------------------- + + Mod1-Control-Prior: | # previous song + @music.previous rescue nil + status 'music' + + Mod1-Control-Next: | # next song + @music.next rescue nil + status 'music' + + Mod1-Control-Return: | # pause song (toggle) + begin + if @music.stopped? + @music.play + else + @music.pause = !@music.paused? + end + rescue + # ignore + end + + status 'music' + + #--------------------------------------------------------------------------- + # volume + #--------------------------------------------------------------------------- + + Mod1-Control-Shift-Prior: | # increase volume + system 'amixer set Master 3dB+' + status 'volume' + + Mod1-Control-Shift-Next: | # decrease volume + system 'amixer set Master 3dB-' + status 'volume' + + Mod1-Control-Shift-Return: | # mute volume (toggle) + system 'amixer set Master toggle' + status 'volume' + + ## + # Event handlers. + # + # <event name>: <Ruby code to execute> + # + # The Ruby code has access to an "argv" variable which is an + # array of arguments passed that were specified by the event. + # + event: + CreateTag: | + tag = argv[0] + but = fs.lbar[tag] + but.create unless but.exist? + but.write "#{CONFIG['display']['color']['normal']} #{tag}" + + DestroyTag: | + tag = argv[0] + but = fs.lbar[tag] + but.remove if but.exist? + + FocusTag: | + tag = argv[0] + but = fs.lbar[tag] + but.write "#{CONFIG['display']['color']['focus']} #{tag}" if but.exist? + + UnfocusTag: | + tag = argv[0] + but = fs.lbar[tag] + but.write "#{CONFIG['display']['color']['normal']} #{tag}" if but.exist? + + UrgentTag: | + tag = argv[1] + but = fs.lbar[tag] + but.write '*' + tag if but.exist? + + NotUrgentTag: | + tag = argv[1] + but = fs.lbar[tag] + but.write tag if but.exist? + + LeftBarClick: &LeftBarClick | + mouse_button, view_id = argv + + if mouse_button == '1' # primary button + focus_view view_id + end + + ## + # allows the user to drag a file over a + # view button and activate that view while + # still holding on to their dragged file! + # + LeftBarDND: *LeftBarClick + + RightBarMouseDown: | + mouse_button, status_id = argv + + if mouse_button == '3' # secondary button + case click_menu %w[refresh], status_id + when 'refresh' then status(status_id) + else # TODO: mouse events on status buttons + end + end + + Unresponsive: | + client_id = argv[0] + client = Client.new(client_id) + + IO.popen('xmessage -nearmouse -file - -buttons Kill,Wait -print', 'w+') do |f| + f.puts 'The following client is not responding.', '' + f.puts client.inspect + f.puts client.label.read + + f.puts '', 'What would you like to do?' + f.close_write + + if f.read.chomp == 'Kill' + client.slay + end + end + + Notice: | + unless defined? @notice_mutex + require 'thread' + @notice_mutex = Mutex.new + end + + Thread.new do + # prevent notices from overwriting each other + @notice_mutex.synchronize do + button = fs.rbar['!notice'] + button.create unless button.exist? + + # display the notice + message = argv.join(' ') + + LOG.info message # also log it in case the user is AFK + button.write "#{CONFIG['display']['color']['notice']} #{message}" + + # clear the notice + sleep [1, CONFIG['display']['notice'].to_i].max + button.remove + end + end + + ClientMouseDown: | + client_id, mouse_button = argv + + if mouse_button == '3' # secondary button + client = Client.new(client_id) + + case click_menu %w[stick group fullscreen kill slay], 'client' + when 'stick' then client.stick! + when 'group' then client.group! + when 'fullscreen' then client.fullscreen! + when 'kill' then client.kill + when 'slay' then client.slay + end + end + + ## + # Internal scripts. + # + # <action name>: <Ruby code to execute> + # + action: + rehash: | # scan for available programs and actions + @program_menu = find_programs(ENV['PATH'].squeeze(':').split(':')) + @action_menu = find_programs(File.dirname(__FILE__)) + + clear: | # kill all clients + # firefox's restore session feature does not + # work unless the whole process is killed. + system 'killall firefox firefox-bin thunderbird thunderbird-bin &' + + # gnome-panel refuses to die by any other means + system 'killall -s TERM gnome-panel &' + + Thread.pass until clients.each do |c| + begin + c.focus # XXX: client must be on current view in order to be killed + c.kill + rescue + # ignore + end + end.empty? + + kill: | # kill the window manager only; do not touch the clients! + fs.ctl.write 'quit' + + quit: | # kill both clients and window manager + action 'clear' + action 'kill' + + +## +# Ruby code that is executed after this file has been loaded. +# +script: | + DETACHED_TAG = '|' + ZOOMED_SUFFIX = /~(\d+)$/ + + action 'rehash' + + # desktop wallpaper + system 'sh ~/.fehbg &' diff --git a/wmiirc b/wmiirc @@ -1,154 +1,65 @@ -#!/usr/bin/ruby -w -# Loader for ruby-based wmii configuration. - -# load the Ruby interface to wmii - require 'rubygems' - gem 'rumai', '~> 2' - require 'rumai' - - include Rumai +#!/usr/bin/env ruby +# Bootloader for wmii configuration. +#-- +# Copyright 2006 Suraj N. Kurapati +# See the LICENSE file for details. +#++ # create a logger to aid debugging - require 'logger' - LOG = Logger.new(__FILE__ + '.log', 5) - - unless $DEBUG - class << LOG - # emulate IO.write - alias write << +require 'logger' +LOG = Logger.new(__FILE__ + '.log', 5) - def flush - # ignore - end - end +class << LOG + # emulate IO.write + alias write << - # capture standard output in logger - $stdout = $stderr = LOG + def flush + # ignore end +end -LOG.info "birth" +# capture standard output in logger +$stdout = $stderr = LOG begin - # miniature DSL to ease configuration - class Handler < Hash - def initialize - super {|h,k| h[k] = [] } - end - - # When a block is given, registers a handler for the given key. - # Otherwise, executes all registered handlers for the given key. - def handle key, *args, &block - if block_given? - self[key] << block - - elsif self.key? key - self[key].each do |block| - block.call(*args) - end - end - end - end - - EVENTS = Handler.new - ACTIONS = Handler.new - KEYS = Handler.new - - def event *a, &b - EVENTS.handle(*a, &b) - end - - def action *a, &b - ACTIONS.handle(*a, &b) - end - - def key *a, &b - KEYS.handle a.map {|k| k.flatten.join('-') rescue k }.join(','), &b - end - - def unevent *a - EVENTS.delete(*a) - end - - def unaction *a - ACTIONS.delete(*a) - end + LOG.info 'birth' - def unkey *a - KEYS.delete(*a) - end - - # utility methods - - # Shows a menu with the given items and returns the chosen - # item. If nothing was chosen, then *nil* is returned. - def show_menu choices, prompt = nil - cmd = "dmenu -b -fn #{WMII_FONT.inspect} " << - %w[-nf -nb -sf -sb].zip( - Color::NORMAL.split[0,2] + Color::FOCUSED.split[0,2] - ).flatten.map {|s| s.inspect}.join(' ') + # load user configuration + config_home = File.dirname(__FILE__) + config_libs = File.join(config_home, 'config.rb') + config_file = File.join(config_home, 'config.yaml') - cmd << " -p #{prompt.to_s.inspect}" if prompt + require config_libs + load_config_file config_file - IO.popen(cmd, 'r+') do |menu| - menu.puts choices - menu.close_write - - choice = menu.read - choice unless choice.empty? - end - end - - require 'pathname' - # Returns the names of programs present in the given directories. - def find_programs *dirs - dirs.flatten.map do |d| - Pathname.new(d).expand_path.children rescue [] - end.flatten.map do |f| - f.basename.to_s if f.file? and f.executable? - end.compact.uniq.sort - end - - # terminate existing instances of this program - fs.event.write 'Start wmiirc' - - event :Start do |arg| - exit if arg == 'wmiirc' - end + # add view buttons to left-bar + fs.lbar.clear + tags.each {|t| event 'CreateTag', t } + event 'FocusTag', curr_tag - # load user's configuration file - load File.join(File.dirname(__FILE__), 'config.rb') - - # populate lbar with buttons for every tag - bar = fs.lbar - bar.clear - - tags.each do |tag| - color = (tag == curr_tag) ? Color::FOCUSED : Color::NORMAL - - btn = bar[tag] - btn.create - btn.write "#{color} #{tag}" - end - - # enable keyboard shortcuts + # register keyboard shortcuts fs.keys.write KEYS.keys.join("\n") + event('Key') {|*a| key(*a) } - event :Key do |*args| - key(*args) + # terminate existing instances of this configuration + fs.event.write 'Start wmiirc' + + event 'Start' do |arg| + exit if arg == 'wmiirc' end # the main event loop fs.event.each_line do |line| - line.split("\n").each do |event| - type, parms = event.split(' ', 2) + line.split("\n").each do |call| + name, args = call.split(' ', 2) - args = parms.split(' ') rescue [] - event type.to_sym, *args + argv = args.to_s.split(' ') + event name, *argv end end rescue SystemExit - # ignore it + # ignore it; the program wants to terminate rescue Exception => e LOG.error e @@ -156,13 +67,15 @@ rescue Exception => e # allow the user to rescue themselves system 'xterm &' - IO.popen('xmessage -file - -buttons recover:0,ignore:1', 'w') do |f| + IO.popen('xmessage -nearmouse -file - -buttons Recover,Ignore -print', 'w+') do |f| f.puts e.inspect, e.backtrace - end + f.close_write - if $?.exitstatus == 0 - system $0 + ' &' + if f.read.chomp == 'Recover' + system $0 + ' &' + end end -end -LOG.info "death" +ensure + LOG.info 'death' +end