fusuma-plugin-thumbsense 0.7.0 → 0.9.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: 30004fbb68724dafdc6cdafc0b8ab756ed1f3104c505327adb2a4435617c6400
4
- data.tar.gz: bbdd89545b405e673e87e61cae5c9fb3052c4b8a788c28e889664618cbd95a4b
3
+ metadata.gz: a991e73e2eace58e3f6732a6a0f39b12b9e968550c613977eeba03ec650dadda
4
+ data.tar.gz: 8735f0050e321d7171558df00925b94f39ea307e6914f350fdcf69ed805002bc
5
5
  SHA512:
6
- metadata.gz: af54a3831c6b69e9449efde47bad16be777902eaf65492edd7cf2291cbfc30202b16cbe63e5632707f253ff5eb445ed3a54654618a409783629ae3168547bfde
7
- data.tar.gz: afdb2ed48a5f1629875a77c282606c0460a7dc6664561ffb4b12e2d947281a0988d2c2af78ad662818c85a14f12f128130bc8c853beaf68cf484f6e99bb1f1fb
6
+ metadata.gz: 104fcb225426e2c0310a33277ab82e9a174bfa38dd3212d3b9294f72561b48b8a9ec73860478bc9ab16f53deb6a1272c8d21ca0221c71fe797e571786f6983a8
7
+ data.tar.gz: d5bf0fba767a5a1d1952cd2fd7f2fd4b9f033ac3c89b76dbfdc31ad5235a5ad0b3727e04437ddd5367eac27ccc256661959859cbd816708ab841588a4a61c524
data/README.md CHANGED
@@ -3,54 +3,135 @@
3
3
  Remapper from key to click only while tapping the touchpad.
4
4
  Implemented as [Fusuma](https://github.com/iberianpig/fusuma) Plugin.
5
5
 
6
- **THIS PLUGIN IS EXPERIMENTAL.**
7
-
8
6
  ## What is ThumbSense?
9
7
  [ThumbSense](https://www2.sonycsl.co.jp/person/rekimoto/tsense/soft/index.html) is a tool that lets you control a laptop's touchpad using the keyboard. It assigns certain keyboard keys as mouse buttons and switches between acting as mouse buttons or normal keyboard keys based on whether the user's thumb is touching the touchpad. ThumbSense aims to make it easier to use the touchpad without moving your hand away from the keyboard.
10
8
 
11
9
  ## Installation
12
10
 
13
- ### Requirements
11
+ ### Prerequisites
14
12
 
15
13
  - [fusuma](https://github.com/iberianpig/fusuma#update) 2.0 or later
16
- - [fusuma-plugin-keypress](https://github.com/iberianpig/fusuma-plugin-keypress) 0.5 or later
17
- - fusuma-plugin-keypress is used to get keyboard input and is installed automatically.
18
- - [fusuma-plugin-remap](https://github.com/iberianpig/fusuma-plugin-remap)
19
- - You need to set up udev rules for creating a virtual input device.
20
- - Please refer to [fusuma-plugin-remap's README](https://github.com/iberianpig/fusuma-plugin-remap?tab=readme-ov-file#set-up-udev-rules) for details.
14
+ - [fusuma-plugin-keypress](https://github.com/iberianpig/fusuma-plugin-keypress) 0.5 or later (automatically installed)
15
+ - [fusuma-plugin-remap](https://github.com/iberianpig/fusuma-plugin-remap) (udev rules setup required)
16
+
17
+ ### Steps to Install and Set Up Fusuma::Plugin::Thumbsense
18
+
19
+ 1. Install the necessary packages for native extensions:
20
+ ```sh
21
+ $ sudo apt install ruby-dev build-essential
22
+ ```
21
23
 
22
- ### Install fusuma-plugin-thumbsense
24
+ 2. Install the required library for building fusuma-plugin-remap:
25
+ ```sh
26
+ $ sudo apt install libevdev-dev
27
+ ```
23
28
 
24
- Run the following code in your terminal.
29
+ 3. Set up udev rules to create a virtual input device (for fusuma-plugin-remap):
30
+ ```sh
31
+ $ echo 'KERNEL=="uinput", MODE="0660", GROUP="input", OPTIONS+="static_node=uinput"' | sudo tee /etc/udev/rules.d/60-udev-fusuma-remap.rules
32
+ $ sudo udevadm control --reload-rules && sudo udevadm trigger
33
+ ```
25
34
 
35
+ 4. Install fusuma-plugin-thumbsense:
26
36
  ```sh
27
37
  $ sudo gem install fusuma-plugin-thumbsense
28
38
  ```
29
39
 
30
- ## Properties
40
+ ## Configuration
41
+
42
+ ### Thumbsense Context
43
+
44
+ To add the thumbsense `context`, edit `~/.config/fusuma/config.yml`.
45
+ The `context` section is separated by `---` and specified as `context: thumbsense`.
46
+ Fusuma will switch to the `thumbsense` context while tapping the touchpad.
47
+
48
+ ### Key to Mouse Button Remap
31
49
 
32
- ### Thumbsense
50
+ You can remap keys to mouse buttons while tapping the touchpad.
51
+ The `remap` property is configured within the `thumbsense` context.
33
52
 
34
- First, add the `thumbsense` context to `~/.config/fusuma/config.yml`.
35
- The context is separated by `---` and specified by `context: thumbsense`.
53
+ Available mouse buttons include:
54
+ - `BTN_LEFT`
55
+ - `BTN_MIDDLE`
56
+ - `BTN_RIGHT`
57
+ - `BTN_SIDE`
58
+ - `BTN_EXTRA`
59
+ - `BTN_FORWARD`
60
+ - `BTN_BACK`
61
+ - `BTN_TASK`
62
+ - `BTN_0`
63
+ - `BTN_1`
64
+ - ...
65
+ - `BTN_9`
36
66
 
37
- ## Example
67
+ ### Example Configuration
38
68
 
39
- Set the following code in `~/.config/fusuma/config.yml`.
69
+ Add the following code to `~/.config/fusuma/config.yml`:
40
70
 
41
71
  ```yaml
72
+ # Add thumbsense context
42
73
  ---
43
74
  context: thumbsense
44
75
 
45
76
  remap:
77
+ F: BTN_LEFT
78
+ E: BTN_MIDDLE
79
+ D: BTN_RIGHT
80
+ SPACE: BTN_LEFT
46
81
  J: BTN_LEFT
47
82
  K: BTN_RIGHT
48
83
  ```
49
84
 
50
- ### TODO
85
+ ## Pointing Stick Support
86
+
87
+ ### Overview
88
+ Fusuma::Plugin::Thumbsense provides experimental support for pointing stick devices. This functionality is currently limited to the **HHKB Studio** and utilizes HIDRAW. Please note that this feature is still in testing, and improvements may be made in future updates.
89
+
90
+ see: https://github.com/iberianpig/fusuma-plugin-thumbsense/pull/4
91
+
92
+ ### Setting Up Udev Rules
93
+
94
+ To use the pointing stick touch support, you need to set up the following Udev rules to ensure that the HHKB Studio device is correctly recognized:
95
+
96
+ 1. **Create the Udev Rule File**:
97
+ Create a Udev rule file with the following command:
98
+
99
+ ```sh
100
+ sudo nano /etc/udev/rules.d/60-udev-fusuma-thumbsense-hhkb-studio.rules
101
+ ```
102
+
103
+ Add the following content to the file:
104
+
105
+ ```plaintext
106
+ # HHKB Studio (USB)
107
+ KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="04fe", ATTRS{idProduct}=="0016", MODE="0666"
108
+
109
+ # HHKB Studio (Bluetooth)
110
+ KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ENV{DEVPATH}=="/devices/virtual/misc/uhid/*:04FE:0016.*/hidraw*", MODE="0666"
111
+ ```
112
+
113
+ 2. **Reload the Udev Rules**:
114
+ Execute the following command to reload the Udev rules:
115
+
116
+ ```sh
117
+ sudo udevadm control --reload-rules && sudo udevadm trigger
118
+ ```
119
+
120
+ ## TODO LIST
121
+
122
+ - ThumbSense
123
+ - [x] Change remap layer while tapping
124
+ - [x] Enable executing commands like `command:` and `sendkey:`
125
+ - [x] Support pointing stick devices(https://github.com/iberianpig/fusuma-plugin-thumbsense/pull/4)
126
+ - Now only HHKB Studio is supported using HIDRAW
51
127
 
52
- - Using the fusuma-plugin-sendkey to emulate mouse buttons
53
- - Creating a simple key remapper using evdev and uinput to prevent the pressing of J/K keys on the physical keyboard from being sent.
128
+ - Remap
129
+ - [x] Remap to single key (e.g., `remap: { J: BTN_LEFT }`)
130
+ - [x] Send mouse clicks with `remap: { I: BTN_MIDDLE }`
131
+ - [x] Remap multiple keys
132
+ - Support sending multiple keys with fusuma-plugin-sendkey(https://github.com/iberianpig/fusuma-plugin-sendkey/pull/34)
133
+ - `remap: { T: { sendkey: [LEFTSHIFT+F10, T, ENTER, ESC] } }`
134
+ - [ ] Remap POINTER_MOTION to POINTER_SCROLL_FINGER (e.g., `remap: { S: POINTER_SCROLL_FINGER }`)
54
135
 
55
136
  ## Contributing
56
137
 
@@ -25,8 +25,8 @@ Gem::Specification.new do |spec|
25
25
  # https://packages.ubuntu.com/search?keywords=ruby&searchon=names&exact=1&suite=all&section=main
26
26
  # support focal (20.04LTS) 2.7
27
27
  spec.add_dependency "fusuma", ">= 3.2"
28
- spec.add_dependency "fusuma-plugin-keypress", ">= 0.5"
29
- spec.add_dependency "fusuma-plugin-remap"
28
+ spec.add_dependency "fusuma-plugin-keypress", ">= 0.11"
29
+ spec.add_dependency "fusuma-plugin-remap", ">= 0.4"
30
30
  spec.metadata = {
31
31
  "rubygems_mfa_required" => "true"
32
32
  }
@@ -5,7 +5,8 @@ module Fusuma
5
5
  module Buffers
6
6
  # manage events and generate command
7
7
  class ThumbsenseBuffer < Buffer
8
- DEFAULT_SOURCE = "thumbsense_parser"
8
+ DEFAULT_SOURCE = "remap_touchpad_input"
9
+ POINTING_STICK_SOURCE = "pointing_stick_input"
9
10
 
10
11
  def config_param_types
11
12
  {
@@ -20,17 +21,17 @@ module Fusuma
20
21
  # skip palm/begin record
21
22
  return if !ended?(@events.last)
22
23
 
23
- released_finger = @events.last.record.finger
24
- @events.delete_if { |e| e.record.finger == released_finger }
24
+ clear
25
25
  end
26
26
 
27
27
  # @param event [Event]
28
28
  # @return [NilClass, ThumbsenseBuffer]
29
29
  def buffer(event)
30
- return if event&.tag != source
31
-
32
- @events.push(event)
33
- self
30
+ case event.tag
31
+ when source, POINTING_STICK_SOURCE
32
+ @events.push(event)
33
+ self
34
+ end
34
35
  end
35
36
 
36
37
  # return [Integer]
@@ -38,21 +39,6 @@ module Fusuma
38
39
  @events.map { |e| e.record.finger }.max
39
40
  end
40
41
 
41
- def empty?
42
- @events.empty?
43
- end
44
-
45
- def present?
46
- !empty?
47
- end
48
-
49
- def select_by_events(&block)
50
- return enum_for(:select) unless block
51
-
52
- events = @events.select(&block)
53
- self.class.new events
54
- end
55
-
56
42
  def ended?(event)
57
43
  event.record.status == "end"
58
44
  end
@@ -0,0 +1,4 @@
1
+ plugin:
2
+ buffers:
3
+ thumbsense_buffer:
4
+ source: remap_touchpad_input
@@ -10,8 +10,8 @@ module Fusuma
10
10
  module Detectors
11
11
  # Detect Thumbsense context and change remap layer of fusuma-plugin-remap
12
12
  class ThumbsenseDetector < Detector
13
+ # keypress buffer is used to detect modifier keys
13
14
  SOURCES = %w[thumbsense keypress].freeze
14
- BUFFER_TYPE = "thumbsense"
15
15
 
16
16
  MODIFIER_KEYS = Set.new(%w[
17
17
  CAPSLOCK
@@ -28,51 +28,74 @@ module Fusuma
28
28
  LAYER_CONTEXT = {thumbsense: true}.freeze
29
29
 
30
30
  # Detect Context event and change remap layer of fusuma-plugin-remap
31
- # @param buffers [Array<Buffer>]
31
+ # @param buffers [Array<Buffer>] ThumbsenseBuffer, KeypressBuffer
32
32
  # @return [Event] if Thumbsense context is detected
33
+ # @return [Array<Event>] if Thumbsense context and Remap index when keypress is detected
33
34
  # @return [NilClass] if event is NOT detected
34
35
  def detect(buffers)
35
- thumbsense_buffer = buffers.find { |b| b.type == BUFFER_TYPE }
36
-
37
- return if thumbsense_buffer.empty?
38
-
39
- MultiLogger.debug("thumbsense_buffer: #{thumbsense_buffer.events.map(&:record).map { |r| "#{r.finger} #{r.gesture} #{r.status}" }}")
36
+ @thumbsense_buffer ||= find_buffer(buffers, "thumbsense")
37
+ @keypress_buffer ||= find_buffer(buffers, "keypress")
40
38
 
41
39
  layer_manager = Fusuma::Plugin::Remap::LayerManager.instance
42
40
 
43
- if touch_released?(thumbsense_buffer)
41
+ # layer is thumbsense => create thumbsense context and remap index
42
+ # touch is touching => create thumbsense context and remap index
43
+ # touch is released => remove thumbsense context
44
+ # keypress -> touch => remove thumbsense context
45
+ if touch_released? && !thumbsense_layer?
46
+ MultiLogger.debug("thumbsense layer removed")
44
47
  layer_manager.send_layer(layer: LAYER_CONTEXT, remove: true)
45
48
  return
46
49
  end
47
50
 
48
- keypress_buffer = buffers.find { |b| b.type == "keypress" }
51
+ # When keypress event is first:
52
+ # If current layer is thumbsense, the layer should not be changed
53
+ # If current layer is not thumbsense, it should remain a normal key
54
+ # In other words, if the key event comes first, do nothing
55
+ if keypress_first?
56
+ MultiLogger.debug("keypress event is first")
49
57
 
50
- # If only modifier keys are pressed or no key is pressed
51
- if pressed_codes(keypress_buffer).all? { |code| MODIFIER_KEYS.include?(code) }
52
-
53
- # Even if the palm is detected, keep the thumbsense layer until `:end` event
54
- if palm_detected?(thumbsense_buffer)
55
- layer_manager.send_layer(layer: LAYER_CONTEXT, remove: true)
56
- return
57
- end
58
+ return
59
+ end
58
60
 
59
- layer_manager.send_layer(layer: LAYER_CONTEXT)
61
+ MultiLogger.debug("thumbsense context created") unless thumbsense_layer?
62
+ layer_manager.send_layer(layer: LAYER_CONTEXT)
60
63
 
61
- # create thumbsense context
62
- record = Events::Records::ContextRecord.new(
64
+ # create thumbsense context
65
+ context = create_event(
66
+ record: Events::Records::ContextRecord.new(
63
67
  name: :thumbsense,
64
68
  value: true
65
69
  )
66
- return create_event(record: record)
70
+ )
71
+
72
+ # TODO: Threshold
73
+ # create remap index
74
+ index = if (keys = pressed_codes) && !keys.empty?
75
+ MultiLogger.debug("thumbsense remap index created: #{keys}")
76
+ combined_keys = keys.join("+")
77
+ create_event(
78
+ record: Events::Records::IndexRecord.new(
79
+ index: Config::Index.new([:remap, combined_keys])
80
+ )
81
+ )
67
82
  end
68
83
 
69
- nil
84
+ [context, index].compact
70
85
  end
71
86
 
72
87
  private
73
88
 
74
- def pressed_codes(keypress_buffer)
75
- records = keypress_buffer.events.map(&:record)
89
+ # @param buffers [Array<Buffer>]
90
+ # @param type [String]
91
+ # @return [Buffer]
92
+ def find_buffer(buffers, type)
93
+ buffers.find { |b| b.type == type }
94
+ end
95
+
96
+ # @return [Array<String>]
97
+ def pressed_codes
98
+ records = @keypress_buffer.events.map(&:record)
76
99
  codes = []
77
100
  records.each do |r|
78
101
  if r.status == "pressed"
@@ -84,45 +107,40 @@ module Fusuma
84
107
  codes
85
108
  end
86
109
 
87
- def touching?(thumbsense_buffer)
88
- !touch_released?(thumbsense_buffer)
110
+ # @return [TrueClass, FalseClass]
111
+ def touch_released?
112
+ return true if @thumbsense_buffer.empty?
113
+
114
+ @thumbsense_buffer.events.map(&:record).last&.status == "end"
89
115
  end
90
116
 
91
117
  # @return [TrueClass, FalseClass]
92
- def touch_released?(thumbsense_buffer)
93
- thumbsense_events = thumbsense_buffer.events
94
- touch_num = thumbsense_events.count { |e| (e.record.status == "begin") }
95
- release_num = thumbsense_events.count { |e| e.record.status == "end" }
118
+ def thumbsense_layer?
119
+ return if @keypress_buffer.empty?
120
+
121
+ last_keypress = @keypress_buffer.events.last.record
122
+ return if last_keypress.status == "released"
123
+
124
+ return if MODIFIER_KEYS.include?(last_keypress.code)
96
125
 
97
- touch_num <= release_num
126
+ current_layer = last_keypress&.layer
127
+ current_layer && current_layer["thumbsense"]
98
128
  end
99
129
 
100
- # Detect palm, except when there is another touch
130
+ # Check if keypress event is first, before thumbsense event
131
+ # If last keypress event is modifier key, return false
132
+ # @param keypress_buffer [Buffer]
101
133
  # @param thumbsense_buffer [Buffer]
102
- # @return [TrueClass, FalseClass]
103
- def palm_detected?(thumbsense_buffer)
104
- # finger is a number to distinguish different touches and palms
105
- # If the count remains, it is judged as a touch state
106
- touch_state_per_finger = {}
107
- thumbsense_buffer.events.each do |e|
108
- f = e.record.finger
109
- touch_state_per_finger[f] ||= 0
110
-
111
- case e.record.status
112
- when "begin"
113
- touch_state_per_finger[f] += 1
114
- when "palm"
115
- if touch_state_per_finger[f] < 0
116
- # NOTE: If Palm continues, it is equivalent to end
117
- touch_state_per_finger[f] = 0
118
- else
119
- touch_state_per_finger[f] -= 1
120
- end
121
- when "end"
122
- touch_state_per_finger[f] = 0
123
- end
134
+ # @return [TrueClass] if keypress event is first
135
+ # @return [FalseClass] if keypress event is NOT first or buffers are empty
136
+ def keypress_first?
137
+ return false if @thumbsense_buffer.empty? || @keypress_buffer.empty?
138
+
139
+ if (keys = pressed_codes) && !keys.empty?
140
+ return false if MODIFIER_KEYS.include?(keys.first)
124
141
  end
125
- touch_state_per_finger.values.all? { |count| count <= 0 }
142
+
143
+ @keypress_buffer.events.first.time < @thumbsense_buffer.events.first.time
126
144
  end
127
145
  end
128
146
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fusuma/device"
4
+ require "fusuma/multi_logger"
5
+
6
+ module Fusuma
7
+ module Plugin
8
+ module Inputs
9
+ # Read pointing stick events
10
+ class Hidraw
11
+ class Device
12
+ # Definitions of IOCTL commands
13
+ HIDIOCGRAWNAME = 0x80804804
14
+ HIDIOCGRAWPHYS = 0x80404805
15
+ HIDIOCGRAWINFO = 0x80084803
16
+ HIDIOCGRDESCSIZE = 0x80044801
17
+ HIDIOCGRDESC = 0x90044802
18
+
19
+ # Definitions of bus types
20
+ BUS_PCI = 0x01
21
+ BUS_ISAPNP = 0x02
22
+ BUS_USB = 0x03
23
+ BUS_HIL = 0x04
24
+ BUS_BLUETOOTH = 0x05
25
+ BUS_VIRTUAL = 0x06
26
+
27
+ attr_reader :hidraw_path, :name, :bustype, :vendor_id, :product_id
28
+
29
+ def initialize(hidraw_path:)
30
+ @hidraw_path = hidraw_path
31
+ load_device_info
32
+ end
33
+
34
+ private
35
+
36
+ def load_device_info
37
+ File.open(@hidraw_path, "rb+") do |file|
38
+ @name = fetch_ioctl_data(file, HIDIOCGRAWNAME).strip
39
+
40
+ info = fetch_ioctl_data(file, HIDIOCGRAWINFO, [0, 0, 0].pack("LSS"))
41
+ @bustype, vendor, product = info.unpack("LSS")
42
+ @vendor_id = vendor.to_s(16)
43
+ @product_id = product.to_s(16)
44
+ end
45
+ rescue => e
46
+ MultiLogger.error "Error loading device info: #{e.message}"
47
+ end
48
+
49
+ def fetch_ioctl_data(file, ioctl_command, buffer = " " * 256)
50
+ file.ioctl(ioctl_command, buffer)
51
+ buffer
52
+ rescue Errno::EIO
53
+ MultiLogger.warn "Failed to retrieve data with IOCTL command #{ioctl_command}, the device might not support this operation."
54
+ end
55
+ end
56
+
57
+ class DeviceFinder
58
+ def find(device_name_pattern)
59
+ device_name_pattern = Regexp.new(device_name_pattern) if device_name_pattern.is_a?(String)
60
+ event_path = find_pointer_device_path(device_name_pattern)
61
+ return nil unless event_path
62
+
63
+ hidraw_path = find_hidraw_path(event_path)
64
+
65
+ return Device.new(hidraw_path: hidraw_path) if hidraw_path
66
+
67
+ nil
68
+ end
69
+
70
+ private
71
+
72
+ def find_hidraw_path(event_path)
73
+ event_abs_path = File.realpath(event_path)
74
+ parent_path = event_abs_path.gsub(%r{/input/input\d+/.*}, "")
75
+ locate_hidraw_device(parent_path)
76
+ end
77
+
78
+ def find_pointer_device_path(device_name_pattern)
79
+ Fusuma::Device.reset
80
+
81
+ device = Fusuma::Device.all.find do |device|
82
+ device.name =~ device_name_pattern && device.capabilities == "pointer"
83
+ end
84
+
85
+ device&.then { |d| "/sys/class/input/#{d.id}" }
86
+ end
87
+
88
+ def locate_hidraw_device(parent_path)
89
+ Dir.glob("#{parent_path}/hidraw/hidraw*").find do |path|
90
+ if File.exist?(path)
91
+ hidraw_device_path = path.gsub(%r{^/.*hidraw/hidraw}, "/dev/hidraw")
92
+ return hidraw_device_path if File.readable?(hidraw_device_path)
93
+ end
94
+ end
95
+
96
+ nil
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fusuma
4
+ module Plugin
5
+ module Inputs
6
+ class Hidraw
7
+ class HhkbBluetoothParser
8
+ BASE_TIMEOUT = 0.03 # Base timeout value for reading reports
9
+ MAX_TIMEOUT = 0.2 # Maximum timeout value before failure
10
+ MULTIPLIER = 1.1 # Multiplier to exponentially increase timeout
11
+
12
+ MAX_REPORT_SIZE = 9 # Maximum report size in bytes
13
+
14
+ # @param hidraw_device [Hidraw::Device] the HID raw device
15
+ def initialize(hidraw_device)
16
+ @hidraw_device = hidraw_device
17
+ end
18
+
19
+ # Parse HID raw device events.
20
+ def parse
21
+ File.open(@hidraw_device.hidraw_path, "rb") do |device|
22
+ timeout = nil
23
+
24
+ # Continuously read reports from the device.
25
+ while (report = read_with_timeout(device, timeout))
26
+ mouse_state = if report.empty?
27
+ # Handle timeout case
28
+ :end
29
+ else
30
+ case parse_hid_report(report)
31
+ when :mouse
32
+ case mouse_state
33
+ when :begin, :update
34
+ :update
35
+ else
36
+ :begin
37
+ end
38
+ when :keyboard
39
+ # Continue mouse_state when keyboard operation
40
+ mouse_state
41
+ else
42
+ :end
43
+ end
44
+ end
45
+
46
+ case mouse_state
47
+ when :begin, :update
48
+ timeout = update_timeout(timeout)
49
+ when :end
50
+ timeout = nil
51
+ end
52
+
53
+ yield mouse_state
54
+ end
55
+ end
56
+ end
57
+
58
+ # Reads the HID report from the device with a timeout.
59
+ # @param device [File] the opened device file
60
+ # @param timeout [Float] the timeout duration
61
+ # @return [String] the HID report as bytes or an empty string on timeout
62
+ def read_with_timeout(device, timeout)
63
+ # puts "Timeout: #{timeout}" # Log timeout for debugging
64
+ Timeout.timeout(timeout) { device.read(MAX_REPORT_SIZE) }
65
+ rescue Timeout::Error
66
+ ""
67
+ end
68
+
69
+ # Update the timeout based on previous value.
70
+ # @param timeout [Float, nil] previously set timeout
71
+ # @return [Float] the updated timeout value
72
+ def update_timeout(timeout)
73
+ return BASE_TIMEOUT if timeout.nil?
74
+
75
+ [timeout * MULTIPLIER, MAX_TIMEOUT].min
76
+ end
77
+
78
+ # Parse the HID report to determine its type.
79
+ # @param report_bytes [String] the HID report as byte data
80
+ # @return [Symbol, nil] symbol indicating type of report or nil on error
81
+ def parse_hid_report(report_bytes)
82
+ report_id = report_bytes.getbyte(0)
83
+ case report_id
84
+ when 1
85
+ # parse_mouse_report(report_bytes)
86
+ :mouse
87
+ when 127
88
+ # parse_keyboard_report(report_bytes)
89
+ :keyboard
90
+ else
91
+ MultiLogger.warn "Unknown Report ID: #{report_id}"
92
+ nil
93
+ end
94
+ end
95
+
96
+ # Parse mouse report data.
97
+ # @param report_bytes [String] the HID mouse report as byte data
98
+ def parse_mouse_report(report_bytes)
99
+ puts "Raw bytes: #{report_bytes.inspect}" # Display raw byte bytes
100
+
101
+ report_id, buttons, x, y, wheel, ac_pan = report_bytes.unpack("CCcccc") # Retrieve 6-byte report
102
+ # - `C`: 1 byte unsigned integer (report ID) (0..255)
103
+ # - `C`: 1 byte unsigned integer (button state) (0..255)
104
+ # - `c`: 1 byte signed integer (x-axis) (-128..127)
105
+ # - `c`: 1 byte signed integer (y-axis) (-128..127)
106
+ # - `c`: 1 byte signed integer (wheel) (-128..127)
107
+ # - `c`: 1 byte signed integer (AC pan) (-128..127)
108
+ button_states = buttons.to_s(2).rjust(8, "0").chars.map(&:to_i)
109
+
110
+ puts "# ReportID: #{report_id} / Button: #{button_states.join(" ")} | X: #{x.to_s.rjust(4)} | Y: #{y.to_s.rjust(4)} | Wheel: #{wheel.to_s.rjust(4)} | AC Pan: #{ac_pan.to_s.rjust(4)}"
111
+ end
112
+
113
+ # Parse keyboard report data.
114
+ # @param report_bytes [String] the HID keyboard report as byte data
115
+ def parse_keyboard_report(report_bytes)
116
+ report_id, modifiers, _reserved1, *keys = report_bytes.unpack("CCCC6") # Retrieve 9-byte report
117
+ # - `C`: 1 byte unsigned integer (report ID) (0..255)
118
+ # - `C`: 1 byte unsigned integer (modifier keys) (0..255)
119
+ # - `C`: 1 byte reserved (0)
120
+ # - `C`: 6 bytes of keycodes (0..255)
121
+ modifier_states = %w[LeftControl LeftShift LeftAlt LeftGUI RightControl RightShift RightAlt RightGUI].map.with_index { |m, i| "#{m}: #{((modifiers & (1 << i)) != 0) ? 1 : 0}" }
122
+ keys_output = keys.map { |key| (key == 0) ? "0x70000" : translate_keycode(key) }
123
+ puts "# ReportID: #{report_id} / #{modifier_states.join(" | ")} | Keyboard #{keys_output}"
124
+ end
125
+
126
+ # Translate keycode to its string representation.
127
+ # @param keycode [Integer] the keycode to translate
128
+ # @return [String] the string representation of the keycode
129
+ def translate_keycode(keycode)
130
+ # Map of keycodes to their respective characters
131
+ keycodes = {
132
+ 4 => "a and A", 7 => "d and D", 16 => "s and S", 19 => "w and W",
133
+ 9 => "f and F", 10 => "g and G", 14 => "j and J", 15 => "k and K",
134
+ 33 => "[ and {", 47 => "] and }"
135
+ # Add more as needed
136
+ }
137
+ keycodes[keycode] || "0x#{keycode.to_s(16)}" # Return hexadecimal if not found
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ if $PROGRAM_NAME == __FILE__
146
+ require "timeout"
147
+
148
+ require_relative "device"
149
+ require "fusuma/plugin/inputs/libinput_command_input"
150
+
151
+ device = Fusuma::Plugin::Inputs::Hidraw::DeviceFinder.new.find("HHKB-Studio")
152
+ return if device.nil?
153
+
154
+ puts "Device: #{device.name} (#{device.vendor_id}:#{device.product_id})"
155
+ if device.bustype == Fusuma::Plugin::Inputs::Hidraw::Device::BUS_BLUETOOTH
156
+ Fusuma::Plugin::Inputs::Hidraw::HhkbBluetoothParser.new(device).parse do |state|
157
+ puts "Touch state: #{state}"
158
+ end
159
+ else
160
+ puts "Bustype is not BUS_BLUETOOTH"
161
+ end
162
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fusuma
4
+ module Plugin
5
+ module Inputs
6
+ class Hidraw
7
+ class HhkbUsbParser
8
+ BASE_TIMEOUT = 0.03 # Base timeout value for reading reports
9
+ MAX_TIMEOUT = 0.2 # Maximum timeout value before failure
10
+ MULTIPLIER = 1.1 # Multiplier to exponentially increase timeout
11
+
12
+ MAX_REPORT_SIZE = 5 # Maximum report size in bytes
13
+
14
+ # @param hidraw_device [Hidraw::Device] the HID raw device
15
+ def initialize(hidraw_device)
16
+ @hidraw_device = hidraw_device
17
+ end
18
+
19
+ # Parse HID raw device events.
20
+ def parse
21
+ File.open(@hidraw_device.hidraw_path, "rb") do |device|
22
+ timeout = nil
23
+
24
+ # Continuously read reports from the device.
25
+ while (report = read_with_timeout(device, timeout))
26
+ mouse_state = if report.empty?
27
+ # Handle timeout case
28
+ :end
29
+ else
30
+ # instance.parse_hid_report(report_bytes)
31
+ case mouse_state
32
+ when :begin, :update
33
+ :update
34
+ else
35
+ :begin
36
+ end
37
+ end
38
+
39
+ case mouse_state
40
+ when :begin, :update
41
+ timeout = update_timeout(timeout)
42
+ when :end
43
+ timeout = nil
44
+ end
45
+
46
+ yield mouse_state
47
+ end
48
+ end
49
+ end
50
+
51
+ # Reads the HID report from the device with a timeout.
52
+ # @param device [File] the opened device file
53
+ # @param timeout [Float] the timeout duration
54
+ # @return [String] the HID report as bytes or an empty string on timeout
55
+ def read_with_timeout(device, timeout)
56
+ # puts "Timeout: #{timeout}" # Log timeout for debugging
57
+ Timeout.timeout(timeout) { device.read(MAX_REPORT_SIZE) }
58
+ rescue Timeout::Error
59
+ ""
60
+ end
61
+
62
+ # Update the timeout based on previous value.
63
+ # @param timeout [Float, nil] previously set timeout
64
+ # @return [Float] the updated timeout value
65
+ def update_timeout(timeout)
66
+ return BASE_TIMEOUT if timeout.nil?
67
+
68
+ [timeout * MULTIPLIER, MAX_TIMEOUT].min
69
+ end
70
+
71
+ # Parse the HID report to determine its type.
72
+ # @param report_bytes [String] the HID report as byte data
73
+ # @return [Symbol, nil] symbol indicating type of report or nil on error
74
+ def parse_hid_report(report_bytes)
75
+ return :end if report_bytes.nil?
76
+
77
+ # buttons, x, y, wheel, ac_pan = report_bytes.unpack("Ccccc") # Retrieve 5-byte report
78
+ # - `C`: 1 byte unsigned integer (button state) (0..255)
79
+ # - `c`: 1 byte signed integer (X-axis) (-127..127)
80
+ # - `c`: 1 byte signed integer (Y-axis) (-127..127)
81
+ # - `c`: 1 byte signed integer (Wheel) (-127..127)
82
+ # - `c`: 1 byte signed integer (AC pan) (-127..127)
83
+ # button_states = buttons.to_s(2).rjust(8, "0").chars.map(&:to_i)
84
+
85
+ # puts "Raw bytes: #{report_bytes.inspect}" # Display raw byte sequence
86
+ # puts "# Button: #{button_states.join(" ")} | X: #{x.to_s.rjust(4)} | Y: #{y.to_s.rjust(4)} | Wheel: #{wheel.to_s.rjust(4)} | AC Pan: #{ac_pan.to_s.rjust(4)}"
87
+
88
+ :begin
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ if $PROGRAM_NAME == __FILE__
97
+ require "timeout"
98
+
99
+ require_relative "device"
100
+ require "fusuma/plugin/inputs/libinput_command_input"
101
+
102
+ device = Fusuma::Plugin::Inputs::Hidraw::DeviceFinder.new.find("HHKB-Studio")
103
+ return if device.nil?
104
+
105
+ puts "Device: #{device.name} (#{device.vendor_id}:#{device.product_id})"
106
+ if device.bustype == Fusuma::Plugin::Inputs::Hidraw::Device::BUS_USB
107
+ Fusuma::Plugin::Inputs::Hidraw::HhkbUsbParser.new(device).parse do |state|
108
+ puts "Touch state: #{state}"
109
+ end
110
+ else
111
+ puts "Bustype is not USB"
112
+ end
113
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fusuma/device"
4
+
5
+ require_relative "hidraw/device"
6
+ require_relative "hidraw/hhkb_bluetooth_parser"
7
+ require_relative "hidraw/hhkb_usb_parser"
8
+
9
+ module Fusuma
10
+ module Plugin
11
+ module Inputs
12
+ # Read pointing stick events
13
+ class PointingStickInput < Input
14
+ def config_param_types
15
+ {
16
+ device_name_pattern: String
17
+ }
18
+ end
19
+
20
+ def initialize
21
+ super
22
+ @device_name_pattern = config_params(:device_name_pattern)
23
+ end
24
+
25
+ def io
26
+ @io ||= begin
27
+ reader, writer = IO.pipe
28
+ Thread.new do
29
+ process_device_events(writer)
30
+ writer.close
31
+ end
32
+
33
+ reader
34
+ end
35
+ end
36
+
37
+ def process_device_events(writer)
38
+ hidraw_device = find_hidraw_device(@device_name_pattern, wait: 3)
39
+ hidraw_parser = select_hidraw_parser(hidraw_device.bustype)
40
+
41
+ mouse_state = nil
42
+
43
+ hidraw_parser.new(hidraw_device).parse do |new_state|
44
+ next if mouse_state == new_state
45
+
46
+ mouse_state = new_state
47
+ writer.puts(mouse_state)
48
+ end
49
+ rescue Errno::EIO => e
50
+ MultiLogger.error "#{self.class.name}: #{e}"
51
+ retry
52
+ end
53
+
54
+ # Override Input#read_from_io
55
+ def read_from_io
56
+ status = io.readline(chomp: true)
57
+ Events::Records::GestureRecord.new(gesture: "touch", status: status, finger: 1, delta: nil)
58
+ rescue EOFError => e
59
+ MultiLogger.error "#{self.class.name}: #{e}"
60
+ MultiLogger.error "Shutdown fusuma process..."
61
+ Process.kill("TERM", Process.pid)
62
+ end
63
+
64
+ private
65
+
66
+ # Retry and wait until hidraw is found
67
+ def find_hidraw_device(device_name_pattern, wait:)
68
+ device_finder = Hidraw::DeviceFinder.new
69
+ logged = false
70
+ loop do
71
+ device = device_finder.find(device_name_pattern)
72
+ if device
73
+ MultiLogger.info "Found pointing stick device: #{device_name_pattern}"
74
+
75
+ return device
76
+ end
77
+
78
+ MultiLogger.warn "No pointing stick device found: #{device_name_pattern}" unless logged
79
+ logged = true
80
+
81
+ sleep wait
82
+ end
83
+ end
84
+
85
+ # Select parser based on the bus type
86
+ # @param bustype [Integer]
87
+ def select_hidraw_parser(bustype)
88
+ case bustype
89
+ when Hidraw::Device::BUS_BLUETOOTH
90
+ Hidraw::HhkbBluetoothParser
91
+ when Hidraw::Device::BUS_USB
92
+ Hidraw::HhkbUsbParser
93
+ else
94
+ MultiLogger.error "Unsupported bus type: #{bustype}"
95
+ exit 1
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,4 @@
1
+ plugin:
2
+ inputs:
3
+ pointing_stick_input:
4
+ device_name_pattern:
@@ -3,7 +3,7 @@
3
3
  module Fusuma
4
4
  module Plugin
5
5
  module Thumbsense
6
- VERSION = "0.7.0"
6
+ VERSION = "0.9.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-thumbsense
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - iberianpig
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-24 00:00:00.000000000 Z
11
+ date: 2024-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fusuma
@@ -30,28 +30,28 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0.5'
33
+ version: '0.11'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0.5'
40
+ version: '0.11'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: fusuma-plugin-remap
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: '0.4'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
54
+ version: '0.4'
55
55
  description: fusuma-plugin-thumbsense is Fusuma plugin for thumbsense.
56
56
  email:
57
57
  - yhkyky@gmail.com
@@ -65,10 +65,13 @@ files:
65
65
  - bin/setup
66
66
  - fusuma-plugin-thumbsense.gemspec
67
67
  - lib/fusuma/plugin/buffers/thumbsense_buffer.rb
68
+ - lib/fusuma/plugin/buffers/thumbsense_buffer.yml
68
69
  - lib/fusuma/plugin/detectors/thumbsense_detector.rb
69
- - lib/fusuma/plugin/filters/thumbsense_filter.rb
70
- - lib/fusuma/plugin/filters/thumbsense_filter.yml
71
- - lib/fusuma/plugin/parsers/thumbsense_parser.rb
70
+ - lib/fusuma/plugin/inputs/hidraw/device.rb
71
+ - lib/fusuma/plugin/inputs/hidraw/hhkb_bluetooth_parser.rb
72
+ - lib/fusuma/plugin/inputs/hidraw/hhkb_usb_parser.rb
73
+ - lib/fusuma/plugin/inputs/pointing_stick_input.rb
74
+ - lib/fusuma/plugin/inputs/pointing_stick_input.yml
72
75
  - lib/fusuma/plugin/thumbsense.rb
73
76
  - lib/fusuma/plugin/thumbsense/version.rb
74
77
  homepage: https://github.com/iberianpig/fusuma-plugin-thumbsense
@@ -91,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
94
  - !ruby/object:Gem::Version
92
95
  version: '0'
93
96
  requirements: []
94
- rubygems_version: 3.4.10
97
+ rubygems_version: 3.4.19
95
98
  signing_key:
96
99
  specification_version: 4
97
100
  summary: Thumbsense plugin for Fusuma
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fusuma/device"
4
-
5
- module Fusuma
6
- module Plugin
7
- module Filters
8
- # Filter keyboard events from libinput_command_input
9
- class ThumbsenseFilter < Filter
10
- DEFAULT_SOURCE = "libinput_command_input"
11
-
12
- # @return [TrueClass] when keeping it
13
- # @return [FalseClass] when discarding it
14
- def keep?(record)
15
- case record.to_s
16
- when %r{\sevent\d+\s+-\sbutton state: touch (?<finger>[[:digit:]])}
17
- true
18
- when %r{\sevent\d+\s+-\spalm: touch (?<finger>[[:digit:]])}
19
- true
20
- else
21
- false
22
- end
23
- end
24
- end
25
- end
26
- end
27
- end
@@ -1,5 +0,0 @@
1
- plugin:
2
- inputs:
3
- libinput_command_input:
4
- verbose: true
5
- disable-dwt: true
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # require 'fusuma/plugin/parsers/parser.rb'
4
- # require 'fusuma/plugin/events/event.rb'
5
-
6
- module Fusuma
7
- module Plugin
8
- module Parsers
9
- # parse libinput and generate event
10
- class ThumbsenseParser < Parser
11
- DEFAULT_SOURCE = "libinput_command_input"
12
-
13
- # ... event7 - button state: touch 3 from BUTTON_STATE_AREA event BUTTON_EVENT_UP to BUTTON_STATE_NONE
14
- # 10766: event7 - button state: touch 1 from BUTTON_STATE_AREA event BUTTON_EVENT_UP to BUTTON_STATE_NONE
15
- # 10768: event7 - button state: touch 0 from BUTTON_STATE_AREA event BUTTON_EVENT_UP to BUTTON_STATE_NONE
16
-
17
- # @param record [String]
18
- # @return [Records::Gesture, nil]
19
- def parse_record(record)
20
- gesture = "touch"
21
-
22
- case record.to_s
23
-
24
- # touched
25
- when %r{\sevent\d+\s+-\sbutton state: touch (?<finger>[[:digit:]]) from BUTTON_STATE_NONE}
26
- status = "begin"
27
- finger = $~[:finger].to_i + 1
28
- # released
29
- when %r{\sevent\d+\s+-\sbutton state: touch (?<finger>[[:digit:]]) .* to BUTTON_STATE_NONE}
30
- status = "end"
31
- finger = $~[:finger].to_i + 1
32
-
33
- # palm
34
- when %r{\sevent\d+\s+-\spalm: touch (?<finger>[[:digit:]]) .*}
35
- status = "palm"
36
- finger = $~[:finger].to_i + 1
37
- else
38
- return
39
- end
40
-
41
- Events::Records::GestureRecord.new(status: status, gesture: gesture, finger: finger, delta: nil)
42
- end
43
-
44
- def tag
45
- "thumbsense_parser"
46
- end
47
- end
48
- end
49
- end
50
- end