lib/guard/notifiers/tmux.rb



require 'guard/notifiers/base'

module Guard
  module Notifier

    # Changes the color of the Tmux status bar and optionally
    # shows messages in the status bar.
    #
    # @example Add the `:tmux` notifier to your `Guardfile`
    #   notification :tmux
    #
    # @example Enable text messages
    #   notification :tmux, display_message: true
    #
    # @example Customize the tmux status colored for notifications
    #   notification :tmux, color_location: 'status-right-bg'
    #
    class Tmux < Base

      # Default options for the tmux notifications.
      DEFAULTS = {
        client:                 'tmux',
        tmux_environment:       'TMUX',
        success:                'green',
        failed:                 'red',
        pending:                'yellow',
        default:                'green',
        timeout:                5,
        display_message:        false,
        default_message_format: '%s - %s',
        default_message_color:  'white',
        line_separator:         ' - ',
        change_color:           true,
        color_location:         'status-left-bg'
      }

      def self.available?(opts = {})
        super and _register!(opts)
      end

      # @private
      #
      # @return [Boolean] whether or not a TMUX environment is available
      #
      def self._tmux_environment_available?(opts)
        !ENV[opts.fetch(:tmux_environment, DEFAULTS[:tmux_environment])].nil?
      end

      # @private
      #
      # Detects if a TMUX environment is available and if not,
      # displays an error message unless `opts[:silent]` is true.
      #
      # @return [Boolean] whether or not a TMUX environment is available
      #
      def self._register!(opts)
        if _tmux_environment_available?(opts)
          true
        else
          unless opts[:silent]
            ::Guard::UI.error 'The :tmux notifier runs only on when Guard is executed inside of a tmux session.'
          end
          false
        end
      end

      # Shows a system notification.
      #
      # By default, the Tmux notifier only makes
      # use of a color based notification, changing the background color of the
      # `color_location` to the color defined in either the `success`,
      # `failed`, `pending` or `default`, depending on the notification type.
      #
      # You may enable an extra explicit message by setting `display_message`
      # to true, and may further disable the colorization by setting
      # `change_color` to false.
      #
      # @param [String] title the notification title
      # @param [Hash] opts additional notification library options
      # @option opts [String] type the notification type. Either 'success',
      #   'pending', 'failed' or 'notify'
      # @option opts [String] message the notification message body
      # @option opts [String] image the path to the notification image
      # @option opts [Boolean] change_color whether to show a color
      #   notification
      # @option opts [String] color_location the location where to draw the
      #   color notification
      # @option opts [Boolean] display_message whether to display a message
      #   or not
      #
      def notify(message, opts = {})
        super
        opts.delete(:image)

        if opts.fetch(:change_color, DEFAULTS[:change_color])
          color_location = opts.fetch(:color_location, DEFAULTS[:color_location])
          color = tmux_color(opts[:type], opts)

          _run_client "set #{ color_location } #{ color }"
        end

        if opts.fetch(:display_message, DEFAULTS[:display_message])
          display_message(opts.delete(:type).to_s, opts.delete(:title), message, opts)
        end
      end

      # Displays a message in the status bar of tmux.
      #
      # @param [String] type the notification type. Either 'success',
      #   'pending', 'failed' or 'notify'
      # @param [String] title the notification title
      # @param [String] message the notification message body
      # @param [Hash] options additional notification library options
      # @option options [Integer] timeout the amount of seconds to show the
      #   message in the status bar
      # @option options [String] success_message_format a string to use as
      #   formatter for the success message.
      # @option options [String] failed_message_format a string to use as
      #   formatter for the failed message.
      # @option options [String] pending_message_format a string to use as
      #   formatter for the pending message.
      # @option options [String] default_message_format a string to use as
      #   formatter when no format per type is defined.
      # @option options [String] success_message_color the success notification
      #   foreground color name.
      # @option options [String] failed_message_color the failed notification
      #   foreground color name.
      # @option options [String] pending_message_color the pending notification
      #   foreground color name.
      # @option options [String] default_message_color a notification
      #   foreground color to use when no color per type is defined.
      # @option options [String] line_separator a string to use instead of a
      #   line-break.
      #
      def display_message(type, title, message, opts = {})
        message_format = opts.fetch("#{ type }_message_format".to_sym, opts.fetch(:default_message_format, DEFAULTS[:default_message_format]))
        message_color = opts.fetch("#{ type }_message_color".to_sym, opts.fetch(:default_message_color, DEFAULTS[:default_message_color]))
        display_time = opts.fetch(:timeout, DEFAULTS[:timeout])
        separator = opts.fetch(:line_separator, DEFAULTS[:line_separator])

        color = tmux_color type, opts
        formatted_message = message.split("\n").join(separator)
        display_message = message_format % [title, formatted_message]

        _run_client "set display-time #{ display_time * 1000 }"
        _run_client "set message-fg #{ message_color }"
        _run_client "set message-bg #{ color }"
        _run_client "display-message '#{ display_message }'"
      end

      # Get the Tmux color for the notification type.
      # You can configure your own color by overwriting the defaults.
      #
      # @param [String] type the notification type
      # @return [String] the name of the emacs color
      #
      def tmux_color(type, opts = {})
        type = type.to_sym
        type = :default unless [:success, :failed, :pending].include?(type)

        opts.fetch(type, DEFAULTS[type])
      end

      # Notification starting, save the current Tmux settings
      # and quiet the Tmux output.
      #
      def turn_on
        unless @options_stored
          _reset_options_store
          `#{ DEFAULTS[:client] } show`.each_line do |line|
            option, _, setting = line.chomp.partition(' ')
            options_store[option] = setting
          end

          @options_stored = true
        end

        _run_client 'set quiet on'
      end

      # Notification stopping. Restore the previous Tmux state
      # if available (existing options are restored, new options
      # are unset) and unquiet the Tmux output.
      #
      def turn_off
        if @options_stored
          @options_store.each do |key, value|
            if value
              _run_client "set #{ key } #{ value }"
            else
              _run_client "set -u #{ key }"
            end
          end
          _reset_options_store
        end

        _run_client 'set quiet off'
      end

      def options_store
        @options_store ||= {}
      end

      private

      def system(args)
        args += " >#{ DEV_NULL } 2>&1" if ENV['GUARD_ENV'] == 'test'
        super
      end

      def _run_client(args)
        system("#{ DEFAULTS[:client] } #{args}")
      end

      # Reset the internal Tmux options store defaults.
      #
      def _reset_options_store
        @options_stored = false
        @options_store = {
          'status-left-bg'  => nil,
          'status-right-bg' => nil,
          'status-left-fg'  => nil,
          'status-right-fg' => nil,
          'message-bg'      => nil,
          'message-fg'      => nil,
          'display-time'    => nil
        }
      end

    end

  end
end