alloy-kicker 0.1.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +34 -0
- data/README.rdoc +20 -11
- data/Rakefile +10 -1
- data/VERSION.yml +3 -3
- data/bin/kicker +2 -31
- data/kicker.gemspec +2 -2
- data/lib/kicker.rb +140 -0
- data/test/kicker_test.rb +224 -0
- data/test/test_helper.rb +6 -0
- data/vendor/growlnotifier/growl.rb +170 -0
- data/vendor/growlnotifier/growl_helpers.rb +25 -0
- metadata +10 -1
data/LICENSE
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
Kicker:
|
2
|
+
|
1
3
|
Copyright (c) 2009 Eloy Duran <eloy.de.enige@gmail.com>
|
2
4
|
|
3
5
|
Permission is hereby granted, free of charge, to any person obtaining
|
@@ -18,3 +20,35 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
20
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
21
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
22
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
23
|
+
|
24
|
+
======================================================================
|
25
|
+
|
26
|
+
Rucola: http://github.com/alloy/rucola/tree/master
|
27
|
+
|
28
|
+
Copyright (c) 2008 Eloy Duran <eloy.de.enige@gmail.com>
|
29
|
+
|
30
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
31
|
+
a copy of this software and associated documentation files (the
|
32
|
+
"Software"), to deal in the Software without restriction, including
|
33
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
34
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
35
|
+
permit persons to whom the Software is furnished to do so, subject to
|
36
|
+
the following conditions:
|
37
|
+
|
38
|
+
The above copyright notice and this permission notice shall be
|
39
|
+
included in all copies or substantial portions of the Software.
|
40
|
+
|
41
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
42
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
43
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
44
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
45
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
46
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
47
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
48
|
+
|
49
|
+
======================================================================
|
50
|
+
|
51
|
+
growlnotifier: http://github.com/psychs/growlnotifier/tree/master
|
52
|
+
|
53
|
+
Copyright (c) 2007-2008 Satoshi Nakagawa <psychs@limechat.net>, Eloy Duran <e.duran@superalloy.nl>
|
54
|
+
You can redistribute it and/or modify it under the same terms as Ruby.
|
data/README.rdoc
CHANGED
@@ -1,24 +1,33 @@
|
|
1
|
-
=
|
1
|
+
= Kicker
|
2
2
|
|
3
3
|
A simple OS X CLI tool which uses FSEvents to run a given shell command.
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
== Usage
|
6
|
+
|
7
|
+
At the least give it a path to a file or directory and a shell command to
|
8
|
+
execute when any changes occur.
|
9
|
+
|
10
|
+
Usage: kicker [options] -e [command] [path]
|
11
|
+
-e, --execute [COMMAND] The command to execute.
|
12
|
+
--[no-]growl Whether or not to use Growl. Default is to use growl.
|
13
|
+
--growl-message [MESSAGE] The message to Growl when the command succeeded.
|
14
|
+
--growl-command [COMMAND] The command to execute when the Growl succeeded message is clicked.
|
15
|
+
|
16
|
+
== Examples
|
7
17
|
|
8
18
|
Show all files whenever a change occurs in the current work directory:
|
9
19
|
|
10
|
-
$ kicker
|
20
|
+
$ kicker -e "ls -l" .
|
11
21
|
|
12
22
|
Run a Rake task whenever a given file is changed:
|
13
23
|
|
14
|
-
$ kicker
|
24
|
+
$ kicker -e "ONLY=nested_model_forms rake guides" guides/source/nested_model_forms.textile
|
15
25
|
|
16
|
-
|
26
|
+
Run a Run task whenever a given file is changed and specify a command to be
|
27
|
+
executed if the user clicks a `succeeded' Growl message:
|
17
28
|
|
18
|
-
$
|
29
|
+
$ kicker -e "ONLY=nested_model_forms rake guides" --growl-command "open -a Safari guides/output/nested_model_forms.html" guides/source/nested_model_forms.textile
|
19
30
|
|
20
|
-
==
|
31
|
+
== Installation
|
21
32
|
|
22
|
-
|
23
|
-
from Rucola, which is tested there. Once/if this will every be developed
|
24
|
-
further test cases will be added where appropriate.
|
33
|
+
$ sudo gem install alloy-kicker -s http://gems.github.com
|
data/Rakefile
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'rake'
|
3
|
+
require 'rake/testtask'
|
3
4
|
|
4
5
|
begin
|
5
6
|
require 'jeweler'
|
@@ -16,4 +17,12 @@ begin
|
|
16
17
|
end
|
17
18
|
rescue LoadError
|
18
19
|
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
19
|
-
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Rake::TestTask.new do |t|
|
23
|
+
t.libs << "test"
|
24
|
+
t.test_files = FileList['test/*_test.rb']
|
25
|
+
t.options = '-rs'
|
26
|
+
end
|
27
|
+
|
28
|
+
task :default => :test
|
data/VERSION.yml
CHANGED
@@ -1,4 +1,4 @@
|
|
1
1
|
---
|
2
|
-
:
|
3
|
-
:
|
4
|
-
:
|
2
|
+
:major: 1
|
3
|
+
:minor: 0
|
4
|
+
:patch: 0
|
data/bin/kicker
CHANGED
@@ -1,33 +1,4 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require File.expand_path('../../
|
4
|
-
|
5
|
-
path, command = ARGV
|
6
|
-
|
7
|
-
unless path && command
|
8
|
-
puts "Usage: #{$0} [PATH] [COMMAND]"
|
9
|
-
exit
|
10
|
-
else
|
11
|
-
unless File.exist?(path)
|
12
|
-
puts "The given path `#{path}' does not exist."
|
13
|
-
exit 1
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
path = File.expand_path(path)
|
18
|
-
file, path = path, File.dirname(path) unless File.directory?(path)
|
19
|
-
|
20
|
-
puts "Watching for changes on `#{file || path}'"
|
21
|
-
|
22
|
-
stream = Rucola::FSEvents.start_watching(path) do |events|
|
23
|
-
unless file && !events.find { |e| e.last_modified_file == file }
|
24
|
-
system command
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
trap("INT") do
|
29
|
-
stream.stop
|
30
|
-
exit
|
31
|
-
end
|
32
|
-
|
33
|
-
OSX::NSApplication.sharedApplication.run
|
3
|
+
require File.expand_path('../../lib/kicker', __FILE__)
|
4
|
+
Kicker.run!
|
data/kicker.gemspec
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{kicker}
|
5
|
-
s.version = "0.
|
5
|
+
s.version = "1.0.0"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["Eloy Duran"]
|
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
|
|
11
11
|
s.email = %q{eloy.de.enige@gmail.com}
|
12
12
|
s.executables = ["kicker"]
|
13
13
|
s.extra_rdoc_files = ["README.rdoc", "LICENSE"]
|
14
|
-
s.files = ["bin", "bin/kicker", "kicker.gemspec", "LICENSE", "pkg", "pkg/kicker-0.1.0.gem", "Rakefile", "README.rdoc", "vendor", "vendor/rucola", "vendor/rucola/fsevents.rb", "VERSION.yml"]
|
14
|
+
s.files = ["bin", "bin/kicker", "kicker.gemspec", "lib", "lib/kicker.rb", "LICENSE", "pkg", "pkg/kicker-0.1.0.gem", "pkg/kicker-0.1.1.gem", "Rakefile", "README.rdoc", "test", "test/kicker_test.rb", "test/test_helper.rb", "vendor", "vendor/growlnotifier", "vendor/growlnotifier/growl.rb", "vendor/growlnotifier/growl_helpers.rb", "vendor/rucola", "vendor/rucola/fsevents.rb", "VERSION.yml"]
|
15
15
|
s.has_rdoc = true
|
16
16
|
s.homepage = %q{http://github.com/alloy/kicker}
|
17
17
|
s.rdoc_options = ["--inline-source", "--charset=UTF-8"]
|
data/lib/kicker.rb
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
$:.unshift File.expand_path('../../vendor', __FILE__)
|
2
|
+
require 'rucola/fsevents'
|
3
|
+
require 'growlnotifier/growl_helpers'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
class Kicker
|
7
|
+
OPTION_PARSER = lambda do |options|
|
8
|
+
OptionParser.new do |opts|
|
9
|
+
opts.banner = "Usage: #{$0} [options] -e [command] [path]"
|
10
|
+
|
11
|
+
opts.on('-e', '--execute [COMMAND]', 'The command to execute.') do |command|
|
12
|
+
options[:command] = command
|
13
|
+
end
|
14
|
+
|
15
|
+
opts.on('--[no-]growl', 'Whether or not to use Growl. Default is to use growl.') do |growl|
|
16
|
+
options[:growl] = growl
|
17
|
+
end
|
18
|
+
|
19
|
+
opts.on('--growl-command [COMMAND]', 'The command to execute when the Growl succeeded message is clicked.') do |command|
|
20
|
+
options[:growl_command] = command
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.parse_options(argv)
|
26
|
+
argv = argv.dup
|
27
|
+
options = { :growl => true }
|
28
|
+
OPTION_PARSER.call(options).parse!(argv)
|
29
|
+
options[:path] = argv.first
|
30
|
+
options
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.run!(argv = ARGV)
|
34
|
+
new(parse_options(argv)).start
|
35
|
+
end
|
36
|
+
|
37
|
+
include Growl
|
38
|
+
GROWL_NOTIFICATIONS = {
|
39
|
+
:change => 'Change occured',
|
40
|
+
:succeeded => 'Command succeeded',
|
41
|
+
:failed => 'Command failed'
|
42
|
+
}
|
43
|
+
GROWL_DEFAULT_CALLBACK = lambda do
|
44
|
+
OSX::NSWorkspace.sharedWorkspace.launchApplication('Terminal')
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_writer :command
|
48
|
+
attr_reader :path, :file
|
49
|
+
attr_accessor :use_growl, :growl_command
|
50
|
+
|
51
|
+
def initialize(options)
|
52
|
+
self.path = options[:path] if options[:path]
|
53
|
+
@command = options[:command]
|
54
|
+
@use_growl = options[:growl]
|
55
|
+
@growl_command = options[:growl_command]
|
56
|
+
end
|
57
|
+
|
58
|
+
def path=(path)
|
59
|
+
@path = File.expand_path(path)
|
60
|
+
@file, @path = @path, File.dirname(@path) unless File.directory?(@path)
|
61
|
+
end
|
62
|
+
|
63
|
+
def start
|
64
|
+
validate_options!
|
65
|
+
|
66
|
+
log "Watching for changes on `#{file || path}'"
|
67
|
+
log "With command: #{command}"
|
68
|
+
log ''
|
69
|
+
|
70
|
+
watch_dog = Rucola::FSEvents.start_watching(path) { |events| process(events) }
|
71
|
+
|
72
|
+
trap('INT') do
|
73
|
+
log "Cleaning up…"
|
74
|
+
watch_dog.stop
|
75
|
+
exit
|
76
|
+
end
|
77
|
+
|
78
|
+
Growl::Notifier.sharedInstance.register('Kicker', Kicker::GROWL_NOTIFICATIONS.values) if @use_growl
|
79
|
+
|
80
|
+
OSX.CFRunLoopRun
|
81
|
+
end
|
82
|
+
|
83
|
+
def command
|
84
|
+
"sh -c #{@command.inspect}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def process(events)
|
88
|
+
execute! unless file && !events.find { |e| e.last_modified_file == file }
|
89
|
+
end
|
90
|
+
|
91
|
+
def log(message)
|
92
|
+
puts "[#{Time.now}] #{message}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def execute!
|
96
|
+
log "Change occured. Executing command:"
|
97
|
+
growl(GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command') if @use_growl
|
98
|
+
|
99
|
+
output = `#{command}`
|
100
|
+
output.strip.split("\n").each { |line| log " #{line}" }
|
101
|
+
|
102
|
+
log "Command #{last_command_succeeded? ? 'succeeded' : "failed (#{last_command_status})"}"
|
103
|
+
|
104
|
+
if @use_growl
|
105
|
+
if last_command_succeeded?
|
106
|
+
callback = @growl_command.nil? ? GROWL_DEFAULT_CALLBACK : lambda { system(@growl_command) }
|
107
|
+
growl(GROWL_NOTIFICATIONS[:succeeded], "Kicker: Command succeeded", output, &callback)
|
108
|
+
else
|
109
|
+
growl(GROWL_NOTIFICATIONS[:failed], "Kicker: Command failed (#{last_command_status})", output, &GROWL_DEFAULT_CALLBACK)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def last_command_succeeded?
|
115
|
+
$?.success?
|
116
|
+
end
|
117
|
+
|
118
|
+
def last_command_status
|
119
|
+
$?.to_i
|
120
|
+
end
|
121
|
+
|
122
|
+
def validate_options!
|
123
|
+
validate_path_and_command!
|
124
|
+
validate_path_exists!
|
125
|
+
end
|
126
|
+
|
127
|
+
def validate_path_and_command!
|
128
|
+
unless @path && @command
|
129
|
+
puts OPTION_PARSER.call(nil).help
|
130
|
+
exit
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def validate_path_exists!
|
135
|
+
unless File.exist?(@path)
|
136
|
+
puts "The given path `#{@path}' does not exist"
|
137
|
+
exit 1
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
data/test/kicker_test.rb
ADDED
@@ -0,0 +1,224 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "Kicker.parse_options" do
|
4
|
+
it "should parse the path" do
|
5
|
+
Kicker.parse_options(['/some/file.rb'])[:path].should == '/some/file.rb'
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should parse the command" do
|
9
|
+
Kicker.parse_options(%w{ -e ls })[:command].should == 'ls'
|
10
|
+
Kicker.parse_options(%w{ --execute ls })[:command].should == 'ls'
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should parse if growl shouldn't be used" do
|
14
|
+
Kicker.parse_options([])[:growl].should == true
|
15
|
+
Kicker.parse_options(%w{ --no-growl })[:growl].should == false
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should parse the Growl command to use when the user clicks the Growl succeeded message" do
|
19
|
+
Kicker.parse_options(%w{ --growl-command ls })[:growl_command].should == 'ls'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "Kicker, when initializing" do
|
24
|
+
before do
|
25
|
+
@kicker = Kicker.new(:path => '/some/dir', :command => 'ls -l')
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should return the path to watch" do
|
29
|
+
File.stubs(:directory?).with('/some/dir').returns(true)
|
30
|
+
Kicker.new(:path => '/some/dir').path.should == '/some/dir'
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should return the command to execute once a change occurs" do
|
34
|
+
Kicker.new(:command => 'ls -l').command.should == 'sh -c "ls -l"'
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should return the dirname of the path if the given path is a file" do
|
38
|
+
File.stubs(:directory?).with('/some/file.rb').returns(false)
|
39
|
+
Kicker.new(:path => '/some/file.rb').path.should == '/some'
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should return the path to the file if the given path is a file" do
|
43
|
+
@kicker = Kicker.new(:path => '/some/file.rb', :command => 'ls -l')
|
44
|
+
@kicker.file.should == '/some/file.rb'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "Kicker, when starting" do
|
49
|
+
before do
|
50
|
+
@kicker = Kicker.new(:path => '/some/file.rb', :command => 'ls -l')
|
51
|
+
@kicker.stubs(:log)
|
52
|
+
Rucola::FSEvents.stubs(:start_watching)
|
53
|
+
OSX.stubs(:CFRunLoopRun)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should show the usage banner when path and command are nil and exit" do
|
57
|
+
@kicker.instance_variable_set("@path", nil)
|
58
|
+
@kicker.command = nil
|
59
|
+
@kicker.stubs(:validate_path_exists!)
|
60
|
+
|
61
|
+
Kicker::OPTION_PARSER.stubs(:call).returns(mock('OptionParser', :help => 'help'))
|
62
|
+
@kicker.expects(:puts).with("help")
|
63
|
+
@kicker.expects(:exit)
|
64
|
+
|
65
|
+
@kicker.start
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should warn the user if the given path doesn't exist and exit" do
|
69
|
+
@kicker.expects(:puts).with("The given path `#{@kicker.path}' does not exist")
|
70
|
+
@kicker.expects(:exit).with(1)
|
71
|
+
|
72
|
+
@kicker.start
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should start a FSEvents stream with a block which calls #process with the events" do
|
76
|
+
@kicker.stubs(:validate_options!)
|
77
|
+
|
78
|
+
Rucola::FSEvents.expects(:start_watching).with(@kicker.path).yields(['event'])
|
79
|
+
@kicker.expects(:process).with(['event'])
|
80
|
+
|
81
|
+
@kicker.start
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should setup a signal handler for `INT' which stops the FSEvents stream and exits" do
|
85
|
+
@kicker.stubs(:validate_options!)
|
86
|
+
|
87
|
+
watch_dog = stub('Rucola::FSEvents')
|
88
|
+
Rucola::FSEvents.stubs(:start_watching).returns(watch_dog)
|
89
|
+
|
90
|
+
@kicker.expects(:trap).with('INT').yields
|
91
|
+
watch_dog.expects(:stop)
|
92
|
+
@kicker.expects(:exit)
|
93
|
+
|
94
|
+
@kicker.start
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should start a CFRunLoop" do
|
98
|
+
@kicker.stubs(:validate_options!)
|
99
|
+
|
100
|
+
OSX.expects(:CFRunLoopRun)
|
101
|
+
@kicker.start
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should register with growl if growl should be used" do
|
105
|
+
@kicker.stubs(:validate_options!)
|
106
|
+
@kicker.use_growl = true
|
107
|
+
|
108
|
+
Growl::Notifier.sharedInstance.expects(:register).with('Kicker', Kicker::GROWL_NOTIFICATIONS.values)
|
109
|
+
@kicker.start
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should _not_ register with growl if growl should not be used" do
|
113
|
+
@kicker.stubs(:validate_options!)
|
114
|
+
@kicker.use_growl = false
|
115
|
+
|
116
|
+
Growl::Notifier.sharedInstance.expects(:register).never
|
117
|
+
@kicker.start
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "Kicker, when a change occurs" do
|
122
|
+
before do
|
123
|
+
File.stubs(:directory?).returns(false)
|
124
|
+
Kicker.any_instance.stubs(:last_command_succeeded?).returns(true)
|
125
|
+
Kicker.any_instance.stubs(:log)
|
126
|
+
@kicker = Kicker.new(:path => '/some/file.rb', :command => 'ls -l')
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should execute the command if a change occured to the watched file" do
|
130
|
+
event = mock('Rucola::FSEvents::Event', :last_modified_file => '/some/file.rb')
|
131
|
+
|
132
|
+
@kicker.expects(:`).with(@kicker.command).returns('')
|
133
|
+
@kicker.process([event])
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should _not_ execute the command if a change occured to another file than the one being watched" do
|
137
|
+
event = mock('Rucola::FSEvents::Event', :last_modified_file => '/some/other_file.rb')
|
138
|
+
|
139
|
+
@kicker.expects(:`).never
|
140
|
+
@kicker.process([event])
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should execute the command if a change occured in the watched directory" do
|
144
|
+
File.stubs(:directory?).returns(true)
|
145
|
+
kicker = Kicker.new(:path => '/some/dir', :command => 'ls -l')
|
146
|
+
event = mock('Rucola::FSEvents::Event')
|
147
|
+
|
148
|
+
kicker.expects(:`).with(kicker.command).returns('')
|
149
|
+
kicker.process([event])
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe "Kicker, in general" do
|
154
|
+
before do
|
155
|
+
Kicker.any_instance.stubs(:last_command_succeeded?).returns(true)
|
156
|
+
@kicker = Kicker.new(:path => '/some/dir', :command => 'ls -l')
|
157
|
+
end
|
158
|
+
|
159
|
+
it "should print a log entry with timestamp" do
|
160
|
+
now = Time.now
|
161
|
+
Time.stubs(:now).returns(now)
|
162
|
+
|
163
|
+
@kicker.expects(:puts).with("[#{now}] the message")
|
164
|
+
@kicker.log('the message')
|
165
|
+
end
|
166
|
+
|
167
|
+
it "should log the output of the command indented by 2 spaces and whether or not the command succeeded" do
|
168
|
+
@kicker.stubs(:`).returns("line 1\nline 2")
|
169
|
+
|
170
|
+
@kicker.expects(:log).with('Change occured. Executing command:')
|
171
|
+
@kicker.expects(:log).with(' line 1')
|
172
|
+
@kicker.expects(:log).with(' line 2')
|
173
|
+
@kicker.expects(:log).with('Command succeeded')
|
174
|
+
@kicker.execute!
|
175
|
+
|
176
|
+
@kicker.stubs(:last_command_succeeded?).returns(false)
|
177
|
+
@kicker.stubs(:last_command_status).returns(123)
|
178
|
+
@kicker.expects(:log).with('Change occured. Executing command:')
|
179
|
+
@kicker.expects(:log).with(' line 1')
|
180
|
+
@kicker.expects(:log).with(' line 2')
|
181
|
+
@kicker.expects(:log).with('Command failed (123)')
|
182
|
+
@kicker.execute!
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should send the Growl messages with the default click callback" do
|
186
|
+
@kicker.stubs(:log)
|
187
|
+
|
188
|
+
@kicker.stubs(:`).returns("line 1\nline 2")
|
189
|
+
@kicker.use_growl = true
|
190
|
+
|
191
|
+
OSX::NSWorkspace.sharedWorkspace.expects(:launchApplication).with('Terminal').times(2)
|
192
|
+
|
193
|
+
@kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
|
194
|
+
@kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:succeeded], 'Kicker: Command succeeded', "line 1\nline 2").yields
|
195
|
+
@kicker.execute!
|
196
|
+
|
197
|
+
@kicker.stubs(:last_command_succeeded?).returns(false)
|
198
|
+
@kicker.stubs(:last_command_status).returns(123)
|
199
|
+
@kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
|
200
|
+
@kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:failed], 'Kicker: Command failed (123)', "line 1\nline 2").yields
|
201
|
+
@kicker.execute!
|
202
|
+
end
|
203
|
+
|
204
|
+
it "should send the Growl messages with a click callback which executes the specified growl command when succeeded" do
|
205
|
+
@kicker.stubs(:log)
|
206
|
+
|
207
|
+
@kicker.stubs(:`).returns("line 1\nline 2")
|
208
|
+
@kicker.use_growl = true
|
209
|
+
@kicker.growl_command = 'ls -l'
|
210
|
+
|
211
|
+
@kicker.expects(:system).with('ls -l').times(1)
|
212
|
+
OSX::NSWorkspace.sharedWorkspace.expects(:launchApplication).with('Terminal').times(1)
|
213
|
+
|
214
|
+
@kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
|
215
|
+
@kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:succeeded], 'Kicker: Command succeeded', "line 1\nline 2").yields
|
216
|
+
@kicker.execute!
|
217
|
+
|
218
|
+
@kicker.stubs(:last_command_succeeded?).returns(false)
|
219
|
+
@kicker.stubs(:last_command_status).returns(123)
|
220
|
+
@kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:change], 'Kicker: Change occured', 'Executing command')
|
221
|
+
@kicker.expects(:growl).with(Kicker::GROWL_NOTIFICATIONS[:failed], 'Kicker: Command failed (123)', "line 1\nline 2").yields
|
222
|
+
@kicker.execute!
|
223
|
+
end
|
224
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'osx/cocoa'
|
2
|
+
|
3
|
+
module Growl
|
4
|
+
class Notifier < OSX::NSObject
|
5
|
+
VERSION = '1.0.2'
|
6
|
+
|
7
|
+
GROWL_IS_READY = "Lend Me Some Sugar; I Am Your Neighbor!"
|
8
|
+
GROWL_NOTIFICATION_CLICKED = "GrowlClicked!"
|
9
|
+
GROWL_NOTIFICATION_TIMED_OUT = "GrowlTimedOut!"
|
10
|
+
GROWL_KEY_CLICKED_CONTEXT = "ClickedContext"
|
11
|
+
|
12
|
+
PRIORITIES = {
|
13
|
+
:emergency => 2,
|
14
|
+
:high => 1,
|
15
|
+
:normal => 0,
|
16
|
+
:moderate => -1,
|
17
|
+
:very_low => -2,
|
18
|
+
}
|
19
|
+
|
20
|
+
class << self
|
21
|
+
# Returns the singleton instance of Growl::Notifier with which you register and send your Growl notifications.
|
22
|
+
def sharedInstance
|
23
|
+
@sharedInstance ||= alloc.init
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :application_name, :application_icon, :notifications, :default_notifications
|
28
|
+
attr_accessor :delegate
|
29
|
+
|
30
|
+
# Set to +true+ if you want to receive delegate callback messages,
|
31
|
+
# <tt>growlNotifierClicked_context</tt> & <tt>growlNotifierTimedOut_context</tt>,
|
32
|
+
# without the need to specify a <tt>:click_context</tt>.
|
33
|
+
#
|
34
|
+
# The default is +false+, which means your application won't receive any delegate
|
35
|
+
# callback messages if the <tt>:click_context</tt> is omitted.
|
36
|
+
attr_accessor :always_callback
|
37
|
+
|
38
|
+
# Registers the applications metadata and the notifications, that your application might send, to Growl.
|
39
|
+
# The +default_notifications+ are notifications that will be enabled by default, the regular +notifications+ are
|
40
|
+
# optional and should be enabled by the user in the Growl system preferences.
|
41
|
+
#
|
42
|
+
# Register the applications name and the notifications that will be used.
|
43
|
+
# * +default_notifications+ defaults to the regular +notifications+.
|
44
|
+
# * +application_icon+ defaults to OSX::NSApplication.sharedApplication.applicationIconImage.
|
45
|
+
#
|
46
|
+
# Growl::Notifier.sharedInstance.register 'FoodApp', ['YourHamburgerIsReady', 'OhSomeoneElseAteIt']
|
47
|
+
#
|
48
|
+
# Register the applications name, the notifications plus the default notifications that will be used and the icon that's to be used in the Growl notifications.
|
49
|
+
#
|
50
|
+
# Growl::Notifier.sharedInstance.register 'FoodApp', ['YourHamburgerIsReady', 'OhSomeoneElseAteIt'], ['DefaultNotification], OSX::NSImage.imageNamed('GreasyHamburger')
|
51
|
+
def register(application_name, notifications, default_notifications = nil, application_icon = nil)
|
52
|
+
@application_name, @application_icon = application_name, (application_icon || OSX::NSApplication.sharedApplication.applicationIconImage)
|
53
|
+
@notifications, @default_notifications = notifications, (default_notifications || notifications)
|
54
|
+
@callbacks = {}
|
55
|
+
send_registration!
|
56
|
+
end
|
57
|
+
|
58
|
+
# Sends a Growl notification.
|
59
|
+
#
|
60
|
+
# * +notification_name+ : the name of one of the notifcations that your apllication registered with Growl. See register for more info.
|
61
|
+
# * +title+ : the title that should be used in the Growl notification.
|
62
|
+
# * +description+ : the body of the Grow notification.
|
63
|
+
# * +options+ : specifies a few optional options:
|
64
|
+
# * <tt>:sticky</tt> : indicates if the Grow notification should "stick" to the screen. Defaults to +false+.
|
65
|
+
# * <tt>:priority</tt> : sets the priority level of the Growl notification. Defaults to 0.
|
66
|
+
# * <tt>:click_context</tt> : a string describing the context of the notification. This is send back to the delegate so you can check what kind of notification it was. If omitted, no delegate messages will be send. You can disable this behaviour by setting always_callback to +true+.
|
67
|
+
# * <tt>:icon</tt> : specifies the icon to be used in the Growl notification. Defaults to the registered +application_icon+, see register for more info.
|
68
|
+
#
|
69
|
+
# Simple example:
|
70
|
+
#
|
71
|
+
# name = 'YourHamburgerIsReady'
|
72
|
+
# title = 'Your hamburger is ready for consumption!'
|
73
|
+
# description = 'Please pick it up at isle 4.'
|
74
|
+
#
|
75
|
+
# Growl::Notifier.sharedInstance.notify(name, title, description)
|
76
|
+
#
|
77
|
+
# Example with optional options:
|
78
|
+
#
|
79
|
+
# Growl::Notifier.sharedInstance.notify(name, title, description, :sticky => true, :priority => 1, :icon => OSX::NSImage.imageNamed('SuperBigHamburger'))
|
80
|
+
#
|
81
|
+
# When you pass notify a block, that block will be used as the callback handler if the Growl notification was clicked. Eg:
|
82
|
+
#
|
83
|
+
# Growl::Notifier.sharedInstance.notify(name, title, description, :sticky => true) do
|
84
|
+
# user_clicked_notification_so_do_something!
|
85
|
+
# end
|
86
|
+
def notify(notification_name, title, description, options = {}, &callback)
|
87
|
+
dict = {
|
88
|
+
:ApplicationName => @application_name,
|
89
|
+
:ApplicationPID => pid,
|
90
|
+
:NotificationName => notification_name,
|
91
|
+
:NotificationTitle => title,
|
92
|
+
:NotificationDescription => description,
|
93
|
+
:NotificationPriority => PRIORITIES[options[:priority]] || options[:priority] || 0
|
94
|
+
}
|
95
|
+
dict[:NotificationIcon] = options[:icon].TIFFRepresentation if options[:icon]
|
96
|
+
dict[:NotificationSticky] = 1 if options[:sticky]
|
97
|
+
|
98
|
+
context = {}
|
99
|
+
context[:user_click_context] = options[:click_context] if options[:click_context]
|
100
|
+
if block_given?
|
101
|
+
@callbacks[callback.object_id] = callback
|
102
|
+
context[:callback_object_id] = callback.object_id.to_s
|
103
|
+
end
|
104
|
+
dict[:NotificationClickContext] = context if always_callback || !context.empty?
|
105
|
+
|
106
|
+
notification_center.postNotificationName_object_userInfo_deliverImmediately(:GrowlNotification, nil, dict, true)
|
107
|
+
end
|
108
|
+
|
109
|
+
def onReady(notification)
|
110
|
+
send_registration!
|
111
|
+
end
|
112
|
+
|
113
|
+
def onClicked(notification)
|
114
|
+
user_context = nil
|
115
|
+
if context = notification.userInfo[GROWL_KEY_CLICKED_CONTEXT]
|
116
|
+
user_context = context[:user_click_context]
|
117
|
+
if callback_object_id = context[:callback_object_id]
|
118
|
+
@callbacks.delete(callback_object_id.to_i).call
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
@delegate.growlNotifierClicked_context(self, user_context) if @delegate && @delegate.respond_to?(:growlNotifierClicked_context)
|
123
|
+
end
|
124
|
+
|
125
|
+
def onTimeout(notification)
|
126
|
+
user_context = nil
|
127
|
+
if context = notification.userInfo[GROWL_KEY_CLICKED_CONTEXT]
|
128
|
+
@callbacks.delete(context[:callback_object_id].to_i) if context[:callback_object_id]
|
129
|
+
user_context = context[:user_click_context]
|
130
|
+
end
|
131
|
+
|
132
|
+
@delegate.growlNotifierTimedOut_context(self, user_context) if @delegate && @delegate.respond_to?(:growlNotifierTimedOut_context)
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def pid
|
138
|
+
OSX::NSProcessInfo.processInfo.processIdentifier.to_i
|
139
|
+
end
|
140
|
+
|
141
|
+
def notification_center
|
142
|
+
OSX::NSDistributedNotificationCenter.defaultCenter
|
143
|
+
end
|
144
|
+
|
145
|
+
def send_registration!
|
146
|
+
add_observer 'onReady:', GROWL_IS_READY, false
|
147
|
+
add_observer 'onClicked:', GROWL_NOTIFICATION_CLICKED, true
|
148
|
+
add_observer 'onTimeout:', GROWL_NOTIFICATION_TIMED_OUT, true
|
149
|
+
|
150
|
+
dict = {
|
151
|
+
:ApplicationName => @application_name,
|
152
|
+
:ApplicationIcon => application_icon.TIFFRepresentation,
|
153
|
+
:AllNotifications => @notifications,
|
154
|
+
:DefaultNotifications => @default_notifications
|
155
|
+
}
|
156
|
+
|
157
|
+
notification_center.objc_send(
|
158
|
+
:postNotificationName, :GrowlApplicationRegistrationNotification,
|
159
|
+
:object, nil,
|
160
|
+
:userInfo, dict,
|
161
|
+
:deliverImmediately, true
|
162
|
+
)
|
163
|
+
end
|
164
|
+
|
165
|
+
def add_observer(selector, name, prepend_name_and_pid)
|
166
|
+
name = "#{@application_name}-#{pid}-#{name}" if prepend_name_and_pid
|
167
|
+
notification_center.addObserver_selector_name_object self, selector, name, nil
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.expand_path('../growl', __FILE__)
|
2
|
+
|
3
|
+
# Defines a few convenience methods that you can use in your class if you include the Growl module.
|
4
|
+
# Eg:
|
5
|
+
#
|
6
|
+
# class FoodReporter < OSX::NSObject
|
7
|
+
# include Growl
|
8
|
+
#
|
9
|
+
# def hamburger_time!
|
10
|
+
# growl 'YourHamburgerIsReady', 'Your hamburger is ready for consumption!', 'Please pick it up at isle 4.', :priority => 1 do
|
11
|
+
# throw_it_away_before_user_reaches_counter!
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
module Growl
|
16
|
+
# Sends a Growl notification. See Growl::Notifier#notify for more info.
|
17
|
+
def growl(name, title, description, options = {}, &callback)
|
18
|
+
Growl::Notifier.sharedInstance.notify name, title, description, options, &callback
|
19
|
+
end
|
20
|
+
|
21
|
+
# Sends a sticky Growl notification. See Growl::Notifier#notify for more info.
|
22
|
+
def sticky_growl(name, title, description, options = {}, &callback)
|
23
|
+
growl name, title, description, options.merge!(:sticky => true), &callback
|
24
|
+
end
|
25
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: alloy-kicker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eloy Duran
|
@@ -26,12 +26,21 @@ files:
|
|
26
26
|
- bin
|
27
27
|
- bin/kicker
|
28
28
|
- kicker.gemspec
|
29
|
+
- lib
|
30
|
+
- lib/kicker.rb
|
29
31
|
- LICENSE
|
30
32
|
- pkg
|
31
33
|
- pkg/kicker-0.1.0.gem
|
34
|
+
- pkg/kicker-0.1.1.gem
|
32
35
|
- Rakefile
|
33
36
|
- README.rdoc
|
37
|
+
- test
|
38
|
+
- test/kicker_test.rb
|
39
|
+
- test/test_helper.rb
|
34
40
|
- vendor
|
41
|
+
- vendor/growlnotifier
|
42
|
+
- vendor/growlnotifier/growl.rb
|
43
|
+
- vendor/growlnotifier/growl_helpers.rb
|
35
44
|
- vendor/rucola
|
36
45
|
- vendor/rucola/fsevents.rb
|
37
46
|
- VERSION.yml
|