fusuma-plugin-thumbsense 0.8.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: 1bdd2539bacc9ce3db7375ce445affc2a22fcadfc5a7849243737c8fe028911e
4
- data.tar.gz: 4b4b1e40b305ef98b032ae186828dbd69b50fb8d82e978fb02d7a2540f8da517
3
+ metadata.gz: a991e73e2eace58e3f6732a6a0f39b12b9e968550c613977eeba03ec650dadda
4
+ data.tar.gz: 8735f0050e321d7171558df00925b94f39ea307e6914f350fdcf69ed805002bc
5
5
  SHA512:
6
- metadata.gz: e69801eb4e32d544bf7ba7a322b9204683211372e94ef36375e1c2c70a730254638da594fcbf3e13b4c31a7d3a9cba64a25ef3b086696e44bf1dbccb5b145724
7
- data.tar.gz: 16d973013a91adc273687a849f48915c6acff2d3bff72ed97aaf44e7b89328051d32c32e1e0a2fb514453cdda80b0989df12bbe039df3209bd69d3d659780e1b
6
+ metadata.gz: 104fcb225426e2c0310a33277ab82e9a174bfa38dd3212d3b9294f72561b48b8a9ec73860478bc9ab16f53deb6a1272c8d21ca0221c71fe797e571786f6983a8
7
+ data.tar.gz: d5bf0fba767a5a1d1952cd2fd7f2fd4b9f033ac3c89b76dbfdc31ad5235a5ad0b3727e04437ddd5367eac27ccc256661959859cbd816708ab841588a4a61c524
data/README.md CHANGED
@@ -3,64 +3,54 @@
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
- - fusuma-plugin-remap is installed automatically.
21
- - 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.
22
-
23
- ### Install and set up fusuma-plugin-thumbsense
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)
24
16
 
25
- Run the following code in your terminal.
17
+ ### Steps to Install and Set Up Fusuma::Plugin::Thumbsense
26
18
 
27
- #### Install ruby-dev and build-essential for installing native extension
19
+ 1. Install the necessary packages for native extensions:
28
20
  ```sh
29
21
  $ sudo apt install ruby-dev build-essential
30
22
  ```
31
23
 
32
- #### Install libevdev-dev for building fusuma-plugin-remap
24
+ 2. Install the required library for building fusuma-plugin-remap:
33
25
  ```sh
34
26
  $ sudo apt install libevdev-dev
35
27
  ```
36
28
 
37
- #### set up udev rules for creating a virtual input device (fusuma-plugin-remap)
29
+ 3. Set up udev rules to create a virtual input device (for fusuma-plugin-remap):
38
30
  ```sh
39
31
  $ echo 'KERNEL=="uinput", MODE="0660", GROUP="input", OPTIONS+="static_node=uinput"' | sudo tee /etc/udev/rules.d/60-udev-fusuma-remap.rules
40
32
  $ sudo udevadm control --reload-rules && sudo udevadm trigger
41
33
  ```
42
34
 
43
-
44
- #### Install fusuma-plugin-thumbsense
35
+ 4. Install fusuma-plugin-thumbsense:
45
36
  ```sh
46
37
  $ sudo gem install fusuma-plugin-thumbsense
47
38
  ```
48
39
 
49
- ## Properties
40
+ ## Configuration
50
41
 
51
- ### Thumbsense context
42
+ ### Thumbsense Context
52
43
 
53
- First, add the thumbsense `context` to `~/.config/fusuma/config.yml`.
54
- The `context` is separated by `---` and specified by `context: thumbsense`.
44
+ To add the thumbsense `context`, edit `~/.config/fusuma/config.yml`.
45
+ The `context` section is separated by `---` and specified as `context: thumbsense`.
55
46
  Fusuma will switch to the `thumbsense` context while tapping the touchpad.
56
47
 
57
- ### Remap key to mouse button
48
+ ### Key to Mouse Button Remap
58
49
 
59
50
  You can remap keys to mouse buttons while tapping the touchpad.
60
- The `remap` property is specified in the `thumbsense` context.
61
-
62
- Available mouse buttons are:
51
+ The `remap` property is configured within the `thumbsense` context.
63
52
 
53
+ Available mouse buttons include:
64
54
  - `BTN_LEFT`
65
55
  - `BTN_MIDDLE`
66
56
  - `BTN_RIGHT`
@@ -74,13 +64,12 @@ Available mouse buttons are:
74
64
  - ...
75
65
  - `BTN_9`
76
66
 
77
- ## Example
67
+ ### Example Configuration
78
68
 
79
- Set the following code in `~/.config/fusuma/config.yml`.
69
+ Add the following code to `~/.config/fusuma/config.yml`:
80
70
 
81
71
  ```yaml
82
- # add thumbsense context
83
-
72
+ # Add thumbsense context
84
73
  ---
85
74
  context: thumbsense
86
75
 
@@ -93,17 +82,56 @@ remap:
93
82
  K: BTN_RIGHT
94
83
  ```
95
84
 
96
- ## 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
97
121
 
98
- - thumbsense
99
- - [x] change layer of remap while tapping
100
- - [ ] make it possible to execute executor like `command:` `sendkey:`
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
101
127
 
102
- - remap
103
- - [x] remap to single key like `remap: { J: BTN_LEFT }`
104
- - [x] send BTN_LEFT/BTN_MIDDLE/BTN_RIGHT click `remap: { I: BTN_MIDDLE }`
105
- - [ ] remap multiple keys like `remap: { H: LEFTCTRL+TAB }`
106
- - [ ] remap POINTER_MOTION to POINTER_SCROLL_FINGER `remap: { S: POINTER_SCROLL_FINGER }`
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 }`)
107
135
 
108
136
  ## Contributing
109
137
 
@@ -6,6 +6,7 @@ module Fusuma
6
6
  # manage events and generate command
7
7
  class ThumbsenseBuffer < Buffer
8
8
  DEFAULT_SOURCE = "remap_touchpad_input"
9
+ POINTING_STICK_SOURCE = "pointing_stick_input"
9
10
 
10
11
  def config_param_types
11
12
  {
@@ -26,10 +27,11 @@ module Fusuma
26
27
  # @param event [Event]
27
28
  # @return [NilClass, ThumbsenseBuffer]
28
29
  def buffer(event)
29
- return if event&.tag != source
30
-
31
- @events.push(event)
32
- self
30
+ case event.tag
31
+ when source, POINTING_STICK_SOURCE
32
+ @events.push(event)
33
+ self
34
+ end
33
35
  end
34
36
 
35
37
  # return [Integer]
@@ -37,21 +39,6 @@ module Fusuma
37
39
  @events.map { |e| e.record.finger }.max
38
40
  end
39
41
 
40
- def empty?
41
- @events.empty?
42
- end
43
-
44
- def present?
45
- !empty?
46
- end
47
-
48
- def select_by_events(&block)
49
- return enum_for(:select) unless block
50
-
51
- events = @events.select(&block)
52
- self.class.new events
53
- end
54
-
55
42
  def ended?(event)
56
43
  event.record.status == "end"
57
44
  end
@@ -107,11 +107,6 @@ module Fusuma
107
107
  codes
108
108
  end
109
109
 
110
- # @return [TrueClass, FalseClass]
111
- def touching?
112
- !touch_released?(@thumbsense_buffer)
113
- end
114
-
115
110
  # @return [TrueClass, FalseClass]
116
111
  def touch_released?
117
112
  return true if @thumbsense_buffer.empty?
@@ -126,6 +121,8 @@ module Fusuma
126
121
  last_keypress = @keypress_buffer.events.last.record
127
122
  return if last_keypress.status == "released"
128
123
 
124
+ return if MODIFIER_KEYS.include?(last_keypress.code)
125
+
129
126
  current_layer = last_keypress&.layer
130
127
  current_layer && current_layer["thumbsense"]
131
128
  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.8.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.8.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: 2024-04-15 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
@@ -67,6 +67,11 @@ files:
67
67
  - lib/fusuma/plugin/buffers/thumbsense_buffer.rb
68
68
  - lib/fusuma/plugin/buffers/thumbsense_buffer.yml
69
69
  - lib/fusuma/plugin/detectors/thumbsense_detector.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
70
75
  - lib/fusuma/plugin/thumbsense.rb
71
76
  - lib/fusuma/plugin/thumbsense/version.rb
72
77
  homepage: https://github.com/iberianpig/fusuma-plugin-thumbsense