watchcat 0.3.0-aarch64-linux → 0.5.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: b70d83200e444d52bd66b5e665ea1508e9d263e6f0d90a46f11b68406b74539a
4
- data.tar.gz: 45b2c11dca1570a599e39f3a82630af892bddded1d13313c2f413f6adcfb780b
3
+ metadata.gz: 860241f917e368193619b3cea591929d6e7022b152caf59c05dc26f97f1b7403
4
+ data.tar.gz: e6097f5f83d6dbad40ac8d0b650fd6714364ea181f2f5b11608e0a40c013d050
5
5
  SHA512:
6
- metadata.gz: 007fb981c97cb6f07afb036e8fc7d05abebdbeb205abe68ba7849af93cdb114e5f55359676dc6dde9f71554bb66889f3621160bdfa59cd453a26ddc2ec80b867
7
- data.tar.gz: 3edca86fb218a87f6ddb1914878194fe35eb32b8e296ff62563486f130719b7c196fbe564001ea18d32b685d861f412380ab874cb12af1dc5b1248fda833416e
6
+ metadata.gz: e2b257d68018378f0f605fcc2eba602c3912927ac1198787bcb02ffc33e84d17780f7d96a104bd4f3c7ea1644f0758c32f67aeaa4c7228b679f5e52c8fb052a8
7
+ data.tar.gz: 573e0727e642058fd88cd3f00822f70d4f39c359e856f51c13e56e514170e06b1dceab446697f1c793c606e062a8e6824dddaca9e9e4e5f14ec4ec9abeedfdbb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 0.5.0
2
+
3
+ * Rework the debounce feature. Now all events are debounced.
4
+ * Add `init` option to CLI
5
+
6
+ ## 0.4.0
7
+
8
+ * Add CLI
9
+ * Add filters option
10
+
1
11
  ## 0.3.0
2
12
 
3
13
  * 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.0)
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,72 @@
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
+ patterns:
34
+ - "*.js"
35
+ - "*.ts"
36
+ - "*.css"
37
+ actions:
38
+ - command: "echo 'File changed: {{file_path}}'"
39
+
40
+ - path: "./docs"
41
+ recursive: true
42
+ patterns:
43
+ - "*.md"
44
+ actions:
45
+ - command: "echo 'Documentation updated: {{file_name}}'"
46
+ YAML
47
+
48
+ if File.exist?(file_path)
49
+ raise Error, "File already exists: #{file_path}. Won't overwrite."
50
+ end
51
+
52
+ File.write(file_path, template)
53
+ puts "Config template generated at #{file_path}"
54
+ end
55
+
56
+ private
57
+
58
+ def parse_watches(watches_data)
59
+ watches_data.map do |watch_config|
60
+ {
61
+ path: watch_config["path"],
62
+ recursive: watch_config.fetch("recursive", true),
63
+ patterns: watch_config["patterns"] || [],
64
+ actions: watch_config["actions"] || [],
65
+ debounce: watch_config.fetch("debounce", -1),
66
+ filters: watch_config["filters"]&.transform_keys(&:to_sym) || {},
67
+ }
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,85 @@
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
+ return if event.kind.access?
58
+
59
+ event.paths.each do |file_path|
60
+ next unless should_process_file?(file_path, watch_config[:patterns])
61
+
62
+ puts "File changed: #{file_path}"
63
+ execute_actions(file_path, event, watch_config[:actions])
64
+ end
65
+ end
66
+
67
+ def should_process_file?(file_path, patterns)
68
+ return true if patterns.empty?
69
+
70
+ patterns.any? do |pattern|
71
+ File.fnmatch?(pattern, File.basename(file_path)) ||
72
+ File.fnmatch?(pattern, file_path)
73
+ end
74
+ end
75
+
76
+ def execute_actions(file_path, event, actions)
77
+ executor = ActionExecutor.new(file_path, event)
78
+
79
+ actions.each do |action|
80
+ executor.execute(action)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ 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.0"
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.0
5
5
  platform: aarch64-linux
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-06 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
@@ -121,12 +149,19 @@ files:
121
149
  - LICENSE.txt
122
150
  - README.md
123
151
  - Rakefile
152
+ - cli_example.yml
153
+ - exe/watchcat
124
154
  - ext/watchcat/.gitignore
125
155
  - lib/watchcat.rb
126
156
  - lib/watchcat/3.1/watchcat.so
127
157
  - lib/watchcat/3.2/watchcat.so
128
158
  - lib/watchcat/3.3/watchcat.so
129
159
  - lib/watchcat/3.4/watchcat.so
160
+ - lib/watchcat/cli.rb
161
+ - lib/watchcat/cli/action_executor.rb
162
+ - lib/watchcat/cli/config.rb
163
+ - lib/watchcat/cli/watcher.rb
164
+ - lib/watchcat/debouncer.rb
130
165
  - lib/watchcat/event.rb
131
166
  - lib/watchcat/executor.rb
132
167
  - lib/watchcat/kind.rb