├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Makefile ├── README.markdown ├── control ├── action.yaml ├── action │ ├── client.yaml │ ├── detach.yaml │ ├── focus.yaml │ ├── group.yaml │ ├── launch.yaml │ ├── layout.yaml │ ├── resize.yaml │ ├── send.yaml │ ├── swap.yaml │ └── zoom.yaml ├── keyboard.yaml └── mouse.yaml ├── display ├── barlet.yaml ├── client.yaml ├── color.yaml ├── color │ ├── cadmium.yaml │ ├── chrome-gtk.yaml │ ├── chrome.yaml │ ├── github.yaml │ ├── gruvbox.yaml │ ├── lucius.yaml │ ├── maglione.yaml │ ├── wmii.yaml │ ├── wombat.yaml │ ├── zenburn.yaml │ └── zukitwo-dark.yaml ├── desktop.yaml ├── desktop │ └── feh.yaml ├── status.yaml └── tags.yaml ├── lib ├── wmiirc.rb └── wmiirc │ ├── config.rb │ ├── handler.rb │ ├── import.rb │ ├── loader.rb │ ├── menu.rb │ └── system.rb ├── schema.yaml ├── status ├── arrange.yaml ├── backlight.yaml ├── battery.yaml ├── clock.yaml ├── disk.yaml ├── ipaddr.yaml ├── loadavg.yaml ├── memory.yaml ├── music │ ├── mpd.yaml │ └── rhythmbox.yaml ├── network.yaml ├── notice.yaml ├── pomodoro.yaml ├── spacer.yaml ├── thermal.yaml ├── volume.yaml └── weather.yaml ├── wmiirc └── wmiirc.rb /.gitignore: -------------------------------------------------------------------------------- 1 | wmiirc.log 2 | config.dump 3 | session.dump 4 | history/ 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # for generating docs 4 | gem 'yard' 5 | 6 | # for controlling wmii 7 | gem 'rumai', '>= 4.1.3' 8 | 9 | # for validating YAML 10 | gem 'kwalify', '>= 0.7.2' 11 | 12 | # for status/weather.yaml 13 | # gem 'barometer', '~> 0.9.7' 14 | 15 | # for status/music/mpd.yaml 16 | # gem 'librmpd', '~> 0.1' 17 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | kwalify (0.7.2) 5 | rumai (4.1.3) 6 | yard (0.9.20) 7 | 8 | PLATFORMS 9 | ruby 10 | 11 | DEPENDENCIES 12 | kwalify (>= 0.7.2) 13 | rumai (>= 4.1.3) 14 | yard 15 | 16 | BUNDLED WITH 17 | 1.16.1 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (the ISC license) 2 | 3 | Copyright 2006 Suraj N. Kurapati 4 | Copyright 2007 Kris Maglione 5 | Copyright 2007 Nick Stenning 6 | Copyright 2009 Daniel Wäber 7 | Copyright 2009 Michael Andrus 8 | Copyright 2009 Mike Simons 9 | Copyright 2009 Simon Hafner 10 | Copyright 2009 Yuval Hager 11 | Copyright 2010 Aditya Mahajan 12 | Copyright 2010 Florian Eitel 13 | Copyright 2010 Nathan Neff 14 | Copyright 2010 Sebastian Chmielewski 15 | Copyright 2010 tha 16 | Copyright 2010 Tom Kazimiers 17 | Copyright 2011 Mattia Gheda 18 | Copyright 2011 Nikhilesh S. 19 | 20 | Permission to use, copy, modify, and/or distribute this software for any 21 | purpose with or without fee is hereby granted, provided that the above 22 | copyright notice and this permission notice appear in all copies. 23 | 24 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 25 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 26 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 27 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 28 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 29 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 30 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | rebase: 4 | git rebase HEAD # ensure that there are no uncommitted changes 5 | git remote show upstream || git remote add upstream https://github.com/sunaku/wmiirc.git 6 | git fetch upstream 7 | git rebase upstream/master 8 | 9 | branch: rebase 10 | git checkout -b personal 11 | git checkout upstream/personal -- config.yaml 12 | @echo '---------------------------------------------------------------' 13 | @echo 'IMPORTANT: You must now edit config.yaml to suit your needs ...' 14 | @echo '---------------------------------------------------------------' 15 | 16 | rumai: 17 | git clone git://github.com/sunaku/rumai.git 18 | sed '2a$$:.unshift File.expand_path("../rumai/lib", __FILE__)' -i wmiirc.rb 19 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | sunaku's wmii configuration in Ruby and YAML 2 | ============================================================================== 3 | 4 | ![Floating](https://github.com/sunaku/wmiirc/raw/gh-pages/floating.png) 5 | ![Tiling](https://github.com/sunaku/wmiirc/raw/gh-pages/tiling.png) 6 | 7 | This is a [Ruby] and [YAML] based configuration of the [wmii] window manager. 8 | It manipulates wmii through the [Rumai] library (which speaks directly to wmii 9 | via the 9P2000 protocol and features [an interactive Ruby shell][RumaiShell] 10 | for live experimentation) and offers a near "Desktop Environment" experience: 11 | 12 | * Status bar applets with mouse, keyboard, and menu access. 13 | * System, dialog, and menu (with history) integration. 14 | * Client grouping and mass manipulation thereof. 15 | * View and client access by menu and alphanumeric keys. 16 | * Automated client arrangements with optional persistence. 17 | * Detaching clients from current view and restoring them. 18 | * Zooming clients to temporary views and restoring them. 19 | * Closing all clients before exiting the window manager. 20 | * Script and stdout/err logging with automatic rotation. 21 | * Crash handling with error trace and recovery console. 22 | * Session state propagation between wmiirc instances. 23 | * And oh so much more... :-] 24 | 25 | All of this can be configured to suit your needs, of course. This wmii 26 | configuration was also described in the following articles in the past: 27 | 28 | * 29 | * 30 | 31 | API documentation for Ruby code that powers this configuration is here: 32 | 33 | * 34 | 35 | [Ruby]: http://ruby-lang.org 36 | [YAML]: http://yaml.org 37 | [wmii]: http://wmii.suckless.org 38 | [Rumai]: http://snk.tuxfamily.org/lib/rumai/ 39 | [RumaiShell]: http://snk.tuxfamily.org/lib/rumai/#_usage 40 | [Kwalify]: http://www.kuwata-lab.com/kwalify/ 41 | 42 | ------------------------------------------------------------------------------ 43 | Requirements 44 | ------------------------------------------------------------------------------ 45 | 46 | * [wmii] 3.9 or newer. I recommend that you use [my personal fork 47 | of wmii-hg]( https://github.com/sunaku/wmii ) for best results. 48 | 49 | Note that the `display/status/arrange` status bar applet requires a 50 | [patched version of wmii-hg revision 2758 or greater]( 51 | https://github.com/sunaku/wmii/commit/33bf199436213788078581a8a94c2dcc98d6af16 52 | ) in order to *persist* automated client arrangements. 53 | 54 | * [Ruby] 2 or newer. 55 | 56 | * If you want to use the `status/weather.yaml` status bar applet, 57 | uncomment the following line in the `Gemfile` and restart wmii: 58 | 59 | gem 'barometer' 60 | 61 | * If you want to use the `status/music/mpd.yaml` status bar applet, 62 | uncomment the following line in the `Gemfile` and restart wmii: 63 | 64 | gem 'librmpd' 65 | 66 | ------------------------------------------------------------------------------ 67 | Installing 68 | ------------------------------------------------------------------------------ 69 | 70 | Backup: 71 | 72 | mv ~/.wmii ~/.wmii.backup 73 | mv ~/.wmii-hg ~/.wmii-hg.backup 74 | 75 | Install: 76 | 77 | git clone https://github.com/sunaku/wmiirc.git ~/.wmii 78 | ln -s ~/.wmii ~/.wmii-hg 79 | 80 | Branch: 81 | 82 | cd ~/.wmii 83 | make branch 84 | 85 | ------------------------------------------------------------------------------ 86 | Configuring 87 | ------------------------------------------------------------------------------ 88 | 89 | * Edit the `~/.wmii/config.yaml` file (see the "Configuration File Format" 90 | section below) to suit your needs. See [my personal configuration file]( 91 | https://github.com/sunaku/wmiirc/blob/personal/config.yaml ) for example. 92 | 93 | * If wmii is already running, run `~/.wmii/wmiirc` or invoke the "reload" 94 | action from within an existing wmiirc instance to apply your changes. 95 | 96 | ### Configuration File Format 97 | 98 | All Ruby code snippets in the configuration file have access to a `CONFIG` 99 | constant which contains the data from the fully expanded configuration. They 100 | also have access to a `SESSION` constant which is a hash that is automatically 101 | persisted across multiple instances of the wmiirc. 102 | 103 | * **custom:** Any value of your own choosing to use in your configuration. 104 | For example, you can define a list of applications to always launch 105 | (unless they're already running) whenever your configuration starts: 106 | 107 | custom: 108 | startup: 109 | - wpa_gui -t # network manager 110 | - claws-mail # e-mail client 111 | - redshift-gtk # color temperature 112 | 113 | script: 114 | after: | 115 | Array(CONFIG['custom']['startup']). # <== NOTICE THE KEYS HERE !!! 116 | each {|app| launch! app unless system 'pgrep', '-f', app } 117 | 118 | * **import:** A list of files to inject into this one before evaluating it. 119 | Imported files may themselves import other files, recursively. The 120 | contents of each successive imported file are merged with the previous 121 | one while *overwriting* the imported content in the following manner: 122 | 123 | * If the object being overwritten is a hash, then: 124 | 125 | * For keys that are present in the old hash but absent in the new 126 | hash, key-value pairs from the old hash are retained. 127 | 128 | * For keys that are present in the new hash but absent in the old 129 | hash, key-value pairs from the new hash are added. 130 | 131 | * For keys in common between the old and new hashes, key-value pairs 132 | from the old hash are replaced by key-value pairs from the new hash. 133 | 134 | * If the object being overwritten is an array, then items from the new 135 | array are appended to end of the old array. 136 | 137 | * If the object being overwritten is a scalar value such as a string, 138 | integer, or boolean, then the old value is replaced by the new value. 139 | 140 | * **ignore:** A list of files to remove from the **import** statement above. 141 | 142 | * **require:** A list of Ruby libraries to load before evaluating this 143 | configuration file. If a library is a RubyGem, you can constrain its 144 | version number like this: 145 | 146 | require: 147 | - some_gem 148 | - another_gem: '>= 1.0.9' 149 | - yet_another_gem: ['>= 1.0.9', '< 2'] 150 | - some_ruby_library 151 | 152 | * **script:** Arbitrary logic to evaluate while processing this file. 153 | 154 | * **before:** Array of Ruby code snippets to evaluate before processing 155 | the overall configuration. 156 | 157 | * **after:** Array of Ruby code snippets to evaluate after processing the 158 | overall configuration. 159 | 160 | * **status:** Status bar applet definitions. 161 | 162 | All Ruby code snippets that are evaluated inside a `Wmiirc::Status` 163 | object have access to a `refresh` method that triggers redrawing of 164 | the label of that status bar applet. They also have access to a `@id` 165 | variable which is a sequence number counting the number of instances of 166 | this particular status bar applet that have been created thus far. 167 | 168 | * **_name of the status bar applet that you want to define_:** 169 | 170 | * **params:** Hash of parameters to pass to the constructor. These 171 | are later available as instance variables in the Ruby code 172 | snippets that are evaluated inside this status bar applet. 173 | 174 | * **refresh:** Number of seconds to wait before updating the label. 175 | To disable automatic refreshing, set this parameter to 0 (zero). 176 | 177 | * **script:** Ruby code to evaluate in the `Wmiirc::Status` object. 178 | 179 | * **label:** Ruby code whose result is displayed as the content. 180 | This code is placed in a `label()` method in the `Wmiirc::Status` 181 | object. 182 | 183 | * **control:** 184 | 185 | * **event:** Hash of event name to Ruby code to evaluate in the 186 | `Wmiirc::Status` object. 187 | 188 | * **action:** Hash of action name to Ruby code to evaluate in 189 | the `Wmiirc::Status` object. 190 | 191 | * **mouse_action:** Hash of mouse event name to action name. 192 | 193 | * **display:** Appearance settings. 194 | 195 | * **bar:** Where to display the horizontal status bar? 196 | 197 | * **font:** Font to use in all text drawn by wmii. 198 | 199 | * **border:** Thickness of client border (measured in pixels). 200 | 201 | * **color:** Color schemes for everything drawn by wmii. These are 202 | expressed in `#foreground #background #border` format, where 203 | *foreground*, *background*, and *border* are 6-digit HEX values. 204 | 205 | * **desktop:** Color of the desktop background (single color only). 206 | 207 | * **focus:** Colors of things that have focus. 208 | 209 | * **normal:** Colors of things that do not have focus. 210 | 211 | * **columns:** Settings for columns drawn by wmii. 212 | 213 | * **mode:** The wmii "colmode" setting. 214 | 215 | * **rule:** The wmii "colrules" setting. 216 | 217 | * **client:** Settings for clients handled by wmii. See the documentation 218 | for the underlying wmii "rules" setting for more information. 219 | 220 | * **_rule to apply_:** Array of strings that represent regular 221 | expressions to match against a string containing a newly created 222 | client's WM_CLASS and WM_NAME attributes separated by a colon (:). 223 | 224 | * **refresh:** Refresh rate for status bar applets (measured in seconds). 225 | 226 | * **status:** Status bar applet instances. 227 | 228 | All Ruby code snippets that are evaluated inside a `Wmiirc::Status` 229 | object have access to a `refresh` method that triggers redrawing of 230 | the label of that status bar applet. They also have access to a `@id` 231 | variable which is a sequence number counting the number of instances 232 | of this particular status bar applet that have been created thus far. 233 | 234 | * **- _name of the status bar applet that you want to instantiate_:** 235 | 236 | * **params:** Hash of parameters to pass to the constructor. These 237 | are later available as instance variables in the Ruby code 238 | snippets that are evaluated inside this status bar applet. 239 | 240 | * **refresh:** Number of seconds to wait before updating the label. 241 | To disable automatic refreshing, set this parameter to 0 (zero). 242 | 243 | * **script:** Ruby code to evaluate in the `Wmiirc::Status` object. 244 | 245 | * **label:** Ruby code whose result is displayed as the content. 246 | This code is placed in a `label()` method in the `Wmiirc::Status` 247 | object. 248 | 249 | * **control:** 250 | 251 | * **event:** Hash of event name to Ruby code to evaluate in the 252 | `Wmiirc::Status` object. 253 | 254 | * **action:** Hash of action name to Ruby code to evaluate in 255 | the `Wmiirc::Status` object. 256 | 257 | * **mouse_action:** Hash of mouse event name to action name. 258 | 259 | * **control:** Interaction settings. 260 | 261 | * **action:** Hash of action name to Ruby code to evaluate. 262 | 263 | * **event:** Hash of event name to Ruby code to evaluate. 264 | 265 | The Ruby code has access to an "argv" variable which is a list of 266 | arguments that were passed to the event. 267 | 268 | Keep in mind that these event handlers *block* the wmiirc event loop. 269 | In other words, no new events can be handled until the current one 270 | finishes. So try to keep your event handlers short and quick. 271 | 272 | If your event handler needs to perform a long-running operation, then be 273 | sure to wrap that operation inside a Ruby thread. 274 | 275 | * **mouse:** Mapping from X mouse codes to event names. 276 | 277 | * **grab:** The wmii "grabmod" setting. 278 | 279 | * **keyboard:** Hash of shortcut prefix name to shortcut key sequence. 280 | 281 | * **keyboard_action:** Hash of shortcut key sequence to action name. 282 | 283 | A key sequence may contain `${...}` expressions which are replaced 284 | with the value corresponding to `...` in the "control:keyboard" 285 | section of this configuration. 286 | 287 | For example, if the "control:keyboard" section was defined as follows, 288 | then the `${d},${c}` key sequence would be expanded into `Mod4-y,y`. 289 | 290 | control: 291 | keyboard: 292 | a: 4 293 | b: Mod${a} 294 | c: y 295 | d: ${b}-${c} 296 | 297 | ------------------------------------------------------------------------------ 298 | Running 299 | ------------------------------------------------------------------------------ 300 | 301 | * Ensure that your `~/.xinitrc` allows you to restart wmii without having to 302 | lose your running applications if wmii crashes or is accidentally killed: 303 | 304 | xterm -e tail -f ~/.wmii/wmiirc.log & 305 | while true; do wmii 306 | xmessage 'INSERT COIN TO CONTINUE' \ 307 | -buttons 'Insert Coin:0,Game Over' \ 308 | -default 'Insert Coin' -timeout 30 \ 309 | -center || break 310 | done 311 | 312 | For a working example, see [my personal configuration file]( 313 | https://github.com/sunaku/home/blob/master/.xinitrc ). 314 | 315 | * Run `startx` and wmii will automatically find and load this configuration. 316 | 317 | ------------------------------------------------------------------------------ 318 | Upgrading 319 | ------------------------------------------------------------------------------ 320 | 321 | cd ~/.wmii 322 | make rebase 323 | 324 | If this fails because Git reports that you have unstaged changes, you can 325 | stash your changes away temporarily and restore them after the upgrade: 326 | 327 | git stash 328 | make rebase # now it works 329 | git stash apply 330 | 331 | ------------------------------------------------------------------------------ 332 | Hacking 333 | ------------------------------------------------------------------------------ 334 | 335 | To use the development version of [Rumai] directly from its source code 336 | repository (instead of the currently published gem version), run this: 337 | 338 | cd ~/.wmii 339 | make rumai 340 | 341 | ------------------------------------------------------------------------------ 342 | Contributing 343 | ------------------------------------------------------------------------------ 344 | 345 | Fork this project on GitHub and send pull requests. 346 | 347 | ------------------------------------------------------------------------------ 348 | Bugs, Features, Issues, Questions 349 | ------------------------------------------------------------------------------ 350 | 351 | File a report on [the issue tracker](http://github.com/sunaku/wmiirc/issues/). 352 | 353 | ------------------------------------------------------------------------------ 354 | License 355 | ------------------------------------------------------------------------------ 356 | 357 | [Spare A Life]: https://sunaku.github.io/vegan-for-life.html 358 | > Like my work? :+1: Please [spare a life] today as thanks! 359 | :cow::pig::chicken::fish::speak_no_evil::v::revolving_hearts: 360 | 361 | Copyright 2006 Suraj N. Kurapati 362 | 363 | Released under the ISC license. See the LICENSE file for details. 364 | -------------------------------------------------------------------------------- /control/action.yaml: -------------------------------------------------------------------------------- 1 | control: 2 | action: 3 | 4 | action_menu: | 5 | if choice = key_menu(actions, 'action:', :action) 6 | action choice 7 | end 8 | 9 | reload: | 10 | # reload this wmii configuration 11 | Wmiirc::Loader.reload 12 | 13 | # restart the system tray applet 14 | spawn 'witray' 15 | 16 | clear: | 17 | system 'killall', 'witray', 18 | # the "restore session" feature in these browsers does 19 | # not work unless their entire process group is killed 20 | 'opera', 21 | 'firefox', 'firefox-bin', 22 | 'chromium', 'chromium-browser', 23 | 'thunderbird', 'thunderbird-bin' 24 | 25 | clients.each {|c| c.kill rescue nil } 26 | 27 | kill: | 28 | # kill the window manager only; do not touch the clients! 29 | fs.ctl.write 'quit' 30 | 31 | quit: | 32 | # kill both clients and window manager 33 | action 'clear' 34 | action 'kill' 35 | -------------------------------------------------------------------------------- /control/action/client.yaml: -------------------------------------------------------------------------------- 1 | script: 2 | before: 3 | - | 4 | # Do not allow witray to be selected by any grouping commands. 5 | module Rumai::ClientContainer 6 | alias _e7ce5cfa_da6d_467f_837b_e9e738c0ee1b clients 7 | def clients 8 | _e7ce5cfa_da6d_467f_837b_e9e738c0ee1b.reject do |client| 9 | 'witray:witray:witray' == client.props.read rescue nil 10 | end 11 | end 12 | end 13 | 14 | control: 15 | action: 16 | 17 | client_fullscreen!: | 18 | # wmii doesn't properly restore a client to its pre-fullscreen position 19 | # so we keep track of that information and perform correct restoration 20 | # https://github.com/sunaku/wmii/issues/3 21 | @client_fullscreen_restore_info ||= {} 22 | client = curr_client 23 | if client.fullscreen? 24 | client.unfullscreen 25 | if restore_info = @client_fullscreen_restore_info.delete(client.id) 26 | is_top_left = restore_info[:is_top] and restore_info[:is_left] 27 | neighbor_id = restore_info[is_top_left ? :next_id : :prev_id] 28 | if (neighbor = Client.new(neighbor_id)).exist? 29 | unless restore_info[:is_top] and restore_info[:is_right] 30 | client.send :left rescue nil 31 | neighbor.focus 32 | neighbor.area.insert client 33 | end 34 | if is_top_left 35 | client.send :left rescue nil 36 | client.send :up rescue nil 37 | end 38 | if restore_info[:is_top] and not restore_info[:is_left] 39 | if client.area.next.floating? 40 | client.send :right rescue nil 41 | else 42 | client.area.next.unshift client 43 | end 44 | end 45 | if restore_info[:is_alone] 46 | lengths = curr_view.columns.flat_map do |column| 47 | if column == client.area 48 | [1, column.length-1] 49 | else 50 | column.length 51 | end 52 | end 53 | curr_view.arrange_columns lengths 54 | end 55 | end 56 | end 57 | else 58 | @client_fullscreen_restore_info[client.id] = { 59 | prev_id: client.prev.id, 60 | next_id: client.next.id, 61 | is_left: client.area.id == 1, 62 | is_right: client.area.next.floating?, 63 | is_top: client.area.client_ids.first == client.id, 64 | is_alone: client.area.client_ids.length == 1, 65 | } unless client.float? 66 | client.fullscreen 67 | end 68 | 69 | client_close: curr_client.kill 70 | 71 | client_kill: curr_client.slay 72 | 73 | # Show a list of clients, and if user selects one, 74 | # tag it with the same tag as the current view. 75 | client_invite_menu: | 76 | if client = client_menu('invite client:', :client) 77 | client.tag curr_tag 78 | end 79 | -------------------------------------------------------------------------------- /control/action/detach.yaml: -------------------------------------------------------------------------------- 1 | script: 2 | before: 3 | - DETACHED_TAG = '?' 4 | 5 | control: 6 | action: 7 | 8 | detach_group: | 9 | grouping.each do |c| 10 | c.tag DETACHED_TAG 11 | c.untag curr_tag 12 | end 13 | 14 | attach_client: | 15 | v = View.new(DETACHED_TAG) 16 | 17 | if v.exist? and c = v.clients.reject(&:stick?).last 18 | c.tag curr_tag 19 | c.untag DETACHED_TAG 20 | end 21 | -------------------------------------------------------------------------------- /control/action/focus.yaml: -------------------------------------------------------------------------------- 1 | script: 2 | before: 3 | - | 4 | # remember most recently focused view whenever view focus changes 5 | recent_view_ids = [SESSION[:recent_view_id]] 6 | event 'FocusTag' do |view_id| 7 | SESSION[:recent_view_id] = recent_view_ids.shift 8 | recent_view_ids << view_id 9 | end 10 | 11 | # remember most recently focused client whenever client focus changes 12 | recent_client_ids = [SESSION[:recent_client_id]] 13 | event 'ClientFocus' do |client_id| 14 | SESSION[:recent_client_id] = recent_client_ids.shift 15 | recent_client_ids << client_id 16 | end 17 | 18 | control: 19 | action: 20 | 21 | focus_client_menu: | 22 | if client = client_menu('show client:', :client) 23 | client.focus 24 | end 25 | 26 | focus_client_up: curr_view.select(:up) rescue nil 27 | focus_client_down: curr_view.select(:down) rescue nil 28 | focus_client_left: curr_view.select(:left) rescue nil 29 | focus_client_right: curr_view.select(:right) rescue nil 30 | focus_client_recent: | 31 | before = curr_client 32 | target = SESSION[:recent_client_id] 33 | begin 34 | Client.focus(target, curr_view) # focus target in current view 35 | Client.focus(target) if before.focus? # not found; search all views 36 | next_client.focus(curr_view) if before.focus? # fallback to neighbor 37 | rescue 38 | # ignore 39 | end 40 | 41 | focus_client_view_next: | 42 | c = curr_client 43 | v = next_view 44 | v = v.next until v.client_ids.include? c.id 45 | v.focus 46 | c.focus v 47 | 48 | focus_client_view_previous: | 49 | c = curr_client 50 | v = prev_view 51 | v = v.prev until v.client_ids.include? c.id 52 | v.focus 53 | c.focus v 54 | 55 | focus_floating!: curr_view.select(:toggle) 56 | 57 | focus_view_menu: | 58 | if choice = key_menu(tags, 'show view:', :tag) 59 | focus_view choice 60 | end 61 | 62 | focus_view_previous: prev_view.focus 63 | focus_view_next: next_view.focus 64 | focus_view_recent: View.focus(SESSION[:recent_view_id]) rescue nil 65 | 66 | focus_view_1: focus_view tags[0] || 1 67 | focus_view_2: focus_view tags[1] || 2 68 | focus_view_3: focus_view tags[2] || 3 69 | focus_view_4: focus_view tags[3] || 4 70 | focus_view_5: focus_view tags[4] || 5 71 | focus_view_6: focus_view tags[5] || 6 72 | focus_view_7: focus_view tags[6] || 7 73 | focus_view_8: focus_view tags[7] || 8 74 | focus_view_9: focus_view tags[8] || 9 75 | focus_view_0: focus_view tags[9] || 10 76 | 77 | focus_view_a: t = tags.grep(/^a/i).first and focus_view(t) 78 | focus_view_b: t = tags.grep(/^b/i).first and focus_view(t) 79 | focus_view_c: t = tags.grep(/^c/i).first and focus_view(t) 80 | focus_view_d: t = tags.grep(/^d/i).first and focus_view(t) 81 | focus_view_e: t = tags.grep(/^e/i).first and focus_view(t) 82 | focus_view_f: t = tags.grep(/^f/i).first and focus_view(t) 83 | focus_view_g: t = tags.grep(/^g/i).first and focus_view(t) 84 | focus_view_h: t = tags.grep(/^h/i).first and focus_view(t) 85 | focus_view_i: t = tags.grep(/^i/i).first and focus_view(t) 86 | focus_view_j: t = tags.grep(/^j/i).first and focus_view(t) 87 | focus_view_k: t = tags.grep(/^k/i).first and focus_view(t) 88 | focus_view_l: t = tags.grep(/^l/i).first and focus_view(t) 89 | focus_view_m: t = tags.grep(/^m/i).first and focus_view(t) 90 | focus_view_n: t = tags.grep(/^n/i).first and focus_view(t) 91 | focus_view_o: t = tags.grep(/^o/i).first and focus_view(t) 92 | focus_view_p: t = tags.grep(/^p/i).first and focus_view(t) 93 | focus_view_q: t = tags.grep(/^q/i).first and focus_view(t) 94 | focus_view_r: t = tags.grep(/^r/i).first and focus_view(t) 95 | focus_view_s: t = tags.grep(/^s/i).first and focus_view(t) 96 | focus_view_t: t = tags.grep(/^t/i).first and focus_view(t) 97 | focus_view_u: t = tags.grep(/^u/i).first and focus_view(t) 98 | focus_view_v: t = tags.grep(/^v/i).first and focus_view(t) 99 | focus_view_w: t = tags.grep(/^w/i).first and focus_view(t) 100 | focus_view_x: t = tags.grep(/^x/i).first and focus_view(t) 101 | focus_view_y: t = tags.grep(/^y/i).first and focus_view(t) 102 | focus_view_z: t = tags.grep(/^z/i).first and focus_view(t) 103 | -------------------------------------------------------------------------------- /control/action/group.yaml: -------------------------------------------------------------------------------- 1 | control: 2 | action: 3 | 4 | group_client!: | 5 | curr_client.group! 6 | 7 | group_area!: | 8 | curr_area.group! 9 | 10 | group_floating!: | 11 | Area.floating.group! 12 | 13 | group_managed_areas!: | 14 | curr_view.managed_areas.each {|a| a.group! } 15 | 16 | group_view!: | 17 | curr_view.group! 18 | 19 | group_all!: | 20 | Rumai.group! 21 | 22 | ungroup_all: | 23 | Rumai.ungroup 24 | 25 | group_close: | 26 | grouping.each {|c| c.kill } 27 | 28 | group_kill: | 29 | grouping.each {|c| c.slay } 30 | -------------------------------------------------------------------------------- /control/action/launch.yaml: -------------------------------------------------------------------------------- 1 | script: 2 | after: 3 | - action 'launch_menu_rehash' 4 | 5 | control: 6 | action: 7 | 8 | launch_menu_rehash: | 9 | $launch_menu = ENV['PATH'].to_s.split(/:+/).map do |d| 10 | Dir.exist? d and Dir.entries(d).select do |f| 11 | entry = File.stat(File.join(d, f)) rescue nil 12 | entry and entry.file? and entry.executable? 13 | end 14 | end.compact.flatten.uniq 15 | 16 | launch_menu: | 17 | if choice = key_menu($launch_menu, 'launch:', :program) 18 | launch choice 19 | end 20 | 21 | -------------------------------------------------------------------------------- /control/action/layout.yaml: -------------------------------------------------------------------------------- 1 | control: 2 | action: 3 | 4 | layout_column_equal: | 5 | curr_area.layout = :default 6 | 7 | layout_view_equal: | 8 | curr_view.columns.each do |a| 9 | a.layout = :default 10 | end 11 | 12 | layout_column_stack: | 13 | curr_area.layout = :stack 14 | 15 | layout_view_stack: | 16 | curr_view.columns.each do |a| 17 | a.layout = :stack 18 | end 19 | 20 | layout_column_max: | 21 | curr_area.layout = :max 22 | 23 | layout_view_max: | 24 | curr_view.columns.each do |a| 25 | a.layout = :max 26 | end 27 | -------------------------------------------------------------------------------- /control/action/resize.yaml: -------------------------------------------------------------------------------- 1 | control: 2 | action: 3 | 4 | resize_client_up: | 5 | begin 6 | c = curr_client 7 | if !c.float? && c == c.area.clients.first 8 | c.shrink :down, 5 9 | else 10 | c.grow :up, 5 11 | end 12 | rescue 13 | end 14 | 15 | resize_client_down: | 16 | begin 17 | c = curr_client 18 | if !c.float? && c == c.area.clients.last 19 | c.shrink :up, 5 20 | else 21 | c.grow :down, 5 22 | end 23 | rescue 24 | end 25 | 26 | resize_client_left: | 27 | begin 28 | c = curr_client 29 | if !c.float? && (a = c.area) == a.view.columns.first 30 | c.shrink :right, 5 31 | else 32 | c.grow :left, 5 33 | end 34 | rescue 35 | end 36 | 37 | resize_client_right: | 38 | begin 39 | c = curr_client 40 | if !c.float? && (a = c.area) == a.view.columns.last 41 | c.shrink :left, 5 42 | else 43 | c.grow :right, 5 44 | end 45 | rescue 46 | end 47 | 48 | grow_client_up: | 49 | curr_client.grow(:up, 5) rescue nil 50 | 51 | grow_client_down: | 52 | curr_client.grow(:down, 5) rescue nil 53 | 54 | grow_client_left: | 55 | curr_client.grow(:left, 5) rescue nil 56 | 57 | grow_client_right: | 58 | curr_client.grow(:right, 5) rescue nil 59 | 60 | shrink_client_up: | 61 | curr_client.shrink(:down, 5) rescue nil 62 | 63 | shrink_client_down: | 64 | curr_client.shrink(:up, 5) rescue nil 65 | 66 | shrink_client_left: | 67 | curr_client.shrink(:right, 5) rescue nil 68 | 69 | shrink_client_right: | 70 | curr_client.shrink(:left, 5) rescue nil 71 | -------------------------------------------------------------------------------- /control/action/send.yaml: -------------------------------------------------------------------------------- 1 | control: 2 | action: 3 | 4 | send_group_up: | 5 | grouping.each {|c| c.send(:up) rescue nil } 6 | 7 | send_group_down: | 8 | grouping.each {|c| c.send(:down) rescue nil } 9 | 10 | send_group_left: | 11 | grouping.each {|c| c.send(:left) rescue nil } 12 | 13 | send_group_right: | 14 | grouping.each {|c| c.send(:right) rescue nil } 15 | 16 | send_group_floating!: | 17 | grouping.each {|c| c.send(:toggle) rescue nil } 18 | 19 | send_group_view_menu: | 20 | # 21 | # Changes the tag (according to a menu choice) of 22 | # each grouped client and returns the chosen tag. 23 | # 24 | # The +tag -tag idea is from Jonas Pfenniger: 25 | # 26 | # http://zimbatm.oree.ch/articles/2006/06/15/wmii-3-and-ruby 27 | # 28 | choices = tags.map {|t| [t, "+#{t}", "-#{t}"] }.flatten 29 | 30 | if target = key_menu(choices, 'tag as:', :tag) 31 | grouping.each do |c| 32 | case target 33 | when /^\+/ then c.tag $' 34 | when /^\-/ then c.untag $' 35 | else c.tags = target 36 | end 37 | end 38 | end 39 | 40 | send_group_view_1: grouping.each {|c| c.tags = tags[0] || 1 } 41 | send_group_view_2: grouping.each {|c| c.tags = tags[1] || 2 } 42 | send_group_view_3: grouping.each {|c| c.tags = tags[2] || 3 } 43 | send_group_view_4: grouping.each {|c| c.tags = tags[3] || 4 } 44 | send_group_view_5: grouping.each {|c| c.tags = tags[4] || 5 } 45 | send_group_view_6: grouping.each {|c| c.tags = tags[5] || 6 } 46 | send_group_view_7: grouping.each {|c| c.tags = tags[6] || 7 } 47 | send_group_view_8: grouping.each {|c| c.tags = tags[7] || 8 } 48 | send_group_view_9: grouping.each {|c| c.tags = tags[8] || 9 } 49 | send_group_view_0: grouping.each {|c| c.tags = tags[9] || 10 } 50 | 51 | send_group_away: | 52 | current = curr_tag 53 | grouping.each do |c| 54 | c.untag current 55 | end 56 | -------------------------------------------------------------------------------- /control/action/swap.yaml: -------------------------------------------------------------------------------- 1 | control: 2 | action: 3 | 4 | swap_client_up: curr_client.swap(:up) rescue nil 5 | swap_client_down: curr_client.swap(:down) rescue nil 6 | swap_client_left: curr_client.swap(:left) rescue nil 7 | swap_client_right: curr_client.swap(:right) rescue nil 8 | 9 | swap_client_master: | 10 | unless defined? $swap_client_master 11 | $swap_client_master = Hash.new(2) 12 | end 13 | begin 14 | c = curr_client 15 | a = c.area.id 16 | t = curr_tag 17 | if a == 1 18 | c.swap $swap_client_master[t] 19 | else 20 | $swap_client_master[t] = a 21 | c.swap 1 22 | end 23 | rescue 24 | end 25 | 26 | # swap current client with the selected one in column 27 | swap_client_column_1: curr_client.swap 1 28 | swap_client_column_2: curr_client.swap 2 29 | swap_client_column_3: curr_client.swap 3 30 | swap_client_column_4: curr_client.swap 4 31 | swap_client_column_5: curr_client.swap 5 32 | swap_client_column_6: curr_client.swap 6 33 | swap_client_column_7: curr_client.swap 7 34 | swap_client_column_8: curr_client.swap 8 35 | swap_client_column_9: curr_client.swap 9 36 | swap_client_column_0: curr_client.swap 10 37 | -------------------------------------------------------------------------------- /control/action/zoom.yaml: -------------------------------------------------------------------------------- 1 | require: 2 | - securerandom 3 | 4 | script: 5 | before: 6 | - ZOOMED_SUFFIX = /!\d+\.[[:xdigit:]]+$/ 7 | 8 | control: 9 | action: 10 | 11 | zoom_group: | 12 | orig_clients = grouping 13 | 14 | unless orig_clients.empty? 15 | curr_client_before_zoom = curr_client 16 | 17 | # calculate tag for temporary view 18 | orig_tag = curr_tag 19 | if orig_tag =~ ZOOMED_SUFFIX 20 | orig_tag = $` 21 | if curr_view.clients.length == 1 22 | action 'unzoom_group' 23 | return 24 | end 25 | end 26 | 27 | num = tags.grep(ZOOMED_SUFFIX).length 28 | hex = SecureRandom.hex(2) 29 | temp_tag = "#{orig_tag}!#{num}.#{hex}" 30 | 31 | # travel to temporary view 32 | temp_view = View.new(temp_tag) 33 | temp_view.focus 34 | 35 | # add chosen clients to temporary view 36 | orig_clients.each {|c| c.tag temp_tag } 37 | 38 | # restore focus on "current" client 39 | curr_client_before_zoom.focus temp_view 40 | end 41 | 42 | unzoom_group: | 43 | temp_clients = grouping 44 | 45 | unless temp_clients.empty? 46 | curr_client_before_zoom = curr_client 47 | temp_tag = curr_tag 48 | 49 | if temp_tag =~ ZOOMED_SUFFIX 50 | orig_tag = $` 51 | 52 | # travel to original view 53 | orig_view = View.new(orig_tag) 54 | orig_view.focus 55 | 56 | # restore clients to original view 57 | temp_clients.each do |c| 58 | c.tag orig_tag 59 | c.untag temp_tag 60 | end 61 | 62 | # restore focus on "current" client 63 | curr_client_before_zoom.focus orig_view 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /control/keyboard.yaml: -------------------------------------------------------------------------------- 1 | control: 2 | action: 3 | keyboard: | # re-register shortcuts under new keyboard layout 4 | Rumai.fs.keys.tap {|f| f.write f.read } 5 | 6 | keyboard_shortcuts_help: | # show a list of keyboard shortcuts 7 | shortcuts, actions = Wmiirc::CONFIG.shortcuts.to_a.sort.transpose 8 | shortcuts_width = shortcuts.map(&:length).max 9 | mappings = shortcuts.map {|s| s.ljust(shortcuts_width) }.zip(actions) 10 | listing = mappings.map {|*mapping| mapping.join(' : ') }.join("\n") 11 | dialog listing 12 | -------------------------------------------------------------------------------- /control/mouse.yaml: -------------------------------------------------------------------------------- 1 | control: 2 | mouse: 3 | 1: left_click 4 | 2: wheel_click 5 | 3: right_click 6 | 4: wheel_up 7 | 5: wheel_down 8 | 6: wheel_left 9 | 7: wheel_right 10 | 11 | script: 12 | before: 13 | - | 14 | module Mouse 15 | NAME_BY_CODE = CONFIG['control']['mouse'] 16 | 17 | ## 18 | # Converts the given mouse code into the event 19 | # name defined in the "control:mouse" section. 20 | # 21 | def self.[] code 22 | NAME_BY_CODE[code.to_i] || code 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /display/barlet.yaml: -------------------------------------------------------------------------------- 1 | script: 2 | before: 3 | - | 4 | class Barlet < Rumai::Barlet 5 | def initialize *args 6 | super 7 | 8 | unless exist? 9 | create 10 | self.colors = 'normal' 11 | end 12 | end 13 | 14 | def colors= key 15 | super CONFIG['display']['color'][key] || key 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /display/client.yaml: -------------------------------------------------------------------------------- 1 | import: 2 | - control/mouse.yaml 3 | 4 | control: 5 | event: 6 | ClientMouseDown: | 7 | client_id, mouse_code = argv 8 | mouse_event = Mouse[mouse_code] 9 | 10 | if mouse_event == 'right_click' 11 | client = Client.new(client_id) 12 | 13 | case click_menu %w[fullscreen sticky group close kill], 'client' 14 | when 'fullscreen' then client.fullscreen! 15 | when 'sticky' then client.stick! 16 | when 'group' then client.group! 17 | when 'close' then client.kill 18 | when 'kill' then client.slay 19 | end 20 | end 21 | 22 | Unresponsive: | 23 | client = Client.new(argv[0]) 24 | 25 | IO.popen('xmessage -nearmouse -file - -buttons Kill,Wait -print', 'w+') do |f| 26 | f.puts 'The following client is not responding.', '' 27 | f.puts client.inspect 28 | f.puts client.label.read 29 | 30 | f.puts '', 'What would you like to do?' 31 | f.close_write 32 | 33 | if f.read.chomp == 'Kill' 34 | client.slay 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /display/color.yaml: -------------------------------------------------------------------------------- 1 | # Common UI interaction cues from Blueprint CSS: 2 | # http://blueprintcss.org/tests/parts/forms.html 3 | display: 4 | color: 5 | info: "#205791 #D5EDF8 #92CAE4" 6 | error: "#8A1F11 #FBE3E4 #FBC2C4" 7 | notice: "#514721 #FFF6BF #FFD324" 8 | success: "#264409 #E6EFC2 #C6D880" 9 | -------------------------------------------------------------------------------- /display/color/cadmium.yaml: -------------------------------------------------------------------------------- 1 | # Colors from the xfce-cadmium GTK+ theme. 2 | display: 3 | color: 4 | desktop: "#eeeeef" 5 | focus: "#2A2A4A #CECEDE #5A5A8A" 6 | normal: "#3A3A6A #EEEEEF #DADAEA" 7 | error: "#EF0020 #FBD3E4 #FF0020" 8 | notice: "#514721 #FFF6BF #FFD324" 9 | success: "#264409 #E6EFE2 #B6D870" 10 | -------------------------------------------------------------------------------- /display/color/chrome-gtk.yaml: -------------------------------------------------------------------------------- 1 | # Colors from the GTK+ theme for Google Chrome. 2 | display: 3 | color: 4 | desktop: "#48647E" 5 | focus: "#000000 #DCDAD5 #707A83" 6 | normal: "#000000 #A2B0BD #919DA9" 7 | -------------------------------------------------------------------------------- /display/color/chrome.yaml: -------------------------------------------------------------------------------- 1 | # Colors from the Classic theme for Google Chrome. 2 | display: 3 | color: 4 | desktop: "#34649E" 5 | focus: "#000000 #FCFDFF #6A85A7" 6 | normal: "#000000 #A0C5F3 #89ACD7" 7 | -------------------------------------------------------------------------------- /display/color/github.yaml: -------------------------------------------------------------------------------- 1 | # Colors from GitHub.com 2 | display: 3 | color: 4 | desktop: "#EAF2F5" 5 | focus: "#FFFFFF #447DBB #5C8AB3" 6 | normal: "#444444 #F8F8F8 #E9E9E9" 7 | -------------------------------------------------------------------------------- /display/color/gruvbox.yaml: -------------------------------------------------------------------------------- 1 | # Colors from Gruvbox color scheme: 2 | # https://github.com/morhetz/gruvbox 3 | # 4 | # GruvboxFg0 xxx ctermfg=229 guifg=#fdf4c1 5 | # GruvboxFg1 xxx ctermfg=223 guifg=#ebdbb2 6 | # GruvboxFg2 xxx ctermfg=250 guifg=#d5c4a1 7 | # GruvboxFg3 xxx ctermfg=248 guifg=#bdae93 8 | # GruvboxFg4 xxx ctermfg=246 guifg=#a89984 9 | # GruvboxGray xxx ctermfg=245 guifg=#928374 10 | # GruvboxBg0 xxx ctermfg=236 guifg=#32302f 11 | # GruvboxBg1 xxx ctermfg=237 guifg=#3c3836 12 | # GruvboxBg2 xxx ctermfg=239 guifg=#504945 13 | # GruvboxBg3 xxx ctermfg=241 guifg=#665c54 14 | # GruvboxBg4 xxx ctermfg=243 guifg=#7c6f64 15 | # GruvboxRed xxx ctermfg=167 guifg=#fb4934 16 | # GruvboxGreen xxx ctermfg=142 guifg=#b8bb26 17 | # GruvboxYellow xxx ctermfg=214 guifg=#fabd2f 18 | # GruvboxBlue xxx ctermfg=109 guifg=#83a598 19 | # GruvboxPurple xxx ctermfg=175 guifg=#d3869b 20 | # GruvboxAqua xxx ctermfg=108 guifg=#8ec07c 21 | # GruvboxOrange xxx ctermfg=208 guifg=#fe8019 22 | # GruvboxRedSign xxx ctermfg=167 ctermbg=243 guifg=#fb4934 guibg=#7c6f64 23 | # GruvboxGreenSign xxx ctermfg=142 ctermbg=243 guifg=#b8bb26 guibg=#7c6f64 24 | # GruvboxYellowSign xxx ctermfg=214 ctermbg=243 guifg=#fabd2f guibg=#7c6f64 25 | # GruvboxBlueSign xxx ctermfg=109 ctermbg=243 guifg=#83a598 guibg=#7c6f64 26 | # GruvboxPurpleSign xxx ctermfg=175 ctermbg=243 guifg=#d3869b guibg=#7c6f64 27 | # GruvboxAquaSign xxx ctermfg=108 ctermbg=243 guifg=#8ec07c guibg=#7c6f64 28 | # 29 | display: 30 | color: 31 | desktop: "#3c3836" 32 | normal: "#d5c4a1 #504945 #3c3836" 33 | focus: "#fabd2f #3c3836 #32302f" 34 | info: "#3c3836 #ebdbb2 #7c6f64" 35 | error: "#fdf4c1 #fb4934 #7c6f64" 36 | notice: "#3c3836 #fabd2f #7c6f64" 37 | success: "#3c3836 #8ec07c #7c6f64" 38 | -------------------------------------------------------------------------------- /display/color/lucius.yaml: -------------------------------------------------------------------------------- 1 | # Colors from the "Lucius" color scheme for Vim: 2 | # http://www.vim.org/scripts/script.php?script_id=2536 3 | display: 4 | color: 5 | desktop: "#363946" # LineNr 6 | focus: "#cae682 #505860 #303840" # PmenuSel 7 | normal: "#e0e0e0 #202020 #202438" # Normal 8 | -------------------------------------------------------------------------------- /display/color/maglione.yaml: -------------------------------------------------------------------------------- 1 | # Kris Maglione's color scheme, 2 | # extracted from cmd/rc/wmii.sh 3 | display: 4 | color: 5 | desktop: "#333333" 6 | normal: "#000000 #c1c48b #81654f" 7 | focus: "#000000 #81654f #000000" 8 | -------------------------------------------------------------------------------- /display/color/wmii.yaml: -------------------------------------------------------------------------------- 1 | # wmii's default color scheme, 2 | # extracted from cmd/wmii/dat.h 3 | display: 4 | color: 5 | desktop: "#333333" 6 | focus: "#ffffff #335577 #447799" 7 | normal: "#222222 #eeeeee #666666" 8 | -------------------------------------------------------------------------------- /display/color/wombat.yaml: -------------------------------------------------------------------------------- 1 | # Colors taken from the Wombat color scheme for Vim: 2 | # http://dengmao.wordpress.com/2007/01/22/vim-color-scheme-wombat/ 3 | # 4 | # And also from this Xresources adaptation thereof: 5 | # https://bitbucket.org/redrampage/dotfiles/src/38fb09ee9afb/.Xdefaults 6 | display: 7 | color: 8 | desktop: "#191919" 9 | normal: "#dddddd #343434 #444444" 10 | focus: "#343434 #a1cdcd #444444" 11 | -------------------------------------------------------------------------------- /display/color/zenburn.yaml: -------------------------------------------------------------------------------- 1 | # Colors from the zenburn color scheme for Vim: 2 | # http://slinky.imukuppi.org/zenburn/ 3 | display: 4 | color: 5 | desktop: "#333333" # VisualNOS 6 | focus: "#000d18 #8faf9f #4f4f4f" # Cursor, Cursor, CursorColumn 7 | normal: "#dcdccc #3f3f3f #434443" # Normal, CursorLine 8 | -------------------------------------------------------------------------------- /display/color/zukitwo-dark.yaml: -------------------------------------------------------------------------------- 1 | # Colors from the Zukitwo-Dark GTK+ theme. 2 | # http://lassekongo83.deviantart.com/art/Zukitwo-203936861 3 | display: 4 | color: 5 | desktop: "#3c3c3c" 6 | normal: "#e6e6e6 #4c4c4c #464646" 7 | focus: "#2c2c2c #c9c9c9 #a2a2a2" 8 | -------------------------------------------------------------------------------- /display/desktop.yaml: -------------------------------------------------------------------------------- 1 | control: 2 | action: 3 | desktop: | 4 | system 'xsetroot', '-solid', CONFIG['display']['color']['desktop'] 5 | 6 | script: 7 | after: 8 | - action 'desktop' 9 | -------------------------------------------------------------------------------- /display/desktop/feh.yaml: -------------------------------------------------------------------------------- 1 | import: 2 | - display/desktop.yaml 3 | 4 | control: 5 | action: 6 | desktop: launch! 'test -f ~/.fehbg && sh ~/.fehbg' 7 | -------------------------------------------------------------------------------- /display/status.yaml: -------------------------------------------------------------------------------- 1 | import: 2 | - control/mouse.yaml 3 | - display/barlet.yaml 4 | 5 | script: 6 | before: 7 | - | 8 | class StatusBarlet < Barlet 9 | def initialize path 10 | super path, :right 11 | end 12 | end 13 | 14 | class StatusBarletSandbox < Sandbox 15 | def initialize name, definition, instance, position, offset 16 | definition_key = "status:#{name}" 17 | instance_key = "display:status:#{name}##{offset}" 18 | 19 | # restore instance variables from previous session 20 | if session = SESSION[instance_key] 21 | session.each do |variable, value| 22 | instance_variable_set variable, value 23 | end 24 | end 25 | 26 | # store instance variables for next session before exiting 27 | at_exit do 28 | SESSION[instance_key] = Hash[ 29 | instance_variables.map do |variable| 30 | [variable, instance_variable_get(variable)] 31 | end 32 | ] 33 | end 34 | 35 | # merge the instance with the standard barlet definition 36 | definition = Wmiirc::Import.merge({}, definition, definition_key) 37 | Wmiirc::Import.merge definition, instance, instance_key, CONFIG.origins 38 | 39 | if params = definition['params'] 40 | params.each do |key, value| 41 | instance_variable_set "@#{key}", value 42 | end 43 | end 44 | 45 | @id = offset 46 | 47 | if code = definition['script'] 48 | instance_eval code.to_s, 49 | CONFIG.origin(code, "#{definition_key}:script") 50 | end 51 | 52 | code = definition['label'] 53 | instance_eval "def label; #{code}; end", 54 | CONFIG.origin(code, "#{definition_key}:label") 55 | 56 | # buttons appear in ASCII order of their IXP file name 57 | # so prepend a number to get ordered status bar applets 58 | file = sprintf('%03d-%s-%d', position, name, offset) 59 | button = StatusBarlet.new(file) 60 | 61 | label_refresh_thread = Thread.new do 62 | refresh_rate = definition['refresh'] || 63 | CONFIG['display']['refresh'] || 60 64 | refresh_rate = nil if refresh_rate <= 0 65 | 66 | colors_hash = CONFIG['display']['color'] 67 | 68 | loop do 69 | button_label = begin 70 | Array(self.label) 71 | rescue Exception => e 72 | LOG.error e 73 | [colors_hash['error'], e] 74 | end 75 | 76 | # provide default color 77 | possible_color = button_label.first.to_s 78 | if colors_hash.key? possible_color 79 | button_label[0] = colors_hash[possible_color] 80 | elsif possible_color !~ /(?:#[[:xdigit:]]{6} ?){3}/ 81 | button_label.unshift colors_hash['normal'] 82 | end 83 | 84 | button.colors = button_label.shift 85 | button.label = button_label.compact.join(' ') 86 | 87 | sleep *refresh_rate 88 | end 89 | end 90 | 91 | singleton_class.class_eval do 92 | define_method :refresh do 93 | label_refresh_thread.wakeup if label_refresh_thread.alive? 94 | end 95 | end 96 | 97 | # register action handlers 98 | if control = definition['control'] 99 | action_by_local_name = {} 100 | 101 | if actions = control['action'] 102 | actions.each do |action_name, code| 103 | origin = CONFIG.origin(code, "#{definition_key}:control:action:#{action_name}") 104 | handler = instance_eval("lambda {#{code}}", origin) 105 | 106 | action_by_local_name[action_name] = handler 107 | 108 | # first instance of this status bar applet gets the honor of 109 | # registering its action handlers under the global namespace 110 | unless Wmiirc.action? action_name 111 | Wmiirc.action action_name, &handler 112 | end 113 | 114 | # the offset suffix makes this action name unique to 115 | # this particular instance of the status bar applet 116 | # when we register the handler in the global namespace 117 | Wmiirc.action "#{action_name}##{offset}", &handler 118 | end 119 | end 120 | 121 | if mouse_actions = control['mouse_action'] 122 | Wmiirc.event 'RightBarClick' do |clicked_button, clicked_file| 123 | if clicked_file == file 124 | if action = mouse_actions[Mouse[clicked_button]] 125 | if handler = action_by_local_name[action] 126 | # the given action is local to this status bar applet 127 | handler.call 128 | else 129 | # the given action is global to the wmii configuration 130 | Wmiirc.action action 131 | end 132 | end 133 | end 134 | end 135 | end 136 | 137 | if events = control['event'] 138 | events.each do |event_name, code| 139 | origin = CONFIG.origin(code, "#{definition_key}:control:event:#{event_name}") 140 | handler = instance_eval("lambda {|*argv| #{code}}", origin) 141 | Wmiirc.event event_name, &handler 142 | end 143 | end 144 | end 145 | end 146 | end 147 | 148 | after: 149 | - | 150 | # populate the status bar with status barlet instances 151 | fs.rbar.clear 152 | count_by_name = Hash.new {|h,k| h[k] = 0 } 153 | 154 | $status_barlets = Array(CONFIG['display']['status']).each_with_index. 155 | map do |hash, position| 156 | raise ArgumentError unless hash.length == 1 157 | name, instance = hash.first 158 | instance ||= {} 159 | 160 | unless definition = CONFIG['status'][name] 161 | raise ArgumentError, "no such status barlet: #{name.inspect}" 162 | end 163 | 164 | offset = count_by_name[name] 165 | count_by_name[name] += 1 166 | 167 | StatusBarletSandbox.new name, definition, instance, position, offset 168 | end 169 | 170 | control: 171 | action: 172 | status_refresh: $status_barlets.each(&:refresh) 173 | -------------------------------------------------------------------------------- /display/tags.yaml: -------------------------------------------------------------------------------- 1 | import: 2 | - control/mouse.yaml 3 | - display/barlet.yaml 4 | 5 | script: 6 | before: 7 | - | 8 | class TagBarlet < Barlet 9 | def initialize tag 10 | super tag, :left 11 | self.label = tag 12 | end 13 | end 14 | 15 | after: 16 | - | 17 | # create buttons for tags in /lbar 18 | fs.lbar.clear 19 | tags.each {|t| event 'CreateTag', t } 20 | event 'FocusTag', curr_tag 21 | 22 | control: 23 | event: 24 | CreateTag: | 25 | TagBarlet.new(argv[0]) 26 | 27 | DestroyTag: | 28 | TagBarlet.new(argv[0]).remove 29 | 30 | FocusTag: | 31 | TagBarlet.new(argv[0]).colors = 'focus' 32 | 33 | UnfocusTag: | 34 | TagBarlet.new(argv[0]).colors = 'normal' 35 | 36 | UrgentTag: | 37 | tag = argv[1] 38 | TagBarlet.new(tag).colors = 'notice' 39 | 40 | NotUrgentTag: | 41 | tag = argv[1] 42 | TagBarlet.new(tag).colors = (curr_view.id == tag) ? 'focus' : 'normal' 43 | 44 | LeftBarClick: &LeftBarClick | 45 | mouse_code, view_id = argv 46 | mouse_event = Mouse[mouse_code] 47 | 48 | case mouse_event 49 | when 'left_click' 50 | focus_view view_id 51 | 52 | when 'wheel_click' 53 | curr_client.tag view_id 54 | 55 | when 'right_click' 56 | curr_client.untag view_id 57 | end 58 | 59 | ## 60 | # allows the user to drag a file over a 61 | # view button and activate that view while 62 | # still holding on to their dragged file! 63 | # 64 | LeftBarDND: *LeftBarClick 65 | -------------------------------------------------------------------------------- /lib/wmiirc.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | require 'rumai' 3 | 4 | module Wmiirc 5 | extend self 6 | 7 | # path to user's wmii configuration directory 8 | DIR = File.dirname(File.dirname(__FILE__)) 9 | 10 | # keep a log file to aid the user in debugging 11 | LOG = Logger.new(File.join(DIR, 'wmiirc.log')) 12 | 13 | # add colors to log messages based on severity 14 | fmt = Logger::Formatter.new 15 | LOG.formatter = proc do |severity, datetime, progname, msg| 16 | color = case severity 17 | when 'DEBUG' then "\e[1;36m" # bold cyan 18 | when 'INFO' then "\e[1;32m" # bold green 19 | when 'WARN' then "\e[1;33m" # bold yellow 20 | when 'ERROR' then "\e[1;31m" # bold red 21 | when 'FATAL' then "\e[1;30m\e[41m" # bold black on red 22 | end 23 | [color, fmt.call(severity, datetime, progname, "\e[0m#{msg}")].join 24 | end 25 | 26 | # ensure that exception stack traces are logged 27 | class << LOG 28 | alias _950b6400_5a0b_4f38_8f1f_bdb9e8c095d3 error 29 | def error e 30 | _950b6400_5a0b_4f38_8f1f_bdb9e8c095d3 [e, e.backtrace].join("\n") 31 | end 32 | end 33 | 34 | # insulation for code in user's configuration 35 | class Sandbox 36 | include Rumai 37 | include Wmiirc 38 | 39 | alias eval instance_eval 40 | end 41 | 42 | SANDBOX = Sandbox.new 43 | end 44 | 45 | require 'wmiirc/import' 46 | require 'wmiirc/handler' 47 | require 'wmiirc/system' 48 | require 'wmiirc/menu' 49 | -------------------------------------------------------------------------------- /lib/wmiirc/config.rb: -------------------------------------------------------------------------------- 1 | require 'wmiirc' 2 | require 'yaml' 3 | 4 | module Wmiirc 5 | class Config < Hash 6 | 7 | attr_reader :origins, :shortcuts 8 | 9 | def initialize file 10 | Import.import self, file, @origins={} 11 | end 12 | 13 | def apply 14 | script 'before' 15 | display 16 | control 17 | script 'after' 18 | end 19 | 20 | ## 21 | # Qualifies the given section name with the YAML file 22 | # from which the given value originated. If this is 23 | # not possible, the given section name is returned. 24 | # 25 | def origin value, section 26 | if origin = @origins[value] 27 | "#{origin}:#{section}" 28 | else 29 | section 30 | end 31 | end 32 | 33 | private 34 | 35 | def script key 36 | if scripts = self['script'] 37 | scripts[key].each do |code| 38 | SANDBOX.eval code.to_s, origin(code, "script:#{key}") 39 | end 40 | end 41 | end 42 | 43 | def display 44 | font = ENV['WMII_FONT'] = self['display']['font'] 45 | focus = ENV['WMII_FOCUSCOLORS'] = self['display']['color']['focus'] 46 | normal = ENV['WMII_NORMCOLORS'] = self['display']['color']['normal'] 47 | 48 | settings = { 49 | 'font' => font, 50 | 'focuscolors' => focus, 51 | 'normcolors' => normal, 52 | 'border' => self['display']['border'], 53 | 'bar on' => self['display']['bar'], 54 | 'colmode' => self['display']['column']['mode'], 55 | 'grabmod' => self['control']['mouse']['grab'], 56 | } 57 | 58 | begin 59 | Rumai.fs.ctl.write settings.map {|pair| pair.join(' ') }.join("\n") 60 | Rumai.fs.colrules.write self['display']['column']['rule'] 61 | Rumai.fs.rules.write self['display']['client']. 62 | flat_map {|rule, regs| regs.map {|e| "/#{e}/ #{rule}" } }.join("\n") 63 | rescue Rumai::IXP::Error => error 64 | # 65 | # settings that are not supported in a particular wmii version 66 | # are ignored, and those that are supported are (silently) 67 | # applied. but a "bad command" error is raised nevertheless! 68 | # 69 | LOG.warn "could not apply some wmii settings: #{error.inspect}" 70 | end 71 | end 72 | 73 | def control 74 | @shortcuts = {} 75 | %w[event action keyboard_action].each do |section| 76 | if settings = self['control'][section] 77 | settings.each do |key, code| 78 | if section == 'keyboard_action' 79 | 80 | # expand symbolic references in the keyboard shortcut 81 | if keyboard = self['control']['keyboard'] 82 | key = key.dup 83 | nil while key.gsub!(/\$\{(\w+)\}/){ keyboard[$1] } 84 | end 85 | 86 | # store expanded shortcut and mapping for help menu 87 | if @shortcuts.key? key 88 | LOG.warn 'overriding shortcut %s to %s (was previously %s)' % 89 | [key, code, @shortcuts[key]].map(&:inspect) 90 | end 91 | @shortcuts[key] = code 92 | 93 | meth = 'key' 94 | code = self['control']['action'][code] || "action #{code.inspect}" 95 | else 96 | name = key 97 | meth = section 98 | end 99 | 100 | SANDBOX.eval( 101 | "#{meth}(#{key.inspect}) {|*argv| #{code} }", 102 | origin(code, "control:#{section}:#{name}") 103 | ) 104 | end 105 | end 106 | end 107 | 108 | # register keyboard shortcuts 109 | SANDBOX.eval do 110 | fs.keys.write keys.join("\n") 111 | event('Key') {|*a| key(*a) } 112 | end 113 | end 114 | 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/wmiirc/handler.rb: -------------------------------------------------------------------------------- 1 | # DSL for wmiirc configuration. 2 | module Wmiirc 3 | 4 | class Handler < Hash 5 | def initialize 6 | super {|h,k| h[k] = [] } 7 | end 8 | 9 | ## 10 | # Registers the given block as a handler for the given key 11 | # and returns that handler, or if a block is not given, 12 | # executes all handlers registered for the given key. 13 | # 14 | def handle key, *args, &block 15 | if block 16 | self[key] << block 17 | 18 | elsif key? key 19 | self[key].each do |block| 20 | begin 21 | block.call(*args) 22 | rescue LocalJumpError => error 23 | # let handlers return early, just like methods 24 | raise unless error.message == 'unexpected return' 25 | end 26 | end 27 | end 28 | 29 | block 30 | end 31 | end 32 | 33 | EVENTS = Handler.new 34 | ACTIONS = Handler.new 35 | KEYS = Handler.new 36 | 37 | ## 38 | # If a block is given, registers a handler for the 39 | # given event and returns the handler. Otherwise, 40 | # executes all handlers for the given event. 41 | # 42 | def event(*a, &b) 43 | EVENTS.handle(*a, &b) 44 | end 45 | 46 | ## 47 | # Checks if a handler for the given event name has been registered. 48 | # 49 | def event? name 50 | EVENTS.key? name 51 | end 52 | 53 | ## 54 | # Returns a list of registered event names. 55 | # 56 | def events 57 | EVENTS.keys 58 | end 59 | 60 | ## 61 | # If a block is given, registers a handler for the 62 | # given action and returns the handler. Otherwise, 63 | # executes all handlers for the given action. 64 | # 65 | def action(*a, &b) 66 | ACTIONS.handle(*a, &b) 67 | end 68 | 69 | ## 70 | # Checks if a handler for the given action name has been registered. 71 | # 72 | def action? name 73 | ACTIONS.key? name 74 | end 75 | 76 | ## 77 | # Returns a list of registered action names. 78 | # 79 | def actions 80 | ACTIONS.keys 81 | end 82 | 83 | ## 84 | # If a block is given, registers a handler 85 | # for the given keypress name and returns 86 | # the handler. Otherwise, executes all 87 | # handlers for the given keypress name. 88 | # 89 | def key(*a, &b) 90 | KEYS.handle(*a, &b) 91 | end 92 | 93 | ## 94 | # Checks if a handler for the given keypress name has been registered. 95 | # 96 | def key? name 97 | KEYS.key? name 98 | end 99 | 100 | ## 101 | # Returns a list of registered keypress names. 102 | # 103 | def keys 104 | KEYS.keys 105 | end 106 | 107 | end 108 | -------------------------------------------------------------------------------- /lib/wmiirc/import.rb: -------------------------------------------------------------------------------- 1 | require 'wmiirc' 2 | require 'yaml' 3 | 4 | module Wmiirc 5 | module Import 6 | extend self 7 | 8 | def import result, paths, origins={}, imported={}, importer=$0 9 | Array(paths).each do |path| 10 | next if imported[path] 11 | imported[path] = true 12 | 13 | begin 14 | data = YAML.load_file(path) 15 | rescue => error 16 | error.message << ' when importing %s into %s' % 17 | [path, importer].map(&:inspect) 18 | raise error 19 | end 20 | 21 | mark_origin data, path, origins 22 | expand result, data, path, origins, imported 23 | merge result, data, path, origins 24 | end 25 | 26 | result 27 | end 28 | 29 | def expand result, src_data, src_file, origins={}, imported={} 30 | to_import = expand_paths src_data['import'] 31 | to_ignore = expand_paths src_data['ignore'] 32 | import result, to_import - to_ignore, origins, imported, src_file 33 | result 34 | end 35 | 36 | def expand_paths virtual_paths 37 | Array(virtual_paths).flat_map do |virtual_path| 38 | Dir[File.join(DIR, virtual_path)] 39 | end 40 | end 41 | 42 | def merge dst_hash, src_hash, src_file, origins={}, backtrace=[] 43 | src_hash.each do |key, src_val| 44 | backtrace.push key 45 | 46 | catch :merged do 47 | if dst_hash.key? key 48 | dst_val = dst_hash[key] 49 | 50 | dst_file = origins[dst_val] 51 | section = backtrace.join(':') 52 | 53 | if src_val.nil? 54 | LOG.warn 'empty section %s in %s removes value %s from %s' % 55 | [section, src_file, dst_val, dst_file].map(&:inspect) 56 | 57 | dst_hash.delete key 58 | throw :merged 59 | 60 | elsif dst_val.is_a? Hash and src_val.is_a? Hash 61 | merge dst_val, src_val, src_file, origins, backtrace 62 | throw :merged 63 | 64 | elsif dst_val.is_a? Array 65 | dst_val.concat Array(src_val) 66 | throw :merged 67 | 68 | else 69 | LOG.warn 'value %s from %s overrides %s from %s in section %s' % 70 | [src_val, src_file, dst_val, dst_file, section].map(&:inspect) 71 | # fall through 72 | end 73 | end 74 | 75 | dst_hash[key] = src_val 76 | end 77 | 78 | backtrace.pop 79 | end 80 | 81 | dst_hash 82 | end 83 | 84 | private 85 | 86 | def mark_origin data, origin, result 87 | result[data] = 88 | if result.key? data 89 | Array(result[data]).push(origin).uniq 90 | else 91 | origin 92 | end 93 | 94 | if data.respond_to? :each 95 | data.each do |*values| 96 | values.each do |value| 97 | mark_origin value, origin, result 98 | end 99 | end 100 | end 101 | end 102 | 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/wmiirc/loader.rb: -------------------------------------------------------------------------------- 1 | require 'wmiirc' 2 | require 'wmiirc/config' 3 | require 'wmiirc/system' 4 | require 'kwalify' 5 | 6 | module Wmiirc 7 | module Loader 8 | class << self 9 | 10 | CONFIG_FILE = File.join(DIR, 'config.yaml') 11 | CONFIG_DUMP_FILE = File.join(DIR,'config.dump') 12 | 13 | CONFIG_SCHEMA_FILE = File.join(DIR, 'schema.yaml') 14 | CONFIG_SCHEMA = YAML.load_file(CONFIG_SCHEMA_FILE) 15 | CONFIG_VALIDATOR = Kwalify::Validator.new(CONFIG_SCHEMA) 16 | CONFIG_PARSER = Kwalify::Yaml::Parser.new(CONFIG_VALIDATOR) 17 | 18 | def run 19 | LOG.info 'start' 20 | 21 | log_standard_outputs 22 | terminate_other_instances 23 | load_user_config 24 | spawn 'witray' # relaunch to accomodate changes in screen resolution 25 | enter_event_loop 26 | 27 | rescue SystemExit 28 | # ignore it; the program wants to terminate 29 | 30 | rescue Errno::EPIPE => e 31 | LOG.error e 32 | LOG.info 'Lost connection to wmii. Attempting to reconnect...' 33 | reload 34 | 35 | rescue Exception => e 36 | LOG.error e 37 | allow_user_rescue e 38 | 39 | ensure 40 | LOG.info 'stop' 41 | end 42 | 43 | def reload 44 | LOG.info 'reload' 45 | # 46 | # NOTE: we launch another process here instead of exec()ing in order to 47 | # let this process terminate normally, thereby triggering at_exit hooks, 48 | # which in turn properly save the SESSION and do other necessary cleanup 49 | # 50 | Wmiirc.launch! File.expand_path($0), *ARGV 51 | end 52 | 53 | private 54 | 55 | def log_standard_outputs 56 | [STDOUT, STDERR].each do |output| 57 | output.singleton_class.class_eval do 58 | alias _547c1ae1_b571_4273_8037_d0c829856680 write 59 | def write string 60 | Wmiirc::LOG << string 61 | _547c1ae1_b571_4273_8037_d0c829856680 string 62 | end 63 | alias << write # update alias to use new method 64 | end 65 | end 66 | end 67 | 68 | def terminate_other_instances 69 | Rumai.fs.event.write 'Start wmiirc' 70 | 71 | Wmiirc.event 'Start' do |arg| 72 | exit if arg == 'wmiirc' 73 | end 74 | end 75 | 76 | def load_user_session 77 | session_file = File.join(DIR, 'session.dump') 78 | 79 | # don't use "w" because it truncates the file on opening 80 | # also, this lock will be automatically released on exit 81 | File.open(session_file, File::RDWR|File::CREAT, 0644).flock(File::LOCK_EX) 82 | 83 | session = 84 | begin 85 | YAML.load_file(session_file).to_hash 86 | rescue => e 87 | LOG.error e 88 | Hash.new 89 | end 90 | 91 | at_exit do 92 | File.write session_file, session.to_yaml 93 | end 94 | 95 | Wmiirc.const_set :SESSION, session 96 | end 97 | 98 | def load_user_requires 99 | Array(CONFIG['require']).each do |library| 100 | if library.kind_of? Hash 101 | library.each do |gem_name, gem_version| 102 | gem gem_name, *Array(gem_version) 103 | require gem_name 104 | end 105 | else 106 | require library 107 | end 108 | end 109 | end 110 | 111 | def load_user_config 112 | config = Config.new(CONFIG_FILE) 113 | File.write CONFIG_DUMP_FILE, config.to_yaml 114 | 115 | errors = CONFIG_VALIDATOR.validate(config) 116 | if errors and not errors.empty? 117 | raise ArgumentError, "invalid configuration:\n#{errors.join("\n")}" 118 | end 119 | 120 | Wmiirc.const_set :CONFIG, config 121 | load_user_requires 122 | load_user_session 123 | config.apply 124 | end 125 | 126 | def enter_event_loop 127 | Rumai.fs.event.each_line do |line| 128 | line.split(/\n/).each do |call| 129 | name, args = call.split(' ', 2) 130 | argv = args.to_s.split(' ') 131 | 132 | Wmiirc.event name, *argv 133 | end 134 | end 135 | end 136 | 137 | def allow_user_rescue error 138 | spawn 'xterm' 139 | 140 | IO.popen('xmessage -nearmouse -file - -buttons Recover,Ignore -print', 'w+') do |f| 141 | f.puts error.inspect, error.backtrace 142 | f.close_write 143 | 144 | reload if f.read.chomp == 'Recover' 145 | end 146 | end 147 | 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/wmiirc/menu.rb: -------------------------------------------------------------------------------- 1 | require 'shellwords' 2 | 3 | module Wmiirc 4 | 5 | HISTORY_DIR = File.join(DIR, 'history') 6 | Dir.mkdir HISTORY_DIR unless Dir.exist? HISTORY_DIR 7 | 8 | ## 9 | # Shows a menu (where the user must press keys on their keyboard to 10 | # make a choice) with the given items and returns the chosen item. 11 | # 12 | # @return nil if nothing was chosen. 13 | # 14 | # @param [Array] choices 15 | # List of choices to display in the menu. 16 | # 17 | # @param [String] prompt 18 | # Instruction on what the user should enter or choose. 19 | # 20 | # @param [String] history_name 21 | # Basename of the file in which the user's 22 | # choices will be stored: the history file. 23 | # 24 | def key_menu choices, prompt, history_name=nil 25 | command = ['wimenu'] 26 | command.push '-p', prompt.to_s if prompt 27 | 28 | if history_name 29 | history_file = File.join(HISTORY_DIR, history_name.to_s) 30 | if File.exist? history_file 31 | # show previous choices before current choices in menu 32 | history = File.readlines(history_file).map(&:chomp) 33 | choices = (history & choices).concat(choices).uniq 34 | end 35 | end 36 | 37 | IO.popen(command, 'r+') do |menu| 38 | menu.puts choices 39 | menu.close_write 40 | 41 | choice = menu.read 42 | unless choice.empty? 43 | if history_name 44 | # record new choice in history file for use next time 45 | # also make sure history is unique to keep file small 46 | File.write history_file, history.unshift(choice).uniq.join(?\n) 47 | end 48 | choice 49 | end 50 | end 51 | end 52 | 53 | ## 54 | # Shows a menu (where the user must click a menu 55 | # item using their mouse to make a choice) with 56 | # the given items and returns the chosen item. 57 | # 58 | # @return (see #key_menu) 59 | # 60 | # @param choices (see #key_menu) 61 | # 62 | # @param initial 63 | # The choice that should be initially selected. 64 | # 65 | # If this choice is not included in the list 66 | # of choices, then this item will be made 67 | # into a makeshift title-bar for the menu. 68 | # 69 | def click_menu choices, initial=nil 70 | command = ['wmii9menu'] 71 | 72 | if initial 73 | command << '-i' 74 | 75 | unless choices.include? initial 76 | initial = "<<#{initial}>>:" 77 | command << initial 78 | end 79 | 80 | command << initial 81 | end 82 | 83 | command.concat choices 84 | 85 | choice = `#{command.shelljoin}`.chomp 86 | choice unless choice.empty? 87 | end 88 | 89 | ## 90 | # Shows a {#key_menu} containing 91 | # all currently available clients 92 | # and returns the chosen client. 93 | # 94 | def client_menu *key_menu_args 95 | clients = Rumai.clients 96 | 97 | choices = clients.map do |c| 98 | "#{c[:label].read.downcase} @#{c[:tags].read}" 99 | end 100 | 101 | if index = index_menu(choices, *key_menu_args) 102 | clients[index] 103 | end 104 | end 105 | 106 | ## 107 | # Shows a {#key_menu} containing 108 | # the given choices and returns 109 | # the index of the chosen item. 110 | # 111 | def index_menu choices, *key_menu_args 112 | choices = choices.each_with_index.map {|c,i| "#{c}\t#{i}" } 113 | if target = key_menu(choices, *key_menu_args) 114 | target[/\d+\z/].to_i 115 | end 116 | end 117 | 118 | end 119 | -------------------------------------------------------------------------------- /lib/wmiirc/system.rb: -------------------------------------------------------------------------------- 1 | module Wmiirc 2 | 3 | # reap dead child processes to save them from becoming zombies 4 | trap :SIGCHLD do 5 | begin 6 | while Process.wait(-1, Process::WNOHANG) 7 | # reap! 8 | end 9 | rescue Errno::ECHILD 10 | # there are no more dead child processes remaining to reap 11 | end 12 | end 13 | 14 | ## 15 | # Runs {#launch!} inside the present working directory of the 16 | # currently focused client or, if undeterminable, that of wmii. 17 | # 18 | def launch *args 19 | if label = curr_client.label.read rescue nil 20 | label.encode(Encoding::UTF_8, undef: :replace, replace: ''). 21 | split(/[\s\[\]\{\}\(\)<>"':]+/).reverse_each do |word| 22 | if File.exist? path = File.expand_path(word) 23 | path = File.dirname(path) unless File.directory? path 24 | Dir.chdir(path) { launch! *args } 25 | return 26 | end 27 | end 28 | end 29 | 30 | launch! *args 31 | end 32 | 33 | ## 34 | # Launches the given command in the background. 35 | # 36 | # @param [String] command 37 | # The name or path to the program you want 38 | # to launch. This can be a self-contained 39 | # shell command if no arguments are given. 40 | # 41 | # @param arguments_then_wihack_options 42 | # Command-line arguments for the command being launched, 43 | # optionally followed by a Hash containing command-line 44 | # option names and values for the `wihack` program. 45 | # 46 | # ==== Examples 47 | # 48 | # Launch a self-contained shell command (while making sure that 49 | # the arguments within the shell command are properly quoted): 50 | # 51 | # launch! "xmessage 'hello world' '#{Time.now}'" 52 | # 53 | # Launch a command with explicit arguments (while not 54 | # having to worry about shell-quoting those arguments): 55 | # 56 | # launch! 'xmessage', 'hello world', Time.now.to_s 57 | # 58 | # Launch a command on the floating layer (treating 59 | # it as a dialog box) using the `wihack` program: 60 | # 61 | # launch! 'xmessage', 'hello world', Time.now.to_s, type: 'DIALOG' 62 | # 63 | def launch! command, *arguments_then_wihack_options 64 | *arguments, wihack_options = arguments_then_wihack_options 65 | 66 | unless wihack_options.nil? or wihack_options.kind_of? Hash 67 | arguments.push wihack_options 68 | wihack_options = nil 69 | end 70 | 71 | unless arguments.empty? 72 | command = [command, *arguments].shelljoin 73 | end 74 | 75 | if wihack_options 76 | wihack_argv = wihack_options.map {|k,v| ["-#{k}", v] }.flatten 77 | command = "wihack #{wihack_argv.shelljoin} #{command}" 78 | end 79 | 80 | spawn command 81 | end 82 | 83 | ## 84 | # Shows a notification with the given title and message. 85 | # 86 | # This is a "fire and forget" operation. The result of 87 | # the notification command is NOT returned by this method! 88 | # 89 | # @param title 90 | # The title to be displayed. 91 | # 92 | # @param message 93 | # The message to be displayed. 94 | # 95 | # @param icon 96 | # The icon to be displayed. 97 | # 98 | # @param arguments 99 | # Additional command-line arguments for `notify-send`. 100 | # 101 | def notify title, message, icon='dialog-information', *arguments 102 | Rumai.fs.event.write "Notice #{title}: #{message}\n" 103 | launch! 'notify-send', '-i', icon, title, message, *arguments 104 | end 105 | 106 | ## 107 | # Shows a dialog box containing the given message. 108 | # 109 | # This is a "fire and forget" operation. The result of 110 | # the launched dialog box is NOT returned by this method! 111 | # 112 | # @param message 113 | # The message to be displayed. 114 | # 115 | # @param arguments 116 | # Additional command-line arguments for `xmessage`. 117 | # 118 | def dialog message, *arguments 119 | launch! 'xmessage', '-nearmouse', *arguments, message, type: 'DIALOG' 120 | end 121 | 122 | end 123 | -------------------------------------------------------------------------------- /schema.yaml: -------------------------------------------------------------------------------- 1 | type: map 2 | required: true 3 | mapping: 4 | 'custom': 5 | type: any 6 | required: false 7 | 8 | 'import': 9 | type: seq 10 | required: false 11 | sequence: 12 | - type: str 13 | required: true 14 | 'ignore': 15 | type: seq 16 | required: false 17 | sequence: 18 | - type: str 19 | required: true 20 | 'require': 21 | type: seq 22 | required: false 23 | sequence: 24 | - type: any 25 | required: true 26 | 'control': 27 | type: map 28 | required: true 29 | mapping: 30 | 'event': &optional_hash_with_string_values 31 | type: map 32 | required: false 33 | mapping: 34 | =: 35 | type: str 36 | required: true 37 | 'mouse': 38 | type: map 39 | required: true 40 | mapping: 41 | 'grab': &required_string 42 | type: str 43 | required: true 44 | =: *required_string 45 | 'keyboard': *optional_hash_with_string_values 46 | 'action': *optional_hash_with_string_values 47 | 'keyboard_action': *optional_hash_with_string_values 48 | 'script': 49 | type: map 50 | required: false 51 | mapping: 52 | 'before': &script 53 | type: seq 54 | required: false 55 | sequence: 56 | - type: str 57 | required: true 58 | 'after': *script 59 | 'status': 60 | type: map 61 | required: false 62 | mapping: 63 | =: 64 | type: map 65 | required: true 66 | mapping: &status_barlet 67 | 'params': &optional_hash 68 | type: map 69 | required: false 70 | mapping: 71 | =: 72 | type: any 73 | 'script': 74 | type: str 75 | required: false 76 | 'label': 77 | type: str 78 | required: false 79 | 'refresh': 80 | type: int 81 | required: false 82 | range: 83 | min: 0 84 | 'control': 85 | type: map 86 | required: false 87 | mapping: 88 | 'event': *optional_hash_with_string_values 89 | 'action': *optional_hash_with_string_values 90 | 'mouse_action': 91 | type: map 92 | required: false 93 | mapping: 94 | 'left_click': &optional_string 95 | type: str 96 | required: false 97 | 'wheel_click': *optional_string 98 | 'right_click': *optional_string 99 | 'wheel_up': *optional_string 100 | 'wheel_down': *optional_string 101 | 'wheel_left': *optional_string 102 | 'wheel_right': *optional_string 103 | 'display': 104 | type: map 105 | required: true 106 | mapping: 107 | 'bar': 108 | type: str 109 | required: true 110 | enum: [top, bottom] 111 | 'font': 112 | type: str 113 | required: true 114 | 'border': 115 | type: int 116 | required: true 117 | range: 118 | min: 0 119 | 'color': 120 | type: map 121 | required: true 122 | mapping: 123 | 'desktop': 124 | type: str 125 | required: true 126 | pattern: '/^#\h{6}$/' 127 | 'normal': &required_color_tuple 128 | type: str 129 | required: true 130 | pattern: '/^#\h{6} #\h{6} #\h{6}$/' 131 | 'focus': *required_color_tuple 132 | =: *required_color_tuple 133 | 'refresh': 134 | type: int 135 | required: false 136 | range: 137 | min: 0 138 | 'column': 139 | type: map 140 | required: true 141 | mapping: 142 | 'mode': 143 | type: str 144 | required: true 145 | enum: [default, stack, max] 146 | 'rule': 147 | type: str 148 | required: true 149 | 'client': 150 | type: map 151 | required: true 152 | mapping: 153 | =: 154 | type: seq 155 | required: true 156 | sequence: 157 | - type: str 158 | required: true 159 | 'status': 160 | type: seq 161 | required: false 162 | sequence: 163 | - type: map 164 | required: false 165 | mapping: 166 | =: 167 | type: map 168 | required: false 169 | mapping: *status_barlet 170 | -------------------------------------------------------------------------------- /status/arrange.yaml: -------------------------------------------------------------------------------- 1 | status: 2 | arrange: 3 | refresh: 0 # this status bar applet will refresh itself as necessary 4 | 5 | params: 6 | ## 7 | # Default arrangement to apply to all views unless chosen otherwise. 8 | # 9 | default: 10 | 11 | ## 12 | # Arrangements that are available for the user to select and apply. 13 | # 14 | # : 15 | # 16 | choices: 17 | right: tile_right 18 | rightward: tile_rightward 19 | left: tile_left 20 | leftward: tile_leftward 21 | inward: tile_inward 22 | outward: tile_outward 23 | stack: stack 24 | grid: grid 25 | join: join 26 | split: grid 1 27 | 28 | script: | 29 | # create actions for each possible choice so that the user can set 30 | # up keybindings to trigger the application of particular choices 31 | @choices.each_key do |choice| 32 | action "arrange_#{choice}" do 33 | apply choice 34 | end 35 | 36 | action "arrange_#{choice}_persist" do 37 | self.current = choice 38 | end 39 | end 40 | 41 | # reapply peristent arrangement when wmii tells us to 42 | event('ArrangeView') { apply } 43 | 44 | def apply choice=current 45 | if @choices.key? choice 46 | curr_view.instance_eval(@choices[choice].to_s) 47 | end 48 | end 49 | 50 | event 'FocusTag' do |tag| 51 | @current_tag = tag 52 | refresh 53 | end 54 | 55 | @choice_by_tag ||= {} 56 | 57 | def current 58 | @choice_by_tag[@current_tag] || @default 59 | end 60 | 61 | def current= choice 62 | raise ArgumentError unless @choices.key? choice 63 | @choice_by_tag[@current_tag] = choice 64 | refresh 65 | apply 66 | end 67 | 68 | def stop 69 | @choice_by_tag.delete @current_tag 70 | refresh 71 | end 72 | 73 | def choose 74 | if choice = click_menu(@choices.keys, current) 75 | self.current = choice 76 | end 77 | end 78 | 79 | label: | 80 | ["\u22C8", current] 81 | 82 | control: 83 | action: 84 | arrange_persist_stop: self.stop 85 | arrange_persist_menu: self.choose 86 | arrange_persist_redo: self.apply 87 | 88 | mouse_action: 89 | left_click: arrange_persist_menu 90 | right_click: arrange_persist_stop 91 | wheel_click: arrange_persist_redo 92 | -------------------------------------------------------------------------------- /status/backlight.yaml: -------------------------------------------------------------------------------- 1 | status: 2 | backlight: 3 | params: 4 | device: '*' 5 | change: 7 6 | 7 | script: | 8 | def level 9 | current, maximum = 10 | Dir["/sys/class/backlight/#{@device}/{,max_}brightness"]. 11 | map { |file| File.read(file).to_f } 12 | (current / maximum) * 100 13 | end 14 | 15 | def change direction 16 | system "xbacklight #{direction} #{@change}" 17 | refresh 18 | end 19 | 20 | def level= value 21 | value = value.to_f 22 | value = [value, 100].min 23 | value = [value, 0].max 24 | system "xbacklight = #{value}" 25 | refresh 26 | end 27 | 28 | def menu 29 | current = self.level 30 | levels = (0 .. 10).map {|i| i * 10 }. 31 | push(current.to_i).sort.uniq. 32 | map {|i| "#{i}%" }.reverse 33 | 34 | if choice = click_menu(levels, current) 35 | self.level = choice.to_i 36 | end 37 | end 38 | 39 | label: | 40 | ["\u2747", level.round(1)] 41 | 42 | control: 43 | action: 44 | backlight_more: self.change '+' 45 | backlight_less: self.change '-' 46 | backlight_full: self.level = 100 47 | backlight_none: self.level = 0 48 | backlight_menu: self.menu 49 | 50 | mouse_action: 51 | left_click: backlight_menu 52 | right_click: backlight_full 53 | wheel_up: backlight_more 54 | wheel_down: backlight_less 55 | -------------------------------------------------------------------------------- /status/battery.yaml: -------------------------------------------------------------------------------- 1 | status: 2 | battery: 3 | params: 4 | device: BAT0 5 | low: 15 6 | sleep_level: 7 | sleep_command: 8 | 9 | script: | 10 | def device_uevent_file 11 | "/sys/class/power_supply/#{@device}/uevent" 12 | end 13 | 14 | def device_uevent_blob 15 | File.read device_uevent_file 16 | end 17 | 18 | def device_uevent_data 19 | Hash[ 20 | device_uevent_blob.split(/[=\s]+/).each_slice(2).map do |key, value| 21 | [key.sub(/^POWER_SUPPLY_/, '').downcase.to_sym, value] 22 | end 23 | ] 24 | end 25 | 26 | label: | 27 | data = device_uevent_data 28 | level = data[:capacity].to_i 29 | icon = 30 | case data[:status] 31 | when 'Charging' then "\u25B4" 32 | when 'Full' then "\u26A1" 33 | else 34 | system @sleep_command if level <= @sleep_level and @sleep_command 35 | "\u25BE" 36 | end 37 | color = 38 | case data[:capacity_level] 39 | when 'Critical' then :error 40 | else :notice if level <= @low 41 | end 42 | 43 | [color, "\u26A1", level, icon] 44 | 45 | control: 46 | action: 47 | battery_details: dialog device_uevent_blob 48 | 49 | mouse_action: 50 | left_click: battery_details 51 | -------------------------------------------------------------------------------- /status/clock.yaml: -------------------------------------------------------------------------------- 1 | status: 2 | clock: 3 | params: 4 | refresh: 1 5 | format: '%c' 6 | 7 | label: | 8 | ["\u25D4", Time.now.strftime(@format)] 9 | 10 | control: 11 | action: 12 | time_month_calendar: | 13 | dialog `cal`.sub(/\b#{Time.now.day}\b/) {|s| '*' * s.length } 14 | 15 | time_year_calendar: | 16 | dialog `cal -y`.sub(/^.*$/) {|line| Date.today.to_s.center(line.length) } 17 | 18 | mouse_action: 19 | left_click: time_month_calendar 20 | right_click: time_year_calendar 21 | -------------------------------------------------------------------------------- /status/disk.yaml: -------------------------------------------------------------------------------- 1 | status: 2 | disk: 3 | params: 4 | path: # the disk path that you want to monitor 5 | 6 | script: | 7 | def usage 8 | `df -h #{@path} 2>/dev/null || df #{@path}` 9 | end 10 | 11 | label: | 12 | free, used, path = usage.split.last(3) 13 | [path, "\u25D3", used, "\u25CC", free] 14 | 15 | control: 16 | action: 17 | show_disk_usage: dialog self.usage 18 | 19 | mouse_action: 20 | left_click: show_disk_usage 21 | -------------------------------------------------------------------------------- /status/ipaddr.yaml: -------------------------------------------------------------------------------- 1 | status: 2 | ipaddr: 3 | params: 4 | path_to_ip: 5 | label: | 6 | ipbin = @path_to_ip || 'ip' 7 | cmd = "#{ipbin} addr | awk '/inet / {sub(/\\/.*/, \"\", $2); print $2}'" 8 | `#{cmd}`.sub("127.0.0.1\n", '').tr("\n", ' ').strip 9 | -------------------------------------------------------------------------------- /status/loadavg.yaml: -------------------------------------------------------------------------------- 1 | require: 2 | - rbconfig 3 | 4 | status: 5 | loadavg: 6 | params: 7 | critical: 1.5 8 | high: 0.75 9 | 10 | label: | 11 | averages = `uptime`[/load average: (.+)$/, 1].split(', ') 12 | level = averages.first 13 | 14 | color = 15 | case [level.to_f, @high, @critical].min 16 | when @critical then :error 17 | when @high then :notice 18 | end 19 | 20 | [color, "\u2691", *averages] 21 | 22 | control: 23 | action: 24 | loadavg_details: | 25 | launch 'xterm', '-e', 'htop || top', type: 'DIALOG' 26 | 27 | mouse_action: 28 | left_click: loadavg_details 29 | -------------------------------------------------------------------------------- /status/memory.yaml: -------------------------------------------------------------------------------- 1 | require: 2 | - rbconfig 3 | 4 | status: 5 | memory: 6 | params: 7 | 8 | script: | 9 | case ::RbConfig::CONFIG['host_os'] 10 | when /linux/ 11 | 12 | def sample! 13 | reading = `free -m` 14 | total, @used, @free = reading[/Mem:(.+)/, 1].split.map(&:to_f) 15 | @swap = reading[/Swap:(.+)/, 1].split[1] 16 | end 17 | 18 | when /bsd/ 19 | 20 | def sample! 21 | reading = `vmstat -h`.split("\n")[2].split 22 | @swap, @used, @free = reading[2...4] 23 | end 24 | 25 | else 26 | raise NotImplementedError, 'OS not supported' 27 | end 28 | 29 | label: | 30 | sample! 31 | used_percent = ((@used.to_f / (@used.to_f + @free.to_f)) * 100).round 32 | ["mem #{used_percent}% \u25CC #{@free}M", 33 | ("\u25C9 #{@swap}M" if @swap.to_f > 0)] 34 | 35 | control: 36 | action: 37 | memory_details: dialog `vmstat` 38 | 39 | mouse_action: 40 | left_click: memory_details 41 | -------------------------------------------------------------------------------- /status/music/mpd.yaml: -------------------------------------------------------------------------------- 1 | require: 2 | - librmpd 3 | - fileutils 4 | 5 | status: 6 | music: 7 | params: 8 | playlist_directory: '~/.mpd/playlists' 9 | 10 | script: | 11 | @playlist_directory = File.expand_path(@playlist_directory) 12 | 13 | def mpd 14 | @mpd or begin 15 | @mpd = MPD.new 16 | at_exit do 17 | @mpd.disconnect 18 | @mpd = nil # avoid storing MPD instance in Wmiirc::SESSION 19 | end 20 | @mpd.connect true 21 | @mpd.password password if @password 22 | @mpd 23 | end 24 | end 25 | 26 | label: | 27 | if mpd.stopped? 28 | icon = "\u25A0" 29 | else 30 | song = mpd.current_song 31 | 32 | if title = song.title || (f = song.file and File.basename(f)) 33 | # infer artist from song title 34 | if not artist = song.artist and title =~ /\s+-\s+/ 35 | artist, title = $`, $' 36 | end 37 | 38 | # remove parenthetical subtext 39 | title = title.sub(/(.+)\s*\(.*/, '\1') 40 | end 41 | 42 | icon = mpd.paused? ? "\u25AE" * 2 : "\u25B6" 43 | label = [title, artist].compact.join(' by ') 44 | end 45 | 46 | [icon, label.force_encoding(Encoding::UTF_8)] rescue label 47 | 48 | control: 49 | action: 50 | music_play!: | 51 | if mpd.stopped? 52 | mpd.play 53 | else 54 | mpd.pause = !mpd.paused? 55 | end 56 | refresh 57 | 58 | music_pause: | 59 | mpd.pause = true 60 | refresh 61 | 62 | music_stop: | 63 | mpd.stop 64 | refresh 65 | 66 | music_next: | 67 | mpd.next 68 | refresh 69 | 70 | music_previous: | 71 | mpd.previous 72 | refresh 73 | 74 | music_menu: | 75 | songs = `mpc playlist`.downcase.lines.map(&:chomp) 76 | if index = index_menu(songs, 'play song:', :song) 77 | # MPD uses natural 1..N numbering 78 | system "mpc play #{index + 1}" 79 | end 80 | refresh 81 | 82 | music_playlist_menu: | # load chosen playlist 83 | if choice = key_menu(mpd.playlists, 'load playlist:', :playlist) 84 | mpd.clear 85 | mpd.load choice 86 | mpd.play 87 | end 88 | 89 | music_favorite_menu: | # add current song to chosen playlist 90 | if choice = key_menu(mpd.playlists, 'save favorite:') 91 | dir = FileUtils.mkdir_p(@playlist_directory).first 92 | m3u = File.join(dir, choice + '.m3u') 93 | 94 | songs = File.readlines(m3u).map(&:chomp) rescue [] 95 | songs.unshift mpd.current_song.file 96 | songs.uniq! 97 | 98 | File.write m3u, songs.join(?\n) 99 | end 100 | 101 | 102 | mouse_action: 103 | left_click: music_play! 104 | right_click: music_stop 105 | wheel_up: music_previous 106 | wheel_down: music_next 107 | wheel_click: music_menu 108 | -------------------------------------------------------------------------------- /status/music/rhythmbox.yaml: -------------------------------------------------------------------------------- 1 | status: 2 | music: 3 | params: 4 | client: rhythmbox-client --no-start 5 | format: '(%ta) %at - %tt' 6 | 7 | label: | 8 | `#{@client} --print-playing-format #{@format.inspect}`.chomp 9 | 10 | control: 11 | action: 12 | music_play!: | 13 | system "#{@client} --play-pause" 14 | refresh 15 | 16 | music_pause: | 17 | system "#{@client} --pause" 18 | refresh 19 | 20 | music_stop: | 21 | system "#{@client} --stop" 22 | refresh 23 | 24 | music_next: | 25 | system "#{@client} --next" 26 | refresh 27 | 28 | music_previous: | 29 | system "#{@client} --previous" 30 | refresh 31 | 32 | mouse_action: 33 | left_click: music_play! 34 | right_click: music_stop 35 | wheel_up: music_previous 36 | wheel_down: music_next 37 | wheel_click: music_menu 38 | -------------------------------------------------------------------------------- /status/network.yaml: -------------------------------------------------------------------------------- 1 | status: 2 | network: 3 | params: 4 | ifconfig_bin: ifconfig 5 | iwconfig_bin: iwconfig 6 | interface: lo 7 | 8 | script: | 9 | def ifconfig 10 | `#{@ifconfig_bin} #{@interface} 2>&1` 11 | end 12 | 13 | def iwconfig 14 | `#{@iwconfig_bin} #{@interface} 2>/dev/null` 15 | end 16 | 17 | label: | 18 | ifconfig = self.ifconfig 19 | alive = ifconfig !~ /\A\S+ error/ 20 | ip_addr = ifconfig[/inet (?:addr:)?([\d.]+)/, 1] 21 | 22 | if alive and ip_addr 23 | color = 'normal' 24 | rx_bytes, tx_bytes = ifconfig.scan(/\((\d+(?:\.\d+)? \w+)\)/) 25 | stats = ['@', ip_addr, "\u25BE", rx_bytes, "\u25B4", tx_bytes] 26 | else 27 | color = 'error' 28 | stats = [] 29 | end 30 | 31 | if self.iwconfig =~ /ESSID:"(.+?)"/ 32 | stats.unshift "\u223F", $1 33 | end 34 | 35 | [color, @interface, stats] 36 | 37 | control: 38 | action: 39 | network_details: dialog [iwconfig, ifconfig].join 40 | 41 | mouse_action: 42 | left_click: network_details 43 | -------------------------------------------------------------------------------- /status/notice.yaml: -------------------------------------------------------------------------------- 1 | status: 2 | notice: 3 | refresh: 0 4 | 5 | params: 6 | duration: 5 7 | 8 | script: | 9 | @lock = Mutex.new 10 | @message = nil 11 | 12 | ## 13 | # Displays the given notice in the status bar. 14 | # 15 | def display message 16 | # don't block the calling thread 17 | Thread.new do 18 | # prevent messages from clobbering 19 | # each other before their time is up 20 | @lock.synchronize do 21 | # display the message 22 | LOG.info message # also log it in case the user is not present 23 | @message = message 24 | refresh 25 | 26 | # clear the notice 27 | sleep [1, @duration.to_i].max 28 | @message = nil 29 | refresh 30 | end 31 | end 32 | end 33 | 34 | label: | 35 | [:notice, @message] if @message 36 | 37 | control: 38 | action: 39 | focus_notice_client: | 40 | @client.focus rescue nil 41 | 42 | mouse_action: 43 | left_click: focus_notice_client 44 | 45 | event: 46 | Notice: | 47 | display argv.join(' ') 48 | 49 | Urgent: | 50 | id, type = argv 51 | 52 | if type == 'Client' 53 | @client = Client.new(id) 54 | message = @client.label.read 55 | else 56 | message = id 57 | end 58 | 59 | display "#{type} #{message}" 60 | -------------------------------------------------------------------------------- /status/pomodoro.yaml: -------------------------------------------------------------------------------- 1 | status: 2 | pomodoro: 3 | refresh: 60 4 | 5 | script: | 6 | POMODOROS_PER_SESSION = 4 7 | MINUTES_PER_BREAK = 5 8 | MINUTES_PER_POMODORO = 25 9 | SECONDS_PER_MINUTE = 60 10 | SECONDS_PER_POMODORO = MINUTES_PER_POMODORO * SECONDS_PER_MINUTE 11 | @pomodoros ||= 0 12 | 13 | def start 14 | completed = Time.now >= @deadline if @deadline 15 | 16 | if not @started or completed 17 | @deadline = Time.now + SECONDS_PER_POMODORO 18 | @started = true 19 | @paused = false 20 | @pomodoros += 1 if completed 21 | 22 | elsif @paused 23 | @deadline = Time.now + @paused 24 | @paused = false 25 | 26 | else 27 | @paused = @deadline - Time.now 28 | end 29 | 30 | refresh 31 | end 32 | 33 | def pause 34 | start unless @started 35 | start 36 | end 37 | 38 | def stop 39 | @started = false 40 | @paused = false 41 | refresh 42 | end 43 | 44 | def reset 45 | @pomodoros = 0 46 | refresh 47 | end 48 | 49 | def seconds_to_minutes seconds 50 | (seconds.to_f / SECONDS_PER_MINUTE).round 51 | end 52 | 53 | def tell_user_to_take_break 54 | message = 55 | if (@pomodoros + 1) % POMODOROS_PER_SESSION == 0 56 | 'Take a longer (10-20 minutes) break!' 57 | else 58 | 'Take a short (5 minutes) break!' 59 | end 60 | 61 | notify 'Pomodoro Technique', message 62 | end 63 | 64 | label: | 65 | # remind the user that they are using their computer without having 66 | # activated the pomodoro timer; we must think of their health! >:-O 67 | color = :error 68 | 69 | if @paused 70 | color = :notice 71 | minutes_remaining = seconds_to_minutes(@paused) 72 | 73 | elsif @started 74 | minutes_remaining = seconds_to_minutes(@deadline - Time.now) 75 | 76 | if minutes_remaining > 0 77 | color = :normal 78 | elsif minutes_remaining > -MINUTES_PER_BREAK 79 | color = :success 80 | end 81 | 82 | tell_user_to_take_break if minutes_remaining <= 0 and 83 | minutes_remaining % MINUTES_PER_BREAK == 0 84 | end 85 | 86 | [color, "\u2692", 87 | ("#{minutes_remaining}m" if minutes_remaining), 88 | ("x#{@pomodoros}" if @started), 89 | ] 90 | 91 | control: 92 | action: 93 | pomodoro_start: self.start 94 | pomodoro_pause: self.pause 95 | pomodoro_stop: self.stop 96 | pomodoro_reset: self.reset 97 | 98 | mouse_action: 99 | left_click: pomodoro_start 100 | right_click: pomodoro_stop 101 | wheel_click: pomodoro_reset 102 | -------------------------------------------------------------------------------- /status/spacer.yaml: -------------------------------------------------------------------------------- 1 | status: 2 | spacer: 3 | refresh: 0 4 | control: 5 | mouse_action: 6 | wheel_up: focus_view_next 7 | wheel_down: focus_view_previous 8 | -------------------------------------------------------------------------------- /status/thermal.yaml: -------------------------------------------------------------------------------- 1 | status: 2 | thermal: 3 | params: 4 | zone: 0 5 | 6 | label: | 7 | device = "/sys/class/thermal/thermal_zone#{@zone}" 8 | 9 | level = File.read("#{device}/temp").to_i 10 | 11 | limits = Hash[ 12 | Dir["#{device}/trip_point_*_{temp,type}"].sort. 13 | map {|file| File.read(file) }.each_slice(2). 14 | map {|temp, type| [temp.to_i, type.chomp] }. 15 | select {|key, val| key > 0 } 16 | ] 17 | 18 | status = limits.keys.sort.reverse.find {|limit| level >= limit } 19 | 20 | color = case limits[status] 21 | when "critical" then :error 22 | when "passive" then :notice 23 | when "active" then :success 24 | end 25 | 26 | [color, "\u2622", "#{(level / 1000.0).round(1)}\u2103", status] 27 | 28 | control: 29 | action: 30 | thermal_details: dialog `acpi -itc` 31 | 32 | mouse_action: 33 | left_click: thermal_details 34 | -------------------------------------------------------------------------------- /status/volume.yaml: -------------------------------------------------------------------------------- 1 | require: 2 | - rbconfig 3 | 4 | status: 5 | volume: 6 | params: 7 | mixer: 8 | 9 | script: | 10 | case ::RbConfig::CONFIG['host_os'] 11 | when /linux/ 12 | 13 | @mixer ||= 'Master' 14 | 15 | def reading 16 | `amixer get #{@mixer}` 17 | end 18 | 19 | def level 20 | reading[/\d+%/] 21 | end 22 | 23 | def level= value 24 | system "amixer -q set #{@mixer} #{value}" 25 | refresh 26 | end 27 | 28 | def increase 29 | self.level = '5%+' 30 | end 31 | 32 | def decrease 33 | self.level = '5%-' 34 | end 35 | 36 | when /bsd/ 37 | 38 | @mixer ||= 'mixer' 39 | 40 | def reading 41 | `mixer #{@mixer}` 42 | end 43 | 44 | def level 45 | reading.split.last.first.split(':').first 46 | end 47 | 48 | def level= value 49 | system "mixer #{@mixer} #{value}" 50 | refresh 51 | end 52 | 53 | def increase 54 | self.level = '+3' 55 | end 56 | 57 | def decrease 58 | self.level = '-3' 59 | end 60 | 61 | else 62 | raise NotImplementedError, 'OS not supported' 63 | end 64 | 65 | def mute! 66 | self.level = 'toggle' 67 | end 68 | 69 | def mute 70 | self.level = 'mute' 71 | end 72 | 73 | def unmute 74 | self.level = 'unmute' 75 | end 76 | 77 | def mute? 78 | reading.include? '[off]' 79 | end 80 | 81 | def menu 82 | current = self.level 83 | levels = (0 .. 10).map {|i| i * 10 }. 84 | push(current.to_i).sort.uniq. 85 | map {|i| "#{i}%" }.reverse 86 | 87 | if choice = click_menu(levels, current) 88 | self.level = choice 89 | end 90 | end 91 | 92 | label: | 93 | color = 'notice' if mute? 94 | [color, "\u266B", level] 95 | 96 | control: 97 | action: 98 | volume_menu: self.menu 99 | volume_more: self.increase 100 | volume_less: self.decrease 101 | volume_mute!: self.mute! 102 | volume_mute: self.mute 103 | volume_unmute: self.unmute 104 | 105 | mouse_action: 106 | left_click: volume_menu 107 | right_click: volume_mute! 108 | wheel_up: volume_more 109 | wheel_down: volume_less 110 | -------------------------------------------------------------------------------- /status/weather.yaml: -------------------------------------------------------------------------------- 1 | require: 2 | - barometer 3 | 4 | status: 5 | weather: 6 | params: 7 | location: 8 | units: metric 9 | 10 | label: | 11 | @weather = Barometer.new(@location).measure 12 | 13 | temp = @weather.temperature.send(@units) 14 | unit = case @units 15 | when 'metric' then "\u2103" 16 | when 'imperial' then "\u2109" 17 | end 18 | desc = @weather.current.icon 19 | icon = case desc 20 | when /cloud|overcast/ then "\u2601" 21 | when /rain|shower/ then "\u2602" 22 | when /snow|hail/ then "\u2603" 23 | when /lightning/ then "\u2607" 24 | when /thunder/ then "\u2608" 25 | else "\u2600" 26 | end 27 | 28 | [icon, temp.to_s + unit, desc] 29 | 30 | control: 31 | action: 32 | weather_details: | 33 | refresh 34 | dialog @weather.to_yaml 35 | 36 | mouse_action: 37 | left_click: weather_details 38 | -------------------------------------------------------------------------------- /wmiirc: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ex 2 | # Launches this wmii configuration. 3 | 4 | dir=$(dirname "$0") 5 | 6 | export GEM_HOME=$dir/.rubygems 7 | export PATH=$GEM_HOME/bin:$PATH 8 | export BUNDLE_GEMFILE=$dir/Gemfile 9 | 10 | which bundle || gem install bundler 11 | bundle install 12 | exec bundle exec env \ 13 | -u BUNDLE_GEMFILE \ 14 | -u RUBYOPT \ 15 | -u RUBYLIB \ 16 | $dir/wmiirc.rb "$@" 17 | -------------------------------------------------------------------------------- /wmiirc.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.unshift File.expand_path('../lib', __FILE__) 4 | require 'wmiirc/loader' 5 | Wmiirc::Loader.run 6 | --------------------------------------------------------------------------------