Compare commits

...

136 Commits

Author SHA1 Message Date
Thibault Jouan
d9b4ac6083 Extract default mux worker timeout in a constant 2015-04-29 09:24:50 +00:00
Thibault Jouan
d24f25bbbe Test blocking worker event handling reliability 2015-04-29 09:24:44 +00:00
Thibault Jouan
2731f9b3a3 Support delay modification in x_key UAT helper 2015-04-29 09:24:35 +00:00
Thibault Jouan
4bc513010e Improve blocking worker acceptance test 2015-04-29 02:45:24 +00:00
Thibault Jouan
8a490e155f Merge branch 'client-killing' 2015-04-29 02:44:44 +00:00
Thibault Jouan
1cc1f98ad2 Implement `kill' action keyword
When invoked, will kill the client returned by
`layout.current_client`. ICCCM WM_DELETE_WINDOW is used instead if the
window reports to support this protocol.
2015-04-29 02:44:34 +00:00
Thibault Jouan
10ad5d9a76 Fix x_window_map_state UAT helper to hide output 2015-04-29 02:44:34 +00:00
Thibault Jouan
eed9cdf0c0 Support usage of ICCCM compliant window in UAT 2015-04-29 02:37:31 +00:00
Thibault Jouan
97abd402b1 Accept window names in x_window_map_state test helper 2015-04-29 02:36:36 +00:00
Thibault Jouan
f26e16fe6e Implement Client#kill and Client#kill! 2015-04-29 01:33:13 +00:00
Thibault Jouan
4050d52af7 Remove deprecated specs 2015-04-29 01:32:57 +00:00
Thibault Jouan
92dedd16e7 Implement `log_separator' action keyword 2015-04-28 10:45:56 +00:00
Thibault Jouan
85e5abac8d Workaround travis CI scrolling bugs (quiet output) 2015-04-28 10:31:04 +00:00
Thibault Jouan
6a4a15e9db Remove deprecated X connection test (UAT) 2015-04-28 09:44:49 +00:00
Thibault Jouan
27448abfc6 Improve expectation in rc key test scenario (UAT) 2015-04-28 09:42:39 +00:00
Thibault Jouan
a6b2e98146 Improve expectation in rc eval test scenario (UAT) 2015-04-28 09:38:40 +00:00
Thibault Jouan
1d5ee7cab7 Remove debug code from layout delegation UAT 2015-04-28 09:37:57 +00:00
Thibault Jouan
d9ce2eb120 Remove deprecated test scenario (UAT) 2015-04-28 09:37:08 +00:00
Thibault Jouan
c9ab16c847 Fix typo in tests (UAT) 2015-04-28 09:35:10 +00:00
Thibault Jouan
265bb1d626 Remove deprecated test (UAT) 2015-04-28 09:34:49 +00:00
Thibault Jouan
c394fee2a6 Improve cli layout test expectations (UAT) 2015-04-28 09:31:40 +00:00
Thibault Jouan
01d9bdb3b3 Improve test expectations consistency (UAT) 2015-04-28 09:28:38 +00:00
Thibault Jouan
ace86cf0e3 Fix how layout delegation is tested (UAT) 2015-04-28 09:20:42 +00:00
Thibault Jouan
daa028e794 Improve cli require cucumber scenario 2015-04-28 09:10:16 +00:00
Thibault Jouan
258812013b Refactor some steps in cucumber scenarios 2015-04-28 08:37:04 +00:00
Thibault Jouan
66413b6ccb Bump patch version 2015-04-28 08:22:28 +00:00
Thibault Jouan
1332be5f39 Include GeoAccessors module in Client 2015-04-28 08:22:09 +00:00
Thibault Jouan
9199efaeda Bump patch version 2015-04-28 07:50:40 +00:00
Thibault Jouan
7d7db62f0d Bump uh dependency requirement 2015-04-28 07:34:59 +00:00
Thibault Jouan
ed478c51b2 Update uh dependency requirement to 2.0.0.pre4 2015-04-28 06:09:25 +00:00
Thibault Jouan
56bf7fb03a Update uh dependency requirement to 2.0.0.pre3 2015-04-28 05:54:18 +00:00
Thibault Jouan
ba28838441 Update documented feature list 2015-04-28 03:05:39 +00:00
Thibault Jouan
a649a2b450 Fix layout protocol explanation in documentation 2015-04-27 23:50:05 +00:00
Thibault Jouan
e8647a938b Update introduction in documentation 2015-04-27 23:48:36 +00:00
Thibault Jouan
1e2bf97762 Fix capitalization in documented feature list 2015-04-27 23:45:34 +00:00
Thibault Jouan
42beda3f50 Bump patch version 2015-04-27 06:55:46 +00:00
Thibault Jouan
c8106d2ef5 Update uh-layout dependency to ~> 0.4.0 2015-04-27 06:55:08 +00:00
Thibault Jouan
341e6fd4d2 Bump patch version 2015-04-26 08:23:31 +00:00
Thibault Jouan
f9d905e498 Merge branch 'layout-rc-config' 2015-04-26 02:55:15 +00:00
Thibault Jouan
7c2712924a Allow layout options configuration from run control 2015-04-26 02:49:39 +00:00
Thibault Jouan
70fe9bdb03 Update uh-layout dependency to ~> 0.3.0 2015-04-26 02:43:39 +00:00
Thibault Jouan
bf6f08e848 Implement `layout' run control keyword
Allow configuration of the layout by specifying either a class or an
instance.
2015-04-26 00:59:24 +00:00
Thibault Jouan
7c06b52664 Update wording in documented feature list 2015-04-24 20:17:51 +00:00
Thibault Jouan
85f799174a Fix markdown formatting in README 2015-04-24 20:15:58 +00:00
Thibault Jouan
4af83c181a Update README 2015-04-24 20:14:50 +00:00
Thibault Jouan
3a338e2bb7 Fix coding standards (use one-line blocks) 2015-04-24 19:58:27 +00:00
Thibault Jouan
f5bd8872c5 Remove debug comment 2015-04-24 19:57:51 +00:00
Thibault Jouan
cd06f99e9f Fix initial events handling for mux worker
* Rename `before_wait' worker callback to `before_watch';
* Setup `before_watch' callback in the runner, handling events with the
  manager;
* Test the mux worker with a cucumber scenario.
2015-04-24 19:53:08 +00:00
Thibault Jouan
e68d3fd6a3 Fix coding standards (runner spec descriptions) 2015-04-24 19:53:08 +00:00
Thibault Jouan
6cfed91653 Test blocking worker (UAT) 2015-04-23 21:25:58 +00:00
Thibault Jouan
eaa1d279dc Refactor Manager 2015-04-22 17:19:41 +00:00
Thibault Jouan
4e4b15871e Merge branch 'window-expose-handling'
Handle X expose events, generate an :expose internal event, and
support #expose layout protocol message.
2015-04-22 16:34:52 +00:00
Thibault Jouan
ddf875eab2 Test layout protocol for #expose message 2015-04-22 16:34:34 +00:00
Thibault Jouan
009d5dbedd Handle and forward expose events to the layout 2015-04-22 09:55:25 +00:00
Thibault Jouan
eb9c8d16db Update uh-layout dependency requirement 2015-04-22 09:50:14 +00:00
Thibault Jouan
44a5be6bd9 Merge branch 'handle-window-property-changes' 2015-04-22 08:14:51 +00:00
Thibault Jouan
a421b496c0 Test layout protocol for #update message 2015-04-22 08:14:45 +00:00
Thibault Jouan
accbebe97c Test how the manager handles window changes 2015-04-22 08:14:45 +00:00
Thibault Jouan
95245e1178 Forward :change events to Layout#update in runner 2015-04-22 08:14:45 +00:00
Thibault Jouan
0601fe0560 Handle property_notify events in Manager#handle 2015-04-22 08:14:45 +00:00
Thibault Jouan
b1a601918e Implement Manager#update_properties 2015-04-22 08:14:45 +00:00
Thibault Jouan
933bcdfcd8 Make the manager listen for changes on new window
When mapping a new window, listen for property notify events.
2015-04-22 08:14:45 +00:00
Thibault Jouan
03a6c323ac Implement Client#update_window_properties 2015-04-22 08:14:45 +00:00
Thibault Jouan
fb4af963eb Update uh-layout dependency requirement to 0.2.2 2015-04-22 08:14:45 +00:00
Thibault Jouan
b58a143aff Fix coding standards in Runner specs 2015-04-22 08:14:32 +00:00
Thibault Jouan
b8e31e1981 Fix typo in Manager#handle specs 2015-04-22 08:14:32 +00:00
Thibault Jouan
a7fe5d767f Avoid Manager#map dependency in Manager specs 2015-04-22 08:14:32 +00:00
Thibault Jouan
b3a82fcf92 Fix how uh-wm is tested for readiness in UAT 2015-04-21 13:58:25 +00:00
Thibault Jouan
730527b836 Fix non-deterministic user acceptance tests 2015-04-21 13:46:54 +00:00
Thibault Jouan
47bc9c9da2 Refactor specs with factory helper methods 2015-04-21 12:39:21 +00:00
Thibault Jouan
2cad5da620 Fix description wording in Client#to_s specs 2015-04-21 12:19:47 +00:00
Thibault Jouan
3776abf405 Fix coding standards 2015-04-21 12:12:20 +00:00
Thibault Jouan
8ed77a0460 Remove deprecated specs 2015-04-21 12:05:43 +00:00
Thibault Jouan
8afb8961f0 Remove dead code 2015-04-21 12:03:11 +00:00
Thibault Jouan
ff2fc8d70d Fix coding standards (specs) 2015-04-21 11:44:49 +00:00
Thibault Jouan
ae3d78497f Remove deprecated specs 2015-04-21 11:42:56 +00:00
Thibault Jouan
cc66b6a760 Accept code as block in ActionsHandler#evaluate 2015-04-21 11:34:01 +00:00
Thibault Jouan
e8dacfa4ac Fix non-deterministic test in layout/unmanage 2015-04-21 08:20:37 +00:00
Thibault Jouan
1d9571766b Refactor cucumber X steps 2015-04-21 08:08:06 +00:00
Thibault Jouan
7ee70b666b Increase layout protocol test coverage (#remove) 2015-04-21 08:02:05 +00:00
Thibault Jouan
4e2ec5f6ec Remove extra cucumber step in session/termination 2015-04-21 07:33:36 +00:00
Thibault Jouan
6346eb5ee6 Simplify cucumber X steps 2015-04-21 07:24:03 +00:00
Thibault Jouan
c7bd6ea28a Fix non-deterministic test in manager/unmanage 2015-04-21 06:59:07 +00:00
Thibault Jouan
ad67456c52 Fix coding standards in UAT helpers 2015-04-21 06:07:47 +00:00
Thibault Jouan
3fd8658adf Simplify cucumber steps (uhwm process) 2015-04-21 06:01:23 +00:00
Thibault Jouan
a4b6127ec8 Refactor cucumber steps (quitting uhwm) 2015-04-21 05:57:37 +00:00
Thibault Jouan
0b7c3e9128 Add license information in gem specification 2015-04-21 05:54:02 +00:00
Thibault Jouan
2cca771ad0 Bump minor version 2015-04-21 05:48:40 +00:00
Thibault Jouan
e2c46d096c Document project description and features 2015-04-21 05:48:05 +00:00
Thibault Jouan
cc401c8733 Update documented usage 2015-04-21 05:04:17 +00:00
Thibault Jouan
04d999e1b0 Fix non-deterministic test in manager/unmanage 2015-04-21 03:41:14 +00:00
Thibault Jouan
3849471a1b Merge branch 'window-destroy-handling' 2015-04-21 02:26:45 +00:00
Thibault Jouan
29f2b4725d Test how manager should handle destroyed windows 2015-04-21 02:26:29 +00:00
Thibault Jouan
e97dcea87a Handle destroy_notify X events in manager 2015-04-21 02:26:29 +00:00
Thibault Jouan
31a326e7eb Implement Manager#destroy 2015-04-21 02:26:29 +00:00
Thibault Jouan
0423d6df9d Update uh dependency requirement to 2.0.0.pre2 2015-04-21 02:26:29 +00:00
Thibault Jouan
4c9342c2f2 Increase default wait timeout in UAT helpers 2015-04-21 02:22:25 +00:00
Thibault Jouan
42751993a2 Test manager client unmanagement with cucumber 2015-04-21 02:22:21 +00:00
Thibault Jouan
a15af6f0ab Merge branch 'window-unmap-handling'
Handle unmap notify event in the manager, generate :unmanage events
with the client, and forward them to the layout.
2015-04-20 10:24:51 +00:00
Thibault Jouan
a6edac962b Add layout client unmanagement feature 2015-04-20 10:24:39 +00:00
Thibault Jouan
38a90ab0af Register for layout unmanage handling in runner 2015-04-20 10:24:39 +00:00
Thibault Jouan
a052140921 Handle unmap_notify X events in Manager 2015-04-20 10:24:39 +00:00
Thibault Jouan
09d7b6ebb5 Implement Manager#unmap 2015-04-20 10:24:39 +00:00
Thibault Jouan
7722909c22 Allow Client unmap_count accessor to be set 2015-04-20 10:24:39 +00:00
Thibault Jouan
b156665236 Add #x_window_unmap to UAT helpers 2015-04-20 10:24:39 +00:00
Thibault Jouan
9a4f2727d2 Supports multiple clients in UAT helpers 2015-04-20 10:24:39 +00:00
Thibault Jouan
f2c97499ce Fix wording in Runner#register_event_hooks specs 2015-04-20 10:24:27 +00:00
Thibault Jouan
a7941d51cc Split Runner#register_event_hooks spec with context 2015-04-20 10:24:27 +00:00
Thibault Jouan
c71fed4108 Reorder Manager specs 2015-04-20 10:24:27 +00:00
Thibault Jouan
f15fa116c9 Rename Manager#manage as Manager#map 2015-04-20 07:14:55 +00:00
Thibault Jouan
d8994dc165 Fix wording in Manager specs 2015-04-20 07:14:45 +00:00
Thibault Jouan
77226594fa Merge branch 'configure-requests-handling'
Handle configure requests events: asks the layout to suggest a geo,
generates configure events for new windows, else just configure the
client if it is known, or return a default hard-coded geometry.
2015-04-20 05:50:05 +00:00
Thibault Jouan
30cb15b5d3 Register :configure event for layout in runner 2015-04-20 05:49:51 +00:00
Thibault Jouan
84d92798a0 Modify manager to handle configure request events 2015-04-20 05:49:51 +00:00
Thibault Jouan
1f7b2269c7 Implement Manager#configure 2015-04-20 05:49:51 +00:00
Thibault Jouan
7fc695f258 Implement Client#configure 2015-04-20 05:49:51 +00:00
Thibault Jouan
c69a600ca4 Return last hook return value in Dispatcher#emit 2015-04-20 05:49:51 +00:00
Thibault Jouan
7c440e28d2 Fix manager/manage feature 2015-04-20 05:49:33 +00:00
Thibault Jouan
e9baae2962 Remove logging for worker read events 2015-04-20 05:37:08 +00:00
Thibault Jouan
ae2beeac20 Change wording in layout logging 2015-04-20 05:37:08 +00:00
Thibault Jouan
bdbeca399a Ensure only one client is managed for given window 2015-04-20 04:11:41 +00:00
Thibault Jouan
036ca1f989 Log X errors 2015-04-20 04:08:55 +00:00
Thibault Jouan
02861a63d4 Prevent manager from handling override_redirect windows 2015-04-19 04:35:37 +00:00
Thibault Jouan
c51d1525ee Delegates `layout_*' messages as Layout#handle_* 2015-04-19 02:13:26 +00:00
Thibault Jouan
d47e7de6d0 Log a message when `quit' action is invoked 2015-04-19 01:50:48 +00:00
Thibault Jouan
5e057ebab3 Fix default arguments in rake `run' task 2015-04-19 01:49:17 +00:00
Thibault Jouan
5e18dbae95 Change default quit key binding to mod+shift+q 2015-04-19 01:28:03 +00:00
Thibault Jouan
6c7b01c9aa Accept modifier in `key' run control keyword 2015-04-19 01:12:35 +00:00
Thibault Jouan
231e72df42 Fix travis CI errors when before_install timeout 2015-04-18 20:27:08 +00:00
Thibault Jouan
26bac5aea9 Test that manager listen to appropriate events 2015-04-18 20:11:04 +00:00
Thibault Jouan
20cdec5fce Fix constant resolution in layout CLI option 2015-04-18 19:53:23 +00:00
Thibault Jouan
68cec67402 Fix uhwm_wait_output acceptance helper with strings 2015-04-18 19:52:43 +00:00
Thibault Jouan
78dffde6c9 Remove a comment left in cucumber steps 2015-04-18 19:42:03 +00:00
Thibault Jouan
82a4161cc2 Improve UAT on layout
* Move cucumber features regarding the layout protocol in
  layout/protocol;
* Add new cucumber features to test uh-layout integration in uh-wm, at
  least simple mapping and focus behavior;
* Add new acceptance testing helpers
2015-04-18 19:33:47 +00:00
Thibault Jouan
2894783b57 Configure travis CI to send email notifications 2015-04-18 18:05:04 +00:00
Thibault Jouan
a9466a49f0 Refactor env logging common usages with a module 2015-04-18 18:04:46 +00:00
57 changed files with 1202 additions and 264 deletions

View File

@ -5,8 +5,11 @@ rvm:
- ruby-head
- 2.1
before_install:
- sudo apt-get update -qq
- sudo apt-get install -qq twm x11-utils xdotool
- travis_retry sudo apt-get update -qq
- travis_retry sudo apt-get install -qq twm x11-utils xdotool
matrix:
allow_failures:
- rvm: ruby-head
notifications:
email:
- tj+travis_uh_wm@a13.fr

View File

@ -1,17 +1,40 @@
uh-wm
=====
Xlib Window Manager prototype.
[![Version ][badge-version-img]][badge-version-uri]
[![Build status ][badge-build-img]][badge-build-uri]
[![Code Climate ][badge-cclimate-img]][badge-cclimate-uri]
uh-wm is a minimalistic tiling and stacking window manager for X. It
shares some similarities with dwm and wmii, but is written in ruby so
you can configure and extend features directly with ruby code.
The layout strategy is interchangeable, the default one being the
`uh-layout` ruby gem. A layout is a simple ruby object responding to
specific messages.
Main features:
* Xinerama support;
* multiple event handling strategy: blocking or multiplexing
with `select()`;
* configuration with a run control file (ruby DSL);
* key bindings with user defined code as callback;
* configurable modifier key;
* support user-defined layout strategies;
* program execution;
* no re-parenting (therefore, no window decoration either);
* no grabbing of the modifier key alone;
* no mouse handling;
* no EWMH support;
* very limited ICCCM support.
Getting started
---------------
### Installation (requires ruby ~> 2.1 and rubygems)
### Installation (requires ruby ~> 2.1 with rubygems)
```
$ gem install uh-wm
@ -24,13 +47,15 @@ $ gem install uh-wm
Usage: uhwm [options]
options:
-h, --help print this message
-v, --verbose enable verbose mode
-d, --debug enable debug mode
-f, --run-control PATH specify alternate run control file
-r, --require PATH require ruby feature
-l, --layout LAYOUT specify layout
-w, --worker WORKER specify worker
-h, --help print this message
-V, --version print version
```

View File

@ -10,13 +10,19 @@ XEPHYR_SCREENS_XINERAMA =
task default: %i[features spec]
Cucumber::Rake::Task.new(:features)
Cucumber::Rake::Task.new :features do |t|
t.profile = 'quiet' if ENV.key? 'TRAVIS'
end
RSpec::Core::RakeTask.new
RSpec::Core::RakeTask.new do |t|
t.rspec_opts = '--format progress' if ENV.key? 'TRAVIS'
end
desc 'Run uhwm in a Xephyr X server'
task :run do
uhwm_args = ARGV.slice_after('--').to_a.last || %w[-d]
uhwm_args = ARGV.include?('--') ?
ARGV.slice_after('--').to_a.last :
%w[-d]
Tempfile.create('uhwm_xinitrc') do |xinitrc|
xinitrc.write <<-eoh
[ -f $HOME/.Xdefaults ] && xrdb $HOME/.Xdefaults

View File

@ -1 +1,2 @@
default: --require features/support --require features/steps
default: --require features/support --require features/steps --no-source
quiet: --format progress

View File

@ -0,0 +1,11 @@
Feature: `kill' action keyword
@icccm_window
Scenario: kills current client
Given uhwm is running with this run control file:
"""
key(:f) { kill_current }
"""
And an ICCCM compliant window is mapped
When I press the alt+f keys
Then the ICCCM window must be unmapped by the manager

View File

@ -0,0 +1,28 @@
Feature: `layout_*' action keywords
Scenario: delegates messages matching `layout_*' to `layout_handle_*'
Given a file named layout.rb with:
"""
class Layout
def register _; end
def handle_some_action arg
puts "testing_layout_action_#{arg}"
end
end
"""
And a run control file with:
"""
key(:f) { layout_some_action 'with_arg' }
"""
And uhwm is running with options -v -r./layout.rb -l Layout
When I press the alt+f keys
Then the output must contain "testing_layout_action_with_arg"
Scenario: logs an error about unimplemented messages
Given uhwm is running with this run control file:
"""
key(:f) { layout_unknown_action }
"""
When I press the alt+f keys
Then the output must match /layout.+no.+implem.+handle_unknown_action/i

View File

@ -0,0 +1,9 @@
Feature: `log_separator' action keyword
Scenario: logs a separator
Given uhwm is running with this run control file:
"""
key(:f) { log_separator }
"""
When I press the alt+f keys
Then the output must contain "- - - - - - - - - - - - - - - - - - - - - - -"

View File

@ -4,8 +4,14 @@ Feature: layout CLI option
Given a file named layout.rb with:
"""
class MyLayout
def register _; end
def register *_
puts 'testing_cli_layout'
end
end
"""
When I run uhwm with option -v -r./layout -l MyLayout
Then the output must match /layout.+mylayout/i
When I run uhwm with options -r./layout -l MyLayout
Then the output must contain "testing_cli_layout"
Scenario: resolves layout class from the root namespace
When I run uhwm with option -l Layout
Then the output must contain "uninitialized constant Layout"

View File

@ -1,5 +1,9 @@
Feature: require CLI option
Scenario: requires a ruby feature
When I run uhwm with option -v -r abbrev
Then the output must match /load.+abbrev.+ruby feature/i
Given a file named my_feature.rb with:
"""
puts 'testing_feature_load'
"""
When I run uhwm with option -r ./my_feature.rb
Then the output must contain "testing_feature_load"

View File

@ -3,7 +3,7 @@ Feature: run control file path CLI option
Scenario: specifies run control file path
Given a file named uhwmrc.rb with:
"""
puts 'run control evaluation'
puts 'testing_run_control'
"""
When I run uhwm with option -f uhwmrc.rb
Then the output must contain "run control evaluation"
Then the output must contain "testing_run_control"

View File

@ -1,9 +1,9 @@
Feature: worker CLI option
Scenario: uses the blocking worker when `block' is given
When I run uhwm with option -v -w block
When I run uhwm with options -v -w block
Then the output must match /work.+event.+block/i
Scenario: uses the multiplexing worker when `mux' is given
When I run uhwm with option -v -w mux
When I run uhwm with options -v -w mux
Then the output must match /work.+event.+mux/i

View File

@ -1,16 +1,12 @@
Feature: layout client management
Scenario: sends the #<< message when telling the layout to manage a client
Given a file named layout.rb with:
"""
class Layout
def register *_; end
Background:
Given uhwm is running
def << client
puts client
end
end
"""
And uhwm is running with options -v -r./layout -l Layout
Scenario: maps new client window
When a window requests to be mapped
Then the output must contain the window name
Then the window must be mapped
Scenario: focuses new client window
When a window is mapped
Then the window must be focused

View File

@ -0,0 +1,56 @@
Feature: layout protocol
Background:
Given a file named layout.rb with:
"""
class Layout
def register display
puts display
display.create_subwindow(Uh::Geo.new(0, 0, 640, 16)).tap do |o|
o.show
end
end
def << client
puts "testing_#<<_#{client.name}"
client.show
end
def remove client
puts "testing_#remove_#{client.name}"
end
def update client
puts "testing_#update_#{client.name}"
end
def expose window
puts "testing_#expose_#{window.id}"
end
end
"""
Scenario: tells the layout to register with #register message
When I run uhwm with option -r./layout -l Layout
Then the output must contain current display
Scenario: tells the layout to manage a client with #<< message
Given uhwm is running with options -v -r./layout -l Layout
When a window requests to be mapped
Then the output must contain "testing_#<<_XClient/default"
Scenario: tells the layout to unmanage a client with #remove message
Given uhwm is running with options -v -r./layout -l Layout
And a window is mapped
When the window is unmapped
Then the output must contain "testing_#remove_XClient/default"
Scenario: tells the layout to update a changed client with #update message
Given uhwm is running with options -v -r./layout -l Layout
And a window is mapped
When the window name changes to "testing_new_name"
Then the output must contain "testing_#update_testing_new_name"
Scenario: tells the layout about an exposed window with #expose message
Given I run uhwm with options -r./layout -l Layout
Then the output must match /testing_#expose_\d+/

View File

@ -1,13 +0,0 @@
Feature: layout registration
Scenario: sends the #register message to the layout with the display
Given a file named layout.rb with:
"""
class Layout
def register display
puts display
end
end
"""
When I run uhwm with option -r./layout -l Layout
Then the output must contain current display

View File

@ -0,0 +1,10 @@
Feature: layout client unmanagement
Background:
Given uhwm is running
And a first window is mapped
And a second window is mapped
Scenario: maps another window
When the second window is unmapped
Then the first window must be mapped

View File

@ -0,0 +1,7 @@
Feature: clients window properties updating
Scenario: logs when the window properties of a client change
Given uhwm is running
And a window is mapped
When the window name changes to "testing_new_name"
Then the output must match /updat.+testing_new_name/i

View File

@ -0,0 +1,5 @@
Feature: expose events handling
Scenario: logs when an expose event is handled
Given uhwm is running
Then the output must match /expos.+window.+\d+/i

View File

@ -0,0 +1,14 @@
Feature: manager client management
Background:
Given uhwm is running
Scenario: logs when a new client is managed
When a window requests to be mapped
Then the output must match /manag.+xclient/i
Scenario: manages a given client only once
When a window requests to be mapped 2 times
And I quit uhwm
Then the output must not match /manag.*\n.*manag/mi
And the output must not match /xerror/i

View File

@ -0,0 +1,13 @@
Feature: manager client unmanagement
Background:
Given uhwm is running
And a window is mapped
Scenario: logs when a new client is unmanaged
When the window is unmapped
Then the output must match /unmanag.+xclient/i
Scenario: unmanages client on destroy notify X events
When the window is destroyed
Then the output must match /unmanag.+xclient/i

View File

@ -0,0 +1,17 @@
Feature: X errors logging
Scenario: logs error details
Given a file named layout.rb with:
"""
class Layout
def register _; end
# Focusing a client before mapping will force an error
def << client
client.focus
end
end
"""
And uhwm is running with options -v -r./layout -l Layout
When a window requests to be mapped
Then the output must match /x.*error.+x_setinputfocus.+/i

View File

@ -1,18 +1,10 @@
Feature: run control file evaluation
Scenario: evaluates the default run control file when present
Given a run control file with:
"""
puts 'run control evaluation'
"""
When I start uhwm
Then the output must contain "run control evaluation"
Scenario: reports run control code in backtrace on errors
Given a run control file with:
"""
'no error on first line'
fail 'fails on second line'
fail 'testing_rc_failure'
"""
When I start uhwm
Then the output must match /\.uhwmrc\.rb:2:.+fails on second line/
Then the output must match /\.uhwmrc\.rb:2:.+testing_rc_failure/

View File

@ -3,23 +3,31 @@ Feature: `key' run control keyword
Scenario: defines code to run when given key is pressed
Given uhwm is running with this run control file:
"""
key(:f) { puts 'trigger f key code' }
key(:f) { puts 'testing_rc_key' }
"""
When I press the alt+f keys
Then the output must contain "trigger f key code"
Then the output must contain "testing_rc_key"
Scenario: defines code to run when given keys are pressed
Given uhwm is running with this run control file:
"""
key(:f, :shift) { puts 'testing_rc_key' }
"""
When I press the alt+shift+f keys
Then the output must contain "testing_rc_key"
Scenario: translates common key names to their X equivalent
Given uhwm is running with this run control file:
"""
key(:enter) { puts 'trigger return key code' }
key(:enter) { puts 'testing_rc_key' }
"""
When I press the alt+Return keys
Then the output must contain "trigger return key code"
Then the output must contain "testing_rc_key"
Scenario: translates upcased key names to combination with shift key
Given uhwm is running with this run control file:
"""
key(:F) { puts 'trigger shift+f key code' }
key(:F) { puts 'testing_rc_key' }
"""
When I press the alt+shift+f keys
Then the output must contain "trigger shift+f key code"
Then the output must contain "testing_rc_key"

View File

@ -0,0 +1,39 @@
Feature: `layout' run control keyword
Background:
Given a file named my_layout.rb with:
"""
class MyLayout
def initialize **options
@options = options
end
def register *_
puts "testing_rc_layout_#{@options.inspect}"
end
end
"""
Scenario: configures a layout class
Given a run control file with:
"""
layout MyLayout
"""
When I run uhwm with options -r./my_layout
Then the output must contain "testing_rc_layout_{}"
Scenario: configures a layout class with options
Given a run control file with:
"""
layout MyLayout, foo: :bar
"""
When I run uhwm with options -r./my_layout
Then the output must contain "testing_rc_layout_{:foo=>:bar}"
Scenario: configures a layout instance
Given a run control file with:
"""
layout MyLayout.new
"""
When I run uhwm with options -r./my_layout
Then the output must contain "testing_rc_layout_{}"

View File

@ -1,10 +1,9 @@
Feature: `modifier' run control keyword
Scenario: configures the modifier key
Given a run control file with:
Given uhwm is running with this run control file:
"""
modifier :ctrl
"""
And uhwm is running
When I press the ctrl+q keys
When I press the ctrl+shift+q keys
Then uhwm must terminate successfully

View File

@ -1,9 +1,8 @@
Feature: `worker' run control keyword
Scenario: configures the modifier key
Given a run control file with:
Given uhwm is running with this run control file:
"""
worker :mux
"""
And I start uhwm
Then the output must match /work.+event.+mux/i

View File

@ -1,5 +0,0 @@
Feature: connection to X server
Scenario: connects to X server
When I start uhwm
Then it must connect to X display

View File

@ -9,5 +9,4 @@ Feature: program termination
Scenario: logs about termination
When I tell uhwm to quit
Then uhwm must terminate successfully
And the output must match /terminat/i
Then the output must match /terminat/i

View File

@ -16,7 +16,6 @@ options:
end
Then /^the output must contain exactly the version$/ do
#require File.expand_path('../lib/uh/wm/version', __FILE__)
assert_exact_output "%s\n" % Uh::WM::VERSION, all_output
end
@ -24,6 +23,21 @@ Then /^the output must match \/([^\/]+)\/([a-z]*)$/ do |pattern, options|
uhwm_wait_output Regexp.new(pattern, options)
end
Then /^the output must not match \/([^\/]+)\/([a-z]*)$/ do |pattern, options|
expect(all_output).not_to match Regexp.new(pattern, options)
end
Then /^the output must match \/([^\/]+)\/([a-z]*) at least (\d+) times$/ do
|pattern, options, times|
uhwm_wait_output Regexp.new(pattern, options), times.to_i
end
Then /^the output must match \/([^\/]+)\/([a-z]*) exactly (\d+) times$/ do
|pattern, options, times|
scans = uhwm_wait_output Regexp.new(pattern, options)
expect(scans.size).to eq times.to_i
end
Then /^the output must contain:$/ do |content|
uhwm_wait_output content.to_s
end

View File

@ -24,7 +24,12 @@ When /^I run uhwm with options? (-.+)$/ do |options|
end
When /^I tell uhwm to quit$/ do
x_key 'alt+q'
uhwm_request_quit
end
When /^I quit uhwm$/ do
uhwm_request_quit
assert_exit_status 0
end
Then /^the exit status must be (\d+)$/ do |exit_status|

View File

@ -1,12 +1,68 @@
Given /^an ICCCM compliant window is mapped$/ do
icccm_window_start
timeout_until 'window not mapped after %d seconds' do
x_window_map_state(icccm_window_name) == 'IsViewable'
end
end
Given /^a(?:\s(\w+))? window is mapped$/ do |ident|
x_client(ident).map.sync
timeout_until 'window not mapped after %d seconds' do
x_window_map_state(x_client(ident).window_id) == 'IsViewable'
end
end
When /^I press the ([^ ]+) keys?$/ do |keys|
x_key keys
end
When /^a window requests to be mapped$/ do
x_window_map
When /^I press the ([^ ]+) keys? (\d+) times$/ do |keys, times|
times.to_i.times { x_key keys }
end
Then /^it must connect to X display$/ do
uhwm_wait_output 'Connected to'
expect(x_socket_check uhwm_pid).to be true
When /^I quickly press the ([^ ]+) keys? (\d+) times$/ do |keys, times|
x_key [keys] * times.to_i, delay: 0
end
When /^a window requests to be mapped$/ do
x_client.map.sync
end
When /^a window requests to be mapped (\d+) times$/ do |times|
x_client.map times: times.to_i
end
When /^the(?:\s(\w+))? window is unmapped$/ do |ident|
x_client(ident).unmap.sync
timeout_until 'window not unmapped after %d seconds' do
x_window_map_state(x_client(ident).window_id) == 'IsUnMapped'
end
end
When /^the window is destroyed$/ do
x_client.destroy.sync
end
When /^the window name changes to "([^"]+)"$/ do |name|
x_client.window_name = name
end
Then /^the(?:\s(\w+))? window must be mapped$/ do |ident|
timeout_until 'window not mapped after %d seconds' do
x_window_map_state(x_client(ident).window_id) == 'IsViewable'
end
end
Then /^the ICCCM window must be unmapped by the manager$/ do
uhwm_wait_output /unmanag.+#{icccm_window_name}/i
end
Then /^the window must be focused$/ do
timeout_until 'window not focused after %d seconds' do
x_focused_window_id == x_client.window_id
end
end
Then /^the input event mask must include (.+)$/ do |mask|
expect(x_input_event_masks).to include mask
end

View File

@ -28,6 +28,10 @@ Around '@other_wm_running' do |_, block|
with_other_wm { block.call }
end
After '@icccm_window' do
icccm_window_ensure_stop
end
if ENV.key? 'TRAVIS'
ENV['UHWMTEST_TIMEOUT'] = 8.to_s
end

View File

@ -0,0 +1,15 @@
Feature: blocking worker
Scenario: processes initial events
Given uhwm is running with options -d -w block
Then the output must match /xevent/i at least 2 times
Scenario: processes generated events
Given a run control file with:
"""
key(:f) { puts 'testing_worker_read' }
"""
And uhwm is running with options -v -w block
When I quickly press the alt+f key 1024 times
And I quit uhwm
Then the output must match /(testing_worker_read)/ exactly 1024 times

View File

@ -0,0 +1,15 @@
Feature: multiplexing worker
Scenario: processes initial events
Given uhwm is running with options -d -w mux
Then the output must match /xevent/i at least 2 times
Scenario: processes generated events
Given a run control file with:
"""
key(:f) { puts 'testing_worker_read' }
"""
And uhwm is running with options -d -w mux
When I press the alt+f key 3 times
And I quit uhwm
Then the output must match /(testing_worker_read)/ exactly 3 times

View File

@ -3,6 +3,8 @@ require 'logger'
require 'optparse'
require 'uh'
require 'uh/wm/env_logging'
require 'uh/wm/actions_handler'
require 'uh/wm/cli'
require 'uh/wm/client'

View File

@ -1,32 +1,75 @@
module Uh
module WM
class ActionsHandler
include EnvLogging
extend Forwardable
def_delegator :@env, :layout
def initialize env, events
@env, @events = env, events
end
def evaluate code
instance_eval &code
def evaluate code = nil, &block
if code
instance_exec &code
else
instance_exec &block
end
end
def quit
log 'Quit requested'
@events.emit :quit
end
def execute command
@env.log "Execute: #{command}"
log "Execute: #{command}"
pid = fork do
fork do
Process.setsid
begin
exec command
rescue Errno::ENOENT => e
@env.log_error "ExecuteError: #{e}"
log_error "ExecuteError: #{e}"
end
end
end
Process.waitpid pid
end
def kill_current
layout.current_client.kill
end
def log_separator
log '- ' * 24
end
def method_missing(m, *args, &block)
if respond_to? m
meth = layout_method m
log "#{layout.class.name}##{meth} #{args.inspect}"
begin
layout.send(meth, *args)
rescue NoMethodError
log_error "Layout does not implement `#{meth}'"
end
else
super
end
end
def respond_to_missing?(m, *)
m.to_s =~ /\Alayout_/ || super
end
private
def layout_method(m)
m.to_s.gsub(/\Alayout_/, 'handle_').to_sym
end
end
end
end

View File

@ -3,6 +3,8 @@ module Uh
class CLI
ArgumentError = Class.new(ArgumentError)
include EnvLogging
USAGE = "Usage: #{File.basename $0} [options]".freeze
EX_USAGE = 64
@ -63,10 +65,10 @@ module Uh
end
opts.on '-r', '--require PATH', 'require ruby feature' do |feature|
require feature
@env.log "Loaded `#{feature}' ruby feature"
log "Loaded `#{feature}' ruby feature"
end
opts.on '-l', '--layout LAYOUT', 'specify layout' do |layout|
@env.layout_class = self.class.const_get layout.to_sym
@env.layout_class = Object.const_get layout.to_sym
end
opts.on '-w', Workers.types, '--worker WORKER',
'specify worker' do |worker|

View File

@ -1,8 +1,10 @@
module Uh
module WM
class Client
attr_reader :window, :unmap_count
attr_accessor :geo
include GeoAccessors
attr_reader :window
attr_accessor :geo, :unmap_count
def initialize window, geo = nil
@window = window
@ -31,6 +33,16 @@ module Uh
@wclass ||= @window.wclass
end
def update_window_properties
@wname = @window.name
@wclass = @window.wclass
end
def configure
@window.configure @geo
self
end
def moveresize
@window.moveresize @geo
self
@ -54,6 +66,20 @@ module Uh
@window.focus
self
end
def kill
if @window.icccm_wm_protocols.include? :WM_DELETE_WINDOW
@window.icccm_wm_delete
else
@window.kill
end
self
end
def kill!
window.kill
self
end
end
end
end

View File

@ -16,7 +16,9 @@ module Uh
end
def emit *key, args: []
@hooks[translate_key key].each { |e| e.call *args }
value = nil
@hooks[translate_key key].each { |e| value = e.call *args }
value
end

View File

@ -5,7 +5,7 @@ module Uh
MODIFIER = :mod1
KEYBINDS = {
q: proc { quit }
[:q, :shift] => proc { quit }
}.freeze
WORKER = :block
@ -21,8 +21,8 @@ module Uh
def_delegators :@output, :print, :puts
attr_reader :output, :keybinds
attr_accessor :verbose, :debug, :rc_path, :layout_class, :modifier,
:worker
attr_accessor :verbose, :debug, :rc_path, :modifier, :worker,
:layout, :layout_class
def initialize output
@output = output

8
lib/uh/wm/env_logging.rb Normal file
View File

@ -0,0 +1,8 @@
module Uh
module WM
module EnvLogging
extend Forwardable
def_delegators :@env, :log, :log_error, :log_debug
end
end
end

View File

@ -6,6 +6,7 @@ module Uh
Events::SUBSTRUCTURE_REDIRECT_MASK |
Events::SUBSTRUCTURE_NOTIFY_MASK |
Events::STRUCTURE_NOTIFY_MASK
DEFAULT_GEO = Geo.new(0, 0, 320, 240).freeze
attr_reader :modifier, :display, :clients
@ -45,6 +46,42 @@ module Uh
@display.grab_key keysym.to_s, mod_mask
end
def configure window
if client = client_for(window)
client.configure
else
geo = @events.emit :configure, args: window
window.configure_event geo ? geo : DEFAULT_GEO
end
end
def map window
return if window.override_redirect? || client_for(window)
@clients << client = Client.new(window)
@events.emit :manage, args: client
@display.listen_events window, Events::PROPERTY_CHANGE_MASK
end
def unmap window
return unless client = client_for(window)
if client.unmap_count > 0
client.unmap_count -= 1
else
unmanage client
end
end
def destroy window
return unless client = client_for(window)
unmanage client
end
def update_properties window
return unless client = client_for(window)
client.update_window_properties
@events.emit :change, args: client
end
def handle_next_event
handle @display.next_event
end
@ -63,7 +100,7 @@ module Uh
private
def handle_error *args
@dispatcher.emit :error, args: args
@events.emit :xerror, args: args
end
def handle_key_press event
@ -73,9 +110,37 @@ module Uh
@events.emit :key, *key_selector
end
def handle_configure_request event
configure event.window
end
def handle_destroy_notify event
destroy event.window
end
def handle_expose event
@events.emit :expose, args: event.window
end
def handle_map_request event
@clients << client = Client.new(event.window)
@events.emit :manage, args: client
map event.window
end
def handle_property_notify event
update_properties event.window
end
def handle_unmap_notify event
unmap event.window
end
def client_for window
@clients.find { |e| e.window == window }
end
def unmanage client
@clients.delete client
@events.emit :unmanage, args: client
end
def check_other_wm!

View File

@ -28,8 +28,20 @@ module Uh
@env.modifier = mod
end
def key keysym, &block
@env.keybinds[translate_keysym keysym] = block
def key *keysyms, &block
@env.keybinds[translate_keysym *keysyms] = block
end
def layout arg, **options
if arg.is_a? Class
if options.any?
@env.layout = arg.new options
else
@env.layout_class = arg
end
else
@env.layout = arg
end
end
def worker type, **options
@ -39,7 +51,8 @@ module Uh
private
def translate_keysym keysym
def translate_keysym keysym, modifier = nil
return [translate_keysym(keysym)[0].to_sym, modifier] if modifier
translate_key = keysym.to_s.downcase.to_sym
translated_keysym = KEYSYM_TRANSLATIONS.key?(translate_key) ?
KEYSYM_TRANSLATIONS[translate_key] :

View File

@ -1,6 +1,8 @@
module Uh
module WM
class Runner
include EnvLogging
class << self
def run env, **options
runner = new env, **options
@ -49,24 +51,17 @@ module Uh
def connect_manager
manager.connect
@env.keybinds.each do |keysym, _|
manager.grab_key *keysym
end
@env.keybinds.each { |keysym, _| manager.grab_key *keysym }
end
def worker
@worker ||= Workers.build(*(@env.worker)).tap do |w|
w.on_read do
@env.log_debug 'Processing pending events'
@manager.handle_pending_events
end
w.on_read_next do
@env.log_debug 'Processing next event'
@manager.handle_next_event
end
w.before_watch { @manager.handle_pending_events }
w.on_read { @manager.handle_pending_events }
w.on_read_next { @manager.handle_next_event }
w.on_timeout do |*args|
@env.log_debug "Worker timeout: #{args.inspect}"
@env.log_debug 'Flushing X output buffer'
log_debug "Worker timeout: #{args.inspect}"
log_debug 'Flushing X output buffer'
@manager.flush
end
end
@ -74,12 +69,12 @@ module Uh
def run_until &block
worker.watch @manager
@env.log "Working events with `#{worker.class}'"
log "Working events with `#{worker.class}'"
worker.work_events until block.call
end
def terminate
@env.log "Terminating..."
log "Terminating..."
manager.disconnect
end
@ -92,24 +87,41 @@ module Uh
def register_manager_hooks
@events.on :connecting do |display|
@env.log_debug "Connecting to X server on `#{display}'"
log_debug "Connecting to X server on `#{display}'"
end
@events.on :connected do |display|
@env.log "Connected to X server on `#{display}'"
log "Connected to X server on `#{display}'"
end
@events.on(:disconnected) { @env.log "Disconnected from X server" }
@events.on(:disconnected) { log "Disconnected from X server" }
@events.on(:xevent) { |event| XEventLogger.new(env).log_event event }
@events.on(:xerror) { |*error| XEventLogger.new(env).log_xerror *error }
end
def register_layout_hooks
@events.on :connected do |display|
@env.log "Registering layout `#{layout.class}'"
log "Registering layout `#{layout.class}'"
layout.register display
end
@events.on :configure do |window|
log "Configuring window: #{window}"
layout.suggest_geo
end
@events.on :manage do |client|
@env.log "Manage client #{client}"
log "Managing client #{client}"
layout << client
end
@events.on :unmanage do |client|
log "Unmanaging client #{client}"
layout.remove client
end
@events.on :change do |client|
log "Updating client #{client}"
layout.update client
end
@events.on :expose do |window|
log "Exposing window: #{window}"
layout.expose window
end
end
def register_keybinds_hooks
@ -120,6 +132,8 @@ module Uh
class XEventLogger
include EnvLogging
def initialize env
@env = env
end
@ -132,13 +146,17 @@ module Uh
"window: #{xev.window}"
end
@env.log_debug [
log_debug [
'XEvent',
xev.type,
xev.send_event ? 'SENT' : nil,
complement
].compact.join ' '
end
def log_xerror req, resource_id, msg
log_error "XERROR: #{resource_id} #{req} #{msg}"
end
end
end
end

View File

@ -4,45 +4,67 @@ module Uh
module WM
module Testing
module AcceptanceHelpers
TIMEOUT_DEFAULT = 2
QUIT_KEYBINDING = 'alt+shift+q'.freeze
LOG_READY = 'Working events'.freeze
def icccm_window_start
@icccm_window = ChildProcess.build(*%w[xmessage window])
@icccm_window.start
end
def icccm_window_ensure_stop
@icccm_window.stop
end
def icccm_window_name
'xmessage'
end
def uhwm_run options = '-v'
command = %w[uhwm]
command << options if options
@interactive = @process = run command.join ' '
end
def uhwm
@process
end
def uhwm_request_quit
x_key QUIT_KEYBINDING
end
def uhwm_ensure_stop
if @process
x_key 'alt+q'
x_key 'alt+shift+q'
@process.terminate
end
end
def uhwm_pid
@process.pid
end
def uhwm_output
@process.stdout
end
def uhwm_wait_output message
def uhwm_wait_output message, times = 1, value = nil
output = -> { @process.stdout + @process.stderr }
timeout_until do
case message
when Regexp then output.call =~ message
when String then assert_partial_output_interactive message
when Regexp then (value = output.call.scan(message)).size >= times
when String then output.call.include? message
end
end
value
rescue TimeoutError => e
fail [
"expected `#{message}' not seen after #{e.timeout} seconds in:",
" ```\n#{output.call.lines.map { |e| " #{e}" }.join} ```"
].join "\n"
fail <<-eoh
expected `#{message}' (#{times}) not seen after #{e.timeout} seconds in:
```\n#{output.call.lines.map { |e| " #{e}" }.join} ```
eoh
end
def uhwm_wait_ready
uhwm_wait_output LOG_READY
end
def uhwm_run_wait_ready options = nil
if options then uhwm_run options else uhwm_run end
uhwm_wait_output 'Connected to'
uhwm_wait_ready
end
def with_other_wm
@ -56,42 +78,48 @@ module Uh
@other_wm
end
def x_client
@x_client ||= XClient.new
end
def x_key key
fail "cannot simulate X key `#{key}'" unless system "xdotool key #{key}"
end
def x_socket_check pid
case RbConfig::CONFIG['host_os']
when /linux/
`netstat -xp 2> /dev/null`.lines.grep /\s+#{pid}\/ruby/
else
`sockstat -u`.lines.grep /\s+ruby.+\s+#{pid}/
end.any?
end
def x_window_name
@x_client.window_name
end
def x_window_map
x_client.map.sync
def x_client ident = nil
ident ||= :default
@x_clients ||= {}
@x_clients[ident] ||= XClient.new(ident)
end
def x_clients_ensure_stop
@x_client and @x_client.terminate
@x_clients and @x_clients.any? and @x_clients.values.each &:terminate
end
def x_focused_window_id
Integer(`xdpyinfo`[/^focus:\s+window\s+(0x\h+)/, 1])
end
def x_input_event_masks
`xdpyinfo`[/current input event mask:\s+0x\h+([\w\s]+):/, 1]
.split(/\s+/)
.grep /Mask\z/
end
def x_key *k, delay: 12
k = k.join " key --delay #{delay} "
fail "cannot simulate X key `#{k}'" unless system "xdotool key #{k}"
end
def x_window_map_state window_selector
select_args = case window_selector
when Integer then "-id #{window_selector}"
when String then "-name #{window_selector}"
else fail ArgumentError,
"not an Integer nor a String: `#{window_selector.inspect}'"
end
`xwininfo #{select_args} 2> /dev/null`[/Map State: (\w+)/, 1]
end
private
def timeout_until
def timeout_until message = 'condition not met after %d seconds'
timeout = ENV.key?('UHWMTEST_TIMEOUT') ?
ENV['UHWMTEST_TIMEOUT'].to_i :
1
TIMEOUT_DEFAULT
Timeout.timeout(timeout) do
loop do
break if yield
@ -99,7 +127,7 @@ module Uh
end
end
rescue Timeout::Error
fail TimeoutError.new('execution expired', timeout)
fail TimeoutError.new(message % timeout, timeout)
end
@ -115,8 +143,8 @@ module Uh
class XClient
attr_reader :name
def initialize
@name = "#{self.class.name.split('::').last}/#{object_id}"
def initialize name = object_id
@name = "#{self.class.name.split('::').last}/#{name}"
@geo = Geo.new(0, 0, 640, 480)
@display = Display.new.tap { |o| o.open }
end
@ -130,19 +158,37 @@ module Uh
end
def window
@window ||= @display.create_window(@geo).tap do |o|
o.name = @name
@window ||= @display.create_window(@geo).tap { |o| o.name = @name }
end
def window_id
@window.id
end
def window_name
@name
end
def map
def window_name= name
@name = @window.name = name
window.name
end
def map times: 1
times.times { window.map }
window.map
self
end
def unmap
window.unmap
self
end
def destroy
window.destroy
self
end
end
end
end

View File

@ -1,5 +1,5 @@
module Uh
module WM
VERSION = '0.0.1'
VERSION = '0.0.6'
end
end

View File

@ -2,7 +2,7 @@ module Uh
module WM
module Workers
class Base
CALLBACKS = %w[before_wait on_timeout on_read on_read_next].freeze
CALLBACKS = %w[before_watch on_timeout on_read on_read_next].freeze
def initialize **options
@ios = []

View File

@ -2,13 +2,15 @@ module Uh
module WM
module Workers
class Mux < Base
def initialize timeout: 1
TIMEOUT_DEFAULT = 1
def initialize timeout: TIMEOUT_DEFAULT
super
@timeout = timeout
end
def work_events
@before_wait.call if @before_wait
@before_watch.call if @before_watch
if res = select(@ios, [], [], @timeout) then @on_read.call res
else @on_timeout.call if @on_timeout end
end

View File

@ -2,7 +2,11 @@ require 'headless'
require 'uh/wm'
Dir['spec/support/**/*.rb'].map { |e| require e.gsub 'spec/', '' }
RSpec.configure do |config|
config.include Factories
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end

28
spec/support/factories.rb Normal file
View File

@ -0,0 +1,28 @@
module Factories
def build_geo x = 0, y = 0, width = 640, height = 480
Uh::Geo.new(x, y, width, height)
end
def build_client window = mock_window
Uh::WM::Client.new(window)
end
def mock_event type = :xany, **options
double 'event', type: type, **options
end
def mock_event_key_press key, modifier_mask
mock_event :key_press,
key: 'f',
modifier_mask: modifier_mask
end
def mock_window override_redirect: false, icccm_wm_protocols: []
instance_spy Uh::Window, 'window',
to_s: 'wid',
name: 'wname',
wclass: 'wclass',
override_redirect?: override_redirect,
icccm_wm_protocols: icccm_wm_protocols
end
end

View File

@ -6,10 +6,15 @@ module Uh
subject(:actions) { described_class.new env, events }
describe '#evaluate' do
it 'evaluates given code' do
it 'evaluates code given as Proc argument' do
expect { actions.evaluate proc { throw :action_code } }
.to throw_symbol :action_code
end
it 'evaluates code given as block' do
expect { actions.evaluate { throw :action_code } }
.to throw_symbol :action_code
end
end
describe '#quit' do
@ -18,6 +23,35 @@ module Uh
actions.quit
end
end
describe '#kill_current' do
let(:client) { instance_spy Client }
context 'when layout has a client' do
before do
allow(actions.layout).to receive(:current_client) { client }
end
it 'kills layout current client' do
expect(client).to receive :kill
actions.kill_current
end
end
end
describe '#log_separator' do
it 'logs a separator' do
expect(env).to receive(:log).with /(?:- ){20,}/
actions.log_separator
end
end
describe '#layout_*' do
it 'delegates messages to the layout with handle_ prefix' do
expect(env.layout).to receive :handle_screen_sel
actions.layout_screen_sel :succ
end
end
end
end
end

View File

@ -1,11 +1,9 @@
module Uh
module WM
RSpec.describe Client do
let(:geo) { Geo.new(0, 0, 640, 480) }
let(:window) do
instance_spy Window, 'window', to_s: 'wid',
name: 'wname', wclass: 'wclass'
end
let(:protocols) { [] }
let(:window) { mock_window icccm_wm_protocols: protocols }
let(:geo) { build_geo }
subject(:client) { described_class.new window, geo }
it 'is not visible' do
@ -33,7 +31,7 @@ module Uh
expect(client.to_s).to include geo.to_s
end
it 'includes window id' do
it 'includes window string representation' do
expect(client.to_s).to include 'wid'
end
end
@ -50,6 +48,35 @@ module Uh
end
end
describe '#update_window_properties' do
it 'updates the cached window name' do
client.name
allow(window).to receive(:name) { 'new name' }
expect { client.update_window_properties }
.to change { client.name }
.from('wname').to 'new name'
end
it 'updates the cached window class' do
client.wclass
allow(window).to receive(:wclass) { 'new class' }
expect { client.update_window_properties }
.to change { client.wclass }
.from('wclass').to 'new class'
end
end
describe '#configure' do
it 'configures the window with client geo' do
expect(window).to receive(:configure).with geo
client.configure
end
it 'returns self' do
expect(client.configure).to be client
end
end
describe '#moveresize' do
it 'moveresizes the window with client geo' do
expect(window).to receive(:moveresize).with geo
@ -117,6 +144,37 @@ module Uh
expect(client.focus).to be client
end
end
describe '#kill' do
it 'kills the window' do
expect(window).to receive :kill
client.kill
end
it 'returns self' do
expect(client.kill).to be client
end
context 'when window supports icccm wm delete' do
let(:protocols) { [:WM_DELETE_WINDOW] }
it 'icccm deletes the window' do
expect(window).to receive :icccm_wm_delete
client.kill
end
end
end
describe '#kill!' do
it 'kills the window' do
expect(window).to receive :kill
client.kill!
end
it 'returns self' do
expect(client.kill!).to be client
end
end
end
end
end

View File

@ -45,6 +45,11 @@ module Uh
expect { dispatcher.emit :hook_key }.to throw_symbol :hook_code
end
it 'returns the value returned by a registered hook' do
dispatcher.on(:hook_key) { :hook_code }
expect(dispatcher.emit :hook_key).to eq :hook_code
end
context 'when no hooks are registered for given key' do
it 'does not call another hook' do
dispatcher.on(:hook_key) { throw :hook_code }

View File

@ -25,8 +25,8 @@ module Uh
expect(env.modifier).to eq :mod1
end
it 'has defaults key bindings set' do
expect(env.keybinds.keys).to eq %i[q]
it 'has default key binding for quit set' do
expect(env.keybinds.keys).to include [:q, :shift]
end
it 'has the blocking worker by default' do
@ -70,19 +70,27 @@ module Uh
end
describe '#layout' do
context 'when a layout class is set' do
let(:some_layout) { Class.new }
let(:some_layout_class) { Class.new }
before { env.layout_class = some_layout }
it 'returns the default layout' do
expect(env.layout).to be_an_instance_of ::Uh::Layout
end
context 'when a layout is set' do
let(:some_layout) { some_layout_class.new }
before { env.layout = some_layout }
it 'returns the assigned layout' do
expect(env.layout).to be some_layout
end
end
context 'when a layout class is set' do
before { env.layout_class = some_layout_class }
it 'returns a new instance of this layout class' do
expect(env.layout).to be_an_instance_of some_layout
end
end
context 'when a layout class is not set' do
it 'returns an instance of the default layout' do
expect(env.layout).to be_an_instance_of ::Uh::Layout
expect(env.layout).to be_an_instance_of some_layout_class
end
end
end
@ -129,7 +137,8 @@ module Uh
describe '#log_logger_level' do
it 'logs the logger level' do
expect(env.logger).to receive(:info).with /log.+(warn|info|debug).+level/i
expect(env.logger)
.to receive(:info).with /log.+(warn|info|debug).+level/i
env.log_logger_level
end
end

View File

@ -2,15 +2,13 @@ module Uh
module WM
RSpec.describe Manager do
let(:block) { proc { } }
let(:window) { mock_window }
let(:client) { build_client window }
let(:events) { Dispatcher.new }
let(:modifier) { :mod1 }
let(:display) { Display.new }
subject(:manager) { described_class.new events, modifier, display }
it 'has a new display' do
expect(manager.display).to be_a Display
end
it 'has no clients' do
expect(manager.clients).to be_empty
end
@ -106,6 +104,174 @@ module Uh
end
end
describe '#configure' do
context 'with new window' do
it 'sends a configure event to the window with a default geo' do
expect(window)
.to receive(:configure_event).with(build_geo 0, 0, 320, 240)
manager.configure window
end
context 'when :configure event returns a geo' do
it 'sends a configure event with geo returned by event' do
geo = build_geo 0, 0, 42, 42
events.on(:configure) { geo }
expect(window).to receive(:configure_event).with geo
manager.configure window
end
end
end
context 'with known window' do
before { manager.clients << client }
it 'tells the client to configure' do
expect(client).to receive :configure
manager.configure window
end
end
end
describe '#map' do
let(:display) { instance_spy Display }
it 'registers a new client wrapping the given window' do
manager.map window
expect(manager.clients[0])
.to be_a(Client)
.and have_attributes(window: window)
end
it 'registers new client only once for a given window' do
manager.map window
expect { manager.map window }.not_to change { manager.clients }
end
it 'ignores event when window has override redirect' do
allow(window).to receive(:override_redirect?) { true }
expect { manager.map window }.not_to change { manager.clients }
end
it 'emits :manage event with the registered client' do
events.on :manage, &block
expect(block).to receive :call do |client|
expect(client)
.to be_a(Client)
.and have_attributes(window: window)
end
manager.map window
end
it 'listens for property notify events on given window' do
expect(display)
.to receive(:listen_events)
.with window, Events::PROPERTY_CHANGE_MASK
manager.map window
end
end
describe '#unmap' do
before { manager.clients << client }
context 'when client unmap count is 0 or less' do
it 'preserves the client unmap count' do
expect { manager.unmap window }.not_to change { client.unmap_count }
end
it 'unregisters the client' do
manager.unmap window
expect(manager.clients).not_to include client
end
it 'emits :unmanage event with the client' do
events.on :unmanage, &block
expect(block).to receive(:call).with client
manager.unmap window
end
end
context 'when client unmap count is strictly positive' do
before { client.unmap_count += 1 }
it 'does not unregister the client' do
manager.unmap window
expect(manager.clients).to include client
end
it 'decrements the unmap count' do
manager.unmap window
expect(client.unmap_count).to eq 0
end
end
context 'with unknown window' do
let(:unknown_window) { window.dup }
it 'does not change registered clients' do
expect { manager.unmap unknown_window }
.not_to change { manager.clients }
end
it 'does not emit any event' do
expect(events).not_to receive :emit
manager.unmap unknown_window
end
end
end
describe '#destroy' do
before { manager.clients << client }
it 'unregisters the client' do
manager.destroy window
expect(manager.clients).not_to include client
end
it 'emits :unmanage event with the client' do
events.on :unmanage, &block
expect(block).to receive(:call).with client
manager.destroy window
end
context 'with unknown window' do
let(:unknown_window) { window.dup }
it 'does not change registered clients' do
expect { manager.destroy unknown_window }
.not_to change { manager.clients }
end
it 'does not emit any event' do
expect(events).not_to receive :emit
manager.destroy unknown_window
end
end
end
describe '#update_properties' do
context 'with known window' do
before { manager.clients << client }
it 'tells the client to update its window properties' do
expect(client).to receive :update_window_properties
manager.update_properties window
end
it 'emits :change event with the client' do
events.on :change, &block
expect(block).to receive(:call).with client
manager.update_properties window
end
end
context 'with unknown window' do
it 'does not emit any event' do
expect(events).not_to receive :emit
manager.update_properties window
end
end
end
describe '#handle_next_event' do
it 'handles the next available event on display' do
event = double 'event'
@ -116,7 +282,7 @@ module Uh
end
describe '#handle_pending_events' do
let(:event) { double 'event' }
let(:event) { mock_event }
context 'when an event is pending on display' do
before do
@ -144,7 +310,7 @@ module Uh
end
describe '#handle' do
let(:event) { double 'event', type: :any }
let(:event) { mock_event }
it 'emits :xevent event with the X event' do
events.on :xevent, &block
@ -153,12 +319,7 @@ module Uh
context 'when key_press event is given' do
let(:mod_mask) { KEY_MODIFIERS[modifier] }
let(:event) do
double 'event',
type: :key_press,
key: 'f',
modifier_mask: mod_mask
end
let(:event) { mock_event_key_press 'f', mod_mask }
it 'emits :key event with the corresponding key' do
events.on(:key, :f) { throw :key_press_code }
@ -175,23 +336,47 @@ module Uh
end
end
context 'when map_request event is given' do
let(:event) { double 'event', type: :map_request, window: :window }
context 'when configure request event is given' do
let(:event) { mock_event :configure_request, window: :window }
it 'registers a new client wrapping the event window' do
it 'configures the event window' do
expect(manager).to receive(:configure).with :window
manager.handle event
expect(manager.clients[0])
.to be_a(Client)
.and have_attributes(window: :window)
end
end
it 'emits :manage event with the registered client' do
events.on :manage, &block
expect(block).to receive :call do |client|
expect(client)
.to be_a(Client)
.and have_attributes(window: :window)
context 'when destroy_notify event is given' do
let(:event) { mock_event :destroy_notify, window: :window }
it 'destroy the event window' do
expect(manager).to receive(:destroy).with :window
manager.handle event
end
end
context 'when map_request event is given' do
let(:event) { mock_event :map_request, window: :window }
it 'maps the event window' do
expect(manager).to receive(:map).with :window
manager.handle event
end
end
context 'when unmap_notify event is given' do
let(:event) { mock_event :unmap_notify, window: :window }
it 'unmaps the event window' do
expect(manager).to receive(:unmap).with :window
manager.handle event
end
end
context 'when property_notify event is given' do
let(:event) { mock_event :property_notify, window: :window }
it 'updates event window properties' do
expect(manager).to receive(:update_properties).with :window
manager.handle event
end
end

View File

@ -5,7 +5,6 @@ module Uh
RSpec.describe RunControl do
include FileSystemHelpers
let(:code) { :run_control_code }
let(:env) { Env.new(StringIO.new) }
subject(:rc) { described_class.new env }
@ -28,14 +27,6 @@ module Uh
allow(described_class).to receive(:new) { rc }
described_class.evaluate env
end
context 'when run control file is not present' do
before { env.rc_path = 'non_existent_rc_file.rb' }
it 'does not raise any error' do
expect { described_class.evaluate env }.not_to raise_error
end
end
end
describe '#evaluate' do
@ -65,6 +56,11 @@ module Uh
expect(env.keybinds.keys).to include :f
end
it 'registers a combined keys binding in the env' do
rc.key :f, :shift, &code
expect(env.keybinds.keys).to include %i[f shift]
end
it 'registers given block with the key binding' do
rc.key :f, &code
expect(env.keybinds[:f].call).to eq :keybind_code
@ -81,6 +77,31 @@ module Uh
end
end
describe '#layout' do
context 'when given a class' do
let(:layout_class) { Class.new }
it 'sets a layout class in the env' do
rc.layout layout_class
expect(env.layout_class).to be layout_class
end
context 'when given options' do
it 'instantiates the class with given options' do
expect(layout_class).to receive(:new).with(foo: :bar)
rc.layout layout_class, foo: :bar
end
end
end
context 'when given an object' do
it 'sets a layout instance in the env' do
rc.layout layout = Object.new
expect(env.layout).to eq layout
end
end
end
describe '#worker' do
it 'sets the worker type in the env' do
rc.worker :some_worker

View File

@ -1,6 +1,11 @@
SomeLayout = Class.new do
define_method(:register) { |*args| }
define_method(:<<) { |*args| }
include Factories
define_method(:register) { |*_| }
define_method(:suggest_geo) { build_geo 0, 0, 42, 42 }
define_method(:<<) { |*_| }
define_method(:remove) { |*_| }
define_method(:update) { |*_| }
end
module Uh
@ -14,10 +19,6 @@ module Uh
env.rc_path = 'non_existent_run_control.rb'
end
it 'has the env' do
expect(runner.env).to be env
end
it 'has a dispatcher' do
expect(runner.events).to be_a Dispatcher
end
@ -70,41 +71,63 @@ module Uh
describe '#register_event_hooks' do
it 'registers quit event hook' do
runner.register_event_hooks
expect(runner).to receive(:stop!)
expect(runner).to receive :stop!
runner.events.emit :quit
end
it 'registers layout hook for :connected event' do
context 'layout hooks' do
it 'registers for :connected event' do
runner.register_event_hooks
expect(env.layout).to receive(:register).with :display
runner.events.emit :connected, args: :display
end
it 'registers layout hook for :manage event' do
it 'registers for :configure event' do
runner.register_event_hooks
expect(env.layout).to receive(:<<).with :window
runner.events.emit :manage, args: :window
expect(runner.events.emit :configure, args: :window)
.to eq build_geo 0, 0, 42, 42
end
it 'registers key bindings event hooks' do
it 'registers for :manage event' do
runner.register_event_hooks
expect(env.layout).to receive(:<<).with :client
runner.events.emit :manage, args: :client
end
it 'registers for :unmanage event' do
runner.register_event_hooks
expect(env.layout).to receive(:remove).with :client
runner.events.emit :unmanage, args: :client
end
it 'registers for :change event' do
runner.register_event_hooks
expect(env.layout).to receive(:update).with :client
runner.events.emit :change, args: :client
end
end
context 'keys hooks' do
it 'registers for :key' do
env.keybinds[:f] = -> { }
runner.register_event_hooks
expect(runner.events[:key, :f]).not_to be_empty
end
it 'registers combined key bindings event hooks' do
it 'registers for :key with combined key bindings' do
env.keybinds[[:f, :shift]] = -> { }
runner.register_event_hooks
expect(runner.events[:key, :f, :shift]).not_to be_empty
end
it 'registers key bindings code evaluation with the actions handler' do
it 'registers code evaluation with the actions handler' do
env.keybinds[:f] = code = proc { }
runner.register_event_hooks
expect(runner.actions).to receive(:evaluate).with code
runner.events.emit :key, :f
end
end
end
describe '#connect_manager' do
let(:manager) { instance_spy Manager }
@ -127,17 +150,22 @@ module Uh
expect(runner.worker).to respond_to :work_events
end
it 'setups the read callback to tell manager to handle pending events' do
it 'setups the before_watch callback' do
expect(runner.manager).to receive :handle_pending_events
runner.worker.before_watch.call
end
it 'setups the read callback' do
expect(runner.manager).to receive :handle_pending_events
runner.worker.on_read.call
end
it 'setups the read_next callback to tell manager to handle next event' do
it 'setups the read_next callback' do
expect(runner.manager).to receive :handle_next_event
runner.worker.on_read_next.call
end
it 'setups the timeout callback to tell manager to flush the output' do
it 'setups the timeout callback' do
expect(runner.manager).to receive :flush
runner.worker.on_timeout.call
end

View File

@ -3,8 +3,9 @@ require File.expand_path('../lib/uh/wm/version', __FILE__)
Gem::Specification.new do |s|
s.name = 'uh-wm'
s.version = Uh::WM::VERSION.dup
s.summary = 'uh window manager'
s.summary = 'minimalistic tiling and stacking window manager for X'
s.description = s.name
s.license = 'BSD-3-Clause'
s.homepage = 'https://rubygems.org/gems/uh-wm'
s.authors = 'Thibault Jouan'
@ -14,8 +15,8 @@ Gem::Specification.new do |s|
s.test_files = s.files.grep /\A(spec|features)\//
s.executables = s.files.grep(/\Abin\//) { |f| File.basename(f) }
s.add_dependency 'uh', '2.0.0.pre1'
s.add_dependency 'uh-layout', '0.2.0.pre'
s.add_dependency 'uh', '2.0.0'
s.add_dependency 'uh-layout', '~> 0.4.0'
s.add_development_dependency 'aruba', '~> 0.6'
s.add_development_dependency 'cucumber', '~> 2.0'