fusuma 1.9.0 → 2.0.0.pre

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.
@@ -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.