watchcat 0.3.0-arm64-darwin → 0.5.1-arm64-darwin

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: 9c7ff3bd9a2d9398769684da7e69b6683ac1dd613473f77323d2e916d7dc131c
4
- data.tar.gz: cdf867a9d838e09f6cb1c81ace80a7bd2ec8ea285d75da616b0e1990cf193c79
3
+ metadata.gz: a4d3b8e28a0a054f4237b62371a3448f029deea4212d42c28f2fb2bf17c8eb40
4
+ data.tar.gz: 2dd33fb6c8069b4595ab838792e89f4d292f4c2759ee776755747a67efb9e34a
5
5
  SHA512:
6
- metadata.gz: d589d3007debe435f37133078452f95244fccbeaef81e5fed12fd5498fbe74e0441eb879d339c37c0b26bd043335a7266eaa7ebd2813a38cb0d2aec41585b0a6
7
- data.tar.gz: 7b4cc9032194a3eb586f974246016a0028abbba1dee75c099cebdee7ad9093002fa126bba3286060cd6e2e2b33de9345f958ecc9eb07ed4922cacbe689bcebb1
6
+ metadata.gz: 4dfd178426a54a264aa2c63d52916440fb7eb8ee54b7d682ff640d0c2e354932dba9785eabf1d46024d80f53b3546045636ed6e62eb07ae2bfe5cfba93576999
7
+ data.tar.gz: fd800e7896a7be0a0a1827e779a575a752d984ef5955616ecfebdceeed0386b8e2207a1ebb4033bd1f2d56d4ae513c85a95dbfec8a6762151a0160686a108ca1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## 0.5.1
2
+
3
+ * Fix missing executable files
4
+
5
+ ## 0.5.0
6
+
7
+ * Rework the debounce feature. Now all events are debounced.
8
+ * Add `init` option to CLI
9
+
10
+ ## 0.4.0
11
+
12
+ * Add CLI
13
+ * Add filters option
14
+
1
15
  ## 0.3.0
2
16
 
3
17
  * Support Windows
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- watchcat (0.3.0)
4
+ watchcat (0.5.1)
5
+ psych
5
6
  rb_sys
6
7
 
7
8
  GEM
@@ -27,6 +28,8 @@ GEM
27
28
  rb-fsevent (~> 0.10, >= 0.10.3)
28
29
  rb-inotify (~> 0.9, >= 0.9.10)
29
30
  minitest (5.25.5)
31
+ minitest-fail-fast (0.1.0)
32
+ minitest (~> 5)
30
33
  minitest-retry (0.2.5)
31
34
  minitest (>= 5.0)
32
35
  nokogiri (1.18.1-arm64-darwin)
@@ -74,6 +77,7 @@ DEPENDENCIES
74
77
  debug
75
78
  listen
76
79
  minitest
80
+ minitest-fail-fast
77
81
  minitest-retry
78
82
  rake
79
83
  rake-compiler
data/README.md CHANGED
@@ -8,7 +8,7 @@ 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 and macOS. Due to the using `fork`, this doesn't support Windows now.
11
+ This gem supports Linux, macOS and Windows.
12
12
 
13
13
  ## Installation
14
14
 
@@ -84,7 +84,6 @@ sleep
84
84
 
85
85
  **CAUTION** The `watchcat` doesn't normalize the events. So the result might change per the platform.
86
86
 
87
-
88
87
  ### Options
89
88
 
90
89
  | Name | Description | Default |
@@ -94,6 +93,86 @@ sleep
94
93
  | **debounce** | Debounce events for the same file. | `-1` |
95
94
 
96
95
 
96
+ ### Filters Option
97
+
98
+ You can use the `filters` option to ignore specific event types:
99
+
100
+ | Key | Description |
101
+ |-----------------|-----------------------------------|
102
+ | ignore_remove | Ignore remove (delete) events |
103
+ | ignore_access | Ignore access events |
104
+ | ignore_create | Ignore create events |
105
+ | ignore_modify | Ignore modify events |
106
+
107
+ Example usage:
108
+
109
+ ```ruby
110
+ Watchcat.watch("/tmp/test", filters: { ignore_remove: true, ignore_access: true }) do |e|
111
+ pp e.paths, e.kind
112
+ end
113
+ ```
114
+
115
+
116
+ ## CLI
117
+
118
+ `watchcat` comes with a command-line interface that allows you to watch files and execute commands when changes occur.
119
+
120
+ ### Usage
121
+
122
+ ```
123
+ # Run watchcat with a config file
124
+ $ watchcat -C config.yml
125
+
126
+ # Generate a template config file
127
+ $ watchcat --init config.yml
128
+ ```
129
+
130
+ ### Configuration File
131
+
132
+ The configuration file should be in YAML format. Here's an example:
133
+
134
+ ```yaml
135
+ watches:
136
+ - path: "./lib"
137
+ recursive: true
138
+ debounce: 300
139
+ filters:
140
+ ignore_access: true
141
+ patterns:
142
+ - "*.rb"
143
+ - "*.yml"
144
+ actions:
145
+ - command: "echo 'Ruby/YAML file changed: {{file_name}}'"
146
+ - command: "rubocop {{file_path}}"
147
+ ```
148
+
149
+ ### Configuration Options
150
+
151
+ Each watch entry supports the following options:
152
+
153
+ | Option | Description | Default |
154
+ |-------------|--------------------------------------------------------|---------|
155
+ | path | Directory or file path to watch (required) | - |
156
+ | recursive | Watch a directory recursively or not | `true` |
157
+ | debounce | Debounce events for the same file (in milliseconds) | `-1` |
158
+ | filters | Event filters (same as library filters option) | `{}` |
159
+ | patterns | File patterns to match (using File.fnmatch) | `[]` |
160
+ | actions | Commands to execute when files change | `[]` |
161
+
162
+ ### Available Variables for Commands
163
+
164
+ When specifying commands, you can use the following variables:
165
+
166
+ | Variable | Description | Example |
167
+ |---------------|------------------------------------------|------------------------|
168
+ | {{file_path}} | Full path of the changed file | `/home/user/app/file.rb` |
169
+ | {{file_dir}} | Directory containing the file | `/home/user/app` |
170
+ | {{file_name}} | File name with extension | `file.rb` |
171
+ | {{file_base}} | File name without extension | `file` |
172
+ | {{file_ext}} | File extension | `.rb` |
173
+
174
+
175
+
97
176
  ## Contributing
98
177
 
99
178
  Bug reports and pull requests are welcome on GitHub at https://github.com/y-yagi/watchcat.
data/cli_example.yml ADDED
@@ -0,0 +1,14 @@
1
+ # Watchcat Configuration File
2
+
3
+ watches:
4
+ - path: "./lib"
5
+ recursive: true
6
+ debounce: 300
7
+ filters:
8
+ ignore_access: true
9
+ patterns:
10
+ - "*.rb"
11
+ - "*.yml"
12
+ actions:
13
+ - command: "echo 'Ruby/YAML file changed: {{file_name}}'"
14
+ - command: "rubocop {{file_path}}"
data/exe/watchcat ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "watchcat"
4
+ require "watchcat/cli"
5
+
6
+ begin
7
+ Watchcat::CLI.start(ARGV)
8
+ rescue Watchcat::CLI::Error => e
9
+ puts "Error: #{e.message}"
10
+ exit 1
11
+ rescue Interrupt
12
+ puts "\nGoodbye!"
13
+ exit 0
14
+ end
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,43 @@
1
+ module Watchcat
2
+ module CLI
3
+ class ActionExecutor
4
+ def initialize(file_path, event)
5
+ @file_path = file_path
6
+ @event = event
7
+ @file_dir = File.dirname(file_path)
8
+ @file_name = File.basename(file_path)
9
+ @file_ext = File.extname(file_path)
10
+ @file_base = File.basename(file_path, @file_ext)
11
+ end
12
+
13
+ def execute(action)
14
+ execute_command(action)
15
+ rescue => e
16
+ puts "Error executing action #{action}: #{e.message}"
17
+ end
18
+
19
+ private
20
+
21
+ def execute_command(action)
22
+ command = substitute_variables(action["command"])
23
+ puts "Executing: #{command}"
24
+
25
+ success = system(command)
26
+ unless success
27
+ puts "Command failed with exit code: #{$?.exitstatus}"
28
+ end
29
+ end
30
+
31
+ def substitute_variables(template)
32
+ return template unless template.is_a?(String)
33
+
34
+ template
35
+ .gsub("{{file_path}}", @file_path)
36
+ .gsub("{{file_dir}}", @file_dir)
37
+ .gsub("{{file_name}}", @file_name)
38
+ .gsub("{{file_base}}", @file_base)
39
+ .gsub("{{file_ext}}", @file_ext)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,76 @@
1
+ require "psych"
2
+
3
+ module Watchcat
4
+ module CLI
5
+ class Config
6
+ attr_reader :watches
7
+
8
+ def initialize(data)
9
+ @watches = parse_watches(data["watches"] || [])
10
+ end
11
+
12
+ def self.load(file_path)
13
+ unless File.exist?(file_path)
14
+ raise Error, "Configuration file not found: #{file_path}"
15
+ end
16
+
17
+ begin
18
+ data = Psych.load_file(file_path)
19
+ new(data)
20
+ rescue Psych::SyntaxError => e
21
+ raise Error, "Invalid YAML syntax in #{file_path}: #{e.message}"
22
+ end
23
+ end
24
+
25
+ def self.generate_template(file_path)
26
+ template = <<~YAML
27
+ # Watchcat Configuration File
28
+
29
+ watches:
30
+ - path: "./src"
31
+ recursive: true
32
+ debounce: 300
33
+ filters:
34
+ ignore_access: true
35
+ patterns:
36
+ - "*.js"
37
+ - "*.ts"
38
+ - "*.css"
39
+ actions:
40
+ - command: "echo 'File changed: {{file_path}}'"
41
+
42
+ - path: "./docs"
43
+ recursive: true
44
+ filters:
45
+ ignore_access: true
46
+ patterns:
47
+ - "*.md"
48
+ actions:
49
+ - command: "echo 'Documentation updated: {{file_name}}'"
50
+ YAML
51
+
52
+ if File.exist?(file_path)
53
+ raise Error, "File already exists: #{file_path}. Won't overwrite."
54
+ end
55
+
56
+ File.write(file_path, template)
57
+ puts "Config template generated at #{file_path}"
58
+ end
59
+
60
+ private
61
+
62
+ def parse_watches(watches_data)
63
+ watches_data.map do |watch_config|
64
+ {
65
+ path: watch_config["path"],
66
+ recursive: watch_config.fetch("recursive", true),
67
+ patterns: watch_config["patterns"] || [],
68
+ actions: watch_config["actions"] || [],
69
+ debounce: watch_config.fetch("debounce", -1),
70
+ filters: watch_config["filters"]&.transform_keys(&:to_sym) || {},
71
+ }
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,83 @@
1
+ module Watchcat
2
+ module CLI
3
+ class Watcher
4
+ def initialize(config)
5
+ @config = config
6
+ @watchers = []
7
+ end
8
+
9
+ def start
10
+ puts "Starting Watchcat file watcher..."
11
+
12
+ @config.watches.each do |watch_config|
13
+ start_watching_path(watch_config)
14
+ end
15
+
16
+ puts "Watchcat is now watching for file changes. Press Ctrl+C to stop."
17
+
18
+ # Keep the main thread alive
19
+ begin
20
+ sleep
21
+ rescue Interrupt
22
+ puts "\nStopping Watchcat..."
23
+ stop
24
+ end
25
+ end
26
+
27
+ def stop
28
+ @watchers.each(&:stop)
29
+ @watchers.clear
30
+ end
31
+
32
+ private
33
+
34
+ def start_watching_path(watch_config)
35
+ path = watch_config[:path]
36
+
37
+ unless File.exist?(path)
38
+ puts "Warning: Path does not exist: #{path}"
39
+ return
40
+ end
41
+
42
+ puts "Watching: #{path} (recursive: #{watch_config[:recursive]}, debounce: #{watch_config[:debounce]}ms)"
43
+
44
+ watcher = Watchcat.watch(
45
+ path,
46
+ recursive: watch_config[:recursive],
47
+ filters: watch_config[:filters],
48
+ debounce: watch_config[:debounce],
49
+ ) do |event|
50
+ handle_file_event(event, watch_config)
51
+ end
52
+
53
+ @watchers << watcher
54
+ end
55
+
56
+ def handle_file_event(event, watch_config)
57
+ event.paths.each do |file_path|
58
+ next unless should_process_file?(file_path, watch_config[:patterns])
59
+
60
+ puts "File changed: #{file_path}"
61
+ execute_actions(file_path, event, watch_config[:actions])
62
+ end
63
+ end
64
+
65
+ def should_process_file?(file_path, patterns)
66
+ return true if patterns.empty?
67
+
68
+ patterns.any? do |pattern|
69
+ File.fnmatch?(pattern, File.basename(file_path)) ||
70
+ File.fnmatch?(pattern, file_path)
71
+ end
72
+ end
73
+
74
+ def execute_actions(file_path, event, actions)
75
+ executor = ActionExecutor.new(file_path, event)
76
+
77
+ actions.each do |action|
78
+ executor.execute(action)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,52 @@
1
+ require "optparse"
2
+ require_relative "cli/watcher"
3
+ require_relative "cli/config"
4
+ require_relative "cli/action_executor"
5
+
6
+ module Watchcat
7
+ module CLI
8
+ class Error < StandardError; end
9
+ class << self
10
+ def start(argv)
11
+ options = parse(argv)
12
+
13
+ if options[:init]
14
+ Config.generate_template(options[:init])
15
+ return
16
+ end
17
+
18
+ config = Config.load(options[:config])
19
+ watcher = Watcher.new(config)
20
+ watcher.start
21
+ rescue => e
22
+ raise Error, "Failed to start Watchcat: #{e.message}"
23
+ end
24
+
25
+ def parse(argv)
26
+ options = { config: 'watchcat.yml' }
27
+ OptionParser.new do |opts|
28
+ opts.banner = "Usage: watchcat [options]"
29
+
30
+ opts.on("-C", "--config PATH", "Path to the config file. Default is 'watchcat.yml'.") do |v|
31
+ options[:config] = v
32
+ end
33
+
34
+ opts.on("--init PATH", "Generate a template config file at the specified path") do |v|
35
+ options[:init] = v
36
+ end
37
+
38
+ opts.on("-h", "--help", "Show this help message") do
39
+ puts opts
40
+ exit
41
+ end
42
+ end.parse!(argv)
43
+
44
+ if !options[:init] && options[:config].nil?
45
+ raise OptionParser::MissingArgument.new("-C")
46
+ end
47
+
48
+ options
49
+ end
50
+ end
51
+ end
52
+ end
@@ -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
@@ -2,14 +2,14 @@ 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:)
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
13
  @block = block
14
14
  @watcher = Watchcat::Watcher.new
15
15
  @watch_thread = nil
@@ -23,11 +23,6 @@ module Watchcat
23
23
  start_watching
24
24
  end
25
25
 
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
26
  at_exit do
32
27
  stop
33
28
  end
@@ -49,14 +44,22 @@ module Watchcat
49
44
  recursive: @recursive,
50
45
  force_polling: @force_polling,
51
46
  poll_interval: @poll_interval,
52
- ignore_remove: @ignore_remove,
53
- debounce: @debounce
47
+ ignore_remove: @filters[:ignore_remove],
48
+ ignore_access: @filters[:ignore_access],
49
+ ignore_create: @filters[:ignore_create],
50
+ ignore_modify: @filters[:ignore_modify]
54
51
  ) do |kind, paths, raw_kind|
55
52
  break if @stop_requested
56
53
 
57
- # Create an event object and call the block
58
- event = Watchcat::Event.new(kind, paths, raw_kind)
59
- @block.call(event)
54
+ 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
59
+ else
60
+ event = Watchcat::Event.new(kind, paths, raw_kind)
61
+ @block.call(event)
62
+ end
60
63
  end
61
64
  end
62
65
  end
@@ -1,3 +1,3 @@
1
1
  module Watchcat
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.1"
3
3
  end
data/lib/watchcat.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require_relative "watchcat/version"
2
2
  require_relative "watchcat/executor"
3
+ require_relative "watchcat/debouncer"
3
4
 
4
5
  begin
5
6
  require "watchcat/#{RUBY_VERSION.to_f}/watchcat"
@@ -14,8 +15,7 @@ module Watchcat
14
15
  recursive: true,
15
16
  force_polling: false,
16
17
  poll_interval: nil,
17
- wait_until_startup: false,
18
- ignore_remove: false,
18
+ filters: {},
19
19
  debounce: -1,
20
20
  &block
21
21
  )
@@ -25,8 +25,7 @@ module Watchcat
25
25
  recursive: recursive,
26
26
  force_polling: force_polling,
27
27
  poll_interval: poll_interval,
28
- wait_until_startup: wait_until_startup,
29
- ignore_remove: ignore_remove,
28
+ filters: filters,
30
29
  debounce: debounce,
31
30
  block: block
32
31
  )
metadata CHANGED
@@ -1,15 +1,29 @@
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.5.1
5
5
  platform: arm64-darwin
6
6
  authors:
7
7
  - Yuji Yaginuma
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-03 00:00:00.000000000 Z
11
+ date: 2025-09-11 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: psych
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: debug
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +66,20 @@ dependencies:
52
66
  - - ">="
53
67
  - !ruby/object:Gem::Version
54
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: minitest-fail-fast
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: rake
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -111,7 +139,8 @@ dependencies:
111
139
  description:
112
140
  email:
113
141
  - yuuji.yaginuma@gmail.com
114
- executables: []
142
+ executables:
143
+ - watchcat
115
144
  extensions: []
116
145
  extra_rdoc_files: []
117
146
  files:
@@ -121,12 +150,19 @@ files:
121
150
  - LICENSE.txt
122
151
  - README.md
123
152
  - Rakefile
153
+ - cli_example.yml
154
+ - exe/watchcat
124
155
  - ext/watchcat/.gitignore
125
156
  - lib/watchcat.rb
126
157
  - lib/watchcat/3.1/watchcat.bundle
127
158
  - lib/watchcat/3.2/watchcat.bundle
128
159
  - lib/watchcat/3.3/watchcat.bundle
129
160
  - lib/watchcat/3.4/watchcat.bundle
161
+ - lib/watchcat/cli.rb
162
+ - lib/watchcat/cli/action_executor.rb
163
+ - lib/watchcat/cli/config.rb
164
+ - lib/watchcat/cli/watcher.rb
165
+ - lib/watchcat/debouncer.rb
130
166
  - lib/watchcat/event.rb
131
167
  - lib/watchcat/executor.rb
132
168
  - lib/watchcat/kind.rb