watchcat 0.3.0-aarch64-linux → 0.4.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: 775e650899bc7f19178beb343c83bf17999267451c28c9cb297596dd306d0c3d
4
+ data.tar.gz: 1e5f2b934fb84c5f6ce166b48f55938faca46c7561c9c18e92cfc0703c479cf2
5
5
  SHA512:
6
- metadata.gz: 007fb981c97cb6f07afb036e8fc7d05abebdbeb205abe68ba7849af93cdb114e5f55359676dc6dde9f71554bb66889f3621160bdfa59cd453a26ddc2ec80b867
7
- data.tar.gz: 3edca86fb218a87f6ddb1914878194fe35eb32b8e296ff62563486f130719b7c196fbe564001ea18d32b685d861f412380ab874cb12af1dc5b1248fda833416e
6
+ metadata.gz: a18a64b697c5186722a1009dda259cb9a6ac72b71e36a0910bd65ae3dcb17233cdb1e7d0c18d63a40ce45e5a5a6373d39d84d53b39fc8afd607b6db8d10b3eb3
7
+ data.tar.gz: 53654def3b98d8844d3f7e377713683031a967ca84f990b533b823e00c55c5353b7a2dc14c1a604c739ed5222fbbd7d09bb2b472e2826b2813c96e33c6556575
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.4.0
2
+
3
+ * Add CLI
4
+ * Add filters option
5
+
1
6
  ## 0.3.0
2
7
 
3
8
  * 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.4.0)
5
+ psych
5
6
  rb_sys
6
7
 
7
8
  GEM
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,82 @@ 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
+ $ watchcat -C config.yml
124
+ ```
125
+
126
+ ### Configuration File
127
+
128
+ The configuration file should be in YAML format. Here's an example:
129
+
130
+ ```yaml
131
+ watches:
132
+ - path: "./lib"
133
+ recursive: true
134
+ debounce: 300
135
+ filters:
136
+ ignore_access: true
137
+ patterns:
138
+ - "*.rb"
139
+ - "*.yml"
140
+ actions:
141
+ - command: "echo 'Ruby/YAML file changed: {{file_name}}'"
142
+ - command: "rubocop {{file_path}}"
143
+ ```
144
+
145
+ ### Configuration Options
146
+
147
+ Each watch entry supports the following options:
148
+
149
+ | Option | Description | Default |
150
+ |-------------|--------------------------------------------------------|---------|
151
+ | path | Directory or file path to watch (required) | - |
152
+ | recursive | Watch a directory recursively or not | `true` |
153
+ | debounce | Debounce events for the same file (in milliseconds) | `500` |
154
+ | filters | Event filters (same as library filters option) | `{}` |
155
+ | patterns | File patterns to match (using File.fnmatch) | `[]` |
156
+ | actions | Commands to execute when files change | `[]` |
157
+
158
+ ### Available Variables for Commands
159
+
160
+ When specifying commands, you can use the following variables:
161
+
162
+ | Variable | Description | Example |
163
+ |---------------|------------------------------------------|------------------------|
164
+ | {{file_path}} | Full path of the changed file | `/home/user/app/file.rb` |
165
+ | {{file_dir}} | Directory containing the file | `/home/user/app` |
166
+ | {{file_name}} | File name with extension | `file.rb` |
167
+ | {{file_base}} | File name without extension | `file` |
168
+ | {{file_ext}} | File extension | `.rb` |
169
+
170
+
171
+
97
172
  ## Contributing
98
173
 
99
174
  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,41 @@
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
+ private
26
+
27
+ def parse_watches(watches_data)
28
+ watches_data.map do |watch_config|
29
+ {
30
+ path: watch_config["path"],
31
+ recursive: watch_config.fetch("recursive", true),
32
+ patterns: watch_config["patterns"] || [],
33
+ actions: watch_config["actions"] || [],
34
+ debounce: watch_config.fetch("debounce", 500),
35
+ filters: watch_config["filters"]&.transform_keys(&:to_sym) || {},
36
+ }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,84 @@
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
+ ) do |event|
49
+ handle_file_event(event, watch_config)
50
+ end
51
+
52
+ @watchers << watcher
53
+ end
54
+
55
+ def handle_file_event(event, watch_config)
56
+ return if event.kind.access?
57
+
58
+ event.paths.each do |file_path|
59
+ next unless should_process_file?(file_path, watch_config[:patterns])
60
+
61
+ puts "File changed: #{file_path} #{event.kind}"
62
+ execute_actions(file_path, event, watch_config[:actions])
63
+ end
64
+ end
65
+
66
+ def should_process_file?(file_path, patterns)
67
+ return true if patterns.empty?
68
+
69
+ patterns.any? do |pattern|
70
+ File.fnmatch?(pattern, File.basename(file_path)) ||
71
+ File.fnmatch?(pattern, file_path)
72
+ end
73
+ end
74
+
75
+ def execute_actions(file_path, event, actions)
76
+ executor = ActionExecutor.new(file_path, event)
77
+
78
+ actions.each do |action|
79
+ executor.execute(action)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,39 @@
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
+ config = Config.load(options[:config])
13
+ watcher = Watcher.new(config)
14
+ watcher.start
15
+ rescue => e
16
+ raise Error, "Failed to start Watchcat: #{e.message}"
17
+ end
18
+
19
+ def parse(argv)
20
+ options = {}
21
+ OptionParser.new do |opts|
22
+ opts.banner = "Usage: watchcat [options]"
23
+
24
+ opts.on("-C", "--config PATH", "Path to the config file") do |v|
25
+ options[:config] = v
26
+ end
27
+
28
+ opts.on("-h", "--help", "Show this help message") do
29
+ puts opts
30
+ exit
31
+ end
32
+ end.parse!(argv)
33
+
34
+ raise OptionParser::MissingArgument.new("-C") if options[:config].nil?
35
+ options
36
+ end
37
+ end
38
+ end
39
+ 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,13 +2,12 @@ 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
13
12
  @block = block
14
13
  @watcher = Watchcat::Watcher.new
@@ -23,11 +22,6 @@ module Watchcat
23
22
  start_watching
24
23
  end
25
24
 
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
25
  at_exit do
32
26
  stop
33
27
  end
@@ -49,12 +43,14 @@ module Watchcat
49
43
  recursive: @recursive,
50
44
  force_polling: @force_polling,
51
45
  poll_interval: @poll_interval,
52
- ignore_remove: @ignore_remove,
46
+ ignore_remove: @filters[:ignore_remove],
47
+ ignore_access: @filters[:ignore_access],
48
+ ignore_create: @filters[:ignore_create],
49
+ ignore_modify: @filters[:ignore_modify],
53
50
  debounce: @debounce
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
54
  event = Watchcat::Event.new(kind, paths, raw_kind)
59
55
  @block.call(event)
60
56
  end
@@ -1,3 +1,3 @@
1
1
  module Watchcat
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.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.4.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-01 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
@@ -121,12 +135,19 @@ files:
121
135
  - LICENSE.txt
122
136
  - README.md
123
137
  - Rakefile
138
+ - cli_example.yml
139
+ - exe/watchcat
124
140
  - ext/watchcat/.gitignore
125
141
  - lib/watchcat.rb
126
142
  - lib/watchcat/3.1/watchcat.so
127
143
  - lib/watchcat/3.2/watchcat.so
128
144
  - lib/watchcat/3.3/watchcat.so
129
145
  - lib/watchcat/3.4/watchcat.so
146
+ - lib/watchcat/cli.rb
147
+ - lib/watchcat/cli/action_executor.rb
148
+ - lib/watchcat/cli/config.rb
149
+ - lib/watchcat/cli/watcher.rb
150
+ - lib/watchcat/debouncer.rb
130
151
  - lib/watchcat/event.rb
131
152
  - lib/watchcat/executor.rb
132
153
  - lib/watchcat/kind.rb