watchcat 0.3.0 → 0.6.1

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.
@@ -0,0 +1,41 @@
1
+ module Watchcat
2
+ class Debouncer
3
+ def initialize
4
+ @timers = {}
5
+ @mutex = Mutex.new
6
+ end
7
+
8
+ def debounce(key, delay_ms, &block)
9
+ @mutex.synchronize do
10
+ # Cancel existing timer for this key
11
+ if @timers[key]
12
+ @timers[key].kill
13
+ end
14
+
15
+ # Create new timer
16
+ @timers[key] = Thread.new do
17
+ sleep(delay_ms / 1000.0) # Convert ms to seconds
18
+
19
+ @mutex.synchronize do
20
+ @timers.delete(key)
21
+ end
22
+
23
+ block.call
24
+ end
25
+ end
26
+ end
27
+
28
+ def clear
29
+ @mutex.synchronize do
30
+ @timers.each_value(&:kill)
31
+ @timers.clear
32
+ end
33
+ end
34
+
35
+ def pending_count
36
+ @mutex.synchronize do
37
+ @timers.size
38
+ end
39
+ end
40
+ end
41
+ end
@@ -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,38 +2,49 @@ require_relative "event"
2
2
 
3
3
  module Watchcat
4
4
  class Executor
5
- def initialize(paths, recursive:, force_polling:, poll_interval:, wait_until_startup:, ignore_remove:, 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
9
9
  @poll_interval = poll_interval
10
- @wait_until_startup = wait_until_startup
11
- @ignore_remove = ignore_remove
10
+ @filters = filters || {}
12
11
  @debounce = debounce
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"
23
30
  start_watching
24
31
  end
25
32
 
26
- # If wait_until_startup is true, give the thread a moment to start
27
- if @wait_until_startup
28
- sleep 0.1
29
- end
30
-
31
33
  at_exit do
32
34
  stop
33
35
  end
34
36
  end
35
37
 
36
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
+
37
48
  @stop_requested = true
38
49
  @watcher.close
39
50
  if @watch_thread && @watch_thread.alive?
@@ -41,6 +52,31 @@ module Watchcat
41
52
  end
42
53
  end
43
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
+
74
+ # Whether the background watcher thread is still running. `false` before
75
+ # `#start` is called, after `#stop`, or if the thread died unexpectedly.
76
+ def alive?
77
+ !@watch_thread.nil? && @watch_thread.alive?
78
+ end
79
+
44
80
  private
45
81
 
46
82
  def start_watching
@@ -49,14 +85,35 @@ module Watchcat
49
85
  recursive: @recursive,
50
86
  force_polling: @force_polling,
51
87
  poll_interval: @poll_interval,
52
- ignore_remove: @ignore_remove,
53
- debounce: @debounce
88
+ ignore_remove: @filters[:ignore_remove],
89
+ ignore_access: @filters[:ignore_access],
90
+ ignore_create: @filters[:ignore_create],
91
+ ignore_modify: @filters[:ignore_modify]
54
92
  ) do |kind, paths, raw_kind|
55
- break if @stop_requested
93
+ next if @stop_requested
56
94
 
57
- # Create an event object and call the block
58
95
  event = Watchcat::Event.new(kind, paths, raw_kind)
59
- @block.call(event)
96
+ next unless dispatch?(event)
97
+
98
+ if @debounce > 0 && paths.size == 1
99
+ @debouncer.debounce(paths[0], @debounce) { @block.call(event) }
100
+ else
101
+ @block.call(event)
102
+ end
103
+ end
104
+ end
105
+
106
+ def dispatch?(event)
107
+ return false if @ignore_directories && event.directory?
108
+ return false if @patterns.any? && !matches_any_pattern?(event.paths, @patterns)
109
+ return false if @ignore_patterns.any? && matches_any_pattern?(event.paths, @ignore_patterns)
110
+
111
+ true
112
+ end
113
+
114
+ def matches_any_pattern?(paths, patterns)
115
+ paths.any? do |path|
116
+ patterns.any? { |pattern| File.fnmatch?(pattern, File.basename(path)) || File.fnmatch?(pattern, path) }
60
117
  end
61
118
  end
62
119
  end
data/lib/watchcat/kind.rb CHANGED
@@ -27,6 +27,14 @@ module Watchcat
27
27
  def any?
28
28
  !@any.nil?
29
29
  end
30
+
31
+ def event_type
32
+ return "create" if create?
33
+ return "modify" if modify?
34
+ return "remove" if remove?
35
+ return "access" if access?
36
+ "unknown"
37
+ end
30
38
  end
31
39
 
32
40
  class AccessKind
@@ -1,3 +1,3 @@
1
1
  module Watchcat
2
- VERSION = "0.3.0"
2
+ VERSION = "0.6.1"
3
3
  end
data/lib/watchcat.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require_relative "watchcat/version"
2
2
  require_relative "watchcat/executor"
3
+ require_relative "watchcat/debouncer"
4
+ require_relative "watchcat/event_handler"
3
5
 
4
6
  begin
5
7
  require "watchcat/#{RUBY_VERSION.to_f}/watchcat"
@@ -14,21 +16,29 @@ module Watchcat
14
16
  recursive: true,
15
17
  force_polling: false,
16
18
  poll_interval: nil,
17
- wait_until_startup: false,
18
- ignore_remove: false,
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),
25
33
  recursive: recursive,
26
34
  force_polling: force_polling,
27
35
  poll_interval: poll_interval,
28
- wait_until_startup: wait_until_startup,
29
- ignore_remove: ignore_remove,
36
+ filters: filters,
30
37
  debounce: debounce,
31
- block: block
38
+ patterns: patterns,
39
+ ignore_patterns: ignore_patterns,
40
+ ignore_directories: ignore_directories,
41
+ block: callback
32
42
  )
33
43
  w.start
34
44
  w
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: watchcat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuji Yaginuma
8
- bindir: bin
8
+ autorequire:
9
+ bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-08-08 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: rb_sys
@@ -23,6 +24,20 @@ dependencies:
23
24
  - - ">="
24
25
  - !ruby/object:Gem::Version
25
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: psych
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
26
41
  - !ruby/object:Gem::Dependency
27
42
  name: debug
28
43
  requirement: !ruby/object:Gem::Requirement
@@ -65,6 +80,20 @@ dependencies:
65
80
  - - ">="
66
81
  - !ruby/object:Gem::Version
67
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: minitest-fail-fast
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
68
97
  - !ruby/object:Gem::Dependency
69
98
  name: rake
70
99
  requirement: !ruby/object:Gem::Requirement
@@ -121,9 +150,11 @@ dependencies:
121
150
  - - ">="
122
151
  - !ruby/object:Gem::Version
123
152
  version: '0'
153
+ description:
124
154
  email:
125
155
  - yuuji.yaginuma@gmail.com
126
- executables: []
156
+ executables:
157
+ - watchcat
127
158
  extensions:
128
159
  - ext/watchcat/extconf.rb
129
160
  extra_rdoc_files: []
@@ -132,10 +163,11 @@ files:
132
163
  - Cargo.lock
133
164
  - Cargo.toml
134
165
  - Gemfile
135
- - Gemfile.lock
136
166
  - LICENSE.txt
137
167
  - README.md
138
168
  - Rakefile
169
+ - cli_example.yml
170
+ - exe/watchcat
139
171
  - ext/watchcat/.gitignore
140
172
  - ext/watchcat/Cargo.lock
141
173
  - ext/watchcat/Cargo.toml
@@ -144,17 +176,23 @@ files:
144
176
  - ext/watchcat/src/gvl_helpers.rs
145
177
  - ext/watchcat/src/lib.rs
146
178
  - lib/watchcat.rb
179
+ - lib/watchcat/cli.rb
180
+ - lib/watchcat/cli/action_executor.rb
181
+ - lib/watchcat/cli/config.rb
182
+ - lib/watchcat/cli/watcher.rb
183
+ - lib/watchcat/debouncer.rb
147
184
  - lib/watchcat/event.rb
185
+ - lib/watchcat/event_handler.rb
148
186
  - lib/watchcat/executor.rb
149
187
  - lib/watchcat/kind.rb
150
188
  - lib/watchcat/version.rb
151
- - watchcat.gemspec
152
189
  homepage: https://github.com/y-yagi/watchcat
153
190
  licenses:
154
191
  - MIT
155
192
  metadata:
156
193
  homepage_uri: https://github.com/y-yagi/watchcat
157
194
  rubygems_mfa_required: 'true'
195
+ post_install_message:
158
196
  rdoc_options: []
159
197
  require_paths:
160
198
  - lib
@@ -169,7 +207,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
169
207
  - !ruby/object:Gem::Version
170
208
  version: '0'
171
209
  requirements: []
172
- rubygems_version: 3.6.7
210
+ rubygems_version: 3.5.22
211
+ signing_key:
173
212
  specification_version: 4
174
213
  summary: Simple filesystem notification library for Ruby.
175
214
  test_files: []
data/Gemfile.lock DELETED
@@ -1,84 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- watchcat (0.3.0)
5
- rb_sys
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- cgi (0.5.0)
11
- date (3.4.1)
12
- debug (1.11.0)
13
- irb (~> 1.10)
14
- reline (>= 0.3.8)
15
- erb (4.0.4)
16
- cgi (>= 0.3.3)
17
- ffi (1.17.1-arm64-darwin)
18
- ffi (1.17.1-x64-mingw-ucrt)
19
- ffi (1.17.1-x86_64-darwin)
20
- ffi (1.17.1-x86_64-linux-gnu)
21
- io-console (0.8.0)
22
- irb (1.15.2)
23
- pp (>= 0.6.0)
24
- rdoc (>= 4.0.0)
25
- reline (>= 0.4.2)
26
- listen (3.9.0)
27
- rb-fsevent (~> 0.10, >= 0.10.3)
28
- rb-inotify (~> 0.9, >= 0.9.10)
29
- minitest (5.25.5)
30
- minitest-retry (0.2.5)
31
- minitest (>= 5.0)
32
- nokogiri (1.18.1-arm64-darwin)
33
- racc (~> 1.4)
34
- nokogiri (1.18.1-x64-mingw-ucrt)
35
- racc (~> 1.4)
36
- nokogiri (1.18.1-x86_64-darwin)
37
- racc (~> 1.4)
38
- nokogiri (1.18.1-x86_64-linux-gnu)
39
- racc (~> 1.4)
40
- pp (0.6.2)
41
- prettyprint
42
- prettyprint (0.2.0)
43
- psych (5.2.6)
44
- date
45
- stringio
46
- racc (1.8.1)
47
- rake (13.3.0)
48
- rake-compiler (1.3.0)
49
- rake
50
- rake-compiler-dock (1.9.1)
51
- rb-fsevent (0.11.2)
52
- rb-inotify (0.11.1)
53
- ffi (~> 1.0)
54
- rb_sys (0.9.117)
55
- rake-compiler-dock (= 1.9.1)
56
- rdoc (6.14.1)
57
- erb
58
- psych (>= 4.0.0)
59
- reline (0.6.1)
60
- io-console (~> 0.5)
61
- ruby_memcheck (3.0.1)
62
- nokogiri
63
- stringio (3.1.7)
64
-
65
- PLATFORMS
66
- arm64-darwin-22
67
- arm64-darwin-23
68
- x64-mingw-ucrt
69
- x86_64-darwin-19
70
- x86_64-darwin-20
71
- x86_64-linux
72
-
73
- DEPENDENCIES
74
- debug
75
- listen
76
- minitest
77
- minitest-retry
78
- rake
79
- rake-compiler
80
- ruby_memcheck
81
- watchcat!
82
-
83
- BUNDLED WITH
84
- 2.6.2
data/watchcat.gemspec DELETED
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/watchcat/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "watchcat"
7
- spec.version = Watchcat::VERSION
8
- spec.authors = ["Yuji Yaginuma"]
9
- spec.email = ["yuuji.yaginuma@gmail.com"]
10
-
11
- spec.summary = "Simple filesystem notification library for Ruby. "
12
- spec.homepage = "https://github.com/y-yagi/watchcat"
13
- spec.license = "MIT"
14
- spec.required_ruby_version = ">= 3.1.0"
15
-
16
- spec.metadata["homepage_uri"] = spec.homepage
17
- spec.metadata["rubygems_mfa_required"] = "true"
18
-
19
- # Specify which files should be added to the gem when it is released.
20
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
- spec.files = Dir.chdir(__dir__) do
22
- `git ls-files -z`.split("\x0").reject do |f|
23
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features|benchmark)/|\.(?:git|travis|circleci)|appveyor)})
24
- end
25
- end
26
- spec.require_paths = ["lib"]
27
- spec.extensions = ["ext/watchcat/extconf.rb"]
28
-
29
- spec.add_dependency "rb_sys"
30
- spec.add_development_dependency "debug"
31
- spec.add_development_dependency "minitest"
32
- spec.add_development_dependency "minitest-retry"
33
- spec.add_development_dependency "rake"
34
- spec.add_development_dependency "rake-compiler"
35
- spec.add_development_dependency "ruby_memcheck"
36
- spec.add_development_dependency "listen"
37
- end