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