From d2c53a66db48e41004534bc6f23504d0370addfb Mon Sep 17 00:00:00 2001 From: iberianpig Date: Tue, 2 Apr 2024 01:16:12 +0900 Subject: [PATCH 1/2] feat: Reload keyboards when a device is removed - Wait for 3 seconds and try again if no keyboard is found. - When a device is removed, reload keyboards and grab them again. Closes: #3 --- .../plugin/inputs/remap_keyboard_input.rb | 53 +--------- lib/fusuma/plugin/remap/keyboard_remapper.rb | 100 ++++++++++++++---- 2 files changed, 85 insertions(+), 68 deletions(-) diff --git a/lib/fusuma/plugin/inputs/remap_keyboard_input.rb b/lib/fusuma/plugin/inputs/remap_keyboard_input.rb index 02445f0..4d362f6 100644 --- a/lib/fusuma/plugin/inputs/remap_keyboard_input.rb +++ b/lib/fusuma/plugin/inputs/remap_keyboard_input.rb @@ -44,21 +44,10 @@ def read_from_io private def setup_remapper - source_keyboards = KeyboardSelector.new(config_params(:keyboard_name_patterns)).select - if source_keyboards.empty? - MultiLogger.error("No keyboard found: #{config_params(:keyboard_name_patterns)}") - exit - end - - internal_touchpad = TouchpadSelector.new(config_params(:touchpad_name_patterns)).select.first - if internal_touchpad.nil? - MultiLogger.error("No touchpad found: #{config_params(:touchpad_name_patterns)}") - exit - end - - MultiLogger.info("set up remapper") - MultiLogger.info("source_keyboards: #{source_keyboards.map(&:device_name)}") - MultiLogger.info("internal_touchpad: #{internal_touchpad.device_name}") + config = { + keyboard_name_patterns: config_params(:keyboard_name_patterns), + touchpad_name_patterns: config_params(:touchpad_name_patterns) + } layer_manager = Remap::LayerManager.instance @@ -70,46 +59,14 @@ def setup_remapper @fusuma_reader.close remapper = Remap::KeyboardRemapper.new( layer_manager: layer_manager, - source_keyboards: source_keyboards, fusuma_writer: fusuma_writer, - internal_touchpad: internal_touchpad + config: config ) remapper.run end layer_manager.reader.close fusuma_writer.close end - - # Devices to detect key presses and releases - class KeyboardSelector - def initialize(names = ["keyboard", "Keyboard", "KEYBOARD"]) - @names = names - end - - # @return [Array] - def select - devices = Fusuma::Device.all.select { |d| Array(@names).any? { |name| d.name =~ /#{name}/ } } - devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") } - end - end - - class TouchpadSelector - def initialize(names = nil) - @names = names - end - - # @return [Array] - def select - devices = if @names - Fusuma::Device.all.select { |d| Array(@names).any? { |name| d.name =~ /#{name}/ } } - else - # available returns only touchpad devices - Fusuma::Device.available - end - - devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") } - end - end end end end diff --git a/lib/fusuma/plugin/remap/keyboard_remapper.rb b/lib/fusuma/plugin/remap/keyboard_remapper.rb index 8832d00..db13919 100644 --- a/lib/fusuma/plugin/remap/keyboard_remapper.rb +++ b/lib/fusuma/plugin/remap/keyboard_remapper.rb @@ -14,22 +14,16 @@ class KeyboardRemapper # @param layer_manager [Fusuma::Plugin::Remap::LayerManager] # @param fusuma_writer [IO] - # @param source_keyboards [Array] - # @param internal_touchpad [Revdev::Device] - def initialize(layer_manager:, fusuma_writer:, source_keyboards:, internal_touchpad:) + # @param config [Hash] + def initialize(layer_manager:, fusuma_writer:, config: {}) @layer_manager = layer_manager # request to change layer @fusuma_writer = fusuma_writer # write event to original keyboard - @source_keyboards = source_keyboards # original keyboard - @internal_touchpad = internal_touchpad # internal touchpad + @config = config end def run create_virtual_keyboard - set_trap - # TODO: Extract to a configuration file or make it optional - # it should stop other remappers - set_emergency_ungrab_keybinds("RIGHTCTRL", "LEFTCTRL") - grab_keyboards + @source_keyboards = reload_keyboards old_ie = nil layer = nil @@ -91,17 +85,33 @@ def run next if remapped_event.code.nil? uinput_keyboard.write_input_event(remapped_event) + rescue Errno::ENODEV => e # device is removed + MultiLogger.error "Device is removed: #{e.message}" + @source_keyboards = reload_keyboards end - rescue Errno::ENODEV => e # device is removed - MultiLogger.error e.message rescue EOFError => e # device is closed - MultiLogger.error e.message + MultiLogger.error "Device is closed: #{e.message}" ensure @destroy.call end private + def reload_keyboards + source_keyboards = KeyboardSelector.new(@config[:keyboard_name_patterns]).select + + MultiLogger.info("Reload keyboards: #{source_keyboards.map(&:device_name)}") + + set_trap(source_keyboards) + # TODO: Extract to a configuration file or make it optional + # it should stop other remappers + set_emergency_ungrab_keybinds("RIGHTCTRL", "LEFTCTRL") + grab_keyboards(source_keyboards) + rescue => e + MultiLogger.error "Failed to reload keyboards: #{e.message}" + MultiLogger.error e.backtrace.join("\n") + end + def uinput_keyboard @uinput_keyboard ||= UinputKeyboard.new("/dev/uinput") end @@ -133,6 +143,14 @@ def virtual_keyboard_all_key_released? end def create_virtual_keyboard + touchpad_name_patterns = @config[:touchpad_name_patterns] + internal_touchpad = TouchpadSelector.new(touchpad_name_patterns).select.first + + if internal_touchpad.nil? + MultiLogger.error("No touchpad found: #{touchpad_name_patterns}") + exit + end + MultiLogger.info "Create virtual keyboard: #{VIRTUAL_KEYBOARD_NAME}" uinput_keyboard.create VIRTUAL_KEYBOARD_NAME, @@ -144,15 +162,15 @@ def create_virtual_keyboard # { bustype: Revdev::BUS_I8042, - vendor: @internal_touchpad.device_id.vendor, - product: @internal_touchpad.device_id.product, - version: @internal_touchpad.device_id.version + vendor: internal_touchpad.device_id.vendor, + product: internal_touchpad.device_id.product, + version: internal_touchpad.device_id.version } ) end - def grab_keyboards - @source_keyboards.each do |keyboard| + def grab_keyboards(keyboards) + keyboards.each do |keyboard| wait_release_all_keys(keyboard) begin keyboard.grab @@ -163,9 +181,10 @@ def grab_keyboards end end - def set_trap + # @param [Array] keyboards + def set_trap(keyboards) @destroy = lambda do - @source_keyboards.each do |kbd| + keyboards.each do |kbd| kbd.ungrab rescue Errno::EINVAL rescue Errno::ENODEV @@ -272,6 +291,47 @@ def wait_release_all_keys(device, &block) end end end + + # Devices to detect key presses and releases + class KeyboardSelector + def initialize(names = ["keyboard", "Keyboard", "KEYBOARD"]) + @names = names + end + + # Select devices that match the name + # If no device is found, it will wait for 3 seconds and try again + # @return [Array] + def select + loop do + Fusuma::Device.reset + devices = Fusuma::Device.all.select { |d| Array(@names).any? { |name| d.name =~ /#{name}/ } } + if devices.empty? + sleep 3 + next + end + + return devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") } + end + end + end + + class TouchpadSelector + def initialize(names = nil) + @names = names + end + + # @return [Array] + def select + devices = if @names + Fusuma::Device.all.select { |d| Array(@names).any? { |name| d.name =~ /#{name}/ } } + else + # available returns only touchpad devices + Fusuma::Device.available + end + + devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") } + end + end end end end From b85af9327273456756f963ed49033e065259fe2b Mon Sep 17 00:00:00 2001 From: iberianpig Date: Mon, 15 Apr 2024 17:10:31 +0900 Subject: [PATCH 2/2] feat: Add wait_for_device method to KeyboardRemapper --- lib/fusuma/plugin/remap/keyboard_remapper.rb | 9 +++- .../plugin/remap/keyboard_remapper_spec.rb | 46 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 spec/fusuma/plugin/remap/keyboard_remapper_spec.rb diff --git a/lib/fusuma/plugin/remap/keyboard_remapper.rb b/lib/fusuma/plugin/remap/keyboard_remapper.rb index db13919..11d505b 100644 --- a/lib/fusuma/plugin/remap/keyboard_remapper.rb +++ b/lib/fusuma/plugin/remap/keyboard_remapper.rb @@ -303,16 +303,21 @@ def initialize(names = ["keyboard", "Keyboard", "KEYBOARD"]) # @return [Array] def select loop do - Fusuma::Device.reset + Fusuma::Device.reset # reset cache to get the latest device information devices = Fusuma::Device.all.select { |d| Array(@names).any? { |name| d.name =~ /#{name}/ } } if devices.empty? - sleep 3 + wait_for_device + next end return devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") } end end + + def wait_for_device + sleep 3 + end end class TouchpadSelector diff --git a/spec/fusuma/plugin/remap/keyboard_remapper_spec.rb b/spec/fusuma/plugin/remap/keyboard_remapper_spec.rb new file mode 100644 index 0000000..dc1ed64 --- /dev/null +++ b/spec/fusuma/plugin/remap/keyboard_remapper_spec.rb @@ -0,0 +1,46 @@ +require "spec_helper" + +require "fusuma/plugin/remap/keyboard_remapper" +require "fusuma/device" + +RSpec.describe Fusuma::Plugin::Remap::KeyboardRemapper do + describe "#initialize" do + end + + describe "#run" do + before do + allow_any_instance_of(described_class).to receive(:create_virtual_keyboard) + allow_any_instance_of(described_class).to receive(:grab_events) + end + end + + describe Fusuma::Plugin::Remap::KeyboardRemapper::KeyboardSelector do + describe "#select" do + let(:selector) { described_class.new(["dummy_valid_device"]) } + let(:event_device) { double(Revdev::EventDevice) } + + context "with find devices" do + before do + allow(Fusuma::Device).to receive(:all).and_return([Fusuma::Device.new(name: "dummy_valid_device", id: "dummy")]) + allow(Revdev::EventDevice).to receive(:new).and_return(event_device) + end + it "should be Array of Revdev::EventDevice" do + expect(selector.select).to be_a_kind_of(Array) + expect(selector.select.first).to eq(event_device) + end + end + + context "without find device" do + before do + allow(selector).to receive(:loop).and_yield + allow(Fusuma::Device).to receive(:all).and_return([]) + end + it "wait for device" do + expect(selector).to receive(:loop).and_yield + expect(selector).to receive(:wait_for_device) + selector.select + end + end + end + end +end