fusuma 1.9.0 → 2.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,5 +8,20 @@ require 'fusuma'
8
8
  # with your gem easier. You can also use a different console, if you like.
9
9
 
10
10
  # (If you use this, don't forget to add pry to your Gemfile!)
11
+
12
+ def reload!(print = true)
13
+ puts 'Reloading ...' if print
14
+ # Main project directory.
15
+ root_dir = File.expand_path('..', __dir__)
16
+ # Directories within the project that should be reloaded.
17
+ reload_dirs = %w[lib]
18
+ # Loop through and reload every file in all relevant project directories.
19
+ reload_dirs.each do |dir|
20
+ Dir.glob("#{root_dir}/#{dir}/**/*.rb").each { |f| load(f) }
21
+ end
22
+ # Return true when complete.
23
+ true
24
+ end
25
+
11
26
  require 'pry'
12
27
  Pry.start
@@ -24,15 +24,5 @@ Gem::Specification.new do |spec|
24
24
  spec.metadata['yard.run'] = 'yri' # use "yard" to build full HTML docs.
25
25
 
26
26
  spec.required_ruby_version = '>= 2.3' # https://packages.ubuntu.com/search?keywords=ruby&searchon=names&exact=1&suite=all&section=main
27
- spec.add_development_dependency 'bundler'
28
- spec.add_development_dependency "coveralls"
29
- spec.add_development_dependency 'github_changelog_generator', '~> 1.14'
30
- spec.add_development_dependency 'pry-byebug', '~> 3.4'
31
- spec.add_development_dependency 'pry-doc'
32
- spec.add_development_dependency 'pry-inline'
33
- spec.add_development_dependency 'rake', '~> 13.0'
34
- spec.add_development_dependency 'reek'
35
- spec.add_development_dependency 'rspec', '~> 3.0'
36
- spec.add_development_dependency 'rubocop'
37
- spec.add_development_dependency 'yard'
27
+ spec.add_dependency 'posix-spawn'
38
28
  end
@@ -4,6 +4,7 @@ require_relative './fusuma/version'
4
4
  require_relative './fusuma/multi_logger'
5
5
  require_relative './fusuma/config.rb'
6
6
  require_relative './fusuma/environment.rb'
7
+ require_relative './fusuma/device.rb'
7
8
  require_relative './fusuma/plugin/manager.rb'
8
9
 
9
10
  # this is top level module
@@ -47,8 +48,6 @@ module Fusuma
47
48
  end
48
49
 
49
50
  def load_custom_config(config_path = nil)
50
- return unless config_path
51
-
52
51
  Config.custom_path = config_path
53
52
  end
54
53
  end
@@ -63,8 +62,9 @@ module Fusuma
63
62
  end
64
63
 
65
64
  def run
66
- # TODO: run with multi thread
67
- @inputs.first.run do |event|
65
+ loop do
66
+ event = input
67
+ event || next
68
68
  clear_expired_events
69
69
  filtered = filter(event) || next
70
70
  parsed = parse(filtered) || next
@@ -75,6 +75,10 @@ module Fusuma
75
75
  end
76
76
  end
77
77
 
78
+ def input
79
+ Plugin::Inputs::Input.select(@inputs)
80
+ end
81
+
78
82
  def filter(event)
79
83
  event if @filters.any? { |f| f.filter(event) }
80
84
  end
@@ -111,11 +115,15 @@ module Fusuma
111
115
  def execute(event)
112
116
  return unless event
113
117
 
114
- executor = @executors.find do |e|
115
- e.executable?(event)
118
+ l = lambda do
119
+ executor = @executors.find { |e| e.executable?(event) }
120
+ executor&.execute(event)
116
121
  end
117
122
 
118
- executor&.execute(event)
123
+ l.call ||
124
+ Config::Searcher.skip { l.call } ||
125
+ Config::Searcher.fallback { l.call } ||
126
+ Config::Searcher.skip { Config::Searcher.fallback { l.call } }
119
127
  end
120
128
 
121
129
  def clear_expired_events
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative './multi_logger.rb'
4
4
  require_relative './config/index.rb'
5
+ require_relative './config/searcher.rb'
6
+ require_relative './config/yaml_duplication_checker.rb'
5
7
  require 'singleton'
6
8
  require 'yaml'
7
9
 
@@ -9,6 +11,9 @@ require 'yaml'
9
11
  module Fusuma
10
12
  # read keymap from yaml file
11
13
  class Config
14
+ class NotFoundError < StandardError; end
15
+ class InvalidFileError < StandardError; end
16
+
12
17
  include Singleton
13
18
 
14
19
  class << self
@@ -23,12 +28,12 @@ module Fusuma
23
28
 
24
29
  attr_reader :keymap
25
30
  attr_reader :custom_path
31
+ attr_reader :searcher
26
32
 
27
33
  def initialize
34
+ @searcher = Searcher.new
28
35
  @custom_path = nil
29
- @cache = nil
30
36
  @keymap = nil
31
- reload
32
37
  end
33
38
 
34
39
  def custom_path=(new_path)
@@ -37,40 +42,51 @@ module Fusuma
37
42
  end
38
43
 
39
44
  def reload
40
- @cache = nil
41
- @keymap = YAML.load_file(file_path).deep_symbolize_keys
42
- MultiLogger.info "reload config : #{file_path}"
45
+ @searcher = Searcher.new
46
+ path = find_filepath
47
+ MultiLogger.info "reload config: #{path}"
48
+ @keymap = validate(path)
43
49
  self
44
50
  end
45
51
 
46
- # @param index [Index]
47
- def search(index)
48
- cache(index.cache_key) do
49
- index.keys.reduce(keymap) do |location, key|
50
- if location.is_a?(Hash)
51
- begin
52
- if key.skippable
53
- location.fetch(key.symbol, location)
54
- else
55
- location.fetch(key.symbol, nil)
56
- end
57
- end
58
- else
59
- location
60
- end
61
- end
52
+ # @return [Hash]
53
+ # @raise [InvalidError]
54
+ def validate(path)
55
+ duplicates = []
56
+ YAMLDuplicationChecker.check(File.read(path), path) do |ignored, duplicate|
57
+ MultiLogger.error "#{path}: #{ignored.value} is duplicated"
58
+ duplicates << duplicate.value
62
59
  end
60
+ raise InvalidFileError, "Detect duplicate keys #{duplicates}" unless duplicates.empty?
61
+
62
+ yaml = YAML.load_file(path)
63
+
64
+ raise InvalidFileError, 'Invaid YAML file' unless yaml.is_a? Hash
65
+
66
+ yaml.deep_symbolize_keys
67
+ rescue StandardError => e
68
+ MultiLogger.error e.message
69
+ raise InvalidFileError, e.message
70
+ end
71
+
72
+ # @param index [Index]
73
+ # @param location [Hash]
74
+ def search(index, location: keymap)
75
+ @searcher.search_with_cache(index, location: location)
63
76
  end
64
77
 
65
78
  private
66
79
 
67
- def file_path
80
+ def find_filepath
68
81
  filename = 'fusuma/config.yml'
69
- if custom_path && File.exist?(expand_custom_path)
70
- expand_custom_path
82
+ if custom_path
83
+ return expand_custom_path if File.exist?(expand_custom_path)
84
+
85
+ raise NotFoundError, "#{expand_custom_path} is NOT FOUND"
71
86
  elsif File.exist?(expand_config_path(filename))
72
87
  expand_config_path(filename)
73
88
  else
89
+ MultiLogger.warn "config file: #{expand_config_path(filename)} is NOT FOUND"
74
90
  expand_default_path(filename)
75
91
  end
76
92
  end
@@ -86,16 +102,6 @@ module Fusuma
86
102
  def expand_default_path(filename)
87
103
  File.expand_path "../../#{filename}", __FILE__
88
104
  end
89
-
90
- def cache(key)
91
- @cache ||= {}
92
- key = key.join(',') if key.is_a? Array
93
- if @cache.key?(key)
94
- @cache[key]
95
- else
96
- @cache[key] = block_given? ? yield : nil
97
- end
98
- end
99
105
  end
100
106
  end
101
107
 
@@ -34,15 +34,22 @@ module Fusuma
34
34
 
35
35
  # Keys in Index
36
36
  class Key
37
- def initialize(symbol_word, skippable: false)
37
+ def initialize(symbol_word, skippable: false, fallback: nil)
38
38
  @symbol = begin
39
39
  symbol_word.to_sym
40
40
  rescue StandardError
41
41
  symbol_word
42
42
  end
43
+
43
44
  @skippable = skippable
45
+
46
+ @fallback = begin
47
+ fallback.to_sym
48
+ rescue StandardError
49
+ fallback
50
+ end
44
51
  end
45
- attr_reader :symbol, :skippable
52
+ attr_reader :symbol, :skippable, :fallback
46
53
  end
47
54
  end
48
55
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Index for searching value from config.yml
4
+ module Fusuma
5
+ class Config
6
+ # Search config.yml
7
+ class Searcher
8
+ def initialize
9
+ @cache
10
+ end
11
+
12
+ # @param index [Index]
13
+ # @param location [Hash]
14
+ # @return [NilClass]
15
+ # @return [Hash]
16
+ # @return [Object]
17
+ def search(index, location:)
18
+ key = index.keys.first
19
+ return location if key.nil?
20
+
21
+ return nil if location.nil?
22
+
23
+ return nil unless location.is_a?(Hash)
24
+
25
+ next_index = Index.new(index.keys[1..-1])
26
+
27
+ value = nil
28
+ next_location_cadidates(location, key).find do |next_location|
29
+ value = search(next_index, location: next_location)
30
+ end
31
+ value
32
+ end
33
+
34
+ def search_with_cache(index, location:)
35
+ cache([index.cache_key, Searcher.skip?, Searcher.fallback?]) do
36
+ search(index, location: location)
37
+ end
38
+ end
39
+
40
+ def cache(key)
41
+ @cache ||= {}
42
+ key = key.join(',') if key.is_a? Array
43
+ if @cache.key?(key)
44
+ @cache[key]
45
+ else
46
+ @cache[key] = block_given? ? yield : nil
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ # next locations' candidates sorted by priority
53
+ # 1. look up location with key
54
+ # 2. fallback to other key
55
+ # 3. skip the key and go to child location
56
+ def next_location_cadidates(location, key)
57
+ [
58
+ location[key.symbol],
59
+ Searcher.fallback? && key.fallback && location[key.fallback],
60
+ Searcher.skip? && key.skippable && location
61
+ ].compact
62
+ end
63
+
64
+ class << self
65
+ def fallback?
66
+ @fallback
67
+ end
68
+
69
+ def skip?
70
+ @skip
71
+ end
72
+
73
+ # switch context for fallback
74
+ def fallback(&block)
75
+ @fallback = true
76
+ result = block.call
77
+ @fallback = false
78
+ result
79
+ end
80
+
81
+ def skip(&block)
82
+ @skip = true
83
+ result = block.call
84
+ @skip = false
85
+ result
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fusuma
4
+ class Config
5
+ # ref: https://github.com/rubocop-hq/rubocop/blob/97e4ffc8a71e9e5239a927c6a534dfc1e0da917f/lib/rubocop/yaml_duplication_checker.rb
6
+ # Find duplicated keys from YAML.
7
+ module YAMLDuplicationChecker
8
+ def self.check(yaml_string, filename, &on_duplicated)
9
+ # Ruby 2.6+
10
+ tree = if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0')
11
+ # Specify filename to display helpful message when it raises
12
+ # an error.
13
+ YAML.parse(yaml_string, filename: filename)
14
+ else
15
+ YAML.parse(yaml_string, filename)
16
+ end
17
+ return unless tree
18
+
19
+ traverse(tree, &on_duplicated)
20
+ end
21
+
22
+ def self.traverse(tree, &on_duplicated)
23
+ case tree
24
+ when Psych::Nodes::Mapping
25
+ tree.children.each_slice(2).with_object([]) do |(key, value), keys|
26
+ exist = keys.find { |key2| key2.value == key.value }
27
+ on_duplicated.call(exist, key) if exist
28
+ keys << key
29
+ traverse(value, &on_duplicated)
30
+ end
31
+ else
32
+ children = tree.children
33
+ return unless children
34
+
35
+ children.each { |c| traverse(c, &on_duplicated) }
36
+ end
37
+ end
38
+
39
+ private_class_method :traverse
40
+ end
41
+ end
42
+ end
@@ -59,7 +59,9 @@ module Fusuma
59
59
  # @return [Array]
60
60
  def fetch_devices
61
61
  line_parser = LineParser.new
62
- LibinputCommand.new.list_devices do |line|
62
+
63
+ libinput_command = Plugin::Inputs::LibinputCommandInput.new.command
64
+ libinput_command.list_devices do |line|
63
65
  line_parser.push(line)
64
66
  end
65
67
  line_parser.generate_devices
@@ -17,8 +17,9 @@ module Fusuma
17
17
  end
18
18
 
19
19
  def print_version
20
+ libinput_command = Plugin::Inputs::LibinputCommandInput.new.command
20
21
  MultiLogger.info "Fusuma: #{VERSION}"
21
- MultiLogger.info "libinput: #{LibinputCommand.new.version}"
22
+ MultiLogger.info "libinput: #{libinput_command.version}"
22
23
  MultiLogger.info "OS: #{`uname -rsv`}".strip
23
24
  MultiLogger.info "Distribution: #{`cat /etc/issue`}".strip
24
25
  MultiLogger.info "Desktop session: #{`echo $DESKTOP_SESSION $XDG_SESSION_TYPE`}".strip
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'open3'
3
+ require 'posix/spawn'
4
4
 
5
5
  module Fusuma
6
6
  # Execute libinput command
7
7
  class LibinputCommand
8
- def initialize(libinput_options: [])
8
+ def initialize(libinput_options: [], commands: {})
9
+ @debug_events_command = commands[:debug_events_command]
10
+ @list_devices_command = commands[:list_devices_command]
9
11
  @libinput_options = libinput_options
10
12
  end
11
13
 
@@ -13,6 +15,9 @@ module Fusuma
13
15
  # use `libinput list-devices` and `libinput debug-events` from 1.8.
14
16
  NEW_CLI_OPTION_VERSION = 1.8
15
17
 
18
+ DEFAULT_WAIT_TIME = 0.3
19
+ TIMEOUT_MESSAGE = 'LIBINPUT TIMEOUT'
20
+
16
21
  # @return [Boolean]
17
22
  def new_cli_option_available?
18
23
  Gem::Version.new(version) >= Gem::Version.new(NEW_CLI_OPTION_VERSION)
@@ -28,27 +33,29 @@ module Fusuma
28
33
  def list_devices
29
34
  cmd = list_devices_command
30
35
  MultiLogger.debug(list_devices: cmd)
31
- Open3.popen3(cmd) do |_i, o, _e, _w|
32
- o.each { |line| yield(line) }
33
- end
36
+ p, i, o, e = POSIX::Spawn.popen4(cmd)
37
+ i.close
38
+ o.each { |line| yield(line) }
39
+ ensure
40
+ [i, o, e].each { |io| io.close unless io.closed? }
41
+ Process.waitpid(p)
34
42
  end
35
43
 
36
- # @yieldparam [String] gives a line in libinput debug-events output to the block
44
+ # @return [String] return a latest line libinput debug-events
37
45
  def debug_events
38
- prefix = 'stdbuf -oL --'
39
- cmd = "#{prefix} #{debug_events_command} #{@libinput_options.join(' ')}".strip
40
- MultiLogger.debug(debug_events: cmd)
41
- Open3.popen3(cmd) do |_i, o, _e, _w|
42
- o.each do |line|
43
- yield(line.chomp)
44
- end
45
- end
46
+ @debug_events ||= begin
47
+ _p, i, o, _e = POSIX::Spawn.popen4(debug_events_with_options)
48
+ i.close
49
+ o
50
+ end
46
51
  end
47
52
 
48
53
  # @return [String] command
49
54
  # @raise [SystemExit]
50
55
  def version_command
51
- if which('libinput')
56
+ if @debug_events_command && @list_devices_command
57
+ "#{@list_devices_command} --version"
58
+ elsif which('libinput')
52
59
  'libinput --version'
53
60
  elsif which('libinput-list-devices')
54
61
  'libinput-list-devices --version'
@@ -59,7 +66,9 @@ module Fusuma
59
66
  end
60
67
 
61
68
  def list_devices_command
62
- if new_cli_option_available?
69
+ if @list_devices_command
70
+ @list_devices_command
71
+ elsif new_cli_option_available?
63
72
  'libinput list-devices'
64
73
  else
65
74
  'libinput-list-devices'
@@ -67,15 +76,26 @@ module Fusuma
67
76
  end
68
77
 
69
78
  def debug_events_command
70
- if new_cli_option_available?
79
+ if @debug_events_command
80
+ @debug_events_command
81
+ elsif new_cli_option_available?
71
82
  'libinput debug-events'
72
83
  else
73
84
  'libinput-debug-events'
74
85
  end
75
86
  end
76
87
 
88
+ def debug_events_with_options
89
+ prefix = 'stdbuf -oL --'
90
+ "#{prefix} #{debug_events_command} #{@libinput_options.join(' ')}".strip
91
+ end
92
+
77
93
  private
78
94
 
95
+ def wait_time
96
+ DEFAULT_WAIT_TIME
97
+ end
98
+
79
99
  # which in ruby: Checking if program exists in $PATH from ruby
80
100
  # (https://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby)
81
101
  # Cross-platform way of finding an executable in the $PATH.