fusuma 1.10.0 → 2.0.0.pre2

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.github/pull_request_template.md +9 -0
  3. data/.rubocop.yml +27 -0
  4. data/.rubocop_todo.yml +34 -19
  5. data/.solargraph.yml +16 -0
  6. data/.travis.yml +1 -3
  7. data/CHANGELOG.md +56 -4
  8. data/CONTRIBUTING.md +72 -0
  9. data/Gemfile +17 -0
  10. data/README.md +53 -7
  11. data/fusuma.gemspec +4 -13
  12. data/lib/fusuma.rb +91 -29
  13. data/lib/fusuma/config.rb +59 -60
  14. data/lib/fusuma/config/index.rb +39 -6
  15. data/lib/fusuma/config/searcher.rb +164 -0
  16. data/lib/fusuma/config/yaml_duplication_checker.rb +42 -0
  17. data/lib/fusuma/custom_process.rb +13 -0
  18. data/lib/fusuma/device.rb +22 -7
  19. data/lib/fusuma/environment.rb +5 -4
  20. data/lib/fusuma/hash_support.rb +40 -0
  21. data/lib/fusuma/libinput_command.rb +16 -20
  22. data/lib/fusuma/multi_logger.rb +2 -6
  23. data/lib/fusuma/plugin/base.rb +18 -15
  24. data/lib/fusuma/plugin/buffers/buffer.rb +3 -2
  25. data/lib/fusuma/plugin/buffers/gesture_buffer.rb +34 -25
  26. data/lib/fusuma/plugin/buffers/timer_buffer.rb +46 -0
  27. data/lib/fusuma/plugin/detectors/detector.rb +26 -5
  28. data/lib/fusuma/plugin/detectors/pinch_detector.rb +109 -58
  29. data/lib/fusuma/plugin/detectors/rotate_detector.rb +91 -50
  30. data/lib/fusuma/plugin/detectors/swipe_detector.rb +93 -56
  31. data/lib/fusuma/plugin/events/event.rb +5 -4
  32. data/lib/fusuma/plugin/events/records/context_record.rb +27 -0
  33. data/lib/fusuma/plugin/events/records/gesture_record.rb +12 -6
  34. data/lib/fusuma/plugin/events/records/index_record.rb +46 -14
  35. data/lib/fusuma/plugin/events/records/record.rb +1 -1
  36. data/lib/fusuma/plugin/events/records/text_record.rb +2 -1
  37. data/lib/fusuma/plugin/executors/command_executor.rb +21 -6
  38. data/lib/fusuma/plugin/executors/executor.rb +45 -3
  39. data/lib/fusuma/plugin/filters/filter.rb +1 -1
  40. data/lib/fusuma/plugin/filters/libinput_device_filter.rb +6 -7
  41. data/lib/fusuma/plugin/filters/libinput_timeout_filter.rb +2 -2
  42. data/lib/fusuma/plugin/inputs/input.rb +63 -8
  43. data/lib/fusuma/plugin/inputs/libinput_command_input.rb +19 -9
  44. data/lib/fusuma/plugin/inputs/timer_input.rb +63 -0
  45. data/lib/fusuma/plugin/manager.rb +10 -28
  46. data/lib/fusuma/plugin/parsers/libinput_gesture_parser.rb +10 -8
  47. data/lib/fusuma/plugin/parsers/parser.rb +8 -9
  48. data/lib/fusuma/string_support.rb +16 -0
  49. data/lib/fusuma/version.rb +1 -1
  50. metadata +20 -149
data/fusuma.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.authors = ['iberianpig']
11
11
  spec.email = ['yhkyky@gmail.com']
12
12
 
13
- spec.summary = 'Multitouch gestures with libinput dirver on X11, Linux'
13
+ spec.summary = 'Multitouch gestures with libinput driver on X11, Linux'
14
14
  spec.description = 'Fusuma is multitouch gesture recognizer. This gem makes your touchpad on Linux able to recognize swipes or pinchs and assign command to them. Read installation on Github(https://github.com/iberianpig/fusuma#installation).'
15
15
  spec.homepage = 'https://github.com/iberianpig/fusuma'
16
16
  spec.license = 'MIT'
@@ -23,16 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.require_paths = ['lib']
24
24
  spec.metadata['yard.run'] = 'yri' # use "yard" to build full HTML docs.
25
25
 
26
- spec.required_ruby_version = '>= 2.3' # https://packages.ubuntu.com/search?keywords=ruby&searchon=names&exact=1&suite=all&section=main
27
- spec.add_development_dependency 'bundler'
28
- spec.add_development_dependency "coveralls"
29
- spec.add_development_dependency 'github_changelog_generator', '~> 1.14'
30
- spec.add_development_dependency 'pry-byebug', '~> 3.4'
31
- spec.add_development_dependency 'pry-doc'
32
- spec.add_development_dependency 'pry-inline'
33
- spec.add_development_dependency 'rake', '~> 13.0'
34
- spec.add_development_dependency 'reek'
35
- spec.add_development_dependency 'rspec', '~> 3.0'
36
- spec.add_development_dependency 'rubocop'
37
- spec.add_development_dependency 'yard'
26
+ spec.required_ruby_version = '>= 2.5.1' # https://packages.ubuntu.com/search?keywords=ruby&searchon=names&exact=1&suite=all&section=main
27
+ # support bionic (18.04LTS) 2.5.1
28
+ spec.add_dependency 'posix-spawn'
38
29
  end
data/lib/fusuma.rb CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  require_relative './fusuma/version'
4
4
  require_relative './fusuma/multi_logger'
5
- require_relative './fusuma/config.rb'
6
- require_relative './fusuma/environment.rb'
7
- require_relative './fusuma/plugin/manager.rb'
5
+ require_relative './fusuma/config'
6
+ require_relative './fusuma/environment'
7
+ require_relative './fusuma/device'
8
+ require_relative './fusuma/plugin/manager'
8
9
 
9
10
  # this is top level module
10
11
  module Fusuma
@@ -15,6 +16,8 @@ module Fusuma
15
16
  set_trap
16
17
  read_options(option)
17
18
  instance = new
19
+ ## NOTE: Uncomment following line to measure performance
20
+ # instance.run_with_lineprof
18
21
  instance.run
19
22
  end
20
23
 
@@ -47,8 +50,6 @@ module Fusuma
47
50
  end
48
51
 
49
52
  def load_custom_config(config_path = nil)
50
- return unless config_path
51
-
52
53
  Config.custom_path = config_path
53
54
  end
54
55
  end
@@ -63,59 +64,120 @@ module Fusuma
63
64
  end
64
65
 
65
66
  def run
66
- # TODO: run with multi thread
67
- @inputs.first.run do |event|
68
- clear_expired_events
69
- filtered = filter(event) || next
70
- parsed = parse(filtered) || next
71
- buffered = buffer(parsed) || next
72
- detected = detect(buffered) || next
73
- merged = merge(detected) || next
74
- execute(merged)
67
+ loop { pipeline }
68
+ end
69
+
70
+ def pipeline
71
+ event = input || return
72
+ clear_expired_events
73
+ filtered = filter(event) || return
74
+ parsed = parse(filtered) || return
75
+ buffered = buffer(parsed) || return
76
+ detected = detect(buffered) || return
77
+ condition, context, event = merge(detected) || return
78
+ execute(condition, context, event)
79
+ end
80
+
81
+ # For performance monitoring
82
+ def run_with_lineprof(count: 1000)
83
+ require 'rblineprof'
84
+ require 'rblineprof-report'
85
+
86
+ profile = lineprof(%r{#{Pathname.new(__FILE__).parent}/.}) do
87
+ count.times { pipeline }
75
88
  end
89
+ LineProf.report(profile)
90
+ exit 0
91
+ end
92
+
93
+ # @return [Plugin::Events::Event]
94
+ def input
95
+ Plugin::Inputs::Input.select(@inputs)
76
96
  end
77
97
 
98
+ # @param [Plugin::Events::Event]
99
+ # @return [Plugin::Events::Event]
78
100
  def filter(event)
79
101
  event if @filters.any? { |f| f.filter(event) }
80
102
  end
81
103
 
104
+ # @param [Plugin::Events::Event]
105
+ # @return [Plugin::Events::Event]
82
106
  def parse(event)
83
107
  @parsers.reduce(event) { |e, p| p.parse(e) if e }
84
108
  end
85
109
 
110
+ # @param [Plugin::Events::Event]
111
+ # @return [Array<Plugin::Buffers::Buffer>]
112
+ # @return [NilClass]
86
113
  def buffer(event)
87
- @buffers.any? { |b| b.buffer(event) } && @buffers
114
+ @buffers.select { |b| b.buffer(event) }
88
115
  end
89
116
 
90
117
  # @param buffers [Array<Buffer>]
91
118
  # @return [Array<Event>]
92
119
  def detect(buffers)
93
- @detectors.each_with_object([]) do |detector, detected|
94
- if (event = detector.detect(buffers))
95
- detected << event
96
- end
120
+ matched_detectors = @detectors.select do |detector|
121
+ detector.watch? ||
122
+ buffers.any? { |b| detector.sources.include?(b.type) }
123
+ end
124
+
125
+ events = matched_detectors.each_with_object([]) do |detector, detected|
126
+ Array(detector.detect(@buffers)).each { |e| detected << e }
97
127
  end
128
+
129
+ return if events.empty?
130
+
131
+ events
98
132
  end
99
133
 
100
- # @param events [Array<Event>]
101
- # @return [Event] a Event merged all records from arguments
134
+ # @param events [Array<Plugin::Events::Event>]
135
+ # @return [Plugin::Events::Event] Event merged all records from arguments
102
136
  # @return [NilClass] when event is NOT given
103
137
  def merge(events)
104
- main_events, modifiers = events.partition { |event| event.record.mergable? }
105
- return nil unless (main_event = main_events.first)
138
+ index_events, context_events = events.partition { |event| event.record.type == :index }
139
+ main_events, modifiers = index_events.partition { |event| event.record.mergable? }
140
+ request_context = context_events.each_with_object({}) do |e, results|
141
+ results[e.record.name] = e.record.value
142
+ end
143
+ main_events.sort_by! { |e| e.record.trigger_priority }
144
+
145
+ condition = nil
146
+ matched_context = nil
147
+ event = main_events.find do |main_event|
148
+ matched_context = Config::Searcher.find_context(request_context) do
149
+ condition, index_record = Config::Searcher.find_condition do
150
+ main_event.record.merge(records: modifiers.map(&:record))
151
+ end
152
+ main_event if index_record
153
+ end
154
+ end
155
+ return if event.nil?
106
156
 
107
- main_event.record.merge(records: modifiers.map(&:record))
108
- main_event
157
+ [condition, matched_context, event]
109
158
  end
110
159
 
111
- def execute(event)
160
+ # @param event [Plugin::Events::Event]
161
+ def execute(condition, context, event)
112
162
  return unless event
113
163
 
114
- executor = @executors.find do |e|
115
- e.executable?(event)
164
+ # Find executable condition and executor
165
+ executor = Config::Searcher.with_context(context) do
166
+ Config::Searcher.with_condition(condition) do
167
+ @executors.find { |e| e.executable?(event) }
168
+ end
116
169
  end
117
170
 
118
- executor&.execute(event)
171
+ return if executor.nil?
172
+
173
+ # Check interval and execute
174
+ Config::Searcher.with_context(context) do
175
+ Config::Searcher.with_condition(condition) do
176
+ executor.enough_interval?(event) &&
177
+ executor.update_interval(event) &&
178
+ executor.execute(event)
179
+ end
180
+ end
119
181
  end
120
182
 
121
183
  def clear_expired_events
data/lib/fusuma/config.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './multi_logger.rb'
4
- require_relative './config/index.rb'
3
+ require_relative './multi_logger'
4
+ require_relative './config/index'
5
+ require_relative './config/searcher'
6
+ require_relative './config/yaml_duplication_checker'
7
+ require_relative './hash_support'
5
8
  require 'singleton'
6
9
  require 'yaml'
7
10
 
@@ -9,11 +12,19 @@ require 'yaml'
9
12
  module Fusuma
10
13
  # read keymap from yaml file
11
14
  class Config
15
+ class NotFoundError < StandardError; end
16
+
17
+ class InvalidFileError < StandardError; end
18
+
12
19
  include Singleton
13
20
 
14
21
  class << self
15
- def search(keys)
16
- instance.search(keys)
22
+ def search(index)
23
+ instance.search(index)
24
+ end
25
+
26
+ def find_execute_key(index)
27
+ instance.find_execute_key(index)
17
28
  end
18
29
 
19
30
  def custom_path=(new_path)
@@ -21,14 +32,12 @@ module Fusuma
21
32
  end
22
33
  end
23
34
 
24
- attr_reader :keymap
25
- attr_reader :custom_path
35
+ attr_reader :keymap, :custom_path, :searcher
26
36
 
27
37
  def initialize
38
+ @searcher = Searcher.new
28
39
  @custom_path = nil
29
- @cache = nil
30
40
  @keymap = nil
31
- reload
32
41
  end
33
42
 
34
43
  def custom_path=(new_path)
@@ -37,40 +46,61 @@ module Fusuma
37
46
  end
38
47
 
39
48
  def reload
40
- @cache = nil
41
- @keymap = YAML.load_file(file_path).deep_symbolize_keys
42
- MultiLogger.info "reload config : #{file_path}"
49
+ @searcher = Searcher.new
50
+ path = find_filepath
51
+ MultiLogger.info "reload config: #{path}"
52
+ @keymap = validate(path)
43
53
  self
44
54
  end
45
55
 
56
+ # @return [Hash] If check passes
57
+ # @raise [InvalidFileError] If check does not pass
58
+ def validate(path)
59
+ duplicates = []
60
+ YAMLDuplicationChecker.check(File.read(path), path) do |ignored, duplicate|
61
+ MultiLogger.error "#{path}: #{ignored.value} is duplicated"
62
+ duplicates << duplicate.value
63
+ end
64
+ raise InvalidFileError, "Detect duplicate keys #{duplicates}" unless duplicates.empty?
65
+
66
+ yamls = YAML.load_stream(File.read(path)).compact
67
+ yamls.map(&:deep_symbolize_keys)
68
+ rescue StandardError => e
69
+ MultiLogger.error e.message
70
+ raise InvalidFileError, e.message
71
+ end
72
+
46
73
  # @param index [Index]
74
+ # @param context [Hash]
47
75
  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
76
+ @searcher.search_with_cache(index, location: keymap)
77
+ end
78
+
79
+ # @param index [Config::Index]
80
+ # @return Symbol
81
+ def find_execute_key(index)
82
+ @execute_keys ||= Plugin::Executors::Executor.plugins.map do |executor|
83
+ executor.new.execute_keys
84
+ end.flatten
85
+
86
+ execute_params = search(index)
87
+ return if execute_params.nil?
88
+
89
+ @execute_keys.find { |k| execute_params.keys.include?(k) }
63
90
  end
64
91
 
65
92
  private
66
93
 
67
- def file_path
94
+ def find_filepath
68
95
  filename = 'fusuma/config.yml'
69
- if custom_path && File.exist?(expand_custom_path)
70
- expand_custom_path
96
+ if custom_path
97
+ return expand_custom_path if File.exist?(expand_custom_path)
98
+
99
+ raise NotFoundError, "#{expand_custom_path} is NOT FOUND"
71
100
  elsif File.exist?(expand_config_path(filename))
72
101
  expand_config_path(filename)
73
102
  else
103
+ MultiLogger.warn "config file: #{expand_config_path(filename)} is NOT FOUND"
74
104
  expand_default_path(filename)
75
105
  end
76
106
  end
@@ -86,36 +116,5 @@ module Fusuma
86
116
  def expand_default_path(filename)
87
117
  File.expand_path "../../#{filename}", __FILE__
88
118
  end
89
-
90
- def cache(key)
91
- @cache ||= {}
92
- key = key.join(',') if key.is_a? Array
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
118
- end
119
- result
120
119
  end
121
120
  end
@@ -19,6 +19,11 @@ module Fusuma
19
19
  [Key.new(keys)]
20
20
  end
21
21
  end
22
+
23
+ def inspect
24
+ @keys.map(&:inspect)
25
+ end
26
+
22
27
  attr_reader :keys
23
28
 
24
29
  def cache_key
@@ -32,17 +37,45 @@ module Fusuma
32
37
  end
33
38
  end
34
39
 
40
+ # @return [Index]
41
+ def with_context
42
+ keys = @keys.map do |key|
43
+ next if Searcher.skip? && key.skippable
44
+
45
+ if Searcher.fallback? && key.fallback
46
+ key.fallback
47
+ else
48
+ key
49
+ end
50
+ end
51
+ self.class.new(keys.compact)
52
+ end
53
+
35
54
  # Keys in Index
36
55
  class Key
37
- def initialize(symbol_word, skippable: false)
56
+ def initialize(symbol_word, skippable: false, fallback: nil)
38
57
  @symbol = begin
39
- symbol_word.to_sym
40
- rescue StandardError
41
- symbol_word
42
- end
58
+ symbol_word.to_sym
59
+ rescue StandardError
60
+ symbol_word
61
+ end
62
+
43
63
  @skippable = skippable
64
+
65
+ @fallback = begin
66
+ fallback.to_sym
67
+ rescue StandardError
68
+ fallback
69
+ end
44
70
  end
45
- attr_reader :symbol, :skippable
71
+
72
+ def inspect
73
+ skip_marker = @skippable && Searcher.skip? ? '(skip)' : ''
74
+ fallback_marker = @fallback && Searcher.fallback? ? '(fallback)' : ''
75
+ "#{@symbol}#{skip_marker}#{fallback_marker}"
76
+ end
77
+
78
+ attr_reader :symbol, :skippable, :fallback
46
79
  end
47
80
  end
48
81
  end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Index for searching value from config.yml
4
+ module Fusuma
5
+ class Config
6
+ # Search config.yml
7
+ class Searcher
8
+ def initialize
9
+ @cache = nil
10
+ end
11
+
12
+ # @param index [Index]
13
+ # @param location [Hash]
14
+ # @return [NilClass]
15
+ # @return [Hash]
16
+ # @return [Object]
17
+ def search(index, location:)
18
+ key = index.keys.first
19
+ return location if key.nil?
20
+
21
+ return nil if location.nil?
22
+
23
+ return nil unless location.is_a?(Hash)
24
+
25
+ next_index = Index.new(index.keys[1..-1])
26
+
27
+ value = nil
28
+ next_location_cadidates(location, key).find do |next_location|
29
+ value = search(next_index, location: next_location)
30
+ end
31
+ value
32
+ end
33
+
34
+ def search_with_context(index, location:, context:)
35
+ return search(index, location: location[0]) if context == {}
36
+
37
+ new_location = location.find do |conf|
38
+ search(index, location: conf) if conf[:context] == context
39
+ end
40
+ search(index, location: new_location)
41
+ end
42
+
43
+ # @param index [Index]
44
+ # @param location [Hash]
45
+ # @return [NilClass]
46
+ # @return [Hash]
47
+ # @return [Object]
48
+ def search_with_cache(index, location:)
49
+ cache([index.cache_key, Searcher.context, Searcher.skip?, Searcher.fallback?]) do
50
+ search_with_context(index, location: location, context: Searcher.context)
51
+ end
52
+ end
53
+
54
+ def cache(key)
55
+ @cache ||= {}
56
+ key = key.join(',') if key.is_a? Array
57
+ if @cache.key?(key)
58
+ @cache[key]
59
+ else
60
+ @cache[key] = block_given? ? yield : nil
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ # next locations' candidates sorted by priority
67
+ # 1. look up location with key
68
+ # 2. fallback to other key
69
+ # 3. skip the key and go to child location
70
+ def next_location_cadidates(location, key)
71
+ [
72
+ location[key.symbol],
73
+ Searcher.skip? && key.skippable && location
74
+ ].compact
75
+ end
76
+
77
+ class << self
78
+ # @return [Hash]
79
+ def conditions(&block)
80
+ {
81
+ nothing: -> { block.call },
82
+ skip: -> { Config::Searcher.skip { block.call } }
83
+ }
84
+ end
85
+
86
+ # Execute block with specified conditions
87
+ # @param conidtion [Symbol]
88
+ # @return [Object]
89
+ def with_condition(condition, &block)
90
+ conditions(&block)[condition].call
91
+ end
92
+
93
+ # Execute block with all conditions
94
+ # @return [Array<Symbol, Object>]
95
+ def find_condition(&block)
96
+ conditions(&block).find do |c, l|
97
+ result = l.call
98
+ return [c, result] if result
99
+
100
+ nil
101
+ end
102
+ end
103
+
104
+ # Search with context from load_streamed Config
105
+ # @param context [Hash]
106
+ # @return [Object]
107
+ def with_context(context, &block)
108
+ @context = context || {}
109
+ result = block.call
110
+ @context = {}
111
+ result
112
+ end
113
+
114
+ # Return a matching context from config
115
+ # @params request_context [Hash]
116
+ # @return [Hash]
117
+ def find_context(request_context, &block)
118
+ # Search in blocks in the following order.
119
+ # 1. complete match config[:context] == request_context
120
+ # 2. partial match config[:context] =~ request_context
121
+ # 3. no context
122
+ Config.instance.keymap.each do |config|
123
+ next unless config[:context] == request_context
124
+ return config[:context] if with_context(config[:context]) { block.call }
125
+ end
126
+ if request_context.keys.size > 1
127
+ Config.instance.keymap.each do |config|
128
+ next if config[:context].nil?
129
+
130
+ next unless config[:context].all? { |k, v| request_context[k] == v }
131
+ return config[:context] if with_context(config[:context]) { block.call }
132
+ end
133
+ end
134
+ return {} if with_context({}) { block.call }
135
+ end
136
+
137
+ attr_reader :context
138
+
139
+ def fallback?
140
+ @fallback
141
+ end
142
+
143
+ def skip?
144
+ @skip
145
+ end
146
+
147
+ # switch context for fallback
148
+ def fallback(&block)
149
+ @fallback = true
150
+ result = block.call
151
+ @fallback = false
152
+ result
153
+ end
154
+
155
+ def skip(&block)
156
+ @skip = true
157
+ result = block.call
158
+ @skip = false
159
+ result
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end