fusuma-plugin-remap 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,343 @@
1
+ require "revdev"
2
+ require "msgpack"
3
+ require "set"
4
+ require_relative "layer_manager"
5
+ require_relative "uinput_keyboard"
6
+
7
+ module Fusuma
8
+ module Plugin
9
+ module Remap
10
+ class KeyboardRemapper
11
+ include Revdev
12
+
13
+ VIRTUAL_KEYBOARD_NAME = "fusuma_virtual_keyboard"
14
+
15
+ # @param layer_manager [Fusuma::Plugin::Remap::LayerManager]
16
+ # @param fusuma_writer [IO]
17
+ # @param config [Hash]
18
+ def initialize(layer_manager:, fusuma_writer:, config: {})
19
+ @layer_manager = layer_manager # request to change layer
20
+ @fusuma_writer = fusuma_writer # write event to original keyboard
21
+ @config = config
22
+ end
23
+
24
+ def run
25
+ create_virtual_keyboard
26
+ @source_keyboards = reload_keyboards
27
+
28
+ old_ie = nil
29
+ layer = nil
30
+ next_mapping = nil
31
+ current_mapping = {}
32
+
33
+ loop do
34
+ ios = IO.select([*@source_keyboards.map(&:file), @layer_manager.reader])
35
+ io = ios.first.first
36
+
37
+ if io == @layer_manager.reader
38
+ layer = @layer_manager.receive_layer # update @current_layer
39
+ if layer.nil?
40
+ next
41
+ end
42
+
43
+ next_mapping = @layer_manager.find_mapping(layer)
44
+ next
45
+ end
46
+
47
+ if next_mapping && virtual_keyboard_all_key_released?
48
+ if current_mapping != next_mapping
49
+ current_mapping = next_mapping
50
+ end
51
+ next_mapping = nil
52
+ end
53
+
54
+ input_event = @source_keyboards.find { |kbd| kbd.file == io }.read_input_event
55
+ input_key = find_key_from_code(input_event.code)
56
+
57
+ if input_event.type == EV_KEY
58
+ @emergency_stop.call(old_ie, input_event)
59
+
60
+ old_ie = input_event
61
+ if input_event.value != 2 # repeat
62
+ data = {key: input_key, status: input_event.value, layer: layer}
63
+ @fusuma_writer.write(data.to_msgpack)
64
+ end
65
+ end
66
+
67
+ remapped = current_mapping.fetch(input_key.to_sym, nil)
68
+ if remapped.nil?
69
+ uinput_keyboard.write_input_event(input_event)
70
+ next
71
+ end
72
+
73
+ remapped_event = InputEvent.new(nil, input_event.type, find_code_from_key(remapped), input_event.value)
74
+
75
+ # Workaround to solve the problem that the remapped key remains pressed
76
+ # when the key pressed before remapping is released after remapping
77
+ unless record_virtual_keyboard_event?(remapped, remapped_event.value)
78
+ # set original key before remapping
79
+ remapped_event.code = input_event.code
80
+ end
81
+
82
+ # remap to command will be nil
83
+ # e.g) remap: { X: { command: echo 'foo' } }
84
+ # this is because the command will be executed by fusuma process
85
+ next if remapped_event.code.nil?
86
+
87
+ uinput_keyboard.write_input_event(remapped_event)
88
+ rescue Errno::ENODEV => e # device is removed
89
+ MultiLogger.error "Device is removed: #{e.message}"
90
+ @source_keyboards = reload_keyboards
91
+ end
92
+ rescue EOFError => e # device is closed
93
+ MultiLogger.error "Device is closed: #{e.message}"
94
+ ensure
95
+ @destroy.call
96
+ end
97
+
98
+ private
99
+
100
+ def reload_keyboards
101
+ source_keyboards = KeyboardSelector.new(@config[:keyboard_name_patterns]).select
102
+
103
+ MultiLogger.info("Reload keyboards: #{source_keyboards.map(&:device_name)}")
104
+
105
+ set_trap(source_keyboards)
106
+ # TODO: Extract to a configuration file or make it optional
107
+ # it should stop other remappers
108
+ set_emergency_ungrab_keybinds("RIGHTCTRL", "LEFTCTRL")
109
+ grab_keyboards(source_keyboards)
110
+ rescue => e
111
+ MultiLogger.error "Failed to reload keyboards: #{e.message}"
112
+ MultiLogger.error e.backtrace.join("\n")
113
+ end
114
+
115
+ def uinput_keyboard
116
+ @uinput_keyboard ||= UinputKeyboard.new("/dev/uinput")
117
+ end
118
+
119
+ def pressed_virtual_keys
120
+ @pressed_virtual_keys ||= Set.new
121
+ end
122
+
123
+ # record virtual keyboard event
124
+ # @param [String] remapped_value remapped key name
125
+ # @param [Integer] event_value event value
126
+ # @return [Boolean] false if the key was pressed before remapping started and was released
127
+ # @return [Boolean] true if the key was not pressed before remapping started
128
+ def record_virtual_keyboard_event?(remapped_value, event_value)
129
+ case event_value
130
+ when 0
131
+ pressed_virtual_keys.delete?(remapped_value)
132
+ when 1
133
+ pressed_virtual_keys.add?(remapped_value)
134
+ true # Always return true because the remapped key may be the same
135
+ else
136
+ # 2 is repeat
137
+ true
138
+ end
139
+ end
140
+
141
+ def virtual_keyboard_all_key_released?
142
+ pressed_virtual_keys.empty?
143
+ end
144
+
145
+ def create_virtual_keyboard
146
+ touchpad_name_patterns = @config[:touchpad_name_patterns]
147
+ internal_touchpad = TouchpadSelector.new(touchpad_name_patterns).select.first
148
+
149
+ if internal_touchpad.nil?
150
+ MultiLogger.error("No touchpad found: #{touchpad_name_patterns}")
151
+ exit
152
+ end
153
+
154
+ MultiLogger.info "Create virtual keyboard: #{VIRTUAL_KEYBOARD_NAME}"
155
+
156
+ uinput_keyboard.create VIRTUAL_KEYBOARD_NAME,
157
+ Revdev::InputId.new(
158
+ # disable while typing is enabled when
159
+ # - Both the keyboard and touchpad are BUS_I8042
160
+ # - The touchpad and keyboard have the same vendor/product
161
+ # ref: (https://wayland.freedesktop.org/libinput/doc/latest/palm-detection.html#disable-while-typing)
162
+ #
163
+ {
164
+ bustype: Revdev::BUS_I8042,
165
+ vendor: internal_touchpad.device_id.vendor,
166
+ product: internal_touchpad.device_id.product,
167
+ version: internal_touchpad.device_id.version
168
+ }
169
+ )
170
+ end
171
+
172
+ def grab_keyboards(keyboards)
173
+ keyboards.each do |keyboard|
174
+ wait_release_all_keys(keyboard)
175
+ begin
176
+ keyboard.grab
177
+ MultiLogger.info "Grabbed #{keyboard.device_name}"
178
+ rescue Errno::EBUSY
179
+ MultiLogger.error "Failed to grab #{keyboard.device_name}"
180
+ end
181
+ end
182
+ end
183
+
184
+ # @param [Array<Revdev::EventDevice>] keyboards
185
+ def set_trap(keyboards)
186
+ @destroy = lambda do
187
+ keyboards.each do |kbd|
188
+ kbd.ungrab
189
+ rescue Errno::EINVAL
190
+ rescue Errno::ENODEV
191
+ # already ungrabbed
192
+ end
193
+
194
+ begin
195
+ uinput_keyboard.destroy
196
+ rescue IOError
197
+ # already destroyed
198
+ end
199
+
200
+ exit 0
201
+ end
202
+
203
+ Signal.trap(:INT) { @destroy.call }
204
+ Signal.trap(:TERM) { @destroy.call }
205
+ end
206
+
207
+ # Emergency stop keybind for virtual keyboard
208
+ def set_emergency_ungrab_keybinds(first_key, second_key)
209
+ first_keycode = find_code_from_key(first_key)
210
+ second_keycode = find_code_from_key(second_key)
211
+ MultiLogger.info "Emergency ungrab keybind: #{first_key} + #{second_key}"
212
+
213
+ @emergency_stop = lambda do |prev, current|
214
+ if (prev&.code == first_keycode && prev.value != 0) && (current.code == second_keycode && current.value != 0)
215
+ MultiLogger.info "Emergency ungrab keybind: #{first_key} + #{second_key}"
216
+ @destroy.call
217
+ end
218
+ end
219
+ end
220
+
221
+ # Find remappable key from mapping and return remapped key code
222
+ # If not found, return original key code
223
+ # If the key is found but its value is not valid, return nil
224
+ # @example
225
+ # find_remapped_code({ "A" => "b" }, 30) # => 48
226
+ # find_remapped_code({ "A" => "b" }, 100) # => 100
227
+ # find_remapped_code({ "A" => {command: 'echo foobar'} }, 30) # => nil
228
+ #
229
+ # @param [Hash] mapping
230
+ # @param [Integer] code
231
+ # @return [Integer, nil]
232
+ def find_remapped_code(mapping, code)
233
+ key = find_key_from_code(code) # key = "A"
234
+ remapped_key = mapping.fetch(key.to_sym, nil) # remapped_key = "b"
235
+ return code unless remapped_key # return original code if key is not found
236
+
237
+ find_code_from_key(remapped_key) # remapped_code = 48
238
+ end
239
+
240
+ # Find key name from key code
241
+ # @example
242
+ # find_key_from_code(30) # => "A"
243
+ # find_key_from_code(48) # => "B"
244
+ # @param [Integer] code
245
+ # @return [String]
246
+ def find_key_from_code(code)
247
+ # { 30 => :A, 48 => :B, ... }
248
+ @keys_per_code ||= Revdev.constants.select { |c| c.start_with? "KEY_" }.map { |c| [Revdev.const_get(c), c.to_s.delete_prefix("KEY_")] }.to_h
249
+ @keys_per_code[code]
250
+ end
251
+
252
+ # Find key code from key name (e.g. "A", "B", "BTN_LEFT")
253
+ # If key name is not found, return nil
254
+ # @example
255
+ # find_code_from_key("A") # => 30
256
+ # find_code_from_key("B") # => 48
257
+ # find_code_from_key("BTN_LEFT") # => 272
258
+ # find_code_from_key("NOT_FOUND") # => nil
259
+ # @param [String] key
260
+ # @return [Integer] when key is available
261
+ # @return [nil] when key is not available
262
+ def find_code_from_key(key)
263
+ # { KEY_A => 30, KEY_B => 48, ... }
264
+ @codes_per_key ||= Revdev.constants.select { |c| c.start_with?("KEY_", "BTN_") }.map { |c| [c, Revdev.const_get(c)] }.to_h
265
+
266
+ case key
267
+ when String
268
+ if key.start_with?("BTN_")
269
+ @codes_per_key[key.upcase.to_sym]
270
+ else
271
+ @codes_per_key["KEY_#{key}".upcase.to_sym]
272
+ end
273
+ when Integer
274
+ @codes_per_key["KEY_#{key}".upcase.to_sym]
275
+ end
276
+ end
277
+
278
+ def released_all_keys?(device)
279
+ # key status if all bytes are 0, the key is not pressed
280
+ bytes = device.read_ioctl_with(Revdev::EVIOCGKEY)
281
+ bytes.unpack("C*").all? { |byte| byte == 0 }
282
+ end
283
+
284
+ def wait_release_all_keys(device, &block)
285
+ loop do
286
+ if released_all_keys?(device)
287
+ break true
288
+ else
289
+ # wait until all keys are released
290
+ device.read_input_event
291
+ end
292
+ end
293
+ end
294
+
295
+ # Devices to detect key presses and releases
296
+ class KeyboardSelector
297
+ def initialize(names = ["keyboard", "Keyboard", "KEYBOARD"])
298
+ @names = names
299
+ end
300
+
301
+ # Select devices that match the name
302
+ # If no device is found, it will wait for 3 seconds and try again
303
+ # @return [Array<Revdev::EventDevice>]
304
+ def select
305
+ loop do
306
+ Fusuma::Device.reset # reset cache to get the latest device information
307
+ devices = Fusuma::Device.all.select { |d| Array(@names).any? { |name| d.name =~ /#{name}/ } }
308
+ if devices.empty?
309
+ wait_for_device
310
+
311
+ next
312
+ end
313
+
314
+ return devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") }
315
+ end
316
+ end
317
+
318
+ def wait_for_device
319
+ sleep 3
320
+ end
321
+ end
322
+
323
+ class TouchpadSelector
324
+ def initialize(names = nil)
325
+ @names = names
326
+ end
327
+
328
+ # @return [Array<Revdev::EventDevice>]
329
+ def select
330
+ devices = if @names
331
+ Fusuma::Device.all.select { |d| Array(@names).any? { |name| d.name =~ /#{name}/ } }
332
+ else
333
+ # available returns only touchpad devices
334
+ Fusuma::Device.available
335
+ end
336
+
337
+ devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") }
338
+ end
339
+ end
340
+ end
341
+ end
342
+ end
343
+ end
@@ -8,28 +8,33 @@ module Fusuma
8
8
  class LayerManager
9
9
  require "singleton"
10
10
  include Singleton
11
- attr_reader :reader, :writer, :current_layer
11
+ attr_reader :reader, :writer, :current_layer, :layers
12
12
 
13
13
  def initialize
14
14
  @layers = {}
15
15
  @reader, @writer = IO.pipe
16
16
  @current_layer = {} # preserve order
17
+ @last_layer = nil
18
+ @last_remove = nil
17
19
  end
18
20
 
19
21
  # @param [Hash] layer
20
22
  # @param [Boolean] remove
21
23
  def send_layer(layer:, remove: false)
22
- # puts "send_layer: #{layer} remove: #{remove}"
23
- return if @last_layer == layer && @last_remove == remove
24
+ return if (@last_layer == layer) && (@last_remove == remove)
24
25
 
25
26
  @last_layer = layer
26
27
  @last_remove = remove
27
- @writer.puts({layer: layer, remove: remove}.to_msgpack)
28
+ @writer.write({layer: layer, remove: remove}.to_msgpack)
28
29
  end
29
30
 
30
- # Read layer from pipe and return remap layer
31
+ # Read layer from pipe and update @current_layer
32
+ # @return [Hash] current layer
31
33
  # @example
32
- # @return [Hash]
34
+ # receive_layer
35
+ # # => { thumbsense: true }
36
+ # receive_layer
37
+ # # => { thumbsense: true, application: "Google-chrome" }
33
38
  def receive_layer
34
39
  @layer_unpacker ||= MessagePack::Unpacker.new(@reader)
35
40
 
@@ -41,28 +46,28 @@ module Fusuma
41
46
  layer = data[:layer] # e.g { thumbsense: true }
42
47
  remove = data[:remove] # e.g true
43
48
 
49
+ # update @current_layer
44
50
  if remove
45
51
  @current_layer.delete_if { |k, _v| layer.key?(k) }
46
52
  else
47
53
  # If duplicate keys exist, order of keys is preserved
48
54
  @current_layer.merge!(layer)
49
55
  end
56
+ @current_layer
50
57
  end
51
58
 
52
59
  # Find remap layer from config
53
60
  # @param [Hash] layer
54
- # @return [Hash]
55
- def find_mapping(layer = @current_layer)
61
+ # @return [Hash] remap layer
62
+ def find_mapping(layer)
56
63
  @layers[layer] ||= begin
57
64
  result = nil
58
- _ = Fusuma::Config::Searcher.find_context(layer) {
65
+ _ = Fusuma::Config::Searcher.find_context(layer) do
59
66
  result = Fusuma::Config.search(Fusuma::Config::Index.new(:remap))
60
67
  next unless result
61
68
 
62
- result = result.deep_transform_keys do |key|
63
- key.upcase.to_sym
64
- end
65
- }
69
+ result = result.deep_transform_keys { |key| key.upcase.to_sym }
70
+ end
66
71
 
67
72
  result || {}
68
73
  end
@@ -0,0 +1,168 @@
1
+ require "revdev"
2
+ require "msgpack"
3
+ require "set"
4
+
5
+ require_relative "uinput_touchpad"
6
+
7
+ module Fusuma
8
+ module Plugin
9
+ module Remap
10
+ class TouchpadRemapper
11
+ include Revdev
12
+
13
+ VIRTUAL_TOUCHPAD_NAME = "fusuma_virtual_touchpad"
14
+
15
+ # @param fusuma_writer [IO]
16
+ # @param source_touchpad [Revdev::Device]
17
+ def initialize(fusuma_writer:, source_touchpad:)
18
+ @source_touchpad = source_touchpad # original touchpad
19
+ @fusuma_writer = fusuma_writer # write event to fusuma_input
20
+ @palm_detector ||= PalmDetection.new(source_touchpad)
21
+ end
22
+
23
+ # TODO: grab touchpad events and remap them
24
+ # send remapped events to virtual touchpad or virtual mouse
25
+ def run
26
+ create_virtual_touchpad
27
+
28
+ touch_state = {}
29
+ mt_slot = 0
30
+ finger_state = nil
31
+ loop do
32
+ IO.select([@source_touchpad.file]) # , @layer_manager.reader])
33
+
34
+ ## example of input_event
35
+ # Event: time 1698456258.380027, type 3 (EV_ABS), code 57 (ABS_MT_TRACKING_ID), value 43679
36
+ # Event: time 1698456258.380027, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 648
37
+ # Event: time 1698456258.380027, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 209
38
+ # Event: time 1698456258.380027, type 1 (EV_KEY), code 330 (BTN_TOUCH), value 1
39
+ # Event: time 1698456258.380027, type 1 (EV_KEY), code 325 (BTN_TOOL_FINGER), value 1
40
+ # Event: time 1698456258.380027, type 3 (EV_ABS), code 0 (ABS_X), value 648
41
+ # Event: time 1698456258.380027, type 3 (EV_ABS), code 1 (ABS_Y), value 209
42
+ # Event: time 1698456258.380027, type 4 (EV_MSC), code 5 (MSC_TIMESTAMP), value 0
43
+ # Event: time 1698456258.380027, -------------- SYN_REPORT ------------
44
+ # Event: time 1698456258.382693, type 3 (EV_ABS), code 47 (ABS_MT_SLOT), value 1
45
+ # Event: time 1698456258.382693, type 3 (EV_ABS), code 57 (ABS_MT_TRACKING_ID), value 43680
46
+ # Event: time 1698456258.382693, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 400
47
+ # Event: time 1698456258.382693, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 252
48
+ # Event: time 1698456258.382693, type 1 (EV_KEY), code 325 (BTN_TOOL_FINGER), value 0
49
+ # Event: time 1698456258.382693, type 1 (EV_KEY), code 333 (BTN_TOOL_DOUBLETAP), value 1
50
+ # Event: time 1698456258.382693, type 4 (EV_MSC), code 5 (MSC_TIMESTAMP), value 7100
51
+ # Event: time 1698456258.382693, -------------- SYN_REPORT ------------
52
+ input_event = @source_touchpad.read_input_event
53
+
54
+ touch_state[mt_slot] ||= {MT_TRACKING_ID: nil, X: nil, Y: nil, valid_touch_point: false}
55
+ syn_report = nil
56
+
57
+ case input_event.type
58
+ when Revdev::EV_ABS
59
+ case input_event.code
60
+ when Revdev::ABS_MT_SLOT
61
+ mt_slot = input_event.value
62
+ touch_state[mt_slot] ||= {}
63
+ when Revdev::ABS_MT_TRACKING_ID
64
+ touch_state[mt_slot][:MT_TRACKING_ID] = input_event.value
65
+ if input_event.value == -1
66
+ touch_state[mt_slot] = {}
67
+ end
68
+ when Revdev::ABS_MT_POSITION_X
69
+ touch_state[mt_slot][:X] = input_event.value
70
+ when Revdev::ABS_MT_POSITION_Y
71
+ touch_state[mt_slot][:Y] = input_event.value
72
+ when Revdev::ABS_X, Revdev::ABS_Y
73
+ # ignore
74
+ when Revdev::ABS_MT_PRESSURE
75
+ # ignore
76
+ when Revdev::ABS_MT_TOOL_TYPE
77
+ # ignore
78
+ else
79
+ raise "unhandled event"
80
+ end
81
+ when Revdev::EV_KEY
82
+ case input_event.code
83
+ when Revdev::BTN_TOUCH
84
+ # ignore
85
+ when Revdev::BTN_TOOL_FINGER
86
+ finger_state = (input_event.value == 1) ? 1 : 0
87
+ when Revdev::BTN_TOOL_DOUBLETAP
88
+ finger_state = (input_event.value == 1) ? 2 : 1
89
+ when Revdev::BTN_TOOL_TRIPLETAP
90
+ finger_state = (input_event.value == 1) ? 3 : 2
91
+ when Revdev::BTN_TOOL_QUADTAP
92
+ finger_state = (input_event.value == 1) ? 4 : 3
93
+ when 0x148 # define BTN_TOOL_QUINTTAP 0x148 /* Five fingers on trackpad */
94
+ finger_state = (input_event.value == 1) ? 5 : 4
95
+ end
96
+ when Revdev::EV_MSC
97
+ case input_event.code
98
+ when 0x05 # define MSC_TIMESTAMP 0x05
99
+ # ignore
100
+ # current_timestamp = input_event.value
101
+ end
102
+ when Revdev::EV_SYN
103
+ case input_event.code
104
+ when Revdev::SYN_REPORT
105
+ syn_report = input_event.value
106
+ when Revdev::SYN_DROPPED
107
+ MultiLogger.error "Dropped: #{input_event.value}"
108
+ else
109
+ raise "unhandled event", "#{input_event.hr_type}, #{input_event.hr_code}, #{input_event.value}"
110
+ end
111
+ else
112
+ raise "unhandled event", "#{input_event.hr_type}, #{input_event.hr_code}, #{input_event.value}"
113
+ end
114
+
115
+ # TODO:
116
+ # Remember the most recent valid touch position and exclude it if it is close to that position
117
+ # For example, when dragging, it is possible to touch around the edge of the touchpad again after reaching the edge of the touchpad, so in that case, you do not want to execute palm detection
118
+ if touch_state[mt_slot][:valid_touch_point] != true
119
+ touch_state[mt_slot][:valid_touch_point] = @palm_detector.palm?(touch_state[mt_slot])
120
+ end
121
+
122
+ if syn_report
123
+ # TODO: define format as fusuma_input
124
+ data = {finger: finger_state, touch_state: touch_state}
125
+ @fusuma_writer.write(data.to_msgpack)
126
+ end
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ def uinput
133
+ @uinput ||= UinputTouchpad.new "/dev/uinput"
134
+ end
135
+
136
+ def create_virtual_touchpad
137
+ MultiLogger.info "Create virtual keyboard: #{VIRTUAL_TOUCHPAD_NAME}"
138
+
139
+ uinput.create_from_device(name: VIRTUAL_TOUCHPAD_NAME, device: @source_touchpad)
140
+ end
141
+
142
+ # Detect palm touch
143
+ class PalmDetection
144
+ def initialize(touchpad)
145
+ @max_x = touchpad.absinfo_for_axis(Revdev::ABS_MT_POSITION_X)[:absmax]
146
+ @max_y = touchpad.absinfo_for_axis(Revdev::ABS_MT_POSITION_Y)[:absmax]
147
+ end
148
+
149
+ def palm?(touch_state)
150
+ return false unless touch_state[:X] && touch_state[:Y]
151
+
152
+ if 0.8 * @max_y < touch_state[:Y]
153
+ true
154
+ else
155
+ !(
156
+ # Disable 20% of the touch area on the left, right
157
+ (touch_state[:X] < 0.2 * @max_x || touch_state[:X] > 0.8 * @max_x) ||
158
+ # Disable 10% of the touch area on the top edge
159
+ (touch_state[:Y] < 0.1 * @max_y && (touch_state[:X] < 0.2 * @max_x || touch_state[:X] > 0.8 * @max_x)
160
+ )
161
+ )
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,95 @@
1
+ require "ruinput"
2
+
3
+ class UinputKeyboard < Ruinput::UinputDevice
4
+ include Ruinput
5
+
6
+ # create virtual event divece
7
+ # _name_ :: device name
8
+ # _id_ :: InputId ("struct input_id" on input.h)
9
+ def create name = DEFAULT_DEVICE_NAME, id = DEFAULT_INPUT_ID
10
+ if !name.is_a? String
11
+ raise ArgumentError, "1st arg expect String"
12
+ elsif !id.is_a? Revdev::InputId
13
+ raise ArgumentError, "2nd arg expect Revdev::InputId"
14
+ end
15
+
16
+ uud = Ruinput::UinputUserDev.new({
17
+ name: name,
18
+ id: id,
19
+ ff_effects_max: 0,
20
+ absmax: [],
21
+ absmin: [],
22
+ absfuzz: [],
23
+ absflat: []
24
+ })
25
+
26
+ @file.syswrite uud.to_byte_string
27
+
28
+ set_all_events
29
+
30
+ @file.ioctl UI_DEV_CREATE, nil
31
+ @is_created = true
32
+ end
33
+
34
+ def set_all_events
35
+ raise "invalid method call: this uinput is already created" if @is_created
36
+
37
+ mouse_btns = [
38
+ Revdev::BTN_0,
39
+ Revdev::BTN_MISC,
40
+ Revdev::BTN_1,
41
+ Revdev::BTN_2,
42
+ Revdev::BTN_3,
43
+ Revdev::BTN_4,
44
+ Revdev::BTN_5,
45
+ Revdev::BTN_6,
46
+ Revdev::BTN_7,
47
+ Revdev::BTN_8,
48
+ Revdev::BTN_9,
49
+ Revdev::BTN_LEFT,
50
+ Revdev::BTN_MOUSE,
51
+ Revdev::BTN_MIDDLE,
52
+ Revdev::BTN_RIGHT,
53
+ Revdev::BTN_SIDE,
54
+ Revdev::BTN_EXTRA,
55
+ Revdev::BTN_FORWARD,
56
+ Revdev::BTN_BACK,
57
+ Revdev::BTN_TASK
58
+ # Revdev::BTN_TRIGGER, # libinput recognized as joystick if set
59
+ ].freeze
60
+
61
+ keyboard_keys = Revdev.constants.select { |c| c.start_with? "KEY_" }.map { |c| Revdev.const_get(c) }.freeze
62
+
63
+ @file.ioctl UI_SET_EVBIT, Revdev::EV_KEY
64
+ @counter = 0
65
+ Revdev::KEY_CNT.times do |i|
66
+ # https://github.com/mooz/xkeysnail/pull/101/files
67
+ if keyboard_keys.include?(i) || mouse_btns.include?(i)
68
+ # puts "setting #{i} (#{Revdev::REVERSE_MAPS[:KEY][i]})"
69
+ @file.ioctl UI_SET_KEYBIT, i
70
+ end
71
+ end
72
+
73
+ # @file.ioctl UI_SET_EVBIT, Revdev::EV_MSC
74
+ # Revdev::MSC_CNT.times do |i|
75
+ # @file.ioctl UI_SET_MSCBIT, i
76
+ # end
77
+
78
+ mouse_rel = [
79
+ Revdev::REL_X,
80
+ Revdev::REL_Y,
81
+ Revdev::REL_WHEEL,
82
+ Revdev::REL_HWHEEL
83
+ ].freeze
84
+
85
+ @file.ioctl UI_SET_EVBIT, Revdev::EV_REL
86
+ Revdev::REL_CNT.times do |i|
87
+ if mouse_rel.include?(i)
88
+ # puts "setting #{i} (#{Revdev::REVERSE_MAPS[:REL][i]})"
89
+ @file.ioctl UI_SET_RELBIT, i
90
+ end
91
+ end
92
+
93
+ @file.ioctl UI_SET_EVBIT, Revdev::EV_REP
94
+ end
95
+ end