watchcat 0.5.2-aarch64-linux → 0.6.0-aarch64-linux

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: 28c859a3e1b24251bfb29c094e9af910f641a2154a2b1c2673097e3ec1e446a2
4
- data.tar.gz: 9d0303e99b830e31a966a7458d96ec84a4269294e30d5956b1d63e9fb0c1ee0a
3
+ metadata.gz: b9d81fa2550b74f1fc51df8391f8b83f7bce7e93b180b6753da9fc247d1a67da
4
+ data.tar.gz: e7705470952ff1c3dd9f22191233ea9573bbc980a4fe583849e0f04366927637
5
5
  SHA512:
6
- metadata.gz: 1ac78245c41ac7d7ba242e4fd914c393f11009e7f9843198f9e96adc96ee4c9fdc625d2de35ef52b8d56410732c0b85a4acce99865010551ec9bee2dc5374cab
7
- data.tar.gz: 3b70cdd2e61074bd95461e2bd131db377f7c0eb1c53171d490768042a5f2a310097af02ff26e847054c992704731daf3f3be265ef7b36e228daa717a82f0ddc1
6
+ metadata.gz: 15686a635eee451410f2400c0039f76f924614db1c23a4fd780437659e98a9dc638add34549d2683c269c3bef64dda27e0b872327fbce5c2b9db6626621c224b
7
+ data.tar.gz: 27d15cdfb98d5f8eaece63725e34f3da1d39794eeb6ad77a4efb6d75f6026c715180933fff44cb78dfe7019d57184b53159e3ad448b025bf56c8fddb5b6a3476
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## Unreleased
2
+
3
+ ## 0.6.0
4
+
5
+ * Add `#watch`, `#unwatch`, and `#watched` to the watcher returned by `Watchcat.watch` for dynamically managing watched paths.
6
+ * Add `Watchcat::EventHandler` for handling events with callback methods.
7
+ * Add `Watchcat::Event#src_path` and `Watchcat::Event#dest_path` for move/rename events.
8
+ * Add `patterns`, `ignore_patterns`, and `ignore_directories` options to `Watchcat.watch`.
9
+ * Fix `Watchcat::Executor#stop` so that it is a no-op in a forked child process, and make it idempotent.
10
+
1
11
  ## 0.5.2
2
12
 
3
13
  * Support Ruby 4.0
data/Gemfile CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- # Specify your gem's dependencies in watchcat.gemspec
5
+ gem "fiddle", platforms: %i[ mri windows ]
6
6
  gemspec
data/README.md CHANGED
@@ -8,7 +8,10 @@ This gem uses [Notify](https://github.com/notify-rs/notify) to get notifications
8
8
 
9
9
  ## Platforms
10
10
 
11
- This gem supports Linux, macOS and Windows.
11
+ - Linux: inotify
12
+ - macOS: FSEvents
13
+ - Windows: ReadDirectoryChangesW
14
+ - All platforms: polling (via `force_polling` option)
12
15
 
13
16
  ## Installation
14
17
 
@@ -112,6 +115,111 @@ Watchcat.watch("/tmp/test", filters: { ignore_remove: true, ignore_access: true
112
115
  end
113
116
  ```
114
117
 
118
+ ### Pattern Options
119
+
120
+ You can use the `patterns`, `ignore_patterns`, and `ignore_directories` options to filter events by path or type, using `File.fnmatch` glob patterns:
121
+
122
+ | Name | Description | Default |
123
+ | --------------------- | ------------------------------------------------------------------------| ------- |
124
+ | **patterns** | Only dispatch events where at least one path matches one of the patterns | `[]` |
125
+ | **ignore_patterns** | Skip events where at least one path matches one of the patterns | `[]` |
126
+ | **ignore_directories**| Skip events for directories | `false` |
127
+
128
+ **CAUTION** For `access`/`modify`/`rename` events, notify doesn't tell whether the path is a file or a directory, so `ignore_directories` falls back to a live `File.directory?` check on the path (best-effort; e.g. it can't tell for a path that no longer exists).
129
+
130
+ Example usage:
131
+
132
+ ```ruby
133
+ Watchcat.watch(
134
+ "/tmp/test",
135
+ patterns: ["*.rb", "*.yml"],
136
+ ignore_patterns: ["*.tmp"],
137
+ ignore_directories: true
138
+ ) do |e|
139
+ pp e.paths, e.kind
140
+ end
141
+ ```
142
+
143
+ ### Move (Rename) Events
144
+
145
+ For move/rename events (`e.kind.modify?` and `e.kind.modify.rename?`),
146
+ `Watchcat::Event#src_path` and `#dest_path` give the old and new path without
147
+ having to interpret the raw `paths` array and `RenameMode` yourself:
148
+
149
+ ```ruby
150
+ Watchcat.watch("/tmp/test") do |e|
151
+ if e.kind.modify? && e.kind.modify.rename?
152
+ puts "moved: #{e.src_path} -> #{e.dest_path}"
153
+ end
154
+ end
155
+ ```
156
+
157
+ Platform differences affect what is available:
158
+
159
+ - **Linux**: a `both` event fires with both paths, so `src_path` and
160
+ `dest_path` are both set.
161
+ - **Windows**: `from` and `to` fire as separate events, each with only one
162
+ side set (`src_path` on `from`, `dest_path` on `to`).
163
+ - **macOS**: FSEvents can't distinguish old/new paths, so both `src_path` and
164
+ `dest_path` are `nil`.
165
+
166
+ For non-rename events, both accessors return `nil`.
167
+
168
+ ### Event Handler
169
+
170
+ Instead of writing a single block and branching on `event.kind` yourself, you
171
+ can subclass `Watchcat::EventHandler` and override just the callbacks you
172
+ need:
173
+
174
+ ```ruby
175
+ class MyHandler < Watchcat::EventHandler
176
+ def on_create(event)
177
+ puts "created: #{event.paths[0]}"
178
+ end
179
+
180
+ def on_rename(event)
181
+ puts "moved: #{event.src_path} -> #{event.dest_path}"
182
+ end
183
+ end
184
+
185
+ Watchcat.watch("/tmp/test", handler: MyHandler.new)
186
+ sleep
187
+ ```
188
+
189
+ Pass an instance via the `handler:` keyword instead of a block. `Watchcat::EventHandler` provides the following no-op callbacks to override:
190
+
191
+ | Callback | Description |
192
+ | --------------- | ----------------------------------------------------- |
193
+ | `on_any_event` | Called for every event, before the type-specific callback |
194
+ | `on_create` | Called for create events |
195
+ | `on_modify` | Called for modify events (excluding renames) |
196
+ | `on_remove` | Called for remove events |
197
+ | `on_rename` | Called for rename/move events (`src_path`/`dest_path` available) |
198
+ | `on_access` | Called for access events |
199
+
200
+ ### Dynamically Adding / Removing Paths
201
+
202
+ The watcher returned by `Watchcat.watch` can have paths added or removed while
203
+ it's running:
204
+
205
+ ```ruby
206
+ w = Watchcat.watch("/tmp/a") { |e| pp e.paths, e.kind }
207
+
208
+ w.watch("/tmp/b") # also watch /tmp/b
209
+ w.watch("/tmp/c", recursive: false) # non-recursive
210
+ w.unwatch("/tmp/a") # stop watching /tmp/a
211
+ w.watched # => current watched paths
212
+
213
+ sleep
214
+ ```
215
+
216
+ All watched paths share the single callback/handler passed to `Watchcat.watch`
217
+ (and the same `filters`/`patterns`/`debounce` settings). `recursive:` on `watch`
218
+ defaults to the value passed to `Watchcat.watch`. `watch` raises `ArgumentError`
219
+ immediately if a path does not exist. Applying `unwatch` is asynchronous, so
220
+ its exact timing (and behavior) can differ per platform, notably on macOS
221
+ (FSEvents). Both `watch` and `unwatch` accept a single path or an array of
222
+ paths.
115
223
 
116
224
  ## CLI
117
225
 
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -14,8 +14,45 @@ module Watchcat
14
14
  { paths: @paths, event: @event }
15
15
  end
16
16
 
17
+ def directory?
18
+ if kind.create?
19
+ kind.create.folder?
20
+ elsif kind.remove?
21
+ kind.remove.folder?
22
+ elsif kind.any?
23
+ kind.any.folder?
24
+ else
25
+ File.directory?(@paths.first)
26
+ end
27
+ rescue
28
+ false
29
+ end
30
+
31
+ def src_path
32
+ return nil unless rename_event?
33
+
34
+ rename = kind.modify.rename
35
+ return @paths[0] if rename.both? || rename.from?
36
+
37
+ nil
38
+ end
39
+
40
+ def dest_path
41
+ return nil unless rename_event?
42
+
43
+ rename = kind.modify.rename
44
+ return @paths[1] if rename.both?
45
+ return @paths[0] if rename.to?
46
+
47
+ nil
48
+ end
49
+
17
50
  private
18
51
 
52
+ def rename_event?
53
+ kind.modify? && kind.modify.rename?
54
+ end
55
+
19
56
  def build_kind(kinds)
20
57
  @kind = Watchcat::EventKind.new
21
58
  @event = kinds.shift
@@ -0,0 +1,29 @@
1
+ module Watchcat
2
+ class EventHandler
3
+ def dispatch(event)
4
+ on_any_event(event)
5
+
6
+ kind = event.kind
7
+ if kind.create?
8
+ on_create(event)
9
+ elsif kind.remove?
10
+ on_remove(event)
11
+ elsif kind.modify?
12
+ if kind.modify.rename?
13
+ on_rename(event)
14
+ else
15
+ on_modify(event)
16
+ end
17
+ elsif kind.access?
18
+ on_access(event)
19
+ end
20
+ end
21
+
22
+ def on_any_event(event); end
23
+ def on_create(event); end
24
+ def on_modify(event); end
25
+ def on_remove(event); end
26
+ def on_rename(event); end
27
+ def on_access(event); end
28
+ end
29
+ end
@@ -2,7 +2,7 @@ require_relative "event"
2
2
 
3
3
  module Watchcat
4
4
  class Executor
5
- def initialize(paths, recursive:, force_polling:, poll_interval:, filters:, debounce:, block:)
5
+ def initialize(paths, recursive:, force_polling:, poll_interval:, filters:, debounce:, block:, patterns: [], ignore_patterns: [], ignore_directories: false)
6
6
  @paths = paths
7
7
  @recursive = recursive
8
8
  @force_polling = force_polling
@@ -10,13 +10,20 @@ module Watchcat
10
10
  @filters = filters || {}
11
11
  @debounce = debounce
12
12
  @debouncer = Debouncer.new if @debounce > 0
13
+ @patterns = Array(patterns)
14
+ @ignore_patterns = Array(ignore_patterns)
15
+ @ignore_directories = ignore_directories
13
16
  @block = block
14
17
  @watcher = Watchcat::Watcher.new
15
18
  @watch_thread = nil
16
19
  @stop_requested = false
20
+ @owner_pid = nil
21
+ @stopped = false
17
22
  end
18
23
 
19
24
  def start
25
+ @owner_pid = Process.pid
26
+
20
27
  # Always start watching in a background thread to avoid blocking
21
28
  @watch_thread = Thread.new do
22
29
  Thread.current.name = "watchcat-watcher"
@@ -29,6 +36,15 @@ module Watchcat
29
36
  end
30
37
 
31
38
  def stop
39
+ # A forked child inherits the at_exit hook without inheriting the watcher
40
+ # thread, so it would call #close -- which sends on a channel, and a send
41
+ # can allocate. If the watcher thread held the allocator lock at fork
42
+ # time, that lock is never released in the child.
43
+ return if @owner_pid != Process.pid
44
+
45
+ return if @stopped
46
+ @stopped = true
47
+
32
48
  @stop_requested = true
33
49
  @watcher.close
34
50
  if @watch_thread && @watch_thread.alive?
@@ -36,6 +52,25 @@ module Watchcat
36
52
  end
37
53
  end
38
54
 
55
+ def watch(paths, recursive: @recursive)
56
+ paths = Array(paths)
57
+ paths.each { |p| raise ArgumentError, "path does not exist: #{p}" unless File.exist?(p) }
58
+ @watcher.add(paths, recursive: recursive)
59
+ @paths |= paths
60
+ self
61
+ end
62
+
63
+ def unwatch(paths)
64
+ paths = Array(paths)
65
+ @watcher.unwatch(paths)
66
+ @paths -= paths
67
+ self
68
+ end
69
+
70
+ def watched
71
+ @paths.dup
72
+ end
73
+
39
74
  private
40
75
 
41
76
  def start_watching
@@ -49,18 +84,31 @@ module Watchcat
49
84
  ignore_create: @filters[:ignore_create],
50
85
  ignore_modify: @filters[:ignore_modify]
51
86
  ) do |kind, paths, raw_kind|
52
- break if @stop_requested
87
+ next if @stop_requested
88
+
89
+ event = Watchcat::Event.new(kind, paths, raw_kind)
90
+ next unless dispatch?(event)
53
91
 
54
92
  if @debounce > 0 && paths.size == 1
55
- @debouncer.debounce(paths[0], @debounce) do
56
- event = Watchcat::Event.new(kind, paths, raw_kind)
57
- @block.call(event)
58
- end
93
+ @debouncer.debounce(paths[0], @debounce) { @block.call(event) }
59
94
  else
60
- event = Watchcat::Event.new(kind, paths, raw_kind)
61
95
  @block.call(event)
62
96
  end
63
97
  end
64
98
  end
99
+
100
+ def dispatch?(event)
101
+ return false if @ignore_directories && event.directory?
102
+ return false if @patterns.any? && !matches_any_pattern?(event.paths, @patterns)
103
+ return false if @ignore_patterns.any? && matches_any_pattern?(event.paths, @ignore_patterns)
104
+
105
+ true
106
+ end
107
+
108
+ def matches_any_pattern?(paths, patterns)
109
+ paths.any? do |path|
110
+ patterns.any? { |pattern| File.fnmatch?(pattern, File.basename(path)) || File.fnmatch?(pattern, path) }
111
+ end
112
+ end
65
113
  end
66
114
  end
@@ -1,3 +1,3 @@
1
1
  module Watchcat
2
- VERSION = "0.5.2"
2
+ VERSION = "0.6.0"
3
3
  end
data/lib/watchcat.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require_relative "watchcat/version"
2
2
  require_relative "watchcat/executor"
3
3
  require_relative "watchcat/debouncer"
4
+ require_relative "watchcat/event_handler"
4
5
 
5
6
  begin
6
7
  require "watchcat/#{RUBY_VERSION.to_f}/watchcat"
@@ -17,8 +18,15 @@ module Watchcat
17
18
  poll_interval: nil,
18
19
  filters: {},
19
20
  debounce: -1,
21
+ patterns: [],
22
+ ignore_patterns: [],
23
+ ignore_directories: false,
24
+ handler: nil,
20
25
  &block
21
26
  )
27
+ callback = block || (handler && handler.method(:dispatch))
28
+ raise ArgumentError, "must provide a block or a handler:" unless callback
29
+
22
30
  w =
23
31
  Watchcat::Executor.new(
24
32
  Array(paths),
@@ -27,7 +35,10 @@ module Watchcat
27
35
  poll_interval: poll_interval,
28
36
  filters: filters,
29
37
  debounce: debounce,
30
- block: block
38
+ patterns: patterns,
39
+ ignore_patterns: ignore_patterns,
40
+ ignore_directories: ignore_directories,
41
+ block: callback
31
42
  )
32
43
  w.start
33
44
  w
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: watchcat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.6.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Yuji Yaginuma
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-18 00:00:00.000000000 Z
11
+ date: 2026-08-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: psych
@@ -164,6 +164,7 @@ files:
164
164
  - lib/watchcat/cli/watcher.rb
165
165
  - lib/watchcat/debouncer.rb
166
166
  - lib/watchcat/event.rb
167
+ - lib/watchcat/event_handler.rb
167
168
  - lib/watchcat/executor.rb
168
169
  - lib/watchcat/kind.rb
169
170
  - lib/watchcat/version.rb