lib/guard/engine.rb
# frozen_string_literal: true
require "forwardable"
require "listen"
require "guard/dsl_describer"
require "guard/guardfile/evaluator"
require "guard/interactor"
require "guard/internals/helpers"
require "guard/internals/queue"
require "guard/internals/session"
require "guard/internals/traps"
require "guard/notifier"
require "guard/runner"
require "guard/ui"
# Guard is the main module for all Guard related modules and classes.
# Also Guard plugins should use this namespace.
module Guard
# Engine is the main orchestrator class.
class Engine
extend Forwardable
include Internals::Helpers
# @private
ERROR_NO_PLUGINS = "No Guard plugins found in Guardfile,"\
" please add at least one."
# Initialize a new Guard::Engine object.
#
# @option options [Boolean] clear if auto clear the UI should be done
# @option options [Boolean] notify if system notifications should be shown
# @option options [Boolean] debug if debug output should be shown
# @option options [Array<String>] group the list of groups to start
# @option options [Array<String>] watchdirs the directories to watch
# @option options [String] guardfile the path to the Guardfile
# @option options [String] inline the inline content of a Guardfile
#
# @return [Guard::Engine] a Guard::Engine instance
def initialize(options = {})
@options = options
Thread.current[:engine] = self
end
def session
@session ||= Guard::Internals::Session.new(options)
end
def evaluator
@evaluator ||= Guardfile::Evaluator.new(options)
end
def to_s
"#<#{self.class}:#{object_id} @options=#{options}>"
end
alias_method :inspect, :to_s
delegate %i[plugins groups watchdirs] => :session
delegate paused?: :_listener
# Evaluate the Guardfile and instantiate internals.
#
def setup
_instantiate
UI.reset_and_clear
if evaluator.inline?
UI.info("Using inline Guardfile.")
elsif evaluator.custom?
UI.info("Using Guardfile at #{evaluator.guardfile_path}.")
end
self
end
# Start Guard by evaluating the `Guardfile`, initializing declared Guard
# plugins and starting the available file change listener.
# Main method for Guard that is called from the CLI when Guard starts.
#
# - Setup Guard internals
# - Evaluate the `Guardfile`
# - Configure Notifiers
# - Initialize the declared Guard plugins
# - Start the available file change listener
#
# @option options [Boolean] clear if auto clear the UI should be done
# @option options [Boolean] notify if system notifications should be shown
# @option options [Boolean] debug if debug output should be shown
# @option options [Array<String>] group the list of groups to start
# @option options [String] watchdirs the director to watch
# @option options [String] guardfile the path to the Guardfile
# @see CLI#start
#
def start
setup
_initialize_listener
_initialize_signal_traps
_initialize_notifier
UI.debug "Guard starts all plugins"
_runner.run(:start)
UI.info "Guard is now watching at '#{session.watchdirs.join("', '")}'"
_listener.start
exitcode = 0
begin
loop do
break if _interactor.foreground == :exit
loop do
break unless _queue.pending?
_queue.process
end
end
rescue Interrupt
rescue SystemExit => e
exitcode = e.status
end
exitcode
ensure
stop
end
def stop
_listener.stop
_interactor.background
UI.debug "Guard stops all plugins"
_runner.run(:stop)
Notifier.disconnect
UI.info "Bye bye...", reset: true
end
# Reload Guardfile and all Guard plugins currently enabled.
# If no scope is given, then the Guardfile will be re-evaluated,
# which results in a stop/start, which makes the reload obsolete.
#
# @param [Hash] scopes hash with a Guard plugin or a group scope
#
def reload(*entries)
entries.flatten!
UI.clear(force: true)
UI.action_with_scopes("Reload", session.scope_titles(entries))
_runner.run(:reload, entries)
end
# Trigger `run_all` on all Guard plugins currently enabled.
#
# @param [Hash] scopes hash with a Guard plugin or a group scope
#
def run_all(*entries)
entries.flatten!
UI.clear(force: true)
UI.action_with_scopes("Run", session.scope_titles(entries))
_runner.run(:run_all, entries)
end
# Pause Guard listening to file changes.
#
def pause(expected = nil)
states = { paused: true, unpaused: false, toggle: !paused? }
key = expected || :toggle
raise ArgumentError, "invalid mode: #{expected.inspect}" unless states.key?(key)
pause = states[key]
return if pause == paused?
_listener.public_send(pause ? :pause : :start)
UI.info "File event handling has been #{pause ? 'paused' : 'resumed'}"
end
def show
DslDescriber.new(self).show
end
# @private
# Asynchronously trigger changes
#
# Currently supported args:
#
# @example Old style hash:
# async_queue_add(modified: ['foo'], added: ['bar'], removed: [])
#
# @example New style signals with args:
# async_queue_add([:pause, :unpaused ])
#
def async_queue_add(changes)
_queue << changes
# Putting interactor in background puts guard into foreground
# so it can handle change notifications
Thread.new { _interactor.background }
end
private
attr_reader :options
def _restart
stop
@session = nil
@_listener = nil
start
end
def _runner
@_runner ||= Runner.new(session)
end
def _queue
@_queue ||= Internals::Queue.new(self, _runner)
end
def _listener
@_listener ||= Listen.send(*session.listener_args, &_listener_callback)
end
def _interactor
@_interactor ||= Interactor.new(self, session.interactor_name == :pry_wrapper)
end
# Instantiate Engine internals based on the `Guard::Guardfile::Result` populated from the `Guardfile` evaluation.
#
# @example Programmatically evaluate a Guardfile
# engine = Guard::Engine.new.setup
#
# @example Programmatically evaluate a Guardfile with a custom Guardfile
# path
#
# options = { guardfile: '/Users/guardfile/MyAwesomeGuardfile' }
# engine = Guard::Engine.new(options).setup
#
# @example Programmatically evaluate a Guardfile with an inline Guardfile
#
# options = { inline: 'guard :rspec' }
# engine = Guard::Engine.new(options).setup
#
def _instantiate
guardfile_result = evaluator.evaluate
guardfile_result_plugins = guardfile_result.plugins
UI.error(ERROR_NO_PLUGINS) if guardfile_result_plugins.empty?
session.guardfile_notification = guardfile_result.notification
session.guardfile_ignore = guardfile_result.ignore
session.guardfile_ignore_bang = guardfile_result.ignore_bang
session.guardfile_scopes = guardfile_result.scopes
session.watchdirs = guardfile_result.directories
session.clearing(guardfile_result.clearing)
_instantiate_logger(guardfile_result.logger.dup)
_instantiate_interactor(guardfile_result.interactor)
_instantiate_groups(guardfile_result.groups)
_instantiate_plugins(guardfile_result_plugins)
end
def _instantiate_interactor(interactor_options)
case interactor_options
when :off
_interactor.interactive = false
when Hash
_interactor.options = interactor_options
end
end
def _instantiate_logger(logger_options)
if logger_options.key?(:level)
UI.level = logger_options.delete(:level)
end
if logger_options.key?(:template)
UI.template = logger_options.delete(:template)
end
UI.options.merge!(logger_options)
end
def _instantiate_groups(groups_hash)
groups_hash.each do |name, options|
groups.add(name, options)
end
end
def _instantiate_plugins(plugins_array)
plugins_array.each do |name, options|
options[:group] = groups.find(options[:group])
plugins.add(name, options)
end
end
def _listener_callback
lambda do |modified, added, removed|
relative_paths = {
modified: _relative_pathnames(modified),
added: _relative_pathnames(added),
removed: _relative_pathnames(removed)
}
async_queue_add(relative_paths)
end
end
def _initialize_listener
ignores = session.guardfile_ignore
_listener.ignore(ignores) unless ignores.empty?
ignores_bang = session.guardfile_ignore_bang
_listener.ignore!(ignores_bang) unless ignores_bang.empty?
end
def _initialize_signal_traps
traps = Internals::Traps
traps.handle("USR1") { async_queue_add(%i(pause paused)) }
traps.handle("USR2") { async_queue_add(%i(pause unpaused)) }
traps.handle("INT") { _interactor.handle_interrupt }
end
def _initialize_notifier
Notifier.connect(session.notify_options)
end
end
end