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