watchly 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f99ea235b8e5f9266131b1e25353c54c6a5c725cb9fca3c2ec7a503ae84bd73b
4
- data.tar.gz: 170a74f2b7e9642f57d19a141b65b3ab50d20199de44c9f48f0d5a16692732c5
3
+ metadata.gz: 6c133cfc132b5dcfb5c57e0835bd80e2dd7239af7c1631f9d72ccfc7acbb8f60
4
+ data.tar.gz: 2516f37a4cd11b70705b57a77b26d803989322d07cc265751e246dd5394db9ab
5
5
  SHA512:
6
- metadata.gz: 61e82c4f0f9077a6af68fe057ece3bffacd1030eaeab69f77b6476b71f2dfa172d0f3f934552a45b9e6cd2d1d898f35e896dd5bf1d8dca59d53e16b2dca47a1c
7
- data.tar.gz: 30bcceb7691daab5d6def2ff15a5284ebcb89c2a492c83020713a8bea3db9ec470ed732dccc197e65f8f507e1e6a882a1e7ec884635a1286a2d797dac4c4d6f1
6
+ metadata.gz: 51d950ab49d3e18e064ee3e24263789aefe7b2f4614d78f5a98e858fdc00b75a2992f4c24d02f841f76e2a38f058e9b4960de048b019a61e11f3916c9c6733b1
7
+ data.tar.gz: b0a543c5fd955d3d4fb206cbfbb793f2a18e2c67d9dca68feb09ccb98647cfc73d42a0a9b91128b8561633701c74b6a3d3af89ebd8472a61a1403753788ed2ae
data/README.md CHANGED
@@ -1,7 +1,18 @@
1
1
  # Watchly
2
2
 
3
+ ![repocard](https://repocard.dannyben.com/svg/watchly.svg)
4
+
3
5
  Watchly is a lightweight, dependency-free, polling-based file watcher for Ruby.
4
- It watches one or more glob patterns and reports changes.
6
+ It watches one or more files, directories, or glob patterns and reports file
7
+ changes in a simple, predictable way.
8
+
9
+ Watchly is designed around *batch semantics*: when files change, a single
10
+ callback is triggered with a complete description of what changed.
11
+
12
+ An official command-line interface is available via the
13
+ [watchly-cli][watchly-cli] gem.
14
+
15
+ ---
5
16
 
6
17
  ## Installation
7
18
 
@@ -15,9 +26,11 @@ Or in your Gemfile:
15
26
  gem 'watchly'
16
27
  ```
17
28
 
29
+ ---
30
+
18
31
  ## Usage
19
32
 
20
- **Initialize a watcher:**
33
+ ### Initialize a watcher
21
34
 
22
35
  ```ruby
23
36
  require 'watchly'
@@ -28,30 +41,135 @@ watcher = Watchly::Watcher.new '**/*'
28
41
  # with a different interval
29
42
  watcher = Watchly::Watcher.new '**/*', interval: 2.0
30
43
 
31
- # with multiple glob patterns
44
+ # watch a directory recursively
45
+ watcher = Watchly::Watcher.new 'src'
46
+
47
+ # with multiple targets
32
48
  watcher = Watchly::Watcher.new 'spec/**/*.rb', 'lib/*.*'
33
49
  ```
34
50
 
35
- **Watch for changes:**
51
+ Directory targets are watched recursively. Like Ruby's default glob behavior,
52
+ implicit directory expansion excludes dotfiles. Dotfiles can still be watched
53
+ when passed as literal files or matched by an explicit glob pattern.
54
+
55
+ ---
56
+
57
+ ### Watch for changes
58
+
59
+ When changes are detected, Watchly yields a Changeset object describing
60
+ all filesystem changes since the last poll.
61
+
62
+ ```ruby
63
+ watcher.on_change do |changes|
64
+ changes.each do |type, path|
65
+ puts "#{type}: #{path}"
66
+ end
67
+ end
68
+ ```
69
+
70
+ This will output events such as:
71
+
72
+ ```
73
+ added: spec/new_spec.rb
74
+ modified: lib/watchly.rb
75
+ removed: tmp/debug.log
76
+ ```
77
+
78
+ ---
79
+
80
+ ### Accessing specific change types
81
+
82
+ The `changes` object exposes separate collections for each type of change:
83
+
84
+ - `changes.added`
85
+ - `changes.removed`
86
+ - `changes.modified`
87
+
88
+ Example:
89
+
90
+ ```ruby
91
+ watcher.on_change do |changes|
92
+ puts "Added files:"
93
+ changes.added.each { |path| puts " + #{path}" }
94
+
95
+ puts "Modified files:"
96
+ changes.modified.each { |path| puts " * #{path}" }
97
+
98
+ puts "Removed files:"
99
+ changes.removed.each { |path| puts " - #{path}" }
100
+ end
101
+ ```
102
+
103
+ ---
104
+
105
+ ### Working with actionable files
106
+
107
+ In many cases, you only want to work with files that currently exist.
108
+
109
+ For this purpose, `changes.files` provides a convenient, pre-filtered list of
110
+ **actionable files**:
111
+
112
+ - Includes: added and modified files
113
+ - Excludes: removed files
114
+
115
+ ```ruby
116
+ watcher.on_change do |changes|
117
+ next if changes.empty?
118
+
119
+ puts "Processing files:"
120
+ changes.files.each do |path|
121
+ puts " - #{path}"
122
+ end
123
+ end
124
+ ```
125
+
126
+ A changeset that only contains removed files is considered empty.
127
+ This prevents unnecessary processing when files are deleted.
128
+
129
+ ---
130
+
131
+ ### Checking for changes
132
+
133
+ You can test whether a changeset contains any actionable files:
36
134
 
37
135
  ```ruby
38
136
  watcher.on_change do |changes|
39
- puts "Added: #{changes.added.join(', ')}" if changes.added.any?
40
- puts "Removed: #{changes.removed.join(', ')}" if changes.removed.any?
41
- puts "Modified: #{changes.modified.join(', ')}" if changes.modified.any?
137
+ if changes.any?
138
+ puts "Files changed:"
139
+ changes.files.each { |path| puts " - #{path}" }
140
+ else
141
+ puts "Files were removed:"
142
+ changes.removed.each { |path| puts " - #{path}" }
143
+ end
42
144
  end
43
145
  ```
44
146
 
45
- **Stop the watcher:**
147
+ Or explicitly:
46
148
 
47
149
  ```ruby
48
- # Mainly for tests, but can be called from another thread
150
+ changes.empty? # => true if no added or modified files
151
+ changes.any? # => true if at least one actionable file exists
152
+ ```
153
+
154
+ The on_change callback is triggered for any filesystem change, including deletions.
155
+ The `any?` and `empty?` helpers reflect whether there are actionable files
156
+ (added or modified).
157
+
158
+ ---
159
+
160
+ ### Stopping the watcher
161
+
162
+ ```ruby
163
+ # Mainly useful for tests, but can be called from another thread
49
164
  watcher.stop
50
165
  ```
51
166
 
167
+ ---
168
+
52
169
  ## Contributing / Support
53
170
 
54
- If you experience any issue, have a question, or if you wish
55
- to contribute, feel free to [open an issue][issues].
171
+ If you experience any issue, have a question, or wish to contribute,
172
+ feel free to [open an issue][issues].
56
173
 
57
174
  [issues]: https://github.com/dannyben/watchly/issues
175
+ [watchly-cli]: https://github.com/dannyben/watchly-cli
@@ -1,17 +1,18 @@
1
1
  module Watchly
2
2
  class Changeset
3
- attr_reader :added, :removed, :modified
3
+ attr_reader :added, :removed, :modified, :files
4
4
 
5
5
  def initialize(added:, removed:, modified:)
6
6
  @added = added.freeze
7
7
  @removed = removed.freeze
8
8
  @modified = modified.freeze
9
+ @files = (added + modified).freeze
9
10
  freeze
10
11
  end
11
12
 
12
- def empty? = added.empty? && removed.empty? && modified.empty?
13
+ def empty? = files.empty?
13
14
  def any? = !empty?
14
- def to_h = { added: added, removed: removed, modified: modified }
15
+ def to_h = { added: added, removed: removed, modified: modified, files: files }
15
16
 
16
17
  def each
17
18
  return enum_for(:each) unless block_given?
@@ -1,10 +1,11 @@
1
1
  module Watchly
2
2
  class Snapshot
3
- attr_reader :globs, :files
3
+ attr_reader :targets, :files
4
+ alias globs targets
4
5
 
5
- def initialize(globs)
6
- @globs = globs
7
- @files = capture(globs)
6
+ def initialize(targets)
7
+ @targets = targets
8
+ @files = capture(targets)
8
9
  end
9
10
 
10
11
  def diff(other)
@@ -32,16 +33,22 @@ module Watchly
32
33
  end
33
34
  end
34
35
 
35
- def capture(globs)
36
- Array(globs)
37
- .flat_map { |glob| Dir.glob glob }
38
- .uniq
36
+ def capture(targets)
37
+ Array(targets)
38
+ .flat_map { |target| expand_target target }
39
+ .uniq { |path| File.expand_path path }
39
40
  .each_with_object({}) do |path, acc|
40
41
  next unless File.file? path
41
42
 
42
43
  stat = File.stat path
43
- acc[path] = [stat.mtime.to_i, stat.size]
44
+ acc[path] = [stat.mtime, stat.size]
44
45
  end
45
46
  end
47
+
48
+ def expand_target(target)
49
+ return Dir.glob File.join(target, '**', '*') if File.directory? target
50
+
51
+ Dir.glob target
52
+ end
46
53
  end
47
54
  end
@@ -1,3 +1,3 @@
1
1
  module Watchly
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -2,10 +2,11 @@ module Watchly
2
2
  class Watcher
3
3
  DEFAULT_INTERVAL = 1.0
4
4
 
5
- attr_reader :globs, :interval, :stopped
5
+ attr_reader :targets, :interval, :stopped
6
+ alias globs targets
6
7
 
7
- def initialize(*globs, interval: nil)
8
- @globs = globs
8
+ def initialize(*targets, interval: nil)
9
+ @targets = targets
9
10
  @interval = interval || DEFAULT_INTERVAL
10
11
  @stopped = false
11
12
  end
@@ -15,12 +16,12 @@ module Watchly
15
16
  def on_change
16
17
  raise ArgumentError, 'Block required' unless block_given?
17
18
 
18
- previous = Snapshot.new globs
19
+ previous = Snapshot.new targets
19
20
 
20
21
  until stopped
21
22
  sleep interval
22
23
 
23
- current = Snapshot.new globs
24
+ current = Snapshot.new targets
24
25
  next if previous == current
25
26
 
26
27
  changes = previous.diff current
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: watchly
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danny Ben Shitrit
@@ -10,7 +10,7 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: A small, dependency-free, polling-based library that watches one or more
13
- glob patterns and reports on change
13
+ files, directories, or glob patterns and reports on change
14
14
  email: db@dannyben.com
15
15
  executables: []
16
16
  extensions: []
@@ -44,7 +44,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
44
44
  - !ruby/object:Gem::Version
45
45
  version: '0'
46
46
  requirements: []
47
- rubygems_version: 4.0.3
47
+ rubygems_version: 4.0.17
48
48
  specification_version: 4
49
49
  summary: Lightweight, polling-based file system watcher
50
50
  test_files: []