wmii

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

config.rb (13445B)


      1 # DSL for wmiirc configuration.
      2 #--
      3 # Copyright protects this work.
      4 # See LICENSE file for details.
      5 #++
      6 
      7 require 'shellwords'
      8 require 'pathname'
      9 require 'yaml'
     10 
     11 require 'rubygems'
     12 gem 'rumai', '~> 3'
     13 require 'rumai'
     14 
     15 include Rumai
     16 
     17 class Handler < Hash
     18   def initialize
     19     super {|h,k| h[k] = [] }
     20   end
     21 
     22   ##
     23   # If a block is given, registers a handler
     24   # for the given key and returns the handler.
     25   #
     26   # Otherwise, executes all handlers registered for the given key.
     27   #
     28   def handle key, *args, &block
     29     if block
     30       self[key] << block
     31 
     32     elsif key? key
     33       self[key].each do |block|
     34         block.call(*args)
     35       end
     36     end
     37 
     38     block
     39   end
     40 end
     41 
     42 EVENTS  = Handler.new
     43 ACTIONS = Handler.new
     44 KEYS    = Handler.new
     45 
     46 ##
     47 # If a block is given, registers a handler
     48 # for the given event and returns the handler.
     49 #
     50 # Otherwise, executes all handlers for the given event.
     51 #
     52 def event *a, &b
     53   EVENTS.handle(*a, &b)
     54 end
     55 
     56 ##
     57 # Returns a list of registered event names.
     58 #
     59 def events
     60   EVENTS.keys
     61 end
     62 
     63 ##
     64 # If a block is given, registers a handler for
     65 # the given action and returns the handler.
     66 #
     67 # Otherwise, executes all handlers for the given action.
     68 #
     69 def action *a, &b
     70   ACTIONS.handle(*a, &b)
     71 end
     72 
     73 ##
     74 # Returns a list of registered action names.
     75 #
     76 def actions
     77   ACTIONS.keys
     78 end
     79 
     80 ##
     81 # If a block is given, registers a handler for
     82 # the given keypress and returns the handler.
     83 #
     84 # Otherwise, executes all handlers for the given keypress.
     85 #
     86 def key *a, &b
     87   KEYS.handle(*a, &b)
     88 end
     89 
     90 ##
     91 # Returns a list of registered action names.
     92 #
     93 def keys
     94   KEYS.keys
     95 end
     96 
     97 ##
     98 # Shows a menu (where the user must press keys on their keyboard to
     99 # make a choice) with the given items and returns the chosen item.
    100 #
    101 # If nothing was chosen, then nil is returned.
    102 #
    103 # ==== Parameters
    104 #
    105 # [prompt]
    106 #   Instruction on what the user should enter or choose.
    107 #
    108 def key_menu choices, prompt = nil
    109   words = ['dmenu', '-fn', CONFIG['display']['font']]
    110 
    111   # show menu at the same location as the status bar
    112   words << '-b' if CONFIG['display']['bar'] == 'bottom'
    113 
    114   words.concat %w[-nf -nb -sf -sb].zip(
    115     [
    116       CONFIG['display']['color']['normal'],
    117       CONFIG['display']['color']['focus'],
    118 
    119     ].map {|c| c.to_s.split[0,2] }.flatten
    120 
    121   ).flatten
    122 
    123   words.push '-p', prompt if prompt
    124 
    125   command = words.shelljoin
    126   IO.popen(command, 'r+') do |menu|
    127     menu.puts choices
    128     menu.close_write
    129 
    130     choice = menu.read
    131     choice unless choice.empty?
    132   end
    133 end
    134 
    135 ##
    136 # Shows a menu (where the user must click a menu
    137 # item using their mouse to make a choice) with
    138 # the given items and returns the chosen item.
    139 #
    140 # If nothing was chosen, then nil is returned.
    141 #
    142 # ==== Parameters
    143 #
    144 # [choices]
    145 #   List of choices to display in the menu.
    146 #
    147 # [initial]
    148 #   The choice that should be initially selected.
    149 #
    150 #   If this choice is not included in the list
    151 #   of choices, then this item will be made
    152 #   into a makeshift title-bar for the menu.
    153 #
    154 def click_menu choices, initial = nil
    155   words = ['wmii9menu']
    156 
    157   if initial
    158     words << '-i'
    159 
    160     unless choices.include? initial
    161       initial = "<<#{initial}>>:"
    162       words << initial
    163     end
    164 
    165     words << initial
    166   end
    167 
    168   words.concat choices
    169   command = words.shelljoin
    170 
    171   choice = `#{command}`.chomp
    172   choice unless choice.empty?
    173 end
    174 
    175 ##
    176 # Shows a key_menu() containing the given
    177 # clients and returns the chosen client.
    178 #
    179 # If nothing was chosen, then nil is returned.
    180 #
    181 # ==== Parameters
    182 #
    183 # [prompt]
    184 #   Instruction on what the user should enter or choose.
    185 #
    186 # [clients]
    187 #   List of clients to present as choices to the user.
    188 #
    189 #   If this parameter is not specified,
    190 #   its default value will be a list of
    191 #   all currently available clients.
    192 #
    193 def client_menu prompt = nil, clients = Rumai.clients
    194   choices = []
    195 
    196   clients.each_with_index do |c, i|
    197     choices << "%d. [%s] %s" % [i, c[:tags].read, c[:label].read.downcase]
    198   end
    199 
    200   if target = key_menu(choices, prompt)
    201     clients[target.scan(/\d+/).first.to_i]
    202   end
    203 end
    204 
    205 ##
    206 # Returns the basenames of executable files present in the given directories.
    207 #
    208 def find_programs *dirs
    209   dirs.flatten.
    210   map {|d| Pathname.new(d).expand_path.children rescue [] }.flatten.
    211   map {|f| f.basename.to_s if f.file? and f.executable? }.compact.uniq.sort
    212 end
    213 
    214 ##
    215 # Launches the command built from the given words in the background.
    216 #
    217 def launch *words
    218   command = words.shelljoin
    219   system "#{command} &"
    220 end
    221 
    222 ##
    223 # A button on a bar.
    224 #
    225 class Button < Thread
    226   ##
    227   # Creates a new button at the given node and updates its label
    228   # according to the given refresh rate (measured in seconds).  The
    229   # given block is invoked to calculate the label of the button.
    230   #
    231   # The return value of the given block can be either an
    232   # array (whose first item is a wmii color sequence for the
    233   # button, and the remaining items compose the label of the
    234   # button) or a string containing the label of the button.
    235   #
    236   # If the given block raises a standard exception, then that will be
    237   # rescued and displayed (using error colors) as the button's label.
    238   #
    239   def initialize fs_bar_node, refresh_rate, &button_label
    240     raise ArgumentError, 'block must be given' unless block_given?
    241 
    242     super(fs_bar_node) do |button|
    243       while true
    244         label =
    245           begin
    246             Array(button_label.call)
    247           rescue Exception => e
    248             LOG.error e
    249             [CONFIG['display']['color']['error'], e]
    250           end
    251 
    252         # provide default color
    253         unless label.first =~ /(?:#[[:xdigit:]]{6} ?){3}/
    254           label.unshift CONFIG['display']['color']['normal']
    255         end
    256 
    257         button.create unless button.exist?
    258         button.write label.join(' ')
    259         sleep refresh_rate
    260       end
    261     end
    262   end
    263 
    264   ##
    265   # Refreshes the label of this button.
    266   #
    267   alias refresh wakeup
    268 end
    269 
    270 ##
    271 # Loads the given YAML configuration file.
    272 #
    273 def load_config config_file
    274   Object.const_set :CONFIG, YAML.load_file(config_file)
    275 
    276   # script
    277     eval CONFIG['script']['before'].to_s, TOPLEVEL_BINDING,
    278          "#{config_file}:script:before"
    279 
    280   # display
    281     fo = ENV['WMII_FONT']        = CONFIG['display']['font']
    282     fc = ENV['WMII_FOCUSCOLORS'] = CONFIG['display']['color']['focus']
    283     nc = ENV['WMII_NORMCOLORS']  = CONFIG['display']['color']['normal']
    284 
    285     settings = {
    286       'font'        => fo,
    287       'focuscolors' => fc,
    288       'normcolors'  => nc,
    289       'border'      => CONFIG['display']['border'],
    290       'bar on'      => CONFIG['display']['bar'],
    291       'colmode'     => CONFIG['display']['column']['mode'],
    292       'grabmod'     => CONFIG['control']['grab'],
    293     }
    294 
    295     begin
    296       fs.ctl.write settings.map {|pair| pair.join(' ') }.join("\n")
    297 
    298     rescue Rumai::IXP::Error => e
    299       #
    300       # settings that are not supported in a particular wmii version
    301       # are ignored, and those that are supported are (silently)
    302       # applied.  but a "bad command" error is raised nevertheless!
    303       #
    304       warn e.inspect
    305       warn e.backtrace.join("\n")
    306     end
    307 
    308     launch 'xsetroot', '-solid', CONFIG['display']['background']
    309 
    310     # column
    311       fs.colrules.write CONFIG['display']['column']['rule']
    312 
    313     # client
    314       event 'CreateClient' do |client_id|
    315         client = Client.new(client_id)
    316 
    317         unless defined? @client_tags_by_regexp
    318           @client_tags_by_regexp = CONFIG['display']['client'].map {|hash|
    319             k, v = hash.to_a.first
    320             [eval(k, TOPLEVEL_BINDING, "#{config_file}:display:client"), v]
    321           }
    322         end
    323 
    324         if label = client.props.read rescue nil
    325           catch :found do
    326             @client_tags_by_regexp.each do |regexp, tags|
    327               if label =~ regexp
    328                 client.tags = tags
    329                 throw :found
    330               end
    331             end
    332 
    333             # force client onto current view
    334             begin
    335               client.tags = curr_tag
    336               client.focus
    337             rescue
    338               # ignore
    339             end
    340           end
    341         end
    342       end
    343 
    344     # status
    345       action 'status' do
    346         fs.rbar.clear
    347 
    348         unless defined? @status_button_by_name
    349           @status_button_by_name     = {}
    350           @status_button_by_file     = {}
    351           @on_click_by_status_button = {}
    352 
    353           CONFIG['display']['status'].each_with_index do |hash, position|
    354             name, defn = hash.to_a.first
    355 
    356             # buttons appear in ASCII order of their IXP file name
    357             file = "#{position}-#{name}"
    358 
    359             button = eval(
    360               "Button.new(fs.rbar[#{file.inspect}], #{defn['refresh']}) { #{defn['content']} }",
    361               TOPLEVEL_BINDING, "#{config_file}:display:status:#{name}"
    362             )
    363 
    364             @status_button_by_name[name] = button
    365             @status_button_by_file[file] = button
    366 
    367             # mouse click handler
    368             if code = defn['click']
    369               @on_click_by_status_button[button] = eval(
    370                 "lambda {|mouse_button| #{code} }", TOPLEVEL_BINDING,
    371                 "#{config_file}:display:status:#{name}:click"
    372               )
    373             end
    374           end
    375         end
    376 
    377         @status_button_by_name.each_value {|b| b.refresh }
    378 
    379       end
    380 
    381       ##
    382       # Returns the status button associated with the given name.
    383       #
    384       # ==== Parameters
    385       #
    386       # [name]
    387       #   Either the the user-defined name of
    388       #   the status button or the basename
    389       #   of the status button's IXP file.
    390       #
    391       def status_button name
    392         @status_button_by_name[name] || @status_button_by_file[name]
    393       end
    394 
    395       ##
    396       # Refreshes the content of the status button with the given name.
    397       #
    398       # ==== Parameters
    399       #
    400       # [name]
    401       #   Either the the user-defined name of
    402       #   the status button or the basename
    403       #   of the status button's IXP file.
    404       #
    405       def status name
    406         if button = status_button(name)
    407           button.refresh
    408         end
    409       end
    410 
    411       ##
    412       # Invokes the mouse click handler for the given mouse
    413       # button on the status button that has the given name.
    414       #
    415       # ==== Parameters
    416       #
    417       # [name]
    418       #   Either the the user-defined name of
    419       #   the status button or the basename
    420       #   of the status button's IXP file.
    421       #
    422       # [mouse_button]
    423       #   The identification number of
    424       #   the mouse button (as defined
    425       #   by X server) that was clicked.
    426       #
    427       def status_click name, mouse_button
    428         if button = status_button(name) and
    429            handle = @on_click_by_status_button[button]
    430         then
    431           handle.call mouse_button.to_i
    432         end
    433       end
    434 
    435   # control
    436     action 'reload' do
    437       # reload this wmii configuration
    438       reload_config
    439     end
    440 
    441     action 'rehash' do
    442       # scan for available programs and actions
    443       @programs = find_programs(ENV['PATH'].squeeze(':').split(':'))
    444     end
    445 
    446     # kill all currently open clients
    447     action 'clear' do
    448       # firefox's restore session feature does not
    449       # work unless the whole process is killed.
    450       system 'killall firefox firefox-bin thunderbird thunderbird-bin'
    451 
    452       # gnome-panel refuses to die by any other means
    453       system 'killall -s TERM gnome-panel'
    454 
    455       Thread.pass until clients.each do |c|
    456         begin
    457           c.focus # XXX: client must be on current view in order to be killed
    458           c.kill
    459         rescue
    460           # ignore
    461         end
    462       end.empty?
    463     end
    464 
    465     # kill the window manager only; do not touch the clients!
    466     action 'kill' do
    467       fs.ctl.write 'quit'
    468     end
    469 
    470     # kill both clients and window manager
    471     action 'quit' do
    472       action 'clear'
    473       action 'kill'
    474     end
    475 
    476     event 'Unresponsive' do |client_id|
    477       client = Client.new(client_id)
    478 
    479       IO.popen('xmessage -nearmouse -file - -buttons Kill,Wait -print', 'w+') do |f|
    480         f.puts 'The following client is not responding.', ''
    481         f.puts client.inspect
    482         f.puts client.label.read
    483 
    484         f.puts '', 'What would you like to do?'
    485         f.close_write
    486 
    487         if f.read.chomp == 'Kill'
    488           client.slay
    489         end
    490       end
    491     end
    492 
    493     event 'Notice' do |*argv|
    494       unless defined? @notice_mutex
    495         require 'thread'
    496         @notice_mutex = Mutex.new
    497       end
    498 
    499       Thread.new do
    500         # prevent notices from overwriting each other
    501         @notice_mutex.synchronize do
    502           button = fs.rbar['!notice']
    503           button.create unless button.exist?
    504 
    505           # display the notice
    506           message = argv.join(' ')
    507 
    508           LOG.info message # also log it in case the user is AFK
    509           button.write "#{CONFIG['display']['color']['notice']} #{message}"
    510 
    511           # clear the notice
    512           sleep [1, CONFIG['display']['notice'].to_i].max
    513           button.remove
    514         end
    515       end
    516     end
    517 
    518     %w[key action event].each do |param|
    519       if settings = CONFIG['control'][param]
    520         settings.each do |name, code|
    521           if param == 'key'
    522             # expand ${...} expressions in shortcut key sequences
    523             name = name.gsub(/\$\{(.+?)\}/) { CONFIG['control'][$1] }
    524           end
    525 
    526           eval "#{param}(#{name.inspect}) {|*argv| #{code} }",
    527                TOPLEVEL_BINDING, "#{config_file}:control:#{param}:#{name}"
    528         end
    529       end
    530     end
    531 
    532   # script
    533     action 'status'
    534     action 'rehash'
    535 
    536     eval CONFIG['script']['after'].to_s, TOPLEVEL_BINDING,
    537          "#{config_file}:script:after"
    538 
    539 end
    540 
    541 ##
    542 # Reloads the entire wmii configuration.
    543 #
    544 def reload_config
    545   LOG.info 'reload'
    546   exec $0
    547 end