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