fusuma 2.5.1 → 3.1.0

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