global_hotkeys_manager 0.1.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e210552ed9e7cdb2861b2176b6dcd338e4239bac
4
+ data.tar.gz: 638412ee12c65e40ff94a5ff3a2e501c83addd69
5
+ SHA512:
6
+ metadata.gz: 690284184197e08a9ab0c2d7eeb8c7dbe88ef42e7c05797407f4d9a583f44d868cefb034b4b872837baa1ac467b87a994d4e63e96959b64f8fc8b510f8b7f20f
7
+ data.tar.gz: b4ab0a3a36023d641e883cacb1803d0de934ae7f451bccafeca6fe23ba5579705d17f952c9ae89c31bf829056d37b9d831fd3389caae2c3f540675ac7fe7e897
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in global_hotkeys_manager.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Rodrigo Rosenfeld Rosas
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.
@@ -0,0 +1,65 @@
1
+ # Global Hotkeys Manager
2
+
3
+ Global hotkeys management for Linux/X11 environments.
4
+
5
+ ## About
6
+
7
+ I like to toggle the visibility of some common apps through global shortcuts/hotkeys, like the
8
+ browser, terminal emulator, code editor, instant message client and so on. Too much open windows
9
+ in my desktop feel like a mess to me and it's hard to switch among many of them by using Alt+Tab.
10
+
11
+ Tools like kdocker allow us to send any window to the tray, but I wasn't able to find any tool
12
+ that would allow me to assign the windows some shortcuts so that I wouldn't need the mouse to
13
+ switch among them. I created [Ktrayshortcut](https://github.com/rosenfeld/ktrayshortcut) a while
14
+ back to allow me to assign global hotkeys in addition to sending them to the tray. But after KDE
15
+ moved from version 4 to KF5 lots of API changes made it hard to update the source-code as many
16
+ things got much more complicated. I wasn't comfortable with having to spend too much time updating
17
+ the source code everytime the API changed so today I was experimenting with some command line
18
+ tools and decided to take another approach using tools I'm comfortable with, like Ruby, web
19
+ development and command line tools.
20
+
21
+ So, after a few hours I was able to put the pieces together and create a simple Sinatra web
22
+ application with little of Ruby and plain JavaScript to get what I need done. After using
23
+ Ktrayshorcut for a long time I realized I didn't really need any tray icons, just the hotkeys to
24
+ toggle the applications, so this program does just that. It's simple to create and simple to
25
+ maintain as long the required tools don't change too much.
26
+
27
+ I thought about using Shoes to create the UI but I didn't like to add another big dependency
28
+ (Java) and I'm not aware of any other GUI tool supporting Ruby that is lightweight, well
29
+ documented and easy to use, so I decided to create a web app since I'm comfortable with maintaining
30
+ them.
31
+
32
+ ## Requirements
33
+
34
+ - xdotool
35
+ - xbindkeys
36
+ - xwininfo (from x11-utils package)
37
+ - Ruby
38
+ - sinatra and sinatra-contrib (installed from bundle)
39
+
40
+ sudo apt-get install xdotool x11-utils xbindkeys ruby
41
+ gem install global_hotkeys_manager
42
+
43
+ ## Usage
44
+
45
+ global_hotkeys_manager (simply start the daemon)
46
+
47
+ Other commands:
48
+
49
+ global_hotkeys_manager help
50
+ global_hotkeys_manager status
51
+ global_hotkeys_manager stop
52
+ global_hotkeys_manager toggle window_id (used internally)
53
+ global_hotkeys_manager debug (like start but not in daemon mode)
54
+
55
+ Access the UI through http://127.0.0.1:4242
56
+
57
+ I also found useful to assign a global shortcut for accessing this address in Chrome any time I
58
+ want to add a new window. Just add something like this to your ~/.xbindkeysrc:
59
+
60
+ "google-chrome --app=http://127.0.0.1:4242 && sleep 0.2 && xdotool search "Global Hotkeys Manager" windowactivate "
61
+ m:0x19 + c:58
62
+ Shift+Alt+Mod2 + m
63
+
64
+ All hotkeys managed by this application uses a separate configuration file rather than
65
+ ~/.xbindkeysrc (~/.config/global-hotkeys-manager/xbindkeysrc).
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "global_hotkeys_manager"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -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,62 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'global_hotkeys_manager'
4
+ require 'global_hotkeys_manager/web_app'
5
+
6
+ unless ['xbindkeys', 'xwininfo', 'xdotool'].all?{|app| system "which #{app} > /dev/null"}
7
+ puts "The following tools are required to be installed for this app to work:
8
+ - xbindkeys
9
+ - xwininfo (from x11-utils package)
10
+ - xdotool"
11
+ exit -1
12
+ end
13
+
14
+ PID_FILE = "#{GlobalHotkeysManager::CONFIG_DIR}/server.pid"
15
+
16
+ _pid = nil
17
+ def pid
18
+ _pid ||= Integer(File.read PID_FILE)
19
+ end
20
+
21
+ def running?
22
+ return false unless File.exist? PID_FILE
23
+ Process.kill 0, pid
24
+ true
25
+ rescue => e
26
+ puts "invalid PID: #{e.message} (deleted pid file)"
27
+ File.delete PID_FILE
28
+ false
29
+ end
30
+
31
+ def stop
32
+ GlobalHotkeysManager.stop
33
+ exit 0 unless running?
34
+ Process.kill 'SIGINT', pid
35
+ end
36
+
37
+ def start
38
+ exit 0 if running?
39
+ Process.daemon unless ARGV[0] == 'debug'
40
+ File.write PID_FILE, Process.pid
41
+ at_exit{ File.delete PID_FILE }
42
+ GlobalHotkeysManager.ensure_running
43
+ GlobalHotkeysManager::WebApp.run! environment: 'production'
44
+ end
45
+
46
+ case ARGV[0]
47
+ when 'help'
48
+ puts %q{running without arguments will start the application daemons. Available commands:
49
+ status - report current status
50
+ stop - stop the daemon
51
+ toggle window_id - used internally
52
+ debug - start in no daemon mode
53
+ The default port is 4242 and can be overriden with the environment variable
54
+ HOTKEYS_MANAGER_PORT. Eg.: HOTKEYS_MANAGER_PORT=4444 global_hotkeys_manager}
55
+ when 'toggle' then GlobalHotkeysManager.toggle Integer(ARGV[1])
56
+ when 'status' then puts running? ? 'running' : 'stopped'
57
+ when 'stop' then stop
58
+ when 'restart' then (stop; sleep 1; start)
59
+ when nil, 'debug', 'start' then start
60
+ else
61
+ puts "unknown command: #{ARGV[0]}"
62
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'global_hotkeys_manager/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'global_hotkeys_manager'
8
+ spec.version = GlobalHotkeysManager::VERSION
9
+ spec.authors = ['Rodrigo Rosenfeld Rosas']
10
+ spec.email = ['rr.rosas@gmail.com']
11
+
12
+ spec.summary = %q{Global Hotkeys Management for Linux/X11 with a web interface}
13
+ spec.description = %q{Integrates with command line tools like xbindkeys and xdotool to assign global hotkeys to toggle the visibility of any captured window}
14
+ spec.homepage = 'https://github.com/rosenfeld/hotkeys-manager'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.bindir = 'exe'
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_dependency 'sinatra', '1.4.7'
23
+ spec.add_dependency 'sinatra-contrib', '1.4.7'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.11'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ end
@@ -0,0 +1,115 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+
4
+ require_relative 'global_hotkeys_manager/version'
5
+
6
+ module GlobalHotkeysManager
7
+ CONFIG_DIR = File.expand_path '~/.config/global-hotkeys-manager'
8
+ XBINDKEYSRC = "#{CONFIG_DIR}/xbindkeysrc"
9
+ HOTKEYS_JSON = "#{CONFIG_DIR}/hotkeys.json"
10
+ PORT = ENV['HOTKEYS_MANAGER_PORT'] || 4242
11
+ HOST = '127.0.0.1'
12
+ URL_BASE = "http://#{HOST}:#{PORT}"
13
+
14
+ FileUtils.mkdir_p CONFIG_DIR
15
+
16
+ def self.pid
17
+ return unless pid = `pgrep -o -f xbindkeys.*hotkeys`.split("\n").find{|p| system "kill -0 #{p}"}
18
+ system("kill -0 #{pid}") and pid.to_i
19
+ end
20
+
21
+ def self.ensure_running
22
+ id = pid and `kill #{id}`
23
+ `[ -f #{XBINDKEYSRC} ] && xbindkeys -f #{XBINDKEYSRC}`
24
+ end
25
+
26
+ def self.stop
27
+ pid and `kill #{pid}`
28
+ end
29
+
30
+ def self.hotkeys
31
+ return [] unless File.exist? HOTKEYS_JSON
32
+ JSON.parse(File.read HOTKEYS_JSON).find_all do |(id, name, key, mapped)|
33
+ valid_window_id?(id)
34
+ end
35
+ end
36
+
37
+ def self.valid_window_id?(id)
38
+ system "xwininfo -id #{id} > /dev/null 2>&1"
39
+ end
40
+
41
+ def self.map_all
42
+ hotkeys.each do |(id, name, key, mapped)|
43
+ `xdotool windowmap #{id}` rescue nil
44
+ end
45
+ end
46
+
47
+ def self.toggle(wid)
48
+ new_state = []
49
+ hotkeys.each do |(id, name, key, mapped)|
50
+ if id == wid
51
+ mapped = !mapped
52
+ if mapped
53
+ `xdotool windowmap #{id}`
54
+ `xdotool windowactivate #{id}`
55
+ else
56
+ `xdotool windowunmap #{id}`
57
+ end
58
+ end
59
+ new_state.push [id, name, key, mapped]
60
+ end
61
+ update_config new_state
62
+ end
63
+
64
+ def self.update_config(hotkeys)
65
+ hotkeys = hotkeys.find_all{|(id, name, key, mapped)| valid_window_id? id}
66
+ File.write HOTKEYS_JSON, JSON.unparse(hotkeys)
67
+ File.write XBINDKEYSRC, hotkeys.map{|(id, name, key, mapped)|
68
+ # webrick has a bug and require content-length to be sent
69
+ # using curl is faster but in case it's not installed we fall back to the command line
70
+ %Q{\##{name}\n"curl -H 'Content-Length: 0' -X POST #{URL_BASE}/toggle/#{id} || global_hotkeys_manager toggle #{id}"\n#{key}\n}
71
+ }.join("\n\n")
72
+ ensure_running
73
+ end
74
+
75
+ def self.update_name(wid, new_name)
76
+ new_state = []
77
+ hotkeys.each do |(id, name, key, mapped)|
78
+ name = new_name if id == wid
79
+ new_state.push [id, name, key, mapped]
80
+ end
81
+ update_config new_state
82
+ end
83
+
84
+ def self.update_key(wid)
85
+ new_key = ask_for_key
86
+ new_state = []
87
+ hotkeys.each do |(id, name, key, mapped)|
88
+ key = new_key if id == wid
89
+ new_state.push [id, name, key, mapped]
90
+ end
91
+ update_config new_state
92
+ end
93
+
94
+ def self.grab_window
95
+ wid = `xdotool selectwindow`.to_i
96
+ new_key = ask_for_key
97
+ new_name = `xdotool getwindowname #{wid}`
98
+ new_state = []
99
+ updated = false
100
+ hotkeys.each do |(id, name, key, mapped)|
101
+ if id == wid
102
+ updated = true
103
+ key = new_key
104
+ end
105
+ new_state.push [id, name, key, mapped]
106
+ end
107
+ new_state.push [wid, new_name, new_key, true] unless updated
108
+ update_config new_state
109
+ end
110
+
111
+ def self.ask_for_key
112
+ `xbindkeys -k`.split("\n").last
113
+ end
114
+ end
115
+
@@ -0,0 +1,20 @@
1
+ function live(id, type, handler) {
2
+ document.addEventListener(type, function(ev) {
3
+ if (ev.target.id == id || ev.target.className == id) handler.apply(this, arguments);
4
+ });
5
+ }
6
+
7
+ function post(path, data) {
8
+ var xhr = new XMLHttpRequest();
9
+ xhr.open('POST', path, true);
10
+ xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
11
+ xhr.send(data);
12
+ }
13
+
14
+ function idFromEv(ev) { return ev.target.dataset.id }
15
+
16
+ live('js-name', 'change', function(ev) {
17
+ var id = idFromEv(ev);
18
+ post('/update_name/' + id, 'name=' + encodeURIComponent(ev.target.value));
19
+ });
20
+
@@ -0,0 +1,3 @@
1
+ module GlobalHotkeysManager
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,30 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
5
+
6
+ <title>Global Hotkeys Manager</title>
7
+ <style>form {display: inline; }</style>
8
+ </head>
9
+
10
+ <body>
11
+ <div id=list>
12
+ <% hotkeys.each do |(id, name, key, mapped)| %>
13
+ <div>
14
+ <input data-id=<%= id %> type=text class=js-name size=30 value="<%= name %>" />
15
+ <form method=post action="/update_key/<%= id %>">
16
+ <button type=submit >Change key</button>
17
+ </form>
18
+ (<%= key %>)
19
+ </div>
20
+ <% end %>
21
+ </div>
22
+ <div>
23
+ <form method=post action="/capture">
24
+ <button type=submit >Capture new window</button>
25
+ </form>
26
+ </div>
27
+
28
+ <script type="text/javascript" charset="utf-8" src="/index.js"></script>
29
+ </body>
30
+ </html>
@@ -0,0 +1,42 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/reloader'
3
+ require 'tilt/erb'
4
+
5
+ require_relative '../global_hotkeys_manager'
6
+
7
+ module GlobalHotkeysManager
8
+ class WebApp < Sinatra::Base
9
+ configure :development do
10
+ register Sinatra::Reloader
11
+ end
12
+
13
+ configure do
14
+ set :port, GlobalHotkeysManager::PORT
15
+ set :bind, GlobalHotkeysManager::HOST
16
+ end
17
+
18
+ get '/' do
19
+ erb :index, layout: false, locals: { hotkeys: GlobalHotkeysManager.hotkeys }
20
+ end
21
+
22
+ post '/toggle/:id' do
23
+ GlobalHotkeysManager.toggle Integer(params[:id])
24
+ halt 200
25
+ end
26
+
27
+ post '/update_name/:id' do
28
+ GlobalHotkeysManager.update_name Integer(params[:id]), params[:name]
29
+ halt 200
30
+ end
31
+
32
+ post '/update_key/:id' do
33
+ GlobalHotkeysManager.update_key Integer(params[:id])
34
+ redirect '/'
35
+ end
36
+
37
+ post '/capture' do
38
+ GlobalHotkeysManager.grab_window
39
+ redirect '/'
40
+ end
41
+ end
42
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: global_hotkeys_manager
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Rodrigo Rosenfeld Rosas
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-04-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sinatra
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.4.7
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 1.4.7
27
+ - !ruby/object:Gem::Dependency
28
+ name: sinatra-contrib
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 1.4.7
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 1.4.7
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.11'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.11'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ description: Integrates with command line tools like xbindkeys and xdotool to assign
70
+ global hotkeys to toggle the visibility of any captured window
71
+ email:
72
+ - rr.rosas@gmail.com
73
+ executables:
74
+ - global_hotkeys_manager
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".gitignore"
79
+ - Gemfile
80
+ - Gemfile.lock
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - bin/console
85
+ - bin/setup
86
+ - exe/global_hotkeys_manager
87
+ - global_hotkeys_manager.gemspec
88
+ - lib/global_hotkeys_manager.rb
89
+ - lib/global_hotkeys_manager/public/index.js
90
+ - lib/global_hotkeys_manager/version.rb
91
+ - lib/global_hotkeys_manager/views/index.erb
92
+ - lib/global_hotkeys_manager/web_app.rb
93
+ homepage: https://github.com/rosenfeld/hotkeys-manager
94
+ licenses:
95
+ - MIT
96
+ metadata: {}
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 2.5.1
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Global Hotkeys Management for Linux/X11 with a web interface
117
+ test_files: []