diff --git a/.gitignore b/.gitignore index e398d24..0fcf412 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /Gemfile-custom.rb /Gemfile.lock +/tmp/ diff --git a/bin/uhwm b/bin/uhwm index e03193c..562a83f 100755 --- a/bin/uhwm +++ b/bin/uhwm @@ -3,4 +3,3 @@ require 'uh/wm' Uh::WM::CLI.run(ARGV) -sleep 8 diff --git a/features/actions/quit.feature b/features/actions/quit.feature new file mode 100644 index 0000000..40c0839 --- /dev/null +++ b/features/actions/quit.feature @@ -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 diff --git a/features/steps/run_steps.rb b/features/steps/run_steps.rb index e617e27..70e5a56 100644 --- a/features/steps/run_steps.rb +++ b/features/steps/run_steps.rb @@ -4,6 +4,15 @@ def uhwm_run options = nil @interactive = @process = run command.join ' ' 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 uhwm_run end @@ -15,3 +24,7 @@ end Then /^the exit status must be (\d+)$/ do |exit_status| assert_exit_status exit_status.to_i end + +Then /^uhwm should terminate successfully$/ do + assert_exit_status 0 +end diff --git a/features/steps/x_steps.rb b/features/steps/x_steps.rb index d7bcd9e..a900916 100644 --- a/features/steps/x_steps.rb +++ b/features/steps/x_steps.rb @@ -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 uhwm_wait_output 'Connected to' expect(`sockstat -u`.lines.grep /\s+ruby.+\s+#{@process.pid}/) diff --git a/lib/uh/wm.rb b/lib/uh/wm.rb index 553cec2..4034c03 100644 --- a/lib/uh/wm.rb +++ b/lib/uh/wm.rb @@ -1,7 +1,10 @@ require 'uh' require 'uh/wm/cli' +require 'uh/wm/dispatcher' require 'uh/wm/env' +require 'uh/wm/manager' +require 'uh/wm/runner' module Uh module WM diff --git a/lib/uh/wm/cli.rb b/lib/uh/wm/cli.rb index 5ea3e54..bf478db 100644 --- a/lib/uh/wm/cli.rb +++ b/lib/uh/wm/cli.rb @@ -32,9 +32,7 @@ module Uh end def run - @display = Display.new - @display.open - @env.log "Connected to X server on `#{@display}'" + Runner.run env end diff --git a/lib/uh/wm/dispatcher.rb b/lib/uh/wm/dispatcher.rb new file mode 100644 index 0000000..697e5af --- /dev/null +++ b/lib/uh/wm/dispatcher.rb @@ -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 diff --git a/lib/uh/wm/manager.rb b/lib/uh/wm/manager.rb new file mode 100644 index 0000000..0148d8a --- /dev/null +++ b/lib/uh/wm/manager.rb @@ -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 diff --git a/lib/uh/wm/runner.rb b/lib/uh/wm/runner.rb new file mode 100644 index 0000000..849acac --- /dev/null +++ b/lib/uh/wm/runner.rb @@ -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 diff --git a/spec/uh/wm/cli_spec.rb b/spec/uh/wm/cli_spec.rb index 5b6b5de..7bf4482 100644 --- a/spec/uh/wm/cli_spec.rb +++ b/spec/uh/wm/cli_spec.rb @@ -15,6 +15,10 @@ module Uh described_class.run arguments, stdout: stdout, stderr: stderr 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 expect(described_class) .to receive(:new).with(arguments, stdout: stdout).and_call_original @@ -63,19 +67,10 @@ module Uh end describe '#run' do - let(:display) { instance_spy Display } - - before { allow(Display).to receive(:new) { display } } - - it 'opens a new X display' do - expect(display).to receive :open + it 'runs a runner with the env' do + expect(Runner).to receive(:run).with(cli.env) cli.run end - - it 'prints a message on standard output when connected' do - cli.run - expect(stdout.string).to match /connected/i - end end describe '#parse_arguments!' do diff --git a/spec/uh/wm/dispatcher_spec.rb b/spec/uh/wm/dispatcher_spec.rb new file mode 100644 index 0000000..bd06d11 --- /dev/null +++ b/spec/uh/wm/dispatcher_spec.rb @@ -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 diff --git a/spec/uh/wm/manager_spec.rb b/spec/uh/wm/manager_spec.rb new file mode 100644 index 0000000..032a40b --- /dev/null +++ b/spec/uh/wm/manager_spec.rb @@ -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 diff --git a/spec/uh/wm/runner_spec.rb b/spec/uh/wm/runner_spec.rb new file mode 100644 index 0000000..7f4a65a --- /dev/null +++ b/spec/uh/wm/runner_spec.rb @@ -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