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:
commit
7896af9485
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
|
Loading…
x
Reference in New Issue
Block a user