fusuma 2.5.1 → 3.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.
@@ -9,8 +9,14 @@ module Fusuma
9
9
  # Inherite this base
10
10
  # @abstract Subclass and override {#io} to implement
11
11
  class Input < Base
12
+ def initialize(*args)
13
+ super(*args)
14
+ @tag = self.class.name.split("Inputs::").last.underscore
15
+ end
16
+
17
+ attr_reader :tag
18
+
12
19
  # Wait multiple inputs until it becomes readable
13
- # and read lines with nonblock
14
20
  # @param inputs [Array<Input>]
15
21
  # @return [Event]
16
22
  def self.select(inputs)
@@ -20,20 +26,17 @@ module Fusuma
20
26
  input = inputs.find { |i| i.io == io }
21
27
 
22
28
  begin
23
- line = io.readline_nonblock("\n").chomp
29
+ # NOTE: io.readline is blocking method
30
+ # each input plugin must write line to pipe (include `\n`)
31
+ line = io.readline(chomp: true)
24
32
  rescue EOFError => e
25
- warn "#{input.class.name}: #{e}"
26
- warn "Send SIGKILL to fusuma processes"
27
- inputs.reject { |i| i == input }.each do |i|
28
- warn "stop process: #{i.class.name.underscore}"
29
- Process.kill(:SIGKILL, i.pid)
30
- end
31
- exit 1
33
+ MultiLogger.error "#{input.class.name}: #{e}"
34
+ MultiLogger.error "Shutdown fusuma process..."
35
+ Process.kill("TERM", Process.pid)
32
36
  rescue => e
33
- warn "#{input.class.name}: #{e}"
37
+ MultiLogger.error "#{input.class.name}: #{e}"
34
38
  exit 1
35
39
  end
36
-
37
40
  input.create_event(record: line)
38
41
  end
39
42
 
@@ -53,32 +56,7 @@ module Fusuma
53
56
  MultiLogger.debug(input_event: e)
54
57
  e
55
58
  end
56
-
57
- def tag
58
- self.class.name.split("Inputs::").last.underscore
59
- end
60
59
  end
61
60
  end
62
61
  end
63
62
  end
64
-
65
- # ref: https://github.com/Homebrew/brew/blob/6b2dbbc96f7d8aa12f9b8c9c60107c9cc58befc4/Library/Homebrew/extend/io.rb
66
- class IO
67
- def readline_nonblock(sep = $INPUT_RECORD_SEPARATOR)
68
- line = +""
69
- buffer = +""
70
-
71
- loop do
72
- break if buffer == sep
73
-
74
- read_nonblock(1, buffer)
75
- line.concat(buffer)
76
- end
77
-
78
- line.freeze
79
- rescue IO::WaitReadable, EOFError => e
80
- raise e if line.empty?
81
-
82
- line.freeze
83
- end
84
- end
@@ -0,0 +1,3 @@
1
+ plugin:
2
+ inputs:
3
+ libinput_command_input:
@@ -1,20 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "./input"
4
+ require "timeout"
4
5
 
5
6
  module Fusuma
6
7
  module Plugin
7
8
  module Inputs
8
9
  # libinput commands wrapper
9
10
  class TimerInput < Input
10
- DEFAULT_INTERVAL = 0.3
11
+ include Singleton
12
+ DEFAULT_INTERVAL = 5
13
+ EPSILON_TIME = 0.02
11
14
  def config_param_types
12
15
  {
13
16
  interval: [Float]
14
17
  }
15
18
  end
16
19
 
17
- attr_reader :pid
20
+ def initialize(*args, interval: nil)
21
+ super(*args)
22
+ @interval = interval || config_params(:interval) || DEFAULT_INTERVAL
23
+ @early_wake_queue = Queue.new
24
+ end
25
+
26
+ attr_reader :pid, :interval
18
27
 
19
28
  def io
20
29
  @io ||= begin
@@ -26,26 +35,36 @@ module Fusuma
26
35
  end
27
36
 
28
37
  def start(reader, writer)
29
- pid = fork do
30
- timer_loop(reader, writer)
38
+ Thread.new do
39
+ timer_loop(writer)
31
40
  end
32
- Process.detach(pid)
33
- writer.close
34
- pid
41
+ nil
35
42
  end
36
43
 
37
- def timer_loop(reader, writer)
38
- reader.close
39
- begin
40
- loop do
41
- sleep interval
42
- writer.puts "timer"
44
+ def timer_loop(writer)
45
+ delta_t = @interval
46
+ next_wake = Time.now + delta_t
47
+ loop do
48
+ sleep_time = next_wake - Time.now
49
+ if sleep_time <= 0
50
+ raise Timeout::Error
51
+ end
52
+
53
+ Timeout.timeout(sleep_time) do
54
+ next_wake = [@early_wake_queue.deq, next_wake].min
43
55
  end
44
- rescue Errno::EPIPE
45
- exit 0
46
- rescue => e
47
- MultiLogger.error e
56
+ rescue Timeout::Error
57
+ writer.puts "timer"
58
+ next_wake = Time.now + delta_t
48
59
  end
60
+ rescue Errno::EPIPE
61
+ exit 0
62
+ rescue => e
63
+ MultiLogger.error e
64
+ end
65
+
66
+ def wake_early(t)
67
+ @early_wake_queue.push(t + EPSILON_TIME)
49
68
  end
50
69
 
51
70
  private
@@ -53,10 +72,6 @@ module Fusuma
53
72
  def create_io
54
73
  IO.pipe
55
74
  end
56
-
57
- def interval
58
- config_params(:interval) || DEFAULT_INTERVAL
59
- end
60
75
  end
61
76
  end
62
77
  end
@@ -27,29 +27,34 @@ module Fusuma
27
27
  @_fusuma_default_plugin_paths ||= Dir.glob(File.expand_path("#{__dir__}/../../#{search_key}")).grep_v(exclude_path_pattern).sort
28
28
  end
29
29
 
30
+ # @return [Array<String>] paths of external plugins (installed by gem)
30
31
  def fusuma_external_plugin_paths
31
32
  @_fusuma_external_plugin_paths ||=
32
33
  Gem.find_latest_files(search_key).map do |siblings_plugin|
33
34
  next unless %r{fusuma-plugin-(.+).*/lib/#{plugin_dir_name}/.+\.rb}.match?(siblings_plugin)
34
35
 
35
36
  match_data = siblings_plugin.match(%r{(.*)/(.*)/lib/(.*)})
36
- gemspec_path = Dir.glob("#{match_data[1]}/#{match_data[2]}/*.gemspec").first
37
- raise "Not Found: #{match_data[1]}/#{match_data[2]}/*.gemspec" unless gemspec_path
37
+ plugin_gemspec_path = Dir.glob("#{match_data[1]}/#{match_data[2]}/*.gemspec").first
38
+ raise "Not Found: #{match_data[1]}/#{match_data[2]}/*.gemspec" unless plugin_gemspec_path
38
39
 
39
- gemspec = Gem::Specification.load(gemspec_path)
40
+ plugin_gemspec = Gem::Specification.load(plugin_gemspec_path)
40
41
  fusuma_gemspec_path = File.expand_path("../../../fusuma.gemspec", __dir__)
41
42
  fusuma_gemspec = Gem::Specification.load(fusuma_gemspec_path)
42
43
 
43
- if gemspec.dependencies.find { |d| d.name == "fusuma" }&.match?(fusuma_gemspec)
44
+ if plugin_gemspec.dependencies.find { |d| d.name == "fusuma" }&.match?(fusuma_gemspec)
44
45
  siblings_plugin
45
46
  else
46
- MultiLogger.warn "#{gemspec.name} #{gemspec.version} is incompatible with running #{fusuma_gemspec.name} #{fusuma_gemspec.version}"
47
- MultiLogger.warn "gemspec: #{gemspec_path}"
47
+ MultiLogger.warn "#{plugin_gemspec.name} #{plugin_gemspec.version} is incompatible with running #{fusuma_gemspec.name} #{fusuma_gemspec.version}"
48
+ MultiLogger.warn "gemspec: #{plugin_gemspec_path}"
48
49
  next
49
50
  end
50
51
  end.compact.grep_v(exclude_path_pattern).sort
51
52
  end
52
53
 
54
+ # @return [String] search key for plugin
55
+ # @example
56
+ # search_key
57
+ # => "fusuma/plugin/detectors/*rb"
53
58
  def search_key
54
59
  File.join(plugin_dir_name, "*rb")
55
60
  end
@@ -110,6 +115,12 @@ module Fusuma
110
115
  @plugins ||= {}
111
116
  end
112
117
 
118
+ # @return [Array<String>]
119
+ # @example
120
+ # Manager.load_paths
121
+ # => ["/path/to/fusuma/lib/fusuma/plugin/inputs/input.rb",
122
+ # "/path/to/fusuma/lib/fusuma/plugin/inputs/libinput_command_input.rb",
123
+ # "/path/to/fusuma/lib/fusuma/plugin/inputs/timer_input.rb"]
113
124
  def load_paths
114
125
  @load_paths ||= []
115
126
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fusuma
4
- VERSION = "2.5.1"
4
+ VERSION = "3.1.0"
5
5
  end
data/lib/fusuma.rb CHANGED
@@ -13,9 +13,9 @@ module Fusuma
13
13
  class Runner
14
14
  class << self
15
15
  def run(option = {})
16
- set_trap
17
16
  read_options(option)
18
17
  instance = new
18
+ instance.set_trap
19
19
  ## NOTE: Uncomment following line to measure performance
20
20
  # instance.run_with_lineprof
21
21
  instance.run
@@ -23,22 +23,22 @@ module Fusuma
23
23
 
24
24
  private
25
25
 
26
- def set_trap
27
- Signal.trap("INT") { puts exit } # Trap ^C
28
- Signal.trap("TERM") { puts exit } # Trap `Kill `
29
- end
30
-
31
26
  def read_options(option)
32
27
  MultiLogger.filepath = option[:log_filepath]
33
28
  MultiLogger.instance.debug_mode = option[:verbose]
34
29
 
35
- load_custom_config(option[:config_path])
36
-
37
30
  Plugin::Manager.require_base_plugins
38
31
 
32
+ load_custom_config(option[:config_path])
33
+
39
34
  Environment.dump_information
40
35
  Kernel.exit(0) if option[:version]
41
36
 
37
+ if option[:show_config]
38
+ Environment.print_config
39
+ Kernel.exit(0)
40
+ end
41
+
42
42
  if option[:list]
43
43
  Environment.print_device_list
44
44
  Kernel.exit(0)
@@ -56,7 +56,9 @@ module Fusuma
56
56
  end
57
57
 
58
58
  def initialize
59
- @inputs = Plugin::Inputs::Input.plugins.map(&:new)
59
+ @inputs = Plugin::Inputs::Input.plugins.map do |cls|
60
+ cls.ancestors.include?(Singleton) ? cls.instance : cls.new
61
+ end
60
62
  @filters = Plugin::Filters::Filter.plugins.map(&:new)
61
63
  @parsers = Plugin::Parsers::Parser.plugins.map(&:new)
62
64
  @buffers = Plugin::Buffers::Buffer.plugins.map(&:new)
@@ -75,8 +77,8 @@ module Fusuma
75
77
  parsed = parse(filtered) || return
76
78
  buffered = buffer(parsed) || return
77
79
  detected = detect(buffered) || return
78
- condition, context, event = merge(detected) || return
79
- execute(condition, context, event)
80
+ context, event = merge(detected) || return
81
+ execute(context, event)
80
82
  end
81
83
 
82
84
  # For performance monitoring
@@ -98,12 +100,14 @@ module Fusuma
98
100
 
99
101
  # @param [Plugin::Events::Event]
100
102
  # @return [Plugin::Events::Event]
103
+ # @return [NilClass]
101
104
  def filter(event)
102
105
  event if @filters.any? { |f| f.filter(event) }
103
106
  end
104
107
 
105
108
  # @param [Plugin::Events::Event]
106
109
  # @return [Plugin::Events::Event]
110
+ # @return [NilClass]
107
111
  def parse(event)
108
112
  @parsers.reduce(event) { |e, p| p.parse(e) if e }
109
113
  end
@@ -117,6 +121,7 @@ module Fusuma
117
121
 
118
122
  # @param buffers [Array<Buffer>]
119
123
  # @return [Array<Event>]
124
+ # @return [NilClass]
120
125
  def detect(buffers)
121
126
  matched_detectors = @detectors.select do |detector|
122
127
  detector.watch? ||
@@ -124,7 +129,8 @@ module Fusuma
124
129
  end
125
130
 
126
131
  events = matched_detectors.each_with_object([]) do |detector, detected|
127
- Array(detector.detect(@buffers)).each { |e| detected << e }
132
+ # Array(detector.detect(@buffers)).each { |e| detected << e }
133
+ detected.concat(Array(detector.detect(@buffers)))
128
134
  end
129
135
 
130
136
  return if events.empty?
@@ -133,7 +139,7 @@ module Fusuma
133
139
  end
134
140
 
135
141
  # @param events [Array<Plugin::Events::Event>]
136
- # @return [Plugin::Events::Event] Event merged all records from arguments
142
+ # @return [Array<Hash, Plugin::Events::Event>] Event merged all events from arguments and used context
137
143
  # @return [NilClass] when event is NOT given
138
144
  def merge(events)
139
145
  index_events, context_events = events.partition { |event| event.record.type == :index }
@@ -143,44 +149,34 @@ module Fusuma
143
149
  end
144
150
  main_events.sort_by! { |e| e.record.trigger_priority }
145
151
 
146
- matched_condition = nil
147
152
  matched_context = nil
148
153
  event = main_events.find do |main_event|
149
154
  matched_context = Config::Searcher.find_context(request_context) do
150
- matched_condition, modified_record = Config::Searcher.find_condition do
151
- main_event.record.merge(records: modifiers.map(&:record))
152
- end
153
- if matched_condition && modified_record
155
+ if (modified_record = main_event.record.merge(records: modifiers.map(&:record)))
154
156
  main_event.record = modified_record
155
- else
156
- matched_condition, = Config::Searcher.find_condition do
157
- Config.search(main_event.record.index) &&
158
- Config.find_execute_key(main_event.record.index)
159
- end
157
+ elsif !modifiers.empty?
158
+ # try basically the same, but without any modifiers
159
+ # if modifiers is empty then we end up here only if there is no execute key for this
160
+ Config.instance.search(main_event.record.index) &&
161
+ Config.instance.find_execute_key(main_event.record.index)
160
162
  end
161
163
  end
162
164
  end
163
165
  return if event.nil?
164
166
 
165
- [matched_condition, matched_context, event]
167
+ [matched_context, event]
166
168
  end
167
169
 
170
+ # @return [NilClass] when event is NOT given or executable context is NOT found
168
171
  # @param event [Plugin::Events::Event]
169
- def execute(condition, context, event)
172
+ def execute(context, event)
170
173
  return unless event
171
174
 
172
- # Find executable condition and executor
173
- executor = Config::Searcher.with_context(context) do
174
- Config::Searcher.with_condition(condition) do
175
- @executors.find { |e| e.executable?(event) }
176
- end
177
- end
178
-
179
- return if executor.nil?
180
-
181
- # Check interval and execute
175
+ # Find executable context
182
176
  Config::Searcher.with_context(context) do
183
- Config::Searcher.with_condition(condition) do
177
+ executor = @executors.find { |e| e.executable?(event) }
178
+ if executor
179
+ # Check interval and execute
184
180
  executor.enough_interval?(event) &&
185
181
  executor.update_interval(event) &&
186
182
  executor.execute(event)
@@ -191,5 +187,24 @@ module Fusuma
191
187
  def clear_expired_events
192
188
  @buffers.each(&:clear_expired)
193
189
  end
190
+
191
+ def set_trap
192
+ Signal.trap("INT") {
193
+ shutdown
194
+ puts exit
195
+ } # Trap ^C
196
+ Signal.trap("TERM") {
197
+ shutdown
198
+ puts exit
199
+ } # Trap `Kill `
200
+ end
201
+
202
+ private
203
+
204
+ def shutdown
205
+ [@inputs, @filters, @parsers, @buffers, @detectors, @executors].flatten.each do |plugin|
206
+ plugin.shutdown
207
+ end
208
+ end
194
209
  end
195
210
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fusuma
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.1
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - iberianpig
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-11-12 00:00:00.000000000 Z
11
+ date: 2023-09-03 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Fusuma is multitouch gesture recognizer. This gem makes your touchpad
14
14
  on Linux able to recognize swipes or pinchs and assign command to them. Read installation
@@ -59,6 +59,7 @@ files:
59
59
  - lib/fusuma/plugin/filters/libinput_device_filter.rb
60
60
  - lib/fusuma/plugin/inputs/input.rb
61
61
  - lib/fusuma/plugin/inputs/libinput_command_input.rb
62
+ - lib/fusuma/plugin/inputs/libinput_command_input.yml
62
63
  - lib/fusuma/plugin/inputs/timer_input.rb
63
64
  - lib/fusuma/plugin/manager.rb
64
65
  - lib/fusuma/plugin/parsers/libinput_gesture_parser.rb
@@ -79,14 +80,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
79
80
  requirements:
80
81
  - - ">="
81
82
  - !ruby/object:Gem::Version
82
- version: 2.5.1
83
+ version: '2.7'
83
84
  required_rubygems_version: !ruby/object:Gem::Requirement
84
85
  requirements:
85
86
  - - ">="
86
87
  - !ruby/object:Gem::Version
87
88
  version: '0'
88
89
  requirements: []
89
- rubygems_version: 3.0.3.1
90
+ rubygems_version: 3.4.10
90
91
  signing_key:
91
92
  specification_version: 4
92
93
  summary: Multitouch gestures with libinput driver, Linux