Skip to content

Commit

Permalink
Make Listen::Listener capable of listening to multiple directories
Browse files Browse the repository at this point in the history
It also deprecates Listen::MultiListener since all the logic has been
generalized and moved to Listen::Listener.
  • Loading branch information
rymai committed Apr 6, 2013
1 parent 81030fd commit e11be0f
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 415 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -10,6 +10,8 @@

### Improvements

- [#95][] Make `Listen::Listener` capable of listening to multiple directories. ([@rymai][])
- [#95][] Deprecate `Listen::MultiListener`. ([@rymai][])
- Add `Listen::Adapter#pause`, `Listen::Adapter#unpause` and `Listen::Adapter#paused?`. ([@rymai][])
- Refactor `Listen::DirectoryRecord` internals. ([@rymai][])
- Refactor `Listen::DependencyManager` internals. ([@rymai][])
Expand Down Expand Up @@ -227,6 +229,7 @@
[#85]: https://github.com/guard/listen/issues/85
[#88]: https://github.com/guard/listen/issues/88
[#93]: https://github.com/guard/listen/issues/93
[#95]: https://github.com/guard/listen/issues/95
[@Maher4Ever]: https://github.com/Maher4Ever
[@akerbos]: https://github.com/akerbos
[@antifuchs]: https://github.com/antifuchs
Expand Down
28 changes: 2 additions & 26 deletions README.md
Expand Up @@ -81,30 +81,6 @@ listener.unpause
listener.stop
```

## Listening to changes on multiple directories

The Listen gem provides the `MultiListener` class to watch multiple directories and
handle their changes from a single listener:

```ruby
listener = Listen::MultiListener.new('app/css', 'app/js')
listener.latency(0.5)

# Configure the listener to your needs...

listener.start # blocks execution!
````

For an easier access, the `Listen.to` method can also be used to create a multi-listener:

``` ruby
listener = Listen.to('app/css', 'app/js')
.ignore(%r{^vendor/}) # both js/vendor and css/vendor will be ignored
.change(&assets_callback)
listener.start # blocks execution!
```

## Changes callback

Changes to the listened-to directories gets reported back to the user in a callback.
Expand Down Expand Up @@ -168,7 +144,7 @@ Listen.to('/home/user/app/css', :relative_paths => true) do |modified, added, re
end
```

Passing the `:relative_paths => true` option won't work when listeneing to multiple
Passing the `:relative_paths => true` option won't work when listening to multiple
directories:

```ruby
Expand Down Expand Up @@ -224,7 +200,7 @@ Starting a listener blocks the current thread by default. That means any code af

For advanced usage there is an option to disable this behavior and have the listener start working
in the background without blocking. To enable non-blocking listening the `start` method of
the listener (be it `Listener` or `MultiListener`) needs to be called with `false` as a parameter.
the listener needs to be called with `false` as a parameter.

Here is an example of using a listener in the non-blocking mode:

Expand Down
6 changes: 1 addition & 5 deletions lib/listen.rb
Expand Up @@ -25,11 +25,7 @@ module Adapters
# @return [Listen::Listener] the file listener if no block given
#
def self.to(*args, &block)
listener = if args.length == 1 || ! args[1].is_a?(String)
Listener.new(*args, &block)
else
MultiListener.new(*args, &block)
end
listener = Listener.new(*args, &block)

block ? listener.start : listener
end
Expand Down
82 changes: 57 additions & 25 deletions lib/listen/listener.rb
Expand Up @@ -2,14 +2,11 @@

module Listen
class Listener
attr_reader :directory, :directory_record, :adapter
attr_reader :directories, :directories_records, :adapter

# The default value for using relative paths in the callback.
DEFAULT_TO_RELATIVE_PATHS = false

# Initializes the directory listener.
# Initializes the directories listener.
#
# @param [String] directory the directory to listen to
# @param [String] directory the directories to listen to
# @param [Hash] options the listen options
# @option options [Regexp] ignore a pattern for ignoring paths
# @option options [Regexp] filter a pattern for filtering paths
Expand All @@ -23,15 +20,15 @@ class Listener
# @yieldparam [Array<String>] added the list of added files
# @yieldparam [Array<String>] removed the list of removed files
#
def initialize(directory, options = {}, &block)
@block = block
@directory = Pathname.new(directory).realpath.to_s
@directory_record = DirectoryRecord.new(@directory)
@use_relative_paths = DEFAULT_TO_RELATIVE_PATHS
def initialize(*args, &block)
options = args.last.is_a?(Hash) ? args.pop : {}
directories = args.flatten
initialize_directories_and_directories_records(directories)
initialize_relative_paths_usage(options)
@block = block

@use_relative_paths = options.delete(:relative_paths) if options[:relative_paths]
@directory_record.ignore(*options.delete(:ignore)) if options[:ignore]
@directory_record.filter(*options.delete(:filter)) if options[:filter]
ignore(*options.delete(:ignore))
filter(*options.delete(:filter))

@adapter_options = options
end
Expand All @@ -43,7 +40,7 @@ def initialize(directory, options = {}, &block)
# @param [Boolean] blocking whether or not to block the current thread after starting
#
def start(blocking = true)
t = Thread.new { @directory_record.build }
t = Thread.new { build_directories_records }
@adapter = initialize_adapter
t.join
@adapter.start(blocking)
Expand All @@ -69,7 +66,7 @@ def pause
# @return [Listen::Listener] the listener
#
def unpause
@directory_record.build
build_directories_records
@adapter.unpause
self
end
Expand All @@ -91,7 +88,7 @@ def paused?
# @see Listen::DirectoryRecord#ignore
#
def ignore(*regexps)
@directory_record.ignore(*regexps)
@directories_records.each { |r| r.ignore(*regexps) }
self
end

Expand All @@ -104,7 +101,7 @@ def ignore(*regexps)
# @see Listen::DirectoryRecord#ignore!
#
def ignore!(*regexps)
@directory_record.ignore!(*regexps)
@directories_records.each { |r| r.ignore!(*regexps) }
self
end

Expand All @@ -117,7 +114,7 @@ def ignore!(*regexps)
# @see Listen::DirectoryRecord#filter
#
def filter(*regexps)
@directory_record.filter(*regexps)
@directories_records.each { |r| r.filter(*regexps) }
self
end

Expand All @@ -130,7 +127,7 @@ def filter(*regexps)
# @see Listen::DirectoryRecord#filter!
#
def filter!(*regexps)
@directory_record.filter!(*regexps)
@directories_records.each { |r| r.filter!(*regexps) }
self
end

Expand Down Expand Up @@ -215,21 +212,56 @@ def change(&block) # modified, added, removed
# @see Listen::DirectoryRecord#fetch_changes
#
def on_change(directories, options = {})
changes = @directory_record.fetch_changes(directories, options.merge(
:relative_paths => @use_relative_paths
))
changes = fetch_records_changes(directories, options)
unless changes.values.all? { |paths| paths.empty? }
@block.call(changes[:modified],changes[:added],changes[:removed])
@block.call(changes[:modified], changes[:added], changes[:removed])
end
end

private

def initialize_directories_and_directories_records(directories)
@directories = directories.map { |d| Pathname.new(d).realpath.to_s }
@directories_records = @directories.map { |d| DirectoryRecord.new(d) }
end

def initialize_relative_paths_usage(options)
@use_relative_paths = @directories.one? && options.delete(:relative_paths) { true }
end

# Initializes an adapter passing it the callback and adapters' options.
#
def initialize_adapter
callback = lambda { |changed_dirs, options| self.on_change(changed_dirs, options) }
Adapter.select_and_initialize(@directory, @adapter_options, &callback)
Adapter.select_and_initialize(@directories, @adapter_options, &callback)
end

# Build the watched directories' records.
#
def build_directories_records
@directories_records.each { |r| r.build }
end

# Returns the sum of all the changes to the directories records
#
# @param (see Listen::DirectoryRecord#fetch_changes)
#
# @return [Hash] the changes
#
def fetch_records_changes(directories_to_search, options)
@directories_records.inject({}) do |h, r|
# directory records skips paths outside their range, so passing the
# whole `directories` array is not a problem.
record_changes = r.fetch_changes(directories_to_search, options.merge(:relative_paths => @use_relative_paths))

if h.empty?
h.merge!(record_changes)
else
h.each { |k, v| h[k] += record_changes[k] }
end

h
end
end

end
Expand Down
138 changes: 5 additions & 133 deletions lib/listen/multi_listener.rb
@@ -1,143 +1,15 @@
module Listen
class MultiListener < Listener
attr_reader :directories, :directories_records, :adapter

# Initializes the multiple directories listener.
# This class is deprecated, please use Listen::Listener instead.
#
# @param [String] directories the directories to listen to
# @param [Hash] options the listen options
# @option options [Regexp] ignore a pattern for ignoring paths
# @option options [Regexp] filter a pattern for filtering paths
# @option options [Float] latency the delay between checking for changes in seconds
# @option options [Boolean] force_polling whether to force the polling adapter or not
# @option options [String, Boolean] polling_fallback_message to change polling fallback message or remove it
#
# @yield [modified, added, removed] the changed files
# @yieldparam [Array<String>] modified the list of modified files
# @yieldparam [Array<String>] added the list of added files
# @yieldparam [Array<String>] removed the list of removed files
# @see Listen::Listener
# @deprecated
#
def initialize(*args, &block)
options = args.last.is_a?(Hash) ? args.pop : {}
directories = args

@block = block
@directories = directories.map { |d| Pathname.new(d).realpath.to_s }
@directories_records = @directories.map { |d| DirectoryRecord.new(d) }

ignore(*options.delete(:ignore)) if options[:ignore]
filter(*options.delete(:filter)) if options[:filter]

@adapter_options = options
end

# Starts the listener by initializing the adapter and building
# the directory record concurrently, then it starts the adapter to watch
# for changes.
#
# @param [Boolean] blocking whether or not to block the current thread after starting
#
def start(blocking = true)
t = Thread.new { @directories_records.each { |r| r.build } }
@adapter = initialize_adapter
t.join
@adapter.start(blocking)
end

# Unpauses the listener.
#
# @return [Listen::Listener] the listener
#
def unpause
@directories_records.each { |r| r.build }
@adapter.unpause
self
end

# Adds ignored paths to the listener.
#
# @param (see Listen::DirectoryRecord#ignore)
#
# @return [Listen::Listener] the listener
#
def ignore(*paths)
@directories_records.each { |r| r.ignore(*paths) }
self
end

# Replaces ignored paths in the listener.
#
# @param (see Listen::DirectoryRecord#ignore!)
#
# @return [Listen::Listener] the listener
#
def ignore!(*paths)
@directories_records.each { |r| r.ignore!(*paths) }
self
end

# Adds file filters to the listener.
#
# @param (see Listen::DirectoryRecord#filter)
#
# @return [Listen::Listener] the listener
#
def filter(*regexps)
@directories_records.each { |r| r.filter(*regexps) }
self
end

# Replaces file filters in the listener.
#
# @param (see Listen::DirectoryRecord#filter!)
#
# @return [Listen::Listener] the listener
#
def filter!(*regexps)
@directories_records.each { |r| r.filter!(*regexps) }
self
puts "[DEPRECATED] Listen::MultiListener is deprecated, please use Listen::Listener instead."
super
end

# Runs the callback passing it the changes if there are any.
#
# @param (see Listen::DirectoryRecord#fetch_changes)
#
def on_change(directories_to_search, options = {})
changes = fetch_records_changes(directories_to_search, options)
unless changes.values.all? { |paths| paths.empty? }
@block.call(changes[:modified],changes[:added],changes[:removed])
end
end

private

# Initializes an adapter passing it the callback and adapters' options.
#
def initialize_adapter
callback = lambda { |changed_dirs, options| self.on_change(changed_dirs, options) }
Adapter.select_and_initialize(@directories, @adapter_options, &callback)
end

# Returns the sum of all the changes to the directories records
#
# @param (see Listen::DirectoryRecord#fetch_changes)
#
# @return [Hash] the changes
#
def fetch_records_changes(directories_to_search, options)
@directories_records.inject({}) do |h, r|
# directory records skips paths outside their range, so passing the
# whole `directories` array is not a problem.
record_changes = r.fetch_changes(directories_to_search, options.merge(:relative_paths => DEFAULT_TO_RELATIVE_PATHS))

if h.empty?
h.merge!(record_changes)
else
h.each { |k, v| h[k] += record_changes[k] }
end

h
end
end
end
end

0 comments on commit e11be0f

Please sign in to comment.