├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── .watch.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── error.rs ├── main.rs └── watch.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [grego] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | -------------------------------------------------------------------------------- /.watch.toml: -------------------------------------------------------------------------------- 1 | [[watch]] 2 | # What the command does? 3 | name = "print hello" 4 | # Where to look for changes? 5 | path = "src" 6 | # What to execute on change? 7 | command = "echo \"hello $EVENT_PATH\"" 8 | 9 | # Repeat this to watch multiple paths 10 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.1.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "1.3.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 16 | 17 | [[package]] 18 | name = "caretaker" 19 | version = "0.2.4" 20 | dependencies = [ 21 | "clap", 22 | "clap_derive", 23 | "crossbeam-channel", 24 | "custom_error", 25 | "glob", 26 | "notify", 27 | "nu-ansi-term", 28 | "serde", 29 | "toml", 30 | ] 31 | 32 | [[package]] 33 | name = "cfg-if" 34 | version = "1.0.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 37 | 38 | [[package]] 39 | name = "clap" 40 | version = "3.2.22" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" 43 | dependencies = [ 44 | "bitflags", 45 | "clap_lex", 46 | "indexmap", 47 | "textwrap", 48 | ] 49 | 50 | [[package]] 51 | name = "clap_derive" 52 | version = "3.2.18" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" 55 | dependencies = [ 56 | "heck", 57 | "proc-macro-error", 58 | "proc-macro2", 59 | "quote", 60 | "syn", 61 | ] 62 | 63 | [[package]] 64 | name = "clap_lex" 65 | version = "0.2.4" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 68 | dependencies = [ 69 | "os_str_bytes", 70 | ] 71 | 72 | [[package]] 73 | name = "crossbeam-channel" 74 | version = "0.5.6" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" 77 | dependencies = [ 78 | "cfg-if", 79 | "crossbeam-utils", 80 | ] 81 | 82 | [[package]] 83 | name = "crossbeam-utils" 84 | version = "0.8.11" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" 87 | dependencies = [ 88 | "cfg-if", 89 | "once_cell", 90 | ] 91 | 92 | [[package]] 93 | name = "custom_error" 94 | version = "1.9.2" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "4f8a51dd197fa6ba5b4dc98a990a43cc13693c23eb0089ebb0fcc1f04152bca6" 97 | 98 | [[package]] 99 | name = "filetime" 100 | version = "0.2.17" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" 103 | dependencies = [ 104 | "cfg-if", 105 | "libc", 106 | "redox_syscall", 107 | "windows-sys", 108 | ] 109 | 110 | [[package]] 111 | name = "fsevent-sys" 112 | version = "4.1.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" 115 | dependencies = [ 116 | "libc", 117 | ] 118 | 119 | [[package]] 120 | name = "glob" 121 | version = "0.3.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 124 | 125 | [[package]] 126 | name = "hashbrown" 127 | version = "0.12.3" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 130 | 131 | [[package]] 132 | name = "heck" 133 | version = "0.4.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 136 | 137 | [[package]] 138 | name = "indexmap" 139 | version = "1.9.1" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" 142 | dependencies = [ 143 | "autocfg", 144 | "hashbrown", 145 | ] 146 | 147 | [[package]] 148 | name = "inotify" 149 | version = "0.9.6" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" 152 | dependencies = [ 153 | "bitflags", 154 | "inotify-sys", 155 | "libc", 156 | ] 157 | 158 | [[package]] 159 | name = "inotify-sys" 160 | version = "0.1.5" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" 163 | dependencies = [ 164 | "libc", 165 | ] 166 | 167 | [[package]] 168 | name = "kqueue" 169 | version = "1.0.6" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "4d6112e8f37b59803ac47a42d14f1f3a59bbf72fc6857ffc5be455e28a691f8e" 172 | dependencies = [ 173 | "kqueue-sys", 174 | "libc", 175 | ] 176 | 177 | [[package]] 178 | name = "kqueue-sys" 179 | version = "1.0.3" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" 182 | dependencies = [ 183 | "bitflags", 184 | "libc", 185 | ] 186 | 187 | [[package]] 188 | name = "libc" 189 | version = "0.2.132" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" 192 | 193 | [[package]] 194 | name = "log" 195 | version = "0.4.17" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 198 | dependencies = [ 199 | "cfg-if", 200 | ] 201 | 202 | [[package]] 203 | name = "mio" 204 | version = "0.8.4" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" 207 | dependencies = [ 208 | "libc", 209 | "log", 210 | "wasi", 211 | "windows-sys", 212 | ] 213 | 214 | [[package]] 215 | name = "notify" 216 | version = "5.0.0" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "ed2c66da08abae1c024c01d635253e402341b4060a12e99b31c7594063bf490a" 219 | dependencies = [ 220 | "bitflags", 221 | "crossbeam-channel", 222 | "filetime", 223 | "fsevent-sys", 224 | "inotify", 225 | "kqueue", 226 | "libc", 227 | "mio", 228 | "walkdir", 229 | "winapi", 230 | ] 231 | 232 | [[package]] 233 | name = "nu-ansi-term" 234 | version = "0.46.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 237 | dependencies = [ 238 | "overload", 239 | "winapi", 240 | ] 241 | 242 | [[package]] 243 | name = "once_cell" 244 | version = "1.14.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" 247 | 248 | [[package]] 249 | name = "os_str_bytes" 250 | version = "6.3.0" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" 253 | 254 | [[package]] 255 | name = "overload" 256 | version = "0.1.1" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 259 | 260 | [[package]] 261 | name = "proc-macro-error" 262 | version = "1.0.4" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 265 | dependencies = [ 266 | "proc-macro-error-attr", 267 | "proc-macro2", 268 | "quote", 269 | "syn", 270 | "version_check", 271 | ] 272 | 273 | [[package]] 274 | name = "proc-macro-error-attr" 275 | version = "1.0.4" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 278 | dependencies = [ 279 | "proc-macro2", 280 | "quote", 281 | "version_check", 282 | ] 283 | 284 | [[package]] 285 | name = "proc-macro2" 286 | version = "1.0.43" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" 289 | dependencies = [ 290 | "unicode-ident", 291 | ] 292 | 293 | [[package]] 294 | name = "quote" 295 | version = "1.0.21" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 298 | dependencies = [ 299 | "proc-macro2", 300 | ] 301 | 302 | [[package]] 303 | name = "redox_syscall" 304 | version = "0.2.16" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 307 | dependencies = [ 308 | "bitflags", 309 | ] 310 | 311 | [[package]] 312 | name = "same-file" 313 | version = "1.0.6" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 316 | dependencies = [ 317 | "winapi-util", 318 | ] 319 | 320 | [[package]] 321 | name = "serde" 322 | version = "1.0.144" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" 325 | dependencies = [ 326 | "serde_derive", 327 | ] 328 | 329 | [[package]] 330 | name = "serde_derive" 331 | version = "1.0.144" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" 334 | dependencies = [ 335 | "proc-macro2", 336 | "quote", 337 | "syn", 338 | ] 339 | 340 | [[package]] 341 | name = "syn" 342 | version = "1.0.99" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" 345 | dependencies = [ 346 | "proc-macro2", 347 | "quote", 348 | "unicode-ident", 349 | ] 350 | 351 | [[package]] 352 | name = "textwrap" 353 | version = "0.15.1" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" 356 | 357 | [[package]] 358 | name = "toml" 359 | version = "0.5.9" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" 362 | dependencies = [ 363 | "serde", 364 | ] 365 | 366 | [[package]] 367 | name = "unicode-ident" 368 | version = "1.0.4" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" 371 | 372 | [[package]] 373 | name = "version_check" 374 | version = "0.9.4" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 377 | 378 | [[package]] 379 | name = "walkdir" 380 | version = "2.3.2" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 383 | dependencies = [ 384 | "same-file", 385 | "winapi", 386 | "winapi-util", 387 | ] 388 | 389 | [[package]] 390 | name = "wasi" 391 | version = "0.11.0+wasi-snapshot-preview1" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 394 | 395 | [[package]] 396 | name = "winapi" 397 | version = "0.3.9" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 400 | dependencies = [ 401 | "winapi-i686-pc-windows-gnu", 402 | "winapi-x86_64-pc-windows-gnu", 403 | ] 404 | 405 | [[package]] 406 | name = "winapi-i686-pc-windows-gnu" 407 | version = "0.4.0" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 410 | 411 | [[package]] 412 | name = "winapi-util" 413 | version = "0.1.5" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 416 | dependencies = [ 417 | "winapi", 418 | ] 419 | 420 | [[package]] 421 | name = "winapi-x86_64-pc-windows-gnu" 422 | version = "0.4.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 425 | 426 | [[package]] 427 | name = "windows-sys" 428 | version = "0.36.1" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 431 | dependencies = [ 432 | "windows_aarch64_msvc", 433 | "windows_i686_gnu", 434 | "windows_i686_msvc", 435 | "windows_x86_64_gnu", 436 | "windows_x86_64_msvc", 437 | ] 438 | 439 | [[package]] 440 | name = "windows_aarch64_msvc" 441 | version = "0.36.1" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 444 | 445 | [[package]] 446 | name = "windows_i686_gnu" 447 | version = "0.36.1" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 450 | 451 | [[package]] 452 | name = "windows_i686_msvc" 453 | version = "0.36.1" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 456 | 457 | [[package]] 458 | name = "windows_x86_64_gnu" 459 | version = "0.36.1" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 462 | 463 | [[package]] 464 | name = "windows_x86_64_msvc" 465 | version = "0.36.1" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 468 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "caretaker" 3 | version = "0.2.4" 4 | authors = ["Maroš Grego "] 5 | edition = "2018" 6 | description = "A simple, configurable filesystem watcher" 7 | repository = "https://github.com/grego/caretaker" 8 | homepage = "https://github.com/grego/caretaker" 9 | keywords = ["fsevents", "notify", "inotify", "watcher"] 10 | categories = ["command-line-utilities","development-tools"] 11 | license = "MIT" 12 | readme = "README.md" 13 | 14 | [dependencies] 15 | toml = "0.5" 16 | notify = "5" 17 | serde = { version = "^1.0.130", features = ["derive"] } 18 | clap = { version = "3", default_features = false, features = ["std"] } 19 | clap_derive = "3" 20 | crossbeam-channel = "0.5" 21 | nu-ansi-term = "0.46" 22 | glob = "0.3" 23 | custom_error = "1.9.2" 24 | 25 | [profile.release] 26 | lto = "fat" 27 | strip = "debuginfo" 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Maroš Grego 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # caretaker 2 | [![Build status](https://badgen.net/travis/grego/caretaker)](https://travis-ci.org/grego/caretaker) 3 | [![Crates.io status](https://badgen.net/crates/v/caretaker)](https://crates.io/crates/caretaker) 4 | [![Docs](https://docs.rs/caretaker/badge.svg)](https://docs.rs/caretaker) 5 | 6 | A simple tool that loads a list of paths to watch from a TOML file. 7 | ```toml 8 | [[watch]] 9 | name = "print hello" 10 | path = "src" 11 | command = "echo $EVENT_PATH" 12 | 13 | [[watch]] 14 | name = "compile sass" 15 | path = "sass/*.sass" 16 | command = "sassc -t compressed sass/style.scss static/style.css" 17 | ``` 18 | On a change in the `path`, it executes the `command`. Directories are watched recursively. 19 | Paths can also be specified with [globs](https://docs.rs/glob/0.3.0/glob/struct.Pattern.html). 20 | Any shell command can be used, along with pipes and so on. 21 | By default, the shell specified in the `$SHELL` environment variable is used to parse and execute the command. 22 | Otherwise, on Unix system, it invokes the default 23 | Bourne shell (`sh` command), on windows [cmd.exe](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmd). 24 | Additionally, each command gets the `$EVENT_PATH` environment variable, containing the path that changed. 25 | 26 | Using [notify](https://github.com/notify-rs/notify) crate, which provides efficient event handling 27 | support for the most operating systems (apart from BSD). 28 | 29 | ## Installing 30 | Currently, Caretaker is available on [AUR](https://aur.archlinux.org/packages/caretaker-bin/). You can 31 | install it with some AUR helper, like `yay -S caretaker-bin`. 32 | 33 | If you have Rust toolchain installed, you can install it with Cargo: 34 | ``` 35 | cargo install caretaker 36 | ``` 37 | 38 | ## Running 39 | Initialising with a dummy `.watch.toml` file: 40 | ``` 41 | caretaker init 42 | ```` 43 | 44 | Watching: 45 | ``` 46 | caretaker 47 | ``` 48 | 49 | You can also pass another file to load the config from via the `-w` option. 50 | 51 | ## License 52 | [MIT](LICENSE) 53 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use custom_error::custom_error; 2 | 3 | custom_error! { 4 | /// All possible ways how caretaking may fail. 5 | pub Error 6 | /// Error of the underlying watch mechanism 7 | Notify{ 8 | /// Source of the error. 9 | source: notify::Error 10 | } = "Notify error: {source}", 11 | /// Error of the underlying watch mechanism 12 | PathWatch{ 13 | /// The path being watched 14 | path: String, 15 | /// Source of the error. 16 | source: notify::Error 17 | } = "Notify error watching {path}: {source}", 18 | /// The provided glob path was not valid. 19 | Pattern{ 20 | /// Source of the error. 21 | source: glob::PatternError 22 | } = "Invalid glob: {source}", 23 | /// Input / output error 24 | Io{ 25 | /// Source of the error. 26 | source: std::io::Error 27 | } = "Input / output error: {source}", 28 | /// Channel receive error 29 | Receive{ 30 | /// Source of the error. 31 | source: crossbeam_channel::RecvError 32 | } = "Channel receive error: {source}", 33 | } 34 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! A simple tool that loads a list of paths to watch from a TOML file. 2 | //! ```toml 3 | //! [[watch]] 4 | //! name = "print hello" 5 | //! path = "src" 6 | //! command = "echo $EVENT_PATH" 7 | //! 8 | //! [[watch]] 9 | //! name = "compile sass" 10 | //! path = "sass/*.sass" 11 | //! command = "sassc -t compressed sass/style.scss static/style.css" 12 | //! ``` 13 | //! On a change in the `path`, it executes the `command`. Directories are watched recursively. 14 | //! Paths can also be specified with [globs](https://docs.rs/glob/0.3.0/glob/struct.Pattern.html). 15 | //! Any shell command can be used, along with pipes and so on. 16 | //! By default, the shell specified in the `$SHELL` environment variable is used to parse and execute the command. 17 | //! Otherwise, on Unix system, it invokes the default 18 | //! Bourne shell (`sh` command), on windows [cmd.exe](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/cmd). 19 | //! Additionally, each command gets the `$EVENT_PATH` environment variable, containing the path that changed. 20 | //! 21 | //! Using [notify](https://docs.rs/notify) crate, which provides efficient event handling 22 | //! support for the most operating systems (apart from BSD). 23 | #![warn(missing_docs)] 24 | mod error; 25 | mod watch; 26 | 27 | pub use error::Error; 28 | pub use watch::{watch, Config, Watch}; 29 | 30 | use watch::{ARGUMENT, SHELL}; 31 | 32 | use nu_ansi_term::Style; 33 | use clap::Parser; 34 | use std::env; 35 | 36 | #[derive(clap_derive::Parser)] 37 | #[clap(version, about, rename_all = "kebab-case")] 38 | /// A simple, configurable filesystem watcher 39 | struct Opt { 40 | /// File to read what to watch from 41 | #[structopt(short, long, default_value = ".watch.toml")] 42 | config: String, 43 | /// Shell to parse and execute the commands with 44 | #[structopt(short, long)] 45 | shell: Option, 46 | /// Command marker argument, such as "-c", to pass to the current shell 47 | #[structopt(long, allow_hyphen_values = true)] 48 | arg: Option, 49 | #[structopt(subcommand)] 50 | cmd: Option, 51 | } 52 | 53 | #[derive(clap_derive::Subcommand)] 54 | enum Cmd { 55 | /// Create a dummy .watch.toml file 56 | Init, 57 | /// Watch for changes (default) 58 | Watch, 59 | } 60 | 61 | const DUMMY: &str = "[[watch]] 62 | # What does the command do? 63 | name = \"print hello\" 64 | # Where to look for changes? 65 | path = \"src\" 66 | # What to execute on change? 67 | command = \"echo $EVENT_PATH\" 68 | 69 | # Repeat this to watch multiple paths"; 70 | 71 | fn main() { 72 | let opt: Opt = Parser::parse(); 73 | 74 | let bold = Style::new().bold(); 75 | match opt.cmd { 76 | Some(Cmd::Init) => { 77 | let config = &opt.config; 78 | if std::fs::metadata(config).is_ok() { 79 | println!("{} already exists, exiting", bold.paint(config)) 80 | } else if let Err(e) = std::fs::write(config, DUMMY) { 81 | eprintln!("Error writing config file {}: {}", config, e); 82 | } else { 83 | println!("{} created!", bold.paint(config)); 84 | } 85 | } 86 | _ => { 87 | let config = match std::fs::read(&opt.config) { 88 | Ok(cfg) => cfg, 89 | Err(e) => return eprintln!("Error writing config file {}: {}", &opt.config, e), 90 | }; 91 | match toml::from_slice(&config) { 92 | Ok(config) => { 93 | let shell = opt.shell.or_else(|| env::var("SHELL").ok()); 94 | let arg = opt.arg.or_else(|| env::var("CARETAKER_ARG").ok()); 95 | if let Err(e) = watch( 96 | config, 97 | shell.as_deref().unwrap_or(SHELL), 98 | arg.as_deref().unwrap_or(ARGUMENT), 99 | ) { 100 | eprintln!("{}", e); 101 | } 102 | } 103 | Err(e) => { 104 | eprintln!("Unable to parse {}: {}", bold.paint(&opt.config), e); 105 | } 106 | }; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/watch.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | 3 | use nu_ansi_term::Style; 4 | use crossbeam_channel::unbounded; 5 | use glob::Pattern; 6 | use notify::{recommended_watcher, RecursiveMode, Watcher}; 7 | use serde::Deserialize; 8 | 9 | use std::convert::Infallible; 10 | use std::path::{is_separator, Path}; 11 | use std::process::{Command, Stdio}; 12 | 13 | #[cfg(target_family = "unix")] 14 | pub(crate) static SHELL: &str = "sh"; 15 | #[cfg(target_family = "unix")] 16 | pub(crate) static ARGUMENT: &str = "-c"; 17 | #[cfg(target_family = "windows")] 18 | pub(crate) static SHELL: &str = "cmd"; 19 | #[cfg(target_family = "windows")] 20 | pub(crate) static ARGUMENT: &str = "/c"; 21 | 22 | /// One path to watch 23 | #[derive(Deserialize)] 24 | pub struct Watch { 25 | /// A name of the action to do on the path change. 26 | #[serde(default)] 27 | pub name: String, 28 | /// The path to watch for change. 29 | pub path: String, 30 | /// The command to execute on path change. 31 | pub command: String, 32 | } 33 | 34 | /// The config file of Caretaker 35 | #[derive(Deserialize)] 36 | pub struct Config { 37 | /// A list of paths and commands to watch. 38 | pub watch: Vec, 39 | } 40 | 41 | /// Watch the paths specified in the config, executing the commands using the provided shell. 42 | pub fn watch(config: Config, shell: &str, arg: &str) -> Result { 43 | use notify::event::{EventKind::*, *}; 44 | 45 | let len = config.watch.len(); 46 | let mut watchers = Vec::new(); 47 | let (tx, rx) = unbounded(); 48 | let bold = Style::new().bold(); 49 | 50 | let is_glob = |c| c == '*' || c == '?' || c == '['; 51 | let matches = 52 | |pattern: &Pattern, path: &Path| path.to_str().map(|s| pattern.matches(s)).unwrap_or(false); 53 | 54 | for Watch { 55 | name, 56 | mut path, 57 | command, 58 | } in config.watch.into_iter() 59 | { 60 | let tx = tx.clone(); 61 | let glob = if path.contains(is_glob) { 62 | let mut last = 0; 63 | for (index, matched) in path.match_indices(is_separator) { 64 | if path[last..index].contains(is_glob) { 65 | break; 66 | }; 67 | last = index + matched.len(); 68 | } 69 | let pattern = Pattern::new( 70 | &Path::new(&path[0..last]) 71 | .canonicalize()? 72 | .join(&path[last..]) 73 | .to_string_lossy(), 74 | )?; 75 | path.truncate(last); 76 | Some(pattern) 77 | } else { 78 | None 79 | }; 80 | 81 | let mut cmd = Command::new(shell); 82 | cmd.args(&[arg, &command]).stdin(Stdio::null()); 83 | 84 | let mut watcher = recommended_watcher(move |res: Result| match res { 85 | Ok(Event { kind, paths, .. }) => match kind { 86 | Access(AccessKind::Close(AccessMode::Write)) 87 | | Modify(ModifyKind::Name(RenameMode::To)) 88 | | Remove(_) => { 89 | for path in &paths { 90 | if !glob 91 | .as_ref() 92 | .map(|pattern| matches(pattern, path)) 93 | .unwrap_or(true) 94 | { 95 | return; 96 | } 97 | 98 | println!("{:?} changed, running {}", path, bold.paint(&name)); 99 | if let Err(e) = cmd.env("EVENT_PATH", path).status() { 100 | tx.send(e.into()).unwrap(); 101 | } 102 | } 103 | } 104 | _ => {} 105 | }, 106 | Err(e) => { 107 | tx.send(e).unwrap(); 108 | } 109 | })?; 110 | watcher 111 | .watch(path.as_ref(), RecursiveMode::Recursive) 112 | .map_err(|source| Error::PathWatch { source, path })?; 113 | watchers.push(watcher); 114 | } 115 | 116 | println!( 117 | "Watching {} path{} for changes...", 118 | bold.paint(len.to_string()), 119 | if len == 1 { "" } else { "s" } 120 | ); 121 | rx.recv().map_err(|e| e.into()).and_then(|e| Err(e.into())) 122 | } 123 | --------------------------------------------------------------------------------