Skip to content

Commit

Permalink
Merge pull request #5 from iberianpig/fix/watch-keyboard
Browse files Browse the repository at this point in the history
Wait for device removal and reload keyboard
  • Loading branch information
iberianpig authored Apr 15, 2024
2 parents 9d5659e + b85af93 commit 6946056
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 68 deletions.
53 changes: 5 additions & 48 deletions lib/fusuma/plugin/inputs/remap_keyboard_input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<Revdev::EventDevice>]
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<Revdev::EventDevice>]
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
Expand Down
105 changes: 85 additions & 20 deletions lib/fusuma/plugin/remap/keyboard_remapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,16 @@ class KeyboardRemapper

# @param layer_manager [Fusuma::Plugin::Remap::LayerManager]
# @param fusuma_writer [IO]
# @param source_keyboards [Array<Revdev::Device>]
# @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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -163,9 +181,10 @@ def grab_keyboards
end
end

def set_trap
# @param [Array<Revdev::EventDevice>] 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
Expand Down Expand Up @@ -272,6 +291,52 @@ 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<Revdev::EventDevice>]
def select
loop do
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?
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
def initialize(names = nil)
@names = names
end

# @return [Array<Revdev::EventDevice>]
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
Expand Down
46 changes: 46 additions & 0 deletions spec/fusuma/plugin/remap/keyboard_remapper_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 6946056

Please sign in to comment.