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.
- checksums.yaml +4 -4
- data/.github/pull_request_template.md +9 -0
- data/.rubocop.yml +24 -0
- data/CHANGELOG.md +65 -2
- data/CONTRIBUTING.md +72 -0
- data/Gemfile +12 -0
- data/README.md +140 -70
- data/bin/console +15 -0
- data/fusuma.gemspec +1 -11
- data/lib/fusuma.rb +15 -7
- data/lib/fusuma/config.rb +40 -34
- data/lib/fusuma/config/index.rb +9 -2
- data/lib/fusuma/config/searcher.rb +90 -0
- data/lib/fusuma/config/yaml_duplication_checker.rb +42 -0
- data/lib/fusuma/device.rb +3 -1
- data/lib/fusuma/environment.rb +2 -1
- data/lib/fusuma/libinput_command.rb +37 -17
- data/lib/fusuma/plugin/buffers/timer_buffer.rb +46 -0
- data/lib/fusuma/plugin/events/records/gesture_record.rb +4 -1
- data/lib/fusuma/plugin/executors/command_executor.rb +2 -4
- data/lib/fusuma/plugin/filters/libinput_timeout_filter.rb +21 -0
- data/lib/fusuma/plugin/inputs/input.rb +46 -3
- data/lib/fusuma/plugin/inputs/libinput_command_input.rb +27 -5
- data/lib/fusuma/plugin/inputs/timer_input.rb +63 -0
- data/lib/fusuma/version.rb +1 -1
- metadata +17 -150
data/bin/console
CHANGED
@@ -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
|
data/fusuma.gemspec
CHANGED
@@ -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§ion=main
|
27
|
-
spec.
|
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
|
data/lib/fusuma.rb
CHANGED
@@ -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
|
-
|
67
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/fusuma/config.rb
CHANGED
@@ -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
|
-
@
|
41
|
-
|
42
|
-
MultiLogger.info "reload config
|
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
|
-
# @
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
80
|
+
def find_filepath
|
68
81
|
filename = 'fusuma/config.yml'
|
69
|
-
if 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
|
|
data/lib/fusuma/config/index.rb
CHANGED
@@ -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
|
data/lib/fusuma/device.rb
CHANGED
@@ -59,7 +59,9 @@ module Fusuma
|
|
59
59
|
# @return [Array]
|
60
60
|
def fetch_devices
|
61
61
|
line_parser = LineParser.new
|
62
|
-
|
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
|
data/lib/fusuma/environment.rb
CHANGED
@@ -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: #{
|
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 '
|
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
|
-
|
32
|
-
|
33
|
-
|
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
|
-
# @
|
44
|
+
# @return [String] return a latest line libinput debug-events
|
37
45
|
def debug_events
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
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
|
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
|
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.
|