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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/Cargo.lock +93 -189
- data/Cargo.toml +1 -1
- data/Gemfile +1 -1
- data/README.md +190 -4
- data/cli_example.yml +14 -0
- data/exe/watchcat +14 -0
- data/ext/watchcat/Cargo.toml +5 -6
- data/ext/watchcat/src/gvl_helpers.rs +65 -0
- data/ext/watchcat/src/lib.rs +233 -126
- data/lib/watchcat/cli/action_executor.rb +44 -0
- data/lib/watchcat/cli/config.rb +76 -0
- data/lib/watchcat/cli/watcher.rb +83 -0
- data/lib/watchcat/cli.rb +52 -0
- data/lib/watchcat/debouncer.rb +41 -0
- data/lib/watchcat/event.rb +37 -0
- data/lib/watchcat/event_handler.rb +29 -0
- data/lib/watchcat/executor.rb +84 -34
- data/lib/watchcat/kind.rb +8 -0
- data/lib/watchcat/version.rb +1 -1
- data/lib/watchcat.rb +15 -5
- metadata +36 -15
- data/Gemfile.lock +0 -70
- data/lib/watchcat/client.rb +0 -26
- data/lib/watchcat/server.rb +0 -14
- data/watchcat.gemspec +0 -38
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
data/ext/watchcat/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "watchcat"
|
|
3
|
-
version = "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.
|
|
12
|
-
magnus = "0.
|
|
13
|
-
notify = { version = "
|
|
14
|
-
|
|
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
|
+
}
|