fusuma 0.11.1 → 1.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 (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