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,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base.rb'
4
+ require_relative './records/record.rb'
5
+ require_relative './records/text_record.rb'
6
+
7
+ module Fusuma
8
+ module Plugin
9
+ module Events
10
+ # Event format
11
+ class Event < Base
12
+ attr_reader :time
13
+ attr_accessor :tag, :record
14
+
15
+ # @param time [Time]
16
+ # @param tag [Tag]
17
+ # @param record [String, Record]
18
+ def initialize(time: Time.now, tag:, record:)
19
+ @time = time
20
+ @tag = tag
21
+ @record = case record
22
+ when Records::Record
23
+ record
24
+ when String
25
+ Records::TextRecord.new(record)
26
+ else
27
+ raise ArgumentError,
28
+ '@record should be String or Record'
29
+ end
30
+ end
31
+
32
+ def inspect
33
+ "time: #{time}, tag: #{tag}, record: #{record}"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './text_record.rb'
4
+
5
+ module Fusuma
6
+ module Plugin
7
+ module Events
8
+ module Records
9
+ # Gesture Record
10
+ class GestureRecord < Record
11
+ # define gesture format
12
+ attr_reader :status, :gesture, :finger, :direction
13
+
14
+ Delta = Struct.new(:move_x, :move_y, :zoom, :rotate)
15
+
16
+ # @param status [String]
17
+ def initialize(status:, gesture:, finger:, direction:)
18
+ @status = status
19
+ @gesture = gesture
20
+ @finger = finger
21
+ @direction = direction
22
+ end
23
+
24
+ def type
25
+ :gesture
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fusuma
4
+ module Plugin
5
+ module Events
6
+ module Records
7
+ # Vector Record
8
+ # have index
9
+ class IndexRecord < Record
10
+ # define gesture format
11
+ attr_reader :index
12
+
13
+ # @param [Config::Index] index
14
+ # @param [Symbol] position [:prefix, :body, :surfix]
15
+ def initialize(index:, position: :body)
16
+ @index = index
17
+ @position = position
18
+ end
19
+
20
+ def type
21
+ :index
22
+ end
23
+
24
+ # @param records [Array<IndexRecord>]
25
+ # @return [IndexRecord]
26
+ def merge(records:)
27
+ raise "position is NOT body: #{self}" unless mergable?
28
+
29
+ @index = records.each_with_object(@index) do |record, merged_index|
30
+ case record.position
31
+ when :prefix
32
+ Index.new([*record.index.keys, *merged_index.keys])
33
+ when :surfix
34
+ Index.new([*merged_index.keys, *record.index.keys])
35
+ else
36
+ raise "invalid index position: #{record}"
37
+ end
38
+ end
39
+ self
40
+ end
41
+
42
+ def mergable?
43
+ @position == :body
44
+ end
45
+
46
+ protected
47
+
48
+ attr_reader :position
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../base.rb'
4
+
5
+ module Fusuma
6
+ module Plugin
7
+ module Events
8
+ module Records
9
+ # Record
10
+ # @abstract Subclass and override {#type} to implement
11
+ class Record < Base
12
+ # @return [Symbol]
13
+ def type
14
+ raise NotImplementedError, 'override #type'
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './record.rb'
4
+
5
+ module Fusuma
6
+ module Plugin
7
+ module Events
8
+ module Records
9
+ # Default Record
10
+ class TextRecord < Record
11
+ # @param text [String]
12
+ def initialize(text)
13
+ @text = text
14
+ end
15
+
16
+ def type
17
+ :text
18
+ end
19
+
20
+ # @return [String]
21
+ def to_s
22
+ @text
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './executor.rb'
4
+
5
+ module Fusuma
6
+ module Plugin
7
+ module Executors
8
+ # Exector plugin
9
+ class CommandExecutor < Executor
10
+ def execute(event)
11
+ search_command(event).tap do |command|
12
+ break unless command
13
+
14
+ MultiLogger.info(command: command)
15
+ pid = fork do
16
+ Process.daemon(true)
17
+ exec(command.to_s)
18
+ end
19
+
20
+ Process.detach(pid)
21
+ end
22
+ end
23
+
24
+ def executable?(event)
25
+ event.tag.end_with?('_detector') &&
26
+ event.record.type == :index &&
27
+ search_command(event)
28
+ end
29
+
30
+ # @param event [Event]
31
+ # @return [String]
32
+ def search_command(event)
33
+ command_index = Config::Index.new([*event.record.index.keys, :command])
34
+ Config.search(command_index)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base.rb'
4
+
5
+ module Fusuma
6
+ module Plugin
7
+ # executor class
8
+ module Executors
9
+ # Inherite this base
10
+ class Executor < Base
11
+ # check executable
12
+ # @param _event [Event]
13
+ # @return [TrueClass, FalseClass]
14
+ def executable?(_event)
15
+ raise NotImplementedError, "override #{self.class.name}##{__method__}"
16
+ end
17
+
18
+ # execute somthing
19
+ # @param _event [Event]
20
+ # @return [nil]
21
+ def execute(_event)
22
+ raise NotImplementedError, "override #{self.class.name}##{__method__}"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base.rb'
4
+
5
+ module Fusuma
6
+ module Plugin
7
+ # filter class
8
+ module Filters
9
+ # Inherite this base
10
+ class Filter < Base
11
+ # Filter input event
12
+ # @param event [Event]
13
+ # @return [Event, nil]
14
+ def filter(event)
15
+ event.tap do |e|
16
+ next if e.tag != source
17
+ next if keep?(e.record)
18
+
19
+ MultiLogger.debug(filtered: e)
20
+
21
+ break nil
22
+ end
23
+ end
24
+
25
+ # @abstract override `#keep?` to implement
26
+ # @param record [String]
27
+ # @return [True, False]
28
+ def keep?(record)
29
+ true if record
30
+ end
31
+
32
+ # Set source for tag from config.yml.
33
+ # DEFAULT_SOURCE is defined in each Filter plugins.
34
+ def source
35
+ @source ||= config_params(:source) || self.class.const_get('DEFAULT_SOURCE')
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './filter.rb'
4
+ require_relative '../../device.rb'
5
+
6
+ module Fusuma
7
+ module Plugin
8
+ module Filters
9
+ # Filter device log
10
+ class LibinputDeviceFilter < Filter
11
+ DEFAULT_SOURCE = 'libinput_command_input'
12
+
13
+ def config_param_types
14
+ {
15
+ source: String,
16
+ keep_device_names: [Array, String]
17
+ }
18
+ end
19
+
20
+ def keep?(record)
21
+ keep_device_ids.any? { |device_id| record.to_s =~ /^\s?#{device_id}/ }
22
+ end
23
+
24
+ private
25
+
26
+ # @return [Array]
27
+ def keep_device_ids
28
+ @keep_device_ids ||= Device.all.select do |device|
29
+ keep_device_names.any? { |name| device.name.match? name }
30
+ end.map(&:id)
31
+ end
32
+
33
+ # @return [Array]
34
+ def keep_device_names
35
+ Array(config_params(:keep_device_names)).tap do |names|
36
+ break Device.all.map(&:name) if names.empty?
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,28 @@
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
+ # input class
9
+ module Inputs
10
+ # Inherite this base
11
+ class Input < Base
12
+ def run
13
+ raise NotImplementedError, "override #{self.class.name}##{__method__}"
14
+ end
15
+
16
+ def event(record: 'dummy input')
17
+ Events::Event.new(tag: tag, record: record).tap do |e|
18
+ MultiLogger.debug(input_event: e)
19
+ end
20
+ end
21
+
22
+ def tag
23
+ self.class.name.split('Inputs::').last.underscore
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './input.rb'
4
+ require 'open3'
5
+
6
+ module Fusuma
7
+ module Plugin
8
+ module Inputs
9
+ # libinput commands wrapper
10
+ class LibinputCommandInput < Input
11
+ def config_param_types
12
+ {
13
+ 'enable-tap': [TrueClass, FalseClass],
14
+ 'enable-dwt': [TrueClass, FalseClass],
15
+ 'device': [String]
16
+ }
17
+ end
18
+
19
+ def run
20
+ debug_events do |line|
21
+ yield event(record: line)
22
+ end
23
+ end
24
+
25
+ # `libinput-list-devices` and `libinput-debug-events` are deprecated,
26
+ # use `libinput list-devices` and `libinput debug-events` from 1.8.
27
+ NEW_CLI_OPTION_VERSION = 1.8
28
+
29
+ # @return [Boolean]
30
+ def new_cli_option_available?
31
+ Gem::Version.new(version) >= Gem::Version.new(NEW_CLI_OPTION_VERSION)
32
+ end
33
+
34
+ # @return [String]
35
+ def version
36
+ # versiom_command prints "1.6.3\n"
37
+ @version ||= `#{version_command}`.strip
38
+ end
39
+
40
+ # @yield [line] gives a line in libinput list-devices output to the block
41
+ def list_devices
42
+ cmd = list_devices_command
43
+ MultiLogger.debug(list_devices: cmd)
44
+ Open3.popen3(cmd) do |_i, o, _e, _w|
45
+ o.each { |line| yield(line) }
46
+ end
47
+ end
48
+
49
+ # @yield [line] gives a line in libinput debug-events output to the block
50
+ def debug_events
51
+ prefix = 'stdbuf -oL --'
52
+ options = [*libinput_options, device_option]
53
+ cmd = "#{prefix} #{debug_events_command} #{options.join(' ')}".strip
54
+ MultiLogger.debug(debug_events: cmd)
55
+ Open3.popen3(cmd) do |_i, o, _e, _w|
56
+ o.each do |line|
57
+ yield(line.chomp)
58
+ end
59
+ end
60
+ end
61
+
62
+ # @return [String] command
63
+ # @raise [SystemExit]
64
+ def version_command
65
+ if which('libinput')
66
+ 'libinput --version'
67
+ elsif which('libinput-list-devices')
68
+ 'libinput-list-devices --version'
69
+ else
70
+ MultiLogger.error 'install libinput-tools'
71
+ exit 1
72
+ end
73
+ end
74
+
75
+ def list_devices_command
76
+ if new_cli_option_available?
77
+ 'libinput list-devices'
78
+ else
79
+ 'libinput-list-devices'
80
+ end
81
+ end
82
+
83
+ def debug_events_command
84
+ if new_cli_option_available?
85
+ 'libinput debug-events'
86
+ else
87
+ 'libinput-debug-events'
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ # use device option only if libinput detect only 1 device
94
+ # @return [String]
95
+ def device_option
96
+ return unless Device.available.size == 1
97
+
98
+ "--device=/dev/input/#{Device.available.first.id}"
99
+ end
100
+
101
+ # TODO: add specs
102
+ def libinput_options
103
+ enable_tap = '--enable-tap' if config_params(:'enable-tap')
104
+ device = ("--device=#{config_params(:device)}" if config_params(:device))
105
+ enable_dwt = '--enable-dwt' if config_params(:'enable-dwt')
106
+
107
+ [
108
+ enable_tap,
109
+ device,
110
+ enable_dwt
111
+ ].compact
112
+ end
113
+
114
+ # which in ruby: Checking if program exists in $PATH from ruby
115
+ # (https://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby)
116
+ # Cross-platform way of finding an executable in the $PATH.
117
+ #
118
+ # which('ruby') #=> /usr/bin/ruby
119
+ # @return [String, nil]
120
+ def which(command)
121
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
122
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
123
+ exts.each do |ext|
124
+ exe = File.join(path, "#{command}#{ext}")
125
+ return exe if File.executable?(exe) && !File.directory?(exe)
126
+ end
127
+ end
128
+ nil
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end