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 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
- = kicker
1
+ = Kicker
2
2
 
3
3
  A simple OS X CLI tool which uses FSEvents to run a given shell command.
4
4
 
5
- Give it a path to a file or directory and a shell command to execute when any
6
- changes occur:
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 . "ls -l"
20
+ $ kicker -e "ls -l" .
11
21
 
12
22
  Run a Rake task whenever a given file is changed:
13
23
 
14
- $ kicker guides/source/nested_model_forms.textile "ONLY=nested_model_forms rake guides && open -a Safari guides/output/nested_model_forms.html"
24
+ $ kicker -e "ONLY=nested_model_forms rake guides" guides/source/nested_model_forms.textile
15
25
 
16
- == Installation
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
- $ sudo gem install alloy-kicker -s http://gems.github.com
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
- == Tests??
31
+ == Installation
21
32
 
22
- For now it's just a very simple bin script which uses the FSEvents abstraction
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
- :patch: 1
3
- :major: 0
4
- :minor: 1
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('../../vendor/rucola/fsevents', __FILE__)
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.1.1"
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
@@ -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
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'test/spec'
3
+ require 'mocha'
4
+
5
+ $:.unshift File.expand_path('../../lib', __FILE__)
6
+ require 'kicker'
@@ -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.1.1
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