fusuma 1.10.1 → 2.0.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.
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 +38 -1
  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 +166 -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 +6 -4
  20. data/lib/fusuma/hash_support.rb +40 -0
  21. data/lib/fusuma/libinput_command.rb +17 -21
  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 +9 -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 +64 -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 +22 -29
  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 +21 -150
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,166 @@
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 nil if location.nil?
36
+
37
+ return search(index, location: location[0]) if context == {}
38
+
39
+ new_location = location.find do |conf|
40
+ search(index, location: conf) if conf[:context] == context
41
+ end
42
+ search(index, location: new_location)
43
+ end
44
+
45
+ # @param index [Index]
46
+ # @param location [Hash]
47
+ # @return [NilClass]
48
+ # @return [Hash]
49
+ # @return [Object]
50
+ def search_with_cache(index, location:)
51
+ cache([index.cache_key, Searcher.context, Searcher.skip?, Searcher.fallback?]) do
52
+ search_with_context(index, location: location, context: Searcher.context)
53
+ end
54
+ end
55
+
56
+ def cache(key)
57
+ @cache ||= {}
58
+ key = key.join(',') if key.is_a? Array
59
+ if @cache.key?(key)
60
+ @cache[key]
61
+ else
62
+ @cache[key] = block_given? ? yield : nil
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ # next locations' candidates sorted by priority
69
+ # 1. look up location with key
70
+ # 2. fallback to other key
71
+ # 3. skip the key and go to child location
72
+ def next_location_cadidates(location, key)
73
+ [
74
+ location[key.symbol],
75
+ Searcher.skip? && key.skippable && location
76
+ ].compact
77
+ end
78
+
79
+ class << self
80
+ # @return [Hash]
81
+ def conditions(&block)
82
+ {
83
+ nothing: -> { block.call },
84
+ skip: -> { Config::Searcher.skip { block.call } }
85
+ }
86
+ end
87
+
88
+ # Execute block with specified conditions
89
+ # @param conidtion [Symbol]
90
+ # @return [Object]
91
+ def with_condition(condition, &block)
92
+ conditions(&block)[condition].call
93
+ end
94
+
95
+ # Execute block with all conditions
96
+ # @return [Array<Symbol, Object>]
97
+ def find_condition(&block)
98
+ conditions(&block).find do |c, l|
99
+ result = l.call
100
+ return [c, result] if result
101
+
102
+ nil
103
+ end
104
+ end
105
+
106
+ # Search with context from load_streamed Config
107
+ # @param context [Hash]
108
+ # @return [Object]
109
+ def with_context(context, &block)
110
+ @context = context || {}
111
+ result = block.call
112
+ @context = {}
113
+ result
114
+ end
115
+
116
+ # Return a matching context from config
117
+ # @params request_context [Hash]
118
+ # @return [Hash]
119
+ def find_context(request_context, &block)
120
+ # Search in blocks in the following order.
121
+ # 1. complete match config[:context] == request_context
122
+ # 2. partial match config[:context] =~ request_context
123
+ # 3. no context
124
+ Config.instance.keymap.each do |config|
125
+ next unless config[:context] == request_context
126
+ return config[:context] if with_context(config[:context]) { block.call }
127
+ end
128
+ if request_context.keys.size > 1
129
+ Config.instance.keymap.each do |config|
130
+ next if config[:context].nil?
131
+
132
+ next unless config[:context].all? { |k, v| request_context[k] == v }
133
+ return config[:context] if with_context(config[:context]) { block.call }
134
+ end
135
+ end
136
+ return {} if with_context({}) { block.call }
137
+ end
138
+
139
+ attr_reader :context
140
+
141
+ def fallback?
142
+ @fallback
143
+ end
144
+
145
+ def skip?
146
+ @skip
147
+ end
148
+
149
+ # switch context for fallback
150
+ def fallback(&block)
151
+ @fallback = true
152
+ result = block.call
153
+ @fallback = false
154
+ result
155
+ end
156
+
157
+ def skip(&block)
158
+ @skip = true
159
+ result = block.call
160
+ @skip = false
161
+ result
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end