fusuma 1.10.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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