fusuma 0.11.1 → 1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +8 -0
- data/.gitignore +6 -0
- data/.reek.yml +93 -45
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +16 -75
- data/Gemfile +2 -0
- data/README.md +12 -5
- data/Rakefile +2 -1
- data/bin/console +1 -1
- data/exe/fusuma +1 -0
- data/fusuma.gemspec +9 -2
- data/lib/fusuma.rb +88 -31
- data/lib/fusuma/config.rb +65 -66
- data/lib/fusuma/config/index.rb +49 -0
- data/lib/fusuma/device.rb +58 -37
- data/lib/fusuma/multi_logger.rb +3 -0
- data/lib/fusuma/plugin/base.rb +56 -0
- data/lib/fusuma/plugin/buffers/buffer.rb +41 -0
- data/lib/fusuma/plugin/buffers/gesture_buffer.rb +70 -0
- data/lib/fusuma/plugin/detectors/detector.rb +41 -0
- data/lib/fusuma/plugin/detectors/pinch_detector.rb +141 -0
- data/lib/fusuma/plugin/detectors/rotate_detector.rb +135 -0
- data/lib/fusuma/plugin/detectors/swipe_detector.rb +145 -0
- data/lib/fusuma/plugin/events/event.rb +38 -0
- data/lib/fusuma/plugin/events/records/gesture_record.rb +31 -0
- data/lib/fusuma/plugin/events/records/index_record.rb +53 -0
- data/lib/fusuma/plugin/events/records/record.rb +20 -0
- data/lib/fusuma/plugin/events/records/text_record.rb +28 -0
- data/lib/fusuma/plugin/executors/command_executor.rb +39 -0
- data/lib/fusuma/plugin/executors/executor.rb +27 -0
- data/lib/fusuma/plugin/filters/filter.rb +40 -0
- data/lib/fusuma/plugin/filters/libinput_device_filter.rb +42 -0
- data/lib/fusuma/plugin/inputs/input.rb +28 -0
- data/lib/fusuma/plugin/inputs/libinput_command_input.rb +133 -0
- data/lib/fusuma/plugin/manager.rb +118 -0
- data/lib/fusuma/plugin/parsers/libinput_gesture_parser.rb +54 -0
- data/lib/fusuma/plugin/parsers/parser.rb +46 -0
- data/lib/fusuma/version.rb +3 -1
- metadata +74 -14
- data/lib/fusuma/command_executor.rb +0 -43
- data/lib/fusuma/event_stack.rb +0 -87
- data/lib/fusuma/gesture_event.rb +0 -50
- data/lib/fusuma/libinput_commands.rb +0 -98
- data/lib/fusuma/pinch.rb +0 -58
- data/lib/fusuma/swipe.rb +0 -59
data/lib/fusuma/config.rb
CHANGED
@@ -1,88 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './multi_logger.rb'
|
4
|
+
require_relative './config/index.rb'
|
5
|
+
require 'singleton'
|
6
|
+
require 'yaml'
|
7
|
+
|
1
8
|
# module as namespace
|
2
9
|
module Fusuma
|
3
|
-
require 'singleton'
|
4
10
|
# read keymap from yaml file
|
5
11
|
class Config
|
6
12
|
include Singleton
|
7
13
|
|
8
14
|
class << self
|
9
|
-
def
|
10
|
-
instance.
|
15
|
+
def search(keys)
|
16
|
+
instance.search(keys)
|
11
17
|
end
|
12
18
|
|
13
|
-
def
|
14
|
-
instance.
|
15
|
-
end
|
16
|
-
|
17
|
-
def threshold(command_executor)
|
18
|
-
instance.threshold(command_executor)
|
19
|
-
end
|
20
|
-
|
21
|
-
def interval(command_executor)
|
22
|
-
instance.interval(command_executor)
|
23
|
-
end
|
24
|
-
|
25
|
-
def reload
|
26
|
-
instance.reload
|
19
|
+
def custom_path=(new_path)
|
20
|
+
instance.custom_path = new_path
|
27
21
|
end
|
28
22
|
end
|
29
23
|
|
30
24
|
attr_reader :keymap
|
31
|
-
|
25
|
+
attr_reader :custom_path
|
32
26
|
|
33
27
|
def initialize
|
34
|
-
|
28
|
+
@custom_path = nil
|
29
|
+
@cache = nil
|
30
|
+
@keymap = nil
|
31
|
+
reload
|
32
|
+
end
|
33
|
+
|
34
|
+
def custom_path=(new_path)
|
35
|
+
@custom_path = new_path
|
35
36
|
reload
|
36
37
|
end
|
37
38
|
|
38
39
|
def reload
|
39
40
|
@cache = nil
|
40
|
-
@keymap = YAML.load_file(file_path)
|
41
|
+
@keymap = YAML.load_file(file_path).deep_symbolize_keys
|
42
|
+
MultiLogger.info "reload config : #{file_path}"
|
41
43
|
self
|
42
44
|
end
|
43
45
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
def interval(command_executor)
|
62
|
-
seek_index_specific = [*event_index(command_executor), 'interval']
|
63
|
-
seek_index_global = ['interval', command_executor.event_type]
|
64
|
-
search_config_cached(seek_index_specific) ||
|
65
|
-
search_config_cached(seek_index_global) || 1
|
46
|
+
# @param index [Index]
|
47
|
+
def search(index)
|
48
|
+
cache(index.cache_key) do
|
49
|
+
index.keys.reduce(keymap) do |location, key|
|
50
|
+
if location.is_a?(Hash)
|
51
|
+
begin
|
52
|
+
if key.skippable
|
53
|
+
location.fetch(key.symbol, location)
|
54
|
+
else
|
55
|
+
location.fetch(key.symbol, nil)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
else
|
59
|
+
location
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
66
63
|
end
|
67
64
|
|
68
65
|
private
|
69
66
|
|
70
|
-
def search_config_cached(seek_index)
|
71
|
-
cache(seek_index) { search_config(keymap, seek_index) }
|
72
|
-
end
|
73
|
-
|
74
|
-
def search_config(keymap_node, seek_index)
|
75
|
-
if seek_index == []
|
76
|
-
return nil if keymap_node.is_a? Hash
|
77
|
-
return keymap_node
|
78
|
-
end
|
79
|
-
key = seek_index[0]
|
80
|
-
child_node = keymap_node[key]
|
81
|
-
next_index = seek_index[1..-1]
|
82
|
-
return search_config(child_node, next_index) if child_node
|
83
|
-
search_config(keymap_node, next_index)
|
84
|
-
end
|
85
|
-
|
86
67
|
def file_path
|
87
68
|
filename = 'fusuma/config.yml'
|
88
69
|
if custom_path && File.exist?(expand_custom_path)
|
@@ -106,17 +87,35 @@ module Fusuma
|
|
106
87
|
File.expand_path "../../#{filename}", __FILE__
|
107
88
|
end
|
108
89
|
|
109
|
-
def event_index(command_executor)
|
110
|
-
event_type = command_executor.event_type
|
111
|
-
finger = command_executor.finger
|
112
|
-
direction = command_executor.direction
|
113
|
-
[event_type, finger, direction]
|
114
|
-
end
|
115
|
-
|
116
90
|
def cache(key)
|
117
91
|
@cache ||= {}
|
118
92
|
key = key.join(',') if key.is_a? Array
|
119
|
-
@cache
|
93
|
+
if @cache.key?(key)
|
94
|
+
@cache[key]
|
95
|
+
else
|
96
|
+
@cache[key] = block_given? ? yield : nil
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# activesupport-4.1.1/lib/active_support/core_ext/hash/keys.rb
|
103
|
+
class Hash
|
104
|
+
def deep_symbolize_keys
|
105
|
+
deep_transform_keys do |key|
|
106
|
+
begin
|
107
|
+
key.to_sym
|
108
|
+
rescue StandardError
|
109
|
+
key
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def deep_transform_keys(&block)
|
115
|
+
result = {}
|
116
|
+
each do |key, value|
|
117
|
+
result[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys(&block) : value
|
120
118
|
end
|
119
|
+
result
|
121
120
|
end
|
122
121
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Index for searching value from config.yml
|
4
|
+
module Fusuma
|
5
|
+
class Config
|
6
|
+
# index for config.yml
|
7
|
+
class Index
|
8
|
+
def initialize(keys)
|
9
|
+
@keys = case keys
|
10
|
+
when Array
|
11
|
+
keys.map do |key|
|
12
|
+
if key.is_a? Key
|
13
|
+
key
|
14
|
+
else
|
15
|
+
Key.new(key)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
else
|
19
|
+
[Key.new(keys)]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
attr_reader :keys
|
23
|
+
|
24
|
+
def cache_key
|
25
|
+
case @keys
|
26
|
+
when Array
|
27
|
+
@keys.map(&:symbol).join(',')
|
28
|
+
when Key
|
29
|
+
@keys.symbol
|
30
|
+
else
|
31
|
+
raise 'invalid keys'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Keys in Index
|
36
|
+
class Key
|
37
|
+
def initialize(symbol_word, skippable: false)
|
38
|
+
@symbol = begin
|
39
|
+
symbol_word.to_sym
|
40
|
+
rescue StandardError
|
41
|
+
symbol_word
|
42
|
+
end
|
43
|
+
@skippable = skippable
|
44
|
+
end
|
45
|
+
attr_reader :symbol, :skippable
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/fusuma/device.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Fusuma
|
2
4
|
# detect input device
|
3
5
|
class Device
|
4
|
-
|
5
|
-
|
6
|
-
|
6
|
+
attr_reader :available
|
7
|
+
attr_reader :name
|
8
|
+
attr_reader :id
|
7
9
|
|
8
10
|
def initialize(id: nil, name: nil, available: nil)
|
9
11
|
@id = id
|
@@ -11,68 +13,81 @@ module Fusuma
|
|
11
13
|
@available = available
|
12
14
|
end
|
13
15
|
|
14
|
-
# @param [Hash]
|
16
|
+
# @param attributes [Hash]
|
15
17
|
def assign_attributes(attributes)
|
16
18
|
attributes.each do |k, v|
|
17
19
|
case k
|
18
20
|
when :id
|
19
|
-
|
21
|
+
@id = v
|
20
22
|
when :name
|
21
|
-
|
23
|
+
@name = v
|
22
24
|
when :available
|
23
|
-
|
25
|
+
@available = v
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
27
29
|
|
28
30
|
class << self
|
29
|
-
|
30
|
-
def ids
|
31
|
-
available.map(&:id)
|
32
|
-
end
|
31
|
+
attr_reader :given_devices
|
33
32
|
|
34
33
|
# @return [Array]
|
35
|
-
def
|
36
|
-
|
34
|
+
def all
|
35
|
+
@all ||= fetch_devices
|
37
36
|
end
|
38
37
|
|
39
38
|
# @raise [SystemExit]
|
40
39
|
# @return [Array]
|
41
40
|
def available
|
42
|
-
@available ||=
|
41
|
+
@available ||= all.select(&:available).tap do |d|
|
43
42
|
MultiLogger.debug(available_devices: d)
|
44
43
|
raise 'Touchpad is not found' if d.empty?
|
45
44
|
end
|
46
|
-
rescue RuntimeError =>
|
47
|
-
|
45
|
+
rescue RuntimeError => e
|
46
|
+
# FIXME: should not exit without Runner class
|
47
|
+
MultiLogger.error(e.message)
|
48
48
|
exit 1
|
49
49
|
end
|
50
50
|
|
51
51
|
def reset
|
52
|
+
@all = nil
|
52
53
|
@available = nil
|
53
54
|
end
|
54
55
|
|
55
|
-
#
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
56
|
+
# Narrow down available device list
|
57
|
+
# @param names [String, Array]
|
58
|
+
def given_devices=(names)
|
59
|
+
# NOTE: convert to Array
|
60
|
+
device_names = Array(names)
|
61
|
+
return if device_names.empty?
|
62
|
+
|
63
|
+
@given_devices = narrow_available_devices(device_names: device_names)
|
64
|
+
return unless @given_devices.empty?
|
65
|
+
|
62
66
|
exit 1
|
63
67
|
end
|
64
68
|
|
65
69
|
private
|
66
70
|
|
67
71
|
# @return [Array]
|
68
|
-
def
|
72
|
+
def fetch_devices
|
69
73
|
line_parser = LineParser.new
|
70
|
-
|
74
|
+
Plugin::Inputs::LibinputCommandInput.new.list_devices do |line|
|
71
75
|
line_parser.push(line)
|
72
76
|
end
|
73
77
|
line_parser.generate_devices
|
74
78
|
end
|
75
79
|
|
80
|
+
def narrow_available_devices(device_names:)
|
81
|
+
device_names.select do |name|
|
82
|
+
if available.map(&:name).include? name
|
83
|
+
MultiLogger.info("Touchpad is found: #{name}")
|
84
|
+
true
|
85
|
+
else
|
86
|
+
MultiLogger.warn("Touchpad is not found: #{name}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
76
91
|
# parse line and generate devices
|
77
92
|
class LineParser
|
78
93
|
attr_reader :lines
|
@@ -88,25 +103,28 @@ module Fusuma
|
|
88
103
|
|
89
104
|
# @return [Array]
|
90
105
|
def generate_devices
|
91
|
-
device =
|
106
|
+
device = Device.new
|
92
107
|
lines.each_with_object([]) do |line, devices|
|
93
|
-
|
94
|
-
|
95
|
-
|
108
|
+
attributes = extract_attribute(line: line)
|
109
|
+
|
110
|
+
# detect new line including device name
|
111
|
+
if attributes[:name] && (device&.name != attributes[:name])
|
96
112
|
devices << device
|
97
|
-
device =
|
113
|
+
device = Device.new
|
98
114
|
end
|
115
|
+
|
116
|
+
devices.last.assign_attributes(attributes)
|
99
117
|
end
|
100
118
|
end
|
101
119
|
|
102
|
-
# @param
|
120
|
+
# @param line [String]
|
103
121
|
# @return [Hash]
|
104
122
|
def extract_attribute(line:)
|
105
123
|
if (id = id_from(line))
|
106
124
|
{ id: id }
|
107
125
|
elsif (name = name_from(line))
|
108
126
|
{ name: name }
|
109
|
-
elsif (available =
|
127
|
+
elsif (available = available_from(line))
|
110
128
|
{ available: available }
|
111
129
|
else
|
112
130
|
{}
|
@@ -125,11 +143,14 @@ module Fusuma
|
|
125
143
|
end
|
126
144
|
end
|
127
145
|
|
128
|
-
def
|
129
|
-
# NOTE: natural scroll
|
130
|
-
|
131
|
-
|
132
|
-
|
146
|
+
def available_from(line)
|
147
|
+
# NOTE: is natural scroll available?
|
148
|
+
if line =~ /^Nat.scrolling: /
|
149
|
+
return false if line =~ %r{n/a}
|
150
|
+
|
151
|
+
return true # disabled / enabled
|
152
|
+
end
|
153
|
+
nil
|
133
154
|
end
|
134
155
|
end
|
135
156
|
end
|
data/lib/fusuma/multi_logger.rb
CHANGED
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './manager.rb'
|
4
|
+
require_relative '../config.rb'
|
5
|
+
|
6
|
+
module Fusuma
|
7
|
+
module Plugin
|
8
|
+
# Create a Plugin Class with extending this class
|
9
|
+
class Base
|
10
|
+
# when inherited from subclass
|
11
|
+
def self.inherited(subclass)
|
12
|
+
subclass_path = caller_locations(1..1).first.path
|
13
|
+
Manager.add(plugin_class: subclass, plugin_path: subclass_path)
|
14
|
+
end
|
15
|
+
|
16
|
+
# get inherited classes
|
17
|
+
# @example
|
18
|
+
# [Vectors::Vector]
|
19
|
+
# @return [Array]
|
20
|
+
def self.plugins
|
21
|
+
Manager.plugins[name]
|
22
|
+
end
|
23
|
+
|
24
|
+
# config parameter name and Type of the value of parameter
|
25
|
+
# @return [Hash]
|
26
|
+
def config_param_types
|
27
|
+
raise NotImplementedError, "override #{self.class.name}##{__method__}"
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Object]
|
31
|
+
def config_params(key = nil, base: config_index)
|
32
|
+
params = Config.search(base) || {}
|
33
|
+
|
34
|
+
return params unless key
|
35
|
+
|
36
|
+
params.fetch(key, nil).tap do |val|
|
37
|
+
next if val.nil?
|
38
|
+
|
39
|
+
# NOTE: Type checking for config.yml
|
40
|
+
param_types = Array(config_param_types.fetch(key))
|
41
|
+
|
42
|
+
next if param_types.any? { |klass| val.is_a?(klass) }
|
43
|
+
|
44
|
+
MultiLogger.error('Please fix config.yml.')
|
45
|
+
MultiLogger.error(":#{base.keys.map(&:symbol)
|
46
|
+
.join(' => :')} => :#{key} should be #{param_types.join(' OR ')}.")
|
47
|
+
exit 1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def config_index
|
52
|
+
Config::Index.new(self.class.name.gsub('Fusuma::', '').underscore.split('/'))
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|