consular-terminator 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.
data/README.rdoc ADDED
@@ -0,0 +1,13 @@
1
+ = Terminator support for Consular
2
+
3
+ This gem is an add-on to Consular that implements support for the Terminator
4
+ terminal emulator on Linux.
5
+
6
+ Since Terminator does not have an API for controlling it, a commandline tool
7
+ called `xdotool` is used to send keyboard shortcuts to a running Terminator
8
+ in order to control it.
9
+
10
+ == Copyright
11
+
12
+ Copyright (c) 2011 Ilkka Laukkanen. See LICENSE.txt for further details.
13
+
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ require 'rspec/core'
6
+ require 'rspec/core/rake_task'
7
+ RSpec::Core::RakeTask.new(:spec) do |t|
8
+ t.pattern = 'spec/**/*_spec.rb'
9
+ end
10
+
11
+ RSpec::Core::RakeTask.new(:rcov) do |t|
12
+ t.pattern = 'spec/**/*_spec.rb'
13
+ t.rcov = true
14
+ end
15
+
16
+ require 'cucumber/rake/task'
17
+ Cucumber::Rake::Task.new(:features)
18
+
19
+ require 'yard'
20
+ YARD::Rake::YardocTask.new
21
+
22
+ task :default => [:features, :spec]
23
+
@@ -0,0 +1,50 @@
1
+ # Terminator support for Consular
2
+ # Copyright (C) 2011 Ilkka Laukkanen
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ $:.push File.expand_path("../lib", __FILE__)
18
+ require 'consular/terminator/version'
19
+
20
+ Gem::Specification.new do |s|
21
+ s.name = 'consular-terminator'
22
+ s.version = Consular::Terminator::VERSION
23
+ s.authors = ['Ilkka Laukkanen']
24
+ s.email = [%q{ilkka.s.laukkanen@gmail.com}]
25
+ s.homepage = %q{http://github.com/ilkka/consular-terminator}
26
+ s.summary = %q{Terminator support for Consular}
27
+ s.licenses = ["GPLv3"]
28
+ s.description =
29
+ %q{Add support for automating the Terminator terminal with Consular.}
30
+
31
+ s.files = `git ls-files`.split("\n")
32
+ s.test_files = `git ls-files -- test/* spec/* features/*`.split("\n")
33
+
34
+ s.extra_rdoc_files = [
35
+ 'LICENSE.txt',
36
+ 'README.rdoc'
37
+ ]
38
+ s.require_paths = ["lib"]
39
+
40
+ s.add_runtime_dependency('consular')
41
+
42
+ s.add_development_dependency('rake')
43
+ s.add_development_dependency('rspec')
44
+ s.add_development_dependency('yard')
45
+ s.add_development_dependency('cucumber')
46
+ s.add_development_dependency('spork')
47
+ s.add_development_dependency('watchr')
48
+ s.add_development_dependency('bundler')
49
+ s.add_development_dependency('mocha')
50
+ end
data/cucumber.yml ADDED
@@ -0,0 +1 @@
1
+ default: --color --drb --port 8990
@@ -0,0 +1,18 @@
1
+ require 'spork'
2
+
3
+ Spork.prefork do
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+
13
+ require 'rspec/expectations'
14
+ end
15
+
16
+ Spork.each_run do
17
+
18
+ end
@@ -0,0 +1,16 @@
1
+ require 'consular'
2
+
3
+ module Consular
4
+ class Terminator < Core
5
+ module Version
6
+ MAJOR = 0
7
+ MINOR = 1
8
+ PATCH = 0
9
+ BUILD = nil
10
+ end
11
+
12
+ VERSION = [
13
+ Version::MAJOR, Version::MINOR, Version::PATCH, Version::BUILD
14
+ ].compact.join('.')
15
+ end
16
+ end
@@ -0,0 +1,260 @@
1
+ require 'consular'
2
+ require 'rbconfig'
3
+
4
+ module Consular
5
+ # Consular core for interacting with Terminator.
6
+ #
7
+ # Since we don't have any real API to work with and just send
8
+ # keypresses via XTEST, we don't have tab references either.
9
+ # Instead we just count tabs and return tab indexes.
10
+ #
11
+ # Largely adapted from http://github.com/achiu/consular-osx
12
+ class Terminator < Core
13
+
14
+ Consular.add_core self
15
+
16
+ class << self
17
+ # Check to see if current system is valid for this core
18
+ #
19
+ # @api public
20
+ def valid_system?
21
+ if (RbConfig::CONFIG['host_os'] =~ /linux/) != nil
22
+ if !(xdotool = `which xdotool`.chomp).empty?
23
+ begin
24
+ return File::Stat.new(xdotool).executable?
25
+ rescue
26
+ return false
27
+ end
28
+ end
29
+ end
30
+ return false
31
+ end
32
+ end
33
+
34
+ # Initialize
35
+ #
36
+ # @param [String] path
37
+ # path to Termfile.
38
+ #
39
+ # @api public
40
+ def initialize(path)
41
+ super
42
+ @tabidx = nil
43
+ end
44
+
45
+ # Method called by runner to Execute Termfile setup.
46
+ #
47
+ # @api public
48
+ def setup!
49
+ @termfile[:setup].each { |cmd| execute_command(cmd, :in => active_window) }
50
+ end
51
+
52
+ # Method called by runner to execute Termfile.
53
+ #
54
+ # @api public
55
+ def process!
56
+ windows = @termfile[:windows]
57
+ default = windows.delete('default')
58
+ execute_window(default, :default => true) unless default[:tabs].empty?
59
+ windows.each_pair { |_, cont| execute_window(cont) }
60
+ end
61
+
62
+ # Executes the commands for each designated window.
63
+ # .run_windows will iterate through each of the tabs in
64
+ # sorted order to execute the tabs in the order they were set.
65
+ # The logic follows this:
66
+ #
67
+ # If the content is for the 'default' window,
68
+ # then use the current active window and generate the commands.
69
+ #
70
+ # If the content is for a new window,
71
+ # then generate a new window and activate the windows.
72
+ #
73
+ # Otherwise, open a new tab and execute the commands.
74
+ #
75
+ # @param [Hash] content
76
+ # The hash contents of the window from the Termfile.
77
+ # @param [Hash] options
78
+ # Addional options to pass. You can use:
79
+ # :default - Whether this is being run as the default window.
80
+ #
81
+ # @example
82
+ # @core.execute_window contents, :default => true
83
+ # @core.execute_window contents, :default => true
84
+ #
85
+ # @api public
86
+ def execute_window(content, options = {})
87
+ window_options = content[:options]
88
+ _contents = content[:tabs]
89
+ _first_run = true
90
+
91
+ _contents.keys.sort.each do |key|
92
+ _content = _contents[key]
93
+ _options = content[:options]
94
+ _name = options[:name]
95
+
96
+ _tab =
97
+ if _first_run && !options[:default]
98
+ open_window options.merge(window_options)
99
+ else
100
+ key == 'default' ? nil : open_tab(_options)
101
+ end
102
+
103
+ _first_run = false
104
+ commands = prepend_befores _content[:commands], _contents[:befores]
105
+ commands = set_title _name, commands
106
+ commands.each { |cmd| execute_command cmd, :in => _tab }
107
+ end
108
+
109
+ end
110
+
111
+ # Prepend a title setting command prior to the other commands.
112
+ #
113
+ # @param [String] title
114
+ # The title to set for the context of the commands.
115
+ # @param [Array<String>] commands
116
+ # The context of commands to preprend to.
117
+ #
118
+ # @api public
119
+ def set_title(title, commands)
120
+ cmd = "PS1=\"$PS1\\[\\e]2;#{title}\\a\\]\""
121
+ title ? commands.insert(0, cmd) : commands
122
+ end
123
+
124
+ # Prepends the :before commands to the current context's
125
+ # commands if it exists.
126
+ #
127
+ # @param [Array<String>] commands
128
+ # The current tab commands
129
+ # @param [Array<String>] befores
130
+ # The current window's :befores
131
+ #
132
+ # @return [Array<String>]
133
+ # The current context commands with the :before commands prepended
134
+ #
135
+ # @api public
136
+ def prepend_befores(commands, befores = nil)
137
+ unless befores.nil? || befores.empty?
138
+ commands.insert(0, befores).flatten!
139
+ else
140
+ commands
141
+ end
142
+ end
143
+
144
+ # Execute the given command in the context of the
145
+ # active window.
146
+ #
147
+ # @param [String] cmd
148
+ # The command to execute.
149
+ # @param [Hash] options
150
+ # Additional options to pass into appscript for the context.
151
+ #
152
+ # @example
153
+ # @osx.execute_command 'ps aux', :in => @tab_object
154
+ #
155
+ # @api public
156
+ def execute_command(cmd, options = {})
157
+ run_in_active_terminator cmd, options
158
+ end
159
+
160
+ # Opens a new tab and return the last instantiated tab(itself).
161
+ #
162
+ # @param [Hash] options
163
+ # Options to further customize the window. You can use:
164
+ # :settings -
165
+ # :selected -
166
+ #
167
+ # @return
168
+ # Returns a refernce to the last instantiated tab of the
169
+ # window.
170
+ #
171
+ # @api public
172
+ def open_tab(options = nil)
173
+ send_keypress(active_terminator_window, "ctrl+shift+t")
174
+ # FIXME: horribly hacky but can't come up with any way
175
+ # to truly make sure tab has opened.
176
+ sleep 1
177
+ end
178
+
179
+ # Opens a new window and returns its
180
+ # last instantiated tab.(The first 'tab' in the window).
181
+ #
182
+ # @param [Hash] options
183
+ # Options to further customize the window. You can use:
184
+ # :bound - Set the bounds of the windows
185
+ # :visible - Set whether or not the current window is visible
186
+ # :miniaturized - Set whether or not the window is minimized
187
+ #
188
+ # @return
189
+ # Returns a refernce to the last instantiated tab of the
190
+ # window.
191
+ #
192
+ # @api public
193
+ def open_window(options = nil)
194
+ windowid_before = active_terminator_window
195
+ send_keypress(active_terminator_window, "ctrl+shift+i")
196
+ # wait for the active window to change -> new window opened
197
+ while active_terminator_window == windowid_before
198
+ sleep 1
199
+ end
200
+ return (@tabidx = 0)
201
+ end
202
+
203
+ private
204
+
205
+ # Send keypresses to window winid
206
+ #
207
+ # @api private
208
+ def send_keypress(winid, keys)
209
+ xdotool("windowfocus #{winid}")
210
+ xdotool("key #{keys}")
211
+ end
212
+
213
+ # Run command with active terminator
214
+ #
215
+ # @api private
216
+ def run_in_active_terminator(cmd, options = {})
217
+ type_in_window(active_terminator_window, "#{cmd}\n")
218
+ end
219
+
220
+ # Get active terminator window winid.
221
+ # Gets the active window and returns it if it is
222
+ # a Terminator window. If it is not a terminator window,
223
+ # the method gets all terminator windows and returns the
224
+ # first one. If no terminator windows exist, aborts.
225
+ #
226
+ # @api private
227
+ def active_terminator_window
228
+ active = xdotool("getactivewindow").chomp
229
+ if not all_terminator_windows.include? active
230
+ active = all_terminator_windows.first
231
+ end
232
+ if not active
233
+ abort("No Terminator windows found")
234
+ end
235
+ return active
236
+ end
237
+
238
+ # Get window IDs of all terminator windows.
239
+ #
240
+ # @api private
241
+ def all_terminator_windows
242
+ xdotool("search --onlyvisible --class terminator").split("\n")
243
+ end
244
+
245
+ # Type text in window winid.
246
+ #
247
+ # @api private
248
+ def type_in_window(winid, text)
249
+ xdotool("windowfocus #{winid}")
250
+ xdotool("type \"#{text}\"")
251
+ end
252
+
253
+ # Execute xdotool with the given args and return output
254
+ #
255
+ # @api private
256
+ def xdotool(cmd)
257
+ IO.popen("xdotool #{cmd}").read
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,30 @@
1
+ require 'spork'
2
+
3
+ Spork.prefork do
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
6
+ require 'rspec'
7
+ require 'mocha'
8
+
9
+ # Requires supporting files with custom matchers and macros, etc,
10
+ # in ./support/ and its subdirectories.
11
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
12
+
13
+ RSpec.configure do |config|
14
+ end
15
+
16
+ require 'tempfile'
17
+
18
+ class EmptyTermfile < Tempfile
19
+ def initialize
20
+ super('consular')
21
+ write(%q(setup "echo 'setup'"))
22
+ rewind
23
+ end
24
+ end
25
+ end
26
+
27
+ Spork.each_run do
28
+ require 'consular/terminator'
29
+ end
30
+
@@ -0,0 +1,56 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require 'rbconfig'
3
+
4
+ describe Consular::Terminator do
5
+ it 'should be added as a core to Consular' do
6
+ Consular.cores.should include Consular::Terminator
7
+ end
8
+
9
+ it 'should treat linux as a valid system' do
10
+ RbConfig::CONFIG.expects(:[]).with('host_os').returns('x86-linux')
11
+ Consular::Terminator.expects(:`).with('which xdotool').returns('/usr/bin/xdotool')
12
+ stat = mock()
13
+ stat.expects(:executable?).returns(true)
14
+ File::Stat.expects(:new).returns(stat)
15
+ Consular::Terminator.valid_system?.should == true
16
+ end
17
+
18
+ it 'should treat non-linux as an invalid system' do
19
+ RbConfig::CONFIG.expects(:[]).with('host_os').returns('darwin')
20
+ Consular::Terminator.valid_system?.should == false
21
+ end
22
+
23
+ it 'should treat systems without "which" as invalid' do
24
+ RbConfig::CONFIG.expects(:[]).with('host_os').returns('x86-linux')
25
+ Consular::Terminator.expects(:`).with('which xdotool').returns('which: command not found')
26
+ File::Stat.expects(:new).throws('no such file')
27
+ Consular::Terminator.valid_system?.should == false
28
+ end
29
+
30
+ it 'should treat systems without "xdotool" as invalid' do
31
+ RbConfig::CONFIG.expects(:[]).with('host_os').returns('x86-linux')
32
+ Consular::Terminator.expects(:`).with('which xdotool').returns('')
33
+ File::Stat.expects(:new).throws('no such file')
34
+ Consular::Terminator.valid_system?.should == false
35
+ end
36
+
37
+ it 'should send ctrl+shift+i when creating new window' do
38
+ f = EmptyTermfile.new
39
+ core = Consular::Terminator.new f.path
40
+ core.expects(:active_terminator_window).once.returns(1)
41
+ core.expects(:active_terminator_window).once.returns(1)
42
+ core.expects(:xdotool).with("windowfocus 1")
43
+ core.expects(:xdotool).with("key ctrl+shift+i")
44
+ core.expects(:active_terminator_window).once.returns(2)
45
+ core.open_window
46
+ end
47
+
48
+ it 'should send ctrl+shift+t when creating new tab' do
49
+ f = EmptyTermfile.new
50
+ core = Consular::Terminator.new f.path
51
+ core.expects(:active_terminator_window).returns(1)
52
+ core.expects(:xdotool).with("windowfocus 1")
53
+ core.expects(:xdotool).with("key ctrl+shift+t")
54
+ core.open_tab
55
+ end
56
+ end
data/tests.watchr ADDED
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env watchr
2
+ # vim:ft=ruby
3
+
4
+ def have_notify_send?
5
+ case `which notify-send`.empty?
6
+ when true
7
+ false
8
+ else
9
+ true
10
+ end
11
+ end
12
+
13
+ def have_growl?
14
+ @have_growl ||= begin
15
+ require 'growl'
16
+ rescue LoadError
17
+ false
18
+ end
19
+ end
20
+
21
+ def error_icon_name
22
+ "gtk-dialog-error"
23
+ end
24
+
25
+ def success_icon_name
26
+ "gtk-dialog-info"
27
+ end
28
+
29
+ # Rules
30
+ watch('^spec/.+_spec\.rb$') { |md| spec md[0] }
31
+ watch('^lib/.+\.rb$') { |md| spec "spec/#{File.basename(md[0]).gsub(/\..*?$/, '')}_spec.rb" }
32
+ watch('^features/.+\.feature$') { |md| feature md[0] }
33
+ watch('^features/step_definitions/(.+)_steps\.rb$') { |md| feature "features/#{md[1]}.feature" }
34
+
35
+ # Notify using notify-send.
36
+ #
37
+ # @param icon [String] name of stock icon to use.
38
+ # @param title [String] title of notification.
39
+ # @param message [String] message for notification body.
40
+ # @return [Boolean] true if the command ran successfully, false
41
+ # otherwise.
42
+ def notify(icon, title, message)
43
+ system("notify-send -t 3000 -i #{icon} \"#{title}\" \"#{message}\"")
44
+ end
45
+
46
+ # Notify of success.
47
+ #
48
+ def notify_success
49
+ puts "Success"
50
+ if have_notify_send?
51
+ notify success_icon_name, "All green!", "Now write more tests :)"
52
+ elsif have_growl?
53
+ Growl.notify_ok "All green!"
54
+ end
55
+ end
56
+
57
+ # Notify of failure.
58
+ #
59
+ def notify_failure
60
+ puts "Failure"
61
+ if have_notify_send?
62
+ notify error_icon_name, "Something is broken", "Now go fix it :)"
63
+ elsif have_growl?
64
+ Growl.notify_error "Something is broken"
65
+ end
66
+ end
67
+
68
+ # Run a single ruby command. Notify appropriately.
69
+ #
70
+ # @param cmd [String] command to run.
71
+ def run(cmd)
72
+ system('clear')
73
+ puts "Running #{cmd}"
74
+ if system(cmd)
75
+ notify_success
76
+ true
77
+ else
78
+ notify_failure
79
+ false
80
+ end
81
+ end
82
+
83
+ # Run a single spec.
84
+ #
85
+ # @param specfiles [String] path to specfile or [Array] of paths.
86
+ def spec(specfiles)
87
+ specs = case specfiles.respond_to? :join
88
+ when true
89
+ specfiles.join(" ")
90
+ when false
91
+ specfiles
92
+ end
93
+ if run(%Q(rspec #{specs}))
94
+ if @last_run_failed
95
+ @last_run_failed = false
96
+ run_all_specs
97
+ end
98
+ else
99
+ @last_run_failed = true
100
+ end
101
+ !@last_run_failed
102
+ end
103
+
104
+ # Run a single feature.
105
+ #
106
+ # @param featurefiles [String] path to feature file or [Array] of paths.
107
+ def feature(featurefiles)
108
+ features = case featurefiles.respond_to? :join
109
+ when true
110
+ featurefiles.join(" ")
111
+ when false
112
+ featurefiles
113
+ end
114
+ run(%Q(cucumber #{features}))
115
+ end
116
+
117
+ # Run all specs.
118
+ #
119
+ def run_all_specs
120
+ spec Dir['spec/*_spec.rb']
121
+ end
122
+
123
+ # Run all features.
124
+ #
125
+ def run_features
126
+ feature Dir['features/*.feature']
127
+ end
128
+
129
+ # Run specs and features.
130
+ #
131
+ def run_suite
132
+ run_all_specs && run_features
133
+ end
134
+
135
+ # Run all specs on Ctrl-\
136
+ Signal.trap('QUIT') { run_all_specs }
137
+
138
+ # Run full suite on one Ctrl-C, quit on two
139
+ @interrupted = false
140
+ Signal.trap('INT') do
141
+ if @interrupted
142
+ abort("\n")
143
+ else
144
+ puts "Interrupt a second time to quit"
145
+ @interrupted = true
146
+ Kernel.sleep 1.5
147
+ run_suite
148
+ @interrupted = false
149
+ end
150
+ end
151
+