wmiirc-rumai

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

config.rb (10219B)


      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 # When 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 = %w[dmenu -b -fn].push(CONFIG['display']['font'])
    110 
    111   words.concat %w[-nf -nb -sf -sb].zip(
    112     [
    113       CONFIG['display']['color']['normal'],
    114       CONFIG['display']['color']['focus'],
    115 
    116     ].map {|c| c.to_s.split[0,2] }.flatten
    117 
    118   ).flatten
    119 
    120   words.push '-p', prompt if prompt
    121 
    122   command = words.shelljoin
    123   IO.popen(command, 'r+') do |menu|
    124     menu.puts choices
    125     menu.close_write
    126 
    127     choice = menu.read
    128     choice unless choice.empty?
    129   end
    130 end
    131 
    132 ##
    133 # Shows a menu (where the user must click a menu
    134 # item using their mouse to make a choice) with
    135 # the given items and returns the chosen item.
    136 #
    137 # If nothing was chosen, then nil is returned.
    138 #
    139 # ==== Parameters
    140 #
    141 # [choices]
    142 #   List of choices to display in the menu.
    143 #
    144 # [initial]
    145 #   The choice that should be initially selected.
    146 #
    147 #   If this choice is not included in the list
    148 #   of cohices, then this item will be made
    149 #   into a makeshift title-bar for the menu.
    150 #
    151 def click_menu choices, initial = nil
    152   words = %w[wmii9menu]
    153 
    154   if initial
    155     words << '-i'
    156 
    157     unless choices.include? initial
    158       initial = "<<#{initial}>>:"
    159       words << initial
    160     end
    161 
    162     words << initial
    163   end
    164 
    165   words.concat choices
    166   command = words.shelljoin
    167 
    168   choice = `#{command}`.chomp
    169   choice unless choice.empty?
    170 end
    171 
    172 ##
    173 # Returns the basenames of executable files present in the given directories.
    174 #
    175 def find_programs *dirs
    176   dirs.flatten.
    177   map {|d| Pathname.new(d).expand_path.children rescue [] }.flatten.
    178   map {|f| f.basename.to_s if f.file? and f.executable? }.compact.uniq.sort
    179 end
    180 
    181 ##
    182 # Launches the command built from the given words in the background.
    183 #
    184 def launch *words
    185   command = words.shelljoin
    186   system "#{command} &"
    187 end
    188 
    189 ##
    190 # A button on a bar.
    191 #
    192 class Button < Thread
    193   ##
    194   # Creates a new button at the given node and updates its label
    195   # according to the given refresh rate (measured in seconds).  The
    196   # given block is invoked to calculate the label of the button.
    197   #
    198   # The return value of the given block can be either an
    199   # array (whose first item is a wmii color sequence for the
    200   # button, and the remaining items compose the label of the
    201   # button) or a string containing the label of the button.
    202   #
    203   # If the given block raises a standard exception, then that will be
    204   # rescued and displayed (using error colors) as the button's label.
    205   #
    206   def initialize fs_bar_node, refresh_rate, &button_label
    207     raise ArgumentError, 'block must be given' unless block_given?
    208 
    209     super(fs_bar_node) do |button|
    210       while true
    211         label =
    212           begin
    213             Array(button_label.call)
    214           rescue Exception => e
    215             LOG.error e
    216             [CONFIG['display']['color']['error'], e]
    217           end
    218 
    219         # provide default color
    220         unless label.first =~ /(?:#[[:xdigit:]]{6} ?){3}/
    221           label.unshift CONFIG['display']['color']['normal']
    222         end
    223 
    224         button.create unless button.exist?
    225         button.write label.join(' ')
    226         sleep refresh_rate
    227       end
    228     end
    229   end
    230 
    231   ##
    232   # Refreshes the label of this button.
    233   #
    234   alias refresh wakeup
    235 end
    236 
    237 ##
    238 # Loads the given YAML configuration file.
    239 #
    240 def load_config config_file
    241   Object.const_set :CONFIG, YAML.load_file(config_file)
    242 
    243   # script
    244     eval CONFIG['script']['before'].to_s, TOPLEVEL_BINDING,
    245          "#{config_file}:script:before"
    246 
    247   # display
    248     fo = ENV['WMII_FONT']        = CONFIG['display']['font']
    249     fc = ENV['WMII_FOCUSCOLORS'] = CONFIG['display']['color']['focus']
    250     nc = ENV['WMII_NORMCOLORS']  = CONFIG['display']['color']['normal']
    251 
    252     settings = {
    253       'font'        => fo,
    254       'focuscolors' => fc,
    255       'normcolors'  => nc,
    256       'border'      => CONFIG['display']['border'],
    257       'bar on'      => CONFIG['display']['bar'],
    258       'colmode'     => CONFIG['display']['column']['mode'],
    259       'grabmod'     => CONFIG['control']['grab'],
    260     }
    261 
    262     begin
    263       fs.ctl.write settings.map {|pair| pair.join(' ') }.join("\n")
    264 
    265     rescue Rumai::IXP::Error => e
    266       #
    267       # settings that are not supported in a particular wmii version
    268       # are ignored, and those that are supported are (silently)
    269       # applied.  but a "bad command" error is raised nevertheless!
    270       #
    271       warn e.inspect
    272       warn e.backtrace
    273     end
    274 
    275     launch 'xsetroot', '-solid', CONFIG['display']['background']
    276 
    277     # column
    278       fs.colrules.write CONFIG['display']['column']['rule']
    279 
    280     # client
    281       event 'CreateClient' do |client_id|
    282         client = Client.new(client_id)
    283 
    284         unless defined? @client_tags_by_regexp
    285           @client_tags_by_regexp = CONFIG['display']['client'].map {|hash|
    286             k, v = hash.to_a.first
    287             [eval(k, TOPLEVEL_BINDING, "#{config_file}:display:client"), v]
    288           }
    289         end
    290 
    291         if label = client.props.read rescue nil
    292           catch :found do
    293             @client_tags_by_regexp.each do |regexp, tags|
    294               if label =~ regexp
    295                 client.tags = tags
    296                 throw :found
    297               end
    298             end
    299 
    300             # force client onto current view
    301             begin
    302               client.tags = curr_tag
    303               client.focus
    304             rescue
    305               # ignore
    306             end
    307           end
    308         end
    309       end
    310 
    311     # status
    312       action 'status' do
    313         fs.rbar.clear
    314 
    315         unless defined? @status_button_by_name
    316           @status_button_by_name     = {}
    317           @status_button_by_file     = {}
    318           @on_click_by_status_button = {}
    319 
    320           CONFIG['display']['status'].each_with_index do |hash, position|
    321             name, defn = hash.to_a.first
    322 
    323             # buttons appear in ASCII order of their IXP file name
    324             file = "#{position}-#{name}"
    325 
    326             button = eval(
    327               "Button.new(fs.rbar[#{file.inspect}], #{defn['refresh']}) { #{defn['content']} }",
    328               TOPLEVEL_BINDING, "#{config_file}:display:status:#{name}"
    329             )
    330 
    331             @status_button_by_name[name] = button
    332             @status_button_by_file[file] = button
    333 
    334             # mouse click handler
    335             if code = defn['click']
    336               @on_click_by_status_button[button] = eval(
    337                 "lambda {|mouse_button| #{code} }", TOPLEVEL_BINDING,
    338                 "#{config_file}:display:status:#{name}:click"
    339               )
    340             end
    341           end
    342         end
    343 
    344         @status_button_by_name.each_value {|b| b.refresh }
    345 
    346       end.call
    347 
    348       ##
    349       # Returns the status button associated with the given name.
    350       #
    351       # ==== Parameters
    352       #
    353       # [name]
    354       #   Either the the user-defined name of
    355       #   the status button or the basename
    356       #   of the status button's IXP file.
    357       #
    358       def status_button name
    359         @status_button_by_name[name] || @status_button_by_file[name]
    360       end
    361 
    362       ##
    363       # Refreshes the content of the status button with the given name.
    364       #
    365       # ==== Parameters
    366       #
    367       # [name]
    368       #   Either the the user-defined name of
    369       #   the status button or the basename
    370       #   of the status button's IXP file.
    371       #
    372       def status name
    373         if button = status_button(name)
    374           button.refresh
    375         end
    376       end
    377 
    378       ##
    379       # Invokes the mouse click handler for the given mouse
    380       # button on the status button that has the given name.
    381       #
    382       # ==== Parameters
    383       #
    384       # [name]
    385       #   Either the the user-defined name of
    386       #   the status button or the basename
    387       #   of the status button's IXP file.
    388       #
    389       # [mouse_button]
    390       #   The identification number of
    391       #   the mouse button (as defined
    392       #   by X server) that was clicked.
    393       #
    394       def status_click name, mouse_button
    395         if button = status_button(name) and
    396            handle = @on_click_by_status_button[button]
    397         then
    398           handle.call mouse_button.to_i
    399         end
    400       end
    401 
    402   # control
    403     %w[key action event].each do |param|
    404       CONFIG['control'][param].each do |name, code|
    405         eval "#{param}(#{name.inspect}) {|*argv| #{code} }",
    406              TOPLEVEL_BINDING, "#{config_file}:control:#{param}:#{name}"
    407       end
    408     end
    409 
    410   # script
    411     eval CONFIG['script']['after'].to_s, TOPLEVEL_BINDING,
    412          "#{config_file}:script:after"
    413 
    414 end
    415 
    416 ##
    417 # Reloads the entire wmii configuration.
    418 #
    419 def reload_config
    420   LOG.info 'reload'
    421   launch $0
    422 end