Merge branch 'events-key_press-quit'
Implement a few basic classes to handle different concerns (Runner, Manager, Dispatcher) and integrate them together with a hard coded hook for `mod1+q' key binding, which will initiate program termination.
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,3 @@ | |||||||
| /Gemfile-custom.rb | /Gemfile-custom.rb | ||||||
| /Gemfile.lock | /Gemfile.lock | ||||||
|  | /tmp/ | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								bin/uhwm
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								bin/uhwm
									
									
									
									
									
								
							| @@ -3,4 +3,3 @@ | |||||||
| require 'uh/wm' | require 'uh/wm' | ||||||
|  |  | ||||||
| Uh::WM::CLI.run(ARGV) | Uh::WM::CLI.run(ARGV) | ||||||
| sleep 8 |  | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								features/actions/quit.feature
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								features/actions/quit.feature
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | Feature: quit action | ||||||
|  |  | ||||||
|  |   Scenario: quits on keybing press | ||||||
|  |     Given uhwm is running | ||||||
|  |     When I press the default quit key binding | ||||||
|  |     Then uhwm should terminate successfully | ||||||
| @@ -4,6 +4,15 @@ def uhwm_run options = nil | |||||||
|   @interactive = @process = run command.join ' ' |   @interactive = @process = run command.join ' ' | ||||||
| end | end | ||||||
|  |  | ||||||
|  | def uhwm_run_wait_ready | ||||||
|  |   uhwm_run | ||||||
|  |   uhwm_wait_output 'Connected to' | ||||||
|  | end | ||||||
|  |  | ||||||
|  | Given /^uhwm is running$/ do | ||||||
|  |   uhwm_run_wait_ready | ||||||
|  | end | ||||||
|  |  | ||||||
| When /^I start uhwm$/ do | When /^I start uhwm$/ do | ||||||
|   uhwm_run |   uhwm_run | ||||||
| end | end | ||||||
| @@ -15,3 +24,7 @@ end | |||||||
| Then /^the exit status must be (\d+)$/ do |exit_status| | Then /^the exit status must be (\d+)$/ do |exit_status| | ||||||
|   assert_exit_status exit_status.to_i |   assert_exit_status exit_status.to_i | ||||||
| end | end | ||||||
|  |  | ||||||
|  | Then /^uhwm should terminate successfully$/ do | ||||||
|  |   assert_exit_status 0 | ||||||
|  | end | ||||||
|   | |||||||
| @@ -1,3 +1,11 @@ | |||||||
|  | def x_key key | ||||||
|  |   fail "cannot simulate X key `#{key}'" unless system "xdotool key #{key}" | ||||||
|  | end | ||||||
|  |  | ||||||
|  | When /^I press the default quit key binding$/ do | ||||||
|  |   x_key 'alt+q' | ||||||
|  | end | ||||||
|  |  | ||||||
| Then /^it must connect to X display$/ do | Then /^it must connect to X display$/ do | ||||||
|   uhwm_wait_output 'Connected to' |   uhwm_wait_output 'Connected to' | ||||||
|   expect(`sockstat -u`.lines.grep /\s+ruby.+\s+#{@process.pid}/) |   expect(`sockstat -u`.lines.grep /\s+ruby.+\s+#{@process.pid}/) | ||||||
|   | |||||||
| @@ -1,7 +1,10 @@ | |||||||
| require 'uh' | require 'uh' | ||||||
|  |  | ||||||
| require 'uh/wm/cli' | require 'uh/wm/cli' | ||||||
|  | require 'uh/wm/dispatcher' | ||||||
| require 'uh/wm/env' | require 'uh/wm/env' | ||||||
|  | require 'uh/wm/manager' | ||||||
|  | require 'uh/wm/runner' | ||||||
|  |  | ||||||
| module Uh | module Uh | ||||||
|   module WM |   module WM | ||||||
|   | |||||||
| @@ -32,9 +32,7 @@ module Uh | |||||||
|       end |       end | ||||||
|  |  | ||||||
|       def run |       def run | ||||||
|         @display = Display.new |         Runner.run env | ||||||
|         @display.open |  | ||||||
|         @env.log "Connected to X server on `#{@display}'" |  | ||||||
|       end |       end | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								lib/uh/wm/dispatcher.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								lib/uh/wm/dispatcher.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | module Uh | ||||||
|  |   module WM | ||||||
|  |     class Dispatcher | ||||||
|  |       attr_reader :hooks | ||||||
|  |  | ||||||
|  |       def initialize hooks = Hash.new | ||||||
|  |         @hooks = hooks | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       def [] *key | ||||||
|  |         @hooks[translate_key key] or [] | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       def on *key, &block | ||||||
|  |         @hooks[translate_key key] ||= [] | ||||||
|  |         @hooks[translate_key key] << block | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       def emit *key | ||||||
|  |         @hooks[translate_key key].tap { |o| o.each { |e| e.call } if o } | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |  | ||||||
|  |       private | ||||||
|  |  | ||||||
|  |       def translate_key key | ||||||
|  |         key.one? ? key[0] : key | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										30
									
								
								lib/uh/wm/manager.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								lib/uh/wm/manager.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | module Uh | ||||||
|  |   module WM | ||||||
|  |     class Manager | ||||||
|  |       attr_reader :display | ||||||
|  |  | ||||||
|  |       def initialize events, display = Display.new | ||||||
|  |         @events   = events | ||||||
|  |         @display  = display | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       def connect | ||||||
|  |         @display.open | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       def grab_key keysym | ||||||
|  |         @display.grab_key keysym.to_s, KEY_MODIFIERS[:mod1] | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       def handle_pending_events | ||||||
|  |         handle @display.next_event while @display.pending? | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       def handle event | ||||||
|  |         case event.type | ||||||
|  |         when :key_press then @events.emit :key, event.key.to_sym | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										52
									
								
								lib/uh/wm/runner.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								lib/uh/wm/runner.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | module Uh | ||||||
|  |   module WM | ||||||
|  |     class Runner | ||||||
|  |       class << self | ||||||
|  |         def run env, **options | ||||||
|  |           runner = new env, **options | ||||||
|  |           runner.register_event_hooks | ||||||
|  |           runner.connect_manager | ||||||
|  |           runner.run_until { runner.stopped? } | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       attr_reader :env, :events, :manager | ||||||
|  |  | ||||||
|  |       def initialize env, manager: nil, stopped: false | ||||||
|  |         @env      = env | ||||||
|  |         @events   = Dispatcher.new | ||||||
|  |         @manager  = manager || Manager.new(@events) | ||||||
|  |         @stopped  = stopped | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       def stopped? | ||||||
|  |         !!@stopped | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       def stop! | ||||||
|  |         @stopped = true | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       def register_event_hooks | ||||||
|  |         register_key_bindings_hooks | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       def connect_manager | ||||||
|  |         @manager.connect | ||||||
|  |         @env.log "Connected to X server" | ||||||
|  |         @manager.grab_key :q | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       def run_until &block | ||||||
|  |         @manager.handle_pending_events until block.call | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |  | ||||||
|  |       private | ||||||
|  |  | ||||||
|  |       def register_key_bindings_hooks | ||||||
|  |         @events.on(:key, :q) { stop! } | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @@ -15,6 +15,10 @@ module Uh | |||||||
|           described_class.run arguments, stdout: stdout, stderr: stderr |           described_class.run arguments, stdout: stdout, stderr: stderr | ||||||
|         end |         end | ||||||
|  |  | ||||||
|  |         # FIXME: remove this hack we currently need to prevent the Runner from | ||||||
|  |         # blocking. | ||||||
|  |         before { allow(Runner).to receive :run } | ||||||
|  |  | ||||||
|         it 'builds a new CLI with given arguments' do |         it 'builds a new CLI with given arguments' do | ||||||
|           expect(described_class) |           expect(described_class) | ||||||
|             .to receive(:new).with(arguments, stdout: stdout).and_call_original |             .to receive(:new).with(arguments, stdout: stdout).and_call_original | ||||||
| @@ -63,19 +67,10 @@ module Uh | |||||||
|       end |       end | ||||||
|  |  | ||||||
|       describe '#run' do |       describe '#run' do | ||||||
|         let(:display) { instance_spy Display } |         it 'runs a runner with the env' do | ||||||
|  |           expect(Runner).to receive(:run).with(cli.env) | ||||||
|         before { allow(Display).to receive(:new) { display } } |  | ||||||
|  |  | ||||||
|         it 'opens a new X display' do |  | ||||||
|           expect(display).to receive :open |  | ||||||
|           cli.run |           cli.run | ||||||
|         end |         end | ||||||
|  |  | ||||||
|         it 'prints a message on standard output when connected' do |  | ||||||
|           cli.run |  | ||||||
|           expect(stdout.string).to match /connected/i |  | ||||||
|         end |  | ||||||
|       end |       end | ||||||
|  |  | ||||||
|       describe '#parse_arguments!' do |       describe '#parse_arguments!' do | ||||||
|   | |||||||
							
								
								
									
										64
									
								
								spec/uh/wm/dispatcher_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								spec/uh/wm/dispatcher_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | module Uh | ||||||
|  |   module WM | ||||||
|  |     RSpec.describe Dispatcher do | ||||||
|  |       let(:hooks)           { {} } | ||||||
|  |       subject(:dispatcher)  { described_class.new hooks } | ||||||
|  |  | ||||||
|  |       describe '#[]' do | ||||||
|  |         context 'when given key for existing hook' do | ||||||
|  |           let(:hooks) { { hook_key: [:hook] } } | ||||||
|  |  | ||||||
|  |           it 'returns registered hooks for this key' do | ||||||
|  |             expect(dispatcher[:hook_key]).to eq [:hook] | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         context 'when given multiple keys for existing hook' do | ||||||
|  |           let(:hooks) { { %i[hook key] => [:hook] } } | ||||||
|  |  | ||||||
|  |           it 'returns registered hooks for this key' do | ||||||
|  |             expect(dispatcher[:hook, :key]).to eq [:hook] | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         context 'when given key for unknown hook' do | ||||||
|  |           it 'returns an empty array' do | ||||||
|  |             expect(dispatcher[:unknown_hook]).to eq [] | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       describe '#on' do | ||||||
|  |         it 'registers given hook for given key' do | ||||||
|  |           dispatcher.on(:hook_key) { :hook } | ||||||
|  |           expect(dispatcher.hooks[:hook_key]).to be | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         it 'registers given hook for given multiple keys' do | ||||||
|  |           dispatcher.on(:hook, :key) { :hook } | ||||||
|  |           expect(dispatcher.hooks[%i[hook key]]).to be | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       describe '#emit' do | ||||||
|  |         it 'calls hooks registered for given key' do | ||||||
|  |           dispatcher.on(:hook_key) { throw :hook_code } | ||||||
|  |           expect { dispatcher.emit :hook_key }.to throw_symbol :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 } | ||||||
|  |             expect { dispatcher.emit :other_hook_key }.not_to throw_symbol | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         context 'when no hooks are registered at all' do | ||||||
|  |           it 'does not raise any error' do | ||||||
|  |             expect { dispatcher.emit :hook_key }.not_to raise_error | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										69
									
								
								spec/uh/wm/manager_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								spec/uh/wm/manager_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | module Uh | ||||||
|  |   module WM | ||||||
|  |     RSpec.describe Manager do | ||||||
|  |       let(:events)      { Dispatcher.new } | ||||||
|  |       let(:display)     { Display.new } | ||||||
|  |       subject(:manager) { described_class.new events, display } | ||||||
|  |  | ||||||
|  |       describe '#initialize' do | ||||||
|  |         it 'assigns a new display' do | ||||||
|  |           expect(manager.display).to be_a Display | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       describe '#connect' do | ||||||
|  |         it 'opens the display' do | ||||||
|  |           expect(manager.display).to receive :open | ||||||
|  |           manager.connect | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       describe '#grab_key' do | ||||||
|  |         it 'grabs given key on the display' do | ||||||
|  |           expect(manager.display) | ||||||
|  |             .to receive(:grab_key).with('q', KEY_MODIFIERS[:mod1]) | ||||||
|  |           manager.grab_key :q | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       describe '#handle_pending_events' do | ||||||
|  |         let(:event) { double 'event' } | ||||||
|  |  | ||||||
|  |         context 'when an event is pending on display' do | ||||||
|  |           before do | ||||||
|  |             allow(display).to receive(:pending?).and_return true, false | ||||||
|  |             allow(display).to receive(:next_event) { event } | ||||||
|  |           end | ||||||
|  |  | ||||||
|  |           it 'handles the event' do | ||||||
|  |             expect(manager).to receive(:handle).with(event).once | ||||||
|  |             manager.handle_pending_events | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         context 'when multiple events are pending on display' do | ||||||
|  |           before do | ||||||
|  |             allow(display).to receive(:pending?).and_return true, true, false | ||||||
|  |             allow(display).to receive(:next_event) { event } | ||||||
|  |           end | ||||||
|  |  | ||||||
|  |           it 'handles all pending events' do | ||||||
|  |             expect(manager).to receive(:handle).with(event).twice | ||||||
|  |             manager.handle_pending_events | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       describe '#handle' do | ||||||
|  |         context 'when key_release event is given' do | ||||||
|  |           let(:event) { double 'event', type: :key_press, key: 'q' } | ||||||
|  |  | ||||||
|  |           it 'emits :key event with the corresponding key' do | ||||||
|  |             events.on(:key, :q) { throw :key_press_code } | ||||||
|  |             expect { manager.handle event }.to throw_symbol :key_press_code | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										112
									
								
								spec/uh/wm/runner_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								spec/uh/wm/runner_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | |||||||
|  | module Uh | ||||||
|  |   module WM | ||||||
|  |     RSpec.describe Runner do | ||||||
|  |       let(:env)         { Env.new(StringIO.new) } | ||||||
|  |       subject(:runner)  { described_class.new env } | ||||||
|  |  | ||||||
|  |       describe '.run' do | ||||||
|  |         subject(:run) { described_class.run env, stopped: true } | ||||||
|  |  | ||||||
|  |         it 'builds a new Runner with given env' do | ||||||
|  |           expect(described_class) | ||||||
|  |             .to receive(:new).with(env, anything).and_call_original | ||||||
|  |           run | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         it 'registers event hooks' do | ||||||
|  |           runner.stop! | ||||||
|  |           allow(described_class).to receive(:new) { runner } | ||||||
|  |           expect(runner).to receive(:register_event_hooks) | ||||||
|  |           run | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         it 'connects the manager' do | ||||||
|  |           runner.stop! | ||||||
|  |           allow(described_class).to receive(:new) { runner } | ||||||
|  |           expect(runner).to receive(:connect_manager) | ||||||
|  |           run | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       describe '#initialize' do | ||||||
|  |         it 'assigns the env' do | ||||||
|  |           expect(runner.env).to be env | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         it 'assigns a new Dispatcher' do | ||||||
|  |           expect(runner.events).to be_a Dispatcher | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         it 'assigns a new Manager' do | ||||||
|  |           expect(runner.manager).to be_a Manager | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         it 'is not stopped' do | ||||||
|  |           expect(runner).not_to be_stopped | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       describe '#stopped?' do | ||||||
|  |         context 'when not stopped' do | ||||||
|  |           it 'returns false' do | ||||||
|  |             expect(runner.stopped?).to be false | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         context 'when stopped' do | ||||||
|  |           before { runner.stop! } | ||||||
|  |  | ||||||
|  |           it 'returns true' do | ||||||
|  |             expect(runner.stopped?).to be true | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       describe '#stop!' do | ||||||
|  |         it 'sets the runner as stopped' do | ||||||
|  |           expect { runner.stop! } | ||||||
|  |             .to change { runner.stopped? } | ||||||
|  |             .from(false).to(true) | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       describe '#register_event_hooks' do | ||||||
|  |         context 'key bindings' do | ||||||
|  |           it 'registers key bindings event hooks' do | ||||||
|  |             runner.register_event_hooks | ||||||
|  |             expect(runner.events[:key, :q]).not_to be_empty | ||||||
|  |           end | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       describe '#connect_manager' do | ||||||
|  |         let(:manager)     { instance_spy Manager } | ||||||
|  |         subject(:runner)  { described_class.new env, manager: manager } | ||||||
|  |  | ||||||
|  |         it 'connects the manager' do | ||||||
|  |           expect(runner.manager).to receive :connect | ||||||
|  |           runner.connect_manager | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         it 'logs a message when connected' do | ||||||
|  |           expect(env).to receive(:log).with /connected/i | ||||||
|  |           runner.connect_manager | ||||||
|  |         end | ||||||
|  |  | ||||||
|  |         it 'tells the manager to grab keys' do | ||||||
|  |           expect(runner.manager).to receive(:grab_key).with :q | ||||||
|  |           runner.connect_manager | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |  | ||||||
|  |       describe '#run_until' do | ||||||
|  |         it 'tells the manager to handle events until given block is true' do | ||||||
|  |           block = proc { } | ||||||
|  |           allow(block).to receive(:call).and_return(false, false, false, true) | ||||||
|  |           expect(runner.manager).to receive(:handle_pending_events).exactly(3).times | ||||||
|  |           runner.run_until &block | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
		Reference in New Issue
	
	Block a user