watchcat 0.2.0 → 0.6.0

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.
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 and macOS. Due to the using `fork`, this doesn't support Windows now.
11
+ - Linux: inotify
12
+ - macOS: FSEvents
13
+ - Windows: ReadDirectoryChangesW
14
+ - All platforms: polling (via `force_polling` option)
12
15
 
13
16
  ## Installation
14
17
 
@@ -27,7 +30,7 @@ Please specify a filename or directory and callback block to `Watchcat.watch`. T
27
30
  ```ruby
28
31
  require "watchcat"
29
32
 
30
- w = Watchcat.watch("/tmp/test") do |e|
33
+ Watchcat.watch("/tmp/test") do |e|
31
34
  pp e.paths, e.kind
32
35
  end
33
36
 
@@ -58,7 +61,7 @@ You can know what event is happened with `Watchcat::EventKind`. For example, wha
58
61
  ```ruby
59
62
  require "watchcat"
60
63
 
61
- w = Watchcat.watch("/tmp/target") do |e|
64
+ Watchcat.watch("/tmp/target") do |e|
62
65
  if e.kind.create?
63
66
  if e.kind.create.file?
64
67
  puts "'#{e.paths[0]}'(File) is added."
@@ -84,7 +87,6 @@ sleep
84
87
 
85
88
  **CAUTION** The `watchcat` doesn't normalize the events. So the result might change per the platform.
86
89
 
87
-
88
90
  ### Options
89
91
 
90
92
  | Name | Description | Default |
@@ -94,6 +96,190 @@ sleep
94
96
  | **debounce** | Debounce events for the same file. | `-1` |
95
97
 
96
98
 
99
+ ### Filters Option
100
+
101
+ You can use the `filters` option to ignore specific event types:
102
+
103
+ | Key | Description |
104
+ |-----------------|-----------------------------------|
105
+ | ignore_remove | Ignore remove (delete) events |
106
+ | ignore_access | Ignore access events |
107
+ | ignore_create | Ignore create events |
108
+ | ignore_modify | Ignore modify events |
109
+
110
+ Example usage:
111
+
112
+ ```ruby
113
+ Watchcat.watch("/tmp/test", filters: { ignore_remove: true, ignore_access: true }) do |e|
114
+ pp e.paths, e.kind
115
+ end
116
+ ```
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.
223
+
224
+ ## CLI
225
+
226
+ `watchcat` comes with a command-line interface that allows you to watch files and execute commands when changes occur.
227
+
228
+ ### Usage
229
+
230
+ ```
231
+ # Run watchcat with a config file
232
+ $ watchcat -C config.yml
233
+
234
+ # Generate a template config file
235
+ $ watchcat --init config.yml
236
+ ```
237
+
238
+ ### Configuration File
239
+
240
+ The configuration file should be in YAML format. Here's an example:
241
+
242
+ ```yaml
243
+ watches:
244
+ - path: "./lib"
245
+ recursive: true
246
+ debounce: 300
247
+ filters:
248
+ ignore_access: true
249
+ patterns:
250
+ - "*.rb"
251
+ - "*.yml"
252
+ actions:
253
+ - command: "echo 'Ruby/YAML file changed: {{file_name}}'"
254
+ - command: "rubocop {{file_path}}"
255
+ ```
256
+
257
+ ### Configuration Options
258
+
259
+ Each watch entry supports the following options:
260
+
261
+ | Option | Description | Default |
262
+ |-------------|--------------------------------------------------------|---------|
263
+ | path | Directory or file path to watch (required) | - |
264
+ | recursive | Watch a directory recursively or not | `true` |
265
+ | debounce | Debounce events for the same file (in milliseconds) | `-1` |
266
+ | filters | Event filters (same as library filters option) | `{}` |
267
+ | patterns | File patterns to match (using File.fnmatch) | `[]` |
268
+ | actions | Commands to execute when files change | `[]` |
269
+
270
+ ### Available Variables for Commands
271
+
272
+ When specifying commands, you can use the following variables:
273
+
274
+ | Variable | Description | Example |
275
+ |----------------|------------------------------------------|--------------------------|
276
+ | {{file_path}} | Full path of the changed file | `/home/user/app/file.rb` |
277
+ | {{file_dir}} | Directory containing the file | `/home/user/app` |
278
+ | {{file_name}} | File name with extension | `file.rb` |
279
+ | {{file_base}} | File name without extension | `file` |
280
+ | {{file_ext}} | File extension | `.rb` |
281
+ | {{event_type}} | Type of event | `create` |
282
+
97
283
  ## Contributing
98
284
 
99
285
  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
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "watchcat"
3
- version = "0.1.0"
3
+ version = "0.3.0"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
@@ -8,8 +8,7 @@ publish = false
8
8
  crate-type = ["cdylib"]
9
9
 
10
10
  [dependencies]
11
- crossbeam-channel = "0.5.13"
12
- magnus = "0.7"
13
- notify = { version = "7.0.0", features = ["crossbeam-channel"] }
14
- notify-debouncer-mini = { version = "0.5.0", features = ["crossbeam-channel"] }
15
- rb-sys = "0.9.102"
11
+ crossbeam-channel = "0.5.15"
12
+ magnus = "0.8"
13
+ notify = { version = "8.2.0", features = ["crossbeam-channel"] }
14
+ rb-sys = "0.9.128"
@@ -0,0 +1,65 @@
1
+ use std::{ffi::c_void, ptr::null_mut};
2
+
3
+ use magnus::Ruby;
4
+ use rb_sys::{
5
+ rb_thread_call_with_gvl, rb_thread_call_without_gvl
6
+ };
7
+
8
+ pub fn call_without_gvl<F, R>(f: F) -> R
9
+ where
10
+ F: Send + FnOnce() -> R,
11
+ {
12
+ extern "C" fn trampoline<F, R>(arg: *mut c_void) -> *mut c_void
13
+ where
14
+ F: FnOnce() -> R,
15
+ {
16
+ let closure_ptr = arg as *mut Option<F>;
17
+ let closure = unsafe { (*closure_ptr).take().expect("Closure already taken") };
18
+
19
+ let result = closure();
20
+
21
+ let boxed_result = Box::new(result);
22
+ Box::into_raw(boxed_result) as *mut c_void
23
+ }
24
+
25
+ let mut closure_opt = Some(f);
26
+ let closure_ptr = &mut closure_opt as *mut Option<F> as *mut c_void;
27
+
28
+ let raw_result_ptr = unsafe {
29
+ rb_thread_call_without_gvl(
30
+ Some(trampoline::<F, R>),
31
+ closure_ptr,
32
+ None,
33
+ null_mut(),
34
+ )
35
+ };
36
+
37
+ let result_box = unsafe { Box::from_raw(raw_result_ptr as *mut R) };
38
+ *result_box
39
+ }
40
+
41
+ pub fn call_with_gvl<F, R>(f: F) -> R
42
+ where
43
+ F: FnOnce(Ruby) -> R,
44
+ {
45
+ extern "C" fn trampoline<F, R>(arg: *mut c_void) -> *mut c_void
46
+ where
47
+ F: FnOnce(Ruby) -> R,
48
+ {
49
+ let closure_ptr = arg as *mut Option<F>;
50
+ let closure = unsafe { (*closure_ptr).take().expect("Closure already taken") };
51
+
52
+ let result = closure(Ruby::get().unwrap());
53
+
54
+ let boxed_result = Box::new(result);
55
+ Box::into_raw(boxed_result) as *mut c_void
56
+ }
57
+
58
+ let mut closure_opt = Some(f);
59
+ let closure_ptr = &mut closure_opt as *mut Option<F> as *mut c_void;
60
+
61
+ let raw_result_ptr = unsafe { rb_thread_call_with_gvl(Some(trampoline::<F, R>), closure_ptr) };
62
+
63
+ let result_box = unsafe { Box::from_raw(raw_result_ptr as *mut R) };
64
+ *result_box
65
+ }