fusuma-plugin-remap 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +98 -0
- data/bin/console +18 -0
- data/bin/setup +8 -0
- data/exe/fusuma-remap +44 -0
- data/fusuma-plugin-remap.gemspec +32 -0
- data/lib/fusuma/plugin/inputs/remap_keyboard_input.rb +113 -0
- data/lib/fusuma/plugin/remap/layer_manager.rb +73 -0
- data/lib/fusuma/plugin/remap/remapper.rb +232 -0
- data/lib/fusuma/plugin/remap/ruinput_device_patched.rb +49 -0
- data/lib/fusuma/plugin/remap/version.rb +9 -0
- data/lib/fusuma/plugin/remap.rb +10 -0
- metadata +132 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0cb6b77fe516b413998fa4e025528e5532e620fd7c7693c613d6cc3a02b9a8c0
|
4
|
+
data.tar.gz: bfb505583972722526bb56e5d8056fa1445b36e37bce3de3fe54cc824c44a2c2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e3e93232d8b457bed61e3e3d0cc5edca4a16277ec7064280920c026398df36de31ec4c8673113d79517c8d8e19c64feb2f938ce5f3e85c1735ab639b781c981f
|
7
|
+
data.tar.gz: 21c9eee15afcf106d5935e2b6aec029adafa953cbc1d3bc6160afb6abe42ccb4a395130c88a7fe52f9c506f4f1995ad55714a6a43d5cebd3ce15b04f06b83bc4
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 iberianpig
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# Fusuma::Plugin::Remap [![Gem Version](https://badge.fury.io/rb/fusuma-plugin-remap.svg)](https://badge.fury.io/rb/fusuma-plugin-remap) [![Build Status](https://github.com/iberianpig/fusuma-plugin-remap/actions/workflows/ubuntu.yml/badge.svg)](https://github.com/iberianpig/fusuma-plugin-remap/actions/workflows/ubuntu.yml)
|
2
|
+
|
3
|
+
## Installation
|
4
|
+
|
5
|
+
**THIS PLUGIN IS EXPERIMENTAL.**
|
6
|
+
|
7
|
+
A Fusuma plugin for remapping keyboard events into virtual input devices. Compatible with other Fusuma plugins.
|
8
|
+
|
9
|
+
This plugin empowers users to manipulate keyboard events and convert them into virtual input devices. It is designed to integrate seamlessly with other Fusuma plugins, thus enabling users to construct sophisticated input configurations and achieve distinctive functionalities. A key feature is the dynamic alteration of remapping layers within the Fusuma process, thereby enabling users to adapt their keyboard inputs to suit specific tasks or applications.
|
10
|
+
|
11
|
+
This plugin requires [fusuma](https://github.com/iberianpig/fusuma#update) 2.0
|
12
|
+
|
13
|
+
### Install dependencies
|
14
|
+
|
15
|
+
**NOTE: If you have installed ruby by apt, you must install ruby-dev.**
|
16
|
+
```sh
|
17
|
+
$ sudo apt-get install libevdev-dev ruby-dev build-essential
|
18
|
+
```
|
19
|
+
|
20
|
+
### Set up udev rules
|
21
|
+
|
22
|
+
fusuma-plugin-remap create virtual input device(`fusuma_virtual_keyboard`) by uinput. So you need to set up udev rules.
|
23
|
+
|
24
|
+
```sh
|
25
|
+
$ echo 'KERNEL=="uinput", MODE="0660", GROUP="input", OPTIONS+="static_node=uinput"' | sudo tee /etc/udev/rules.d/60-udev-fusuma-remap.rules
|
26
|
+
```
|
27
|
+
|
28
|
+
Then, reload udev rules.
|
29
|
+
|
30
|
+
```sh
|
31
|
+
$ sudo udevadm control --reload-rules && sudo udevadm trigger
|
32
|
+
```
|
33
|
+
|
34
|
+
### Install fusuma-plugin-remap
|
35
|
+
|
36
|
+
```sh
|
37
|
+
$ sudo gem install fusuma-plugin-remap
|
38
|
+
```
|
39
|
+
|
40
|
+
### Set plugin properties
|
41
|
+
|
42
|
+
Open `~/.config/fusuma/config.yml` and add the following code at the bottom in primary context(first section separated by `---`).
|
43
|
+
|
44
|
+
```yaml
|
45
|
+
plugin:
|
46
|
+
inputs:
|
47
|
+
remap_keyboard_input:
|
48
|
+
keyboard_name_patterns: xremap # (optional) specifiy other source keyboard name
|
49
|
+
buffers:
|
50
|
+
keypress_buffer:
|
51
|
+
source: remap_keyboard_input # (required) use buffer of fusuma-plugin-keypress
|
52
|
+
executors:
|
53
|
+
sendkey_executor:
|
54
|
+
device_name: fusuma_virtual_keyboard # (optional) when you use fusuma-plugin-sendkey
|
55
|
+
|
56
|
+
---
|
57
|
+
|
58
|
+
```
|
59
|
+
|
60
|
+
## Properties
|
61
|
+
|
62
|
+
### Remap
|
63
|
+
|
64
|
+
Currently, remapping is only possible in the thumbsense context.
|
65
|
+
Please install [fusuma-plugin-thumbsense](https://github.com/iberianpig/fusuma-plugin-thumbsense)
|
66
|
+
|
67
|
+
First, add the 'thumbsense' context to `~/.config/fusuma/config.yml`.
|
68
|
+
The context is separated by `---` and specified by `context: { thumbsense: true }`.
|
69
|
+
|
70
|
+
### Example
|
71
|
+
|
72
|
+
Set the following code in `~/.config/fusuma/config.yml`.
|
73
|
+
|
74
|
+
```yaml
|
75
|
+
|
76
|
+
---
|
77
|
+
context:
|
78
|
+
thumbsense: true
|
79
|
+
|
80
|
+
remap:
|
81
|
+
J: BTN_LEFT
|
82
|
+
K: BTN_RIGHT
|
83
|
+
F: BTN_LEFT
|
84
|
+
D: BTN_RIGHT
|
85
|
+
SPACE: BTN_LEFT
|
86
|
+
```
|
87
|
+
|
88
|
+
## Contributing
|
89
|
+
|
90
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/iberianpig/fusuma-plugin-remap. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
91
|
+
|
92
|
+
## License
|
93
|
+
|
94
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
95
|
+
|
96
|
+
## Code of Conduct
|
97
|
+
|
98
|
+
Everyone interacting in the Fusuma::Plugin::Remap project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/iberianpig/fusuma-plugin-remap/blob/master/CODE_OF_CONDUCT.md).
|
data/bin/console
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "fusuma/plugin/manager"
|
6
|
+
Fusuma::Plugin::Manager.require_base_plugins
|
7
|
+
|
8
|
+
require "fusuma/plugin/remap"
|
9
|
+
|
10
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
11
|
+
# with your gem easier. You can also use a different console, if you like.
|
12
|
+
|
13
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
14
|
+
# require "pry"
|
15
|
+
# Pry.start
|
16
|
+
|
17
|
+
require "irb"
|
18
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/exe/fusuma-remap
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "../lib/fusuma/plugin/remap/version"
|
5
|
+
require_relative "../lib/fusuma/plugin/remap/remapper"
|
6
|
+
require_relative "../lib/fusuma/plugin/remap/layer_manager"
|
7
|
+
require "fusuma/config"
|
8
|
+
require "revdev"
|
9
|
+
require "msgpack"
|
10
|
+
|
11
|
+
include Revdev
|
12
|
+
|
13
|
+
if ARGV.length > 1
|
14
|
+
source_keyboards = [Revdev::EventDevice.new(ARGV.shift)]
|
15
|
+
internal_touchpad = Revdev::EventDevice.new(ARGV.shift)
|
16
|
+
else
|
17
|
+
warn "Usage: #{$PROGRAM_NAME} /dev/input/KEYBOARD /dev/input/TOUCHPAD"
|
18
|
+
exit false
|
19
|
+
end
|
20
|
+
|
21
|
+
Fusuma::Config.instance.custom_path = "~/.config/fusuma/config.yml"
|
22
|
+
|
23
|
+
layer_manager = Fusuma::Plugin::Remap::LayerManager.instance
|
24
|
+
|
25
|
+
_keyboard_reader, keyboard_writer = IO.pipe
|
26
|
+
Thread.new do
|
27
|
+
layer_manager.send_layer(layer: {foo: "bar"})
|
28
|
+
sleep 3
|
29
|
+
layer_manager.send_layer(layer: {thumbsense: true})
|
30
|
+
sleep 3
|
31
|
+
layer_manager.send_layer(layer: {thumbsense: true}, remove: true)
|
32
|
+
|
33
|
+
# u = MessagePack::Unpacker.new(keyboard_reader)
|
34
|
+
# loop do
|
35
|
+
# puts "unpack: #{u.unpack}"
|
36
|
+
# end
|
37
|
+
end
|
38
|
+
|
39
|
+
Fusuma::Plugin::Remap::Remapper.new(
|
40
|
+
layer_manager: layer_manager,
|
41
|
+
keyboard_writer: keyboard_writer,
|
42
|
+
source_keyboards: source_keyboards,
|
43
|
+
internal_touchpad: internal_touchpad
|
44
|
+
).run
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "fusuma/plugin/remap/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "fusuma-plugin-remap"
|
9
|
+
spec.version = Fusuma::Plugin::Remap::VERSION
|
10
|
+
spec.authors = ["iberianpig"]
|
11
|
+
spec.email = ["yhkyky@gmail.com"]
|
12
|
+
|
13
|
+
spec.summary = "A Fusuma plugin for remapping keyboard events into virtual input devices."
|
14
|
+
spec.description = "This plugin empowers users to manipulate keyboard events and convert them into virtual input devices. It is designed to integrate seamlessly with other Fusuma plugins, thus enabling users to construct sophisticated input configurations and achieve distinctive functionalities. A key feature is the dynamic alteration of remapping layers within the Fusuma process, thereby enabling users to adapt their keyboard inputs to suit specific tasks or applications."
|
15
|
+
spec.homepage = "https://github.com/iberianpig/fusuma-plugin-remap"
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
spec.files = Dir["{bin,lib,exe}/**/*", "LICENSE*", "README*", "*.gemspec"]
|
19
|
+
spec.bindir = "exe"
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.required_ruby_version = ">= 2.5.1"
|
24
|
+
spec.add_dependency "fusuma", "~> 2.0"
|
25
|
+
spec.add_dependency "fusuma-plugin-keypress", "~> 0.5"
|
26
|
+
spec.add_dependency "msgpack"
|
27
|
+
spec.add_dependency "revdev"
|
28
|
+
spec.add_dependency "ruinput"
|
29
|
+
spec.metadata = {
|
30
|
+
"rubygems_mfa_required" => "true"
|
31
|
+
}
|
32
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fusuma/device"
|
4
|
+
require "fusuma/plugin/remap/remapper"
|
5
|
+
require "fusuma/plugin/remap/layer_manager"
|
6
|
+
|
7
|
+
module Fusuma
|
8
|
+
module Plugin
|
9
|
+
module Inputs
|
10
|
+
# Get keyboard events from remapper
|
11
|
+
class RemapKeyboardInput < Input
|
12
|
+
def config_param_types
|
13
|
+
{
|
14
|
+
keyboard_name_patterns: [Array, String],
|
15
|
+
touchpad_name_patterns: [Array, String]
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :pid
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
super
|
23
|
+
setup_remapper
|
24
|
+
end
|
25
|
+
|
26
|
+
def io
|
27
|
+
@keyboard_reader
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param record [String]
|
31
|
+
# @return [Event]
|
32
|
+
def create_event(record:)
|
33
|
+
data = MessagePack.unpack(record) # => {"key"=>"J", "status"=>1}
|
34
|
+
|
35
|
+
unless data.is_a? Hash
|
36
|
+
MultiLogger.error("Invalid record: #{record}", data: data)
|
37
|
+
return
|
38
|
+
end
|
39
|
+
|
40
|
+
code = data["key"]
|
41
|
+
status = (data["status"] == 1) ? "pressed" : "released"
|
42
|
+
record = Events::Records::KeypressRecord.new(status: status, code: code)
|
43
|
+
|
44
|
+
e = Events::Event.new(tag: tag, record: record)
|
45
|
+
MultiLogger.debug(input_event: e)
|
46
|
+
e
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def setup_remapper
|
52
|
+
layer_manager = Remap::LayerManager.instance
|
53
|
+
|
54
|
+
# physical keyboard input event
|
55
|
+
@keyboard_reader, keyboard_writer = IO.pipe
|
56
|
+
|
57
|
+
source_keyboards = KeyboardSelector.new(config_params(:keyboard_name_patterns)).select
|
58
|
+
internal_touchpad = TouchpadSelector.new(config_params(:touchpad_name_patterns)).select.first
|
59
|
+
MultiLogger.info(source_keyboards: source_keyboards.map(&:device_name), internal_touchpad: internal_touchpad.device_name)
|
60
|
+
|
61
|
+
@pid = fork do
|
62
|
+
layer_manager.writer.close
|
63
|
+
@keyboard_reader.close
|
64
|
+
remapper = Remap::Remapper.new(
|
65
|
+
layer_manager: layer_manager,
|
66
|
+
source_keyboards: source_keyboards,
|
67
|
+
keyboard_writer: keyboard_writer,
|
68
|
+
internal_touchpad: internal_touchpad
|
69
|
+
)
|
70
|
+
remapper.run
|
71
|
+
end
|
72
|
+
layer_manager.reader.close
|
73
|
+
keyboard_writer.close
|
74
|
+
end
|
75
|
+
|
76
|
+
# Devices to detect key presses and releases
|
77
|
+
class KeyboardSelector
|
78
|
+
def initialize(names = ["keyboard", "Keyboard", "KEYBOARD"])
|
79
|
+
@names = names
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Array<Revdev::EventDevice>]
|
83
|
+
def select
|
84
|
+
devices = Fusuma::Device.all.select { |d| Array(@names).any? { |name| d.name =~ /#{name}/ } }
|
85
|
+
devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class TouchpadSelector
|
90
|
+
def initialize(names)
|
91
|
+
@names = names
|
92
|
+
end
|
93
|
+
|
94
|
+
# @return [Array<Revdev::EventDevice>]
|
95
|
+
def select
|
96
|
+
devices = if @names
|
97
|
+
Fusuma::Device.all.select do |d|
|
98
|
+
Array(@names).any? do |name|
|
99
|
+
d.name =~ /#{name}/
|
100
|
+
end
|
101
|
+
end
|
102
|
+
else
|
103
|
+
# available returns only touchpad devices
|
104
|
+
Fusuma::Device.available
|
105
|
+
end
|
106
|
+
|
107
|
+
devices.map { |d| Revdev::EventDevice.new("/dev/input/#{d.id}") }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "fusuma/config"
|
2
|
+
require "msgpack"
|
3
|
+
|
4
|
+
module Fusuma
|
5
|
+
module Plugin
|
6
|
+
module Remap
|
7
|
+
# Set layer sent from pipe
|
8
|
+
class LayerManager
|
9
|
+
require "singleton"
|
10
|
+
include Singleton
|
11
|
+
attr_reader :reader, :writer, :current_layer
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@layers = {}
|
15
|
+
@reader, @writer = IO.pipe
|
16
|
+
@current_layer = {} # preserve order
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param [Hash] layer
|
20
|
+
# @param [Boolean] remove
|
21
|
+
def send_layer(layer:, remove: false)
|
22
|
+
# puts "send_layer: #{layer} remove: #{remove}"
|
23
|
+
return if @last_layer == layer && @last_remove == remove
|
24
|
+
|
25
|
+
@last_layer = layer
|
26
|
+
@last_remove = remove
|
27
|
+
@writer.puts({layer: layer, remove: remove}.to_msgpack)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Read layer from pipe and return remap layer
|
31
|
+
# @example
|
32
|
+
# @return [Hash]
|
33
|
+
def receive_layer
|
34
|
+
@layer_unpacker ||= MessagePack::Unpacker.new(@reader)
|
35
|
+
|
36
|
+
data = @layer_unpacker.unpack
|
37
|
+
|
38
|
+
return unless data.is_a? Hash
|
39
|
+
|
40
|
+
data = data.deep_symbolize_keys
|
41
|
+
layer = data[:layer] # e.g { thumbsense: true }
|
42
|
+
remove = data[:remove] # e.g true
|
43
|
+
|
44
|
+
if remove
|
45
|
+
@current_layer.delete_if { |k, _v| layer.key?(k) }
|
46
|
+
else
|
47
|
+
# If duplicate keys exist, order of keys is preserved
|
48
|
+
@current_layer.merge!(layer)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Find remap layer from config
|
53
|
+
# @param [Hash] layer
|
54
|
+
# @return [Hash]
|
55
|
+
def find_mapping(layer = @current_layer)
|
56
|
+
@layers[layer] ||= begin
|
57
|
+
result = nil
|
58
|
+
_ = Fusuma::Config::Searcher.find_context(layer) {
|
59
|
+
result = Fusuma::Config.search(Fusuma::Config::Index.new(:remap))
|
60
|
+
next unless result
|
61
|
+
|
62
|
+
result = result.deep_transform_keys do |key|
|
63
|
+
key.upcase.to_sym
|
64
|
+
end
|
65
|
+
}
|
66
|
+
|
67
|
+
result || {}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
require "revdev"
|
2
|
+
require "msgpack"
|
3
|
+
require "set"
|
4
|
+
require_relative "./layer_manager"
|
5
|
+
|
6
|
+
require_relative "./ruinput_device_patched"
|
7
|
+
|
8
|
+
module Fusuma
|
9
|
+
module Plugin
|
10
|
+
module Remap
|
11
|
+
class Remapper
|
12
|
+
include Revdev
|
13
|
+
# @param layer_manager [Fusuma::Plugin::Remap::LayerManager]
|
14
|
+
# @param keyboard_writer [IO]
|
15
|
+
# @param source_keyboards [Array<Revdev::Device>]
|
16
|
+
# @param internal_touchpad [Revdev::Device]
|
17
|
+
def initialize(layer_manager:, keyboard_writer:, source_keyboards:, internal_touchpad:)
|
18
|
+
@layer_manager = layer_manager # request to change layer
|
19
|
+
@keyboard_writer = keyboard_writer # write event to original keyboard
|
20
|
+
@source_keyboards = source_keyboards # original keyboard
|
21
|
+
@internal_touchpad = internal_touchpad # internal touchpad
|
22
|
+
@uinput = RuinputDevicePatched.new "/dev/uinput"
|
23
|
+
@pressed_virtual_keys = Set.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def run
|
27
|
+
create_virtual_keyboard
|
28
|
+
set_trap
|
29
|
+
grab_keyboards
|
30
|
+
|
31
|
+
old_ie = nil
|
32
|
+
next_mapping = nil
|
33
|
+
current_mapping = {}
|
34
|
+
|
35
|
+
loop do
|
36
|
+
ios = IO.select([*@source_keyboards.map(&:file), @layer_manager.reader])
|
37
|
+
io = ios.first.first
|
38
|
+
|
39
|
+
if io == @layer_manager.reader
|
40
|
+
begin
|
41
|
+
@layer_manager.receive_layer
|
42
|
+
rescue => e
|
43
|
+
MultiLogger.error "#{e.class}: #{e.message}, #{e.backtrace.first}"
|
44
|
+
raise
|
45
|
+
end
|
46
|
+
|
47
|
+
MultiLogger.debug "Remapper#run: layer changed to #{@layer_manager.current_layer}"
|
48
|
+
next_mapping = @layer_manager.find_mapping
|
49
|
+
next
|
50
|
+
end
|
51
|
+
|
52
|
+
if next_mapping && virtual_keyboard_all_key_released?
|
53
|
+
if current_mapping != next_mapping
|
54
|
+
current_mapping = next_mapping
|
55
|
+
end
|
56
|
+
next_mapping = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
input_event = @source_keyboards.find { |kbd| kbd.file == io }.read_input_event
|
60
|
+
input_key = find_key_from_code(input_event.code)
|
61
|
+
|
62
|
+
if input_event.type == EV_KEY
|
63
|
+
# FIXME: exit when RIGHTCTRL-LEFTCTRL is pressed
|
64
|
+
if (old_ie&.code == KEY_RIGHTCTRL && old_ie.value != 0) && (input_event.code == KEY_LEFTCTRL && input_event.value != 0)
|
65
|
+
@destroy.call
|
66
|
+
end
|
67
|
+
|
68
|
+
old_ie = input_event
|
69
|
+
if input_event.value != 2 # repeat
|
70
|
+
packed = {key: input_key, status: input_event.value}.to_msgpack
|
71
|
+
@keyboard_writer.puts(packed)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
remapped = current_mapping.fetch(input_key.to_sym, nil)
|
76
|
+
if remapped.nil?
|
77
|
+
@uinput.write_input_event(input_event)
|
78
|
+
next
|
79
|
+
end
|
80
|
+
|
81
|
+
remapped_event = InputEvent.new(nil, input_event.type, find_code_from_key(remapped), input_event.value)
|
82
|
+
|
83
|
+
# When Set.delete? fails, it means that the key was pressed before remapping started and was released.
|
84
|
+
unless record_virtual_keyboard_event?(remapped, remapped_event.value)
|
85
|
+
# set original key before remapping
|
86
|
+
remapped_event.code = input_event.code
|
87
|
+
end
|
88
|
+
|
89
|
+
# remap to command will be nil
|
90
|
+
# e.g) remap: { X: { command: echo 'foo' } }
|
91
|
+
# this is because the command will be executed by fusuma process
|
92
|
+
next if remapped_event.code.nil?
|
93
|
+
|
94
|
+
@uinput.write_input_event(remapped_event)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# @param [Revdev::InputEvent] event
|
99
|
+
# @return [void]
|
100
|
+
def record_virtual_keyboard_event?(remapped_value, event_value)
|
101
|
+
case event_value
|
102
|
+
when 0
|
103
|
+
@pressed_virtual_keys.delete?(remapped_value)
|
104
|
+
when 1
|
105
|
+
@pressed_virtual_keys.add?(remapped_value)
|
106
|
+
else
|
107
|
+
# 2 is repeat
|
108
|
+
true
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def virtual_keyboard_all_key_released?
|
113
|
+
@pressed_virtual_keys.empty?
|
114
|
+
end
|
115
|
+
|
116
|
+
def create_virtual_keyboard
|
117
|
+
@uinput.create "fusuma_virtual_keyboard",
|
118
|
+
Revdev::InputId.new(
|
119
|
+
# recognized as an internal keyboard on libinput,
|
120
|
+
# touchpad is disabled when typing
|
121
|
+
# see: (https://wayland.freedesktop.org/libinput/doc/latest/palm-detection.html#disable-while-typing)
|
122
|
+
{
|
123
|
+
bustype: Revdev::BUS_I8042,
|
124
|
+
vendor: @internal_touchpad.device_id.vendor,
|
125
|
+
product: @internal_touchpad.device_id.product,
|
126
|
+
version: @internal_touchpad.device_id.version
|
127
|
+
}
|
128
|
+
)
|
129
|
+
end
|
130
|
+
|
131
|
+
def grab_keyboards
|
132
|
+
@source_keyboards.each do |keyboard|
|
133
|
+
wait_release_all_keys(keyboard) && keyboard.grab
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def set_trap
|
138
|
+
@destroy = lambda do
|
139
|
+
begin
|
140
|
+
@source_keyboards.each { |kbd| kbd.ungrab }
|
141
|
+
rescue => e
|
142
|
+
puts e.message
|
143
|
+
end
|
144
|
+
begin
|
145
|
+
@uinput.destroy
|
146
|
+
rescue => e
|
147
|
+
puts e.message
|
148
|
+
end
|
149
|
+
exit 0
|
150
|
+
end
|
151
|
+
|
152
|
+
Signal.trap(:INT) { @destroy.call }
|
153
|
+
Signal.trap(:TERM) { @destroy.call }
|
154
|
+
end
|
155
|
+
|
156
|
+
# Find remappable key from mapping and return remapped key code
|
157
|
+
# If not found, return original key code
|
158
|
+
# If the key is found but its value is not valid, return nil
|
159
|
+
# @example
|
160
|
+
# find_remapped_code({ "A" => "b" }, 30) # => 48
|
161
|
+
# find_remapped_code({ "A" => "b" }, 100) # => 100
|
162
|
+
# find_remapped_code({ "A" => {command: 'echo foobar'} }, 30) # => nil
|
163
|
+
#
|
164
|
+
# @param [Hash] mapping
|
165
|
+
# @param [Integer] code
|
166
|
+
# @return [Integer, nil]
|
167
|
+
def find_remapped_code(mapping, code)
|
168
|
+
key = find_key_from_code(code) # key = "A"
|
169
|
+
remapped_key = mapping.fetch(key.to_sym, nil) # remapped_key = "b"
|
170
|
+
return code unless remapped_key # return original code if key is not found
|
171
|
+
|
172
|
+
find_code_from_key(remapped_key) # remapped_code = 48
|
173
|
+
end
|
174
|
+
|
175
|
+
# Find key name from key code
|
176
|
+
# @example
|
177
|
+
# find_key_from_code(30) # => "A"
|
178
|
+
# find_key_from_code(48) # => "B"
|
179
|
+
# @param [Integer] code
|
180
|
+
# @return [String]
|
181
|
+
def find_key_from_code(code)
|
182
|
+
# { 30 => :A, 48 => :B, ... }
|
183
|
+
@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
|
184
|
+
@keys_per_code[code]
|
185
|
+
end
|
186
|
+
|
187
|
+
# Find key code from key name (e.g. "A", "B", "BTN_LEFT")
|
188
|
+
# If key name is not found, return nil
|
189
|
+
# @example
|
190
|
+
# find_code_from_key("A") # => 30
|
191
|
+
# find_code_from_key("B") # => 48
|
192
|
+
# find_code_from_key("BTN_LEFT") # => 272
|
193
|
+
# find_code_from_key("NOT_FOUND") # => nil
|
194
|
+
# @param [String] key
|
195
|
+
# @return [Integer] when key is available
|
196
|
+
# @return [nil] when key is not available
|
197
|
+
def find_code_from_key(key)
|
198
|
+
# { KEY_A => 30, KEY_B => 48, ... }
|
199
|
+
@codes_per_key ||= Revdev.constants.select { |c| c.start_with?("KEY_", "BTN_") }.map { |c| [c, Revdev.const_get(c)] }.to_h
|
200
|
+
|
201
|
+
case key
|
202
|
+
when String
|
203
|
+
if key.start_with?("BTN_")
|
204
|
+
@codes_per_key[key.upcase.to_sym]
|
205
|
+
else
|
206
|
+
@codes_per_key["KEY_#{key}".upcase.to_sym]
|
207
|
+
end
|
208
|
+
when Integer
|
209
|
+
@codes_per_key["KEY_#{key}".upcase.to_sym]
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def released_all_keys?(device)
|
214
|
+
# key status if all bytes are 0, the key is not pressed
|
215
|
+
bytes = device.read_ioctl_with(Revdev::EVIOCGKEY)
|
216
|
+
bytes.unpack("C*").all? { |byte| byte == 0 }
|
217
|
+
end
|
218
|
+
|
219
|
+
def wait_release_all_keys(device, &block)
|
220
|
+
loop do
|
221
|
+
if released_all_keys?(device)
|
222
|
+
break true
|
223
|
+
else
|
224
|
+
# wait until all keys are released
|
225
|
+
device.read_input_event
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "ruinput"
|
2
|
+
|
3
|
+
class RuinputDevicePatched < Ruinput::UinputDevice
|
4
|
+
include Ruinput
|
5
|
+
def set_all_events
|
6
|
+
raise "invalid method call: this uinput is already created" if @is_created
|
7
|
+
|
8
|
+
mouse_btns = [
|
9
|
+
Revdev::BTN_0,
|
10
|
+
Revdev::BTN_MISC,
|
11
|
+
Revdev::BTN_1,
|
12
|
+
Revdev::BTN_2,
|
13
|
+
Revdev::BTN_3,
|
14
|
+
Revdev::BTN_4,
|
15
|
+
Revdev::BTN_5,
|
16
|
+
Revdev::BTN_6,
|
17
|
+
Revdev::BTN_7,
|
18
|
+
Revdev::BTN_8,
|
19
|
+
Revdev::BTN_9,
|
20
|
+
Revdev::BTN_LEFT,
|
21
|
+
Revdev::BTN_MOUSE,
|
22
|
+
Revdev::BTN_MIDDLE,
|
23
|
+
Revdev::BTN_RIGHT
|
24
|
+
].freeze
|
25
|
+
|
26
|
+
keyboard_keys = Revdev.constants.select { |c| c.start_with? "KEY_" }.map { |c| Revdev.const_get(c) }.freeze
|
27
|
+
|
28
|
+
@file.ioctl UI_SET_EVBIT, Revdev::EV_KEY
|
29
|
+
(Revdev::KEY_RESERVED...Revdev::KEY_CNT).each do |n|
|
30
|
+
# https://github.com/mooz/xkeysnail/pull/101/files
|
31
|
+
next unless keyboard_keys.include?(n) || mouse_btns.include?(n)
|
32
|
+
|
33
|
+
@file.ioctl UI_SET_KEYBIT, n
|
34
|
+
end
|
35
|
+
|
36
|
+
# @file.ioctl UI_SET_EVBIT, Revdev::EV_MSC
|
37
|
+
# Revdev::MSC_CNT.times do |i|
|
38
|
+
# @file.ioctl UI_SET_MSCBIT, i
|
39
|
+
# end
|
40
|
+
|
41
|
+
# kernel bug: device has min == max on ABS_Y
|
42
|
+
# @file.ioctl UI_SET_EVBIT, Revdev::EV_ABS
|
43
|
+
# Revdev::ABS_CNT.times do |i|
|
44
|
+
# @file.ioctl UI_SET_ABSBIT, i
|
45
|
+
# end
|
46
|
+
|
47
|
+
@file.ioctl UI_SET_EVBIT, Revdev::EV_REP
|
48
|
+
end
|
49
|
+
end
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fusuma-plugin-remap
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- iberianpig
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-05-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: fusuma
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: fusuma-plugin-keypress
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.5'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.5'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: msgpack
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: revdev
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: ruinput
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: This plugin empowers users to manipulate keyboard events and convert
|
84
|
+
them into virtual input devices. It is designed to integrate seamlessly with other
|
85
|
+
Fusuma plugins, thus enabling users to construct sophisticated input configurations
|
86
|
+
and achieve distinctive functionalities. A key feature is the dynamic alteration
|
87
|
+
of remapping layers within the Fusuma process, thereby enabling users to adapt their
|
88
|
+
keyboard inputs to suit specific tasks or applications.
|
89
|
+
email:
|
90
|
+
- yhkyky@gmail.com
|
91
|
+
executables:
|
92
|
+
- fusuma-remap
|
93
|
+
extensions: []
|
94
|
+
extra_rdoc_files: []
|
95
|
+
files:
|
96
|
+
- LICENSE.txt
|
97
|
+
- README.md
|
98
|
+
- bin/console
|
99
|
+
- bin/setup
|
100
|
+
- exe/fusuma-remap
|
101
|
+
- fusuma-plugin-remap.gemspec
|
102
|
+
- lib/fusuma/plugin/inputs/remap_keyboard_input.rb
|
103
|
+
- lib/fusuma/plugin/remap.rb
|
104
|
+
- lib/fusuma/plugin/remap/layer_manager.rb
|
105
|
+
- lib/fusuma/plugin/remap/remapper.rb
|
106
|
+
- lib/fusuma/plugin/remap/ruinput_device_patched.rb
|
107
|
+
- lib/fusuma/plugin/remap/version.rb
|
108
|
+
homepage: https://github.com/iberianpig/fusuma-plugin-remap
|
109
|
+
licenses:
|
110
|
+
- MIT
|
111
|
+
metadata:
|
112
|
+
rubygems_mfa_required: 'true'
|
113
|
+
post_install_message:
|
114
|
+
rdoc_options: []
|
115
|
+
require_paths:
|
116
|
+
- lib
|
117
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: 2.5.1
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
requirements:
|
124
|
+
- - ">="
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubygems_version: 3.2.22
|
129
|
+
signing_key:
|
130
|
+
specification_version: 4
|
131
|
+
summary: A Fusuma plugin for remapping keyboard events into virtual input devices.
|
132
|
+
test_files: []
|