├── BEYOND ├── CREDITS ├── Gemfile ├── HACKING ├── HISTORY ├── INSTALL ├── LICENSE ├── MANUAL ├── README ├── SYNOPSIS ├── USAGE ├── bin └── rumai ├── inochi.conf ├── lib ├── rumai.rb └── rumai │ ├── fs.rb │ ├── inochi.rb │ ├── irb.rb │ ├── ixp.rb │ ├── ixp │ ├── message.rb │ └── transport.rb │ └── wm.rb └── test ├── rumai └── ixp │ └── message_test.rb ├── runner └── test_helper.rb /BEYOND: -------------------------------------------------------------------------------- 1 | == SEE ALSO 2 | 3 | irb(1), wmiir(1), wmii(1) 4 | 5 | [sect2] 6 | === References 7 | 8 | [horizontal] 9 | [[[Bundler]]]:: http://gembundler.com 10 | [[[Inochi]]]:: <%= Inochi::WEBSITE %> 11 | [[[libixp]]]:: http://libs.suckless.org/libixp 12 | [[[p9p]]]:: http://cm.bell-labs.com/magic/man2html/5/intro 13 | [[[RubyGems]]]:: http://rubygems.org 14 | [[[Ruby]]]:: http://ruby-lang.org 15 | [[[wmii]]]:: http://wmii.suckless.org 16 | [[[XCB-cookies]]]:: http://www.x.org/releases/X11R7.5/doc/libxcb/tutorial/#requestsreplies 17 | -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- 1 | == AUTHORS 2 | 3 | Suraj N. Kurapati 4 | 5 | === Credits 6 | 7 | Christoph Blank, 8 | Kenneth De Winter, 9 | Mattia Gheda, 10 | Michael Andrus, 11 | Nathan Neff, 12 | Sebastian Chmielewski, 13 | Simon Hafner 14 | 15 | === License 16 | 17 | %< 'LICENSE' 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'inochi', '>= 6.0.1', '< 7' 4 | gem 'minitest', '>= 2.10.0', '< 3' 5 | -------------------------------------------------------------------------------- /HACKING: -------------------------------------------------------------------------------- 1 | == HACKING 2 | 3 | === Prerequisites 4 | 5 | Install Ruby libraries necessary for development using <>: 6 | 7 | ------------------------------------------------------------------------------ 8 | bundle install 9 | ------------------------------------------------------------------------------ 10 | 11 | === Infrastructure 12 | 13 | <> serves as the project infrastructure for Rumai. It 14 | handles tasks such as building this help manual and API documentation, and 15 | packaging, announcing, and publishing new releases. See its help manual and 16 | list of tasks to get started: 17 | 18 | ------------------------------------------------------------------------------ 19 | inochi --help # display help manual 20 | inochi --tasks # list available tasks 21 | ------------------------------------------------------------------------------ 22 | 23 | === $LOAD_PATH setup 24 | 25 | Ensure that the `lib/` directory is listed in Ruby's `$LOAD_PATH` before you 26 | use any libraries therein or run any executables in the `bin/` directory. 27 | 28 | This can be achieved by passing an option to Ruby: 29 | 30 | ------------------------------------------------------------------------------ 31 | ruby -Ilib bin/rumai 32 | irb -Ilib -r rumai 33 | ------------------------------------------------------------------------------ 34 | 35 | Or by setting the `$RUBYLIB` environment variable: 36 | 37 | ------------------------------------------------------------------------------ 38 | export RUBYLIB=lib # bash, ksh, zsh 39 | setenv RUBYLIB lib # csh 40 | set -x RUBYLIB lib # fish 41 | 42 | ruby bin/rumai 43 | irb -r rumai 44 | ------------------------------------------------------------------------------ 45 | 46 | Or by running Ruby through the 47 | http://github.com/chneukirchen/rup/blob/master/ruby-wrapper[ruby-wrapper] 48 | tool. 49 | 50 | === RubyGems setup 51 | 52 | If you use Ruby 1.8 or older, then ensure that RubyGems is activated before 53 | you use any libraries in the `lib/` directory or run any executables in the 54 | `bin/` directory. 55 | 56 | This can be achieved by passing an option to Ruby: 57 | 58 | ------------------------------------------------------------------------------ 59 | ruby -rubygems bin/rumai 60 | irb -rubygems -r rumai 61 | ------------------------------------------------------------------------------ 62 | 63 | Or by setting the `$RUBYOPT` environment variable: 64 | 65 | ------------------------------------------------------------------------------ 66 | export RUBYOPT=-rubygems # bash, ksh, zsh 67 | setenv RUBYOPT -rubygems # csh 68 | set -x RUBYOPT -rubygems # fish 69 | ------------------------------------------------------------------------------ 70 | 71 | === Running tests 72 | 73 | Simply execute the included test runner, which sets up Ruby's `$LOAD_PATH` for 74 | testing, loads the included `test/test_helper.rb` file, and then evaluates all 75 | `test/**/*_test.rb` files: 76 | 77 | ------------------------------------------------------------------------------ 78 | ruby test/runner 79 | ------------------------------------------------------------------------------ 80 | 81 | Its exit status will indicate whether all tests have passed. It may also 82 | print additional pass/fail information depending on the testing library used 83 | in the `test/test_helper.rb` file. 84 | 85 | === Contributing 86 | 87 | <%= @code_repo_url %>[Fork this project on GitHub] and send a pull request. 88 | -------------------------------------------------------------------------------- /HISTORY: -------------------------------------------------------------------------------- 1 | == HISTORY 2 | 3 | === Version 4.1.3 (2011-08-21) 4 | 5 | This release fixes a bug in the equilateral triangle calulation used by the 6 | inward and outward automated client arrangements. 7 | 8 | === Version 4.1.2 (2011-04-21) 9 | 10 | This release fixes a bug in the inward automated client arrangement where 11 | clients in the middle column were divided into separate columns when they 12 | really should have been in the same column. 13 | 14 | === Version 4.1.1 (2011-03-28) 15 | 16 | This release fixes bugs in the inward & outward automated client arrangements. 17 | 18 | === Version 4.1.0 (2011-03-28) 19 | 20 | This release adds new automated client arrangements and cleans up the code. 21 | 22 | .New features 23 | 24 | - Added new automated client arrangements: 25 | 26 | * `Rumai::View#tile_left()` - Horizontal mirror of the LarsWM arrangement. 27 | 28 | * `Rumai::View#tile_leftward()` - Imagine an equilateral triangle with its 29 | base on the left side of the screen and its peak on the right side of the 30 | screen. 31 | 32 | * `Rumai::View#tile_rightward()` - Imagine an equilateral triangle with its 33 | base on the right side of the screen and its peak on the left side of the 34 | screen. 35 | 36 | * `Rumai::View#tile_inward()` - Imagine two equilateral triangles with 37 | their bases on the left and right sides of the screen and their peaks 38 | meeting in the middle of the screen. 39 | 40 | * `Rumai::View#tile_outward()` - Imagine two equilateral triangles 41 | with their bases meeting in the middle of the screen and their peaks 42 | reaching outward to the left and right sides of the screen. 43 | 44 | - Renamed existing automated client arrangement method names: 45 | 46 | * `Rumai::View#arrange_as_larswm()` is now aliased to `tile_right()` 47 | * `Rumai::View#arrange_in_diamond()` is now aliased to `tile_inward()` 48 | * `Rumai::View#arrange_in_stacks()` is now aliased to `stack()` 49 | * `Rumai::View#arrange_in_grid()` is now aliased to `grid()` 50 | 51 | === Version 4.0.0 (2011-02-25) 52 | 53 | This release fixes a bug regarding the `$WMII_ADDRESS` environment variable. 54 | 55 | .Incompatible changes 56 | 57 | * `Rumai::Area#push()`, `#insert()`, and `#unshift()` methods no longer accept 58 | an Array object as an argument. If you still wish to pass an Array, then 59 | use the splat operator to pass the contents of your Array to these methods. 60 | + 61 | Thanks to Mattia Gheda for reporting 62 | http://github.com/sunaku/rumai/issues/10[this issue]. 63 | 64 | * Add 'amount' parameter to `Rumai::Client#nudge()` and `#grow()`. 65 | 66 | .New features 67 | 68 | * Add `Rumai::Client#shrink()` method for opposite of `#grow()`. 69 | 70 | .Bug fixes 71 | 72 | * Fix ability to read and write Unicode strings to files in wmii IXP. 73 | + 74 | Thanks to OneLastTry for reporting 75 | http://github.com/sunaku/rumai/issues/9[this issue]. 76 | 77 | * Fix parsing of area IDs from view manifest when *witray* is present. 78 | 79 | === Version 3.3.1 (2010-08-11) 80 | 81 | This release fixes a bug regarding the `$WMII_ADDRESS` environment variable. 82 | 83 | .Bug fixes 84 | 85 | * Fix incorrect syntax when amending error message about the `$WMII_ADDRESS` 86 | environment variable not being set. 87 | 88 | .Housekeeping 89 | 90 | * Dump 9P2000 packets if `$VERBOSE`, not if `$DEBUG`, in unit tests. 91 | 92 | * Upgrade to Inochi 5.0.2; the help manual is now written in AsciiDoc. 93 | 94 | === Version 3.3.0 (2010-07-16) 95 | 96 | This release adds support for growing and nudging clients, adds an 97 | abstraction for status bar applets, and beautifies the source code. 98 | 99 | .New features 100 | 101 | * Add `Rumai::Barlet` class for easier status bar applets. It exposes the 102 | new, independent `colors` and `label` attributes introduced into the bar 103 | file format by wmii-hg2743. It is also backwards-compatible with older 104 | wmii versions where the aforementioned attributes were conjoined. 105 | 106 | * Add `Rumai::Client#grow` and `Rumai::Client#nudge` methods 107 | http://github.com/sunaku/rumai/issues/6[requested by Nathan Neff]. 108 | See "The /tag/ Hierarchy" in the wmii manpage for usage information. 109 | 110 | .Bug fixes 111 | 112 | * Add workaround for the 113 | http://code.google.com/p/wmii/issues/detail?id=206[wmii-hg2734 color tuple 114 | bug] in the test suite. 115 | 116 | .Housekeeping 117 | 118 | * Found real names for some anonymous contributors. 119 | 120 | * Clean up the source code formatting and organization. 121 | 122 | === Version 3.2.4 (2010-06-06) 123 | 124 | This release fixes an IXP transport layer bug under Ruby 1.8.7. 125 | 126 | .Bug fixes 127 | 128 | * `IO#ungetc` does not accept a one-character string in Ruby 1.8.7. 129 | + 130 | Thanks to Sebastian Chmielewski for reporting 131 | http://github.com/sunaku/rumai/issues/3[this issue]. 132 | 133 | === Version 3.2.3 (2010-04-28) 134 | 135 | This release adds a UNIX manual page and requires wmii 3.9 or newer. 136 | 137 | .Bug fixes 138 | 139 | * `Rumai::Area#unshift` needs wmii 3.9 or newer. The help manual has been 140 | corrected accordingly. 141 | + 142 | Thanks to Mattia Gheda for reporting 143 | http://github.com/sunaku/wmiirc/issues/8[this issue]. 144 | 145 | .Housekeeping 146 | 147 | * Upgrade to Inochi 3.0.0. Run `rumai --help` to see the UNIX manual page! 148 | 149 | * Move IRB session creation code from rumai(1) into `rumai/irb` sub-library. 150 | 151 | === Version 3.2.2 (2010-04-01) 152 | 153 | This release fixes some warnings that appeared during installation and 154 | performs some minor housekeeping. 155 | 156 | .Bug fixes 157 | 158 | * Warnings of the following form appeared during gem installation: 159 | + 160 | Unrecognized directive '...' in lib/rumai/inochi.yaml 161 | + 162 | Thanks to Mattia Gheda for reporting this. 163 | 164 | .Housekeeping 165 | 166 | * Upgrade to Inochi 2.0.0-rc2 for managing this project. 167 | 168 | === Version 3.2.1 (2010-03-22) 169 | 170 | This release improves multi-threading support in Rumai's pure-Ruby 171 | implementation of the <>. 172 | 173 | .Thank you 174 | 175 | * Kenneth De Winter reported the issue of status bar applets not refreshing 176 | according to their prescribed schedule (this is particularly noticable 177 | in the clock applet) and verified my fix for the problem. 178 | 179 | .Bug fixes 180 | 181 | * Perform a blocking I/O read to recieve a 9P2000 message in 182 | `Rumai::IXP::Agent#recv` only if recieve buffer is empty. This gives 183 | other threads a chance to check the recieve buffer for their response. 184 | instead of being blocked by us as we greedily hold on to the 9P2000 185 | message stream until our expected response arrives. 186 | 187 | .Housekeeping 188 | 189 | * Upgrade to Inochi 2.0.0-rc1 and Dfect 2.0.0. 190 | 191 | === Version 3.2.0 (2009-11-17) 192 | 193 | This release adds a new automated view arrangement, simplifies the IXP 194 | transport layer, and cleans up the code and API documentation. 195 | 196 | .New features 197 | 198 | * Add `Rumai::View#arrange_in_stacks` automated view arrangement. 199 | 200 | * Convert `:stack` and `:max` arguments into wmii 3.9 syntax in 201 | `Rumai::Area#layout=`. 202 | 203 | .Bug fixes 204 | 205 | * Rewrote IXP transport layer (`Rumai::IXP::Agent`) to _not_ use a 206 | background thread, according to <>. 207 | 208 | .Housekeeping 209 | 210 | * Clean up some code and API docs. 211 | 212 | * Reduce amount of string concatenation in `Struct#to_9p`. 213 | 214 | === Version 3.1.1 (2009-11-16) 215 | 216 | This release fixes bugs in automated view arrangements and updates the user 217 | manual. 218 | 219 | .Bug fixes 220 | 221 | * The relative order of clients was not being preserved during view 222 | arrangements. 223 | + 224 | Thanks to Nathan Neff for reporting this bug. 225 | 226 | * Focus on the current view was lost after automated view arrangement was 227 | applied if the current view was not the first view on which the initially 228 | focused (before the automated arrangement was applied) client appeared. 229 | 230 | === Version 3.1.0 (2009-10-02) 231 | 232 | This release adds new methods, fixes some bugs, and revises the manual. 233 | 234 | .New features 235 | 236 | * Add `Client#float` methods to manipulate floating status. 237 | 238 | * Add `Client#manage` methods to manipulate managed status. 239 | 240 | * The `Client#tags=` method now accepts '~' and '!' tag prefixes. 241 | 242 | .Bug fixes 243 | 244 | * There is no `View#move_focus` method, only `View#select`. 245 | 246 | * Assertion failure in test suite because all files in `/rbar` 247 | (inside wmii's IXP filesystem) contain an automatic color header when 248 | read. 249 | 250 | .Housekeeping 251 | 252 | * Use simpler Copyright reminder at the top of every file. 253 | 254 | * Open source is for fun, so speak of "related works", not "competitors". 255 | 256 | === Version 3.0.0 (2009-05-11) 257 | 258 | This release revises method names, adds new methods, and fixes a bug. 259 | 260 | .Incompatible changes 261 | 262 | * Rename `#toggle_` methods to use `!` suffix in their names. 263 | 264 | * Rename `#float` methods to `#floating`. 265 | 266 | * Rename `View#floater` method to `View#floating_area`. 267 | 268 | .New features 269 | 270 | * Add `Client#stick` methods to manipulate sticky status. 271 | 272 | * Add `Client#fullscreen` methods to manipulate fullscreen status. 273 | 274 | * Add `Client#slay` method which is a forceful version of `#kill`. 275 | 276 | * Add `View#select` method to move focus relatively inside a view. 277 | 278 | * Add `Area::floating` method for symmetry with `Area::curr`. 279 | 280 | * Add `View#managed_area` aliases for `View#column` methods. 281 | 282 | .Bug fixes 283 | 284 | * Fix error when unzooming clients from temporary view. 285 | 286 | * Fix code that launches temporary terminals in the Tutorial. 287 | + 288 | Use the `/bin/sh` version of the read(1) command for portability. 289 | 290 | .Housekeeping 291 | 292 | * Use `Client#send` instead of `#swap` in automated arrangements because 293 | it causes less traffic on /event/. 294 | 295 | * Add old release notes from blog to user manual. 296 | 297 | === Version 2.1.0 (2009-05-09) 298 | 299 | This release improves client arrangement, fixes several bugs, and cleans up 300 | the code. 301 | 302 | .Thank you 303 | 304 | * Simon Hafner reported several bugs. 305 | * Michael Andrus verified bug fixes. 306 | 307 | .New features 308 | 309 | * Focus is now restored on the initially focused client after applying 310 | automated client arrangements. 311 | 312 | * The `push()`, `insert()`, and `unshift()` instance methods of the 313 | `Rumai::Area` class now preserve the order of inserted clients. 314 | 315 | * The `Rumai::View#arrange_in_grid()` method now accepts 1 as a parameter. 316 | This invocation causes every column to contain at most 1 client. 317 | 318 | .Bug fixes 319 | 320 | * Fix error caused by focusing the top/bottom client in the destination 321 | area before sending new clients into that area. 322 | 323 | * Fix error when importing clients into an empty area. 324 | 325 | .Housekeeping 326 | 327 | * Use snake_case instead of camelCase for variable names. 328 | 329 | * Add copyright notice at the top of every file. 330 | 331 | * Plenty of code formatting and beautification. 332 | 333 | === Version 2.0.2 (2009-02-26) 334 | 335 | This release fixes a connection bug. 336 | 337 | .Bug fixes 338 | 339 | * wmii omits the fractional portion of `$DISPLAY` in its socket file path. 340 | Rumai was trying to connect with the entire `$DISPLAY` value (including 341 | the fractional portion) and thus could not find wmii's socket file. 342 | + 343 | Thanks to Simon Hafner for reporting this bug. 344 | 345 | === Version 2.0.1 (2009-01-25) 346 | 347 | This release simplifies project administrivia using <>, improves the 348 | unit tests, and revises the user manual. 349 | 350 | .Bug fixes 351 | 352 | * The `rumai/ixp/message` library's unit test failed if 353 | `/rbar/status` did not already exist in wmii. 354 | 355 | .Housekeeping 356 | 357 | * Store IXP socket address in `Rumai::IXP_SOCK_ADDR`. 358 | 359 | * Added missing test cases for (TR)create and (TR)remove messages in the 360 | unit test for the `rumai/ixp/message` library. 361 | 362 | === Version 2.0.0 (2008-02-04) 363 | 364 | This release adds support for wmii 3.6, improves the performance of the IXP 365 | library, and fixes some bugs. 366 | 367 | .Thank you 368 | 369 | * Christoph Blank tested Rumai 1.0.0 under wmii 3.6 and reported bugs. 370 | 371 | .Incompatible changes 372 | 373 | * wmii version 3.6 or newer is now required. 374 | 375 | * The `Rumai::IXP::Agent::FidStream#read_partial` method has been replaced 376 | by `Rumai::IXP::Agent::FidStream#read(true)` for efficiency. 377 | 378 | * The `Rumai::IXP::Agent::FidStream#write` method no longer writes to 379 | the beginning of the stream. Instead, it writes to the current position 380 | in the stream. 381 | 382 | * The `Rumai::View#floating_area` method has been renamed to 383 | `Rumai::View#floater` for brevity. 384 | 385 | .New features 386 | 387 | * Added several more methods (such as `rewind`, `pos=`, `eof?`, and so on) 388 | from Ruby's IO class to the `Rumai::IXP::Agent::FidStream` class. 389 | 390 | * Added the `Rumai::Client#kill` method to simplify client termination. 391 | 392 | .Bug fixes 393 | 394 | * Fixed a race condition in `Rumai::Agent#talk` which would cause Rumai to 395 | hang when multiple threads used it. 396 | 397 | === Version 1.0.0 (2008-01-26) 398 | 399 | This is the first release of Rumai, the evolution of 400 | http://article.gmane.org/gmane.comp.window-managers.wmii/1704[wmii-irb], which 401 | lets you manipulate the <> window manager through <>. Enjoy! 402 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | == INSTALL 2 | 3 | === Prerequisites 4 | 5 | * <> 1.8.7 or newer. 6 | 7 | * <> 1.3.6 or newer. 8 | 9 | * <> 3.9 or newer. 10 | 11 | === Installing 12 | 13 | ------------------------------------------------------------------------------ 14 | gem install rumai 15 | ------------------------------------------------------------------------------ 16 | 17 | === Upgrading 18 | 19 | ------------------------------------------------------------------------------ 20 | gem update rumai 21 | ------------------------------------------------------------------------------ 22 | 23 | === Removing 24 | 25 | ------------------------------------------------------------------------------ 26 | gem uninstall rumai 27 | ------------------------------------------------------------------------------ 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (the ISC license) 2 | 3 | Copyright 2006 Suraj N. Kurapati 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /MANUAL: -------------------------------------------------------------------------------- 1 | %+ 'SYNOPSIS' 2 | %+ 'README' 3 | %+ 'INSTALL' 4 | %+ 'USAGE' 5 | %+ 'HACKING' 6 | %+ 'HISTORY' 7 | %+ 'CREDITS' 8 | %+ 'BEYOND' 9 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | == DESCRIPTION 2 | 3 | Rumai is a pure <> interface to the <> window manager. Its name 4 | is a portmanteau of "**Ru**by" and "w**mi**i", which I pronounce as "vim eye". 5 | 6 | === Features 7 | 8 | * Provides an interactive shell for live experimentation. 9 | 10 | * Arranges clients, columns, views, and tags dynamically. 11 | 12 | * Talks directly to wmii's <>. 13 | 14 | * Includes a pure Ruby client for the <>. 15 | 16 | * Powers http://github.com/sunaku/wmiirc[my personal] wmiirc and 17 | http://github.com/sunaku/wmiirc/network[many others] like it. 18 | 19 | === Resources 20 | 21 | Project website:: 22 | <%= @proj_home_url = Rumai::WEBSITE %> 23 | 24 | Announcements feed:: 25 | <%= @ann_feed_url = File.join(@proj_home_url, 'ann.xml') %> 26 | 27 | API documentation:: 28 | <%= @api_docs_url = File.join(@proj_home_url, 'api/') %> 29 | 30 | Source code (browse online, download, or checkout):: 31 | <%= @code_repo_url = 'http://github.com/sunaku/rumai' %> 32 | 33 | Issue tracker (report bugs, request features, get help):: 34 | <%= @bug_track_url = File.join(@code_repo_url, 'issues') %> 35 | -------------------------------------------------------------------------------- /SYNOPSIS: -------------------------------------------------------------------------------- 1 | == SYNOPSIS 2 | 3 | *rumai* ['OPTIONS'] ['IRB_OPTIONS'] 4 | 5 | === Command 6 | 7 | Starts an interactive Ruby shell (IRB) session by passing the given 8 | 'IRB_OPTIONS' to irb(1) and placing you at a command prompt like this: 9 | 10 | ------------------------------------------------------------------------------ 11 | irb(Rumai):001:0> 12 | ------------------------------------------------------------------------------ 13 | 14 | The *irb(Rumai)* token in the command prompt indicates that your commands will 15 | be evaluated _inside_ the `Rumai` module. As a result, you can omit the 16 | "Rumai" prefix from your commands. 17 | 18 | For example, to get the currently selected client, you can type `curr_client` 19 | instead of `Rumai.curr_client` at the prompt. Both commands achieve the same 20 | effect. 21 | 22 | The next thing to notice is that *TAB completion* is enabled. So you can type 23 | part of a command and press the TAB key to see a list of possible completions. 24 | 25 | === Options 26 | 27 | *-h*, *--help*:: 28 | Display this manual and exit. 29 | 30 | *-v*, *--version*:: 31 | Print version number and exit. 32 | -------------------------------------------------------------------------------- /USAGE: -------------------------------------------------------------------------------- 1 | == USAGE 2 | 3 | Now that you know how to start the interactive shell (see **DESCRIPTION** 4 | above) let us walk through a series of examples that highlight the main 5 | features of Rumai. You can follow along by copying & pasting the presented 6 | commands into the interactive shell. 7 | 8 | %|open_terms = lambda do 9 | Launch a few terminals so that we have something to work with: 10 | 11 | [source,ruby] 12 | ---------------------------------------------------------------------------- 13 | colors = %w[ red green blue black orange brown gray navy gold ] 14 | colors.each {|c| system "xterm -bg #{c} -title #{c} -e sh -c read &" } 15 | ---------------------------------------------------------------------------- 16 | 17 | %|close_terms = lambda do 18 | Close the terminals we launched earlier: 19 | 20 | [source,ruby] 21 | ---------------------------------------------------------------------------- 22 | terms = curr_view.clients.select {|c| colors.include? c.label.read } 23 | terms.each {|c| c.kill } 24 | ---------------------------------------------------------------------------- 25 | 26 | === Automated client arrangement 27 | 28 | % open_terms.call 29 | 30 | Arrange all clients in a grid: 31 | 32 | [source,ruby] 33 | ------------------------------------------------------------------------------ 34 | curr_view.arrange_in_grid 35 | ------------------------------------------------------------------------------ 36 | 37 | Arrange all clients in a diamond shape: 38 | 39 | [source,ruby] 40 | ------------------------------------------------------------------------------ 41 | curr_view.arrange_in_diamond 42 | ------------------------------------------------------------------------------ 43 | 44 | Arrange all clients like LarsWM does: 45 | 46 | [source,ruby] 47 | ------------------------------------------------------------------------------ 48 | curr_view.arrange_as_larswm 49 | ------------------------------------------------------------------------------ 50 | 51 | % close_terms.call 52 | 53 | === Multiple client grouping 54 | 55 | % open_terms.call 56 | 57 | Add the red, green, and blue terminals into the "grouping": 58 | 59 | [source,ruby] 60 | ------------------------------------------------------------------------------ 61 | terms = curr_view.clients.select do |c| 62 | %%w[red green blue].include? c.label.read 63 | end 64 | terms.each {|c| c.group } 65 | ------------------------------------------------------------------------------ 66 | 67 | You should now see a new button labelled as "@" on the left-hand side of 68 | wmii's bar, indicating that there is now a new view labelled "@" in wmii. 69 | Let us inspect what clients this mysterious view contains: 70 | 71 | [source,ruby] 72 | ------------------------------------------------------------------------------ 73 | v = View.new "@" 74 | puts v.clients.map {|c| c.label.read } 75 | ------------------------------------------------------------------------------ 76 | 77 | Aha! The mysterious view contains the red, green, and blue clients we 78 | recently "grouped". Thus, by adding a client to the "grouping", we are 79 | simply tagging the client with the "@" token. 80 | 81 | Now that we have put some clients into the "grouping", let us move all 82 | clients in the grouping to the floating area in the current view: 83 | 84 | [source,ruby] 85 | ------------------------------------------------------------------------------ 86 | grouping.each {|c| c.send "toggle" } 87 | ------------------------------------------------------------------------------ 88 | 89 | Neat! Let us bring them back into the managed area: 90 | 91 | [source,ruby] 92 | ------------------------------------------------------------------------------ 93 | grouping.each {|c| c.send "toggle" } 94 | ------------------------------------------------------------------------------ 95 | 96 | % close_terms.call 97 | 98 | In summary, you can select multiple clients (by adding them to the 99 | "grouping") and perform operations on them. This is useful when you want 100 | to do something with a group of clients but do not want to manually focus 101 | one, perform the action, focus the next one, and so on. 102 | 103 | Another important aspect is that selected clients stay selected until they 104 | are unselected. This allows you to continue performing tasks on the 105 | selection without having to reselect the same clients after every 106 | operation. 107 | 108 | === Easy column manipulation 109 | 110 | % open_terms.call 111 | 112 | You can insert a group of clients to the top, bottom, or after the 113 | currently focused client of _any_ column using Array-like methods. 114 | 115 | Give each client its own column (one client per column): 116 | 117 | [source,ruby] 118 | ------------------------------------------------------------------------------ 119 | curr_view.each_column {|c| c.length = 1 } 120 | ------------------------------------------------------------------------------ 121 | 122 | Put (at most) three clients in every column: 123 | 124 | [source,ruby] 125 | ------------------------------------------------------------------------------ 126 | curr_view.each_column {|c| c.length = 3 } 127 | ------------------------------------------------------------------------------ 128 | 129 | Move the red, green, and blue clients into the floating area: 130 | 131 | [source,ruby] 132 | ------------------------------------------------------------------------------ 133 | rgb = %w[red green blue] 134 | terms = curr_view.clients.select {|c| rgb.include? c.label.read } 135 | curr_view.areas[0].push *terms 136 | ------------------------------------------------------------------------------ 137 | 138 | Slurp all floating clients into the last column: 139 | 140 | [source,ruby] 141 | ------------------------------------------------------------------------------ 142 | list = curr_view.areas 143 | a, b = list.first, list.last 144 | b.concat a 145 | ------------------------------------------------------------------------------ 146 | 147 | Set the last column's layout to stacking mode: 148 | 149 | [source,ruby] 150 | ------------------------------------------------------------------------------ 151 | b.layout = 'stack' 152 | ------------------------------------------------------------------------------ 153 | 154 | Move the red, green, and blue clients to the top of the second column: 155 | 156 | [source,ruby] 157 | ------------------------------------------------------------------------------ 158 | curr_view.areas[2].unshift *terms 159 | ------------------------------------------------------------------------------ 160 | 161 | Move the red, green, and blue clients to the bottom of the third column: 162 | 163 | [source,ruby] 164 | ------------------------------------------------------------------------------ 165 | curr_view.areas[3].push *terms 166 | ------------------------------------------------------------------------------ 167 | 168 | % close_terms.call 169 | 170 | === Easy client manipulation 171 | 172 | % open_terms.call 173 | 174 | Obtain a reference to the red client: 175 | 176 | [source,ruby] 177 | ------------------------------------------------------------------------------ 178 | red = curr_view.clients.find {|c| c.label.read == "red" } 179 | ------------------------------------------------------------------------------ 180 | 181 | Show the red client's current tags: 182 | 183 | [source,ruby] 184 | ------------------------------------------------------------------------------ 185 | red.tags 186 | ------------------------------------------------------------------------------ 187 | 188 | Add the "foo" and "bar" tags to the red client: 189 | 190 | [source,ruby] 191 | ------------------------------------------------------------------------------ 192 | red.tag "foo", "bar" 193 | ------------------------------------------------------------------------------ 194 | 195 | Remove the "bar" tag from the red client: 196 | 197 | [source,ruby] 198 | ------------------------------------------------------------------------------ 199 | red.untag "bar" 200 | ------------------------------------------------------------------------------ 201 | 202 | Do complex operations on the red client's tags: 203 | 204 | [source,ruby] 205 | ------------------------------------------------------------------------------ 206 | red.with_tags { concat %w[a b c]; push 'z'; delete 'c' } 207 | ------------------------------------------------------------------------------ 208 | 209 | Focus the next client after the red client: 210 | 211 | [source,ruby] 212 | ------------------------------------------------------------------------------ 213 | red.next.focus 214 | curr_client == red.next #=> true 215 | ------------------------------------------------------------------------------ 216 | 217 | Notice that by focusing a client, we make it the current client. 218 | 219 | Focus the red client on a different view: 220 | 221 | [source,ruby] 222 | ------------------------------------------------------------------------------ 223 | orig = curr_view 224 | v = red.views.last 225 | red.focus v 226 | ------------------------------------------------------------------------------ 227 | 228 | Return to the original view: 229 | 230 | [source,ruby] 231 | ------------------------------------------------------------------------------ 232 | orig.focus 233 | ------------------------------------------------------------------------------ 234 | 235 | Send the red client to the last column: 236 | 237 | [source,ruby] 238 | ------------------------------------------------------------------------------ 239 | red.send curr_view.areas.last 240 | ------------------------------------------------------------------------------ 241 | 242 | % close_terms.call 243 | 244 | === Traversing the file system 245 | 246 | Show the root node of wmii's IXP file system: 247 | 248 | [source,ruby] 249 | ------------------------------------------------------------------------------ 250 | fs 251 | ------------------------------------------------------------------------------ 252 | 253 | Show the names of all files at the root level: 254 | 255 | [source,ruby] 256 | ------------------------------------------------------------------------------ 257 | fs.entries 258 | ------------------------------------------------------------------------------ 259 | 260 | Show the parent of the root node: 261 | 262 | [source,ruby] 263 | ------------------------------------------------------------------------------ 264 | fs.parent 265 | ------------------------------------------------------------------------------ 266 | 267 | Show the children of the root node: 268 | 269 | [source,ruby] 270 | ------------------------------------------------------------------------------ 271 | fs.children 272 | ------------------------------------------------------------------------------ 273 | 274 | Navigate into to the `/lbar/` directory: 275 | 276 | [source,ruby] 277 | ------------------------------------------------------------------------------ 278 | n1 = fs.lbar 279 | n2 = fs['lbar'] 280 | n1 == n2 #=> true 281 | left_bar = n1 282 | ------------------------------------------------------------------------------ 283 | 284 | Notice that you can traverse the file system hierarchy by simply calling 285 | methods on node objects. Alternatively, you can traverse by specifying an 286 | arbitrary sub-path (relative path) using the `[]` operator on a node. 287 | 288 | Create a new temporary button: 289 | 290 | [source,ruby] 291 | ------------------------------------------------------------------------------ 292 | b = left_bar.rumai_example # path of new button 293 | b.exist? #=> false 294 | b.create 295 | b.exist? #=> true 296 | ------------------------------------------------------------------------------ 297 | 298 | You should now see an empty button on the left-hand side of the wmii bar. 299 | 300 | Color the button black-on-white and label it as "hello world": 301 | 302 | [source,ruby] 303 | ------------------------------------------------------------------------------ 304 | content = "colors #000000 #ffffff #000000\nlabel hello world" 305 | b.write content 306 | b.read == content #=> true 307 | ------------------------------------------------------------------------------ 308 | 309 | Remove the temporary button: 310 | 311 | [source,ruby] 312 | ------------------------------------------------------------------------------ 313 | b.remove 314 | b.exist? #=> false 315 | ------------------------------------------------------------------------------ 316 | -------------------------------------------------------------------------------- /bin/rumai: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rumai' 4 | 5 | if ARGV.delete('-h') or ARGV.delete('--help') 6 | system 'man', '-M', File.join(Rumai::INSTDIR, 'man'), 'rumai' or 7 | warn "Could not display the help manual.\nSee #{Rumai::WEBSITE} instead." 8 | exit 9 | elsif ARGV.delete('-v') or ARGV.delete('--version') 10 | puts Rumai::VERSION 11 | exit 12 | end 13 | 14 | require 'rumai/irb' 15 | require 'irb/completion' 16 | IRB.start_session Rumai 17 | -------------------------------------------------------------------------------- /inochi.conf: -------------------------------------------------------------------------------- 1 | ## 2 | # Attributes to pass to AsciiDoc when rendering the HTML manual page. See 3 | # "Backend Attributes" in the AsciiDoc manual for a list of all attributes: 4 | # 5 | # http://www.methods.co.nz/asciidoc/chunked/aph.html 6 | # 7 | :man_asciidoc_attributes: 8 | 9 | ## 10 | # Location where `inochi pub:web` will upload files. This value 11 | # can use any local/remote/protocol syntax supported by rsync(1). 12 | # 13 | :pub_web_target: $HOME/www/lib/rumai/ 14 | 15 | ## 16 | # Options for rsync(1), which uploads files for `inochi pub:web`. 17 | # 18 | :pub_web_options: --verbose --compress --archive --update --delete 19 | 20 | ## 21 | # Additional files for `inochi pub:web` to upload. The values listed 22 | # here can use any local/remote/protocol syntax supported by rsync(1). 23 | # 24 | # @example 25 | # 26 | # :pub_web_extras: 27 | # - some/file 28 | # - some_user@some_host:some/path 29 | # 30 | :pub_web_extras: 31 | 32 | ## 33 | # Arbitrary Ruby code that will configure this project's RubyGem before it 34 | # is built by `inochi gem`. This code has access to a local variable named 35 | # `gem` which holds a Gem::Specification object representing this project. 36 | # 37 | # @example 38 | # 39 | # :gem_spec_logic: | 40 | # # show the Inochi-provided specification for this project's gem 41 | # puts gem.to_ruby 42 | # 43 | # # add files that are outside this project directory to the gem 44 | # gem.files += ['some', 'files', 'in', 'this', 'directory'] 45 | # 46 | # # omit some added files in this project's directory from the gem 47 | # gem.files -= ['lib/top_secret.rb', 'bin/more_top_secret_stuff'] 48 | # 49 | # # and so on... anything is possible! use your imagination! 50 | # 51 | :gem_spec_logic: | 52 | -------------------------------------------------------------------------------- /lib/rumai.rb: -------------------------------------------------------------------------------- 1 | require 'rumai/inochi' 2 | require 'rumai/fs' 3 | require 'rumai/wm' 4 | -------------------------------------------------------------------------------- /lib/rumai/fs.rb: -------------------------------------------------------------------------------- 1 | # File system abstractions over the 9P2000 protocol. 2 | 3 | require 'rumai/ixp' 4 | require 'socket' 5 | 6 | module Rumai 7 | # address of the IXP server socket on this machine 8 | display = ENV['DISPLAY'] || ':0.0' 9 | 10 | IXP_SOCK_ADDR = ENV['WMII_ADDRESS'].sub(/.*!/, '') rescue 11 | "/tmp/ns.#{ENV['USER']}.#{display[/:\d+/]}/wmii" 12 | 13 | begin 14 | # we use a single, global connection to wmii's IXP server 15 | IXP_AGENT = IXP::Agent.new(UNIXSocket.new(IXP_SOCK_ADDR)) 16 | 17 | rescue => error 18 | error.message << %{\n 19 | Ensure that (1) the WMII_ADDRESS environment variable is set and that (2) it 20 | correctly specifies the absolute filesystem path to wmii's IXP socket file, 21 | which is typically located at "/tmp/ns.${USER}.${DISPLAY}/wmii". 22 | \n} 23 | raise error 24 | end 25 | 26 | ## 27 | # An entry in the IXP file system. 28 | # 29 | class Node 30 | attr_reader :path 31 | 32 | def initialize path 33 | @path = path.to_s.squeeze('/') 34 | end 35 | 36 | ## 37 | # Returns file statistics about this node. 38 | # 39 | # @see Rumai::IXP::Agent#stat 40 | # 41 | def stat 42 | IXP_AGENT.stat @path 43 | end 44 | 45 | ## 46 | # Tests if this node exists on the IXP server. 47 | # 48 | def exist? 49 | begin 50 | true if stat 51 | rescue IXP::Error 52 | false 53 | end 54 | end 55 | 56 | ## 57 | # Tests if this node is a directory. 58 | # 59 | # @see Rumai::IXP::Agent#stat 60 | # 61 | def directory? 62 | exist? and stat.directory? 63 | end 64 | 65 | ## 66 | # Returns the names of all files in this directory. 67 | # 68 | # @see Rumai::IXP::Agent#entries 69 | # 70 | def entries 71 | begin 72 | IXP_AGENT.entries @path 73 | rescue IXP::Error 74 | [] 75 | end 76 | end 77 | 78 | ## 79 | # Opens this node for I/O access. 80 | # 81 | # @see Rumai::IXP::Agent#open 82 | # 83 | def open mode = 'r', &block 84 | IXP_AGENT.open @path, mode, &block 85 | end 86 | 87 | ## 88 | # Returns the entire content of this node. 89 | # 90 | # @see Rumai::IXP::Agent#read 91 | # 92 | def read *args 93 | IXP_AGENT.read @path, *args 94 | end 95 | 96 | ## 97 | # Invokes the given block for every line in the content of this node. 98 | # 99 | # @yieldparam [String] line 100 | # 101 | def each_line &block 102 | raise ArgumentError unless block_given? 103 | open do |file| 104 | until (chunk = file.read(true)).empty? 105 | chunk.each_line(&block) 106 | end 107 | end 108 | end 109 | 110 | ## 111 | # Writes the given content to this node. 112 | # 113 | # @see Rumai::IXP::Agent#write 114 | # 115 | def write content 116 | IXP_AGENT.write @path, content 117 | end 118 | 119 | ## 120 | # Creates a file corresponding to this node on the IXP server. 121 | # 122 | # @see Rumai::IXP::Agent#create 123 | # 124 | def create *args 125 | IXP_AGENT.create @path, *args 126 | end 127 | 128 | ## 129 | # Deletes the file corresponding to this node on the IXP server. 130 | # 131 | # @see Rumai::IXP::Agent#remove 132 | # 133 | def remove 134 | IXP_AGENT.remove @path 135 | end 136 | 137 | @@cache = Hash.new {|h,k| h[k] = Node.new(k) } 138 | 139 | ## 140 | # Returns the given sub-path as a Node object. 141 | # 142 | def [] sub_path 143 | @@cache[ File.join(@path, sub_path.to_s) ] 144 | end 145 | 146 | ## 147 | # Returns the parent node of this node. 148 | # 149 | def parent 150 | @@cache[ File.dirname(@path) ] 151 | end 152 | 153 | ## 154 | # Returns all child nodes of this node. 155 | # 156 | def children 157 | entries.map! {|c| self[c] } 158 | end 159 | 160 | include Enumerable 161 | 162 | ## 163 | # Iterates through each child of this directory. 164 | # 165 | def each &block 166 | children.each(&block) 167 | end 168 | 169 | ## 170 | # Deletes all child nodes. 171 | # 172 | def clear 173 | children.each do |c| 174 | c.remove 175 | end 176 | end 177 | 178 | ## 179 | # Provides access to child nodes through method calls. 180 | # 181 | # :call-seq: node.child -> Node 182 | # 183 | def method_missing meth, *args 184 | child = self[meth] 185 | 186 | # speed up future accesses 187 | (class << self; self; end).instance_eval do 188 | define_method meth do 189 | child 190 | end 191 | end 192 | 193 | child 194 | end 195 | end 196 | 197 | ## 198 | # Makes instance methods accessible through class 199 | # methods. This is done to emulate the File class: 200 | # 201 | # File.exist? "foo" 202 | # File.new("foo").exist? 203 | # 204 | # Both of the above expressions are equivalent. 205 | # 206 | module ExportInstanceMethods 207 | def self.extended target # @private 208 | target.instance_methods(false).each do |meth| 209 | (class << target; self; end).instance_eval do 210 | define_method meth do |path, *args| 211 | new(path).__send__(meth, *args) 212 | end 213 | end 214 | end 215 | end 216 | end 217 | 218 | ## 219 | # NOTE: We use extend() **AFTER** all methods have been defined 220 | # in the class so that the ExportInstanceMethods module 221 | # can do its magic. If, instead, we include()d the module 222 | # before all methods in the class had been defined, then 223 | # the magic would only apply to **SOME** of the methods! 224 | # 225 | Node.extend ExportInstanceMethods 226 | end 227 | -------------------------------------------------------------------------------- /lib/rumai/inochi.rb: -------------------------------------------------------------------------------- 1 | module Rumai 2 | 3 | ## 4 | # Official name of this project. 5 | # 6 | PROJECT = 'Rumai' 7 | 8 | ## 9 | # Short single-line description of this project. 10 | # 11 | TAGLINE = 'Ruby interface to the wmii window manager' 12 | 13 | ## 14 | # Address of this project's official home page. 15 | # 16 | WEBSITE = 'http://snk.tuxfamily.org/lib/rumai/' 17 | 18 | ## 19 | # Number of this release of this project. 20 | # 21 | VERSION = '4.1.3' 22 | 23 | ## 24 | # Date of this release of this project. 25 | # 26 | RELDATE = '2011-08-21' 27 | 28 | ## 29 | # Description of this release of this project. 30 | # 31 | def self.inspect 32 | "#{PROJECT} #{VERSION} (#{RELDATE})" 33 | end 34 | 35 | ## 36 | # Location of this release of this project. 37 | # 38 | INSTDIR = File.expand_path('../../..', __FILE__) 39 | 40 | ## 41 | # RubyGems required by this project during runtime. 42 | # 43 | # @example 44 | # 45 | # GEMDEPS = { 46 | # # this project needs exactly version 1.2.3 of the "an_example" gem 47 | # 'an_example' => [ '1.2.3' ], 48 | # 49 | # # this project needs at least version 1.2 (but not 50 | # # version 1.2.4 or newer) of the "another_example" gem 51 | # 'another_example' => [ '>= 1.2' , '< 1.2.4' ], 52 | # 53 | # # this project needs any version of the "yet_another_example" gem 54 | # 'yet_another_example' => [], 55 | # } 56 | # 57 | GEMDEPS = {} 58 | 59 | end 60 | -------------------------------------------------------------------------------- /lib/rumai/irb.rb: -------------------------------------------------------------------------------- 1 | require 'irb' 2 | 3 | module IRB 4 | ## 5 | # Starts an IRB session *inside* the given object. 6 | # 7 | # This code was adapted from a snippet on Massimiliano Mirra's website: 8 | # http://www.therubymine.com/articles/2007/01/29/programmare-dallinterno 9 | # 10 | def self.start_session context 11 | IRB.setup nil 12 | 13 | env = IRB::WorkSpace.new(context) 14 | irb = IRB::Irb.new(env) 15 | IRB.conf[:MAIN_CONTEXT] = irb.context 16 | 17 | catch :IRB_EXIT do 18 | irb.eval_input 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/rumai/ixp.rb: -------------------------------------------------------------------------------- 1 | # An interface to wmii's IXP library for Rumai. 2 | 3 | require 'rumai/ixp/message' 4 | require 'rumai/ixp/transport' 5 | -------------------------------------------------------------------------------- /lib/rumai/ixp/message.rb: -------------------------------------------------------------------------------- 1 | # Primitives for the 9P2000 protocol. 2 | # 3 | # See http://cm.bell-labs.com/sys/man/5/INDEX.html 4 | # See http://swtch.com/plan9port/man/man9/ 5 | 6 | module Rumai 7 | module IXP 8 | # define constants for easier bit manipulation of 9P2000 field values 9 | # uchar (1 byte), ushort (2 bytes), uint32 (4 bytes), uint64 (8 bytes) 10 | 4.times do |n| 11 | bytes = 2 ** n 12 | bits = 8 * bytes 13 | limit = 2 ** bits 14 | mask = limit - 1 15 | 16 | const_set "BYTE#{bytes}_BITS", bits 17 | const_set "BYTE#{bytes}_LIMIT", limit 18 | const_set "BYTE#{bytes}_MASK", mask 19 | end 20 | 21 | ## 22 | # A 9P2000 byte stream. 23 | # 24 | module Stream 25 | # uchar, ushort, uint32 (all of them little-endian) 26 | PACKING_FLAGS = { 1 => 'C', 2 => 'v', 4 => 'V' }.freeze 27 | 28 | ## 29 | # Unpacks the given number of bytes from this 9P2000 byte stream. 30 | # 31 | def read_9p num_bytes 32 | read(num_bytes).unpack(PACKING_FLAGS[num_bytes])[0] 33 | end 34 | end 35 | 36 | ## 37 | # A common container for exceptions concerning IXP. 38 | # 39 | class Error < StandardError 40 | end 41 | 42 | ## 43 | # A serializable 9P2000 data structure. 44 | # 45 | module Struct 46 | attr_reader :fields 47 | 48 | ## 49 | # Allows field values to be initialized via the constructor. 50 | # 51 | # @param field_values 52 | # a mapping from field name to field value 53 | # 54 | def initialize field_values = {} 55 | @fields = self.class.fields 56 | @values = field_values 57 | end 58 | 59 | ## 60 | # Transforms this object into a string of 9P2000 bytes. 61 | # 62 | def to_9p 63 | @fields.map {|f| f.to_9p(@values) }.join 64 | end 65 | 66 | ## 67 | # Populates this object with information 68 | # from the given 9P2000 byte stream. 69 | # 70 | def load_9p stream 71 | @fields.each do |f| 72 | f.load_9p stream, @values 73 | end 74 | end 75 | 76 | ## 77 | # Provides a convenient DSL (for defining fields) 78 | # to all objects which *include* this module. 79 | # 80 | def self.included target 81 | class << target 82 | ## 83 | # Returns a list of fields which compose this Struct. 84 | # 85 | def fields 86 | @fields ||= 87 | if superclass.respond_to? :fields 88 | superclass.fields.dup 89 | else 90 | [] 91 | end 92 | end 93 | 94 | ## 95 | # Defines a new field in this Struct. 96 | # 97 | # @param args 98 | # arguments for {Field.new} 99 | # 100 | def field name, format = nil, *args 101 | klass = Field.factory(format) 102 | field = klass.new(name.to_sym, format, *args) 103 | 104 | # register field as being part of this structure 105 | fields << field 106 | 107 | # provide accessor methods to field values 108 | self.class_eval <<-EOS, __FILE__, __LINE__ 109 | def #{field.name} 110 | @values[#{field.name.inspect}] 111 | end 112 | 113 | def #{field.name}= value 114 | @values[#{field.name.inspect}] = value 115 | end 116 | EOS 117 | 118 | field 119 | end 120 | 121 | ## 122 | # Creates a new instance of this class from the 123 | # given 9P2000 byte stream and returns the instance. 124 | # 125 | def from_9p stream, msg_class = self 126 | msg = msg_class.new 127 | msg.load_9p(stream) 128 | msg 129 | end 130 | end 131 | end 132 | 133 | ## 134 | # A field inside a Struct. 135 | # 136 | # A field's value is considered to be either: 137 | # * array of format when counter && format.is_a? Class 138 | # * raw byte string when counter && format.nil? 139 | # 140 | class Field 141 | attr_reader :name, :format, :counter, :countee 142 | 143 | ## 144 | # @param name 145 | # unique (among all fields in a struct) name for the field 146 | # 147 | # @param format 148 | # number of bytes, a class, or nil 149 | # 150 | # @param [Field] counter 151 | # field which counts the length of this field's value 152 | # 153 | def initialize name, format = nil, counter = nil 154 | @name = name 155 | @format = format 156 | @countee = nil 157 | self.counter = counter 158 | end 159 | 160 | ## 161 | # Sets the counter for this field (implying that the 162 | # length of this field is counted by the given field). 163 | # 164 | def counter= field 165 | if @counter = field 166 | extend CounteeField 167 | @counter.countee = self 168 | end 169 | end 170 | 171 | ## 172 | # Sets the countee for this field (implying that 173 | # this field counts the length of the given field). 174 | # 175 | def countee= field 176 | if @countee = field 177 | extend CounterField 178 | end 179 | end 180 | 181 | ## 182 | # Returns a Field class that best represents the given format. 183 | # 184 | def self.factory format 185 | case format 186 | when String then StringField 187 | when Class then ClassField 188 | when 8 then Integer8Field 189 | else Field 190 | end 191 | end 192 | 193 | ## 194 | # Transforms this object into a string of 9P2000 bytes. 195 | # 196 | def to_9p field_values 197 | value_to_9p field_values[@name] 198 | end 199 | 200 | ## 201 | # Populates this object with information 202 | # taken from the given 9P2000 byte stream. 203 | # 204 | def load_9p stream, field_values 205 | field_values[@name] = value_from_9p stream 206 | end 207 | 208 | private 209 | 210 | ## 211 | # Converts the given value, according to the format 212 | # of this field, into a string of 9P2000 bytes. 213 | # 214 | def value_to_9p value 215 | value.to_i.to_9p @format.to_i 216 | end 217 | 218 | ## 219 | # Parses a value, according to the format of 220 | # this field, from the given 9P2000 byte stream. 221 | # 222 | def value_from_9p stream 223 | stream.read_9p @format.to_i 224 | end 225 | 226 | ## 227 | # Methods for a field that counts the length of another field. 228 | # 229 | module CounterField 230 | def to_9p field_values 231 | value = field_values[@countee.name] 232 | count = 233 | case value 234 | when String then value.bytesize 235 | else value.length 236 | end 237 | value_to_9p count 238 | end 239 | end 240 | 241 | ## 242 | # Methods for a field whose length is counted by another field. 243 | # 244 | module CounteeField 245 | def to_9p field_values 246 | value = field_values[@name] 247 | 248 | if @format 249 | value.map {|v| value_to_9p v}.join 250 | else 251 | value.to_s # raw byte sequence 252 | end 253 | end 254 | 255 | def load_9p stream, field_values 256 | count = field_values[@counter.name].to_i 257 | 258 | field_values[@name] = 259 | if @format 260 | Array.new(count) { value_from_9p stream } 261 | else 262 | stream.read(count) # raw byte sequence 263 | end 264 | end 265 | end 266 | end 267 | 268 | ## 269 | # A field whose value knows how to convert itself to and from 9p. 270 | # 271 | class ClassField < Field 272 | def value_to_9p value 273 | value.to_9p 274 | end 275 | 276 | def value_from_9p stream 277 | @format.from_9p stream 278 | end 279 | end 280 | 281 | ## 282 | # A field whose value is a string. 283 | # 284 | class StringField < ClassField 285 | def value_to_9p value 286 | value.to_s.to_9p 287 | end 288 | end 289 | 290 | ## 291 | # A field whose value is a 8-byte integer. 292 | # 293 | class Integer8Field < Field 294 | def value_to_9p value 295 | v = value.to_i 296 | (BYTE4_MASK & v).to_9p(4) << # lower bytes 297 | (BYTE4_MASK & (v >> BYTE4_BITS)).to_9p(4) # higher bytes 298 | end 299 | 300 | def value_from_9p stream 301 | stream.read_9p(4) | (stream.read_9p(4) << BYTE4_BITS) 302 | end 303 | end 304 | end 305 | 306 | ## 307 | # Holds information about a file being accessed on a 9P2000 server. 308 | # 309 | # See http://cm.bell-labs.com/magic/man2html/5/intro 310 | # 311 | class Qid 312 | include Struct 313 | 314 | # type[1] version[4] path[8] 315 | field :type , 1 316 | field :version , 4 317 | field :path , 8 318 | 319 | ## 320 | # The following constant definitions originate from: 321 | # http://swtch.com/usr/local/plan9/include/libc.h 322 | # 323 | QTDIR = 0x80 # type bit for directories 324 | QTAPPEND = 0x40 # type bit for append only files 325 | QTEXCL = 0x20 # type bit for exclusive use files 326 | QTMOUNT = 0x10 # type bit for mounted channel 327 | QTAUTH = 0x08 # type bit for authentication file 328 | QTTMP = 0x04 # type bit for non-backed-up file 329 | QTSYMLINK = 0x02 # type bit for symbolic link 330 | QTFILE = 0x00 # type bits for plain file 331 | end 332 | 333 | ## 334 | # Holds information about a file on a 9P2000 server. 335 | # 336 | # See http://cm.bell-labs.com/magic/man2html/5/stat 337 | # 338 | class Stat 339 | include Struct 340 | 341 | field :size , 2 342 | field :type , 2 343 | field :dev , 4 344 | field :qid , Qid 345 | field :mode , 4 346 | field :atime , Time 347 | field :mtime , Time 348 | field :length , 8 349 | field :name , String 350 | field :uid , String 351 | field :gid , String 352 | field :muid , String 353 | 354 | ## 355 | # The following constant definitions originate from: 356 | # http://swtch.com/usr/local/plan9/include/libc.h 357 | # 358 | DMDIR = 0x80000000 # mode bit for directories 359 | DMAPPEND = 0x40000000 # mode bit for append only files 360 | DMEXCL = 0x20000000 # mode bit for exclusive use files 361 | DMMOUNT = 0x10000000 # mode bit for mounted channel 362 | DMAUTH = 0x08000000 # mode bit for authentication file 363 | DMTMP = 0x04000000 # mode bit for non-backed-up file 364 | DMSYMLINK = 0x02000000 # mode bit for symbolic link (Unix, 9P2000.u) 365 | DMDEVICE = 0x00800000 # mode bit for device file (Unix, 9P2000.u) 366 | DMNAMEDPIPE = 0x00200000 # mode bit for named pipe (Unix, 9P2000.u) 367 | DMSOCKET = 0x00100000 # mode bit for socket (Unix, 9P2000.u) 368 | DMSETUID = 0x00080000 # mode bit for setuid (Unix, 9P2000.u) 369 | DMSETGID = 0x00040000 # mode bit for setgid (Unix, 9P2000.u) 370 | DMREAD = 0x4 # mode bit for read permission 371 | DMWRITE = 0x2 # mode bit for write permission 372 | DMEXEC = 0x1 # mode bit for execute permission 373 | 374 | ## 375 | # Tests if this file is a directory. 376 | # 377 | def directory? 378 | mode & DMDIR > 0 379 | end 380 | end 381 | 382 | ## 383 | # Fcall is the basic unit of communication in the 9P2000 protocol. 384 | # It is analogous to a "packet" in the Internetwork Protocol (IP). 385 | # 386 | # See http://cm.bell-labs.com/magic/man2html/2/fcall 387 | # 388 | class Fcall 389 | include Struct 390 | 391 | # The first two fields are disabled because they are automatically 392 | # calculated by the Fcall#to_9p and Fcall::from_9p methods below: 393 | # 394 | # field :size , 4 # disabled 395 | # field :type , 1 # disabled 396 | # 397 | field :tag , 2 398 | 399 | ## 400 | # Transforms this object into a string of 9P2000 bytes. 401 | # 402 | def to_9p 403 | data = type.to_9p(1) << super 404 | size = (data.bytesize + 4).to_9p(4) 405 | size << data 406 | end 407 | 408 | class << self 409 | alias __from_9p__ from_9p 410 | undef from_9p 411 | 412 | ## 413 | # Creates a new instance of this class from the 414 | # given 9P2000 byte stream and returns the instance. 415 | # 416 | def from_9p stream 417 | size = stream.read_9p(4) 418 | type = stream.read_9p(1) 419 | 420 | unless fcall = TYPE_TO_CLASS[type] 421 | raise Error, "illegal fcall type: #{type}" 422 | end 423 | 424 | __from_9p__ stream, fcall 425 | end 426 | end 427 | 428 | NOTAG = BYTE2_MASK # (ushort) 429 | NOFID = BYTE4_MASK # (uint32) 430 | end 431 | 432 | # size[4] Tversion tag[2] msize[4] version[s] 433 | class Tversion < Fcall 434 | field :msize , 4 435 | field :version , String 436 | 437 | VERSION = '9P2000'.freeze 438 | MSIZE = 8192 # magic number defined in Plan9 439 | # for [TR]version and [TR]read 440 | end 441 | 442 | # size[4] Rversion tag[2] msize[4] version[s] 443 | class Rversion < Fcall 444 | field :msize , 4 445 | field :version , String 446 | end 447 | 448 | # size[4] Tauth tag[2] afid[4] uname[s] aname[s] 449 | class Tauth < Fcall 450 | field :afid , 4 451 | field :uname , String 452 | field :aname , String 453 | end 454 | 455 | # size[4] Rauth tag[2] aqid[13] 456 | class Rauth < Fcall 457 | field :aqid , Qid 458 | end 459 | 460 | # size[4] Tattach tag[2] fid[4] afid[4] uname[s] aname[s] 461 | class Tattach < Fcall 462 | field :fid , 4 463 | field :afid , 4 464 | field :uname , String 465 | field :aname , String 466 | end 467 | 468 | # size[4] Rattach tag[2] qid[13] 469 | class Rattach < Fcall 470 | field :qid , Qid 471 | end 472 | 473 | # illegal 474 | class Terror < Fcall 475 | def to_9p 476 | raise Error, 'the Terror fcall cannot be transmitted' 477 | end 478 | end 479 | 480 | # size[4] Rerror tag[2] ename[s] 481 | class Rerror < Fcall 482 | field :ename , String 483 | end 484 | 485 | # size[4] Tflush tag[2] oldtag[2] 486 | class Tflush < Fcall 487 | field :oldtag , 2 488 | end 489 | 490 | # size[4] Rflush tag[2] 491 | class Rflush < Fcall 492 | end 493 | 494 | # size[4] Twalk tag[2] fid[4] newfid[4] nwname[2] nwname*(wname[s]) 495 | class Twalk < Fcall 496 | field :fid , 4 497 | field :newfid , 4 498 | c = field :nwname , 2 499 | field :wname , String , c 500 | end 501 | 502 | # size[4] Rwalk tag[2] nwqid[2] nwqid*(wqid[13]) 503 | class Rwalk < Fcall 504 | c = field :nwqid , 2 505 | field :wqid , Qid , c 506 | end 507 | 508 | # size[4] Topen tag[2] fid[4] mode[1] 509 | class Topen < Fcall 510 | field :fid , 4 511 | field :mode , 1 512 | 513 | ## 514 | # The following constant definitions originate from: 515 | # http://swtch.com/usr/local/plan9/include/libc.h 516 | # 517 | OREAD = 0 # open for read 518 | OWRITE = 1 # write 519 | ORDWR = 2 # read and write 520 | OEXEC = 3 # execute, == read but check execute permission 521 | OTRUNC = 16 # or'ed in (except for exec), truncate file first 522 | OCEXEC = 32 # or'ed in, close on exec 523 | ORCLOSE = 64 # or'ed in, remove on close 524 | ODIRECT = 128 # or'ed in, direct access 525 | ONONBLOCK = 256 # or'ed in, non-blocking call 526 | OEXCL = 0x1000 # or'ed in, exclusive use (create only) 527 | OLOCK = 0x2000 # or'ed in, lock after opening 528 | OAPPEND = 0x4000 # or'ed in, append only 529 | end 530 | 531 | # size[4] Ropen tag[2] qid[13] iounit[4] 532 | class Ropen < Fcall 533 | field :qid , Qid 534 | field :iounit , 4 535 | end 536 | 537 | # size[4] Tcreate tag[2] fid[4] name[s] perm[4] mode[1] 538 | class Tcreate < Fcall 539 | field :fid , 4 540 | field :name , String 541 | field :perm , 4 542 | field :mode , 1 543 | end 544 | 545 | # size[4] Rcreate tag[2] qid[13] iounit[4] 546 | class Rcreate < Fcall 547 | field :qid , Qid 548 | field :iounit , 4 549 | end 550 | 551 | # size[4] Tread tag[2] fid[4] offset[8] count[4] 552 | class Tread < Fcall 553 | field :fid , 4 554 | field :offset , 8 555 | field :count , 4 556 | end 557 | 558 | # size[4] Rread tag[2] count[4] data[count] 559 | class Rread < Fcall 560 | c = field :count , 4 561 | field :data , nil , c 562 | end 563 | 564 | # size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count] 565 | class Twrite < Fcall 566 | field :fid , 4 567 | field :offset , 8 568 | c = field :count , 4 569 | field :data , nil , c 570 | end 571 | 572 | # size[4] Rwrite tag[2] count[4] 573 | class Rwrite < Fcall 574 | field :count , 4 575 | end 576 | 577 | # size[4] Tclunk tag[2] fid[4] 578 | class Tclunk < Fcall 579 | field :fid , 4 580 | end 581 | 582 | # size[4] Rclunk tag[2] 583 | class Rclunk < Fcall 584 | end 585 | 586 | # size[4] Tremove tag[2] fid[4] 587 | class Tremove < Fcall 588 | field :fid , 4 589 | end 590 | 591 | # size[4] Rremove tag[2] 592 | class Rremove < Fcall 593 | end 594 | 595 | # size[4] Tstat tag[2] fid[4] 596 | class Tstat < Fcall 597 | field :fid , 4 598 | end 599 | 600 | # size[4] Rstat tag[2] stat[n] 601 | class Rstat < Fcall 602 | field :nstat , 2 603 | field :stat , Stat 604 | end 605 | 606 | # size[4] Twstat tag[2] fid[4] stat[n] 607 | class Twstat < Fcall 608 | field :fid , 4 609 | field :nstat , 2 610 | field :stat , Stat 611 | end 612 | 613 | # size[4] Rwstat tag[2] 614 | class Rwstat < Fcall 615 | end 616 | 617 | ## 618 | # A remote function call (fcall). 619 | # 620 | class Fcall 621 | CLASS_TO_TYPE = { 622 | Tversion => 100, 623 | Rversion => 101, 624 | Tauth => 102, 625 | Rauth => 103, 626 | Tattach => 104, 627 | Rattach => 105, 628 | Terror => 106, 629 | Rerror => 107, 630 | Tflush => 108, 631 | Rflush => 109, 632 | Twalk => 110, 633 | Rwalk => 111, 634 | Topen => 112, 635 | Ropen => 113, 636 | Tcreate => 114, 637 | Rcreate => 115, 638 | Tread => 116, 639 | Rread => 117, 640 | Twrite => 118, 641 | Rwrite => 119, 642 | Tclunk => 120, 643 | Rclunk => 121, 644 | Tremove => 122, 645 | Rremove => 123, 646 | Tstat => 124, 647 | Rstat => 125, 648 | Twstat => 126, 649 | Rwstat => 127, 650 | }.freeze 651 | 652 | TYPE_TO_CLASS = CLASS_TO_TYPE.invert.freeze 653 | 654 | ## 655 | # Returns the value of the 'type' field for this fcall. 656 | # 657 | def self.type 658 | CLASS_TO_TYPE[self] 659 | end 660 | 661 | ## 662 | # Returns the value of the 'type' field for this fcall. 663 | # 664 | def type 665 | self.class.type 666 | end 667 | end 668 | end 669 | end 670 | 671 | class Integer 672 | ## 673 | # Transforms this object into a string of 9P2000 bytes. 674 | # 675 | def to_9p num_bytes 676 | [self].pack Rumai::IXP::Stream::PACKING_FLAGS[num_bytes] 677 | end 678 | end 679 | 680 | # count[2] s[count] 681 | class String 682 | ## 683 | # Transforms this object into a string of 9P2000 bytes. 684 | # 685 | def to_9p 686 | count = [bytesize, Rumai::IXP::BYTE2_MASK].min 687 | count.to_9p(2) << byteslice(0, count) 688 | end 689 | 690 | ## 691 | # Creates a new instance of this class from the 692 | # given 9P2000 byte stream and returns the instance. 693 | # 694 | def self.from_9p stream 695 | stream.read stream.read_9p(2) 696 | end 697 | 698 | unless method_defined? :byteslice 699 | ## 700 | # Does the same thing as String#slice but 701 | # operates on bytes instead of characters. 702 | # 703 | def byteslice(*args) 704 | unpack('C*').slice(*args).pack('C*') 705 | end 706 | end 707 | end 708 | 709 | class Time 710 | ## 711 | # Transforms this object into a string of 9P2000 bytes. 712 | # 713 | def to_9p 714 | to_i.to_9p(4) 715 | end 716 | 717 | ## 718 | # Creates a new instance of this class from the 719 | # given 9P2000 byte stream and returns the instance. 720 | # 721 | def self.from_9p stream 722 | at stream.read_9p(4) 723 | end 724 | end 725 | 726 | class IO 727 | include Rumai::IXP::Stream 728 | end 729 | 730 | require 'stringio' 731 | class StringIO 732 | include Rumai::IXP::Stream 733 | end 734 | -------------------------------------------------------------------------------- /lib/rumai/ixp/transport.rb: -------------------------------------------------------------------------------- 1 | # Transport layer for 9P2000 protocol. 2 | 3 | require 'rumai/ixp/message' 4 | require 'thread' # for Mutex and Queue 5 | 6 | module Rumai 7 | module IXP 8 | ## 9 | # A thread-safe channel that multiplexes many 10 | # threads onto a single 9P2000 connection. 11 | # 12 | # The send/recv implementation is based on the XCB cookie approach: 13 | # http://www.x.org/releases/X11R7.5/doc/libxcb/tutorial/#requestsreplies 14 | # 15 | class Agent 16 | attr_reader :msize 17 | 18 | ## 19 | # @param stream 20 | # I/O stream on which a 9P2000 server is listening. 21 | # 22 | def initialize stream 23 | @stream = stream 24 | 25 | @recv_buf = {} # tag => message 26 | @recv_lock = Mutex.new 27 | 28 | @tag_pool = RangedPool.new(0...BYTE2_MASK) 29 | @fid_pool = RangedPool.new(0...BYTE4_MASK) 30 | 31 | # establish connection with 9P2000 server 32 | req = Tversion.new( 33 | :tag => Fcall::NOTAG, 34 | :msize => Tversion::MSIZE, 35 | :version => Tversion::VERSION 36 | ) 37 | rsp = talk(req) 38 | 39 | unless req.version == rsp.version 40 | raise Error, "protocol mismatch: self=#{req.version.inspect} server=#{rsp.version.inspect}" 41 | end 42 | 43 | @msize = rsp.msize 44 | 45 | # authenticate the connection (not necessary for wmii) 46 | @auth_fid = Fcall::NOFID 47 | 48 | # attach to filesystem root 49 | @root_fid = @fid_pool.obtain 50 | attach @root_fid, @auth_fid 51 | end 52 | 53 | ## 54 | # A finite, thread-safe pool of range members. 55 | # 56 | class RangedPool 57 | # how many new members should be added 58 | # to the pool when the pool is empty? 59 | FILL_RATE = 10 60 | 61 | def initialize range 62 | @pos = range.first 63 | @lim = range.last 64 | @lim = @lim.succ unless range.exclude_end? 65 | 66 | @pool = Queue.new 67 | end 68 | 69 | ## 70 | # Returns an unoccupied range member from the pool. 71 | # 72 | def obtain 73 | begin 74 | @pool.deq true 75 | 76 | rescue ThreadError 77 | # pool is empty, so fill it 78 | FILL_RATE.times do 79 | if @pos != @lim then 80 | @pool.enq @pos 81 | @pos = @pos.succ 82 | else 83 | # range is exhausted, so give other threads 84 | # a chance to fill the pool before retrying 85 | Thread.pass 86 | break 87 | end 88 | end 89 | 90 | retry 91 | end 92 | end 93 | 94 | ## 95 | # Marks the given member as being unoccupied so 96 | # that it may be occupied again in the future. 97 | # 98 | def release member 99 | @pool.enq member 100 | end 101 | end 102 | 103 | ## 104 | # Sends the given request {Rumai::IXP::Fcall} and returns 105 | # a ticket that you can use later to receive the reply. 106 | # 107 | def send request 108 | tag = @tag_pool.obtain 109 | 110 | request.tag = tag 111 | @stream << request.to_9p 112 | 113 | tag 114 | end 115 | 116 | ## 117 | # Returns the reply for the given ticket, which was previously given 118 | # to you when you sent the corresponding request {Rumai::IXP::Fcall}. 119 | # 120 | def recv tag 121 | loop do 122 | reply = @recv_lock.synchronize do 123 | if @recv_buf.key? tag 124 | @recv_buf.delete tag 125 | else 126 | # reply was not in receive buffer, so read 127 | # the next reply... hoping that it is ours 128 | 129 | next_reply_available = 130 | @recv_buf.empty? || begin 131 | # check (in a non-blocking fashion) if 132 | # the stream has reply for us right now 133 | @stream.ungetc @stream.read_nonblock(1).unpack('C').first 134 | true 135 | rescue Errno::EAGAIN 136 | # the stream is empty 137 | end 138 | 139 | if next_reply_available 140 | msg = Fcall.from_9p(@stream) 141 | 142 | if msg.tag == tag 143 | msg 144 | else 145 | # we got someone else's reply, so buffer 146 | # it (for them to receive) and try again 147 | @recv_buf[msg.tag] = msg 148 | nil 149 | end 150 | end 151 | end 152 | end 153 | 154 | if reply 155 | @tag_pool.release tag 156 | 157 | if reply.is_a? Rerror 158 | raise Error, reply.ename 159 | end 160 | 161 | return reply 162 | else 163 | # give other threads a chance to receive 164 | Thread.pass 165 | end 166 | end 167 | end 168 | 169 | ## 170 | # Sends the given request {Rumai::IXP::Fcall} and returns its reply. 171 | # 172 | def talk request 173 | tag = send(request) 174 | 175 | begin 176 | recv tag 177 | rescue Error => e 178 | e.message << " -- in reply to #{request.inspect}" 179 | raise 180 | end 181 | end 182 | 183 | MODES = { 184 | 'r' => Topen::OREAD, 185 | 'w' => Topen::OWRITE, 186 | 't' => Topen::ORCLOSE, 187 | '+' => Topen::ORDWR, 188 | } 189 | 190 | ## 191 | # Converts the given mode string into an integer. 192 | # 193 | def MODES.parse mode 194 | if mode.respond_to? :split 195 | mode.split(//).inject(0) {|acc,chr| acc | self[chr].to_i } 196 | else 197 | mode.to_i 198 | end 199 | end 200 | 201 | ## 202 | # Opens the given path for I/O access through a {FidStream} 203 | # object. If a block is given, it is invoked with a 204 | # {FidStream} object and the stream is closed afterwards. 205 | # 206 | # @see File::open 207 | # 208 | def open path, mode = 'r' 209 | # open the file 210 | path_fid = walk(path) 211 | talk Topen.new( 212 | :fid => path_fid, 213 | :mode => MODES.parse(mode) 214 | ) 215 | stream = FidStream.new(self, path_fid, @msize) 216 | 217 | # return the file stream 218 | if block_given? 219 | begin 220 | yield stream 221 | ensure 222 | stream.close 223 | end 224 | else 225 | stream 226 | end 227 | end 228 | 229 | ## 230 | # Encapsulates I/O access over a file handle (fid). 231 | # 232 | # @note this class is NOT thread safe! 233 | # 234 | class FidStream 235 | attr_reader :fid, :stat 236 | 237 | attr_reader :eof 238 | alias eof? eof 239 | 240 | attr_accessor :pos 241 | alias tell pos 242 | 243 | def initialize agent, path_fid, message_size 244 | @agent = agent 245 | @fid = path_fid 246 | @msize = message_size 247 | @stat = @agent.stat_fid(@fid) 248 | @closed = false 249 | rewind 250 | end 251 | 252 | ## 253 | # Rewinds the stream to the beginning. 254 | # 255 | def rewind 256 | @pos = 0 257 | @eof = false 258 | end 259 | 260 | ## 261 | # Closes this stream. 262 | # 263 | def close 264 | unless @closed 265 | @agent.clunk @fid 266 | @closed = true 267 | @eof = true 268 | end 269 | end 270 | 271 | ## 272 | # Returns true if this stream is closed. 273 | # 274 | def closed? 275 | @closed 276 | end 277 | 278 | ## 279 | # Reads some data from this stream at the current position. 280 | # If this stream corresponds to a directory, then an Array of 281 | # Stat (one for each file in the directory) will be returned. 282 | # 283 | # @param [Boolean] partial 284 | # 285 | # When false, the entire content of 286 | # this stream is read and returned. 287 | # 288 | # When true, the maximum amount of content that can fit 289 | # inside a single 9P2000 message is read and returned. 290 | # 291 | def read partial = false 292 | raise 'cannot read from a closed stream' if @closed 293 | 294 | content = '' 295 | begin 296 | req = Tread.new( 297 | :fid => @fid, 298 | :offset => @pos, 299 | :count => @msize 300 | ) 301 | rsp = @agent.talk(req) 302 | 303 | content << rsp.data 304 | count = rsp.count 305 | @pos += count 306 | end until @eof = count.zero? or partial 307 | 308 | # the content of a directory is a sequence 309 | # of Stat for all files in that directory 310 | if @stat.directory? 311 | buffer = StringIO.new(content) 312 | content = [] 313 | 314 | until buffer.eof? 315 | content << Stat.from_9p(buffer) 316 | end 317 | end 318 | 319 | content 320 | end 321 | 322 | ## 323 | # Writes the given content at the current position in this stream. 324 | # 325 | def write content 326 | raise 'cannot write to a closed stream' if @closed 327 | raise 'cannot write to a directory' if @stat.directory? 328 | 329 | data = content.to_s 330 | limit = data.bytesize + @pos 331 | 332 | while @pos < limit 333 | chunk = data.byteslice(@pos, @msize) 334 | 335 | req = Twrite.new( 336 | :fid => @fid, 337 | :offset => @pos, 338 | :count => chunk.bytesize, 339 | :data => chunk 340 | ) 341 | rsp = @agent.talk(req) 342 | 343 | @pos += rsp.count 344 | end 345 | end 346 | 347 | alias << write 348 | end 349 | 350 | ## 351 | # Returns the content of the file/directory at the given path. 352 | # 353 | def read path, *args 354 | open(path) {|f| f.read(*args) } 355 | end 356 | 357 | ## 358 | # Returns the basenames of all files 359 | # inside the directory at the given path. 360 | # 361 | # @see Dir::entries 362 | # 363 | def entries path 364 | unless stat(path).directory? 365 | raise ArgumentError, "#{path.inspect} is not a directory" 366 | end 367 | 368 | read(path).map! {|t| t.name } 369 | end 370 | 371 | ## 372 | # Writes the given content to 373 | # the file at the given path. 374 | # 375 | def write path, content 376 | open(path, 'w') {|f| f.write content } 377 | end 378 | 379 | ## 380 | # Creates a new file at the given path that is accessible using 381 | # the given modes for a user having the given permission bits. 382 | # 383 | def create path, mode = 'rw', perm = 0644 384 | prefix = File.dirname(path) 385 | target = File.basename(path) 386 | 387 | with_fid do |prefix_fid| 388 | walk_fid prefix_fid, prefix 389 | 390 | # create the file 391 | talk Tcreate.new( 392 | :fid => prefix_fid, 393 | :name => target, 394 | :perm => perm, 395 | :mode => MODES.parse(mode) 396 | ) 397 | end 398 | end 399 | 400 | ## 401 | # Deletes the file at the given path. 402 | # 403 | def remove path 404 | path_fid = walk(path) 405 | remove_fid path_fid # remove also does clunk 406 | end 407 | 408 | ## 409 | # Deletes the file corresponding to the 410 | # given FID and clunks the given FID. 411 | # 412 | def remove_fid path_fid 413 | talk Tremove.new(:fid => path_fid) 414 | end 415 | 416 | ## 417 | # Returns information about the file at the given path. 418 | # 419 | def stat path 420 | with_fid do |path_fid| 421 | walk_fid path_fid, path 422 | stat_fid path_fid 423 | end 424 | end 425 | 426 | ## 427 | # Returns information about the file referenced by the given FID. 428 | # 429 | def stat_fid path_fid 430 | req = Tstat.new(:fid => path_fid) 431 | rsp = talk(req) 432 | rsp.stat 433 | end 434 | 435 | ## 436 | # Returns an FID corresponding to the given path. 437 | # 438 | def walk path 439 | fid = @fid_pool.obtain 440 | walk_fid fid, path 441 | fid 442 | end 443 | 444 | ## 445 | # Associates the given FID to the given path. 446 | # 447 | def walk_fid path_fid, path 448 | talk Twalk.new( 449 | :fid => @root_fid, 450 | :newfid => path_fid, 451 | :wname => path.to_s.split(%r{/+}).reject {|s| s.empty? } 452 | ) 453 | end 454 | 455 | ## 456 | # Associates the given FID with the FS root. 457 | # 458 | def attach root_fid, auth_fid = Fcall::NOFID, auth_name = ENV['USER'] 459 | talk Tattach.new( 460 | :fid => root_fid, 461 | :afid => auth_fid, 462 | :uname => ENV['USER'], 463 | :aname => auth_name 464 | ) 465 | end 466 | 467 | ## 468 | # Retires the given FID from use. 469 | # 470 | def clunk fid 471 | talk Tclunk.new(:fid => fid) 472 | @fid_pool.release fid 473 | end 474 | 475 | private 476 | 477 | ## 478 | # Invokes the given block with a temporary FID. 479 | # 480 | def with_fid 481 | begin 482 | fid = @fid_pool.obtain 483 | yield fid 484 | ensure 485 | clunk fid 486 | end 487 | end 488 | end 489 | end 490 | end 491 | -------------------------------------------------------------------------------- /lib/rumai/wm.rb: -------------------------------------------------------------------------------- 1 | # Abstractions for the window manager. 2 | 3 | require 'rumai/fs' 4 | require 'enumerator' 5 | 6 | class Object # @private 7 | # prevent these deprecated properties 8 | # from clashing with our usage below 9 | undef id if respond_to? :id 10 | undef type if respond_to? :type 11 | end 12 | 13 | module Rumai 14 | IXP_FS_ROOT = Node.new('/') 15 | FOCUSED_WIDGET_ID = 'sel'.freeze 16 | FLOATING_AREA_ID = '~'.freeze 17 | CLIENT_GROUPING_TAG = '@'.freeze 18 | CLIENT_STICKY_TAG = '/./'.freeze 19 | 20 | #--------------------------------------------------------------------------- 21 | # abstraction of WM components 22 | #--------------------------------------------------------------------------- 23 | 24 | ## 25 | # @note Inheritors must override the {Chain#chain} method. 26 | # 27 | module Chain 28 | ## 29 | # Returns an array of objects related to this one. 30 | # 31 | def chain 32 | [self] 33 | end 34 | 35 | ## 36 | # Returns the object before this one in the chain. 37 | # 38 | def prev 39 | Chain.prev chain, self 40 | end 41 | 42 | ## 43 | # Returns the object after this one in the chain. 44 | # 45 | def next 46 | Chain.next chain, self 47 | end 48 | 49 | ## 50 | # Fetches the object that comes before the 51 | # given target object in the given array. 52 | # 53 | def self.prev array, target 54 | near array, target, -1 55 | end 56 | 57 | ## 58 | # Fetches the object that comes after the 59 | # given target object in the given array. 60 | # 61 | def self.next array, target 62 | near array, target, +1 63 | end 64 | 65 | ## 66 | # Fetches the object at the given offset from 67 | # the given target object in the given array. 68 | # 69 | def self.near array, target, offset 70 | if index = array.index(target) 71 | array[(index + offset) % array.length] 72 | end 73 | end 74 | end 75 | 76 | ## 77 | # The basic building block of the WM hierarchy. 78 | # 79 | # @note Inheritors must define a {curr} class method. 80 | # @note Inheritors must override the {focus} method. 81 | # 82 | module WidgetImpl 83 | attr_reader :id 84 | 85 | def == other 86 | @id == other.id 87 | end 88 | 89 | ## 90 | # Checks if this widget currently has focus. 91 | # 92 | def current? 93 | self == self.class.curr 94 | end 95 | 96 | alias focus? current? 97 | end 98 | 99 | ## 100 | # A widget that has a corresponding representation in the IXP file system. 101 | # 102 | class WidgetNode < Node 103 | include WidgetImpl 104 | 105 | def initialize id, path_prefix 106 | super "#{path_prefix}/#{id}" 107 | 108 | if id == FOCUSED_WIDGET_ID and ctl.exist? 109 | @id = ctl.read.split.first 110 | super "#{path_prefix}/#{@id}" 111 | else 112 | @id = id.to_s 113 | end 114 | end 115 | end 116 | 117 | ## 118 | # A graphical program that is running in your current X Windows session. 119 | # 120 | class Client < WidgetNode 121 | def initialize client_id 122 | super client_id, '/client' 123 | end 124 | 125 | ## 126 | # Returns the currently focused client. 127 | # 128 | def self.curr 129 | new FOCUSED_WIDGET_ID 130 | end 131 | 132 | #------------------------------------------------------------------------- 133 | include Chain 134 | #------------------------------------------------------------------------- 135 | 136 | ## 137 | # Returns a list of all clients in the current view. 138 | # 139 | def chain 140 | View.curr.clients 141 | end 142 | 143 | #------------------------------------------------------------------------- 144 | # WM operations 145 | #------------------------------------------------------------------------- 146 | 147 | ## 148 | # Focuses this client within the given view. 149 | # 150 | def focus view = nil 151 | if exist? and not focus? 152 | (view ? [view] : self.views).each do |v| 153 | if a = self.area(v) and a.exist? 154 | v.focus 155 | a.focus 156 | 157 | # slide focus from the current client onto this client 158 | arr = a.client_ids 159 | src = arr.index Client.curr.id 160 | dst = arr.index @id 161 | 162 | distance = (src - dst).abs 163 | direction = src < dst ? :down : :up 164 | 165 | distance.times { v.select direction } 166 | break 167 | end 168 | end 169 | end 170 | end 171 | 172 | ## 173 | # Sends this client to the given destination within the given view. 174 | # 175 | def send area_or_id, view = View.curr 176 | dst = area_to_id(area_or_id) 177 | view.ctl.write "send #{@id} #{dst}" 178 | end 179 | 180 | alias move send 181 | 182 | ## 183 | # Swaps this client with the given destination within the given view. 184 | # 185 | def swap area_or_id, view = View.curr 186 | dst = area_to_id(area_or_id) 187 | view.ctl.write "swap #{@id} #{dst}" 188 | end 189 | 190 | ## 191 | # Moves this client by the given amount in 192 | # the given direction on the given view. 193 | # 194 | def nudge direction, amount = 1, view = View.curr 195 | reshape :nudge, view, direction, amount 196 | end 197 | 198 | ## 199 | # Grows this client by the given amount in 200 | # the given direction on the given view. 201 | # 202 | def grow direction, amount = 1, view = View.curr 203 | reshape :grow, view, direction, amount 204 | end 205 | 206 | ## 207 | # Shrinks this client by the given amount 208 | # in the given direction on the given view. 209 | # 210 | def shrink direction, amount = 1, view = View.curr 211 | reshape :grow, view, direction, -amount.to_i 212 | end 213 | 214 | ## 215 | # Terminates this client nicely (requests this window to be closed). 216 | # 217 | def kill 218 | ctl.write :kill 219 | end 220 | 221 | ## 222 | # Terminates this client forcefully. 223 | # 224 | def slay 225 | ctl.write :slay 226 | end 227 | 228 | ## 229 | # Maximizes this client to occupy the 230 | # entire screen on the current view. 231 | # 232 | def fullscreen 233 | ctl.write 'Fullscreen on' 234 | end 235 | 236 | ## 237 | # Restores this client back to its original size on the current view. 238 | # 239 | def unfullscreen 240 | ctl.write 'Fullscreen off' 241 | end 242 | 243 | ## 244 | # Toggles the fullscreen status of this client on the current view. 245 | # 246 | def fullscreen! 247 | ctl.write 'Fullscreen toggle' 248 | end 249 | 250 | ## 251 | # Checks if this client is currently fullscreen on the current view. 252 | # 253 | def fullscreen? 254 | # 255 | # If the client's dimensions match those of the 256 | # floating area, then we know it is fullscreen. 257 | # 258 | View.curr.manifest =~ /^# #{FLOATING_AREA_ID} (\d+) (\d+)\n.*^#{FLOATING_AREA_ID} #{@id} \d+ \d+ \1 \2 /m 259 | end 260 | 261 | ## 262 | # Checks if this client is sticky (appears in all views). 263 | # 264 | def stick? 265 | tags.include? CLIENT_STICKY_TAG 266 | end 267 | 268 | ## 269 | # Makes this client sticky (appears in all views). 270 | # 271 | def stick 272 | tag CLIENT_STICKY_TAG 273 | end 274 | 275 | ## 276 | # Makes this client unsticky (does not appear in all views). 277 | # 278 | def unstick 279 | untag CLIENT_STICKY_TAG 280 | end 281 | 282 | ## 283 | # Toggles the stickyness of this client. 284 | # 285 | def stick! 286 | if stick? 287 | unstick 288 | else 289 | stick 290 | end 291 | end 292 | 293 | ## 294 | # Checks if this client is in the floating area of the given view. 295 | # 296 | def float? view = View.curr 297 | area(view).floating? 298 | end 299 | 300 | ## 301 | # Puts this client into the floating area of the given view. 302 | # 303 | def float view = View.curr 304 | send :toggle, view unless float? view 305 | end 306 | 307 | ## 308 | # Puts this client into the managed area of the given view. 309 | # 310 | def unfloat view = View.curr 311 | send :toggle, view if float? view 312 | end 313 | 314 | ## 315 | # Toggles the floating status of this client in the given view. 316 | # 317 | def float! view = View.curr 318 | send :toggle, view 319 | end 320 | 321 | ## 322 | # Checks if this client is in the managed area of the given view. 323 | def manage? view = View.curr 324 | not float? view 325 | end 326 | 327 | alias manage unfloat 328 | 329 | alias unmanage float 330 | 331 | alias manage! float! 332 | 333 | #------------------------------------------------------------------------- 334 | # WM hierarchy 335 | #------------------------------------------------------------------------- 336 | 337 | ## 338 | # Returns the area that contains this client within the given view. 339 | # 340 | def area view = View.curr 341 | view.area_of_client self 342 | end 343 | 344 | ## 345 | # Returns the views that contain this client. 346 | # 347 | def views 348 | tags.map! {|t| View.new t } 349 | end 350 | 351 | #------------------------------------------------------------------------- 352 | # tag manipulations 353 | #------------------------------------------------------------------------- 354 | 355 | TAG_DELIMITER = '+'.freeze 356 | 357 | ## 358 | # Returns the tags associated with this client. 359 | # 360 | def tags 361 | self[:tags].read.split TAG_DELIMITER 362 | end 363 | 364 | ## 365 | # Modifies the tags associated with this client. 366 | # 367 | # If a tag name is '~', this client is placed 368 | # into the floating layer of the current view. 369 | # 370 | # If a tag name begins with '~', then this 371 | # client is placed into the floating layer 372 | # of the view corresponding to that tag. 373 | # 374 | # If a tag name is '!', this client is placed 375 | # into the managed layer of the current view. 376 | # 377 | # If a tag name begins with '!', then this 378 | # client is placed into the managed layer 379 | # of the view corresponding to that tag. 380 | # 381 | def tags= *tags 382 | float = [] 383 | manage = [] 384 | inherit = [] 385 | 386 | tags.join(TAG_DELIMITER).split(TAG_DELIMITER).each do |tag| 387 | case tag 388 | when '~' then float << Rumai.curr_tag 389 | when /^~/ then float << $' 390 | when '!' then manage << Rumai.curr_tag 391 | when /^!/ then manage << $' 392 | else inherit << tag 393 | end 394 | end 395 | 396 | self[:tags].write((float + manage + inherit).uniq.join(TAG_DELIMITER)) 397 | 398 | float.each do |tag| 399 | self.float View.new(tag) 400 | end 401 | 402 | manage.each do |tag| 403 | self.manage View.new(tag) 404 | end 405 | end 406 | 407 | ## 408 | # Evaluates the given block within the 409 | # context of this client's list of tags. 410 | # 411 | def with_tags &block 412 | arr = self.tags 413 | arr.instance_eval(&block) 414 | self.tags = arr 415 | end 416 | 417 | ## 418 | # Adds the given tags to this client. 419 | # 420 | def tag *tags 421 | with_tags do 422 | concat tags 423 | end 424 | end 425 | 426 | ## 427 | # Removes the given tags from this client. 428 | # 429 | def untag *tags 430 | with_tags do 431 | tags.flatten.each do |tag| 432 | delete tag.to_s 433 | end 434 | end 435 | end 436 | 437 | #------------------------------------------------------------------------- 438 | # multiple client grouping 439 | #------------------------------------------------------------------------- 440 | 441 | ## 442 | # Checks if this client is included in the current grouping. 443 | # 444 | def group? 445 | tags.include? CLIENT_GROUPING_TAG 446 | end 447 | 448 | ## 449 | # Adds this client to the current grouping. 450 | # 451 | def group 452 | with_tags do 453 | push CLIENT_GROUPING_TAG 454 | end 455 | end 456 | 457 | ## 458 | # Removes this client to the current grouping. 459 | # 460 | def ungroup 461 | untag CLIENT_GROUPING_TAG 462 | end 463 | 464 | ## 465 | # Toggles the presence of this client in the current grouping. 466 | # 467 | def group! 468 | if group? 469 | ungroup 470 | else 471 | group 472 | end 473 | end 474 | 475 | private 476 | 477 | def reshape command, view, direction, amount 478 | area = self.area(view) 479 | index = area.client_ids.index(@id) + 1 # numbered as 1..N 480 | view.ctl.write "#{command} #{area.id} #{index} #{direction} #{amount}" 481 | end 482 | 483 | ## 484 | # Returns the wmii ID of the given area. 485 | # 486 | def area_to_id area_or_id 487 | if area_or_id.respond_to? :id 488 | id = area_or_id.id 489 | id == FLOATING_AREA_ID ? :toggle : id 490 | else 491 | area_or_id 492 | end 493 | end 494 | end 495 | 496 | ## 497 | # @note Inheritors should override the {client_ids} method. 498 | # 499 | module ClientContainer 500 | ## 501 | # Returns the IDs of the clients in this container. 502 | # 503 | def client_ids 504 | [] 505 | end 506 | 507 | ## 508 | # Returns the clients contained in this container. 509 | # 510 | def clients 511 | client_ids.map! {|i| Client.new i } 512 | end 513 | 514 | # multiple client grouping 515 | %w[group ungroup group!].each do |meth| 516 | define_method meth do 517 | clients.each do |c| 518 | c.__send__ meth 519 | end 520 | end 521 | end 522 | 523 | ## 524 | # Returns all grouped clients in this container. 525 | # 526 | def grouping 527 | clients.select {|c| c.group? } 528 | end 529 | end 530 | 531 | ## 532 | # A region that contains clients. This can be either 533 | # the floating area or a column in the managed area. 534 | # 535 | class Area 536 | attr_reader :view 537 | 538 | ## 539 | # @param [Rumai::View] view 540 | # the view object which contains this area 541 | # 542 | def initialize area_id, view = View.curr 543 | @id = Integer(area_id) rescue area_id 544 | @view = view 545 | end 546 | 547 | ## 548 | # Checks if this area is the floating area. 549 | # 550 | def floating? 551 | @id == FLOATING_AREA_ID 552 | end 553 | 554 | ## 555 | # Checks if this is a managed area (a column). 556 | # 557 | def column? 558 | not floating? 559 | end 560 | 561 | alias managed? column? 562 | 563 | #------------------------------------------------------------------------- 564 | include WidgetImpl 565 | #------------------------------------------------------------------------- 566 | 567 | ## 568 | # Returns the currently focused area. 569 | # 570 | def self.curr 571 | View.curr.area_of_client Client.curr 572 | end 573 | 574 | ## 575 | # Returns the floating area in the given view. 576 | # 577 | def self.floating view = View.curr 578 | new FLOATING_AREA_ID, view 579 | end 580 | 581 | #------------------------------------------------------------------------- 582 | include Chain 583 | #------------------------------------------------------------------------- 584 | 585 | ## 586 | # Returns a list of all areas in the current view. 587 | # 588 | def chain 589 | @view.areas 590 | end 591 | 592 | ## 593 | # Checks if this object exists in the chain. 594 | # 595 | def exist? 596 | chain.include? self 597 | end 598 | 599 | #------------------------------------------------------------------------- 600 | include ClientContainer 601 | #------------------------------------------------------------------------- 602 | 603 | ## 604 | # Returns the IDs of the clients in this area. 605 | # 606 | def client_ids 607 | @view.client_ids @id 608 | end 609 | 610 | #------------------------------------------------------------------------- 611 | include Enumerable 612 | #------------------------------------------------------------------------- 613 | 614 | ## 615 | # Iterates through each client in this container. 616 | # 617 | def each &block 618 | clients.each(&block) 619 | end 620 | 621 | #------------------------------------------------------------------------- 622 | # WM operations 623 | #------------------------------------------------------------------------- 624 | 625 | ## 626 | # Puts focus on this area. 627 | # 628 | def focus 629 | @view.ctl.write "select #{@id}" 630 | end 631 | 632 | ## 633 | # Sets the layout of clients in this column. 634 | # 635 | def layout= mode 636 | case mode 637 | when :stack then mode = 'stack-max' 638 | when :max then mode = 'stack+max' 639 | end 640 | 641 | @view.ctl.write "colmode #{@id} #{mode}" 642 | end 643 | 644 | #------------------------------------------------------------------------- 645 | # array abstraction: area is an array of clients 646 | #------------------------------------------------------------------------- 647 | 648 | ## 649 | # Returns the number of clients in this area. 650 | # 651 | def length 652 | client_ids.length 653 | end 654 | 655 | ## 656 | # Inserts the given clients at the bottom of this area. 657 | # 658 | def push *clients 659 | return if clients.empty? 660 | 661 | insert *clients 662 | 663 | # move inserted clients to bottom 664 | clients.reverse.each_with_index do |c, i| 665 | until c.id == self.client_ids[-i.succ] 666 | c.send :down 667 | end 668 | end 669 | end 670 | 671 | alias << push 672 | 673 | ## 674 | # Inserts the given clients after the 675 | # currently focused client in this area. 676 | # 677 | def insert *clients 678 | clients.each do |c| 679 | import_client c 680 | end 681 | end 682 | 683 | ## 684 | # Inserts the given clients at the top of this area. 685 | # 686 | def unshift *clients 687 | return if clients.empty? 688 | 689 | insert *clients 690 | 691 | # move inserted clients to top 692 | clients.each_with_index do |c, i| 693 | until c.id == self.client_ids[i] 694 | c.send :up 695 | end 696 | end 697 | end 698 | 699 | ## 700 | # Concatenates the given area to the bottom of this area. 701 | # 702 | def concat area 703 | push *area.clients 704 | end 705 | 706 | ## 707 | # Ensures that this area has at most the given number of clients. 708 | # 709 | # Areas to the right of this one serve as a buffer into which excess 710 | # clients are evicted and from which deficit clients are imported. 711 | # 712 | def length= max_clients 713 | return unless max_clients > 0 714 | len, out = length, fringe 715 | 716 | if len > max_clients 717 | out.unshift *clients[max_clients..-1] 718 | 719 | elsif len < max_clients 720 | until (diff = max_clients - length) == 0 721 | importable = out.clients[0, diff] 722 | break if importable.empty? 723 | 724 | push *importable 725 | end 726 | end 727 | end 728 | 729 | private 730 | 731 | ## 732 | # Moves the given client into this area. 733 | # 734 | def import_client c 735 | if exist? 736 | c.send self 737 | 738 | else 739 | # move the client to the nearest existing column 740 | src = c.area 741 | dst = chain.last 742 | 743 | c.send dst unless src == dst 744 | 745 | # slide the client over to this column 746 | c.send :right 747 | @id = dst.id.next 748 | 749 | raise 'column should exist now' unless exist? 750 | end 751 | end 752 | 753 | ## 754 | # Returns the next area, which may or may not exist. 755 | # 756 | def fringe 757 | Area.new @id.next, @view 758 | end 759 | end 760 | 761 | ## 762 | # The visualization of a tag. 763 | # 764 | class View < WidgetNode 765 | def initialize view_id 766 | super view_id, '/tag' 767 | end 768 | 769 | #------------------------------------------------------------------------- 770 | include WidgetImpl 771 | #------------------------------------------------------------------------- 772 | 773 | ## 774 | # Returns the currently focused view. 775 | # 776 | def self.curr 777 | new FOCUSED_WIDGET_ID 778 | end 779 | 780 | ## 781 | # Focuses this view. 782 | # 783 | def focus 784 | IXP_FS_ROOT.ctl.write "view #{@id}" 785 | end 786 | 787 | #------------------------------------------------------------------------- 788 | include Chain 789 | #------------------------------------------------------------------------- 790 | 791 | ## 792 | # Returns a list of all views. 793 | # 794 | def chain 795 | Rumai.views 796 | end 797 | 798 | #------------------------------------------------------------------------- 799 | include ClientContainer 800 | #------------------------------------------------------------------------- 801 | 802 | ## 803 | # Returns the IDs of the clients contained 804 | # in the given area within this view. 805 | # 806 | def client_ids area_id = '\S+' 807 | manifest.scan(/^#{area_id} (0x\S+)/).flatten 808 | end 809 | 810 | #------------------------------------------------------------------------- 811 | include Enumerable 812 | #------------------------------------------------------------------------- 813 | 814 | ## 815 | # Iterates through each area in this view. 816 | # 817 | def each &block 818 | areas.each(&block) 819 | end 820 | 821 | #----------------------------------------------------------------------- 822 | # WM operations 823 | #----------------------------------------------------------------------- 824 | 825 | ## 826 | # Returns the manifest of all areas and clients in this view. 827 | # 828 | def manifest 829 | index.read || '' 830 | end 831 | 832 | ## 833 | # Moves the focus from the current client in the given direction. 834 | # 835 | def select direction 836 | ctl.write "select #{direction}" 837 | end 838 | 839 | #----------------------------------------------------------------------- 840 | # WM hierarchy 841 | #----------------------------------------------------------------------- 842 | 843 | ## 844 | # Returns the area which contains the given client in this view. 845 | # 846 | def area_of_client client_or_id 847 | arg = 848 | if client_or_id.respond_to? :id 849 | client_or_id.id 850 | else 851 | client_or_id 852 | end 853 | 854 | manifest =~ /^(\S+) #{arg}/ 855 | if area_id = $1 856 | Area.new area_id, self 857 | end 858 | end 859 | 860 | ## 861 | # Returns the IDs of all areas in this view. 862 | # 863 | def area_ids 864 | manifest.scan(/^# (\d+) /).flatten.unshift(FLOATING_AREA_ID) 865 | end 866 | 867 | ## 868 | # Returns all areas in this view. 869 | # 870 | def areas 871 | area_ids.map! {|i| Area.new i, self } 872 | end 873 | 874 | ## 875 | # Returns the floating area of this view. 876 | # 877 | def floating_area 878 | Area.floating self 879 | end 880 | 881 | ## 882 | # Returns all columns (managed areas) in this view. 883 | # 884 | def columns 885 | areas[1..-1].to_a 886 | end 887 | 888 | alias managed_areas columns 889 | 890 | ## 891 | # Resiliently iterates through possibly destructive changes to 892 | # each column. That is, if the given block creates new 893 | # columns, then those will also be processed in the iteration. 894 | # 895 | def each_column starting_column_id = 1 896 | i = starting_column_id 897 | while (column = Area.new(i, self)).exist? 898 | yield column 899 | i += 1 900 | end 901 | end 902 | 903 | alias each_managed_area each_column 904 | 905 | #------------------------------------------------------------------------- 906 | # visual arrangement of clients 907 | #----------------------------------------------------------------------- 908 | 909 | ## 910 | # Arranges columns with the following number of clients in them: 911 | # 912 | # 1, N 913 | # 914 | def tile_right 915 | arrange_columns [1, num_managed_clients-1] 916 | end 917 | 918 | ## 919 | # Arranges columns with the following number of clients in them: 920 | # 921 | # 1, 2, 3, ... 922 | # 923 | # Imagine an equilateral triangle with its 924 | # base on the right side of the screen and 925 | # its peak on the left side of the screen. 926 | # 927 | def tile_rightward 928 | num_rising_columns, num_summit_clients = calculate_right_triangle 929 | heights = (1..num_rising_columns).to_a.push(num_summit_clients) 930 | arrange_columns heights 931 | end 932 | 933 | ## 934 | # Arranges columns with the following number of clients in them: 935 | # 936 | # N, 1 937 | # 938 | def tile_left 939 | arrange_columns [num_managed_clients-1, 1] 940 | end 941 | 942 | ## 943 | # Arranges columns with the following number of clients in them: 944 | # 945 | # ..., 3, 2, 1 946 | # 947 | # Imagine an equilateral triangle with its 948 | # base on the left side of the screen and 949 | # its peak on the right side of the screen. 950 | # 951 | def tile_leftward 952 | num_rising_columns, num_summit_clients = calculate_right_triangle 953 | heights = (1..num_rising_columns).to_a.push(num_summit_clients).reverse 954 | arrange_columns heights 955 | end 956 | 957 | ## 958 | # Arranges columns with the following number of clients in them: 959 | # 960 | # 1, 2, 3, ..., 3, 2, 1 961 | # 962 | # Imagine two equilateral triangles with their bases on the left and right 963 | # sides of the screen and their peaks meeting in the middle of the screen. 964 | # 965 | def tile_inward 966 | rising, num_summit_clients, falling = calculate_equilateral_triangle 967 | 968 | # distribute extra clients in the middle 969 | summit = [] 970 | if num_summit_clients > 0 971 | split = num_summit_clients / 2 972 | carry = num_summit_clients % 2 973 | summit = [split, carry, split].reject(&:zero?) 974 | 975 | # one client per column cannot be considered as "tiling" so squeeze 976 | # these singular columns together to create one giant middle column 977 | if summit.length == num_summit_clients 978 | summit = [num_summit_clients] 979 | end 980 | end 981 | 982 | arrange_columns rising + summit + falling 983 | end 984 | 985 | ## 986 | # Arranges columns with the following number of clients in them: 987 | # 988 | # ..., 3, 2, 1, 2, 3, ... 989 | # 990 | # Imagine two equilateral triangles with their bases meeting in the middle 991 | # of the screen and their peaks reaching outward to the left and right 992 | # sides of the screen. 993 | # 994 | def tile_outward 995 | rising, num_summit_clients, falling = calculate_equilateral_triangle 996 | heights = falling + rising[1..-1].to_a 997 | 998 | # distribute extra clients on the outsides 999 | num_summit_clients += rising[0].to_i 1000 | if num_summit_clients > 0 1001 | split = num_summit_clients / 2 1002 | carry = num_summit_clients % 2 1003 | # put the remainder on the left side to minimize the need for 1004 | # rearrangement when clients are removed or added to the view 1005 | heights.unshift split + carry 1006 | heights.push split 1007 | end 1008 | 1009 | arrange_columns heights 1010 | end 1011 | 1012 | ## 1013 | # Arranges the clients in this view, while maintaining 1014 | # their relative order, in the given number of columns. 1015 | # 1016 | def stack num_columns = 2 1017 | heights = [num_managed_clients / num_columns] * num_columns 1018 | heights[-1] += num_managed_clients % num_columns 1019 | arrange_columns heights, :stack 1020 | end 1021 | 1022 | def join 1023 | arrange_columns [num_managed_clients] 1024 | end 1025 | 1026 | ## 1027 | # Arranges the clients in this view, while maintaining 1028 | # their relative order, in (at best) a square grid. 1029 | # 1030 | def grid max_clients_per_column = nil 1031 | # compute client distribution 1032 | unless max_clients_per_column 1033 | num_clients = num_managed_clients 1034 | return unless num_clients > 0 1035 | 1036 | num_columns = Math.sqrt(num_clients) 1037 | max_clients_per_column = (num_clients / num_columns).floor 1038 | end 1039 | 1040 | return if max_clients_per_column < 1 1041 | 1042 | # apply the distribution 1043 | arrange_columns [max_clients_per_column] * num_columns 1044 | end 1045 | 1046 | alias arrange_as_larswm tile_right 1047 | alias arrange_in_diamond tile_inward 1048 | alias arrange_in_stacks stack 1049 | alias arrange_in_grid grid 1050 | 1051 | ## 1052 | # Applies the given length to each column in sequence. Also, 1053 | # the given layout is applied to all columns, if specified. 1054 | # 1055 | def arrange_columns lengths, layout = nil 1056 | maintain_focus do 1057 | i = 0 1058 | each_column do |column| 1059 | if i < lengths.length 1060 | column.length = lengths[i] 1061 | column.layout = layout if layout 1062 | i += 1 1063 | else 1064 | break 1065 | end 1066 | end 1067 | end 1068 | end 1069 | 1070 | private 1071 | 1072 | def calculate_equilateral_triangle 1073 | num_clients = num_managed_clients 1074 | return [[],0,[]] unless num_clients > 1 1075 | 1076 | # calculate the dimensions of the rising sub-triangle 1077 | num_rising_columns, num_rising_summit_clients = 1078 | calculate_right_triangle(num_clients / 2) 1079 | 1080 | num_summit_clients = 1081 | (num_rising_summit_clients * 2) + (num_clients % 2) 1082 | 1083 | # quantify entire triangle as a sequence of heights 1084 | rising = (1 .. num_rising_columns).to_a 1085 | [rising, num_summit_clients, rising.reverse] 1086 | end 1087 | 1088 | def calculate_right_triangle num_rising_clients = num_managed_clients 1089 | num_rising_columns = num_clients_processed = 0 1090 | 1091 | 1.upto num_rising_clients do |height| 1092 | if num_clients_processed + height > num_rising_clients 1093 | break 1094 | else 1095 | num_clients_processed += height 1096 | num_rising_columns += 1 1097 | end 1098 | end 1099 | 1100 | num_summit_clients = num_rising_clients - num_clients_processed 1101 | 1102 | [num_rising_columns, num_summit_clients] 1103 | end 1104 | 1105 | ## 1106 | # Executes the given block and restores 1107 | # focus to the client that had focus 1108 | # before the given block was executed. 1109 | # 1110 | def maintain_focus 1111 | c, v = Client.curr, View.curr 1112 | yield 1113 | c.focus v 1114 | end 1115 | 1116 | ## 1117 | # Returns the number of clients in the non-floating areas of this view. 1118 | # 1119 | def num_managed_clients 1120 | manifest.scan(/^\d+ 0x/).length 1121 | end 1122 | end 1123 | 1124 | ## 1125 | # Subdivision of the bar---the thing that spans the width of the 1126 | # screen---useful for displaying information and system controls. 1127 | # 1128 | class Barlet < Node 1129 | attr_reader :side 1130 | 1131 | def initialize file_name, side 1132 | prefix = 1133 | case @side = side 1134 | when :left then '/lbar' 1135 | when :right then '/rbar' 1136 | else raise ArgumentError, side 1137 | end 1138 | 1139 | super "#{prefix}/#{file_name}" 1140 | end 1141 | 1142 | COLORS_REGEXP = /^\S+ \S+ \S+/ 1143 | 1144 | def label 1145 | case read 1146 | when /^label (.*)$/ then $1 1147 | when /#{COLORS_REGEXP} (.*)$/o then $1 1148 | end 1149 | end 1150 | 1151 | def colors 1152 | case read 1153 | when /^colors (.*)$/ then $1 1154 | when COLORS_REGEXP then $& 1155 | end 1156 | end 1157 | 1158 | # detect the new bar file format introduced in wmii-hg2743 1159 | temp_barlet = IXP_FS_ROOT.rbar["temp_barlet_#{object_id}"] 1160 | begin 1161 | temp_barlet.create 1162 | SPLIT_FILE_FORMAT = temp_barlet.read =~ /\Acolors/ 1163 | ensure 1164 | temp_barlet.remove 1165 | end 1166 | 1167 | def label= label 1168 | if SPLIT_FILE_FORMAT 1169 | write "label #{label}" 1170 | else 1171 | write "#{colors} #{label}" 1172 | end 1173 | end 1174 | 1175 | def colors= colors 1176 | if SPLIT_FILE_FORMAT 1177 | write "colors #{colors}" 1178 | else 1179 | write "#{colors} #{label}" 1180 | end 1181 | end 1182 | end 1183 | 1184 | #--------------------------------------------------------------------------- 1185 | # access to global WM state 1186 | #--------------------------------------------------------------------------- 1187 | 1188 | ## 1189 | # Returns the root of IXP file system hierarchy. 1190 | # 1191 | def fs 1192 | IXP_FS_ROOT 1193 | end 1194 | 1195 | ## 1196 | # Returns the current set of tags. 1197 | # 1198 | def tags 1199 | ary = IXP_FS_ROOT.tag.entries.sort 1200 | ary.delete FOCUSED_WIDGET_ID 1201 | ary 1202 | end 1203 | 1204 | ## 1205 | # Returns the current set of views. 1206 | # 1207 | def views 1208 | tags.map! {|t| View.new t } 1209 | end 1210 | 1211 | ## 1212 | # Returns a list of all grouped clients in 1213 | # the currently focused view. If there are 1214 | # no grouped clients, then the currently 1215 | # focused client is returned in the list. 1216 | # 1217 | def grouping 1218 | list = curr_view.clients.select {|c| c.group? } 1219 | list << curr_client if list.empty? and curr_client.exist? 1220 | list 1221 | end 1222 | 1223 | #--------------------------------------------------------------------------- 1224 | include ClientContainer 1225 | #--------------------------------------------------------------------------- 1226 | 1227 | ## 1228 | # Returns the IDs of the current set of clients. 1229 | # 1230 | def client_ids 1231 | ary = IXP_FS_ROOT.client.entries 1232 | ary.delete FOCUSED_WIDGET_ID 1233 | ary 1234 | end 1235 | 1236 | #--------------------------------------------------------------------------- 1237 | # shortcuts for interactive WM manipulation (via IRB) 1238 | #--------------------------------------------------------------------------- 1239 | 1240 | def curr_client ; Client.curr ; end 1241 | def next_client ; curr_client.next ; end 1242 | def prev_client ; curr_client.prev ; end 1243 | 1244 | def curr_area ; Area.curr ; end 1245 | def next_area ; curr_area.next ; end 1246 | def prev_area ; curr_area.prev ; end 1247 | 1248 | def curr_view ; View.curr ; end 1249 | def next_view ; curr_view.next ; end 1250 | def prev_view ; curr_view.prev ; end 1251 | 1252 | def curr_tag ; curr_view.id ; end 1253 | def next_tag ; next_view.id ; end 1254 | def prev_tag ; prev_view.id ; end 1255 | 1256 | # provide easy access to container state information 1257 | [Client, Area, View].each {|c| c.extend ExportInstanceMethods } 1258 | 1259 | def focus_client id 1260 | Client.focus id 1261 | end 1262 | 1263 | def focus_area id 1264 | Area.focus id 1265 | end 1266 | 1267 | def focus_view id 1268 | View.focus id 1269 | end 1270 | 1271 | # provide easy access to this module's instance methods 1272 | extend self 1273 | end 1274 | -------------------------------------------------------------------------------- /test/rumai/ixp/message_test.rb: -------------------------------------------------------------------------------- 1 | require 'rumai/fs' 2 | require 'pp' if $VERBOSE 3 | 4 | class MessageTest < MiniTest::Spec 5 | include Rumai::IXP 6 | 7 | before do 8 | # connect to the wmii IXP server 9 | @conn = UNIXSocket.new(Rumai::IXP_SOCK_ADDR) 10 | 11 | # at_exit do 12 | # puts "just making sure there is no more data in the pipe" 13 | # while c = @conn.getc 14 | # puts c 15 | # end 16 | # end 17 | 18 | # establish a new session 19 | request, response = talk(Tversion, 20 | :tag => Fcall::NOTAG, 21 | :msize => Tversion::MSIZE, 22 | :version => Tversion::VERSION 23 | ) 24 | response.type.must_equal Rversion.type 25 | response.version.must_equal request.version 26 | end 27 | 28 | it 'can read a directory' do 29 | # attach to FS root 30 | request, response = talk(Tattach, 31 | :tag => 0, 32 | :fid => 0, 33 | :afid => Fcall::NOFID, 34 | :uname => ENV['USER'], 35 | :aname => '' 36 | ) 37 | response.type.must_equal Rattach.type 38 | 39 | # stat FS root 40 | request, response = talk(Tstat, 41 | :tag => 0, 42 | :fid => 0 43 | ) 44 | response.type.must_equal Rstat.type 45 | 46 | # open the FS root for reading 47 | request, response = talk(Topen, 48 | :tag => 0, 49 | :fid => 0, 50 | :mode => Topen::OREAD 51 | ) 52 | response.type.must_equal Ropen.type 53 | 54 | # fetch a Stat for every file in FS root 55 | request, response = talk(Tread, 56 | :tag => 0, 57 | :fid => 0, 58 | :offset => 0, 59 | :count => Tversion::MSIZE 60 | ) 61 | response.type.must_equal Rread.type 62 | 63 | if $VERBOSE 64 | buffer = StringIO.new(response.data, 'r') 65 | stats = [] 66 | 67 | until buffer.eof? 68 | stats << Stat.from_9p(buffer) 69 | end 70 | 71 | puts '--- stats' 72 | pp stats 73 | end 74 | 75 | # close the fid for FS root 76 | request, response = talk(Tclunk, 77 | :tag => 0, 78 | :fid => 0 79 | ) 80 | response.type.must_equal Rclunk.type 81 | 82 | # closed fid should not be readable 83 | request, response = talk(Tread, 84 | :tag => 0, 85 | :fid => 0, 86 | :offset => 0, 87 | :count => Tversion::MSIZE 88 | ) 89 | response.type.must_equal Rerror.type 90 | end 91 | 92 | it 'can read & write a file' do 93 | root_path = ['rbar'] 94 | file_name = "temp#{$$}" 95 | file_path = root_path + [file_name] 96 | 97 | # attach to / 98 | request, response = talk(Tattach, 99 | :tag => 0, 100 | :fid => 0, 101 | :afid => Fcall::NOFID, 102 | :uname => ENV['USER'], 103 | :aname => '' 104 | ) 105 | response.type.must_equal Rattach.type 106 | 107 | # walk to /rbar 108 | request, response = talk(Twalk, 109 | :tag => 0, 110 | :fid => 0, 111 | :newfid => 1, 112 | :wname => root_path 113 | ) 114 | response.type.must_equal Rwalk.type 115 | 116 | # create the file 117 | request, response = talk(Tcreate, 118 | :tag => 0, 119 | :fid => 1, 120 | :name => file_name, 121 | :perm => 0644, 122 | :mode => Topen::ORDWR 123 | ) 124 | response.type.must_equal Rcreate.type 125 | 126 | # close the fid for /rbar 127 | request, response = talk(Tclunk, 128 | :tag => 0, 129 | :fid => 1 130 | ) 131 | response.type.must_equal Rclunk.type 132 | 133 | # walk to the file from / 134 | request, response = talk(Twalk, 135 | :tag => 0, 136 | :fid => 0, 137 | :newfid => 1, 138 | :wname => file_path 139 | ) 140 | response.type.must_equal Rwalk.type 141 | 142 | # close the fid for / 143 | request, response = talk(Tclunk, 144 | :tag => 0, 145 | :fid => 0 146 | ) 147 | response.type.must_equal Rclunk.type 148 | 149 | # open the file for writing 150 | request, response = talk(Topen, 151 | :tag => 0, 152 | :fid => 1, 153 | :mode => Topen::ORDWR 154 | ) 155 | response.type.must_equal Ropen.type 156 | 157 | # write to the file 158 | message = "\u{266A} hello world \u{266B}" 159 | write_request, write_response = talk(Twrite, 160 | :tag => 0, 161 | :fid => 1, 162 | :offset => 0, 163 | :data => ( 164 | require 'rumai/wm' 165 | if Rumai::Barlet::SPLIT_FILE_FORMAT 166 | "colors #000000 #000000 #000000\nlabel #{message}" 167 | else 168 | "#000000 #000000 #000000 #{message}" 169 | end 170 | ) 171 | ) 172 | write_response.type.must_equal Rwrite.type 173 | write_response.count.must_equal write_request.data.bytesize 174 | 175 | # verify the write 176 | read_request, read_response = talk(Tread, 177 | :tag => 0, 178 | :fid => 1, 179 | :offset => 0, 180 | :count => write_response.count 181 | ) 182 | read_response.type.must_equal Rread.type 183 | # wmii responds in ASCII-8BIT whereas we requested in UTF-8 184 | read_response.data.force_encoding(message.encoding).must_equal write_request.data 185 | 186 | # remove the file 187 | request, response = talk(Tremove, 188 | :tag => 0, 189 | :fid => 1 190 | ) 191 | response.type.must_equal Rremove.type 192 | 193 | # fid for the file should have been closed by Tremove 194 | request, response = talk(Tclunk, 195 | :tag => 0, 196 | :fid => 1 197 | ) 198 | response.type.must_equal Rerror.type 199 | end 200 | 201 | ## 202 | # Transmits the given request and returns the received response. 203 | # 204 | def talk request_type, request_options 205 | request = request_type.new(request_options) 206 | 207 | # send the request 208 | if $VERBOSE 209 | puts '--- sending' 210 | pp request, request.to_9p 211 | end 212 | 213 | @conn << request.to_9p 214 | 215 | # receive the response 216 | response = Fcall.from_9p(@conn) 217 | 218 | if $VERBOSE 219 | puts '--- received' 220 | pp response, response.to_9p 221 | end 222 | 223 | if response.type == Rerror.type 224 | response.must_be_kind_of Rerror 225 | else 226 | response.type.must_equal request.type + 1 227 | end 228 | 229 | response.tag.must_equal request.tag 230 | 231 | # return the conversation 232 | [request, response] 233 | end 234 | 235 | end 236 | -------------------------------------------------------------------------------- /test/runner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Adds the project library directory 4 | # and this test directory to Ruby's 5 | # load path, loads the test helper 6 | # and executes the given test files. 7 | # 8 | # ruby test/runner [-d] [-v] [TEST]... 9 | # 10 | # -d Enables $DEBUG mode in Ruby. 11 | # 12 | # -v Enables $VERBOSE mode in Ruby. 13 | # 14 | # TEST Path to a file, or a file globbing 15 | # pattern describing a set of files. 16 | # 17 | # The default value is all *_test.rb 18 | # files beneath this test/ directory. 19 | 20 | $DEBUG = true if ARGV.delete('-d') 21 | $VERBOSE = true if ARGV.delete('-v') 22 | 23 | lib_dir = File.expand_path('../../lib', __FILE__) 24 | test_dir = File.expand_path('..', __FILE__) 25 | $LOAD_PATH.unshift lib_dir, test_dir 26 | 27 | require 'test_helper' 28 | 29 | ARGV << "#{test_dir}/**/*_test.rb" if ARGV.empty? 30 | ARGV.each {|glob| Dir[glob].each {|test| load test } } 31 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/spec' 2 | require 'minitest/autorun' 3 | --------------------------------------------------------------------------------