lib/guard/notifiers/base.rb



require 'rbconfig'
require 'guard/ui'

module Guard
  module Notifier

    # Base class for all notifiers.
    #
    class Base

      HOSTS = {
        darwin:  'Mac OS X',
        linux:   'Linux',
        freebsd: 'FreeBSD',
        openbsd: 'OpenBSD',
        sunos:   'SunOS',
        solaris: 'Solaris',
        mswin:   'Windows',
        mingw:   'Windows',
        cygwin:  'Windows'
      }

      attr_reader :options

      def initialize(opts = {})
        @options = opts
      end

      # This method should be overriden by subclasses and return an array of
      # OSes the notifier supports. By default, it returns :all which mean
      # there's no check against the current OS.
      #
      # @see HOSTS for the list of possible OSes
      #
      def self.supported_hosts
        :all
      end

      # Test if the notifier can be used.
      #
      # @param [Hash] opts notifier options
      # @option opts [Boolean] silent true if no error messages should be shown
      # @return [Boolean] the availability status
      #
      def self.available?(opts = {})
        if _supported_host?
          true
        else
          hosts = supported_hosts.map { |host| HOSTS[host.to_sym] }.join(', ')
          ::Guard::UI.error "The :#{name} notifier runs only on #{hosts}." unless opts.fetch(:silent) { false }
          false
        end
      end

      # This method must be overriden.
      #
      def notify(message, opts = {})
        opts.replace(options.merge(opts))
        normalize_standard_options!(opts)
      end

      # Returns the title of the notifier.
      #
      # @example Un-modulize the class name
      #   Guard::Notifier::FileNotifier.title
      #   #=> 'FileNotifier'
      #
      # @return [String] the title of the notifier
      #
      def self.title
        self.to_s.sub(/.+::(\w+)$/, '\1')
      end

      # Returns the name of the notifier.
      #
      # @example Un-modulize, underscorize and downcase the class name
      #   Guard::Notifier::FileNotifier.name
      #   #=> 'file_notifier'
      #
      # @return [String] the name of the notifier
      #
      def self.name
        title.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
      end

      # Returns the name of the notifier's gem. By default it returns the
      # notifier name. This method can be overriden by subclasses.
      #
      # @example Un-modulize, underscorize and downcase the class name
      #   Guard::Notifier::FileNotifier.gem_name
      #   #=> 'file_notifier'
      #
      # @return [String] the name of the notifier's gem
      #
      def self.gem_name
        name
      end

      # This method tries to require the gem whose name is returned by
      # `.gem_name`. If a LoadError or NameError occurs, it displays an error
      # message (unless opts[:silent] is true) and returns false.
      #
      # @param [Hash] opts some options
      # @option opts [Boolean] silent true if no error messages should be shown
      #
      # @return [Boolean] whether or not the gem is loaded
      #
      def self.require_gem_safely(opts = {})
        require gem_name
        true
      rescue LoadError, NameError
        unless opts[:silent]
          ::Guard::UI.error "Please add \"gem '#{gem_name}'\" to your Gemfile and run Guard with \"bundle exec\"."
        end
        false
      end

      # Returns the title of the notifier.
      #
      # @example Un-modulize the class name
      #   Guard::Notifier::FileNotifier.new.title
      #   #=> 'FileNotifier'
      #
      # @return [String] the title of the notifier
      #
      def title
        self.class.title
      end

      # Returns the name of the notifier.
      #
      # @example Un-modulize, underscorize and downcase the class name
      #   Guard::Notifier::FileNotifier.new.name
      #   #=> 'file_notifier'
      #
      # @return [String] the name of the notifier
      #
      def name
        self.class.name
      end

      # Paths where all Guard images are located
      #
      # @return [Pathname] the path to the images directory
      #
      def images_path
        @images_path ||= Pathname.new(File.dirname(__FILE__)).join('../../../images')
      end

      # @private
      #
      # Checks if the current OS is supported by the notifier.
      #
      # @see .supported_hosts
      #
      def self._supported_host?
        supported_hosts == :all ||
        RbConfig::CONFIG['host_os'] =~ /#{supported_hosts.join('|')}/
      end

      # Set or modify the `:title`, `:type` and `:image` options for a
      # notification. Should be used in `#notify`.
      #
      # @param [Hash] opts additional notification library options
      # @option opts [String] type the notification type. Either 'success',
      #   'pending', 'failed' or 'notify'
      # @option opts [String] title the notification title
      # @option opts [String] image the path to the notification image
      #
      def normalize_standard_options!(opts)
        opts[:title] ||= 'Guard'
        opts[:type]  ||= _notification_type(opts.fetch(:image, :success))
        opts[:image]   = _image_path(opts.delete(:image) { :success })
      end

      private

      # Get the image path for an image symbol for the following
      # known image types:
      #
      # - failed
      # - pending
      # - success
      #
      # If the image is not a known symbol, it will be returned unmodified.
      #
      # @param [Symbol, String] image the image symbol or path to an image
      #
      # @return [String] the image path
      #
      def _image_path(image)
        case image.to_sym
        when :failed, :pending, :success
          images_path.join("#{image.to_s}.png").to_s
        else
          image
        end
      end

      # Get the notification type depending on the
      # image that has been selected for the notification.
      #
      # @param [Symbol, String] image the image symbol or path to an image
      #
      # @return [String] the notification type
      #
      def _notification_type(image)
        case image.to_sym
        when :failed, :pending, :success
          image.to_sym
        else
          :notify
        end
      end

    end

  end
end