From 3b0e2ad30520bb8f3a0a92429c646c903c972e28 Mon Sep 17 00:00:00 2001 From: Michael Kessler Date: Wed, 12 Oct 2011 20:54:57 +0200 Subject: [PATCH] Add support for Growl Notification Transport Protocol. --- CHANGELOG.md | 1 + README.md | 32 ++- lib/guard/notifier.rb | 438 +++++++++++++++++++++--------------- spec/guard/notifier_spec.rb | 127 +++++++++-- 4 files changed, 390 insertions(+), 208 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0726ac..70b52db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Improvements +- Add support for Growl Notification Transport Protocol. ([@netzpirat][]) - [#157](https://github.com/guard/guard/pull/157): Allow any return from the Guard watchers. ([@earlonrails][]) - [#156](https://github.com/guard/guard/pull/156): Log error and diagnostic messages to STDERR. ([@sunaku][]) - [#152](https://github.com/guard/guard/pull/152): Growl Notify API update for a graceful fail. ([@scottdavis][]) diff --git a/README.md b/README.md index a6d5058..c49055a 100644 --- a/README.md +++ b/README.md @@ -51,26 +51,38 @@ Install the rb-fsevent gem for [FSEvent](http://en.wikipedia.org/wiki/FSEvents) $ gem install rb-fsevent -You have two possibilities: +You have three possibilities for getting Growl support: -Use the [growl_notify gem](https://rubygems.org/gems/growl_notify) (recommended, compatible with Growl >= 1.3): +Use the [growl_notify gem](https://rubygems.org/gems/growl_notify): $ gem install growl_notify -Use the [growlnotify](http://growl.info/extras.php#growlnotify) (cli tool for Growl <= 1.2) + the [growl gem](https://rubygems.org/gems/growl). +The `growl_notify` gem is compatible with Growl >= 1.3 and uses AppleScript to send Growl notifications. +The gem needs a native C extension to make use of AppleScript and does not run on JRuby and MacRuby. + +Use the [ruby_gntp gem](https://github.com/snaka/ruby_gntp): + + $ gem install ruby_gntp + +The `ruby_gntp` gem is compatible with Growl >= 0.7 and uses the Growl Notification Transport Protocol to send Growl +notifications. Guard supports multiple notification channels for customizing each notification type, but it's limited +to the local host currently. + +Use the [growl gem](https://rubygems.org/gems/growl): - $ brew install growlnotify $ gem install growl -And add them to your Gemfile: +The `growl` gem is compatible with all versions of Growl and uses a command line tool [growlnotify](http://growl.info/extras.php#growlnotify) +that must be separately downloaded and installed. You can alsi install it with HomeBrew: + + $ brew install growlnotify + +Finally you have to add your Growl library of choice to your Gemfile: gem 'rb-fsevent' - gem 'growl_notify' # or gem 'growl' + gem 'growl_notify' # or gem 'ruby_gntp' or gem 'growl' -The difference between growl and growl_notify is that growl_notify uses AppleScript to -display a message, whereas growl uses the `growlnotify` command. In general the AppleScript -approach is preferred, but you may also use the older growl gem. Have a look at the -[Guard Wiki](https://github.com/guard/guard/wiki/Use-growl_notify-or-growl-gem) for more information. +Have a look at the [Guard Wiki](https://github.com/guard/guard/wiki/Which-Growl-library-should-I-use) for more information. ### On Linux diff --git a/lib/guard/notifier.rb b/lib/guard/notifier.rb index ede2895..cbda995 100644 --- a/lib/guard/notifier.rb +++ b/lib/guard/notifier.rb @@ -15,200 +15,276 @@ module Guard # Application name as shown in the specific notification settings APPLICATION_NAME = "Guard" - # Turn notifications off. - # - def self.turn_off - ENV["GUARD_NOTIFY"] = 'false' - end + class << self - # Turn notifications on. This tries to load the platform - # specific notification library. - # - # @return [Boolean] whether the notification could be enabled. - # - def self.turn_on - ENV["GUARD_NOTIFY"] = 'true' - case RbConfig::CONFIG['target_os'] - when /darwin/i - require_growl - when /linux/i - require_libnotify - when /mswin|mingw/i - require_rbnotifu + attr_accessor :growl_library, :gntp + + # Turn notifications off. + # + def turn_off + ENV["GUARD_NOTIFY"] = 'false' end - end - - # Show a message with the system notification. - # - # @see .image_path - # - # @param [String] the message to show - # @option options [Symbol, String] image the image symbol or path to an image - # @option options [String] title the notification title - # - def self.notify(message, options = {}) - if enabled? - image = options.delete(:image) || :success - title = options.delete(:title) || "Guard" + # Turn notifications on. This tries to load the platform + # specific notification library. + # + # @return [Boolean] whether the notification could be enabled. + # + def turn_on + ENV["GUARD_NOTIFY"] = 'true' case RbConfig::CONFIG['target_os'] - when /darwin/i - notify_mac(title, message, image, options) - when /linux/i - notify_linux(title, message, image, options) - when /mswin|mingw/i - notify_windows(title, message, image, options) + when /darwin/i + require_growl + when /linux/i + require_libnotify + when /mswin|mingw/i + require_rbnotifu end end - end - # Test if the notifications are enabled and available. - # - # @return [Boolean] whether the notifications are available - # - def self.enabled? - ENV["GUARD_NOTIFY"] == 'true' - end + # Show a message with the system notification. + # + # @see .image_path + # + # @param [String] the message to show + # @option options [Symbol, String] image the image symbol or path to an image + # @option options [String] title the notification title + # + def notify(message, options = { }) + if enabled? + image = options.delete(:image) || :success + title = options.delete(:title) || "Guard" - private - - # Send a message to Growl either with the `growl` gem or the `growl_notify` gem. - # - # @param [String] title the notification title - # @param [String] message the message to show - # @param [Symbol, String] the image to user - # @param [Hash] options the growl options - # - def self.notify_mac(title, message, image, options = {}) - require_growl # need for guard-rspec formatter that is called out of guard scope - - default_options = { :title => title, :icon => image_path(image), :name => APPLICATION_NAME } - default_options.merge!(options) - - if defined?(GrowlNotify) - default_options[:description] = message - default_options[:application_name] = APPLICATION_NAME - default_options.delete(:name) - - GrowlNotify.send_notification(default_options) if enabled? - else - Growl.notify message, default_options.merge(options) if enabled? - end - end - - # Send a message to libnotify. - # - # @param [String] title the notification title - # @param [String] message the message to show - # @param [Symbol, String] the image to user - # @param [Hash] options the libnotify options - # - def self.notify_linux(title, message, image, options = {}) - require_libnotify # need for guard-rspec formatter that is called out of guard scope - default_options = { :body => message, :summary => title, :icon_path => image_path(image), :transient => true } - Libnotify.show default_options.merge(options) if enabled? - end - - # Send a message to notifu. - # - # @param [String] title the notification title - # @param [String] message the message to show - # @param [Symbol, String] the image to user - # @param [Hash] options the notifu options - # - def self.notify_windows(title, message, image, options = {}) - require_rbnotifu # need for guard-rspec formatter that is called out of guard scope - default_options = { :message => message, :title => title, :type => image_level(image), :time => 3 } - Notifu.show default_options.merge(options) if enabled? - end - - # Get the image path for an image symbol. - # - # Known symbols are: - # - # - failed - # - pending - # - success - # - # @param [Symbol] image the image name - # @return [String] the image path - # - def self.image_path(image) - images_path = Pathname.new(File.dirname(__FILE__)).join('../../images') - case image - when :failed - images_path.join("failed.png").to_s - when :pending - images_path.join("pending.png").to_s - when :success - images_path.join("success.png").to_s - else - # path given - image - end - end - - # The notification level type for the given image. - # - # @param [Symbol] image the image - # @return [Symbol] the level - # - def self.image_level(image) - case image - when :failed - :error - when :pending - :warn - when :success - :info - else - :info - end - end - - # Try to safely load growl and turns notifications - # off on load failure. - # - def self.require_growl - begin - require 'growl_notify' - - if GrowlNotify.application_name != APPLICATION_NAME - GrowlNotify.config do |c| - c.notifications = c.default_notifications = [ APPLICATION_NAME ] - c.application_name = c.notifications.first + case RbConfig::CONFIG['target_os'] + when /darwin/i + notify_mac(title, message, image, options) + when /linux/i + notify_linux(title, message, image, options) + when /mswin|mingw/i + notify_windows(title, message, image, options) end end - rescue LoadError - require 'growl' - rescue ::GrowlNotify::GrowlNotFound - turn_off - UI.info "Please install Growl from http://growl.info" end - rescue LoadError - turn_off - UI.info "Please install growl_notify or growl gem for Mac OS X notification support and add it to your Gemfile" - end - # Try to safely load libnotify and turns notifications - # off on load failure. - # - def self.require_libnotify - require 'libnotify' - rescue LoadError - turn_off - UI.info "Please install libnotify gem for Linux notification support and add it to your Gemfile" - end + # Test if the notifications are enabled and available. + # + # @return [Boolean] whether the notifications are available + # + def enabled? + ENV["GUARD_NOTIFY"] == 'true' + end - # Try to safely load rb-notifu and turns notifications - # off on load failure. - # - def self.require_rbnotifu - require 'rb-notifu' - rescue LoadError - turn_off - UI.info "Please install rb-notifu gem for Windows notification support and add it to your Gemfile" - end + private + # Send a message to Growl either with the `growl` gem or the `growl_notify` gem. + # + # @param [String] title the notification title + # @param [String] message the message to show + # @param [Symbol, String] the image to user + # @param [Hash] options the growl options + # + def notify_mac(title, message, image, options = { }) + require_growl # need for guard-rspec formatter that is called out of guard scope + + notification = { :title => title, :icon => image_path(image) }.merge(options) + + case self.growl_library + when :growl_notify + notification.delete(:name) + + GrowlNotify.send_notification({ + :description => message, + :application_name => APPLICATION_NAME + }.merge(notification)) + + when :ruby_gntp + icon = "file://#{ notification.delete(:icon) }" + + self.gntp.notify({ + :name => [:pending, :success, :failed].include?(image) ? image.to_s : 'notify', + :text => message, + :icon => icon + }.merge(notification)) + + when :growl + Growl.notify(message, { + :name => APPLICATION_NAME + }.merge(notification)) + end + end + + # Send a message to libnotify. + # + # @param [String] title the notification title + # @param [String] message the message to show + # @param [Symbol, String] the image to user + # @param [Hash] options the libnotify options + # + def notify_linux(title, message, image, options = { }) + require_libnotify # need for guard-rspec formatter that is called out of guard scope + + notification = { :body => message, :summary => title, :icon_path => image_path(image), :transient => true } + Libnotify.show notification.merge(options) + end + + # Send a message to notifu. + # + # @param [String] title the notification title + # @param [String] message the message to show + # @param [Symbol, String] the image to user + # @param [Hash] options the notifu options + # + def notify_windows(title, message, image, options = { }) + require_rbnotifu # need for guard-rspec formatter that is called out of guard scope + + notification = { :message => message, :title => title, :type => image_level(image), :time => 3 } + Notifu.show notification.merge(options) + end + + # Get the image path for an image symbol. + # + # Known symbols are: + # + # - failed + # - pending + # - success + # + # @param [Symbol] image the image name + # @return [String] the image path + # + def image_path(image) + images_path = Pathname.new(File.dirname(__FILE__)).join('../../images') + case image + when :failed + images_path.join("failed.png").to_s + when :pending + images_path.join("pending.png").to_s + when :success + images_path.join("success.png").to_s + else + # path given + image + end + end + + # The notification level type for the given image. + # + # @param [Symbol] image the image + # @return [Symbol] the level + # + def image_level(image) + case image + when :failed + :error + when :pending + :warn + when :success + :info + else + :info + end + end + + # Try to safely load growl and turns notifications off on load failure. + # The Guard notifier knows three different library to handle sending + # Growl messages and tries to loading them in the given order: + # + # - [Growl Notify](https://github.com/scottdavis/growl_notify) + # - [Ruby GNTP](https://github.com/snaka/ruby_gntp) + # - [Growl](https://github.com/visionmedia/growl) + # + # On successful loading of any of the libraries, the active library name is + # accessible through `.growl_library`. + # + def require_growl + self.growl_library = try_growl_notify || try_ruby_gntp || try_growl + + unless self.growl_library + turn_off + UI.info "Please install growl_notify or growl gem for Mac OS X notification support and add it to your Gemfile" + end + end + + # Try to load the `growl_notify` gem. + # + # @return [Symbol, nil] A symbol with the name of the loaded library + # + def try_growl_notify + require 'growl_notify' + + begin + if GrowlNotify.application_name != APPLICATION_NAME + GrowlNotify.config do |c| + c.notifications = c.default_notifications = [APPLICATION_NAME] + c.application_name = c.notifications.first + end + end + + rescue ::GrowlNotify::GrowlNotFound + turn_off + UI.info "Please install Growl from http://growl.info" + end + + :growl_notify + + rescue LoadError + end + + # Try to load the `ruby_gntp` gem and register the available + # notification channels. + # + # @return [Symbol, nil] A symbol with the name of the loaded library + # + def try_ruby_gntp + require 'ruby_gntp' + + self.gntp = GNTP.new(APPLICATION_NAME) + self.gntp.register(:notifications => [ + { :name => 'notify', :enabled => true }, + { :name => 'failed', :enabled => true }, + { :name => 'pending', :enabled => true }, + { :name => 'success', :enabled => true } + ]) + + :ruby_gntp + + rescue LoadError + end + + # Try to load the `growl_notify` gem. + # + # @return [Symbol, nil] A symbol with the name of the loaded library + # + def try_growl + require 'growl' + + :growl + + rescue LoadError + end + + # Try to safely load libnotify and turns notifications + # off on load failure. + # + def require_libnotify + require 'libnotify' + + rescue LoadError + turn_off + UI.info "Please install libnotify gem for Linux notification support and add it to your Gemfile" + end + + # Try to safely load rb-notifu and turns notifications + # off on load failure. + # + def require_rbnotifu + require 'rb-notifu' + + rescue LoadError + turn_off + UI.info "Please install rb-notifu gem for Windows notification support and add it to your Gemfile" + end + + end end end diff --git a/spec/guard/notifier_spec.rb b/spec/guard/notifier_spec.rb index 1c68333..b491b08 100644 --- a/spec/guard/notifier_spec.rb +++ b/spec/guard/notifier_spec.rb @@ -26,15 +26,6 @@ describe Guard::Notifier do def self.config ; end end end - - it "should respond properly to a GrowlNotify exception" do - ::GrowlNotify.should_receive(:config).and_raise ::GrowlNotify::GrowlNotFound - ::GrowlNotify.should_receive(:application_name).and_return '' - ::Guard::UI.should_receive(:info) - described_class.should_receive(:require).with('growl_notify').and_return true - described_class.turn_on - described_class.should_not be_enabled - end it "loads the library and enables the notifications" do described_class.should_receive(:require).with('growl_notify').and_return true @@ -43,26 +34,60 @@ describe Guard::Notifier do described_class.should be_enabled end + it "should respond properly to a GrowlNotify exception" do + ::GrowlNotify.should_receive(:config).and_raise ::GrowlNotify::GrowlNotFound + ::GrowlNotify.should_receive(:application_name).and_return '' + ::Guard::UI.should_receive(:info) + described_class.should_receive(:require).with('growl_notify').and_return true + described_class.turn_on + described_class.should_not be_enabled + described_class.growl_library.should eql :growl_notify + end + after do Object.send(:remove_const, :GrowlNotify) end end + context "with the GNTP library available" do + before do + class ::GNTP + def register(config) ; end + end + end + + it "loads the library and enables the notifications" do + described_class.should_receive(:require).with('growl_notify').and_raise LoadError + described_class.should_receive(:require).with('ruby_gntp').and_return true + described_class.turn_on + described_class.should be_enabled + described_class.growl_library.should eql :ruby_gntp + end + + after do + Object.send(:remove_const, :GNTP) + end + end + context "with the Growl library available" do it "loads the library and enables the notifications" do described_class.should_receive(:require).with('growl_notify').and_raise LoadError + described_class.should_receive(:require).with('ruby_gntp').and_raise LoadError described_class.should_receive(:require).with('growl').and_return true described_class.turn_on described_class.should be_enabled + described_class.growl_library.should eql :growl end end - context "without the Growl library available" do + context "without a Growl library available" do it "disables the notifications" do described_class.should_receive(:require).with('growl_notify').and_raise LoadError + described_class.should_receive(:require).with('ruby_gntp').and_raise LoadError described_class.should_receive(:require).with('growl').and_raise LoadError described_class.turn_on described_class.should_not be_enabled + described_class.growl_library.should be nil end end end @@ -117,7 +142,7 @@ describe Guard::Notifier do context "on Mac OS" do before do - RbConfig::CONFIG.should_receive(:[]).with('target_os').and_return 'darwin' + RbConfig::CONFIG.stub(:[]).and_return 'darwin' described_class.stub(:require_growl) end @@ -125,6 +150,7 @@ describe Guard::Notifier do before do Object.send(:remove_const, :Growl) if defined?(Growl) Growl = Object.new + described_class.growl_library = :growl end after do @@ -142,7 +168,8 @@ describe Guard::Notifier do it "don't passes the notification to Growl if library is not available" do Growl.should_not_receive(:notify) - described_class.should_receive(:enabled?).and_return(true, false) + described_class.growl_library = nil + described_class.should_receive(:enabled?).and_return(false) described_class.notify 'great', :title => 'Guard' end @@ -170,6 +197,7 @@ describe Guard::Notifier do before do Object.send(:remove_const, :GrowlNotify) if defined?(GrowlNotify) GrowlNotify = Object.new + described_class.growl_library = :growl_notify end after do @@ -188,7 +216,8 @@ describe Guard::Notifier do it "don't passes the notification to Growl if library is not available" do GrowlNotify.should_not_receive(:send_notification) - described_class.should_receive(:enabled?).and_return(true, false) + described_class.growl_library = nil + described_class.should_receive(:enabled?).and_return(false) described_class.notify 'great', :title => 'Guard' end @@ -213,11 +242,75 @@ describe Guard::Notifier do described_class.notify 'great', :title => 'Guard', :name => "Guard-Cucumber" end end + + context 'with ruby_gntp gem' do + before do + described_class.growl_library = :ruby_gntp + end + + it "passes a success notification to Ruby GNTP" do + described_class.gntp.should_receive(:notify).with( + :name => "success", + :text => 'great', + :title => "Guard", + :icon => 'file://' + Pathname.new(File.dirname(__FILE__)).join('../../images/success.png').to_s + ) + described_class.notify 'great', :title => 'Guard' + end + + it "passes a pending notification to Ruby GNTP" do + described_class.gntp.should_receive(:notify).with( + :name => "pending", + :text => 'great', + :title => "Guard", + :icon => 'file://' + Pathname.new(File.dirname(__FILE__)).join('../../images/pending.png').to_s + ) + described_class.notify 'great', :title => 'Guard', :image => :pending + end + + it "passes a failure notification to Ruby GNTP" do + described_class.gntp.should_receive(:notify).with( + :name => "failed", + :text => 'great', + :title => "Guard", + :icon => 'file://' + Pathname.new(File.dirname(__FILE__)).join('../../images/failed.png').to_s + ) + described_class.notify 'great', :title => 'Guard', :image => :failed + end + + it "passes a general notification to Ruby GNTP" do + described_class.gntp.should_receive(:notify).with( + :name => "notify", + :text => 'great', + :title => "Guard", + :icon => 'file:///path/to/custom.png' + ) + described_class.notify 'great', :title => 'Guard', :image => '/path/to/custom.png' + end + + it "don't passes the notification to Ruby GNTP if library is not available" do + described_class.gntp.should_not_receive(:notify) + described_class.growl_library = nil + described_class.should_receive(:enabled?).and_return(false) + described_class.notify 'great', :title => 'Guard' + end + + it "allows additional notification options" do + described_class.gntp.should_receive(:notify).with( + :name => "success", + :text => 'great', + :title => "Guard", + :icon => 'file://' + Pathname.new(File.dirname(__FILE__)).join('../../images/success.png').to_s, + :sticky => true + ) + described_class.notify 'great', :title => 'Guard', :sticky => true + end + end end context "on Linux" do before do - RbConfig::CONFIG.should_receive(:[]).with('target_os').and_return 'linux' + RbConfig::CONFIG.stub(:[]).and_return 'linux' described_class.stub(:require_libnotify) Object.send(:remove_const, :Libnotify) if defined?(Libnotify) Libnotify = Object.new @@ -239,7 +332,7 @@ describe Guard::Notifier do it "don't passes the notification to Libnotify if library is not available" do Libnotify.should_not_receive(:show) - described_class.should_receive(:enabled?).and_return(true, false) + described_class.should_receive(:enabled?).and_return(false) described_class.notify 'great', :title => 'Guard' end @@ -267,7 +360,7 @@ describe Guard::Notifier do context "on Windows" do before do - RbConfig::CONFIG.should_receive(:[]).with('target_os').and_return 'mswin' + RbConfig::CONFIG.stub(:[]).and_return 'mswin' described_class.stub(:require_rbnotifu) Object.send(:remove_const, :Notifu) if defined?(Notifu) Notifu = Object.new @@ -289,7 +382,7 @@ describe Guard::Notifier do it "don't passes the notification to rb-notifu if library is not available" do Notifu.should_not_receive(:show) - described_class.should_receive(:enabled?).and_return(true, false) + described_class.should_receive(:enabled?).and_return(false) described_class.notify 'great', :title => 'Guard' end