fusuma-plugin-remap 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1a6f601df78486fb3bf89f9c7902fe9fd46e45c33e2807eff9f0c4b5a38ec45
4
- data.tar.gz: 50ca1d1da716e62d9ed9cc3973b201f9b8c93b00b5e313559bc5d7d4a73bfd3d
3
+ metadata.gz: 22d4ef7f153876d648359c14463da56777ecc7878cac97f79d1e29784a6673e8
4
+ data.tar.gz: f4381c0dcc9c5277fdeb14f0a19ee49772a3a06b07adfd9a31e0f43f5fff99a2
5
5
  SHA512:
6
- metadata.gz: 881a29098c368a60f0d4469d77b1ffc118d0c9eb2df02d2b7c492ba356f30ab0ad740a73bfa96e229a1fd16d46a7a839139c7b4077e26fb30b092d0c142ca28c
7
- data.tar.gz: 50dd4b8c9554ee17e338a5ded7248d1b4f9215d03ca5d19d5e2c8d97731687b42b1edb2105d7ebf7539ac2718bb3d3085db4c800db26cad54426be93fe854643
6
+ metadata.gz: a8111498a2c1d85caaf3a3c03af770c308f4e1b4c395a739f14e7832fd91280755b65424a9626a700b411112ab54b25a27f61a2d37aa64daf460573a3a4fe925
7
+ data.tar.gz: 54b0014a0f13b5a040f284ba8c3f87ecde1729ac0643b1fcb9d17411dfa9f1cd79ed03c8aa24dfeed2c2856df47ac76e69970eddba433b09a223bd695635edb2
@@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
24
24
  # https://packages.ubuntu.com/search?keywords=ruby&searchon=names&exact=1&suite=all&section=main
25
25
  # support focal (20.04LTS) 2.7
26
26
 
27
- spec.add_dependency "fusuma", ">= 3.4"
27
+ spec.add_dependency "fusuma", ">= 3.11.0"
28
28
  spec.add_dependency "fusuma-plugin-keypress", ">= 0.11.0"
29
29
  spec.add_dependency "fusuma-plugin-sendkey", ">= 0.12.0"
30
30
  spec.add_dependency "msgpack"
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../remap/touchpad_remapper"
4
+ require_relative "../remap/device_selector"
4
5
 
5
6
  module Fusuma
6
7
  module Plugin
@@ -46,51 +47,37 @@ module Fusuma
46
47
  private
47
48
 
48
49
  def setup_remapper
49
- source_touchpads = TouchpadSelector.new(config_params(:touchpad_name_patterns)).select
50
- if source_touchpads.empty?
51
- MultiLogger.error("No touchpad found: #{config_params(:touchpad_name_patterns)}")
52
- exit
53
- end
54
-
55
- MultiLogger.info("set up remapper")
56
- MultiLogger.info("touchpad: #{source_touchpads}")
57
-
58
50
  # layer_manager = Remap::LayerManager.instance
59
51
 
60
52
  # physical touchpad input event
61
53
  @fusuma_reader, fusuma_writer = IO.pipe
54
+ touchpad_name_patterns = config_params(:touchpad_name_patterns)
62
55
 
63
56
  fork do
64
57
  # layer_manager.writer.close
65
58
  @fusuma_reader.close
59
+
60
+ # DeviceSelector waits until touchpad is found (like KeyboardSelector)
61
+ # NOTE: This must be inside fork to avoid blocking the main Fusuma process
62
+ source_touchpads = Remap::DeviceSelector.new(
63
+ name_patterns: touchpad_name_patterns,
64
+ device_type: :touchpad
65
+ ).select(wait: true)
66
+
67
+ MultiLogger.info("set up remapper")
68
+ MultiLogger.info("touchpad: #{source_touchpads}")
69
+
66
70
  remapper = Remap::TouchpadRemapper.new(
67
71
  # layer_manager: layer_manager,
68
72
  fusuma_writer: fusuma_writer,
69
- source_touchpads: source_touchpads
73
+ source_touchpads: source_touchpads,
74
+ touchpad_name_patterns: touchpad_name_patterns
70
75
  )
71
76
  remapper.run
72
77
  end
73
78
  # layer_manager.reader.close
74
79
  fusuma_writer.close
75
80
  end
76
-
77
- class TouchpadSelector
78
- def initialize(names = nil)
79
- @names = names
80
- end
81
-
82
- # @return [Array<Revdev::EventDevice>]
83
- def select
84
- devices = if @names
85
- Fusuma::Device.all.select { |d| Array(@names).any? { |name| d.name =~ /#{name}/ } }
86
- else
87
- # available returns only touchpad devices
88
- Fusuma::Device.available
89
- end
90
-
91
- devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") }
92
- end
93
- end
94
81
  end
95
82
  end
96
83
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "revdev"
4
+ require "fusuma/device"
5
+
6
+ module Fusuma
7
+ module Plugin
8
+ module Remap
9
+ # Common device selector for touchpad and keyboard detection
10
+ # Unifies TouchpadSelector implementations across the codebase
11
+ class DeviceSelector
12
+ POLL_INTERVAL = 3 # seconds
13
+
14
+ # @param name_patterns [Array, String, nil] patterns for device names
15
+ # @param device_type [Symbol] :touchpad or :keyboard (for logging)
16
+ def initialize(name_patterns: nil, device_type: :touchpad)
17
+ @name_patterns = name_patterns
18
+ @device_type = device_type
19
+ @displayed_waiting = false
20
+ end
21
+
22
+ # Select devices that match the name patterns
23
+ # @param wait [Boolean] if true, wait until device is found (polling loop)
24
+ # @return [Array<Revdev::EventDevice>]
25
+ def select(wait: false)
26
+ loop do
27
+ Fusuma::Device.reset # reset cache to get the latest device information
28
+ devices = find_devices
29
+ return to_event_devices(devices) unless devices.empty?
30
+ return [] unless wait
31
+
32
+ log_waiting_message unless @displayed_waiting
33
+ sleep POLL_INTERVAL
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def find_devices
40
+ if @name_patterns
41
+ Fusuma::Device.all.select { |d|
42
+ Array(@name_patterns).any? { |name| d.name =~ /#{name}/ }
43
+ }
44
+ else
45
+ # available returns only touchpad devices
46
+ Fusuma::Device.available
47
+ end
48
+ end
49
+
50
+ def to_event_devices(devices)
51
+ devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") }
52
+ end
53
+
54
+ def log_waiting_message
55
+ MultiLogger.warn "No #{@device_type} found: #{@name_patterns || "(default patterns)"}"
56
+ MultiLogger.warn "Waiting for #{@device_type} to be connected..."
57
+ @displayed_waiting = true
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -3,6 +3,7 @@ require "msgpack"
3
3
  require "set"
4
4
  require_relative "layer_manager"
5
5
  require_relative "uinput_keyboard"
6
+ require_relative "device_selector"
6
7
  require "fusuma/device"
7
8
 
8
9
  module Fusuma
@@ -12,6 +13,7 @@ module Fusuma
12
13
  include Revdev
13
14
 
14
15
  VIRTUAL_KEYBOARD_NAME = "fusuma_virtual_keyboard"
16
+ DEFAULT_EMERGENCY_KEYBIND = "RIGHTCTRL+LEFTCTRL".freeze
15
17
 
16
18
  # Key conversion tables for better performance and readability
17
19
  KEYMAP = Revdev.constants.select { |c| c.start_with?("KEY_", "BTN_") }
@@ -80,25 +82,39 @@ module Fusuma
80
82
  end
81
83
 
82
84
  remapped = current_mapping.fetch(input_key.to_sym, nil)
83
- if remapped.nil?
85
+ case remapped
86
+ when String, Symbol
87
+ # Remapped to another key - continue processing below
88
+ when Hash
89
+ # Command execution (e.g., {:SENDKEY=>"LEFTCTRL+BTN_LEFT", :CLEARMODIFIERS=>true})
90
+ # Skip input event processing and let Fusuma's Executor handle this
91
+ next
92
+ when nil
93
+ # Not remapped - write original key event as-is
84
94
  uinput_keyboard.write_input_event(input_event)
85
95
  next
96
+ else
97
+ # Invalid remapping configuration
98
+ MultiLogger.warn("Invalid remapped value - type: #{remapped.class}, key: #{input_key}")
99
+ next
86
100
  end
87
101
 
88
102
  remapped_code = key_to_code(remapped)
89
103
  if remapped_code.nil?
90
- MultiLogger.warn("Invalid remapped key: #{remapped}, skipping...")
104
+ MultiLogger.warn("Invalid remapped value - unknown key: #{remapped}, input: #{input_key}")
91
105
  uinput_keyboard.write_input_event(input_event)
92
106
  next
93
107
  end
94
108
 
95
109
  remapped_event = InputEvent.new(nil, input_event.type, remapped_code, input_event.value)
96
110
 
97
- # Workaround to solve the problem that the remapped key remains pressed
98
- # when the key pressed before remapping is released after remapping
99
- unless record_virtual_keyboard_event?(remapped, remapped_event.value)
100
- # set original key before remapping
111
+ # Workaround: If a key was pressed before remapping started and is being released,
112
+ # use the original key code to ensure proper key release
113
+ if should_use_original_key?(remapped, remapped_event.value)
101
114
  remapped_event.code = input_event.code
115
+ else
116
+ # Only update virtual key state if we're using the remapped key
117
+ update_virtual_key_state(remapped, remapped_event.value)
102
118
  end
103
119
 
104
120
  # remap to command will be nil
@@ -140,21 +156,34 @@ module Fusuma
140
156
  @pressed_virtual_keys ||= Set.new
141
157
  end
142
158
 
143
- # record virtual keyboard event
159
+ # Update virtual keyboard key state
144
160
  # @param [String] remapped_value remapped key name
145
- # @param [Integer] event_value event value
146
- # @return [Boolean] false if the key was pressed before remapping started and was released
147
- # @return [Boolean] true if the key was not pressed before remapping started
148
- def record_virtual_keyboard_event?(remapped_value, event_value)
161
+ # @param [Integer] event_value event value (0: release, 1: press, 2: repeat)
162
+ # @return [void]
163
+ def update_virtual_key_state(remapped_value, event_value)
149
164
  case event_value
150
- when 0
151
- pressed_virtual_keys.delete?(remapped_value)
152
- when 1
153
- pressed_virtual_keys.add?(remapped_value)
154
- true # Always return true because the remapped key may be the same
155
- else
156
- # 2 is repeat
157
- true
165
+ when 0 # key release
166
+ pressed_virtual_keys.delete(remapped_value)
167
+ when 1 # key press
168
+ pressed_virtual_keys.add(remapped_value)
169
+ # when 2 is repeat - no state change needed
170
+ end
171
+ end
172
+
173
+ # Check if we should use the original key code instead of remapped key
174
+ # This handles the case where a key was pressed before remapping started
175
+ # and is released after remapping
176
+ # @param [String] remapped_value remapped key name
177
+ # @param [Integer] event_value event value (0: release, 1: press, 2: repeat)
178
+ # @return [Boolean] true if we should use original key code
179
+ def should_use_original_key?(remapped_value, event_value)
180
+ case event_value
181
+ when 0 # key release
182
+ # If the key was not in our pressed set, it means it was pressed
183
+ # before remapping started, so we should use original key
184
+ !pressed_virtual_keys.include?(remapped_value)
185
+ when 1, 2 # key press or repeat
186
+ false # Always use remapped key for press/repeat events
158
187
  end
159
188
  end
160
189
 
@@ -164,29 +193,36 @@ module Fusuma
164
193
 
165
194
  def create_virtual_keyboard
166
195
  touchpad_name_patterns = @config[:touchpad_name_patterns]
167
- internal_touchpad = TouchpadSelector.new(touchpad_name_patterns).select.first
168
-
169
- if internal_touchpad.nil?
170
- MultiLogger.error("No touchpad found: #{touchpad_name_patterns}")
171
- exit
172
- end
196
+ # Use DeviceSelector without wait - keyboard remap should work even without touchpad
197
+ internal_touchpad = DeviceSelector.new(
198
+ name_patterns: touchpad_name_patterns,
199
+ device_type: :touchpad
200
+ ).select(wait: false).first
173
201
 
174
202
  MultiLogger.info "Create virtual keyboard: #{VIRTUAL_KEYBOARD_NAME}"
175
203
 
176
- uinput_keyboard.create VIRTUAL_KEYBOARD_NAME,
177
- Revdev::InputId.new(
178
- # disable while typing is enabled when
179
- # - Both the keyboard and touchpad are BUS_I8042
180
- # - The touchpad and keyboard have the same vendor/product
181
- # ref: (https://wayland.freedesktop.org/libinput/doc/latest/palm-detection.html#disable-while-typing)
182
- #
183
- {
184
- bustype: Revdev::BUS_I8042,
185
- vendor: internal_touchpad.device_id.vendor,
186
- product: internal_touchpad.device_id.product,
187
- version: internal_touchpad.device_id.version
188
- }
189
- )
204
+ if internal_touchpad.nil?
205
+ MultiLogger.warn("No touchpad found: #{touchpad_name_patterns}")
206
+ MultiLogger.warn("Disable-while-typing feature will not work without a touchpad")
207
+ # Create virtual keyboard without touchpad device ID
208
+ # disable-while-typing will not work in this case
209
+ uinput_keyboard.create VIRTUAL_KEYBOARD_NAME
210
+ else
211
+ uinput_keyboard.create VIRTUAL_KEYBOARD_NAME,
212
+ Revdev::InputId.new(
213
+ # disable while typing is enabled when
214
+ # - Both the keyboard and touchpad are BUS_I8042
215
+ # - The touchpad and keyboard have the same vendor/product
216
+ # ref: (https://wayland.freedesktop.org/libinput/doc/latest/palm-detection.html#disable-while-typing)
217
+ #
218
+ {
219
+ bustype: Revdev::BUS_I8042,
220
+ vendor: internal_touchpad.device_id.vendor,
221
+ product: internal_touchpad.device_id.product,
222
+ version: internal_touchpad.device_id.version
223
+ }
224
+ )
225
+ end
190
226
  end
191
227
 
192
228
  def grab_keyboards(keyboards)
@@ -224,8 +260,6 @@ module Fusuma
224
260
  Signal.trap(:TERM) { @destroy.call(1) }
225
261
  end
226
262
 
227
- DEFAULT_EMERGENCY_KEYBIND = "RIGHTCTRL+LEFTCTRL".freeze
228
-
229
263
  # Emergency stop keybind for virtual keyboard
230
264
  def set_emergency_ungrab_keys(keybind_string)
231
265
  keybinds = keybind_string&.split("+")
@@ -369,24 +403,6 @@ module Fusuma
369
403
  sleep 3
370
404
  end
371
405
  end
372
-
373
- class TouchpadSelector
374
- def initialize(names = nil)
375
- @names = names
376
- end
377
-
378
- # @return [Array<Revdev::EventDevice>]
379
- def select
380
- devices = if @names
381
- Fusuma::Device.all.select { |d| Array(@names).any? { |name| d.name =~ /#{name}/ } }
382
- else
383
- # available returns only touchpad devices
384
- Fusuma::Device.available
385
- end
386
-
387
- devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") }
388
- end
389
- end
390
406
  end
391
407
  end
392
408
  end
@@ -3,6 +3,8 @@ require "msgpack"
3
3
  require "set"
4
4
 
5
5
  require_relative "uinput_touchpad"
6
+ require_relative "device_selector"
7
+ require "fusuma/device"
6
8
 
7
9
  module Fusuma
8
10
  module Plugin
@@ -14,9 +16,11 @@ module Fusuma
14
16
 
15
17
  # @param fusuma_writer [IO]
16
18
  # @param source_touchpads [Revdev::Device]
17
- def initialize(fusuma_writer:, source_touchpads:)
19
+ # @param touchpad_name_patterns [Array, String, nil] patterns for touchpad device names (for reconnection)
20
+ def initialize(fusuma_writer:, source_touchpads:, touchpad_name_patterns: nil)
18
21
  @source_touchpads = source_touchpads # original touchpad
19
22
  @fusuma_writer = fusuma_writer # write event to fusuma_input
23
+ @touchpad_name_patterns = touchpad_name_patterns # for reconnection
20
24
 
21
25
  @palm_detectors = @source_touchpads.each_with_object({}) do |source_touchpad, palm_detectors|
22
26
  palm_detectors[source_touchpad] = PalmDetection.new(source_touchpad)
@@ -157,7 +161,19 @@ module Fusuma
157
161
  prev_status = status
158
162
  prev_valid_touch = valid_touch
159
163
  end
164
+ rescue Errno::ENODEV => e
165
+ MultiLogger.error "Touchpad device is removed: #{e.message}"
166
+ MultiLogger.info "Waiting for touchpad to reconnect..."
167
+ reload_touchpads
168
+ touch_state = {}
169
+ mt_slot = 0
170
+ finger_state = nil
171
+ prev_valid_touch = false
172
+ prev_status = nil
173
+ retry
160
174
  end
175
+ rescue IOError => e
176
+ MultiLogger.error "Touchpad IO error: #{e.message}"
161
177
  rescue => e
162
178
  MultiLogger.error "An error occurred: #{e.message}"
163
179
  ensure
@@ -176,6 +192,34 @@ module Fusuma
176
192
  uinput.create_from_device(name: VIRTUAL_TOUCHPAD_NAME, device: @source_touchpads.first)
177
193
  end
178
194
 
195
+ # Reload touchpads after device disconnection
196
+ # This method waits until a touchpad is reconnected
197
+ def reload_touchpads
198
+ # Destroy virtual touchpad
199
+ begin
200
+ uinput.destroy
201
+ rescue IOError
202
+ # already destroyed
203
+ end
204
+ @uinput = nil
205
+
206
+ # Wait and detect touchpad using DeviceSelector
207
+ @source_touchpads = DeviceSelector.new(
208
+ name_patterns: @touchpad_name_patterns,
209
+ device_type: :touchpad
210
+ ).select(wait: true)
211
+
212
+ # Reinitialize palm detectors
213
+ @palm_detectors = @source_touchpads.each_with_object({}) do |source_touchpad, palm_detectors|
214
+ palm_detectors[source_touchpad] = PalmDetection.new(source_touchpad)
215
+ end
216
+
217
+ # Recreate virtual touchpad
218
+ create_virtual_touchpad
219
+
220
+ MultiLogger.info "Touchpad reconnected: #{@source_touchpads}"
221
+ end
222
+
179
223
  def set_trap
180
224
  @destroy = lambda do
181
225
  begin
@@ -3,7 +3,7 @@
3
3
  module Fusuma
4
4
  module Plugin
5
5
  module Remap
6
- VERSION = "0.11.1"
6
+ VERSION = "0.12.0"
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fusuma-plugin-remap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.1
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - iberianpig
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-09-10 00:00:00.000000000 Z
11
+ date: 2026-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fusuma
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '3.4'
19
+ version: 3.11.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '3.4'
26
+ version: 3.11.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: fusuma-plugin-keypress
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -120,6 +120,7 @@ files:
120
120
  - lib/fusuma/plugin/inputs/remap_touchpad_input.rb
121
121
  - lib/fusuma/plugin/inputs/remap_touchpad_input.yml
122
122
  - lib/fusuma/plugin/remap.rb
123
+ - lib/fusuma/plugin/remap/device_selector.rb
123
124
  - lib/fusuma/plugin/remap/keyboard_remapper.rb
124
125
  - lib/fusuma/plugin/remap/layer_manager.rb
125
126
  - lib/fusuma/plugin/remap/touchpad_remapper.rb