touchscreen-taps 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cd4ca10f5439c6236d2e179e46f19d63531fd59d7b89ad94d421817952b0a0c6
4
+ data.tar.gz: 91ac28937e62700d68e407384a62dba6b4adcd344345b083d7cbcef9f4a09238
5
+ SHA512:
6
+ metadata.gz: 286e7df3bf4c5c21a17adb9e912ff2ffc3c648094b6eb83bfd5646952e9edd3b83dc41bbcd7a300c22990c40ff4227f53973d6de114afd3b9559447f0655dd27
7
+ data.tar.gz: 2beede4c0d9a0e9eafef326fada7d799ad069ee76e3c5d7ff7d3d42ba539d1c3df79a2013a6da832260708522d1cb293f4bd4d70a01c9c1aa09ea43a05ec2c01
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ ## [v0.1.0](https://github.com/antonsokolskyy/touchscreen-taps/tree/v0.1.0) (2020-02-21)
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in touchscreen-taps.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Anton Sokolskyi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # TouchscreenTaps
2
+
3
+ TouchscreenTaps is a touchscreen taps recognizer. It allows "tap and hold" gesture to be recognized and assign a command to it.
4
+
5
+ # Instalation
6
+
7
+ ```bash
8
+ sudo gpasswd -a $USER input
9
+ sudo apt install libinput-tools
10
+ sudo apt install xdotool
11
+ sudo apt install ruby
12
+ sudo gem install touchscreen-taps
13
+ ```
14
+
15
+ # Usage
16
+ ```bash
17
+ touchscreen-taps
18
+ ```
19
+ Pass custom config
20
+ ```bash
21
+ touchscreen-taps /home/user/custom_config.yml
22
+ ```
23
+
24
+ # Config example
25
+ ```yaml
26
+ ---
27
+ tap_and_hold: # event name
28
+ 1: # fingers count
29
+ command: 'xdotool click 3'
30
+ acceptance_delay: '0.5' # seconds
31
+ ```
32
+ # Run as daemon
33
+ ```bash
34
+ touchscreen-taps &
35
+ ```
36
+
37
+ # Autostart
38
+ ```bash
39
+ echo 'touchscreen-taps &' | sudo tee /etc/profile.d/start-touchscreen-taps.sh
40
+ ```
41
+
42
+ # Tested on:
43
+ Surface Pro 3 (i5/8gb/256gb)
44
+ Ubuntu 18.04 LTS (linux 5.5, Gnome, Xorg)
45
+
46
+
47
+ ## License
48
+
49
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'touchscreen_taps'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/touchscreen_taps'
5
+
6
+ TouchscreenTaps::Runner.new(ARGV[0]).process_events
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'touchscreen_taps/libinput'
4
+ require_relative 'touchscreen_taps/touchscreen_device'
5
+ require_relative 'touchscreen_taps/gesture_buffer'
6
+ require_relative 'touchscreen_taps/config'
7
+ require_relative 'touchscreen_taps/event'
8
+ require_relative 'touchscreen_taps/event_filter'
9
+ require_relative 'touchscreen_taps/event_parser'
10
+ require_relative 'touchscreen_taps/tap_and_hold_detector'
11
+ require_relative 'touchscreen_taps/executor'
12
+
13
+ module TouchscreenTaps
14
+ # Main runner
15
+ class Runner
16
+ def initialize(config_path)
17
+ config = Config.new(config_path)
18
+ @buffer = config.buffer
19
+ @touchscreen = config.touchscreen
20
+ @detector = TapAndHoldDetector.new(config)
21
+ @executor = Executor.new(config)
22
+ end
23
+
24
+ def process_events
25
+ Libinput.events(@touchscreen) do |event|
26
+ event = EventFilter.call(event)
27
+ next unless event
28
+
29
+ event = EventParser.call(event)
30
+ @buffer.push(event)
31
+ command = @detector.process(@buffer)
32
+ @executor.run(command)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TouchscreenTaps
4
+ # Holds main configurations
5
+ class Config
6
+ require 'yaml'
7
+
8
+ attr_reader :buffer
9
+ attr_reader :touchscreen
10
+
11
+ def initialize(path = nil)
12
+ @buffer = fetch_buffer
13
+ @touchscreen = fetch_touchscreen
14
+ @config = fetch_config(path)
15
+ end
16
+
17
+ def by_keys(keys)
18
+ @config.dig(*keys)
19
+ end
20
+
21
+ private
22
+
23
+ def fetch_buffer
24
+ GestureBuffer.new
25
+ end
26
+
27
+ def fetch_touchscreen
28
+ devices = Libinput.devices
29
+ touchscreens = TouchscreenDevice.select_from(devices)
30
+ # touchscreen can be nil
31
+ # in that case events from all touch-capable devices will be processed
32
+ touchscreens.count == 1 ? touchscreens.first : nil
33
+ end
34
+
35
+ def fetch_config(path)
36
+ YAML.load_file(path || default_config_path)
37
+ end
38
+
39
+ def default_config_path
40
+ "#{__dir__}/default_config.yml"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,5 @@
1
+ ---
2
+ tap_and_hold: # event name
3
+ 1: # fingers count
4
+ command: 'xdotool click 3'
5
+ acceptance_delay: '0.5' # seconds
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TouchscreenTaps
4
+ # Parsed Event object
5
+ class Event
6
+ attr_reader :timestamp
7
+ attr_reader :type
8
+ attr_reader :finger
9
+ attr_reader :move_x
10
+ attr_reader :move_y
11
+
12
+ def initialize(type, finger, move_x = nil, move_y = nil)
13
+ @type = type
14
+ @finger = finger
15
+ @move_x = move_x
16
+ @move_y = move_y
17
+ @timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TouchscreenTaps
4
+ # Allows only necessary events
5
+ class EventFilter
6
+ class << self
7
+ def call(event)
8
+ return unless event =~ /TOUCH_UP|TOUCH_DOWN|TOUCH_MOTION/
9
+
10
+ event
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TouchscreenTaps
4
+ # Parses a raw line and creates an event object
5
+ class EventParser
6
+ class << self
7
+ def call(event)
8
+ _device, event_name, _time, text = event.strip.split(nil, 4)
9
+ Event.new(type(event_name), finger(text), *direction(text))
10
+ end
11
+
12
+ private
13
+
14
+ def direction(text)
15
+ return if text.nil?
16
+
17
+ _finger, _finger2, x, y = text.tr('/|(|)', ' ').split
18
+ [x, y]
19
+ end
20
+
21
+ def type(text)
22
+ text.split('_').last
23
+ end
24
+
25
+ def finger(text)
26
+ return if text.nil?
27
+
28
+ count, = text.split(nil, 3)
29
+ count.to_i + 1
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TouchscreenTaps
4
+ # Executes commands from config file
5
+ class Executor
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def run(cmd)
11
+ return if cmd.nil?
12
+
13
+ command = @config.by_keys(command_to_keys(cmd)).fetch('command')
14
+ execute(command) if command
15
+ end
16
+
17
+ private
18
+
19
+ def execute(command)
20
+ puts "Executing '#{command}'"
21
+ fork do
22
+ Process.daemon(true)
23
+ exec(command)
24
+ end
25
+ end
26
+
27
+ def command_to_keys(cmd)
28
+ [cmd[:name], cmd[:fingers]]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TouchscreenTaps
4
+ # Buffer for all events related to a single gesture
5
+ # Gesture is considered started when first finger touches the screen
6
+ # Gesture is considired ended when last finger leaves the screen
7
+ # Between start and end points at least one finger
8
+ # should be touching the screen
9
+ class GestureBuffer
10
+ TIME_TO_KEEP = 3 # seconds
11
+ UP_TYPE = 'UP'
12
+ DOWN_TYPE = 'DOWN'
13
+ MOTION_TYPE = 'MOTION'
14
+ attr_reader :events
15
+
16
+ def initialize
17
+ @events = []
18
+ end
19
+
20
+ def push(event)
21
+ delete_old_events
22
+ clear unless gesture_in_progress?
23
+ remove_previous_events(event) if repeating_touch?(event)
24
+
25
+ @events.push(event)
26
+ end
27
+
28
+ def empty?
29
+ @events.empty?
30
+ end
31
+
32
+ def gesture_in_progress?
33
+ down_touches.count > up_touches.count
34
+ end
35
+
36
+ def down_touches
37
+ events_by_type(DOWN_TYPE)
38
+ end
39
+
40
+ def up_touches
41
+ events_by_type(UP_TYPE)
42
+ end
43
+
44
+ def motion_touches
45
+ events_by_type(MOTION_TYPE)
46
+ end
47
+
48
+ private
49
+
50
+ def delete_old_events
51
+ @events.each do |event|
52
+ next if (current_time - event.timestamp) < TIME_TO_KEEP
53
+
54
+ @events.delete(event)
55
+ end
56
+ end
57
+
58
+ def clear
59
+ @events.clear
60
+ end
61
+
62
+ def current_time
63
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
64
+ end
65
+
66
+ def repeating_touch?(event)
67
+ return false unless event.type == DOWN_TYPE
68
+
69
+ !@events.find { |e| e.finger == event.finger }.nil?
70
+ end
71
+
72
+ def remove_previous_events(event)
73
+ @events.select { |e| e.finger == event.finger }.each do |e|
74
+ @events.delete(e)
75
+ end
76
+ @events.delete(@events.find { |e| e.type == UP_TYPE })
77
+ end
78
+
79
+ def events_by_type(type)
80
+ @events.select { |e| e.type == type }
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TouchscreenTaps
4
+ # libinput wrapper
5
+ # makes debug_events available in ruby
6
+ class Libinput
7
+ require 'open3'
8
+
9
+ class << self
10
+ def events(device = nil)
11
+ Open3.popen3(debug_events_cmd(device)) do |_in, out, _err, _wait_thr|
12
+ out.each do |event|
13
+ yield(event.chomp)
14
+ end
15
+ end
16
+ end
17
+
18
+ def devices
19
+ Open3.popen3(list_devices_cmd) do |_in, out, _err, _wait_thr|
20
+ out.read.split("\n\n").map { |d| d.split("\n") }
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def debug_events_cmd(device)
27
+ 'stdbuf -oL -- libinput debug-events' \
28
+ "#{' --device=' + device.kernel unless device.nil?}"
29
+ end
30
+
31
+ def list_devices_cmd
32
+ 'libinput list-devices'
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TouchscreenTaps
4
+ # Detects tap-and-hold gesture
5
+ class TapAndHoldDetector
6
+ COMMAND_TYPE = 'tap_and_hold'
7
+
8
+ def initialize(config)
9
+ @config = config
10
+ end
11
+
12
+ def process(buffer)
13
+ return if return_conditions_met?(buffer)
14
+
15
+ down_touches = buffer.down_touches
16
+ up_touches = buffer.up_touches
17
+
18
+ last_down_touch = down_touches.max_by(&:timestamp)
19
+ last_up_touch = up_touches.max_by(&:timestamp)
20
+
21
+ if hold_enough?(last_up_touch, last_down_touch)
22
+ return { name: COMMAND_TYPE, fingers: down_touches.count }
23
+ end
24
+
25
+ nil
26
+ end
27
+
28
+ private
29
+
30
+ def hold_enough?(up_touch, down_touch)
31
+ return false if up_touch.nil? || down_touch.nil?
32
+
33
+ required_delay = acceptance_delay(down_touch)
34
+ return false if required_delay.nil?
35
+
36
+ up_touch.timestamp - down_touch.timestamp > required_delay.to_f
37
+ end
38
+
39
+ def return_conditions_met?(buffer)
40
+ buffer.empty? ||
41
+ buffer.gesture_in_progress? ||
42
+ buffer.motion_touches.any?
43
+ end
44
+
45
+ def acceptance_delay(down_touch)
46
+ @config.by_keys([COMMAND_TYPE, down_touch.finger])
47
+ &.fetch('acceptance_delay')
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TouchscreenTaps
4
+ # represents a physical input device (e.g touchscreen)
5
+ class TouchscreenDevice
6
+ REQUIRED_CAPABILITY = 'touch'
7
+ attr_reader :kernel
8
+
9
+ def initialize(kernel)
10
+ @kernel = kernel
11
+ end
12
+
13
+ class << self
14
+ def select_from(devices)
15
+ devices.map do |device|
16
+ next unless capable?(device)
17
+
18
+ kernel = kernel_path(device)
19
+ new(kernel) unless kernel.nil?
20
+ end.compact
21
+ end
22
+
23
+ private
24
+
25
+ def capable?(device)
26
+ device.find { |f| f =~ /^Capabilities:/ }
27
+ .include?(REQUIRED_CAPABILITY)
28
+ end
29
+
30
+ def kernel_path(device)
31
+ device.find { |f| f =~ /^Kernel:/ }.split(' ').last
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TouchscreenTaps
4
+ VERSION = '0.1.1'
5
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/touchscreen_taps/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'touchscreen-taps'
7
+ spec.version = TouchscreenTaps::VERSION
8
+ spec.authors = ['Anton Sokolskyi']
9
+ spec.email = ['antonsokolskyi@gmail.com']
10
+
11
+ spec.summary = 'Touchscreen taps recognizer'
12
+ spec.description = 'Allows to recognize tap-and-hold gesture'
13
+ spec.homepage = 'https://github.com/antonsokolskyy/touchscreen-taps'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/antonsokolskyy/touchscreen-taps'
19
+ spec.metadata['changelog_uri'] = 'https://github.com/antonsokolskyy/touchscreen-taps/blob/master/CHANGELOG.md'
20
+
21
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ f.match(%r{^(test|spec|features)/})
24
+ end
25
+ end
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: touchscreen-taps
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Anton Sokolskyi
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-02-21 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Allows to recognize tap-and-hold gesture
14
+ email:
15
+ - antonsokolskyi@gmail.com
16
+ executables:
17
+ - touchscreen-taps
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - bin/console
27
+ - bin/setup
28
+ - exe/touchscreen-taps
29
+ - lib/touchscreen_taps.rb
30
+ - lib/touchscreen_taps/config.rb
31
+ - lib/touchscreen_taps/default_config.yml
32
+ - lib/touchscreen_taps/event.rb
33
+ - lib/touchscreen_taps/event_filter.rb
34
+ - lib/touchscreen_taps/event_parser.rb
35
+ - lib/touchscreen_taps/executor.rb
36
+ - lib/touchscreen_taps/gesture_buffer.rb
37
+ - lib/touchscreen_taps/libinput.rb
38
+ - lib/touchscreen_taps/tap_and_hold_detector.rb
39
+ - lib/touchscreen_taps/touchscreen_device.rb
40
+ - lib/touchscreen_taps/version.rb
41
+ - touchscreen-taps.gemspec
42
+ homepage: https://github.com/antonsokolskyy/touchscreen-taps
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ homepage_uri: https://github.com/antonsokolskyy/touchscreen-taps
47
+ source_code_uri: https://github.com/antonsokolskyy/touchscreen-taps
48
+ changelog_uri: https://github.com/antonsokolskyy/touchscreen-taps/blob/master/CHANGELOG.md
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 2.3.0
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.0.3
65
+ signing_key:
66
+ specification_version: 4
67
+ summary: Touchscreen taps recognizer
68
+ test_files: []