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