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 +4 -4
- data/README.md +66 -38
- data/lib/fusuma/plugin/buffers/thumbsense_buffer.rb +6 -19
- data/lib/fusuma/plugin/detectors/thumbsense_detector.rb +2 -5
- data/lib/fusuma/plugin/inputs/hidraw/device.rb +102 -0
- data/lib/fusuma/plugin/inputs/hidraw/hhkb_bluetooth_parser.rb +162 -0
- data/lib/fusuma/plugin/inputs/hidraw/hhkb_usb_parser.rb +113 -0
- data/lib/fusuma/plugin/inputs/pointing_stick_input.rb +101 -0
- data/lib/fusuma/plugin/inputs/pointing_stick_input.yml +4 -0
- data/lib/fusuma/plugin/thumbsense/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a991e73e2eace58e3f6732a6a0f39b12b9e968550c613977eeba03ec650dadda
|
4
|
+
data.tar.gz: 8735f0050e321d7171558df00925b94f39ea307e6914f350fdcf69ed805002bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
###
|
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
|
-
|
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
|
-
|
17
|
+
### Steps to Install and Set Up Fusuma::Plugin::Thumbsense
|
26
18
|
|
27
|
-
|
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
|
-
|
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
|
-
|
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
|
-
##
|
40
|
+
## Configuration
|
50
41
|
|
51
|
-
### Thumbsense
|
42
|
+
### Thumbsense Context
|
52
43
|
|
53
|
-
|
54
|
-
The `context` is separated by `---` and specified
|
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
|
-
###
|
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
|
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
|
-
|
67
|
+
### Example Configuration
|
78
68
|
|
79
|
-
|
69
|
+
Add the following code to `~/.config/fusuma/config.yml`:
|
80
70
|
|
81
71
|
```yaml
|
82
|
-
#
|
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
|
-
##
|
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
|
-
-
|
99
|
-
- [x]
|
100
|
-
- [
|
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
|
-
-
|
103
|
-
- [x]
|
104
|
-
- [x]
|
105
|
-
- [
|
106
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
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.
|
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-
|
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
|