wmii

git clone git://oldgit.suckless.org/wmii/
Log | Files | Refs | README | LICENSE

commit 5b9594e8ca7187f05e8abd22ecaad4e48ea18c88
parent 86e5151f775c8184f2cbc15080b833c1fff52a8e
Author: Kris Maglione <jg@suckless.org>
Date:   Tue, 29 Sep 2009 17:44:54 -0400

Add Suraj's Rumai-based wmiirc.

Diffstat:
alternative_wmiircs/Makefile | 3++-
alternative_wmiircs/README | 18++++++++++++++----
alternative_wmiircs/ruby/HISTORY | 233+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
alternative_wmiircs/ruby/LICENSE | 46++++++++++++++++++++++++++++++++++++++++++++++
alternative_wmiircs/ruby/README | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
alternative_wmiircs/ruby/config.rb | 422+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
alternative_wmiircs/ruby/config.yaml | 804+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
alternative_wmiircs/ruby/wmiirc | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 1681 insertions(+), 5 deletions(-)

diff --git a/alternative_wmiircs/Makefile b/alternative_wmiircs/Makefile @@ -4,7 +4,8 @@ include $(ROOT)/mk/wmii.mk BIN = $(ETC)/wmii$(CONFVERSION) TARG = python \ - plan9port + plan9port \ + ruby $(TARG:%=%.install): echo INSTALL $$($(CLEANNAME) $(BASE)${@:.install=}) diff --git a/alternative_wmiircs/README b/alternative_wmiircs/README @@ -4,10 +4,20 @@ Alternative wmiirc scripts This folder contains alternative implementations of wmii's rc scripts. Each folder contains a different implementation, described below, including its own README, wmiirc script, and -possibly other suppporting files and libraries. It usually -suffices to copy the entire contents of the directory to -~/.wmii, but see the accompanying README file for more -details. +possibly other suppporting files and libraries. These scripts +are installed along with wmii to $(ETC) as defined in config.mk. +It usually suffices to start the included `wmiirc` script at +wmii startup. Invoking wmii with the flag '-r python/wmiirc', +for instance, will start the python implementation. +Alternatively, if you use a session manager, you can add this +line to ~/.wmii/wmiirc (which must be executable): + + wmiir xwrite /ctl spawn python/wmiirc + + Index + ------------- ---------------------------------------------------- python/ A pure Python wmiirc implementation. + plan9port/ A Plan 9 Port/rc shell based wmiirc implementation + ruby/ A pure-ruby wmiirc implementation, by Suraj Kurapati diff --git a/alternative_wmiircs/ruby/HISTORY b/alternative_wmiircs/ruby/HISTORY @@ -0,0 +1,233 @@ += 2006-09-30 + +* Included 1.1.0 release of Ruby-IXP. + + += 2006-09-29 + +* Fixed bug in toggle_maximize method (in rc.rb) due + to accessing a nonexistent file in IXP file system. + + Thanks to Christian von Mueffling for reporting this bug. + +* Fixed problem with reading + index (Wmii::Client#index) of + currently selected client. + +* Wmii.find_client now accepts a variable number of places to be searched. + + += 2006-09-28 + +* Added number_view_buttons method (in rc.rb) which numbers + the view buttons displayed on the bar, from left to right. + + += 2006-09-27 + +* Included two main concurrency fixes for Ruby-IXP. + + += 2006-09-24 + +* Added two-stage event handling, + to minimize the number of events + missed while processing an event. + + += 2006-09-23 + +* Fixed event & status bar loop. It was forgotten when I transitioned + to the new Ixp::Node#method_missing behavior on 2006-09-22. + + Thanks to Fredrik Ternerot for reporting this bug. + +* When selecting views based on their first letter: if more than one + view matches, then they are cycled (adapted from Fredrik Ternerot). + +* Added focus_view_matching method in rc.rb. + +* Fixed errors that occurred when the tile and + diamond arrangements were applied to empty views. + + += 2006-09-22 + +* Ixp::Node#method_missing now only dereferences files. Also, + the ! notation has been removed, as you can see below. + + >> Wmii.fs.bar.status + => #<Ixp::Node:0xb7b5940c @path="/bar/status"> + >> Wmii.fs.bar.status.read + => ["colors", "data"] + >> Wmii.fs.bar.status.data + => "Fri Sep 22 18:46:11 PDT 2006 | 0.06 0.10 0.08 | 531M 100% /home" + >> Wmii.fs.bar.status.data! + => #<Ixp::Node:0xb7b377e4 @path="/bar/status/data!"> + + += 2006-09-21 + +* Fix some forgotten changes from show_menu() returning *nil*. + +* Exception error message (xmessage) now lets you restart *wmiirc*. + +* Updated event loop to generate less 9P traffic. + + += 2006-09-20 + +* Included code from upcoming Ruby-IXP 1.1.0 release. + +* Ixp::Node#method_missing now only dereferences a node + if the method is suffixed with an exclamation mark. + +* show_menu now returns *nil* if nothing was chosen. + +* Updated event loop for {wmii-3.1's /event overload bug + fix}[http://wmii.de/pipermail/wmii/2006-September/002718.html]. + +* Added explicit termination of already running instances + in *wmiirc* via Process.kill and `ps`, instead of using + /event as a means of coordinating said task. + + += 2006-09-19 + +* Included Ruby-IXP 1.0.3 release. + +* Added Ixp::Node#open method to reduce 9P traffic. + +* Added ability to fetch a sub-node + via Ixp::Node#method_missing, while + not dereferencing it (reading its + contents if it is a file), by adding + an exclamation to the file name. + + For example, consider the following output in *wmiish*. + + >> Wmii.fs.bar.status.data + => "Tue Sep 19 10:50:41 PDT 2006 | 0.30 0.43 0.29 | 1.7G 98% /home" + >> Wmii.fs.bar.status.data! + => #<Ixp::Node:0xb7bf1f18 @path="/bar/status/data"> + +* *wmiirc* no longer automatically resumes from error. Instead, + it throws you a terminal and shows you the error details so + you have a chance to fix it and restart *wmiirc* yourself. + + += 2006-09-18 + +* Included Ruby-IXP 1.0.2 release. + + += 2006-09-17 + +* Added Wmii::View#empty? and Wmii::Area#empty? methods. + +* change_tag_from_menu now returns the chosen tag. + +* Included Ruby-IXP 1.0.1 release. + + += 2006-09-16 + +* Fixed toggling of maximization + of currently focused client, + via toggle_maximize in rc.rb. + + Thanks to Fredrik Ternerot for reporting this bug. + + += 2006-09-15 + +* Added Wmii.get_view and Wmii.get_client + methods, to further minimize hard-coded + IXP file system paths. This will make it + easier to upgrade to wmii-4 later on. + +* Fixed ruby-ixp to be internally buffered for Ixp#read. + +* Event loop now uses Ixp#read instead of *wmiir*. + +* Already running configurations now correctly + exit when another instance starts up. + + += 2006-09-14 + +* Added ability to swap current client with the + currently focused client in any other column. + + += 2006-09-13 + +* Reverted to *wmiir* for event loop, because + Ixp#read isn't internally buffered! + +* Changed Wmii::View#each to Wmii::View#each_column because + floating area isn't a column (it doesn't have /mode file). + +* Added shortcuts for setting layouts of all columns in current view. + +* Added shortcuts for selection of current column. + +* Fixed ability to terminate multiple clients. + + += 2006-09-12 + +* Event loop now uses Ixp#read instead of *wmiir*. + + * Already running configurations now correctly + exit when another instance starts up. + +* Added Wmii::View#diamond! -- a diamond-shaped automated client arrangement. + +* Added Wmii::Area#length= for setting number of clients in a column. + + += 2006-09-11 + +* Added exception logging and recovery mechanism. + + * wmiirc is now split into a loader + file (wmiirc) and a configuration + file (wmiirc-config.rb), just + like in the ruby-wmii project. + +* IXPException' are no longer hidden away inside Ixp. + +* Moved support for destructive area-operations + from Wmii#with_selection into Array#each so + that it is generally available. + + += 2006-09-10 + +* Added wmiish--an interactive Ruby shell for controlling wmii. + +* Lots of major refactoring in Ixp and Wmii. + * Moved utility methods from wmiirc into rc.rb. + + += 2006-09-09 + +* Cleaned up IXP abstraction... now + multiple levels of method_missing + works, and so does self[sub_path] + +* Wmii#with_selection now supports destructive area-operations. + +* Update for compliance with new unique-client-id in filesystem patch. + + += 2006-08-31 + +* Added facility which sends the selection + to temporary view or switches back again. + + += 2006-08-30 + +* Add Wmii#with_selection method for operating on all clients in selection. diff --git a/alternative_wmiircs/ruby/LICENSE b/alternative_wmiircs/ruby/LICENSE @@ -0,0 +1,46 @@ +(the ISC license) + +Copyright 2006 Suraj N. Kurapati <sunaku@gmail.com> +Copyright 2007 Kris Maglione <jg@suckless.org> +Copyright 2007 Nick Stenning <nick@whiteink.com> +Copyright 2009 Daniel Wäber <waeber@inf.fu-berlin.de> +Copyright 2009 Michael Andrus <centyx@centyx.net> +Copyright 2009 Simon Hafner <hafnersimon@gmail.com> + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +Portions of this software originate from wmii <http://wmii.suckless.org>: + +(the MIT license) + +© 2006-2007 Kris Maglione <fbsdaemon@gmail.com> +© 2003-2006 Anselm R. Garbe <garbeam at suckless dot org> + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/alternative_wmiircs/ruby/README b/alternative_wmiircs/ruby/README @@ -0,0 +1,77 @@ + +This is a modified version of sunaku's wmiirc, designed for +his Rumai Ruby module. Minor configuration changes, namely to +the color scheme and default key bindings, as well as the +configuration search path, exist in this version. Builtin mpd +support has also been removed. Also added is support for +string interpolation in key bindings, as should be apparent in +the included config.yaml. + +In particular, not that there is no need to copy any files to +~/.wmii-hg or ~/.wmii other than config.yaml. The script will +happily load the requisite files from their default install +location. They can be loaded either by involing wmii as +follows: + + wmiir -r ruby/wmiirc + +or running the following after startup: + + wmiir xwrite /ctl spawn ruby/wmiirc + +The rumai gem is still required, as noted below. + +The original readme appears below unmodified: + +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: + + wmii 3.6 or newer (preferably wmii-hg) + + Ruby 1.8.6 or newer + + RubyGems 1.3.1 or newer + +Installation: + + # library + gem install rumai # required + gem install librmpd # optional + + # install + mv ~/.wmii-hg ~/.wmii-hg.backup + git clone git://github.com/sunaku/wmiirc.git ~/.wmii-hg + + # run + ~/.wmii-hg/wmiirc + +Documentation: + + # see list of all key bindings + grep 'Mod.*#' ~/.wmii-hg/config.yaml + + # read the configuration file + less ~/.wmii-hg/config.yaml + +Configuration: + + Edit ~/.wmii-hg/config.yaml 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/alternative_wmiircs/ruby/config.rb b/alternative_wmiircs/ruby/config.rb @@ -0,0 +1,422 @@ +# DSL for wmiirc configuration. +#-- +# Copyright protects this work. +# See LICENSE file for details. +#++ + +require 'shellwords' +require 'pathname' +require 'yaml' + +require 'rubygems' +gem 'rumai', '~> 3' +require 'rumai' + +include Rumai + +class Handler < Hash + def initialize + super {|h,k| h[k] = [] } + end + + ## + # If a block is given, registers a handler + # for the given key and returns the handler. + # + # Otherwise, executes all handlers registered for the given key. + # + def handle key, *args, &block + if block + self[key] << block + + elsif key? key + self[key].each do |block| + block.call(*args) + end + end + + block + end +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 + +## +# Returns a list of registered event names. +# +def events + EVENTS.keys +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 + +## +# Returns a list of registered action names. +# +def actions + ACTIONS.keys +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 + +## +# Returns a list of registered action names. +# +def keys + KEYS.keys +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 = words.shelljoin + IO.popen(command, 'r+') do |menu| + menu.puts choices + menu.close_write + + choice = menu.read + choice unless choice.empty? + end +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 + + words.concat choices + command = words.shelljoin + + choice = `#{command}`.chomp + choice unless choice.empty? +end + +## +# 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 + +## +# Launches the command built from the given words in the background. +# +def launch *words + command = words.shelljoin + system "#{command} &" +end + +## +# 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 |button| + while true + label = + begin + Array(button_label.call) + rescue Exception => e + LOG.error e + [CONFIG['display']['color']['error'], e] + end + + # provide default color + unless label.first =~ /(?:#[[:xdigit:]]{6} ?){3}/ + label.unshift CONFIG['display']['color']['normal'] + end + + button.create unless button.exist? + button.write label.join(' ') + sleep refresh_rate + end + end + end + + ## + # Refreshes the label of this button. + # + alias refresh wakeup +end + +## +# Loads the given YAML configuration file. +# +def load_config config_file + Object.const_set :CONFIG, YAML.load_file(config_file) + + # script + eval CONFIG['script']['before'].to_s, TOPLEVEL_BINDING, + "#{config_file}:script:before" + + # 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'], + } + + begin + fs.ctl.write settings.map {|pair| pair.join(' ') }.join("\n") + + rescue Rumai::IXP::Error => e + # + # settings that are not supported in a particular wmii version + # are ignored, and those that are supported are (silently) + # applied. but a "bad command" error is raised nevertheless! + # + warn e.inspect + warn e.backtrace + end + + launch 'xsetroot', '-solid', CONFIG['display']['background'] + + # column + fs.colrules.write CONFIG['display']['column']['rule'] + + # client + event 'CreateClient' do |client_id| + client = Client.new(client_id) + + unless defined? @client_tags_by_regexp + @client_tags_by_regexp = CONFIG['display']['client'].map {|hash| + k, v = hash.to_a.first + [eval(k, TOPLEVEL_BINDING, "#{config_file}:display:client"), v] + } + end + + if label = client.props.read rescue nil + catch :found do + @client_tags_by_regexp.each do |regexp, tags| + if label =~ regexp + client.tags = tags + throw :found + end + end + + # force client onto current view + begin + client.tags = curr_tag + client.focus + rescue + # ignore + end + end + end + end + + # status + action 'status' do + fs.rbar.clear + + unless defined? @status_button_by_name + @status_button_by_name = {} + @status_button_by_file = {} + @on_click_by_status_button = {} + + CONFIG['display']['status'].each_with_index do |hash, position| + name, defn = hash.to_a.first + + # buttons appear in ASCII order of their IXP file name + file = "#{position}-#{name}" + + button = eval( + "Button.new(fs.rbar[#{file.inspect}], #{defn['refresh']}) { #{defn['content']} }", + TOPLEVEL_BINDING, "#{config_file}:display:status:#{name}" + ) + + @status_button_by_name[name] = button + @status_button_by_file[file] = button + + # mouse click handler + if code = defn['click'] + @on_click_by_status_button[button] = eval( + "lambda {|mouse_button| #{code} }", TOPLEVEL_BINDING, + "#{config_file}:display:status:#{name}:click" + ) + end + end + end + + @status_button_by_name.each_value {|b| b.refresh } + + end.call + + ## + # Returns the status button associated with the given name. + # + # ==== Parameters + # + # [name] + # Either the the user-defined name of + # the status button or the basename + # of the status button's IXP file. + # + def status_button name + @status_button_by_name[name] || @status_button_by_file[name] + end + + ## + # Refreshes the content of the status button with the given name. + # + # ==== Parameters + # + # [name] + # Either the the user-defined name of + # the status button or the basename + # of the status button's IXP file. + # + def status name + if button = status_button(name) + button.refresh + end + end + + ## + # Invokes the mouse click handler for the given mouse + # button on the status button that has the given name. + # + # ==== Parameters + # + # [name] + # Either the the user-defined name of + # the status button or the basename + # of the status button's IXP file. + # + # [mouse_button] + # The identification number of + # the mouse button (as defined + # by X server) that was clicked. + # + def status_click name, mouse_button + if button = status_button(name) and + handle = @on_click_by_status_button[button] + then + handle.call mouse_button.to_i + end + 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 + + # script + eval CONFIG['script']['after'].to_s, TOPLEVEL_BINDING, + "#{config_file}:script:after" + +end + +## +# Reloads the entire wmii configuration. +# +def reload_config + LOG.info 'reload' + launch $0 +end diff --git a/alternative_wmiircs/ruby/config.yaml b/alternative_wmiircs/ruby/config.yaml @@ -0,0 +1,804 @@ +# +# High-level wmii configuration. +# +# Ruby code in this file has access +# to a CONFIG constant which contains +# the data in this configuration file. +# +#-- +# Copyright protects this work. +# See LICENSE file for details. +#++ + + +## +# Appearance settings. +# +display: + + ## + # Where to display the horizontal status bar? + # + # Possible choices are "top" and "bottom". + # + bar: bottom + + ## + # The font to use in all text drawn by wmii. + # + font: -*-fixed-medium-r-*-*-18-*-*-*-*-*-*-* + + ## + # Thickness of client border (measured in pixels). + # + border: 1 + + ## + # Number of seconds a notice should be displayed. + # + notice: 5 + + ## + # Color schemes for everything drawn by wmii. + # + # <scheme>: "<text> <background> <border>" + # + # You can find more color schemes here: + # + # 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 wmii "colmode" setting> + # rule: <the wmii "colrules" setting> + # + column: + mode: default + rule: | + /gimp/ -> 17+83+41 + /.*/ -> 50+50 + + ## + # Mapping of clients to views they must appear on. + # + # - <client props regular expression> : <tags to apply> + # + # These mappings are processed in top-to-bottom order. + # Processing stops after the first matching mapping is applied. + # + client: + - /\b(xconsole|alsamixer|XMMS|Sonata)\b/ : 1 + - /^pidgin:|:WeeChat\b/ : chat + - /\b(Liferea|GMail|Thunderbird)\b/ : mail + - /:(Firefox|Shiretoko):.*\bRestore\b.*\bSession\b/ : web + + ## + # Self-refreshing buttons on the status bar. + # + # - <button name>: + # refresh: <number of seconds to wait before refreshing the content> + # content: <Ruby code whose result is displayed as the content> + # click: <Ruby code to handle mouse clicks on the status button. + # This code has access to a "mouse_button" variable which is + # an integer representing the mouse button that was clicked.> + # + # You can refresh a particular status button in Ruby using: + # + # status "your button name" + # + # The horizontal order in which these buttons appear on the status + # bar reflects the vertical order in which they are defined below. + # + status: + - 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 + + music_state = (@music.stopped? || @music.paused?) ? '(-)' : '(>)' + + if song = @music.current_song + artist = song.artist + title = song.title || (f = song.file and File.basename(f)) + song_name = [artist, title].compact.join(': ') + end + + [music_state, song_name].compact + + - volume: + refresh: 60 + content: | + ['volume', `amixer get Master`.scan(/\d+%/).first] + + - disk_space: + refresh: 600 # 10 minutes + content: | + free, used, path = `df -h ~`.split.last(3) + [path, used, 'used', free, 'free'] + + - system_load: + refresh: 10 + content: | + load_averages = File.read('/proc/loadavg').split.first(3) + current_load = load_averages.first.to_f + + # visually indicate the intensity of system load + color = case + when current_load > 3.0 then CONFIG['display']['color']['error'] + when current_load > 1.5 then CONFIG['display']['color']['notice'] + end + + [color, *load_averages] + + - clock: + refresh: 5 + content: Time.now.to_s + + +## +# 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,g: | # toggle current client from grouping + curr_client.group! + + Mod1-Control-g,c: | # add clients in current area to grouping + curr_area.group + + Mod1-Control-g,Shift-c: | # remove clients in current area from grouping + curr_area.ungroup + + Mod1-Control-g,f: | # add clients in floating area to grouping + Area.floating.group + + Mod1-Control-g,Shift-f: | # remove clients in floating area from grouping + Area.floating.ungroup + + Mod1-Control-g,m: | # add clients in managed areas to grouping + curr_view.managed_areas.each {|a| a.group } + + Mod1-Control-g,Shift-m: | # remove clients in managed areas from grouping + curr_view.managed_areas.each {|a| a.ungroup } + + Mod1-Control-g,v: | # add clients in current view to grouping + curr_view.group + + Mod1-Control-g,Shift-v: | # remove clients in current view from grouping + curr_view.ungroup + + Mod1-Control-g,i: | # invert the grouping in the current view + curr_view.group! + + Mod1-Control-g,Shift-i: | # invert the grouping in all views + Rumai.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 + if choice = key_menu(actions, 'run action:') + action choice + end + + Mod1-Control-e: | # run external program chosen from a menu + if choice = key_menu(@programs, 'run program:') + launch 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. + # + work = ENV['HOME'] + + label = curr_client.label.read rescue '' + + # iterate in reverse order because + # paths are usually at end of label + label.split(' ').reverse_each do |s| + path = File.expand_path(s) + + if File.exist? path + unless File.directory? path + path = File.dirname(path) + end + + work = path + break + end + end + + require 'fileutils' + FileUtils.cd work do + launch 'urxvt' + end + + Mod1-Control-k: | # launch a web browser + launch 'firefox' + + Mod1-Control-j: | # launch a file manager + launch 'thunar' + + Mod1-Control-q: | # launch a note taker + launch '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' + + Mod1-Control-Home: | # load a playlist + if list = key_menu(@music.playlists, 'load playlist:') + @music.clear + @music.load list + @music.play + end + + Mod1-Control-End: | # add current song to a playlist + if list = key_menu(@music.playlists, 'add current song to playlist:') + file = File.join(File.expand_path('~/.mpd/playlists'), list + '.m3u') + + songs = File.readlines(file) rescue [] + songs << @music.current_song.file + songs.uniq! + + File.open(file, 'w') {|f| f.puts songs } + end + + #--------------------------------------------------------------------------- + # 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 a list of arguments that were passed to 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 "#{CONFIG['display']['color']['notice']} #{tag}" if but.exist? + + NotUrgentTag: | + tag = argv[1] + but = fs.lbar[tag] + color = curr_view.id == tag ? 'focus' : 'normal' + but.write "#{CONFIG['display']['color'][color]} #{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 + + RightBarClick: | + status_click *argv.reverse + + 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: + reload: | # reload this wmii configuration + reload_config + + rehash: | # scan for available programs and actions + @programs = find_programs(ENV['PATH'].squeeze(':').split(':')) + + 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' + + +## +# Arbitrary logic. +# +# script: +# before: <Ruby code to execute before processing this file> +# after: <Ruby code to execute after processing this file> +# +script: + before: | + DETACHED_TAG = '|' + ZOOMED_SUFFIX = /~(\d+)$/ + + after: | + action 'rehash' + + # desktop wallpaper + system 'sh ~/.fehbg' + diff --git a/alternative_wmiircs/ruby/wmiirc b/alternative_wmiircs/ruby/wmiirc @@ -0,0 +1,83 @@ +#!/usr/bin/env ruby +# Bootloader for wmii configuration. +#-- +# Copyright protects this work. +# See LICENSE file for details. +#++ + +# create a logger to aid debugging +require 'logger' +LOG = Logger.new(__FILE__ + '.log', 5) + +class << LOG + # emulate IO.write + alias write << + + def flush + # ignore + end +end + +# capture standard output in logger +$stdout = $stderr = LOG + +begin + LOG.info 'birth' + + # load configuration library + config_home = File.dirname(__FILE__) + config_libs = File.join(config_home, 'config.rb') + config_file = File.join(config_home, 'config.yaml') + + require config_libs + + # terminate any existing wmiirc + fs.event.write 'Start wmiirc' + + event 'Start' do |arg| + exit if arg == 'wmiirc' + end + + # apply user configuration + load_config config_file + + # setup tag bar (buttons that correspond to views) + fs.lbar.clear + tags.each {|t| event 'CreateTag', t } + event 'FocusTag', curr_tag + + # register key bindings + fs.keys.write keys.join("\n") + event('Key') {|*a| key(*a) } + + # the main event loop + fs.event.each_line do |line| + line.split("\n").each do |call| + name, args = call.split(' ', 2) + + argv = args.to_s.split(' ') + event name, *argv + end + end + +rescue SystemExit + # ignore it; the program wants to terminate + +rescue Exception => e + LOG.error e + + # allow the user to rescue themselves + system 'xterm &' + + IO.popen('xmessage -nearmouse -file - -buttons Recover,Ignore -print', 'w+') do |f| + f.puts e.inspect, e.backtrace + f.close_write + + if f.read.chomp == 'Recover' + reload_config + end + end + +ensure + LOG.info 'death' +end