fusuma 0.11.1 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +8 -0
  3. data/.gitignore +6 -0
  4. data/.reek.yml +93 -45
  5. data/.rubocop.yml +2 -0
  6. data/.rubocop_todo.yml +16 -75
  7. data/Gemfile +2 -0
  8. data/README.md +12 -5
  9. data/Rakefile +2 -1
  10. data/bin/console +1 -1
  11. data/exe/fusuma +1 -0
  12. data/fusuma.gemspec +9 -2
  13. data/lib/fusuma.rb +88 -31
  14. data/lib/fusuma/config.rb +65 -66
  15. data/lib/fusuma/config/index.rb +49 -0
  16. data/lib/fusuma/device.rb +58 -37
  17. data/lib/fusuma/multi_logger.rb +3 -0
  18. data/lib/fusuma/plugin/base.rb +56 -0
  19. data/lib/fusuma/plugin/buffers/buffer.rb +41 -0
  20. data/lib/fusuma/plugin/buffers/gesture_buffer.rb +70 -0
  21. data/lib/fusuma/plugin/detectors/detector.rb +41 -0
  22. data/lib/fusuma/plugin/detectors/pinch_detector.rb +141 -0
  23. data/lib/fusuma/plugin/detectors/rotate_detector.rb +135 -0
  24. data/lib/fusuma/plugin/detectors/swipe_detector.rb +145 -0
  25. data/lib/fusuma/plugin/events/event.rb +38 -0
  26. data/lib/fusuma/plugin/events/records/gesture_record.rb +31 -0
  27. data/lib/fusuma/plugin/events/records/index_record.rb +53 -0
  28. data/lib/fusuma/plugin/events/records/record.rb +20 -0
  29. data/lib/fusuma/plugin/events/records/text_record.rb +28 -0
  30. data/lib/fusuma/plugin/executors/command_executor.rb +39 -0
  31. data/lib/fusuma/plugin/executors/executor.rb +27 -0
  32. data/lib/fusuma/plugin/filters/filter.rb +40 -0
  33. data/lib/fusuma/plugin/filters/libinput_device_filter.rb +42 -0
  34. data/lib/fusuma/plugin/inputs/input.rb +28 -0
  35. data/lib/fusuma/plugin/inputs/libinput_command_input.rb +133 -0
  36. data/lib/fusuma/plugin/manager.rb +118 -0
  37. data/lib/fusuma/plugin/parsers/libinput_gesture_parser.rb +54 -0
  38. data/lib/fusuma/plugin/parsers/parser.rb +46 -0
  39. data/lib/fusuma/version.rb +3 -1
  40. metadata +74 -14
  41. data/lib/fusuma/command_executor.rb +0 -43
  42. data/lib/fusuma/event_stack.rb +0 -87
  43. data/lib/fusuma/gesture_event.rb +0 -50
  44. data/lib/fusuma/libinput_commands.rb +0 -98
  45. data/lib/fusuma/pinch.rb +0 -58
  46. data/lib/fusuma/swipe.rb +0 -59
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base.rb'
4
+
5
+ module Fusuma
6
+ module Plugin
7
+ module Buffers
8
+ # buffer events and output
9
+ class Buffer < Base
10
+ def initialize(*args)
11
+ @events = Array.new(*args)
12
+ end
13
+
14
+ attr_reader :events
15
+
16
+ # @return [String]
17
+ def type
18
+ self.class.name.underscore.split('/').last.gsub('_buffer', '')
19
+ end
20
+
21
+ # @param event [Event]
22
+ def buffer(event)
23
+ return if event&.tag != source
24
+
25
+ @events.push(event)
26
+ end
27
+
28
+ # clear buffer
29
+ def clear
30
+ @events.clear
31
+ end
32
+
33
+ # Set source for tag from config.yml.
34
+ # DEFAULT_SOURCE is defined in each plugins.
35
+ def source
36
+ @source ||= config_params(:source) || self.class.const_get('DEFAULT_SOURCE')
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './buffer.rb'
4
+
5
+ module Fusuma
6
+ module Plugin
7
+ module Buffers
8
+ # manage events and generate command
9
+ class GestureBuffer < Buffer
10
+ DEFAULT_SOURCE = 'libinput_gesture_parser'
11
+
12
+ # @param event [Event]
13
+ def buffer(event)
14
+ # TODO: buffering events into buffer plugins
15
+ # - gesture event buffer
16
+ # - window event buffer
17
+ # - other event buffer
18
+ return if event&.tag != source
19
+ return if event.record.type != :gesture
20
+
21
+ @events.push(event)
22
+ clear unless updating?
23
+ end
24
+
25
+ # @param attr [Symbol]
26
+ # @return [Float]
27
+ def sum_attrs(attr)
28
+ @events.map { |gesture_event| gesture_event.record.direction[attr].to_f }
29
+ .inject(:+)
30
+ end
31
+
32
+ # @param attr [Symbol]
33
+ # @return [Float]
34
+ def avg_attrs(attr)
35
+ sum_attrs(attr).to_f / @events.length
36
+ end
37
+
38
+ # return [Integer]
39
+ def finger
40
+ @events.last.record.finger.to_i
41
+ end
42
+
43
+ # @example
44
+ # event_buffer.gesture
45
+ # => 'swipe'
46
+ # @return [String]
47
+ def gesture
48
+ @events.last.record.gesture
49
+ end
50
+
51
+ def empty?
52
+ @events.empty?
53
+ end
54
+
55
+ def select_by_events
56
+ return enum_for(:select) unless block_given?
57
+
58
+ events = @events.select { |event| yield event }
59
+ self.class.new events
60
+ end
61
+
62
+ private
63
+
64
+ def updating?
65
+ return true unless @events.last.record.status =~ /begin|end/
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base.rb'
4
+ require_relative '../events/event.rb'
5
+
6
+ module Fusuma
7
+ module Plugin
8
+ module Detectors
9
+ # Inherite this base
10
+ class Detector < Base
11
+ # @param _buffers [Array<Buffer>]
12
+ # @return [Event] if event is detected
13
+ # @return [NilClass] if event is NOT detected
14
+ def detect(_buffers)
15
+ raise NotImplementedError, "override #{self.class.name}##{__method__}"
16
+
17
+ # create_event(record:)
18
+ end
19
+
20
+ # @param record [Events::Records::Record]
21
+ # @return [Events::Event]
22
+ def create_event(record:)
23
+ @last_time = Time.now
24
+ Events::Event.new(time: Time.now, tag: tag, record: record)
25
+ end
26
+
27
+ def last_time
28
+ @last_time ||= Time.now
29
+ end
30
+
31
+ def tag
32
+ self.class.name.split('Detectors::').last.underscore
33
+ end
34
+
35
+ def type
36
+ self.class.name.underscore.split('/').last.gsub('_detector', '')
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './detector.rb'
4
+
5
+ module Fusuma
6
+ module Plugin
7
+ module Detectors
8
+ class PinchDetector < Detector
9
+ BUFFER_TYPE = 'gesture'
10
+ GESTURE_RECORD_TYPE = 'pinch'
11
+
12
+ FINGERS = [2, 3, 4].freeze
13
+ BASE_THERESHOLD = 0.1
14
+ BASE_INTERVAL = 0.1
15
+
16
+ # @param buffers [Array<Buffer>]
17
+ # @return [Event] if event is detected
18
+ # @return [NilClass] if event is NOT detected
19
+ def detect(buffers)
20
+ buffer = buffers.find { |b| b.type == BUFFER_TYPE }
21
+ .select_by_events { |e| e.record.gesture == GESTURE_RECORD_TYPE }
22
+
23
+ return if buffer.empty?
24
+
25
+ finger = buffer.finger
26
+
27
+ avg_zoom = buffer.avg_attrs(:zoom)
28
+ first_zoom = buffer.events.first.record.direction.zoom
29
+ diameter = avg_zoom / first_zoom
30
+
31
+ direction = Direction.new(diameter: diameter).to_s
32
+ quantity = Quantity.new(diameter: diameter).to_f
33
+
34
+ index = create_index(gesture: type,
35
+ finger: finger,
36
+ direction: direction)
37
+
38
+ return unless enough?(index: index, quantity: quantity)
39
+
40
+ create_event(record: Events::Records::IndexRecord.new(index: index))
41
+ end
42
+
43
+ # @param [String] gesture
44
+ # @param [Integer] finger
45
+ # @param [String] direction
46
+ # @return [Config::Index]
47
+ def create_index(gesture:, finger:, direction:)
48
+ Config::Index.new(
49
+ [
50
+ Config::Index::Key.new(gesture),
51
+ Config::Index::Key.new(finger.to_i, skippable: true),
52
+ Config::Index::Key.new(direction)
53
+ ]
54
+ )
55
+ end
56
+
57
+ private
58
+
59
+ def enough?(index:, quantity:)
60
+ enough_interval?(index: index) && enough_diameter?(index: index, quantity: quantity)
61
+ end
62
+
63
+ def enough_diameter?(index:, quantity:)
64
+ MultiLogger.info(type: type, quantity: quantity,
65
+ quantity_threshold: threshold(index: index))
66
+ quantity >= threshold(index: index)
67
+ end
68
+
69
+ def enough_interval?(index:)
70
+ return true if first_time?
71
+ return true if (Time.now - @last_time) > interval_time(index: index)
72
+
73
+ false
74
+ end
75
+
76
+ def first_time?
77
+ !@last_time
78
+ end
79
+
80
+ def threshold(index:)
81
+ @threshold ||= {}
82
+ @threshold[index.cache_key] ||= begin
83
+ keys_specific = Config::Index.new [*index.keys, 'threshold']
84
+ keys_global = Config::Index.new ['threshold', type]
85
+ config_value = Config.search(keys_specific) ||
86
+ Config.search(keys_global) || 1
87
+ BASE_THERESHOLD * config_value
88
+ end
89
+ end
90
+
91
+ def interval_time(index:)
92
+ @interval_time ||= {}
93
+ @interval_time[index.cache_key] ||= begin
94
+ keys_specific = Config::Index.new [*index.keys, 'interval']
95
+ keys_global = Config::Index.new ['interval', type]
96
+ config_value = Config.search(keys_specific) ||
97
+ Config.search(keys_global) || 1
98
+ BASE_INTERVAL * config_value
99
+ end
100
+ end
101
+
102
+ # direction of gesture
103
+ class Direction
104
+ IN = 'in'
105
+ OUT = 'out'
106
+
107
+ def initialize(diameter:)
108
+ @diameter = diameter
109
+ end
110
+
111
+ def to_s
112
+ calc
113
+ end
114
+
115
+ def calc
116
+ if @diameter > 1
117
+ IN
118
+ else
119
+ OUT
120
+ end
121
+ end
122
+ end
123
+
124
+ # quantity of gesture
125
+ class Quantity
126
+ def initialize(diameter:)
127
+ @diameter = diameter
128
+ end
129
+
130
+ def to_f
131
+ calc.to_f
132
+ end
133
+
134
+ def calc
135
+ (1.0 - @diameter).abs
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './detector.rb'
4
+
5
+ module Fusuma
6
+ module Plugin
7
+ module Detectors
8
+ class RotateDetector < Detector
9
+ BUFFER_TYPE = 'gesture'
10
+ GESTURE_RECORD_TYPE = 'pinch'
11
+
12
+ FINGERS = [2, 3, 4].freeze
13
+ BASE_THERESHOLD = 0.5
14
+ BASE_INTERVAL = 0.1
15
+
16
+ # @param buffers [Array<Buffer>]
17
+ # @return [Event] if event is detected
18
+ # @return [NilClass] if event is NOT detected
19
+ def detect(buffers)
20
+ buffer = buffers.find { |b| b.type == BUFFER_TYPE }
21
+ .select_by_events { |e| e.record.gesture == GESTURE_RECORD_TYPE }
22
+
23
+ return if buffer.empty?
24
+
25
+ angle = buffer.avg_attrs(:rotate)
26
+
27
+ finger = buffer.finger
28
+ direction = Direction.new(angle: angle).to_s
29
+ quantity = Quantity.new(angle: angle).to_f
30
+
31
+ index = create_index(gesture: type,
32
+ finger: finger,
33
+ direction: direction)
34
+
35
+ return unless enough?(index: index, quantity: quantity)
36
+
37
+ create_event(record: Events::Records::IndexRecord.new(index: index))
38
+ end
39
+
40
+ # @param [String] gesture
41
+ # @param [Integer] finger
42
+ # @param [String] direction
43
+ # @return [Config::Index]
44
+ def create_index(gesture:, finger:, direction:)
45
+ Config::Index.new(
46
+ [
47
+ Config::Index::Key.new(gesture),
48
+ Config::Index::Key.new(finger.to_i, skippable: true),
49
+ Config::Index::Key.new(direction)
50
+ ]
51
+ )
52
+ end
53
+
54
+ private
55
+
56
+ def enough?(index:, quantity:)
57
+ enough_interval?(index: index) && enough_angle?(index: index, quantity: quantity)
58
+ end
59
+
60
+ def enough_angle?(index:, quantity:)
61
+ MultiLogger.info(type: type, quantity: quantity,
62
+ quantity_threshold: threshold(index: index))
63
+
64
+ quantity > threshold(index: index)
65
+ end
66
+
67
+ def enough_interval?(index:)
68
+ return true if first_time?
69
+ return true if (Time.now - @last_time) > interval_time(index: index)
70
+
71
+ false
72
+ end
73
+
74
+ def first_time?
75
+ !@last_time
76
+ end
77
+
78
+ def threshold(index:)
79
+ @threshold ||= {}
80
+ @threshold[index.cache_key] ||= begin
81
+ keys_specific = Config::Index.new [*index.keys, 'threshold']
82
+ keys_global = Config::Index.new ['threshold', type]
83
+ config_value = Config.search(keys_specific) ||
84
+ Config.search(keys_global) || 1
85
+ BASE_THERESHOLD * config_value
86
+ end
87
+ end
88
+
89
+ def interval_time(index:)
90
+ @interval_time ||= {}
91
+ @interval_time[index.cache_key] ||= begin
92
+ keys_specific = Config::Index.new [*index.keys, 'interval']
93
+ keys_global = Config::Index.new ['interval', type]
94
+ config_value = Config.search(keys_specific) ||
95
+ Config.search(keys_global) || 1
96
+ BASE_INTERVAL * config_value
97
+ end
98
+ end
99
+
100
+ # direction of gesture
101
+ class Direction
102
+ CLOCKWISE = 'clockwise'
103
+ COUNTERCLOCKWISE = 'counterclockwise'
104
+
105
+ def initialize(angle:)
106
+ @angle = angle
107
+ end
108
+
109
+ def to_s
110
+ calc
111
+ end
112
+
113
+ def calc
114
+ if @angle > 0
115
+ CLOCKWISE
116
+ else
117
+ COUNTERCLOCKWISE
118
+ end
119
+ end
120
+ end
121
+
122
+ # quantity of gesture
123
+ class Quantity
124
+ def initialize(angle:)
125
+ @angle = angle.abs
126
+ end
127
+
128
+ def to_f
129
+ @angle.to_f
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './detector.rb'
4
+
5
+ module Fusuma
6
+ module Plugin
7
+ module Detectors
8
+ class SwipeDetector < Detector
9
+ BUFFER_TYPE = 'gesture'
10
+ GESTURE_RECORD_TYPE = 'swipe'
11
+
12
+ FINGERS = [3, 4].freeze
13
+ BASE_THERESHOLD = 10
14
+ BASE_INTERVAL = 0.5
15
+
16
+ # @param buffers [Array<Buffers::Buffer>]
17
+ # @return [Event] if event is detected
18
+ # @return [NilClass] if event is NOT detected
19
+ def detect(buffers)
20
+ buffer = buffers.find { |b| b.type == BUFFER_TYPE }
21
+ .select_by_events { |e| e.record.gesture == GESTURE_RECORD_TYPE }
22
+
23
+ return if buffer.empty?
24
+
25
+ move_x = buffer.avg_attrs(:move_x)
26
+ move_y = buffer.avg_attrs(:move_y)
27
+
28
+ finger = buffer.finger
29
+ direction = Direction.new(move_x: move_x.to_f, move_y: move_y.to_f).to_s
30
+ quantity = Quantity.new(move_x: move_x.to_f, move_y: move_y.to_f).to_f
31
+
32
+ index = create_index(gesture: type,
33
+ finger: finger,
34
+ direction: direction)
35
+
36
+ return unless enough?(index: index, quantity: quantity)
37
+
38
+ create_event(record: Events::Records::IndexRecord.new(index: index))
39
+ end
40
+
41
+ # @param [String] gesture
42
+ # @param [Integer] finger
43
+ # @param [String] direction
44
+ # @return [Config::Index]
45
+ def create_index(gesture:, finger:, direction:)
46
+ Config::Index.new(
47
+ [
48
+ Config::Index::Key.new(gesture),
49
+ Config::Index::Key.new(finger.to_i, skippable: true),
50
+ Config::Index::Key.new(direction)
51
+ ]
52
+ )
53
+ end
54
+
55
+ private
56
+
57
+ def enough?(index:, quantity:)
58
+ enough_interval?(index: index) && enough_distance?(index: index, quantity: quantity)
59
+ end
60
+
61
+ def enough_distance?(index:, quantity:)
62
+ MultiLogger.info(type: type, quantity: quantity,
63
+ quantity_threshold: threshold(index: index))
64
+ quantity > threshold(index: index)
65
+ end
66
+
67
+ def enough_interval?(index:)
68
+ return true if first_time?
69
+ return true if (Time.now - @last_time) > interval_time(index: index)
70
+
71
+ false
72
+ end
73
+
74
+ def first_time?
75
+ !@last_time
76
+ end
77
+
78
+ def threshold(index:)
79
+ @threshold ||= {}
80
+ @threshold[index.cache_key] ||= begin
81
+ keys_specific = Config::Index.new [*index.keys, 'threshold']
82
+ keys_global = Config::Index.new ['threshold', type]
83
+ config_value = Config.search(keys_specific) ||
84
+ Config.search(keys_global) || 1
85
+ BASE_THERESHOLD * config_value
86
+ end
87
+ end
88
+
89
+ def interval_time(index:)
90
+ @interval_time ||= {}
91
+ @interval_time[index.cache_key] ||= begin
92
+ keys_specific = Config::Index.new [*index.keys, 'interval']
93
+ keys_global = Config::Index.new ['interval', type]
94
+ config_value = Config.search(keys_specific) ||
95
+ Config.search(keys_global) || 1
96
+ BASE_INTERVAL * config_value
97
+ end
98
+ end
99
+
100
+ # direction of gesture
101
+ class Direction
102
+ RIGHT = 'right'
103
+ LEFT = 'left'
104
+ DOWN = 'down'
105
+ UP = 'up'
106
+
107
+ def initialize(move_x:, move_y:)
108
+ @move_x = move_x
109
+ @move_y = move_y
110
+ end
111
+
112
+ def to_s
113
+ calc
114
+ end
115
+
116
+ def calc
117
+ if @move_x.abs > @move_y.abs
118
+ @move_x > 0 ? RIGHT : LEFT
119
+ elsif @move_y > 0
120
+ DOWN
121
+ else
122
+ UP
123
+ end
124
+ end
125
+ end
126
+
127
+ # quantity of gesture
128
+ class Quantity
129
+ def initialize(move_x:, move_y:)
130
+ @x = move_x.abs
131
+ @y = move_y.abs
132
+ end
133
+
134
+ def to_f
135
+ calc.to_f
136
+ end
137
+
138
+ def calc
139
+ @x > @y ? @x.abs : @y.abs
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end