global_hotkeys_manager 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []