Showing preview only (819K chars total). Download the full file or copy to clipboard to get everything.
Repository: mattgreen/watchexec
Branch: main
Commit: 9b7fed6a52c7
Files: 232
Total size: 756.8 KB
Directory structure:
gitextract_c7s6gsdt/
├── .cargo/
│ └── config.toml
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── feature_request.md
│ │ └── regression.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── clippy.yml
│ ├── dist-manifest.jq
│ ├── release-cli.yml
│ └── tests.yml
├── .gitignore
├── .rustfmt.toml
├── CITATION.cff
├── CONTRIBUTING.md
├── Cargo.toml
├── LICENSE
├── README.md
├── bin/
│ ├── completions
│ ├── dates.mjs
│ ├── manpage
│ └── release-notes
├── cliff.toml
├── completions/
│ ├── bash
│ ├── elvish
│ ├── fish
│ ├── nu
│ ├── powershell
│ └── zsh
├── crates/
│ ├── bosion/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── examples/
│ │ │ ├── clap/
│ │ │ │ ├── Cargo.toml
│ │ │ │ ├── build.rs
│ │ │ │ └── src/
│ │ │ │ └── main.rs
│ │ │ ├── default/
│ │ │ │ ├── Cargo.toml
│ │ │ │ ├── build.rs
│ │ │ │ └── src/
│ │ │ │ ├── common.rs
│ │ │ │ └── main.rs
│ │ │ ├── no-git/
│ │ │ │ ├── Cargo.toml
│ │ │ │ ├── build.rs
│ │ │ │ └── src/
│ │ │ │ └── main.rs
│ │ │ ├── no-std/
│ │ │ │ ├── Cargo.toml
│ │ │ │ ├── build.rs
│ │ │ │ └── src/
│ │ │ │ └── main.rs
│ │ │ └── snapshots/
│ │ │ ├── build_date.txt
│ │ │ ├── build_datetime.txt
│ │ │ ├── crate_features.txt
│ │ │ ├── crate_version.txt
│ │ │ ├── default_long_version.txt
│ │ │ ├── default_long_version_with.txt
│ │ │ ├── git_commit_date.txt
│ │ │ ├── git_commit_datetime.txt
│ │ │ ├── git_commit_hash.txt
│ │ │ ├── git_commit_shorthash.txt
│ │ │ ├── no_git_long_version.txt
│ │ │ └── no_git_long_version_with.txt
│ │ ├── release.toml
│ │ ├── run-tests.sh
│ │ └── src/
│ │ ├── info.rs
│ │ └── lib.rs
│ ├── cli/
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── build.rs
│ │ ├── integration/
│ │ │ ├── env-unix.sh
│ │ │ ├── no-shell-unix.sh
│ │ │ ├── socket.sh
│ │ │ ├── stdin-quit-unix.sh
│ │ │ └── trailingargfile-unix.sh
│ │ ├── release.toml
│ │ ├── run-tests.sh
│ │ ├── src/
│ │ │ ├── args/
│ │ │ │ ├── command.rs
│ │ │ │ ├── events.rs
│ │ │ │ ├── filtering.rs
│ │ │ │ ├── logging.rs
│ │ │ │ └── output.rs
│ │ │ ├── args.rs
│ │ │ ├── config.rs
│ │ │ ├── dirs.rs
│ │ │ ├── emits.rs
│ │ │ ├── filterer/
│ │ │ │ ├── parse.rs
│ │ │ │ ├── proglib/
│ │ │ │ │ ├── file.rs
│ │ │ │ │ ├── hash.rs
│ │ │ │ │ ├── kv.rs
│ │ │ │ │ ├── macros.rs
│ │ │ │ │ └── output.rs
│ │ │ │ ├── proglib.rs
│ │ │ │ ├── progs.rs
│ │ │ │ └── syncval.rs
│ │ │ ├── filterer.rs
│ │ │ ├── lib.rs
│ │ │ ├── main.rs
│ │ │ ├── socket/
│ │ │ │ ├── fallback.rs
│ │ │ │ ├── parser.rs
│ │ │ │ ├── test.rs
│ │ │ │ ├── unix.rs
│ │ │ │ └── windows.rs
│ │ │ ├── socket.rs
│ │ │ └── state.rs
│ │ ├── tests/
│ │ │ ├── common/
│ │ │ │ └── mod.rs
│ │ │ └── ignore.rs
│ │ ├── watchexec-manifest.rc
│ │ └── watchexec.exe.manifest
│ ├── events/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── examples/
│ │ │ └── parse-and-print.rs
│ │ ├── release.toml
│ │ ├── src/
│ │ │ ├── event.rs
│ │ │ ├── fs.rs
│ │ │ ├── keyboard.rs
│ │ │ ├── lib.rs
│ │ │ ├── process.rs
│ │ │ ├── sans_notify.rs
│ │ │ └── serde_formats.rs
│ │ └── tests/
│ │ ├── json.rs
│ │ └── snapshots/
│ │ ├── array.json
│ │ ├── asymmetric.json
│ │ ├── completions.json
│ │ ├── metadata.json
│ │ ├── paths.json
│ │ ├── signals.json
│ │ ├── single.json
│ │ └── sources.json
│ ├── filterer/
│ │ ├── globset/
│ │ │ ├── CHANGELOG.md
│ │ │ ├── Cargo.toml
│ │ │ ├── README.md
│ │ │ ├── release.toml
│ │ │ ├── src/
│ │ │ │ └── lib.rs
│ │ │ └── tests/
│ │ │ ├── filtering.rs
│ │ │ └── helpers/
│ │ │ └── mod.rs
│ │ └── ignore/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── release.toml
│ │ ├── src/
│ │ │ └── lib.rs
│ │ └── tests/
│ │ ├── filtering.rs
│ │ ├── helpers/
│ │ │ └── mod.rs
│ │ └── ignores/
│ │ ├── allowlist
│ │ ├── folders
│ │ ├── globs
│ │ ├── negate
│ │ ├── none-allowed
│ │ ├── scopes-global
│ │ ├── scopes-local
│ │ ├── scopes-sublocal
│ │ └── self.ignore
│ ├── ignore-files/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── release.toml
│ │ ├── src/
│ │ │ ├── discover.rs
│ │ │ ├── error.rs
│ │ │ ├── filter.rs
│ │ │ └── lib.rs
│ │ └── tests/
│ │ ├── filtering.rs
│ │ ├── global/
│ │ │ ├── first
│ │ │ └── second
│ │ ├── helpers/
│ │ │ └── mod.rs
│ │ └── tree/
│ │ ├── base
│ │ └── branch/
│ │ └── inner
│ ├── lib/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── examples/
│ │ │ ├── only_commands.rs
│ │ │ ├── only_events.rs
│ │ │ ├── readme.rs
│ │ │ └── restart_run_on_successful_build.rs
│ │ ├── release.toml
│ │ ├── src/
│ │ │ ├── action/
│ │ │ │ ├── handler.rs
│ │ │ │ ├── quit.rs
│ │ │ │ ├── return.rs
│ │ │ │ └── worker.rs
│ │ │ ├── action.rs
│ │ │ ├── changeable.rs
│ │ │ ├── config.rs
│ │ │ ├── error/
│ │ │ │ ├── critical.rs
│ │ │ │ ├── runtime.rs
│ │ │ │ └── specialised.rs
│ │ │ ├── error.rs
│ │ │ ├── filter.rs
│ │ │ ├── id.rs
│ │ │ ├── late_join_set.rs
│ │ │ ├── lib.rs
│ │ │ ├── paths.rs
│ │ │ ├── sources/
│ │ │ │ ├── fs.rs
│ │ │ │ ├── keyboard.rs
│ │ │ │ └── signal.rs
│ │ │ ├── sources.rs
│ │ │ ├── watched_path.rs
│ │ │ └── watchexec.rs
│ │ └── tests/
│ │ ├── env_reporting.rs
│ │ └── error_handler.rs
│ ├── project-origins/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── examples/
│ │ │ └── find-origins.rs
│ │ ├── release.toml
│ │ └── src/
│ │ └── lib.rs
│ ├── signals/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── release.toml
│ │ └── src/
│ │ └── lib.rs
│ ├── supervisor/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── release.toml
│ │ ├── src/
│ │ │ ├── command/
│ │ │ │ ├── conversions.rs
│ │ │ │ ├── program.rs
│ │ │ │ └── shell.rs
│ │ │ ├── command.rs
│ │ │ ├── errors.rs
│ │ │ ├── flag.rs
│ │ │ ├── job/
│ │ │ │ ├── job.rs
│ │ │ │ ├── messages.rs
│ │ │ │ ├── priority.rs
│ │ │ │ ├── state.rs
│ │ │ │ ├── task.rs
│ │ │ │ ├── test.rs
│ │ │ │ └── testchild.rs
│ │ │ ├── job.rs
│ │ │ └── lib.rs
│ │ └── tests/
│ │ └── programs.rs
│ └── test-socketfd/
│ ├── Cargo.toml
│ ├── README.md
│ └── src/
│ └── main.rs
└── doc/
├── packages.md
├── socket.md
├── watchexec.1
└── watchexec.1.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .cargo/config.toml
================================================
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
[target.armv7-unknown-linux-musleabihf]
linker = "arm-linux-musleabihf-gcc"
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-musl-gcc"
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[cli/tests/snapshots/*]
indent_style = space
trim_trailing_whitespace = false
[*.{md,ronn}]
indent_style = space
indent_size = 4
[*.{cff,yml}]
indent_size = 2
indent_style = space
================================================
FILE: .gitattributes
================================================
Cargo.lock merge=binary
doc/watchexec.* merge=binary
completions/* merge=binary
================================================
FILE: .github/FUNDING.yml
================================================
liberapay: passcod
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Something is wrong
title: ''
labels: bug, need-info
assignees: ''
---
Please delete this template text before filing, but you _need_ to include the following:
- Watchexec's version
- The OS you're using
- A log with `-vvv --log-file` (if it has sensitive info you can email it at felix@passcod.name — do that _after_ filing so you can reference the issue ID)
- A sample command that you've run that has the issue
Thank you
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Something is missing
title: ''
labels: feature
assignees: ''
---
<!-- Please note that this project has a high threshold for changing default behaviour or breaking compatibility. If your feature or change can be done without breaking, present it that way. -->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
If proposing a new CLI option, option names you think would fit.
**Additional context**
Add any other context about the feature request here.
================================================
FILE: .github/ISSUE_TEMPLATE/regression.md
================================================
---
name: Regression
about: Something changed unexpectedly
title: ''
labels: ''
assignees: ''
---
**What used to happen**
**What happens now**
**Details**
- Latest version that worked:
- Earliest version that doesn't: (don't sweat testing earlier versions if you don't remember or have time, your current version will do)
- OS:
- A debug log with `-vvv --log-file`:
```
```
<!-- You may truncate the log to just the part supporting your report if you're confident the rest is irrelevant. If it contains sensitive information (if you can't reduce/reproduce outside of work you'd rather remain private, you can either redact it or send it by email.) -->
================================================
FILE: .github/dependabot.yml
================================================
# Dependabot dependency version checks / updates
version: 2
updates:
- package-ecosystem: "github-actions"
# Workflow files stored in the
# default location of `.github/workflows`
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/cli"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/lib"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/events"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/signals"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/supervisor"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/filterer/ignore"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/filterer/globset"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/bosion"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/ignore-files"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/crates/project-origins"
schedule:
interval: "weekly"
================================================
FILE: .github/workflows/clippy.yml
================================================
name: Clippy
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
tags-ignore:
- "*"
env:
CARGO_TERM_COLOR: always
CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
clippy:
strategy:
fail-fast: false
matrix:
platform:
- ubuntu
- windows
- macos
name: Clippy on ${{ matrix.platform }}
runs-on: "${{ matrix.platform }}-latest"
steps:
- uses: actions/checkout@v6
- name: Configure toolchain
run: |
rustup toolchain install stable --profile minimal --no-self-update --component clippy
rustup default stable
# https://github.com/actions/cache/issues/752
- if: ${{ runner.os == 'Windows' }}
name: Use GNU tar
shell: cmd
run: |
echo "Adding GNU tar to PATH"
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
- name: Configure caching
uses: actions/cache@v5
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- run: cargo clippy
================================================
FILE: .github/workflows/dist-manifest.jq
================================================
{
dist_version: "0.0.2",
releases: [{
app_name: "watchexec",
app_version: $version,
changelog_title: "CLI \($version)",
artifacts: [ $files | split("\n") | .[] | {
name: .,
kind: (if (. | test("[.](deb|rpm)$")) then "installer" else "executable-zip" end),
target_triples: (. | [capture("watchexec-[^-]+-(?<target>[^.]+)[.].+").target]),
assets: ([[
{
kind: "executable",
name: (if (. | test("windows")) then "watchexec.exe" else "watchexec" end),
path: "\(
capture("(?<dir>watchexec-[^-]+-[^.]+)[.].+").dir
)\(
if (. | test("windows")) then "\\watchexec.exe" else "/watchexec" end
)",
},
(if (. | test("[.](deb|rpm)$")) then null else {kind: "readme", name: "README.md"} end),
(if (. | test("[.](deb|rpm)$")) then null else {kind: "license", name: "LICENSE"} end)
][] | select(. != null)])
} ]
}]
}
================================================
FILE: .github/workflows/release-cli.yml
================================================
name: CLI Release
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
env:
CARGO_TERM_COLOR: always
CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
jobs:
info:
name: Gather info
runs-on: ubuntu-latest
outputs:
cli_version: ${{ steps.version.outputs.cli_version }}
steps:
- uses: actions/checkout@v6
- name: Extract version
id: version
shell: bash
run: |
set -euxo pipefail
version=$(grep -m1 -F 'version =' crates/cli/Cargo.toml | cut -d\" -f2)
if [[ -z "$version" ]]; then
echo "Error: no version :("
exit 1
fi
echo "cli_version=$version" >> $GITHUB_OUTPUT
build:
strategy:
matrix:
include:
- name: linux-amd64-gnu
os: ubuntu-22.04
target: x86_64-unknown-linux-gnu
cross: false
experimental: false
- name: linux-amd64-musl
os: ubuntu-24.04
target: x86_64-unknown-linux-musl
cross: false
experimental: false
- name: linux-i686-musl
os: ubuntu-22.04
target: i686-unknown-linux-musl
cross: true
experimental: true
- name: linux-armhf-gnu
os: ubuntu-24.04
target: armv7-unknown-linux-gnueabihf
cross: true
experimental: false
- name: linux-arm64-gnu
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
cross: false
experimental: false
- name: linux-arm64-musl
os: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
cross: false
experimental: false
- name: linux-s390x-gnu
os: ubuntu-24.04
target: s390x-unknown-linux-gnu
cross: true
experimental: true
- name: linux-riscv64gc-gnu
os: ubuntu-24.04
target: riscv64gc-unknown-linux-gnu
cross: true
experimental: true
- name: linux-ppc64le-gnu
os: ubuntu-24.04
target: powerpc64le-unknown-linux-gnu
cross: true
experimental: true
- name: illumos-x86-64
os: ubuntu-24.04
target: x86_64-unknown-illumos
cross: true
experimental: true
- name: freebsd-x86-64
os: ubuntu-24.04
target: x86_64-unknown-freebsd
cross: true
experimental: true
- name: linux-loongarch64-gnu
os: ubuntu-24.04
target: loongarch64-unknown-linux-gnu
cross: true
experimental: true
- name: mac-x86-64
os: macos-14
target: x86_64-apple-darwin
cross: false
experimental: false
- name: mac-arm64
os: macos-15
target: aarch64-apple-darwin
cross: false
experimental: false
- name: windows-x86-64
os: windows-latest
target: x86_64-pc-windows-msvc
cross: false
experimental: false
#- name: windows-arm64
# os: windows-latest
# target: aarch64-pc-windows-msvc
# cross: true
# experimental: true
name: Binaries for ${{ matrix.name }}
needs: info
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
env:
version: ${{ needs.info.outputs.cli_version }}
dst: watchexec-${{ needs.info.outputs.cli_version }}-${{ matrix.target }}
steps:
- uses: actions/checkout@v6
# https://github.com/actions/cache/issues/752
- if: ${{ runner.os == 'Windows' }}
name: Use GNU tar
shell: cmd
run: |
echo "Adding GNU tar to PATH"
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
- run: sudo apt update
if: startsWith(matrix.os, 'ubuntu-')
- name: Add musl tools
run: sudo apt install -y musl musl-dev musl-tools
if: endsWith(matrix.target, '-musl')
- name: Add aarch-gnu tools
run: sudo apt install -y gcc-aarch64-linux-gnu
if: startsWith(matrix.target, 'aarch64-unknown-linux')
- name: Add arm7hf-gnu tools
run: sudo apt install -y gcc-arm-linux-gnueabihf
if: startsWith(matrix.target, 'armv7-unknown-linux-gnueabihf')
- name: Add s390x-gnu tools
run: sudo apt install -y gcc-s390x-linux-gnu
if: startsWith(matrix.target, 's390x-unknown-linux-gnu')
- name: Add riscv64-gnu tools
run: sudo apt install -y gcc-riscv64-linux-gnu
if: startsWith(matrix.target, 'riscv64gc-unknown-linux-gnu')
- name: Add ppc64le-gnu tools
run: sudo apt install -y gcc-powerpc64le-linux-gnu
if: startsWith(matrix.target, 'powerpc64le-unknown-linux-gnu')
- name: Install cargo-deb
if: startsWith(matrix.name, 'linux-')
uses: taiki-e/install-action@v2
with:
tool: cargo-deb
- name: Install cargo-generate-rpm
if: startsWith(matrix.name, 'linux-')
uses: taiki-e/install-action@v2
with:
tool: cargo-generate-rpm
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
rustup target add ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- name: Install cross
if: matrix.cross
uses: taiki-e/install-action@v2
with:
tool: cross
- name: Build
shell: bash
run: |
${{ matrix.cross && 'cross' || 'cargo' }} build \
-p watchexec-cli \
--release --locked \
--target ${{ matrix.target }}
- name: Package
shell: bash
run: |
set -euxo pipefail
ext=""
[[ "${{ matrix.name }}" == windows-* ]] && ext=".exe"
bin="target/${{ matrix.target }}/release/watchexec${ext}"
objcopy --compress-debug-sections "$bin" || true
mkdir "$dst"
mkdir -p "target/release"
cp "$bin" "target/release/" # workaround for cargo-deb silliness with targets
cp "$bin" "$dst/"
cp -r crates/cli/README.md LICENSE completions doc/{logo.svg,watchexec.1{,.*}} "$dst/"
- name: Archive (tar)
if: '! startsWith(matrix.name, ''windows-'')'
run: tar cavf "$dst.tar.xz" "$dst"
- name: Archive (deb)
if: startsWith(matrix.name, 'linux-')
run: cargo deb -p watchexec-cli --no-build --no-strip --target ${{ matrix.target }} --output "$dst.deb"
- name: Archive (rpm)
if: startsWith(matrix.name, 'linux-')
shell: bash
run: |
set -euxo pipefail
shopt -s globstar
cargo generate-rpm -p crates/cli --target "${{ matrix.target }}" --target-dir "target/${{ matrix.target }}"
mv target/**/*.rpm "$dst.rpm"
- name: Archive (zip)
if: startsWith(matrix.name, 'windows-')
shell: bash
run: 7z a "$dst.zip" "$dst"
- uses: actions/upload-artifact@v6
with:
name: ${{ matrix.name }}
retention-days: 1
path: |
watchexec-*.tar.xz
watchexec-*.tar.zst
watchexec-*.deb
watchexec-*.rpm
watchexec-*.zip
upload:
needs: [build, info]
name: Checksum and publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install b3sum
uses: taiki-e/install-action@v2
with:
tool: b3sum
- uses: actions/download-artifact@v7
with:
merge-multiple: true
- name: Dist manifest
run: |
jq -ncf .github/workflows/dist-manifest.jq \
--arg version "${{ needs.info.outputs.cli_version }}" \
--arg files "$(ls watchexec-*)" \
> dist-manifest.json
- name: Bulk checksums
run: |
b3sum watchexec-* | tee B3SUMS
sha512sum watchexec-* | tee SHA512SUMS
sha256sum watchexec-* | tee SHA256SUMS
- name: File checksums
run: |
for file in watchexec-*; do
b3sum --no-names $file > "$file.b3"
sha256sum $file | cut -d ' ' -f1 > "$file.sha256"
sha512sum $file | cut -d ' ' -f1 > "$file.sha512"
done
- uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b
with:
tag_name: v${{ needs.info.outputs.cli_version }}
name: CLI v${{ needs.info.outputs.cli_version }}
append_body: true
files: |
dist-manifest.json
watchexec-*.tar.xz
watchexec-*.tar.zst
watchexec-*.deb
watchexec-*.rpm
watchexec-*.zip
*SUMS
*.b3
*.sha*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/tests.yml
================================================
name: Test suite
on:
workflow_dispatch:
pull_request:
types:
- opened
- reopened
- synchronize
push:
branches:
- main
tags-ignore:
- "*"
env:
CARGO_TERM_COLOR: always
CARGO_UNSTABLE_SPARSE_REGISTRY: "true"
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
libs:
strategy:
fail-fast: false
matrix:
platform:
- macos
- ubuntu
- windows
name: Test libraries ${{ matrix.platform }}
runs-on: "${{ matrix.platform }}-latest"
steps:
- uses: actions/checkout@v6
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
# https://github.com/actions/cache/issues/752
- if: ${{ runner.os == 'Windows' }}
name: Use GNU tar
shell: cmd
run: |
echo "Adding GNU tar to PATH"
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
- uses: Swatinem/rust-cache@v2
- name: Run library test suite
run: cargo test --workspace --exclude watchexec-cli --exclude watchexec-events
- name: Run watchexec-events integration tests
run: cargo test -p watchexec-events -F serde
cli-e2e:
strategy:
fail-fast: false
matrix:
platform:
- macos
- ubuntu
- windows
name: Test CLI (e2e) ${{ matrix.platform }}
runs-on: "${{ matrix.platform }}-latest"
steps:
- uses: actions/checkout@v6
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
# https://github.com/actions/cache/issues/752
- if: ${{ runner.os == 'Windows' }}
name: Use GNU tar
shell: cmd
run: |
echo "Adding GNU tar to PATH"
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
- name: Install coreutils on mac
if: ${{ matrix.platform == 'macos' }}
run: brew install coreutils
- uses: Swatinem/rust-cache@v2
- name: Build CLI programs
run: cargo build
- name: Run CLI integration tests
run: crates/cli/run-tests.sh ${{ matrix.platform }}
shell: bash
env:
WATCHEXEC_BIN: target/debug/watchexec
TEST_SOCKETFD_BIN: target/debug/test-socketfd
cli-docs:
name: Test CLI docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
- uses: Swatinem/rust-cache@v2
- name: Generate manpage
run: cargo run -p watchexec-cli -- --manual > doc/watchexec.1
- name: Check that manpage is up to date
run: git diff --exit-code -- doc/
- name: Generate completions
run: bin/completions
- name: Check that completions are up to date
run: git diff --exit-code -- completions/
cli-unit:
strategy:
fail-fast: false
matrix:
platform:
- macos
- ubuntu
- windows
name: Test CLI (unit) ${{ matrix.platform }}
runs-on: "${{ matrix.platform }}-latest"
steps:
- uses: actions/checkout@v6
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
# https://github.com/actions/cache/issues/752
- if: ${{ runner.os == 'Windows' }}
name: Use GNU tar
shell: cmd
run: |
echo "Adding GNU tar to PATH"
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
- uses: Swatinem/rust-cache@v2
- name: Run CLI unit tests
run: cargo test -p watchexec-cli
bosion:
strategy:
fail-fast: false
matrix:
platform:
- macos
- ubuntu
- windows
name: Bosion integration tests on ${{ matrix.platform }}
runs-on: "${{ matrix.platform }}-latest"
steps:
- uses: actions/checkout@v6
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
# https://github.com/actions/cache/issues/752
- if: ${{ runner.os == 'Windows' }}
name: Use GNU tar
shell: cmd
run: |
echo "Adding GNU tar to PATH"
echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%"
- uses: Swatinem/rust-cache@v2
- name: Run bosion integration tests
run: ./run-tests.sh
working-directory: crates/bosion
shell: bash
cross-checks:
strategy:
fail-fast: false
matrix:
target:
- x86_64-unknown-linux-musl
- x86_64-unknown-freebsd
name: Typecheck only on ${{ matrix.target }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Configure toolchain
run: |
rustup toolchain install --profile minimal --no-self-update stable
rustup default stable
rustup target add ${{ matrix.target }}
- if: matrix.target == 'x86_64-unknown-linux-musl'
run: sudo apt-get install -y musl-tools
- uses: Swatinem/rust-cache@v2
- run: cargo check --target ${{ matrix.target }}
tests-pass:
if: always()
name: Tests pass
needs:
- bosion
- cli-e2e
- cli-unit
- cross-checks
- libs
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
================================================
FILE: .gitignore
================================================
target
/watchexec-*
watchexec.*.log
================================================
FILE: .rustfmt.toml
================================================
hard_tabs = true
================================================
FILE: CITATION.cff
================================================
cff-version: 1.2.0
message: |
If you use this software, please cite it using these metadata.
title: "Watchexec: a tool to react to filesystem changes, and a crate ecosystem to power it"
version: "2.5.1"
date-released: 2026-03-30
repository-code: https://github.com/watchexec/watchexec
license: Apache-2.0
authors:
- family-names: Green
given-names: Matt
- family-names: Saparelli
given-names: Félix
orcid: https://orcid.org/0000-0002-2010-630X
================================================
FILE: CONTRIBUTING.md
================================================
# Contribution guidebook
This is a fairly free-form project, with low contribution traffic.
Maintainers:
- Félix Saparelli (@passcod) (active)
- Matt Green (@mattgreen) (original author, mostly checked out)
There are a few anti goals:
- Calling watchexec is to be a **simple** exercise that remains intuitive. As a specific point, it
should not involve any piping or require xargs.
- Watchexec will not be tied to any particular ecosystem or language. Projects that themselves use
watchexec (the library) can be focused on a particular domain (for example Cargo Watch for Rust),
but watchexec itself will remain generic, usable for any purpose.
## Debugging
To enable verbose logging in tests, run with:
```console
$ env WATCHEXEC_LOG=watchexec=trace,info RUST_TEST_THREADS=1 RUST_NOCAPTURE=1 cargo test --test testfile -- testname
```
To use [Tokio Console](https://github.com/tokio-rs/console):
1. Add `--cfg tokio_unstable` to your `RUSTFLAGS`.
2. Run the CLI with the `dev-console` feature.
## PR etiquette
- Maintainers are busy or may not have the bandwidth, be patient.
- Do _not_ change the version number in the PR.
- Do _not_ change Cargo.toml or other project metadata, unless specifically asked for, or if that's
the point of the PR (like adding a crates.io category).
Apart from that, welcome and thank you for your time!
## Releasing
```
cargo release -p crate-name --execute patch # or minor, major
```
When a CLI release is done, the [release notes](https://github.com/watchexec/watchexec/releases) should be edited with the changelog.
### Release order
Use this command to see the tree of workspace dependencies:
```console
$ cargo tree -p watchexec-cli | rg -F '(/' --color=never | sed 's/ v[0-9].*//'
```
## Overview
The architecture of watchexec is roughly:
- sources gather events
- events are debounced and filtered
- event(s) make it through the debounce/filters and trigger an "action"
- `on_action` handler is called, returning an `Outcome`
- outcome is processed into managing the command that watchexec is running
- outcome can also be to exit
- when a command is started, the `on_pre_spawn` and `on_post_spawn` handlers are called
- commands are also a source of events, so e.g. "command has finished" is handled by `on_action`
And this is the startup sequence:
- init config sets basic immutable facts about the runtime
- runtime starts:
- source workers start, and are passed their runtime config
- action worker starts, and is passed its runtime config
- (unless `--postpone` is given) a synthetic event is injected to kickstart things
## Guides
These are generic guides for implementing specific bits of functionality.
### Adding an event source
- add a worker for "sourcing" events. Looking at the [signal source
worker](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/signal/source.rs) is
probably easiest to get started here.
- because we may not always want to enable this event source, and just to be flexible, add [runtime
config](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/config.rs) for the source.
- for convenience, probably add [a method on the runtime
config](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/config.rs) which
configures the most common usecase.
- because watchexec is reconfigurable, in the worker you'll need to react to config changes. Look at
how the [fs worker does it](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/fs.rs)
for reference.
- you may need to [add to the event tag
enum](https://github.com/watchexec/watchexec/blob/main/crates/lib/src/event.rs).
- if you do, you should [add support to the "tagged
filterer"](https://github.com/watchexec/watchexec/blob/main/crates/filterer/tagged/src/parse.rs),
but this can be done in follow-up work.
### Process a new event in the CLI
- add an option to the
[args](https://github.com/watchexec/watchexec/blob/main/crates/cli/src/args.rs) if necessary
- add to the [runtime
config](https://github.com/watchexec/watchexec/blob/main/crates/cli/src/config/runtime.rs) when
the option is present
- process relevant events [in the action
handler](https://github.com/watchexec/watchexec/blob/main/crates/cli/src/config/runtime.rs)
---
vim: tw=100
================================================
FILE: Cargo.toml
================================================
[workspace]
resolver = "2"
members = [
"crates/lib",
"crates/cli",
"crates/events",
"crates/signals",
"crates/supervisor",
"crates/filterer/globset",
"crates/filterer/ignore",
"crates/bosion",
"crates/ignore-files",
"crates/project-origins",
"crates/test-socketfd",
]
[workspace.dependencies]
rand = "0.9.1"
uuid = "1.5.0"
[profile.release]
lto = true
debug = 1 # for stack traces
codegen-units = 1
strip = "symbols"
[profile.dev.build-override]
opt-level = 0
codegen-units = 1024
debug = false
debug-assertions = false
overflow-checks = false
incremental = false
[profile.release.build-override]
opt-level = 0
codegen-units = 1024
debug = false
debug-assertions = false
overflow-checks = false
incremental = false
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
[](https://github.com/watchexec/watchexec/actions/workflows/tests.yml)
# Watchexec
Software development often involves running the same commands over and over. Boring!
`watchexec` is a simple, standalone tool that watches a path and runs a command whenever it detects modifications.
Example use cases:
* Automatically run unit tests
* Run linters/syntax checkers
* Rebuild artifacts
## Features
* Simple invocation and use, does not require a cryptic command line involving `xargs`
* Runs on OS X, Linux, and Windows
* Monitors current directory and all subdirectories for changes
* Coalesces multiple filesystem events into one, for editors that use swap/backup files during saving
* Loads `.gitignore` and `.ignore` files
* Uses process groups to keep hold of forking programs
* Provides the paths that changed in environment variables or STDIN
* Does not require a language runtime, not tied to any particular language or ecosystem
* [And more!](./crates/cli/#features)
## Quick start
Watch all JavaScript, CSS and HTML files in the current directory and all subdirectories for changes, running `npm run build` when a change is detected:
$ watchexec -e js,css,html npm run build
Call/restart `python server.py` when any Python file in the current directory (and all subdirectories) changes:
$ watchexec -r -e py -- python server.py
More usage examples: [in the CLI README](./crates/cli/#usage-examples)!
## Install
<a href="https://repology.org/project/watchexec/versions"><img align="right" src="https://repology.org/badge/vertical-allrepos/watchexec.svg" alt="Packaging status"></a>
- With [your package manager](./doc/packages.md) for Arch, Debian, Homebrew, Nix, Scoop, Chocolatey…
- From binary with [Binstall](https://github.com/cargo-bins/cargo-binstall): `cargo binstall watchexec-cli` <!-- this line does NOT contain a typo -->
- As [pre-built binary package from Github](https://github.com/watchexec/watchexec/releases/latest)
- From source with Cargo: `cargo install --locked watchexec-cli`
All options in detail: [in the CLI README](./crates/cli/#installation),
in the online help (`watchexec -h`, `watchexec --help`, or `watchexec --manual`),
and [in the manual page](./doc/watchexec.1.md).
## Augment
Watchexec pairs well with:
- [checkexec](https://github.com/kurtbuilds/checkexec): to run only when source files are newer than a target file
- [just](https://github.com/casey/just): a modern alternative to `make`
- [systemfd](https://github.com/mitsuhiko/systemfd): socket-passing in development
## Extend
- [watchexec library](./crates/lib/): to create more specialised watchexec-powered tools.
- [watchexec-events](./crates/events/): event types for watchexec.
- [watchexec-signals](./crates/signals/): signal types for watchexec.
- [watchexec-supervisor](./crates/supervisor/): process lifecycle manager (the _exec_ part of watchexec).
- [clearscreen](https://github.com/watchexec/clearscreen): to clear the (terminal) screen on every platform.
- [command group](https://github.com/watchexec/command-group): to run commands in process groups.
- [ignore files](./crates/ignore-files/): to find, parse, and interpret ignore files.
- [project origins](./crates/project-origins/): to find the origin(s) directory of a project.
- [notify](https://github.com/notify-rs/notify): to respond to file modifications (third-party).
### Downstreams
Selected downstreams of watchexec and associated crates:
- [cargo watch](https://github.com/watchexec/cargo-watch): a specialised watcher for Rust/Cargo projects.
- [cargo lambda](https://github.com/cargo-lambda/cargo-lambda): a dev tool for Rust-powered AWS Lambda functions.
- [create-rust-app](https://create-rust-app.dev): a template for Rust+React web apps.
- [devenv.sh](https://github.com/cachix/devenv): a developer environment with nix-based declarative configs.
- [dotter](https://github.com/supercuber/dotter): a dotfile manager.
- [ghciwatch](https://github.com/mercurytechnologies/ghciwatch): a specialised watcher for Haskell projects.
- [tectonic](https://tectonic-typesetting.github.io/book/latest/): a TeX/LaTeX typesetting system.
================================================
FILE: bin/completions
================================================
#!/bin/sh
cargo run -p watchexec-cli $* -- --completions bash > completions/bash
cargo run -p watchexec-cli $* -- --completions elvish > completions/elvish
cargo run -p watchexec-cli $* -- --completions fish > completions/fish
cargo run -p watchexec-cli $* -- --completions nu > completions/nu
cargo run -p watchexec-cli $* -- --completions powershell > completions/powershell
cargo run -p watchexec-cli $* -- --completions zsh > completions/zsh
================================================
FILE: bin/dates.mjs
================================================
#!/usr/bin/env node
const id = Math.floor(Math.random() * 100);
let n = 0;
const m = 5;
while (n < m) {
n += 1;
console.log(`[${id} : ${n}/${m}] ${new Date}`);
await new Promise(done => setTimeout(done, 2000));
}
================================================
FILE: bin/manpage
================================================
#!/bin/sh
cargo run -p watchexec-cli -- --manual > doc/watchexec.1
pandoc doc/watchexec.1 -t markdown > doc/watchexec.1.md
================================================
FILE: bin/release-notes
================================================
#!/bin/sh
exec git cliff --include-path '**/crates/cli/**/*' --count-tags 'v*' --unreleased $*
================================================
FILE: cliff.toml
================================================
[changelog]
trim = true
header = ""
footer = ""
body = """
{% if version %}\
## v{{ version | trim_start_matches(pat="v") }} ({{ timestamp | date(format="%Y-%m-%d") }})
{% else %}\
## [unreleased]
{% endif %}\
{% raw %}\n{% endraw %}\
{%- for commit in commits | sort(attribute="group") %}
{%- if commit.scope -%}
{% else -%}
- **{{commit.group | striptags | trim | upper_first}}:** \
{% if commit.breaking %} [**⚠️ breaking ⚠️**] {% endif %}\
{{ commit.message | upper_first }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }}))
{% endif -%}
{% endfor -%}
{% for scope, commits in commits | filter(attribute="group") | group_by(attribute="scope") %}
### {{ scope | striptags | trim | upper_first }}
{% for commit in commits | sort(attribute="group") %}
- **{{commit.group | striptags | trim | upper_first}}:** \
{% if commit.breaking %} [**⚠️ breaking ⚠️**] {% endif %}\
{{ commit.message | upper_first }} - ([{{ commit.id | truncate(length=7, end="") }}]($REPO/commit/{{ commit.id }}))
{%- endfor -%}
{% raw %}\n{% endraw %}\
{% endfor %}
"""
postprocessors = [
{ pattern = '\$REPO', replace = "https://github.com/watchexec/watchexec" },
]
[git]
conventional_commits = true
filter_unconventional = true
split_commits = true
protect_breaking_commits = true
filter_commits = true
tag_pattern = "v[0-9].*"
sort_commits = "oldest"
link_parsers = [
{ pattern = "#(\\d+)", href = "https://github.com/watchexec/watchexec/issues/$1"},
{ pattern = "RFC(\\d+)", text = "ietf-rfc$1", href = "https://datatracker.ietf.org/doc/html/rfc$1"},
]
commit_parsers = [
{ message = "^feat", group = "Feature" },
{ message = "^fix", group = "Bugfix" },
{ message = "^tweak", group = "Tweak" },
{ message = "^doc", group = "Documentation" },
{ message = "^perf", group = "Performance" },
{ message = "^deps", group = "Deps" },
{ message = "^Initial [cC]ommit$", skip = true },
{ message = "^(release|merge|fmt|chore|ci|refactor|style|draft|wip|repo)", skip = true },
{ body = ".*breaking", group = "Breaking" },
{ body = ".*security", group = "Security" },
{ message = "^revert", group = "Revert" },
]
================================================
FILE: completions/bash
================================================
_watchexec() {
local i cur prev opts cmd
COMPREPLY=()
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
cur="$2"
else
cur="${COMP_WORDS[COMP_CWORD]}"
fi
prev="$3"
cmd=""
opts=""
for i in "${COMP_WORDS[@]:0:COMP_CWORD}"
do
case "${cmd},${i}" in
",$1")
cmd="watchexec"
;;
*)
;;
esac
done
case "${cmd}" in
watchexec)
opts="-1 -n -E -o -r -s -d -I -p -w -W -F -e -f -j -i -v -c -N -q -h -V --manual --completions --only-emit-events --shell --no-environment --env --no-process-group --wrap-process --stop-signal --stop-timeout --timeout --delay-run --workdir --socket --on-busy-update --restart --signal --map-signal --debounce --stdin-quit --interactive --exit-on-error --postpone --poll --emit-events-to --watch --watch-non-recursive --watch-file --no-vcs-ignore --no-project-ignore --no-global-ignore --no-default-ignore --no-discover-ignore --ignore-nothing --exts --filter --filter-file --project-origin --filter-prog --ignore --ignore-file --fs-events --no-meta --verbose --log-file --print-events --clear --notify --color --timings --quiet --bell --help --version [COMMAND]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--completions)
COMPREPLY=($(compgen -W "bash elvish fish nu powershell zsh" -- "${cur}"))
return 0
;;
--shell)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--env)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-E)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--wrap-process)
COMPREPLY=($(compgen -W "group session none" -- "${cur}"))
return 0
;;
--stop-signal)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--stop-timeout)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--timeout)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--delay-run)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--workdir)
COMPREPLY=()
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o plusdirs
fi
return 0
;;
--socket)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--on-busy-update)
COMPREPLY=($(compgen -W "queue do-nothing restart signal" -- "${cur}"))
return 0
;;
-o)
COMPREPLY=($(compgen -W "queue do-nothing restart signal" -- "${cur}"))
return 0
;;
--signal)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-s)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--map-signal)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--debounce)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-d)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--poll)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--emit-events-to)
COMPREPLY=($(compgen -W "environment stdio file json-stdio json-file none" -- "${cur}"))
return 0
;;
--watch)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-w)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--watch-non-recursive)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-W)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--watch-file)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-F)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--exts)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-e)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--filter)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-f)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--filter-file)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--project-origin)
COMPREPLY=()
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o plusdirs
fi
return 0
;;
--filter-prog)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-j)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--ignore)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-i)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--ignore-file)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--fs-events)
COMPREPLY=($(compgen -W "access create remove rename modify metadata" -- "${cur}"))
return 0
;;
--log-file)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--clear)
COMPREPLY=($(compgen -W "clear reset" -- "${cur}"))
return 0
;;
-c)
COMPREPLY=($(compgen -W "clear reset" -- "${cur}"))
return 0
;;
--notify)
COMPREPLY=($(compgen -W "both start end" -- "${cur}"))
return 0
;;
-N)
COMPREPLY=($(compgen -W "both start end" -- "${cur}"))
return 0
;;
--color)
COMPREPLY=($(compgen -W "auto always never" -- "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
esac
}
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
complete -F _watchexec -o nosort -o bashdefault -o default watchexec
else
complete -F _watchexec -o bashdefault -o default watchexec
fi
================================================
FILE: completions/elvish
================================================
use builtin;
use str;
set edit:completion:arg-completer[watchexec] = {|@words|
fn spaces {|n|
builtin:repeat $n ' ' | str:join ''
}
fn cand {|text desc|
edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc
}
var command = 'watchexec'
for word $words[1..-1] {
if (str:has-prefix $word '-') {
break
}
set command = $command';'$word
}
var completions = [
&'watchexec'= {
cand --completions 'Generate a shell completions script'
cand --shell 'Use a different shell'
cand -E 'Add env vars to the command'
cand --env 'Add env vars to the command'
cand --wrap-process 'Configure how the process is wrapped'
cand --stop-signal 'Signal to send to stop the command'
cand --stop-timeout 'Time to wait for the command to exit gracefully'
cand --timeout 'Kill the command if it runs longer than this duration'
cand --delay-run 'Sleep before running the command'
cand --workdir 'Set the working directory'
cand --socket 'Provide a socket to the command'
cand -o 'What to do when receiving events while the command is running'
cand --on-busy-update 'What to do when receiving events while the command is running'
cand -s 'Send a signal to the process when it''s still running'
cand --signal 'Send a signal to the process when it''s still running'
cand --map-signal 'Translate signals from the OS to signals to send to the command'
cand -d 'Time to wait for new events before taking action'
cand --debounce 'Time to wait for new events before taking action'
cand --poll 'Poll for filesystem changes'
cand --emit-events-to 'Configure event emission'
cand -w 'Watch a specific file or directory'
cand --watch 'Watch a specific file or directory'
cand -W 'Watch a specific directory, non-recursively'
cand --watch-non-recursive 'Watch a specific directory, non-recursively'
cand -F 'Watch files and directories from a file'
cand --watch-file 'Watch files and directories from a file'
cand -e 'Filename extensions to filter to'
cand --exts 'Filename extensions to filter to'
cand -f 'Filename patterns to filter to'
cand --filter 'Filename patterns to filter to'
cand --filter-file 'Files to load filters from'
cand --project-origin 'Set the project origin'
cand -j 'Filter programs'
cand --filter-prog 'Filter programs'
cand -i 'Filename patterns to filter out'
cand --ignore 'Filename patterns to filter out'
cand --ignore-file 'Files to load ignores from'
cand --fs-events 'Filesystem events to filter to'
cand --log-file 'Write diagnostic logs to a file'
cand -c 'Clear screen before running command'
cand --clear 'Clear screen before running command'
cand -N 'Alert when commands start and end'
cand --notify 'Alert when commands start and end'
cand --color 'When to use terminal colours'
cand --manual 'Show the manual page'
cand --only-emit-events 'Only emit events to stdout, run no commands'
cand -1 'Testing only: exit Watchexec after the first run and return the command''s exit code'
cand -n 'Shorthand for ''--shell=none'''
cand --no-environment 'Deprecated shorthand for ''--emit-events=none'''
cand --no-process-group 'Don''t use a process group'
cand -r 'Restart the process if it''s still running'
cand --restart 'Restart the process if it''s still running'
cand --stdin-quit 'Exit when stdin closes'
cand -I 'Respond to keypresses to quit, restart, or pause'
cand --interactive 'Respond to keypresses to quit, restart, or pause'
cand --exit-on-error 'Exit when the command has an error'
cand -p 'Wait until first change before running command'
cand --postpone 'Wait until first change before running command'
cand --no-vcs-ignore 'Don''t load gitignores'
cand --no-project-ignore 'Don''t load project-local ignores'
cand --no-global-ignore 'Don''t load global ignores'
cand --no-default-ignore 'Don''t use internal default ignores'
cand --no-discover-ignore 'Don''t discover ignore files at all'
cand --ignore-nothing 'Don''t ignore anything at all'
cand --no-meta 'Don''t emit fs events for metadata changes'
cand -v 'Set diagnostic log level'
cand --verbose 'Set diagnostic log level'
cand --print-events 'Print events that trigger actions'
cand --timings 'Print how long the command took to run'
cand -q 'Don''t print starting and stopping messages'
cand --quiet 'Don''t print starting and stopping messages'
cand --bell 'Ring the terminal bell on command completion'
cand -h 'Print help (see more with ''--help'')'
cand --help 'Print help (see more with ''--help'')'
cand -V 'Print version'
cand --version 'Print version'
}
]
$completions[$command]
}
================================================
FILE: completions/fish
================================================
complete -c watchexec -l completions -d 'Generate a shell completions script' -r -f -a "bash\t''
elvish\t''
fish\t''
nu\t''
powershell\t''
zsh\t''"
complete -c watchexec -l shell -d 'Use a different shell' -r
complete -c watchexec -s E -l env -d 'Add env vars to the command' -r
complete -c watchexec -l wrap-process -d 'Configure how the process is wrapped' -r -f -a "group\t''
session\t''
none\t''"
complete -c watchexec -l stop-signal -d 'Signal to send to stop the command' -r
complete -c watchexec -l stop-timeout -d 'Time to wait for the command to exit gracefully' -r
complete -c watchexec -l timeout -d 'Kill the command if it runs longer than this duration' -r
complete -c watchexec -l delay-run -d 'Sleep before running the command' -r
complete -c watchexec -l workdir -d 'Set the working directory' -r -f -a "(__fish_complete_directories)"
complete -c watchexec -l socket -d 'Provide a socket to the command' -r
complete -c watchexec -s o -l on-busy-update -d 'What to do when receiving events while the command is running' -r -f -a "queue\t''
do-nothing\t''
restart\t''
signal\t''"
complete -c watchexec -s s -l signal -d 'Send a signal to the process when it\'s still running' -r
complete -c watchexec -l map-signal -d 'Translate signals from the OS to signals to send to the command' -r
complete -c watchexec -s d -l debounce -d 'Time to wait for new events before taking action' -r
complete -c watchexec -l poll -d 'Poll for filesystem changes' -r
complete -c watchexec -l emit-events-to -d 'Configure event emission' -r -f -a "environment\t''
stdio\t''
file\t''
json-stdio\t''
json-file\t''
none\t''"
complete -c watchexec -s w -l watch -d 'Watch a specific file or directory' -r -F
complete -c watchexec -s W -l watch-non-recursive -d 'Watch a specific directory, non-recursively' -r -F
complete -c watchexec -s F -l watch-file -d 'Watch files and directories from a file' -r -F
complete -c watchexec -s e -l exts -d 'Filename extensions to filter to' -r
complete -c watchexec -s f -l filter -d 'Filename patterns to filter to' -r
complete -c watchexec -l filter-file -d 'Files to load filters from' -r -F
complete -c watchexec -l project-origin -d 'Set the project origin' -r -f -a "(__fish_complete_directories)"
complete -c watchexec -s j -l filter-prog -d 'Filter programs' -r
complete -c watchexec -s i -l ignore -d 'Filename patterns to filter out' -r
complete -c watchexec -l ignore-file -d 'Files to load ignores from' -r -F
complete -c watchexec -l fs-events -d 'Filesystem events to filter to' -r -f -a "access\t''
create\t''
remove\t''
rename\t''
modify\t''
metadata\t''"
complete -c watchexec -l log-file -d 'Write diagnostic logs to a file' -r -F
complete -c watchexec -s c -l clear -d 'Clear screen before running command' -r -f -a "clear\t''
reset\t''"
complete -c watchexec -s N -l notify -d 'Alert when commands start and end' -r -f -a "both\t'Notify on both start and end'
start\t'Notify only when the command starts'
end\t'Notify only when the command ends'"
complete -c watchexec -l color -d 'When to use terminal colours' -r -f -a "auto\t''
always\t''
never\t''"
complete -c watchexec -l manual -d 'Show the manual page'
complete -c watchexec -l only-emit-events -d 'Only emit events to stdout, run no commands'
complete -c watchexec -s 1 -d 'Testing only: exit Watchexec after the first run and return the command\'s exit code'
complete -c watchexec -s n -d 'Shorthand for \'--shell=none\''
complete -c watchexec -l no-environment -d 'Deprecated shorthand for \'--emit-events=none\''
complete -c watchexec -l no-process-group -d 'Don\'t use a process group'
complete -c watchexec -s r -l restart -d 'Restart the process if it\'s still running'
complete -c watchexec -l stdin-quit -d 'Exit when stdin closes'
complete -c watchexec -s I -l interactive -d 'Respond to keypresses to quit, restart, or pause'
complete -c watchexec -l exit-on-error -d 'Exit when the command has an error'
complete -c watchexec -s p -l postpone -d 'Wait until first change before running command'
complete -c watchexec -l no-vcs-ignore -d 'Don\'t load gitignores'
complete -c watchexec -l no-project-ignore -d 'Don\'t load project-local ignores'
complete -c watchexec -l no-global-ignore -d 'Don\'t load global ignores'
complete -c watchexec -l no-default-ignore -d 'Don\'t use internal default ignores'
complete -c watchexec -l no-discover-ignore -d 'Don\'t discover ignore files at all'
complete -c watchexec -l ignore-nothing -d 'Don\'t ignore anything at all'
complete -c watchexec -l no-meta -d 'Don\'t emit fs events for metadata changes'
complete -c watchexec -s v -l verbose -d 'Set diagnostic log level'
complete -c watchexec -l print-events -d 'Print events that trigger actions'
complete -c watchexec -l timings -d 'Print how long the command took to run'
complete -c watchexec -s q -l quiet -d 'Don\'t print starting and stopping messages'
complete -c watchexec -l bell -d 'Ring the terminal bell on command completion'
complete -c watchexec -s h -l help -d 'Print help (see more with \'--help\')'
complete -c watchexec -s V -l version -d 'Print version'
================================================
FILE: completions/nu
================================================
module completions {
def "nu-complete watchexec completions" [] {
[ "bash" "elvish" "fish" "nu" "powershell" "zsh" ]
}
def "nu-complete watchexec wrap_process" [] {
[ "group" "session" "none" ]
}
def "nu-complete watchexec on_busy_update" [] {
[ "queue" "do-nothing" "restart" "signal" ]
}
def "nu-complete watchexec emit_events_to" [] {
[ "environment" "stdio" "file" "json-stdio" "json-file" "none" ]
}
def "nu-complete watchexec filter_fs_events" [] {
[ "access" "create" "remove" "rename" "modify" "metadata" ]
}
def "nu-complete watchexec screen_clear" [] {
[ "clear" "reset" ]
}
def "nu-complete watchexec notify" [] {
[ "both" "start" "end" ]
}
def "nu-complete watchexec color" [] {
[ "auto" "always" "never" ]
}
# Execute commands when watched files change
export extern watchexec [
--manual # Show the manual page
--completions: string@"nu-complete watchexec completions" # Generate a shell completions script
--only-emit-events # Only emit events to stdout, run no commands
-1 # Testing only: exit Watchexec after the first run and return the command's exit code
--shell: string # Use a different shell
-n # Shorthand for '--shell=none'
--no-environment # Deprecated shorthand for '--emit-events=none'
--env(-E): string # Add env vars to the command
--no-process-group # Don't use a process group
--wrap-process: string@"nu-complete watchexec wrap_process" # Configure how the process is wrapped
--stop-signal: string # Signal to send to stop the command
--stop-timeout: string # Time to wait for the command to exit gracefully
--timeout: string # Kill the command if it runs longer than this duration
--delay-run: string # Sleep before running the command
--workdir: path # Set the working directory
--socket: string # Provide a socket to the command
--on-busy-update(-o): string@"nu-complete watchexec on_busy_update" # What to do when receiving events while the command is running
--restart(-r) # Restart the process if it's still running
--signal(-s): string # Send a signal to the process when it's still running
--map-signal: string # Translate signals from the OS to signals to send to the command
--debounce(-d): string # Time to wait for new events before taking action
--stdin-quit # Exit when stdin closes
--interactive(-I) # Respond to keypresses to quit, restart, or pause
--exit-on-error # Exit when the command has an error
--postpone(-p) # Wait until first change before running command
--poll: string # Poll for filesystem changes
--emit-events-to: string@"nu-complete watchexec emit_events_to" # Configure event emission
--watch(-w): path # Watch a specific file or directory
--watch-non-recursive(-W): path # Watch a specific directory, non-recursively
--watch-file(-F): path # Watch files and directories from a file
--no-vcs-ignore # Don't load gitignores
--no-project-ignore # Don't load project-local ignores
--no-global-ignore # Don't load global ignores
--no-default-ignore # Don't use internal default ignores
--no-discover-ignore # Don't discover ignore files at all
--ignore-nothing # Don't ignore anything at all
--exts(-e): string # Filename extensions to filter to
--filter(-f): string # Filename patterns to filter to
--filter-file: path # Files to load filters from
--project-origin: path # Set the project origin
--filter-prog(-j): string # Filter programs
--ignore(-i): string # Filename patterns to filter out
--ignore-file: path # Files to load ignores from
--fs-events: string@"nu-complete watchexec filter_fs_events" # Filesystem events to filter to
--no-meta # Don't emit fs events for metadata changes
--verbose(-v) # Set diagnostic log level
--log-file: path # Write diagnostic logs to a file
--print-events # Print events that trigger actions
--clear(-c): string@"nu-complete watchexec screen_clear" # Clear screen before running command
--notify(-N): string@"nu-complete watchexec notify" # Alert when commands start and end
--color: string@"nu-complete watchexec color" # When to use terminal colours
--timings # Print how long the command took to run
--quiet(-q) # Don't print starting and stopping messages
--bell # Ring the terminal bell on command completion
--help(-h) # Print help (see more with '--help')
--version(-V) # Print version
...program: string # Command (program and arguments) to run on changes
]
}
export use completions *
================================================
FILE: completions/powershell
================================================
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
Register-ArgumentCompleter -Native -CommandName 'watchexec' -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
$commandElements = $commandAst.CommandElements
$command = @(
'watchexec'
for ($i = 1; $i -lt $commandElements.Count; $i++) {
$element = $commandElements[$i]
if ($element -isnot [StringConstantExpressionAst] -or
$element.StringConstantType -ne [StringConstantType]::BareWord -or
$element.Value.StartsWith('-') -or
$element.Value -eq $wordToComplete) {
break
}
$element.Value
}) -join ';'
$completions = @(switch ($command) {
'watchexec' {
[CompletionResult]::new('--completions', '--completions', [CompletionResultType]::ParameterName, 'Generate a shell completions script')
[CompletionResult]::new('--shell', '--shell', [CompletionResultType]::ParameterName, 'Use a different shell')
[CompletionResult]::new('-E', '-E ', [CompletionResultType]::ParameterName, 'Add env vars to the command')
[CompletionResult]::new('--env', '--env', [CompletionResultType]::ParameterName, 'Add env vars to the command')
[CompletionResult]::new('--wrap-process', '--wrap-process', [CompletionResultType]::ParameterName, 'Configure how the process is wrapped')
[CompletionResult]::new('--stop-signal', '--stop-signal', [CompletionResultType]::ParameterName, 'Signal to send to stop the command')
[CompletionResult]::new('--stop-timeout', '--stop-timeout', [CompletionResultType]::ParameterName, 'Time to wait for the command to exit gracefully')
[CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'Kill the command if it runs longer than this duration')
[CompletionResult]::new('--delay-run', '--delay-run', [CompletionResultType]::ParameterName, 'Sleep before running the command')
[CompletionResult]::new('--workdir', '--workdir', [CompletionResultType]::ParameterName, 'Set the working directory')
[CompletionResult]::new('--socket', '--socket', [CompletionResultType]::ParameterName, 'Provide a socket to the command')
[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'What to do when receiving events while the command is running')
[CompletionResult]::new('--on-busy-update', '--on-busy-update', [CompletionResultType]::ParameterName, 'What to do when receiving events while the command is running')
[CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Send a signal to the process when it''s still running')
[CompletionResult]::new('--signal', '--signal', [CompletionResultType]::ParameterName, 'Send a signal to the process when it''s still running')
[CompletionResult]::new('--map-signal', '--map-signal', [CompletionResultType]::ParameterName, 'Translate signals from the OS to signals to send to the command')
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Time to wait for new events before taking action')
[CompletionResult]::new('--debounce', '--debounce', [CompletionResultType]::ParameterName, 'Time to wait for new events before taking action')
[CompletionResult]::new('--poll', '--poll', [CompletionResultType]::ParameterName, 'Poll for filesystem changes')
[CompletionResult]::new('--emit-events-to', '--emit-events-to', [CompletionResultType]::ParameterName, 'Configure event emission')
[CompletionResult]::new('-w', '-w', [CompletionResultType]::ParameterName, 'Watch a specific file or directory')
[CompletionResult]::new('--watch', '--watch', [CompletionResultType]::ParameterName, 'Watch a specific file or directory')
[CompletionResult]::new('-W', '-W ', [CompletionResultType]::ParameterName, 'Watch a specific directory, non-recursively')
[CompletionResult]::new('--watch-non-recursive', '--watch-non-recursive', [CompletionResultType]::ParameterName, 'Watch a specific directory, non-recursively')
[CompletionResult]::new('-F', '-F ', [CompletionResultType]::ParameterName, 'Watch files and directories from a file')
[CompletionResult]::new('--watch-file', '--watch-file', [CompletionResultType]::ParameterName, 'Watch files and directories from a file')
[CompletionResult]::new('-e', '-e', [CompletionResultType]::ParameterName, 'Filename extensions to filter to')
[CompletionResult]::new('--exts', '--exts', [CompletionResultType]::ParameterName, 'Filename extensions to filter to')
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Filename patterns to filter to')
[CompletionResult]::new('--filter', '--filter', [CompletionResultType]::ParameterName, 'Filename patterns to filter to')
[CompletionResult]::new('--filter-file', '--filter-file', [CompletionResultType]::ParameterName, 'Files to load filters from')
[CompletionResult]::new('--project-origin', '--project-origin', [CompletionResultType]::ParameterName, 'Set the project origin')
[CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, 'Filter programs')
[CompletionResult]::new('--filter-prog', '--filter-prog', [CompletionResultType]::ParameterName, 'Filter programs')
[CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'Filename patterns to filter out')
[CompletionResult]::new('--ignore', '--ignore', [CompletionResultType]::ParameterName, 'Filename patterns to filter out')
[CompletionResult]::new('--ignore-file', '--ignore-file', [CompletionResultType]::ParameterName, 'Files to load ignores from')
[CompletionResult]::new('--fs-events', '--fs-events', [CompletionResultType]::ParameterName, 'Filesystem events to filter to')
[CompletionResult]::new('--log-file', '--log-file', [CompletionResultType]::ParameterName, 'Write diagnostic logs to a file')
[CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'Clear screen before running command')
[CompletionResult]::new('--clear', '--clear', [CompletionResultType]::ParameterName, 'Clear screen before running command')
[CompletionResult]::new('-N', '-N ', [CompletionResultType]::ParameterName, 'Alert when commands start and end')
[CompletionResult]::new('--notify', '--notify', [CompletionResultType]::ParameterName, 'Alert when commands start and end')
[CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'When to use terminal colours')
[CompletionResult]::new('--manual', '--manual', [CompletionResultType]::ParameterName, 'Show the manual page')
[CompletionResult]::new('--only-emit-events', '--only-emit-events', [CompletionResultType]::ParameterName, 'Only emit events to stdout, run no commands')
[CompletionResult]::new('-1', '-1', [CompletionResultType]::ParameterName, 'Testing only: exit Watchexec after the first run and return the command''s exit code')
[CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'Shorthand for ''--shell=none''')
[CompletionResult]::new('--no-environment', '--no-environment', [CompletionResultType]::ParameterName, 'Deprecated shorthand for ''--emit-events=none''')
[CompletionResult]::new('--no-process-group', '--no-process-group', [CompletionResultType]::ParameterName, 'Don''t use a process group')
[CompletionResult]::new('-r', '-r', [CompletionResultType]::ParameterName, 'Restart the process if it''s still running')
[CompletionResult]::new('--restart', '--restart', [CompletionResultType]::ParameterName, 'Restart the process if it''s still running')
[CompletionResult]::new('--stdin-quit', '--stdin-quit', [CompletionResultType]::ParameterName, 'Exit when stdin closes')
[CompletionResult]::new('-I', '-I ', [CompletionResultType]::ParameterName, 'Respond to keypresses to quit, restart, or pause')
[CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, 'Respond to keypresses to quit, restart, or pause')
[CompletionResult]::new('--exit-on-error', '--exit-on-error', [CompletionResultType]::ParameterName, 'Exit when the command has an error')
[CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'Wait until first change before running command')
[CompletionResult]::new('--postpone', '--postpone', [CompletionResultType]::ParameterName, 'Wait until first change before running command')
[CompletionResult]::new('--no-vcs-ignore', '--no-vcs-ignore', [CompletionResultType]::ParameterName, 'Don''t load gitignores')
[CompletionResult]::new('--no-project-ignore', '--no-project-ignore', [CompletionResultType]::ParameterName, 'Don''t load project-local ignores')
[CompletionResult]::new('--no-global-ignore', '--no-global-ignore', [CompletionResultType]::ParameterName, 'Don''t load global ignores')
[CompletionResult]::new('--no-default-ignore', '--no-default-ignore', [CompletionResultType]::ParameterName, 'Don''t use internal default ignores')
[CompletionResult]::new('--no-discover-ignore', '--no-discover-ignore', [CompletionResultType]::ParameterName, 'Don''t discover ignore files at all')
[CompletionResult]::new('--ignore-nothing', '--ignore-nothing', [CompletionResultType]::ParameterName, 'Don''t ignore anything at all')
[CompletionResult]::new('--no-meta', '--no-meta', [CompletionResultType]::ParameterName, 'Don''t emit fs events for metadata changes')
[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Set diagnostic log level')
[CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Set diagnostic log level')
[CompletionResult]::new('--print-events', '--print-events', [CompletionResultType]::ParameterName, 'Print events that trigger actions')
[CompletionResult]::new('--timings', '--timings', [CompletionResultType]::ParameterName, 'Print how long the command took to run')
[CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Don''t print starting and stopping messages')
[CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Don''t print starting and stopping messages')
[CompletionResult]::new('--bell', '--bell', [CompletionResultType]::ParameterName, 'Ring the terminal bell on command completion')
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help (see more with ''--help'')')
[CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version')
break
}
})
$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
Sort-Object -Property ListItemText
}
================================================
FILE: completions/zsh
================================================
#compdef watchexec
autoload -U is-at-least
_watchexec() {
typeset -A opt_args
typeset -a _arguments_options
local ret=1
if is-at-least 5.2; then
_arguments_options=(-s -S -C)
else
_arguments_options=(-s -C)
fi
local context curcontext="$curcontext" state line
_arguments "${_arguments_options[@]}" : \
'(--manual --only-emit-events)--completions=[Generate a shell completions script]:SHELL:(bash elvish fish nu powershell zsh)' \
'--shell=[Use a different shell]:SHELL:_default' \
'*-E+[Add env vars to the command]:KEY=VALUE:_default' \
'*--env=[Add env vars to the command]:KEY=VALUE:_default' \
'--wrap-process=[Configure how the process is wrapped]:MODE:(group session none)' \
'--stop-signal=[Signal to send to stop the command]:SIGNAL:_default' \
'--stop-timeout=[Time to wait for the command to exit gracefully]:TIMEOUT:_default' \
'--timeout=[Kill the command if it runs longer than this duration]:TIMEOUT:_default' \
'--delay-run=[Sleep before running the command]:DURATION:_default' \
'--workdir=[Set the working directory]:DIRECTORY:_files -/' \
'*--socket=[Provide a socket to the command]:PORT:_default' \
'-o+[What to do when receiving events while the command is running]:MODE:(queue do-nothing restart signal)' \
'--on-busy-update=[What to do when receiving events while the command is running]:MODE:(queue do-nothing restart signal)' \
'(-r --restart)-s+[Send a signal to the process when it'\''s still running]:SIGNAL:_default' \
'(-r --restart)--signal=[Send a signal to the process when it'\''s still running]:SIGNAL:_default' \
'*--map-signal=[Translate signals from the OS to signals to send to the command]:SIGNAL:SIGNAL:_default' \
'-d+[Time to wait for new events before taking action]:TIMEOUT:_default' \
'--debounce=[Time to wait for new events before taking action]:TIMEOUT:_default' \
'--poll=[Poll for filesystem changes]::INTERVAL:_default' \
'--emit-events-to=[Configure event emission]:MODE:(environment stdio file json-stdio json-file none)' \
'*-w+[Watch a specific file or directory]:PATH:_files' \
'*--watch=[Watch a specific file or directory]:PATH:_files' \
'*-W+[Watch a specific directory, non-recursively]:PATH:_files' \
'*--watch-non-recursive=[Watch a specific directory, non-recursively]:PATH:_files' \
'-F+[Watch files and directories from a file]:PATH:_files' \
'--watch-file=[Watch files and directories from a file]:PATH:_files' \
'*-e+[Filename extensions to filter to]:EXTENSIONS:_default' \
'*--exts=[Filename extensions to filter to]:EXTENSIONS:_default' \
'*-f+[Filename patterns to filter to]:PATTERN:_default' \
'*--filter=[Filename patterns to filter to]:PATTERN:_default' \
'*--filter-file=[Files to load filters from]:PATH:_files' \
'--project-origin=[Set the project origin]:DIRECTORY:_files -/' \
'*-j+[Filter programs]:EXPRESSION:_default' \
'*--filter-prog=[Filter programs]:EXPRESSION:_default' \
'*-i+[Filename patterns to filter out]:PATTERN:_default' \
'*--ignore=[Filename patterns to filter out]:PATTERN:_default' \
'*--ignore-file=[Files to load ignores from]:PATH:_files' \
'*--fs-events=[Filesystem events to filter to]:EVENTS:(access create remove rename modify metadata)' \
'--log-file=[Write diagnostic logs to a file]::PATH:_files' \
'-c+[Clear screen before running command]::MODE:(clear reset)' \
'--clear=[Clear screen before running command]::MODE:(clear reset)' \
'-N+[Alert when commands start and end]::WHEN:((both\:"Notify on both start and end"
start\:"Notify only when the command starts"
end\:"Notify only when the command ends"))' \
'--notify=[Alert when commands start and end]::WHEN:((both\:"Notify on both start and end"
start\:"Notify only when the command starts"
end\:"Notify only when the command ends"))' \
'--color=[When to use terminal colours]:MODE:(auto always never)' \
'(--completions --only-emit-events)--manual[Show the manual page]' \
'(--completions --manual)--only-emit-events[Only emit events to stdout, run no commands]' \
'-1[Testing only\: exit Watchexec after the first run and return the command'\''s exit code]' \
'-n[Shorthand for '\''--shell=none'\'']' \
'--no-environment[Deprecated shorthand for '\''--emit-events=none'\'']' \
'--no-process-group[Don'\''t use a process group]' \
'(-o --on-busy-update)-r[Restart the process if it'\''s still running]' \
'(-o --on-busy-update)--restart[Restart the process if it'\''s still running]' \
'--stdin-quit[Exit when stdin closes]' \
'-I[Respond to keypresses to quit, restart, or pause]' \
'--interactive[Respond to keypresses to quit, restart, or pause]' \
'--exit-on-error[Exit when the command has an error]' \
'-p[Wait until first change before running command]' \
'--postpone[Wait until first change before running command]' \
'--no-vcs-ignore[Don'\''t load gitignores]' \
'--no-project-ignore[Don'\''t load project-local ignores]' \
'--no-global-ignore[Don'\''t load global ignores]' \
'--no-default-ignore[Don'\''t use internal default ignores]' \
'--no-discover-ignore[Don'\''t discover ignore files at all]' \
'--ignore-nothing[Don'\''t ignore anything at all]' \
'(--fs-events)--no-meta[Don'\''t emit fs events for metadata changes]' \
'*-v[Set diagnostic log level]' \
'*--verbose[Set diagnostic log level]' \
'--print-events[Print events that trigger actions]' \
'--timings[Print how long the command took to run]' \
'-q[Don'\''t print starting and stopping messages]' \
'--quiet[Don'\''t print starting and stopping messages]' \
'--bell[Ring the terminal bell on command completion]' \
'-h[Print help (see more with '\''--help'\'')]' \
'--help[Print help (see more with '\''--help'\'')]' \
'-V[Print version]' \
'--version[Print version]' \
'*::program -- Command (program and arguments) to run on changes:_cmdstring' \
&& ret=0
}
(( $+functions[_watchexec_commands] )) ||
_watchexec_commands() {
local commands; commands=()
_describe -t commands 'watchexec commands' commands "$@"
}
if [ "$funcstack[1]" = "_watchexec" ]; then
_watchexec "$@"
else
compdef _watchexec watchexec
fi
================================================
FILE: crates/bosion/CHANGELOG.md
================================================
# Changelog
## Next (YYYY-MM-DD)
## v2.0.0 (2026-01-20)
- Remove `GIT_COMMIT_DESCRIPTION`. In practice this had zero usage, and dropping it means we can stop depending on gix.
- Deps: remove gix. This drops dependencies from 327 crates to just 6.
## v1.1.3 (2025-05-15)
- Deps: gix 0.72
## v1.1.2 (2025-02-09)
- Deps: gix 0.70
## v1.1.1 (2024-10-14)
- Deps: gix 0.66
## v1.1.0 (2024-05-16)
- Add `git-describe` support (#832, by @lu-zero)
## v1.0.3 (2024-04-20)
- Deps: gix 0.62
## v1.0.2 (2023-11-26)
- Deps: upgrade to gix 0.55
## v1.0.1 (2023-07-02)
- Deps: upgrade to gix 0.44
## v1.0.0 (2023-03-05)
- Initial release.
================================================
FILE: crates/bosion/Cargo.toml
================================================
[package]
name = "bosion"
version = "2.0.0"
authors = ["Félix Saparelli <felix@passcod.name>"]
license = "Apache-2.0 OR MIT"
description = "Gather build information for verbose versions flags"
keywords = ["version", "git", "verbose", "long"]
documentation = "https://docs.rs/bosion"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
rust-version = "1.64.0"
edition = "2021"
[dependencies]
flate2 = { version = "1.0.35", optional = true }
[dependencies.time]
version = "0.3.30"
features = ["macros", "formatting"]
[features]
default = ["git", "reproducible", "std"]
### Read from git repo, provide GIT_* vars
git = ["dep:flate2"]
### Read from SOURCE_DATE_EPOCH when available
reproducible = []
### Provide a long_version_with() function to add extra info
###
### Specifically this is std support for the _using_ crate, not for the bosion crate itself. It's
### assumed that the bosion crate is always std, as it runs in build.rs.
std = []
[lints.clippy]
nursery = "warn"
pedantic = "warn"
module_name_repetitions = "allow"
similar_names = "allow"
cognitive_complexity = "allow"
too_many_lines = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
default_trait_access = "allow"
enum_glob_use = "allow"
option_if_let_else = "allow"
blocks_in_conditions = "allow"
needless_doctest_main = "allow"
================================================
FILE: crates/bosion/README.md
================================================
# Bosion
_Gather build information for verbose versions flags._
- **[API documentation][docs]**.
- Licensed under [Apache 2.0][license] or [MIT](https://passcod.mit-license.org).
- Status: maintained.
[docs]: https://docs.rs/bosion
[license]: ../../LICENSE
## Quick start
In your `Cargo.toml`:
```toml
[build-dependencies]
bosion = "2.0.0"
```
In your `build.rs`:
```rust ,no_run
fn main() {
bosion::gather();
}
```
In your `src/main.rs`:
```rust ,ignore
include!(env!("BOSION_PATH"));
fn main() {
// default output, like rustc -Vv
println!("{}", Bosion::LONG_VERSION);
// with additional fields
println!("{}", Bosion::long_version_with(&[
("custom data", "value"),
("LLVM version", "15.0.6"),
]));
// enabled features like +feature +an-other
println!("{}", Bosion::CRATE_FEATURE_STRING);
// the raw data
println!("{}", Bosion::GIT_COMMIT_HASH);
println!("{}", Bosion::GIT_COMMIT_SHORTHASH);
println!("{}", Bosion::GIT_COMMIT_DATE);
println!("{}", Bosion::GIT_COMMIT_DATETIME);
println!("{}", Bosion::CRATE_VERSION);
println!("{:?}", Bosion::CRATE_FEATURES);
println!("{}", Bosion::BUILD_DATE);
println!("{}", Bosion::BUILD_DATETIME);
}
```
## Advanced usage
Generating a struct with public visibility:
```rust ,no_run
// build.rs
bosion::gather_pub();
```
Customising the output file and struct names:
```rust ,no_run
// build.rs
bosion::gather_to("buildinfo.rs", "Build", /* public? */ false);
```
Outputting build-time environment variables instead of source:
```rust ,ignore
// build.rs
bosion::gather_to_env();
// src/main.rs
fn main() {
println!("{}", env!("BOSION_GIT_COMMIT_HASH"));
println!("{}", env!("BOSION_GIT_COMMIT_SHORTHASH"));
println!("{}", env!("BOSION_GIT_COMMIT_DATE"));
println!("{}", env!("BOSION_GIT_COMMIT_DATETIME"));
println!("{}", env!("BOSION_BUILD_DATE"));
println!("{}", env!("BOSION_BUILD_DATETIME"));
println!("{}", env!("BOSION_CRATE_VERSION"));
println!("{}", env!("BOSION_CRATE_FEATURES")); // comma-separated
}
```
Custom env prefix:
```rust ,no_run
// build.rs
bosion::gather_to_env_with_prefix("MYAPP_");
```
## Features
- `reproducible`: reads [`SOURCE_DATE_EPOCH`](https://reproducible-builds.org/docs/source-date-epoch/) (default).
- `git`: enables gathering git information (default).
- `std`: enables the `long_version_with` method (default).
Specifically, this is about the downstream crate's std support, not Bosion's, which always requires std.
## Why not...?
- [bugreport](https://github.com/sharkdp/bugreport): runtime library, for bug information.
- [git-testament](https://github.com/kinnison/git-testament): uses the `git` CLI instead of gitoxide.
- [human-panic](https://github.com/rust-cli/human-panic): runtime library, for panics.
- [shadow-rs](https://github.com/baoyachi/shadow-rs): uses libgit2 instead of gitoxide, doesn't rebuild on git changes.
- [vergen](https://github.com/rustyhorde/vergen): uses the `git` CLI instead of gitoxide.
Bosion also requires no dependencies outside of build.rs, and was specifically made for crates
installed in a variety of ways, like with `cargo install`, from pre-built binary, from source with
git, or from source without git (like a tarball), on a variety of platforms. Its default output with
[clap](https://clap.rs) is almost exactly like `rustc -Vv`.
## Examples
The [examples](./examples) directory contains a practical and runnable [clap-based example](./examples/clap/), as well
as several other crates which are actually used for integration testing.
Here is the output for the Watchexec CLI:
```plain
watchexec 1.21.1 (5026793 2023-03-05)
commit-hash: 5026793a12ff895edf2dafb92111e7bd1767650e
commit-date: 2023-03-05
build-date: 2023-03-05
release: 1.21.1
features:
```
For comparison, here's `rustc -Vv`:
```plain
rustc 1.67.1 (d5a82bbd2 2023-02-07)
binary: rustc
commit-hash: d5a82bbd26e1ad8b7401f6a718a9c57c96905483
commit-date: 2023-02-07
host: x86_64-unknown-linux-gnu
release: 1.67.1
LLVM version: 15.0.6
```
================================================
FILE: crates/bosion/examples/clap/Cargo.toml
================================================
[package]
name = "bosion-example-clap"
version = "0.1.0"
publish = false
edition = "2021"
[workspace]
[features]
default = ["foo"]
foo = []
[build-dependencies.bosion]
version = "*"
path = "../.."
[dependencies.clap]
version = "4.1.8"
features = ["cargo", "derive"]
================================================
FILE: crates/bosion/examples/clap/build.rs
================================================
fn main() {
bosion::gather();
}
================================================
FILE: crates/bosion/examples/clap/src/main.rs
================================================
use clap::Parser;
include!(env!("BOSION_PATH"));
#[derive(Parser)]
#[clap(version, long_version = Bosion::LONG_VERSION)]
struct Args {
#[clap(long)]
extras: bool,
#[clap(long)]
features: bool,
#[clap(long)]
dates: bool,
#[clap(long)]
hashes: bool,
}
fn main() {
let args = Args::parse();
if args.extras {
println!(
"{}",
Bosion::long_version_with(&[("extra", "field"), ("custom", "1.2.3"),])
);
} else if args.features {
println!("Features: {}", Bosion::CRATE_FEATURE_STRING);
} else if args.dates {
println!("commit date: {}", Bosion::GIT_COMMIT_DATE);
println!("commit datetime: {}", Bosion::GIT_COMMIT_DATETIME);
println!("build date: {}", Bosion::BUILD_DATE);
println!("build datetime: {}", Bosion::BUILD_DATETIME);
} else if args.hashes {
println!("commit hash: {}", Bosion::GIT_COMMIT_HASH);
println!("commit shorthash: {}", Bosion::GIT_COMMIT_SHORTHASH);
} else {
println!("{}", Bosion::LONG_VERSION);
}
}
================================================
FILE: crates/bosion/examples/default/Cargo.toml
================================================
[package]
name = "bosion-test-default"
version = "0.1.0"
publish = false
edition = "2021"
[workspace]
[features]
default = ["foo"]
foo = []
[build-dependencies.bosion]
version = "*"
path = "../.."
[dependencies]
leon = { version = "3.0.2", default-features = false }
snapbox = "0.5.9"
time = { version = "0.3.30", features = ["formatting", "macros"] }
================================================
FILE: crates/bosion/examples/default/build.rs
================================================
fn main() {
bosion::gather();
}
================================================
FILE: crates/bosion/examples/default/src/common.rs
================================================
#[cfg(test)]
pub(crate) fn git_commit_info(format: &str) -> String {
let output = std::process::Command::new("git")
.arg("show")
.arg("--no-notes")
.arg("--no-patch")
.arg(format!("--pretty=format:{format}"))
.output()
.expect("git");
String::from_utf8(output.stdout)
.expect("git")
.trim()
.to_string()
}
#[macro_export]
macro_rules! test_snapshot {
($name:ident, $actual:expr) => {
#[cfg(test)]
#[test]
fn $name() {
use std::str::FromStr;
let gittime = ::time::OffsetDateTime::from_unix_timestamp(
i64::from_str(&crate::common::git_commit_info("%ct")).expect("git i64"),
)
.expect("git time");
::snapbox::Assert::new().matches(
::leon::Template::parse(
std::fs::read_to_string(format!("../snapshots/{}.txt", stringify!($name)))
.expect("read file")
.trim(),
)
.expect("leon parse")
.render(&[
(
"today date".to_string(),
::time::OffsetDateTime::now_utc()
.format(::time::macros::format_description!("[year]-[month]-[day]"))
.unwrap(),
),
("git hash".to_string(), crate::common::git_commit_info("%H")),
(
"git shorthash".to_string(),
crate::common::git_commit_info("%H").chars().take(8).collect(),
),
(
"git date".to_string(),
gittime
.format(::time::macros::format_description!("[year]-[month]-[day]"))
.expect("git date format"),
),
(
"git datetime".to_string(),
gittime
.format(::time::macros::format_description!(
"[year]-[month]-[day] [hour]:[minute]:[second]"
))
.expect("git time format"),
),
])
.expect("leon render"),
$actual,
);
}
};
}
================================================
FILE: crates/bosion/examples/default/src/main.rs
================================================
include!(env!("BOSION_PATH"));
mod common;
fn main() {}
test_snapshot!(crate_version, Bosion::CRATE_VERSION);
test_snapshot!(crate_features, format!("{:#?}", Bosion::CRATE_FEATURES));
test_snapshot!(build_date, Bosion::BUILD_DATE);
test_snapshot!(build_datetime, Bosion::BUILD_DATETIME);
test_snapshot!(git_commit_hash, Bosion::GIT_COMMIT_HASH);
test_snapshot!(git_commit_shorthash, Bosion::GIT_COMMIT_SHORTHASH);
test_snapshot!(git_commit_date, Bosion::GIT_COMMIT_DATE);
test_snapshot!(git_commit_datetime, Bosion::GIT_COMMIT_DATETIME);
test_snapshot!(default_long_version, Bosion::LONG_VERSION);
test_snapshot!(
default_long_version_with,
Bosion::long_version_with(&[("extra", "field"), ("custom", "1.2.3")])
);
================================================
FILE: crates/bosion/examples/no-git/Cargo.toml
================================================
[package]
name = "bosion-test-no-git"
version = "0.1.0"
publish = false
edition = "2021"
[workspace]
[features]
default = ["foo"]
foo = []
[build-dependencies.bosion]
version = "*"
path = "../.."
default-features = false
features = ["std"]
[dependencies]
leon = { version = "3.0.2", default-features = false }
snapbox = "0.5.9"
time = { version = "0.3.30", features = ["formatting", "macros"] }
================================================
FILE: crates/bosion/examples/no-git/build.rs
================================================
fn main() {
bosion::gather();
}
================================================
FILE: crates/bosion/examples/no-git/src/main.rs
================================================
include!(env!("BOSION_PATH"));
#[path = "../../default/src/common.rs"]
mod common;
fn main() {}
test_snapshot!(crate_version, Bosion::CRATE_VERSION);
test_snapshot!(crate_features, format!("{:#?}", Bosion::CRATE_FEATURES));
test_snapshot!(build_date, Bosion::BUILD_DATE);
test_snapshot!(build_datetime, Bosion::BUILD_DATETIME);
test_snapshot!(no_git_long_version, Bosion::LONG_VERSION);
test_snapshot!(
no_git_long_version_with,
Bosion::long_version_with(&[("extra", "field"), ("custom", "1.2.3")])
);
================================================
FILE: crates/bosion/examples/no-std/Cargo.toml
================================================
[package]
name = "bosion-test-no-std"
version = "0.1.0"
publish = false
edition = "2021"
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
[workspace]
[features]
default = ["foo"]
foo = []
[build-dependencies.bosion]
version = "*"
path = "../.."
default-features = false
[dependencies]
leon = { version = "3.0.2", default-features = false }
snapbox = "0.5.9"
time = { version = "0.3.30", features = ["formatting", "macros"] }
================================================
FILE: crates/bosion/examples/no-std/build.rs
================================================
fn main() {
bosion::gather();
}
================================================
FILE: crates/bosion/examples/no-std/src/main.rs
================================================
#![cfg_attr(not(test), no_main)]
#![cfg_attr(not(test), no_std)]
#[cfg(not(test))]
use core::panic::PanicInfo;
#[cfg(not(test))]
#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
loop {}
}
include!(env!("BOSION_PATH"));
#[cfg(test)]
#[path = "../../default/src/common.rs"]
mod common;
#[cfg(test)]
mod test {
use super::*;
test_snapshot!(crate_version, Bosion::CRATE_VERSION);
test_snapshot!(crate_features, format!("{:#?}", Bosion::CRATE_FEATURES));
test_snapshot!(build_date, Bosion::BUILD_DATE);
test_snapshot!(build_datetime, Bosion::BUILD_DATETIME);
test_snapshot!(no_git_long_version, Bosion::LONG_VERSION);
}
================================================
FILE: crates/bosion/examples/snapshots/build_date.txt
================================================
{today date}
================================================
FILE: crates/bosion/examples/snapshots/build_datetime.txt
================================================
{today date} [..]
================================================
FILE: crates/bosion/examples/snapshots/crate_features.txt
================================================
[
"default",
"foo",
]
================================================
FILE: crates/bosion/examples/snapshots/crate_version.txt
================================================
0.1.0
================================================
FILE: crates/bosion/examples/snapshots/default_long_version.txt
================================================
0.1.0 ({git shorthash} {git date}) +foo
commit-hash: {git hash}
commit-date: {git date}
build-date: {today date}
release: 0.1.0
features: default,foo
================================================
FILE: crates/bosion/examples/snapshots/default_long_version_with.txt
================================================
0.1.0 ({git shorthash} {git date}) +foo
commit-hash: {git hash}
commit-date: {git date}
build-date: {today date}
release: 0.1.0
features: default,foo
extra: field
custom: 1.2.3
================================================
FILE: crates/bosion/examples/snapshots/git_commit_date.txt
================================================
{git date}
================================================
FILE: crates/bosion/examples/snapshots/git_commit_datetime.txt
================================================
{git datetime}
================================================
FILE: crates/bosion/examples/snapshots/git_commit_hash.txt
================================================
{git hash}
================================================
FILE: crates/bosion/examples/snapshots/git_commit_shorthash.txt
================================================
{git shorthash}
================================================
FILE: crates/bosion/examples/snapshots/no_git_long_version.txt
================================================
0.1.0 ({today date}) +foo
build-date: {today date}
release: 0.1.0
features: default,foo
================================================
FILE: crates/bosion/examples/snapshots/no_git_long_version_with.txt
================================================
0.1.0 ({today date}) +foo
build-date: {today date}
release: 0.1.0
features: default,foo
extra: field
custom: 1.2.3
================================================
FILE: crates/bosion/release.toml
================================================
pre-release-commit-message = "release: bosion v{{version}}"
tag-prefix = "bosion-"
tag-message = "bosion {{version}}"
[[pre-release-replacements]]
file = "CHANGELOG.md"
search = "^## Next.*$"
replace = "## Next (YYYY-MM-DD)\n\n## v{{version}} ({{date}})"
prerelease = true
max = 1
[[pre-release-replacements]]
file = "README.md"
search = "^bosion = \".*\"$"
replace = "bosion = \"{{version}}\""
prerelease = true
max = 1
================================================
FILE: crates/bosion/run-tests.sh
================================================
#!/bin/bash
set -euo pipefail
for test in examples/*; do
echo "Testing $test"
pushd $test
if ! test -f Cargo.toml; then
popd
continue
fi
cargo check
cargo test
popd
done
================================================
FILE: crates/bosion/src/info.rs
================================================
use std::{
env::var,
path::{Path, PathBuf},
};
use time::{format_description::FormatItem, macros::format_description, OffsetDateTime};
/// Gathered build-time information
///
/// This struct contains all the information gathered by `bosion`. It is not meant to be used
/// directly under normal circumstances, but is public for documentation purposes and if you wish
/// to build your own frontend for whatever reason. In that case, note that no effort has been made
/// to make this usable outside of the build.rs environment.
///
/// The `git` field is only available when the `git` feature is enabled, and if there is a git
/// repository to read from. The repository is discovered by walking up the directory tree until one
/// is found, which means workspaces or more complex monorepos are automatically supported. If there
/// are any errors reading the repository, the `git` field will be `None` and a rustc warning will
/// be printed.
#[derive(Debug, Clone)]
pub struct Info {
/// The crate version, as read from the `CARGO_PKG_VERSION` environment variable.
pub crate_version: String,
/// The crate features, as found by the presence of `CARGO_FEATURE_*` environment variables.
///
/// These are normalised to lowercase and have underscores replaced by hyphens.
pub crate_features: Vec<String>,
/// The build date, in the format `YYYY-MM-DD`, at UTC.
///
/// This is either current as of build time, or from the timestamp specified by the
/// `SOURCE_DATE_EPOCH` environment variable, for
/// [reproducible builds](https://reproducible-builds.org/).
pub build_date: String,
/// The build datetime, in the format `YYYY-MM-DD HH:MM:SS`, at UTC.
///
/// This is either current as of build time, or from the timestamp specified by the
/// `SOURCE_DATE_EPOCH` environment variable, for
/// [reproducible builds](https://reproducible-builds.org/).
pub build_datetime: String,
/// Git repository information, if available.
pub git: Option<GitInfo>,
}
trait ErrString<T> {
fn err_string(self) -> Result<T, String>;
}
impl<T, E> ErrString<T> for Result<T, E>
where
E: std::fmt::Display,
{
fn err_string(self) -> Result<T, String> {
self.map_err(|e| e.to_string())
}
}
const DATE_FORMAT: &[FormatItem<'static>] = format_description!("[year]-[month]-[day]");
const DATETIME_FORMAT: &[FormatItem<'static>] =
format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
impl Info {
/// Gathers build-time information
///
/// This is not meant to be used directly under normal circumstances, but is public if you wish
/// to build your own frontend for whatever reason. In that case, note that no effort has been
/// made to make this usable outside of the build.rs environment.
pub fn gather() -> Result<Self, String> {
let build_date = Self::build_date()?;
Ok(Self {
crate_version: var("CARGO_PKG_VERSION").err_string()?,
crate_features: Self::features(),
build_date: build_date.format(DATE_FORMAT).err_string()?,
build_datetime: build_date.format(DATETIME_FORMAT).err_string()?,
#[cfg(feature = "git")]
git: GitInfo::gather()
.map_err(|e| {
println!("cargo:warning=git info gathering failed: {e}");
})
.ok(),
#[cfg(not(feature = "git"))]
git: None,
})
}
fn build_date() -> Result<OffsetDateTime, String> {
if cfg!(feature = "reproducible") {
if let Ok(date) = var("SOURCE_DATE_EPOCH") {
if let Ok(date) = date.parse::<i64>() {
return OffsetDateTime::from_unix_timestamp(date).err_string();
}
}
}
Ok(OffsetDateTime::now_utc())
}
fn features() -> Vec<String> {
let mut features = Vec::new();
for (key, _) in std::env::vars() {
if let Some(stripped) = key.strip_prefix("CARGO_FEATURE_") {
features.push(stripped.replace('_', "-").to_lowercase().clone());
}
}
features
}
pub(crate) fn set_reruns(&self) {
if cfg!(feature = "reproducible") {
println!("cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH");
}
if let Some(git) = &self.git {
let git_head = git.git_root.join("HEAD");
println!("cargo:rerun-if-changed={}", git_head.display());
}
}
}
/// Git repository information
#[derive(Debug, Clone)]
pub struct GitInfo {
/// The absolute path to the git repository's data folder.
///
/// In a normal repository, this is `.git`, _not_ the index or working directory.
pub git_root: PathBuf,
/// The full hash of the current commit.
///
/// Note that this makes no effore to handle dirty working directories, so it may not be
/// representative of the current state of the code.
pub git_hash: String,
/// The short hash of the current commit.
///
/// This is truncated to 8 characters.
pub git_shorthash: String,
/// The date of the current commit, in the format `YYYY-MM-DD`, at UTC.
pub git_date: String,
/// The datetime of the current commit, in the format `YYYY-MM-DD HH:MM:SS`, at UTC.
pub git_datetime: String,
}
#[cfg(feature = "git")]
impl GitInfo {
fn gather() -> Result<Self, String> {
let git_root = Self::find_git_dir(Path::new("."))
.ok_or_else(|| "no git repository found".to_string())?;
let hash =
Self::resolve_head(&git_root).ok_or_else(|| "could not resolve HEAD".to_string())?;
let timestamp = Self::read_commit_timestamp(&git_root, &hash)
.ok_or_else(|| "could not read commit timestamp".to_string())?;
let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).err_string()?;
Ok(Self {
git_root: git_root.canonicalize().err_string()?,
git_shorthash: hash.chars().take(8).collect(),
git_hash: hash,
git_date: timestamp.format(DATE_FORMAT).err_string()?,
git_datetime: timestamp.format(DATETIME_FORMAT).err_string()?,
})
}
fn find_git_dir(start: &Path) -> Option<PathBuf> {
use std::fs;
let mut current = start.canonicalize().ok()?;
loop {
let git_dir = current.join(".git");
if git_dir.is_dir() {
return Some(git_dir);
}
// Handle git worktrees: .git can be a file containing "gitdir: <path>"
if git_dir.is_file() {
let content = fs::read_to_string(&git_dir).ok()?;
if let Some(path) = content.strip_prefix("gitdir: ") {
return Some(PathBuf::from(path.trim()));
}
}
if !current.pop() {
return None;
}
}
}
fn resolve_head(git_dir: &Path) -> Option<String> {
use std::fs;
let head_content = fs::read_to_string(git_dir.join("HEAD")).ok()?;
let head_content = head_content.trim();
if let Some(ref_path) = head_content.strip_prefix("ref: ") {
Self::resolve_ref(git_dir, ref_path)
} else {
// Detached HEAD - direct commit hash
Some(head_content.to_string())
}
}
fn resolve_ref(git_dir: &Path, ref_path: &str) -> Option<String> {
use std::fs;
// Try loose ref first
let ref_file = git_dir.join(ref_path);
if let Ok(content) = fs::read_to_string(&ref_file) {
return Some(content.trim().to_string());
}
// Try packed-refs
let packed_refs = git_dir.join("packed-refs");
if let Ok(content) = fs::read_to_string(&packed_refs) {
for line in content.lines() {
if line.starts_with('#') || line.starts_with('^') {
continue;
}
let parts: Vec<_> = line.split_whitespace().collect();
if parts.len() >= 2 && parts[1] == ref_path {
return Some(parts[0].to_string());
}
}
}
None
}
fn read_commit_timestamp(git_dir: &Path, hash: &str) -> Option<i64> {
// Try loose object first
if let Some(timestamp) = Self::read_loose_commit_timestamp(git_dir, hash) {
return Some(timestamp);
}
// Try packfiles
Self::read_packed_commit_timestamp(git_dir, hash)
}
fn read_loose_commit_timestamp(git_dir: &Path, hash: &str) -> Option<i64> {
use flate2::read::ZlibDecoder;
use std::{fs, io::Read};
let (prefix, suffix) = hash.split_at(2);
let object_path = git_dir.join("objects").join(prefix).join(suffix);
let compressed = fs::read(&object_path).ok()?;
let mut decoder = ZlibDecoder::new(&compressed[..]);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed).ok()?;
Self::parse_commit_timestamp(&decompressed)
}
fn read_packed_commit_timestamp(git_dir: &Path, hash: &str) -> Option<i64> {
use std::fs;
let pack_dir = git_dir.join("objects").join("pack");
let entries = fs::read_dir(&pack_dir).ok()?;
// Parse the hash into bytes for comparison
let hash_bytes = Self::hex_to_bytes(hash)?;
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("idx") {
if let Some(offset) = Self::find_object_in_index(&path, &hash_bytes) {
let pack_path = path.with_extension("pack");
if let Some(data) = Self::read_pack_object(&pack_path, offset) {
return Self::parse_commit_timestamp(&data);
}
}
}
}
None
}
fn hex_to_bytes(hex: &str) -> Option<[u8; 20]> {
let mut bytes = [0u8; 20];
if hex.len() != 40 {
return None;
}
for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
let s = std::str::from_utf8(chunk).ok()?;
bytes[i] = u8::from_str_radix(s, 16).ok()?;
}
Some(bytes)
}
fn find_object_in_index(idx_path: &Path, hash: &[u8; 20]) -> Option<u64> {
use std::{
fs::File,
io::{Read, Seek, SeekFrom},
};
let mut file = File::open(idx_path).ok()?;
let mut header = [0u8; 8];
file.read_exact(&mut header).ok()?;
// Check for v2 index magic: 0xff744f63
if header[0..4] != [0xff, 0x74, 0x4f, 0x63] {
return None; // Only support v2 index
}
let version = u32::from_be_bytes([header[4], header[5], header[6], header[7]]);
if version != 2 {
return None;
}
// Read fanout table (256 * 4 bytes)
let mut fanout = [0u32; 256];
for entry in &mut fanout {
let mut buf = [0u8; 4];
file.read_exact(&mut buf).ok()?;
*entry = u32::from_be_bytes(buf);
}
let total_objects = fanout[255] as usize;
let first_byte = hash[0] as usize;
// Find range of objects with this first byte
let start = if first_byte == 0 {
0
} else {
fanout[first_byte - 1] as usize
};
let end = fanout[first_byte] as usize;
if start >= end {
return None;
}
// Binary search within the hash section
// Hashes start at offset 8 + 256*4 = 1032
let hash_section_offset = 8 + 256 * 4;
let mut left = start;
let mut right = end;
while left < right {
let mid = left + (right - left) / 2;
let hash_offset = hash_section_offset + mid * 20;
file.seek(SeekFrom::Start(hash_offset as u64)).ok()?;
let mut found_hash = [0u8; 20];
file.read_exact(&mut found_hash).ok()?;
match found_hash.cmp(hash) {
std::cmp::Ordering::Equal => {
// Found! Now get the offset
// CRC section starts after all hashes
// Offset section starts after CRC section
let offset_section =
hash_section_offset + total_objects * 20 + total_objects * 4;
let offset_entry = offset_section + mid * 4;
file.seek(SeekFrom::Start(offset_entry as u64)).ok()?;
let mut offset_buf = [0u8; 4];
file.read_exact(&mut offset_buf).ok()?;
let offset = u32::from_be_bytes(offset_buf);
// Check if this is a large offset (MSB set)
if offset & 0x80000000 != 0 {
// Large offset - need to read from 8-byte offset table
let large_idx = (offset & 0x7fffffff) as usize;
let large_offset_section = offset_section + total_objects * 4;
let large_entry = large_offset_section + large_idx * 8;
file.seek(SeekFrom::Start(large_entry as u64)).ok()?;
let mut large_buf = [0u8; 8];
file.read_exact(&mut large_buf).ok()?;
return Some(u64::from_be_bytes(large_buf));
}
return Some(u64::from(offset));
}
std::cmp::Ordering::Less => left = mid + 1,
std::cmp::Ordering::Greater => right = mid,
}
}
None
}
fn read_pack_object(pack_path: &Path, offset: u64) -> Option<Vec<u8>> {
use flate2::read::ZlibDecoder;
use std::{
fs::File,
io::{Read, Seek, SeekFrom},
};
let mut file = File::open(pack_path).ok()?;
file.seek(SeekFrom::Start(offset)).ok()?;
// Read object header (variable length encoding)
let mut byte = [0u8; 1];
file.read_exact(&mut byte).ok()?;
let obj_type = (byte[0] >> 4) & 0x07;
let mut size = u64::from(byte[0] & 0x0f);
let mut shift = 4;
while byte[0] & 0x80 != 0 {
file.read_exact(&mut byte).ok()?;
size |= u64::from(byte[0] & 0x7f) << shift;
shift += 7;
}
// Object types: 1=commit, 2=tree, 3=blob, 4=tag, 6=ofs_delta, 7=ref_delta
match obj_type {
1..=4 => {
// Regular object - just decompress
let mut decoder = ZlibDecoder::new(&mut file);
#[allow(clippy::cast_possible_truncation)]
let mut data = Vec::with_capacity(size as usize);
decoder.read_to_end(&mut data).ok()?;
// Add the git object header
let type_name = match obj_type {
1 => "commit",
2 => "tree",
3 => "blob",
4 => "tag",
_ => unreachable!(),
};
let mut result = format!("{} {}\0", type_name, data.len()).into_bytes();
result.extend(data);
Some(result)
}
6 | 7 => {
// Delta objects - not supported for simplicity
// In practice, the HEAD commit is often a delta, but resolving
// deltas requires recursive lookups which adds complexity
None
}
_ => None,
}
}
fn parse_commit_timestamp(data: &[u8]) -> Option<i64> {
let content = std::str::from_utf8(data).ok()?;
// Skip the header (e.g., "commit 123\0")
let content = content.split('\0').nth(1)?;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("committer ") {
// Format: "Name <email> timestamp timezone"
let parts: Vec<_> = rest.rsplitn(3, ' ').collect();
if parts.len() >= 2 {
return parts[1].parse().ok();
}
}
}
None
}
}
================================================
FILE: crates/bosion/src/lib.rs
================================================
#![doc = include_str!("../README.md")]
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
use std::{env::var, fs::File, io::Write, path::PathBuf};
pub use info::*;
mod info;
/// Gather build-time information for the current crate
///
/// See the crate-level documentation for a guide. This function is a convenience wrapper around
/// [`gather_to`] with the most common defaults: it writes to `bosion.rs` a pub(crate) struct named
/// `Bosion`.
pub fn gather() {
gather_to("bosion.rs", "Bosion", false);
}
/// Gather build-time information for the current crate (public visibility)
///
/// See the crate-level documentation for a guide. This function is a convenience wrapper around
/// [`gather_to`]: it writes to `bosion.rs` a pub struct named `Bosion`.
pub fn gather_pub() {
gather_to("bosion.rs", "Bosion", true);
}
/// Gather build-time information for the current crate (custom output)
///
/// Gathers a limited set of build-time information for the current crate and writes it to a file.
/// The file is always written to the `OUT_DIR` directory, as per Cargo conventions. It contains a
/// zero-size struct with a bunch of associated constants containing the gathered information, and a
/// `long_version_with` function (when the `std` feature is enabled) that takes a slice of extra
/// key-value pairs to append in the same format.
///
/// `public` controls whether the struct is `pub` (true) or `pub(crate)` (false).
///
/// The generated code is entirely documented, and will appear in your documentation (in docs.rs, it
/// only will if visibility is public).
///
/// See [`Info`] for a list of gathered data.
///
/// The constants include all the information from [`Info`], as well as the following:
///
/// - `LONG_VERSION`: A clap-ready long version string, including the crate version, features, build
/// date, and git information when available.
/// - `CRATE_FEATURE_STRING`: A string containing the crate features, in the format `+feat1 +feat2`.
///
/// We also instruct rustc to rerun the build script if the environment changes, as necessary.
pub fn gather_to(filename: &str, structname: &str, public: bool) {
let path = PathBuf::from(var("OUT_DIR").expect("bosion")).join(filename);
println!("cargo:rustc-env=BOSION_PATH={}", path.display());
let info = Info::gather().expect("bosion");
info.set_reruns();
let Info {
crate_version,
crate_features,
build_date,
build_datetime,
git,
} = info;
let crate_feature_string = crate_features
.iter()
.filter(|feat| *feat != "default")
.map(|feat| format!("+{feat}"))
.collect::<Vec<_>>()
.join(" ");
let crate_feature_list = crate_features.join(",");
let viz = if public { "pub" } else { "pub(crate)" };
let (git_render, long_version) = if let Some(GitInfo {
git_hash,
git_shorthash,
git_date,
git_datetime,
..
}) = git
{
(format!(
"
/// The git commit hash
///
/// This is the full hash of the commit that was built. Note that if the repository was
/// dirty, this will be the hash of the last commit, not including the changes.
pub const GIT_COMMIT_HASH: &'static str = {git_hash:?};
/// The git commit hash, shortened
///
/// This is the shortened hash of the commit that was built. Same caveats as with
/// `GIT_COMMIT_HASH` apply. The length of the hash is fixed at 8 characters.
pub const GIT_COMMIT_SHORTHASH: &'static str = {git_shorthash:?};
/// The git commit date
///
/// This is the date (`YYYY-MM-DD`) of the commit that was built. Same caveats as with
/// `GIT_COMMIT_HASH` apply.
pub const GIT_COMMIT_DATE: &'static str = {git_date:?};
/// The git commit date and time
///
/// This is the date and time (`YYYY-MM-DD HH:MM:SS`) of the commit that was built. Same
/// caveats as with `GIT_COMMIT_HASH` apply.
pub const GIT_COMMIT_DATETIME: &'static str = {git_datetime:?};
"
), format!("{crate_version} ({git_shorthash} {git_date}) {crate_feature_string}\ncommit-hash: {git_hash}\ncommit-date: {git_date}\nbuild-date: {build_date}\nrelease: {crate_version}\nfeatures: {crate_feature_list}"))
} else {
(String::new(), format!("{crate_version} ({build_date}) {crate_feature_string}\nbuild-date: {build_date}\nrelease: {crate_version}\nfeatures: {crate_feature_list}"))
};
#[cfg(feature = "std")]
let long_version_with_fn = r#"
/// Returns the long version string with extra information tacked on
///
/// This is the same as `LONG_VERSION` but takes a slice of key-value pairs to append to the
/// end in the same format.
pub fn long_version_with(extra: &[(&str, &str)]) -> String {
let mut output = Self::LONG_VERSION.to_string();
for (k, v) in extra {
output.push_str(&format!("\n{k}: {v}"));
}
output
}
"#;
#[cfg(not(feature = "std"))]
let long_version_with_fn = "";
let bosion_version = env!("CARGO_PKG_VERSION");
let render = format!(
r#"
/// Build-time information
///
/// This struct is generated by the [bosion](https://docs.rs/bosion) crate at build time.
///
/// Bosion version: {bosion_version}
#[derive(Debug, Clone, Copy)]
{viz} struct {structname};
#[allow(dead_code)]
impl {structname} {{
/// Clap-compatible long version string
///
/// At minimum, this will be the crate version and build date.
///
/// It presents as a first "summary" line like `crate_version (build_date) features`,
/// followed by `key: value` pairs. This is the same format used by `rustc -Vv`.
///
/// If git info is available, it also includes the git hash, short hash and commit date,
/// and swaps the build date for the commit date in the summary line.
pub const LONG_VERSION: &'static str = {long_version:?};
/// The crate version, as reported by Cargo
///
/// You should probably prefer reading the `CARGO_PKG_VERSION` environment variable.
pub const CRATE_VERSION: &'static str = {crate_version:?};
/// The crate features
///
/// This is a list of the features that were enabled when this crate was built,
/// lowercased and with underscores replaced by hyphens.
pub const CRATE_FEATURES: &'static [&'static str] = &{crate_features:?};
/// The crate features, as a string
///
/// This is in format `+feature +feature2 +feature3`, lowercased with underscores
/// replaced by hyphens.
pub const CRATE_FEATURE_STRING: &'static str = {crate_feature_string:?};
/// The build date
///
/// This is the date that the crate was built, in the format `YYYY-MM-DD`. If the
/// environment variable `SOURCE_DATE_EPOCH` was set, it's used instead of the current
/// time, for [reproducible builds](https://reproducible-builds.org/).
pub const BUILD_DATE: &'static str = {build_date:?};
/// The build datetime
///
/// This is the date and time that the crate was built, in the format
/// `YYYY-MM-DD HH:MM:SS`. If the environment variable `SOURCE_DATE_EPOCH` was set, it's
/// used instead of the current time, for
/// [reproducible builds](https://reproducible-builds.org/).
pub const BUILD_DATETIME: &'static str = {build_datetime:?};
{git_render}
{long_version_with_fn}
}}
"#
);
let mut file = File::create(path).expect("bosion");
file.write_all(render.as_bytes()).expect("bosion");
}
/// Gather build-time information and write it to the environment
///
/// See the crate-level documentation for a guide. This function is a convenience wrapper around
/// [`gather_to_env_with_prefix`] with the most common default prefix of `BOSION_`.
pub fn gather_to_env() {
gather_to_env_with_prefix("BOSION_");
}
/// Gather build-time information and write it to the environment
///
/// Gathers a limited set of build-time information for the current crate and makes it available to
/// the crate as build environment variables. This is an alternative to [`include!`]ing a file which
/// is generated at build time, like for [`gather`] and variants, which doesn't create any new code
/// and doesn't include any information in the binary that you do not explicitly use.
///
/// The environment variables are prefixed with the given string, which should be generally be
/// uppercase and end with an underscore.
///
/// See [`Info`] for a list of gathered data.
///
/// Unlike [`gather`], there is no Clap-ready `LONG_VERSION` string, but you can of course generate
/// one yourself from the environment variables.
///
/// We also instruct rustc to rerun the build script if the environment changes, as necessary.
pub fn gather_to_env_with_prefix(prefix: &str) {
let info = Info::gather().expect("bosion");
info.set_reruns();
let Info {
crate_version,
crate_features,
build_date,
build_datetime,
git,
} = info;
println!("cargo:rustc-env={prefix}CRATE_VERSION={crate_version}");
println!(
"cargo:rustc-env={prefix}CRATE_FEATURES={}",
crate_features.join(",")
);
println!("cargo:rustc-env={prefix}BUILD_DATE={build_date}");
println!("cargo:rustc-env={prefix}BUILD_DATETIME={build_datetime}");
if let Some(GitInfo {
git_hash,
git_shorthash,
git_date,
git_datetime,
..
}) = git
{
println!("cargo:rustc-env={prefix}GIT_COMMIT_HASH={git_hash}");
println!("cargo:rustc-env={prefix}GIT_COMMIT_SHORTHASH={git_shorthash}");
println!("cargo:rustc-env={prefix}GIT_COMMIT_DATE={git_date}");
println!("cargo:rustc-env={prefix}GIT_COMMIT_DATETIME={git_datetime}");
}
}
================================================
FILE: crates/cli/Cargo.toml
================================================
[package]
name = "watchexec-cli"
version = "2.5.1"
authors = ["Félix Saparelli <felix@passcod.name>", "Matt Green <mattgreenrocks@gmail.com>"]
license = "Apache-2.0"
description = "Executes commands in response to file modifications"
keywords = ["watcher", "filesystem", "cli", "watchexec"]
categories = ["command-line-utilities"]
documentation = "https://watchexec.github.io/docs/#watchexec"
homepage = "https://watchexec.github.io"
repository = "https://github.com/watchexec/watchexec"
readme = "README.md"
edition = "2021"
# sets the default for the workspace
default-run = "watchexec"
[[bin]]
name = "watchexec"
path = "src/main.rs"
[dependencies]
argfile = "0.2.0"
chrono = "0.4.31"
clap_complete = "4.5.44"
clap_complete_nushell = "4.4.2"
clap_mangen = "0.2.15"
clearscreen = "4.0.4"
dashmap = "6.1.0"
dirs = "6.0.0"
dunce = "1.0.4"
foldhash = "0.1.5" # needs to be in sync with jaq's requirement
futures = "0.3.29"
humantime = "2.1.0"
indexmap = "2.10.0" # needs to be in sync with jaq's requirement
jaq-core = "2.1.0"
jaq-json = { version = "1.1.0", features = ["serde_json"] }
jaq-std = "2.1.0"
notify-rust = "4.11.7"
serde_json = "1.0.138"
tempfile = "3.16.0"
termcolor = "1.4.0"
tracing = "0.1.40"
tracing-appender = "0.2.3"
which = "8.0.0"
[dependencies.blake3]
version = "1.3.3"
features = ["rayon"]
[dependencies.clap]
version = "4.4.7"
features = ["cargo", "derive", "env", "wrap_help"]
[dependencies.console-subscriber]
version = "0.5.0"
optional = true
[dependencies.eyra]
version = "0.22.0"
features = ["log", "env_logger"]
optional = true
[dependencies.ignore-files]
version = "3.0.5"
path = "../ignore-files"
[dependencies.miette]
version = "7.5.0"
features = ["fancy"]
[dependencies.pid1]
version = "0.1.1"
optional = true
[dependencies.project-origins]
version = "1.4.2"
path = "../project-origins"
[dependencies.watchexec]
version = "8.2.0"
path = "../lib"
[dependencies.watchexec-events]
version = "6.1.0"
path = "../events"
features = ["serde"]
[dependencies.watchexec-signals]
version = "5.0.1"
path = "../signals"
[dependencies.watchexec-filterer-globset]
version = "8.0.0"
path = "../filterer/globset"
[dependencies.tokio]
version = "1.33.0"
features = [
"fs",
"io-std",
"process",
"net",
"rt",
"rt-multi-thread",
"signal",
"sync",
]
[dependencies.tracing-subscriber]
version = "0.3.6"
features = [
"env-filter",
"fmt",
"json",
"tracing-log",
"ansi",
]
[target.'cfg(unix)'.dependencies]
libc = "0.2.74"
nix = { version = "0.30.1", features = ["net"] }
[target.'cfg(windows)'.dependencies]
socket2 = "0.6.1"
uuid = { version = "1.13.1", features = ["v4"] }
windows-sys = { version = ">= 0.59.0, < 0.62.0", features = ["Win32_Networking_WinSock"] }
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1.39"
[build-dependencies]
embed-resource = "3.0.1"
[build-dependencies.bosion]
version = "2.0.0"
path = "../bosion"
[dev-dependencies]
tracing-test = "0.2.4"
uuid = { workspace = true, features = [ "v4", "fast-rng" ] }
rand = { workspace = true }
[features]
default = ["pid1"]
## Build using Eyra's pure-Rust libc
eyra = ["dep:eyra"]
## Enables PID1 handling.
pid1 = ["dep:pid1"]
## Enables logging for PID1 handling.
pid1-withlog = ["pid1"]
## For debugging only: enables the Tokio Console.
dev-console = ["dep:console-subscriber"]
[package.metadata.binstall]
pkg-url = "{ repo }/releases/download/v{ version }/watchexec-{ version }-{ target }.{ archive-format }"
bin-dir = "watchexec-{ version }-{ target }/{ bin }{ binary-ext }"
pkg-fmt = "txz"
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
pkg-fmt = "zip"
[package.metadata.deb]
maintainer = "Félix Saparelli <felix@passcod.name>"
license-file = ["../../LICENSE", "0"]
section = "utility"
depends = "libc6, libgcc-s1" # not needed for musl, but see below
# conf-files = [] # look me up when config file lands
assets = [
["../../target/release/watchexec", "usr/bin/watchexec", "755"],
["README.md", "usr/share/doc/watchexec/README", "644"],
["../../doc/watchexec.1.md", "usr/share/doc/watchexec/watchexec.1.md", "644"],
["../../doc/watchexec.1", "usr/share/man/man1/watchexec.1", "644"],
["../../completions/bash", "usr/share/bash-completion/completions/watchexec", "644"],
["../../completions/fish", "usr/share/fish/vendor_completions.d/watchexec.fish", "644"],
["../../completions/zsh", "usr/share/zsh/site-functions/_watchexec", "644"],
["../../doc/logo.svg", "usr/share/icons/hicolor/scalable/apps/watchexec.svg", "644"],
]
[package.metadata.generate-rpm]
assets = [
{ source = "../../target/release/watchexec", dest = "/usr/bin/watchexec", mode = "755" },
{ source = "README.md", dest = "/usr/share/doc/watchexec/README", mode = "644", doc = true },
{ source = "../../doc/watchexec.1.md", dest = "/usr/share/doc/watchexec/watchexec.1.md", mode = "644", doc = true },
{ source = "../../doc/watchexec.1", dest = "/usr/share/man/man1/watchexec.1", mode = "644" },
{ source = "../../completions/bash", dest = "/usr/share/bash-completion/completions/watchexec", mode = "644" },
{ source = "../../completions/fish", dest = "/usr/share/fish/vendor_completions.d/watchexec.fish", mode = "644" },
{ source = "../../completions/zsh", dest = "/usr/share/zsh/site-functions/_watchexec", mode = "644" },
{ source = "../../doc/logo.svg", dest = "/usr/share/icons/hicolor/scalable/apps/watchexec.svg", mode = "644" },
# set conf = true for config file when that lands
]
auto-req = "disabled"
# technically incorrect when using musl, but these are probably
# present on every rpm-using system, so let's worry about it if
# someone asks.
[package.metadata.generate-rpm.requires]
glibc = "*"
libgcc = "*"
[lints.clippy]
nursery = "warn"
pedantic = "warn"
module_name_repetitions = "allow"
similar_names = "allow"
cognitive_complexity = "allow"
too_many_lines = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
default_trait_access = "allow"
enum_glob_use = "allow"
option_if_let_else = "allow"
blocks_in_conditions = "allow"
doc_markdown = "allow"
================================================
FILE: crates/cli/README.md
================================================
# Watchexec CLI
A simple standalone tool that watches a path and runs a command whenever it detects modifications.
Example use cases:
* Automatically run unit tests
* Run linters/syntax checkers
## Features
* Simple invocation and use
* Runs on Linux, Mac, Windows, and more
* Monitors current directory and all subdirectories for changes
* Uses efficient event polling mechanism (on Linux, Mac, Windows, BSD)
* Coalesces multiple filesystem events into one, for editors that use swap/backup files during saving
* By default, uses `.gitignore`, `.ignore`, and other such files to determine which files to ignore notifications for
* Support for watching files with a specific extension
* Support for filtering/ignoring events based on [glob patterns](https://docs.rs/globset/*/globset/#syntax)
* Launches the command in a new process group (can be disabled with `--no-process-group`)
* Optionally clears screen between executions
* Optionally restarts the command with every modification (good for servers)
* Optionally sends a desktop notification on command start and end
* Does not require a language runtime
* Sets the following environment variables in the process:
`$WATCHEXEC_COMMON_PATH` is set to the longest common path of all of the below variables, and so should be prepended to each path to obtain the full/real path.
| Variable name | Event kind |
|---|---|
| `$WATCHEXEC_CREATED_PATH` | files/folders were created |
| `$WATCHEXEC_REMOVED_PATH` | files/folders were removed |
| `$WATCHEXEC_RENAMED_PATH` | files/folders were renamed |
| `$WATCHEXEC_WRITTEN_PATH` | files/folders were modified |
| `$WATCHEXEC_META_CHANGED_PATH` | files/folders' metadata were modified |
| `$WATCHEXEC_OTHERWISE_CHANGED_PATH` | every other kind of event |
These variables may contain multiple paths: these are separated by the platform's path separator, as with the `PATH` system environment variable. On Unix that is `:`, and on Windows `;`. Within each variable, paths are deduplicated and sorted in binary order (i.e. neither Unicode nor locale aware).
This can be disabled with `--emit-events=none` or changed to JSON events on STDIN with `--emit-events=json-stdio`.
## Anti-Features
* Not tied to any particular language or ecosystem
* Not tied to Git or the presence of a repository/project
* Does not require a cryptic command line involving `xargs`
## Usage Examples
Watch all JavaScript, CSS and HTML files in the current directory and all subdirectories for changes, running `make` when a change is detected:
$ watchexec --exts js,css,html make
Call `make test` when any file changes in this directory/subdirectory, except for everything below `target`:
$ watchexec -i "target/**" make test
Call `ls -la` when any file changes in this directory/subdirectory:
$ watchexec -- ls -la
Call/restart `python server.py` when any Python file in the current directory (and all subdirectories) changes:
$ watchexec -e py -r python server.py
Call/restart `my_server` when any file in the current directory (and all subdirectories) changes, sending `SIGKILL` to stop the command:
$ watchexec -r --stop-signal SIGKILL my_server
Send a SIGHUP to the command upon changes (Note: using `-n` here we're executing `my_server` directly, instead of wrapping it in a shell:
$ watchexec -n --signal SIGHUP my_server
Run `make` when any file changes, using the `.gitignore` file in the current directory to filter:
$ watchexec make
Run `make` when any file in `lib` or `src` changes:
$ watchexec -w lib -w src make
Run `bundle install` when the `Gemfile` changes:
$ watchexec -w Gemfile bundle install
Run two commands:
$ watchexec 'date; make'
Get desktop ("toast") notifications when the command starts and finishes:
$ watchexec -N go build
Only run when files are created:
$ watchexec --fs-events create -- s3 sync . s3://my-bucket
If you come from `entr`, note that the watchexec command is run in a shell by default. You can use `-n` or `--shell=none` to not do that:
$ watchexec -n -- echo ';' lorem ipsum
On Windows, you may prefer to use Powershell:
$ watchexec --shell=pwsh -- Test-Connection example.com
You can eschew running commands entirely and get a stream of events to process on your own:
```console
$ watchexec --emit-events-to=json-stdio --only-emit-events
{"tags":[{"kind":"source","source":"filesystem"},{"kind":"fs","simple":"modify","full":"Modify(Data(Any))"},{"kind":"path","absolute":"/home/code/rust/watchexec/crates/cli/README.md","filetype":"file"}]}
{"tags":[{"kind":"source","source":"filesystem"},{"kind":"fs","simple":"modify","full":"Modify(Data(Any))"},{"kind":"path","absolute":"/home/code/rust/watchexec/crates/lib/Cargo.toml","filetype":"file"}]}
{"tags":[{"kind":"source","source":"filesystem"},{"kind":"fs","simple":"modify","full":"Modify(Data(Any))"},{"kind":"path","absolute":"/home/code/rust/watchexec/crates/cli/src/args.rs","filetype":"file"}]}
```
Print the time commands take to run:
```console
$ watchexec --timings -- make
[Running: make]
...
[Command was successful, lasted 52.748081074s]
```
## Installation
### Package manager
Watchexec is in many package managers. A full list of [known packages](../../doc/packages.md) is available,
and there may be more out there! Please contribute any you find to the list :)
Common package managers:
- Alpine: `$ apk add watchexec`
- ArchLinux: `$ pacman -S watchexec`
- Nix: `$ nix-shell -p watchexec`
- Debian/Ubuntu via [apt.cli.rs](https://apt.cli.rs): `$ apt install watchexec`
- Homebrew on Mac: `$ brew install watchexec`
- Chocolatey on Windows: `#> choco install watchexec`
### [Binstall](https://github.com/cargo-bins/cargo-binstall)
$ cargo binstall watchexec-cli
### Pre-built binaries
Use the download section on [Github](https://github.com/watchexec/watchexec/releases/latest)
or [the website](https://watchexec.github.io/downloads/) to obtain the package appropriate for your
platform and architecture, extract it, and place it in your `PATH`.
There are also Debian/Ubuntu (DEB) and Fedora/RedHat (RPM) packages.
Checksums and signatures are available.
### Cargo (from source)
Only the latest Rust stable is supported, but older versions may work.
$ cargo install watchexec-cli
## Shell completions
Currently available shell completions:
- bash: `completions/bash` should be installed to `/usr/share/bash-completion/completions/watchexec`
- elvish: `completions/elvish` should be installed to `$XDG_CONFIG_HOME/elvish/completions/`
- fish: `completions/fish` should be installed to `/usr/share/fish/vendor_completions.d/watchexec.fish`
- nu: `completions/nu` should be installed to `$XDG_CONFIG_HOME/nu/completions/`
- powershell: `completions/powershell` should be installed to `$PROFILE/`
- zsh: `completions/zsh` should be installed to `/usr/share/zsh/site-functions/_watchexec`
If not bundled, you can generate completions for your shell with `watchexec --completions <shell>`.
## Manual
There's a manual page at `doc/watchexec.1`. Install it to `/usr/share/man/man1/`.
If not bundled, you can generate a manual page with `watchexec --manual > /path/to/watchexec.1`, or view it inline with `watchexec --manual` (requires `man`).
You can also [read a text version](../../doc/watchexec.1.md).
Note that it is automatically generated from the help text, so it is not as pretty as a carefully hand-written one.
## Advanced builds
These are additional options available with custom builds by setting features:
### PID1
If you're using Watchexec as PID1 (most frequently in containers or namespaces), and it's not doing what you expect, you can create a build with PID1 early logging: `--features pid1-withlog`.
If you don't need PID1 support, or if you're doing something that conflicts with this program's PID1 support, you can disable it with `--no-default-features`.
### Eyra
[Eyra](https://github.com/sunfishcode/eyra) is a system to build Linux programs with no dependency on C code (in the libc path). To build Watchexec like this, use `--features eyra` and a Nightly compiler.
This feature also lets you get early logging into program startup, with `RUST_LOG=trace`.
================================================
FILE: crates/cli/build.rs
================================================
fn main() {
embed_resource::compile("watchexec-manifest.rc", embed_resource::NONE)
.manifest_optional()
.unwrap();
bosion::gather();
if std::env::var("CARGO_FEATURE_EYRA").is_ok() {
println!("cargo:rustc-link-arg=-nostartfiles");
}
}
================================================
FILE: crates/cli/integration/env-unix.sh
================================================
#!/bin/bash
set -euxo pipefail
watchexec=${WATCHEXEC_BIN:-watchexec}
$watchexec -1 --env FOO=BAR echo '$FOO' | grep BAR
================================================
FILE: crates/cli/integration/no-shell-unix.sh
================================================
#!/bin/bash
set -euxo pipefail
watchexec=${WATCHEXEC_BIN:-watchexec}
$watchexec -1 -n echo 'foo bar' | grep 'foo bar'
================================================
FILE: crates/cli/integration/socket.sh
================================================
#!/bin/bash
set -euxo pipefail
watchexec=${WATCHEXEC_BIN:-watchexec}
test_socketfd=${TEST_SOCKETFD_BIN:-test-socketfd}
$watchexec --socket 18080 -1 -- $test_socketfd tcp
$watchexec --socket udp::18080 -1 -- $test_socketfd udp
$watchexec --socket 18080 --socket 28080 -1 -- $test_socketfd tcp tcp
$watchexec --socket 18080 --socket 28080 --socket udp::38080 -1 -- $test_socketfd tcp tcp udp
if [[ "$TEST_PLATFORM" = "linux" ]]; then
$watchexec --socket 127.0.1.1:18080 -1 -- $test_socketfd tcp
fi
================================================
FILE: crates/cli/integration/stdin-quit-unix.sh
================================================
#!/bin/bash
set -euxo pipefail
watchexec=${WATCHEXEC_BIN:-watchexec}
timeout -s9 30s sh -c "sleep 10 | $watchexec --stdin-quit echo"
================================================
FILE: crates/cli/integration/trailingargfile-unix.sh
================================================
#!/bin/bash
set -euxo pipefail
watchexec=${WATCHEXEC_BIN:-watchexec}
$watchexec -1 -- echo @trailingargfile
================================================
FILE: crates/cli/release.toml
================================================
pre-release-commit-message = "release: cli v{{version}}"
tag-prefix = ""
tag-message = "watchexec {{version}}"
pre-release-hook = ["sh", "-c", "cd ../.. && bin/completions && bin/manpage"]
[[pre-release-replacements]]
file = "watchexec.exe.manifest"
search = "^ version=\"[\\d.]+[.]0\""
replace = " version=\"{{version}}.0\""
prerelease = false
max = 1
[[pre-release-replacements]]
file = "../../CITATION.cff"
search = "^version: \"?[\\d.]+(-.+)?\"?"
replace = "version: \"{{version}}\""
prerelease = true
max = 1
[[pre-release-replacements]]
file = "../../CITATION.cff"
search = "^date-released: .+"
replace = "date-released: {{date}}"
prerelease = true
max = 1
================================================
FILE: crates/cli/run-tests.sh
================================================
#!/bin/bash
set -euo pipefail
export WATCHEXEC_BIN=$(realpath ${WATCHEXEC_BIN:-$(which watchexec)})
export TEST_SOCKETFD_BIN=$(realpath ${TEST_SOCKETFD_BIN:-$(which test-socketfd)})
export TEST_PLATFORM="${1:-linux}"
cd "$(dirname "${BASH_SOURCE[0]}")/integration"
for test in *.sh; do
if [[ "$test" == *-unix.sh && "$TEST_PLATFORM" = "windows" ]]; then
echo "Skipping $test as it requires unix"
continue
fi
if [[ "$test" == *-win.sh && "$TEST_PLATFORM" != "windows" ]]; then
echo "Skipping $test as it requires windows"
continue
fi
echo
echo
echo "======= Testing $test ======="
./$test
done
================================================
FILE: crates/cli/src/args/command.rs
================================================
use std::{
ffi::{OsStr, OsString},
mem::take,
path::PathBuf,
};
use clap::{
builder::TypedValueParser,
error::{Error, ErrorKind},
Parser, ValueEnum, ValueHint,
};
use miette::{IntoDiagnostic, Result};
use tracing::{info, warn};
use watchexec_signals::Signal;
use crate::socket::{SocketSpec, SocketSpecValueParser};
use super::{TimeSpan, OPTSET_COMMAND};
#[derive(Debug, Clone, Parser)]
pub struct CommandArgs {
/// Use a different shell
///
/// By default, Watchexec will use '$SHELL' if it's defined or a default of 'sh' on Unix-likes,
/// and either 'pwsh', 'powershell', or 'cmd' (CMD.EXE) on Windows, depending on what Watchexec
/// detects is the running shell.
///
/// With this option, you can override that and use a different shell, for example one with more
/// features or one which has your custom aliases and functions.
///
/// If the value has spaces, it is parsed as a command line, and the first word used as the
/// shell program, with the rest as arguments to the shell.
///
/// The command is run with the '-c' flag (except for 'cmd' on Windows, where it's '/C').
///
/// The special value 'none' can be used to disable shell use entirely. In that case, the
/// command provided to Watchexec will be parsed, with the first word being the executable and
/// the rest being the arguments, and executed directly. Note that this parsing is rudimentary,
/// and may not work as expected in all cases.
///
/// Using 'none' is a little more efficient and can enable a stricter interpretation of the
/// input, but it also means that you can't use shell features like globbing, redirection,
/// control flow, logic, or pipes.
///
/// Examples:
///
/// Use without shell:
///
/// $ watchexec -n -- zsh -x -o shwordsplit scr
///
/// Use with powershell core:
///
/// $ watchexec --shell=pwsh -- Test-Connection localhost
///
/// Use with CMD.exe:
///
/// $ watchexec --shell=cmd -- dir
///
/// Use with a different unix shell:
///
/// $ watchexec --shell=bash -- 'echo $BASH_VERSION'
///
/// Use with a unix shell and options:
///
/// $ watchexec --shell='zsh -x -o shwordsplit' -- scr
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "SHELL",
display_order = 190,
)]
pub shell: Option<String>,
/// Shorthand for '--shell=none'
#[arg(
short = 'n',
help_heading = OPTSET_COMMAND,
display_order = 140,
)]
pub no_shell: bool,
/// Deprecated shorthand for '--emit-events=none'
///
/// This is the old way to disable event emission into the environment. See '--emit-events' for
/// more. Will be removed at next major release.
#[arg(
long,
help_heading = OPTSET_COMMAND,
hide = true, // deprecated
)]
pub no_environment: bool,
/// Add env vars to the command
///
/// This is a convenience option for setting environment variables for the command, without
/// setting them for the Watchexec process itself.
///
/// Use key=value syntax. Multiple variables can be set by repeating the option.
#[arg(
long,
short = 'E',
help_heading = OPTSET_COMMAND,
value_name = "KEY=VALUE",
value_parser = EnvVarValueParser,
display_order = 50,
)]
pub env: Vec<EnvVar>,
/// Don't use a process group
///
/// By default, Watchexec will run the command in a process group, so that signals and
/// terminations are sent to all processes in the group. Sometimes that's not what you want, and
/// you can disable the behaviour with this option.
///
/// Deprecated, use '--wrap-process=none' instead.
#[arg(
long,
help_heading = OPTSET_COMMAND,
display_order = 141,
)]
pub no_process_group: bool,
/// Configure how the process is wrapped
///
/// By default, Watchexec will run the command in a session on Mac, in a process group in Unix,
/// and in a Job Object in Windows.
///
/// Some Unix programs prefer running in a session, while others do not work in a process group.
///
/// Use 'group' to use a process group, 'session' to use a process session, and 'none' to run
/// the command directly. On Windows, either of 'group' or 'session' will use a Job Object.
///
/// If you find you need to specify this frequently for different kinds of programs, file an
/// issue at <https://github.com/watchexec/watchexec/issues>. As errors of this nature are hard to
/// debug and can be highly environment-dependent, reports from *multiple affected people* are
/// more likely to be actioned promptly. Ask your friends/colleagues!
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "MODE",
default_value = WRAP_DEFAULT,
display_order = 231,
)]
pub wrap_process: WrapMode,
/// Signal to send to stop the command
///
/// This is used by 'restart' and 'signal' modes of '--on-busy-update' (unless '--signal' is
/// provided). The restart behaviour is to send the signal, wait for the command to exit, and if
/// it hasn't exited after some time (see '--timeout-stop'), forcefully terminate it.
///
/// The default on unix is "SIGTERM".
///
/// Input is parsed as a full signal name (like "SIGTERM"), a short signal name (like "TERM"),
/// or a signal number (like "15"). All input is case-insensitive.
///
/// On Windows this option is technically supported but only supports the "KILL" event, as
/// Watchexec cannot yet deliver other events. Windows doesn't have signals as such; instead it
/// has termination (here called "KILL" or "STOP") and "CTRL+C", "CTRL+BREAK", and "CTRL+CLOSE"
/// events. For portability the unix signals "SIGKILL", "SIGINT", "SIGTERM", and "SIGHUP" are
/// respectively mapped to these.
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "SIGNAL",
display_order = 191,
)]
pub stop_signal: Option<Signal>,
/// Time to wait for the command to exit gracefully
///
/// This is used by the 'restart' mode of '--on-busy-update'. After the graceful stop signal
/// is sent, Watchexec will wait for the command to exit. If it hasn't exited after this time,
/// it is forcefully terminated.
///
/// Takes a unit-less value in seconds, or a time span value such as "5min 20s".
/// Providing a unit-less value is deprecated and will warn; it will be an error in the future.
///
/// The default is 10 seconds. Set to 0 to immediately force-kill the command.
///
/// This has no practical effect on Windows as the command is always forcefully terminated; see
/// '--stop-signal' for why.
#[arg(
long,
help_heading = OPTSET_COMMAND,
default_value = "10s",
hide_default_value = true,
value_name = "TIMEOUT",
display_order = 192,
)]
pub stop_timeout: TimeSpan,
/// Kill the command if it runs longer than this duration
///
/// Takes a time span value such as "30s", "5min", or "1h 30m".
///
/// When the timeout is reached, the command is gracefully stopped using --stop-signal, then
/// forcefully terminated after --stop-timeout if still running.
///
/// Each run of the command has its own independent timeout.
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "TIMEOUT",
display_order = 193,
)]
pub timeout: Option<TimeSpan>,
/// Sleep before running the command
///
/// This option will cause Watchexec to sleep for the specified amount of time before running
/// the command, after an event is detected. This is like using "sleep 5 && command" in a shell,
/// but portable and slightly more efficient.
///
/// Takes a unit-less value in seconds, or a time span value such as "2min 5s".
/// Providing a unit-less value is deprecated and will warn; it will be an error in the future.
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "DURATION",
display_order = 40,
)]
pub delay_run: Option<TimeSpan>,
/// Set the working directory
///
/// By default, the working directory of the command is the working directory of Watchexec. You
/// can change that with this option. Note that paths may be less intuitive to use with this.
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_hint = ValueHint::DirPath,
value_name = "DIRECTORY",
display_order = 230,
)]
pub workdir: Option<PathBuf>,
/// Provide a socket to the command
///
/// This implements the systemd socket-passing protocol, like with `systemfd`: sockets are
/// opened from the watchexec process, and then passed to the commands it runs. This lets you
/// keep sockets open and avoid address reuse issues or dropping packets.
///
/// This option can be supplied multiple times, to open multiple sockets.
///
/// The value can be either of `PORT` (opens a TCP listening socket at that port), `HOST:PORT`
/// (specify a host IP address; IPv6 addresses can be specified `[bracketed]`), `TYPE::PORT` or
/// `TYPE::HOST:PORT` (specify a socket type, `tcp` / `udp`).
///
/// This integration only provides basic support, if you want more control you should use the
/// `systemfd` tool from <https://github.com/mitsuhiko/systemfd>, upon which this is based. The
/// syntax here and the spawning behaviour is identical to `systemfd`, and both watchexec and
/// systemfd are compatible implementations of the systemd socket-activation protocol.
///
/// Watchexec does _not_ set the `LISTEN_PID` variable on unix, which means any child process of
/// your command could accidentally bind to the sockets, unless the `LISTEN_*` variables are
/// removed from the environment.
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "PORT",
value_parser = SocketSpecValueParser,
display_order = 60,
)]
pub socket: Vec<SocketSpec>,
}
impl CommandArgs {
pub(crate) async fn normalise(&mut self) -> Result<()> {
if self.no_process_group {
warn!("--no-process-group is deprecated");
self.wrap_process = WrapMode::None;
}
let workdir = if let Some(w) = take(&mut self.workdir) {
w
} else {
let curdir = std::env::current_dir().into_diagnostic()?;
dunce::canonicalize(curdir).into_diagnostic()?
};
info!(path=?workdir, "effective working directory");
self.workdir = Some(workdir);
debug_assert!(self.workdir.is_some());
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
pub enum WrapMode {
#[default]
Group,
Session,
None,
}
pub const WRAP_DEFAULT: &str = if cfg!(target_os = "macos") {
"session"
} else {
"group"
};
#[derive(Clone, Debug)]
pub struct EnvVar {
pub key: String,
pub value: OsString,
}
#[derive(Clone)]
pub(crate) struct EnvVarValueParser;
impl TypedValueParser for EnvVarValueParser {
type Value = EnvVar;
fn parse_ref(
&self,
_cmd: &clap::Command,
_arg: Option<&clap::Arg>,
value: &OsStr,
) -> Result<Self::Value, Error> {
let value = value
.to_str()
.ok_or_else(|| Error::raw(ErrorKind::ValueValidation, "invalid UTF-8"))?;
let (key, value) = value
.split_once('=')
.ok_or_else(|| Error::raw(ErrorKind::ValueValidation, "missing = separator"))?;
Ok(EnvVar {
key: key.into(),
value: value.into(),
})
}
}
================================================
FILE: crates/cli/src/args/events.rs
================================================
use std::{ffi::OsStr, path::PathBuf};
use clap::{
builder::TypedValueParser, error::ErrorKind, Arg, Command, CommandFactory, Parser, ValueEnum,
};
use miette::Result;
use tracing::warn;
use watchexec_signals::Signal;
use super::{command::CommandArgs, filtering::FilteringArgs, TimeSpan, OPTSET_EVENTS};
#[derive(Debug, Clone, Parser)]
pub struct EventsArgs {
/// What to do when receiving events while the command is running
///
/// Default is to 'do-nothing', which ignores events while the command is running, so that
/// changes that occur due to the command are ignored, like compilation outputs. You can also
/// use 'queue' which will run the command once again when the current run has finished if any
/// events occur while it's running, or 'restart', which terminates the running command and starts
/// a new one. Finally, there's 'signal', which only sends a signal; this can be useful with
/// programs that can reload their configuration without a full restart.
///
/// The signal can be specified with the '--signal' option.
#[arg(
short,
long,
help_heading = OPTSET_EVENTS,
default_value = "do-nothing",
hide_default_value = true,
value_name = "MODE",
display_order = 150,
)]
pub on_busy_update: OnBusyUpdate,
/// Restart the process if it's still running
///
/// This is a shorthand for '--on-busy-update=restart'.
#[arg(
short,
long,
help_heading = OPTSET_EVENTS,
conflicts_with_all = ["on_busy_update"],
display_order = 180,
)]
pub restart: bool,
/// Send a signal to the process when it's still running
///
/// Specify a signal to send to the process when it's still running. This implies
/// '--on-busy-update=signal'; otherwise the signal used when that mode is 'restart' is
/// controlled by '--stop-signal'.
///
/// See the long documentation for '--stop-signal' for syntax.
///
/// Signals are not supported on Windows at the moment, and will always be overridden to 'kill'.
/// See '--stop-signal' for more on Windows "signals".
#[arg(
short,
long,
help_heading = OPTSET_EVENTS,
conflicts_with_all = ["restart"],
value_name = "SIGNAL",
display_order = 190,
)]
pub signal: Option<Signal>,
/// Translate signals from the OS to signals to send to the command
///
/// Takes a pair of signal names, separated by a colon, such as "TERM:INT" to map SIGTERM to
/// SIGINT. The first signal is the one received by watchexec, and the second is the one sent to
/// the command. The second can be omitted to discard the first signal, such as "TERM:" to
/// not do anything on SIGTERM.
///
/// If SIGINT or SIGTERM are mapped, then they no longer quit Watchexec. Besides making it hard
/// to quit Watchexec itself, this is useful to send pass a Ctrl-C to the command without also
/// terminating Watchexec and the underlying program with it, e.g. with "INT:INT".
///
/// This option can be specified multiple times to map multiple signals.
///
/// Signal syntax is case-insensitive for short names (like "TERM", "USR2") and long names (like
/// "SIGKILL", "SIGHUP"). Signal numbers are also supported (like "15", "31"). On Windows, the
/// forms "STOP", "CTRL+C", and "CTRL+BREAK" are also supported to receive, but Watchexec cannot
/// yet deliver other "signals" than a STOP.
#[arg(
long = "map-signal",
help_heading = OPTSET_EVENTS,
value_name = "SIGNAL:SIGNAL",
value_parser = SignalMappingValueParser,
display_order = 130,
)]
pub signal_map: Vec<SignalMapping>,
/// Time to wait for new events before taking action
///
/// When an event is received, Watchexec will wait for up to this amount of time before handling
/// it (such as running the command). This is essential as what you might perceive as a single
/// change may actually emit many events, and without this behaviour, Watchexec would run much
/// too often. Additionally, it's not infrequent that file writes are not atomic, and each write
/// may emit an event, so this is a good way to avoid running a command while a file is
/// partially written.
///
/// An alternative use is to set a high value (like "30min" or longer), to save power or
/// bandwidth on intensive tasks, like an ad-hoc backup script. In those use cases, note that
/// every accumulated event will build up in memory.
///
/// Takes a unit-less value in milliseconds, or a time span value such as "5sec 20ms".
/// Providing a unit-less value is deprecated and will warn; it will be an error in the future.
///
/// The default is 50 milliseconds. Setting to 0 is highly discouraged.
#[arg(
long,
short,
help_heading = OPTSET_EVENTS,
default_value = "50ms",
hide_default_value = true,
value_name = "TIMEOUT",
display_order = 40,
)]
pub debounce: TimeSpan<1_000_000>,
/// Exit when stdin closes
///
/// This watches the stdin file descriptor for EOF, and exits Watchexec gracefully when it is
/// closed. This is used by some process managers to avoid leaving zombie processes around.
#[arg(
long,
help_heading = OPTSET_EVENTS,
display_order = 191,
)]
pub stdin_quit: bool,
/// Respond to keypresses to quit, restart, or pause
///
/// In interactive mode, Watchexec listens for keypresses and responds to them. Currently
/// supported keys are: 'r' to restart the command, 'p' to toggle pausing the watch, and 'q'
/// to quit. This requires a terminal (TTY) and puts stdin into raw mode, so the child process
/// will not receive stdin input.
#[arg(
long,
short = 'I',
help_heading = OPTSET_EVENTS,
display_order = 90,
)]
pub interactive: bool,
/// Exit when the command has an error
///
/// By default, Watchexec will continue to watch and re-run the command after the command
/// exits, regardless of its exit status. With this option, it will instead exit when the
/// command completes with any non-success exit status.
///
/// This is useful when running Watchexec in a process manager or container, where you want
/// the container to restart when the command fails rather than hang waiting for file changes.
#[arg(
long,
help_heading = OPTSET_EVENTS,
display_order = 91,
)]
pub exit_on_error: bool,
/// Wait until first change before running command
///
/// By default, Watchexec will run the command once immediately. With this option, it will
/// instead wait until an event is detected before running the command as normal.
#[arg(
long,
short,
help_heading = OPTSET_EVENTS,
display_order = 161,
)]
pub postpone: bool,
/// Poll for filesystem changes
///
/// By default, and where available, Watchexec uses the operating system's native file system
/// watching capabilities. This option disables that and instead uses a polling mechanism, which
/// is less efficient but can work around issues with some file systems (like network shares) or
/// edge cases.
///
/// Optionally takes a unit-less value in milliseconds, or a time span value such as "2s 500ms",
/// to use as the polling interval. If not specified, the default is 30 seconds.
/// Providing a unit-less value is deprecated and will warn; it will be an error in the future.
///
/// Aliased as '--force-poll'.
#[arg(
long,
help_heading = OPTSET_EVENTS,
alias = "force-poll",
num_args = 0..=1,
default_missing_value = "30s",
value_name = "INTERVAL",
display_order = 160,
)]
pub poll: Option<TimeSpan<1_000_000>>,
/// Configure event emission
///
/// Watchexec can emit event information when running a command, which can be used by the child
/// process to target specific changed files.
///
/// One thing to take care with is assuming inherent behaviour where there is only chance.
/// Notably, it could appear as if the `RENAMED` variable contains both the original and the new
/// path being renamed. In previous versions, it would even appear on some platforms as if the
/// original always came before the new. However, none of this was true. It's impossible to
/// reliably and portably know which changed path is the old or new, "half" renames may appear
/// (only the original, only the new), "unknown" renames may appear (change was a rename, but
/// whether it was the old or new isn't known), rename events might split across two debouncing
/// boundaries, and so on.
///
/// This option controls where that information is emitted. It defaults to 'none', which doesn't
/// emit event information at all. The other options are 'environment' (deprecated), 'stdio',
/// 'file', 'json-stdio', and 'json-file'.
///
/// The 'stdio' and 'file' modes are text-based: 'stdio' writes absolute paths to the stdin of
/// the command, one per line, each prefixed with `create:`, `remove:`, `rename:`, `modify:`,
/// or `other:`, then closes the handle; 'file' writes the same thing to a temporary file, and
/// its path is given with the $WATCHEXEC_EVENTS_FILE environment variable.
///
/// There are also two JSON modes, which are based on JSON objects and can represent the full
/// set of events Watchexec handles. Here's an example of a folder being created on Linux:
///
/// ```json
/// {
/// "tags": [
/// {
/// "kind": "path",
/// "absolute": "/home/user/your/new-folder",
/// "filetype": "dir"
/// },
/// {
/// "kind": "fs",
/// "simple": "create",
/// "full": "Create(Folder)"
/// },
/// {
/// "kind": "source",
/// "source": "filesystem",
/// }
/// ],
/// "metadata": {
/// "notify-backend": "inotify"
/// }
/// }
/// ```
///
/// The fields are as follows:
///
/// - `tags`, structured event data.
/// - `tags[].kind`, which can be:
/// * 'path', along with:
/// + `absolute`, an absolute path.
/// + `filetype`, a file type if known ('dir', 'file', 'symlink', 'other').
/// * 'fs':
/// + `simple`, the "simple" event type ('access', 'create', 'modify', 'remove', or 'other').
/// + `full`, the "full" event type, which is too complex to fully describe here, but looks like 'General(Precise(Specific))'.
/// * 'source', along with:
/// + `source`, the source of the event ('filesystem', 'keyboard', 'mouse', 'os', 'time', 'internal').
/// * 'keyboard', along with:
/// + `keycode`. Currently only the value 'eof' is supported.
/// * 'process', for events caused by processes:
/// + `pid`, the process ID.
/// * 'signal', for signals sent to Watchexec:
/// + `signal`, the normalised signal name ('hangup', 'interrupt', 'quit', 'terminate', 'user1', 'user2').
/// * 'completion', for when a command ends:
/// + `disposition`, the exit disposition ('success', 'error', 'signal', 'stop', 'exception', 'continued').
/// + `code`, the exit, signal, stop, or exception code.
/// - `metadata`, additional information about the event.
///
/// The 'json-stdio' mode will emit JSON events to the standard input of the command, one per
/// line, then close stdin. The 'json-file' mode will create a temporary file, write the
/// events to it, and provide the path to the file with the $WATCHEXEC_EVENTS_FILE
/// environment variable.
///
/// Finally, the 'environment' mode was the default until 2.0. It sets environment variables
/// with the paths of the affected files, for filesystem events:
///
/// $WATCHEXEC_COMMON_PATH is set to the longest common path of all of the below variables,
/// and so should be prepended to each path to obtain the full/real path. Then:
///
/// - $WATCHEXEC_CREATED_PATH is set when files/folders were created
/// - $WATCHEXEC_REMOVED_PATH is set when files/folders were removed
/// - $WATCHEXEC_RENAMED_PATH is set when files/folders were renamed
/// - $WATCHEXEC_WRITTEN_PATH is set when files/folders were modified
/// - $WATCHEXEC_META_CHANGED_PATH is set when files/folders' metadata were modified
/// - $WATCHEXEC_OTHERWISE_CHANGED_PATH is set for every other kind of pathed event
///
/// Multiple paths are separated by the system path separator, ';' on Windows and ':' on unix.
/// Within each variable, paths are deduplicated and sorted in binary order (i.e. neither
/// Unicode nor locale aware).
///
/// This is the legacy mode, is deprecated, and will be removed in the future. The environment
/// is a very restricted space, while also limited in what it can usefully represent. Large
/// numbers of files will either cause the environment to be truncated, or may error or crash
/// the process entirely. The $WATCHEXEC_COMMON_PATH is also unintuitive, as demonstrated by the
/// multiple confused queries that have landed in my inbox over the years.
#[arg(
long,
help_heading = OPTSET_EVENTS,
verbatim_doc_comment,
default_value = "none",
hide_default_value = true,
value_name = "MODE",
display_order = 50,
)]
pub emit_events_to: EmitEvents,
}
impl EventsArgs {
pub(crate) fn normalise(
&mut self,
command: &CommandArgs,
filtering: &FilteringArgs,
only_emit_events: bool,
) -> Result<()> {
if self.signal.is_some() {
self.on_busy_update = OnBusyUpdate::Signal;
} else if self.restart {
self.on_busy_update = OnBusyUpdate::Restart;
}
if command.no_environment {
warn!("--no-environment is deprecated");
self.emit_events_to = EmitEvents::None;
}
if only_emit_events
&& !matches!(
self.emit_events_to,
EmitEvents::JsonStdio | EmitEvents::Stdio
) {
self.emit_events_to = EmitEvents::JsonStdio;
}
if self.stdin_quit && filtering.watch_file == Some(PathBuf::from("-")) {
super::Args::command()
.error(
ErrorKind::InvalidValue,
"stdin-quit cannot be used when --watch-file=-",
)
.exit();
}
if self.interactive && filtering.watch_file == Some(PathBuf::from("-")) {
super::Args::command()
.error(
ErrorKind::InvalidValue,
"interactive mode cannot be used when --watch-file=-",
)
.exit();
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
pub enum EmitEvents {
#[default]
Environment,
Stdio,
File,
JsonStdio,
JsonFile,
None,
}
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
pub enum OnBusyUpdate {
#[default]
Queue,
DoNothing,
Restart,
Signal,
}
#[derive(Clone, Copy, Debug)]
pub struct SignalMapping {
pub from: Signal,
pub to: Option<Signal>,
}
#[derive(Clone)]
struct SignalMappingValueParser;
impl TypedValueParser for SignalMappingValueParser {
type Value = SignalMapping;
fn parse_ref(
&self,
_cmd: &Command,
_arg: Option<&Arg>,
value: &OsStr,
) -> Result<Self::Value, clap::error::Error> {
let value = value
.to_str()
.ok_or_else(|| clap::error::Error::raw(ErrorKind::ValueValidation, "invalid UTF-8"))?;
let (from, to) = value
.split_once(':')
.ok_or_else(|| clap::error::Error::raw(ErrorKind::ValueValidation, "missing ':'"))?;
let from = from
.parse::<Signal>()
.map_err(|sigparse| clap::error::Error::raw(ErrorKind::ValueValidation, sigparse))?;
let to = if to.is_empty() {
None
} else {
Some(to.parse::<Signal>().map_err(|sigparse| {
clap::error::Error::raw(ErrorKind::ValueValidation, sigparse)
})?)
};
Ok(Self::Value { from, to })
}
}
================================================
FILE: crates/cli/src/args/filtering.rs
================================================
use std::{
collections::BTreeSet,
mem::take,
path::{Path, PathBuf},
};
use clap::{Parser, ValueEnum, ValueHint};
use miette::{IntoDiagnostic, Result};
use tokio::{
fs::File,
io::{AsyncBufReadExt, BufReader},
};
use tracing::{debug, info};
use watchexec::{paths::PATH_SEPARATOR, WatchedPath};
use crate::filterer::parse::FilterProgram;
use super::{command::CommandArgs, OPTSET_FILTERING};
#[derive(Debug, Clone, Parser)]
pub struct FilteringArgs {
#[doc(hidden)]
#[arg(skip)]
pub paths: Vec<WatchedPath>,
/// Watch a specific file or directory
///
/// By default, Watchexec watches the current directory.
///
/// When watching a single file, it's often better to watch the containing directory instead,
/// and filter on the filename. Some editors may replace the file with a new one when saving,
/// and some platforms may not detect that or further changes.
///
/// Upon starting, Watchexec resolves a "project origin" from the watched paths. See the help
/// for '--project-origin' for more information.
///
/// This option can be specified multiple times to watch multiple files or directories.
///
/// The special value '/dev/null', provided as the only path watched, will cause Watchexec to
/// not watch any paths. Other event sources (like signals or key events) may still be used.
#[arg(
short = 'w',
long = "watch",
help_heading = OPTSET_FILTERING,
value_hint = ValueHint::AnyPath,
value_name = "PATH",
display_order = 230,
)]
pub recursive_paths: Vec<PathBuf>,
/// Watch a specific directory, non-recursively
///
/// Unlike '-w', folders watched with this option are not recursed into.
///
/// This option can be specified multiple times to watch multiple directories non-recursively.
#[arg(
short = 'W',
long = "watch-non-recursive",
help_heading = OPTSET_FILTERING,
value_hint = ValueHint::AnyPath,
value_name = "PATH",
display_order = 231,
)]
pub non_recursive_paths: Vec<PathBuf>,
/// Watch files and directories from a file
///
/// Each line in the file will be interpreted as if given to '-w'.
///
/// For more complex uses (like watching non-recursively), use the argfile capability: build a
/// file containing command-line options and pass it to watchexec with `@path/to/argfile`.
///
/// The special value '-' will read from STDIN; this in incompatible with '--stdin-quit'.
#[arg(
short = 'F',
long,
help_heading = OPTSET_FILTERING,
value_hint = ValueHint::AnyPath,
value_name = "PATH",
display_order = 232,
)]
pub watch_file: Option<PathBuf>,
/// Don't load gitignores
///
/// Among other VCS exclude files, like for Mercurial, Subversion, Bazaar, DARCS, Fossil. Note
/// that Watchexec will detect which of these is in use, if any, and only load the relevant
/// files. Both global (like '~/.gitignore') and local (like '.gitignore') files are considered.
///
/// This option is useful if you want to watch files that are ignored by Git.
#[arg(
long,
help_heading = OPTSET_FILTERING,
display_order = 145,
)]
pub no_vcs_ignore: bool,
/// Don't load project-local ignores
///
/// This disables loading of project-local ignore files, like '.gitignore' or '.ignore' in the
/// watched project. This is contrasted with '--no-vcs-ignore', which disables loading of Git
/// and other VCS ignore files, and with '--no-global-ignore', which disables loading of global
/// or user ignore files, like '~/.gitignore' or '~/.config/watchexec/ignore'.
///
/// Supported project ignore files:
///
/// - Git: .gitignore at project root and child directories, .git/info/exclude, and the file pointed to by `core.excludesFile` in .git/config.
/// - Mercurial: .hgignore at project root and child directories.
/// - Bazaar: .bzrignore at project root.
/// - Darcs: _darcs/prefs/boring
/// - Fossil: .fossil-settings/ignore-glob
/// - Ripgrep/Watchexec/generic: .ignore at project root and child directories.
///
/// VCS ignore files (Git, Mercurial, Bazaar, Darcs, Fossil) are only used if the corresponding
/// VCS is discovered to be in use for the project/origin. For example, a .bzrignore in a Git
/// repository will be discarded.
#[arg(
long,
help_heading = OPTSET_FILTERING,
verbatim_doc_comment,
display_order = 144,
)]
pub no_project_ignore: bool,
/// Don't load global ignores
///
/// This disables loading of global or user ignore files, like '~/.gitignore',
/// '~/.config/watchexec/ignore', or '%APPDATA%\Bazzar\2.0\ignore'. Contrast with
/// '--no-vcs-ignore' and '--no-project-ignore'.
///
/// Supported global ignore files
///
/// - Git (if core.excludesFile is set): the file at that path
/// - Git (otherwise): the first found of $XDG_CONFIG_HOME/git/ignore, %APPDATA%/.gitignore, %USERPROFILE%/.gitignore, $HOME/.config/git/ignore, $HOME/.gitignore.
/// - Bazaar: the first found of %APPDATA%/Bazzar/2.0/ignore, $HOME/.bazaar/ignore.
/// - Watchexec: the first found of $XDG_CONFIG_HOME/watchexec/ignore, %APPDATA%/watchexec/ignore, %USERPROFILE%/.watchexec/ignore, $HOME/.watchexec/ignore.
///
/// Like for project files, Git and Bazaar global files will only be used for the corresponding
/// VCS as used in the project.
#[arg(
long,
help_heading = OPTSET_FILTERING,
verbatim_doc_comment,
display_order = 142,
)]
pub no_global_ignore: bool,
/// Don't use internal default ignores
///
/// Watchexec has a set of default ignore patterns, such as editor swap files, `*.pyc`, `*.pyo`,
/// `.DS_Store`, `.bzr`, `_darcs`, `.fossil-settings`, `.git`, `.hg`, `.pijul`, `.svn`, and
/// Watchexec log files.
#[arg(
long,
help_heading = OPTSET_FILTERING,
display_order = 140,
)]
pub no_default_ignore: bool,
/// Don't discover ignore files at all
///
/// This is a shorthand for '--no-global-ignore', '--no-vcs-ignore', '--no-project-ignore', but
/// even more efficient as it will skip all the ignore discovery mechanisms from the get go.
///
/// Note that default ignores are still loaded, see '--no-default-ignore'.
#[arg(
long,
help_heading = OPTSET_FILTERING,
display_order = 141,
)]
pub no_discover_ignore: bool,
/// Don't ignore anything at all
///
/// This is a shorthand for '--no-discover-ignore', '--no-default-ignore'.
///
/// Note that ignores explicitly loaded via other command line options, such as '--ignore' or
/// '--ignore-file', will still be used.
#[arg(
long,
help_heading = OPTSET_FILTERING,
display_order = 92,
)]
pub ignore_nothing: bool,
/// Filename extensions to filter to
///
/// This is a quick filter to only emit events for files with the given extensions. Extensions
/// can be given with or without the leading dot (e.g. 'js' or '.js'). Multiple extensions can
/// be given by repeating the option or by separating them with commas.
#[arg(
long = "exts",
short = 'e',
help_heading = OPTSET_FILTERING,
value_delimiter = ',',
value_name = "EXTENSIONS",
display_order = 50,
)]
pub filter_extensions: Vec<String>,
/// Filename patterns to filter to
///
/// Provide a glob-like filter pattern, and only events for files matching the pattern will be
/// emitted. Multiple patterns can be given by repeating the option. Events that are not from
/// files (e.g. signals, keyboard events) will pass through untouched.
#[arg(
long = "filter",
short = 'f',
help_heading = OPTSET_FILTERING,
value_name = "PATTERN",
display_order = 60,
)]
pub filter_patterns: Vec<String>,
/// Files to load filters from
///
/// Provide a path to a file containing filters, one per line. Empty lines and lines starting
/// with '#' are ignored. Uses the same pattern format as the '--filter' option.
///
/// This can also be used via the $WATCHEXEC_FILTER_FILES environment variable.
#[arg(
long = "filter-file",
help_heading = OPTSET_FILTERING,
value_delimiter = PATH_SEPARATOR.chars().next().unwrap(),
value_hint = ValueHint::FilePath,
value_name = "PATH",
env = "WATCHEXEC_FILTER_FILES",
hide_env = true,
display_order = 61,
)]
pub filter_files: Vec<PathBuf>,
/// Set the project origin
///
/// Watchexec will attempt to discover the project's "origin" (or "root") by searching for a
/// variety of markers, like files or directory patterns. It does its best but sometimes gets it
/// it wrong, and you can override that with this option.
///
/// The project origin is used to determine the path of certain ignore files, which VCS is being
/// used, the meaning of a leading '/' in filtering patterns, and maybe more in the future.
///
/// When set, Watchexec will also not bother searching, which can be significantly faster.
#[arg(
long,
help_heading = OPTSET_FILTERING,
value_hint = ValueHint::DirPath,
value_name = "DIRECTORY",
display_order = 160,
)]
pub project_origin: Option<PathBuf>,
/// Filter programs.
///
/// Provide your own custom filter programs in jaq (similar to jq) syntax. Programs are given
/// an event in the same format as described in '--emit-events-to' and must return a boolean.
/// Invalid programs will make watchexec fail to start; use '-v' to see program runtime errors.
///
/// In addition to the jaq stdlib, watchexec adds some custom filter definitions:
///
/// - 'path | file_meta' returns file metadata or null if the file does not exist.
///
/// - 'path | file_size' returns the size of the file at path, or null if it does not exist.
///
/// - 'path | file_read(bytes)' returns a string with the first n bytes of the file at path.
/// If the file is smaller than n bytes, the whole file is returned. There is no filter to
/// read the whole file at once to encourage limiting the amount of data read and processed.
///
/// - 'string | hash', and 'path | file_hash' return the hash of the string or file at path.
/// No guarantee is made about the algorithm used: treat it as an opaque value.
///
/// - 'any | kv_store(key)', 'kv_fetch(key)', and 'kv_clear' provide a simple key-value store.
/// Data is kept in memory only, there is no persistence. Consistency is not guaranteed.
///
/// - 'any | printout', 'any | printerr', and 'any | log(level)' will print or log any given
/// value to stdout, stderr, or the log (levels = error, warn, info, debug, trace), and
/// pass the value through (so '[1] | log("debug") | .[]' will produce a '1' and log '[1]').
///
/// All filtering done with such programs, and especially those using kv or filesystem access,
/// is much slower than the other filtering methods. If filtering is too slow, events will back
/// up and stall watchexec. Take care when designing your filters.
///
/// If the argument to this option starts with an '@', the rest of the argument is taken to be
/// the path to a file containing a jaq program.
///
/// Jaq programs are run in order, after all other filters, and short-circuit: if a filter (jaq
/// or not) rejects an event, execution stops there, and no other filters are run. Additionally,
/// they stop after outputting the first value, so you'll want to use 'any' or 'all' when
/// iterating, otherwise only the first item will be processed, which can be quite confusing!
///
/// Find user-contributed programs or submit your own useful ones at
/// <https://github.com/watchexec/watchexec/discussions/592>.
///
/// ## Examples:
///
/// Regexp ignore filter on paths:
///
/// 'all(.tags[] | select(.kind == "path"); .absolute | test("[.]test[.]js$")) | not'
///
/// Pass any event that creates a file:
///
/// 'any(.tags[] | select(.kind == "fs"); .simple == "create")'
///
/// Pass events that touch executable files:
///
/// 'any(.tags[] | select(.kind == "path" && .filetype == "file"); .absolute | metadata | .executable)'
///
/// Ignore files that start with shebangs:
///
/// 'any(.tags[] | select(.kind == "path" && .filetype == "file"); .absolute | read(2) == "#!") | not'
#[arg(
long = "filter-prog",
short = 'j',
help_heading = OPTSET_FILTERING,
value_name = "EXPRESSION",
display_order = 62,
)]
pub filter_programs: Vec<String>,
#[doc(hidden)]
#[clap(skip)]
pub filter_programs_parsed: Vec<FilterProgram>,
/// Filename patterns to filter out
///
/// Provide a glob-like filter pattern, and events for files matching the pattern will be
/// excluded. Multiple patterns can be given by repeating the option. Events that are not from
/// files (e.g. signals, keyboard events) will pass through untouched.
#[arg(
long = "ignore",
short = 'i',
help_heading = OPTSET_FILTERING,
value_name = "PATTERN",
display_order = 90,
)]
pub ignore_patterns: Vec<String>,
/// Files to load ignores from
///
/// Provide a path to a file containing ignores, one per line. Empty lines and lines starting
/// with '#' are ignored. Uses the same pattern format as the '--ignore' option.
///
/// This can also be used via the $WATCHEXEC_IGNORE_FILES environment variable.
#[arg(
long = "ignore-file",
help_heading = OPTSET_FILTERING,
value_delimiter = PATH_SEPARATOR.chars().next().unwrap(),
value_hint = ValueHint::FilePath,
value_name = "PATH",
env = "WATCHEXEC_IGNORE_FILES",
hide_env = true,
display_order = 91,
)]
pub ignore_files: Vec<PathBuf>,
/// Filesystem events to filter to
///
/// This is a quick filter to only emit events for the given types of filesystem changes. Choose
/// from 'access', 'create', 'remove', 'rename', 'modify', 'metadata'. Multiple types can be
/// given by repeating the option or by separating them with commas. By default, this is all
/// types except for 'access'.
///
/// This may apply filtering at the kernel level when possible, which can be more efficient, but
/// may be more confusing when reading the logs.
#[arg(
long = "fs-events",
help_heading = OPTSET_FILTERING,
default_value = "create,remove,rename,modify,metadata",
value_delimiter = ',',
hide_default_value = true,
value_name = "EVENTS",
display_order = 63,
)]
pub filter_fs_events: Vec<FsEvent>,
/// Don't emit fs events for metadata changes
///
/// This is a shorthand for '--fs-events create,remove,rename,modify'. Using it alongside the
/// '--fs-events' option is non-sensical and not allowed.
#[arg(
long = "no-meta",
help_heading = OPTSET_FILTERING,
conflicts_with = "filter_fs_events",
display_order = 142,
)]
pub filter_fs_meta: bool,
}
impl FilteringArgs {
pub(crate) async fn normalise(&mut self, command: &CommandArgs) -> Result<()> {
if self.ignore_nothing {
self.no_global_ignore = true;
self.no_vcs_ignore = true;
self.no_project_ignore = true;
self.no_default_ignore = true;
self.no_discover_ignore = true;
}
if self.filter_fs_meta {
self.filter_fs_events = vec![
FsEvent::Create,
FsEvent::Remove,
FsEvent::Rename,
FsEvent::Modify,
];
}
if let Some(watch_file) = self.watch_file.as_ref() {
if watch_file == Path::new("-") {
let file = tokio::io::stdin();
let mut lines = BufReader::new(file).lines();
while let Ok(Some(line)) = lines.next_line().await {
self.recursive_paths.push(line.into());
}
} else {
let file = File::open(watch_file).await.into_diagnostic()?;
let mut lines = BufReader::new(file).lines();
while let Ok(Some(line)) = lines.next_line().await {
self.recursive_paths.push(line.into());
}
};
}
let project_origin = if let Some(p) = take(&mut self.project_origin) {
p
} else {
crate::dirs::project_origin(&self, command).await?
};
debug!(path=?project_origin, "resolved project origin");
let project_origin = dunce::canonicalize(project_origin).into_diagnostic()?;
info!(path=?project_origin, "effective project origin");
self.project_origin = Some(project_origin.clone());
self.paths = take(&mut self.recursive_paths)
.into_iter()
.map(|path| {
{
if path.is_absolute() {
Ok(path)
} else {
dunce::canonicalize(project_origin.join(path)).into_diagnostic()
}
}
.map(WatchedPath::recursive)
})
.chain(take(&mut self.non_recursive_paths).into_iter().map(|path| {
{
if path.is_absolute() {
Ok(path)
} else {
dunce::canonicalize(project_origin.join(path)).into_diagnostic()
}
}
.map(WatchedPath::non_recursive)
}))
.collect::<Result<BTreeSet<_>>>()?
.into_iter()
.collect();
if self.paths.len() == 1
&& self
.paths
.first()
.map_or(false, |p| p.as_ref() == Path::new("/dev/null"))
{
info!("only path is /dev/null, not watching anything");
self.paths = Vec::new();
} else if self.paths.is_empty() {
info!("no paths, using current directory");
self.paths.push(command.workdir.as_deref().unwrap().into());
}
info!(paths=?self.paths, "effective watched paths");
for (n, prog) in self.filter_programs.iter().enumerate() {
if let Some(progpath) = prog.strip_prefix('@') {
self.filter_programs_parsed
.push(FilterProgram::new_jaq_from_file(progpath).await?);
} else {
self.filter_programs_parsed
.push(FilterProgram::new_jaq_from_arg(n, prog.clone())?);
}
}
debug_assert!(self.project_origin.is_some());
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum FsEvent {
Access,
Create,
Remove,
Rename,
Modify,
Metadata,
}
================================================
FILE: crates/cli/src/args/logging.rs
================================================
use std::{env::var, io::stderr, path::PathBuf};
use clap::{ArgAction, Parser, ValueHint};
use miette::{bail, Result};
use tokio::fs::metadata;
use tracing::{info, warn};
use tracing_appender::{non_blocking, non_blocking::WorkerGuard, rolling};
use tracing_subscriber::{EnvFilter, FmtSubscriber};
use super::OPTSET_DEBUGGING;
#[derive(Debug, Clone, Parser)]
pub struct LoggingArgs {
/// Set diagnostic log level
///
/// This enables diagnostic logging, which is useful for investigating bugs or gaining more
/// insight into faulty filters or "missing" events. Use multiple times to increase verbosity.
///
/// Goes up to '-vvvv'. When submitting bug reports, default to a '-vvv' log level.
///
/// You may want to use with '--log-file' to avoid polluting your terminal.
///
/// Setting $WATCHEXEC_LOG also works, and takes precedence, but is not recommended. However, using
/// $WATCHEXEC_LOG is the only way to get logs from before these options are parsed.
#[arg(
long,
short,
help_heading = OPTSET_DEBUGGING,
action = ArgAction::Count,
default_value = "0",
num_args = 0,
display_order = 220,
)]
pub verbose: u8,
/// Write diagnostic logs to a file
///
/// This writes diagnostic logs to a file, instead of the terminal, in JSON format. If a log
/// level was not already specified, this will set it to '-vvv'.
///
/// If a path is not provided, the default is the working directory. Note that with
/// '--ignore-nothing', the write events to the log will likely get picked up by Watchexec,
/// causing a loop; prefer setting a path outside of the watched directory.
///
/// If the path provided is a directory, a file will be created in that directory. The file name
/// will be the current date and time, in the format 'watchexec.YYYY-MM-DDTHH-MM-SSZ.log'.
#[arg(
long,
help_heading = OPTSET_DEBUGGING,
num_args = 0..=1,
default_missing_value = ".",
value_hint = ValueHint::AnyPath,
value_name = "PATH",
display_order = 120,
)]
pub log_file: Option<PathBuf>,
/// Print events that trigger actions
///
/// This prints the events that triggered the action when handling it (after debouncing), in a
/// human readable form. This is useful for debugging filters.
///
/// Use '-vvv' instead when you need more diagnostic information.
#[arg(
long,
help_heading = OPTSET_DEBUGGING,
display_order = 160,
)]
pub print_events: bool,
}
pub fn preargs() -> bool {
let mut log_on = false;
#[cfg(feature = "dev-console")]
match console_subscriber::try_init() {
Ok(_) => {
warn!("dev-console enabled");
log_on = true;
}
Err(e) => {
eprintln!("Failed to initialise tokio console, falling back to normal logging\n{e}")
}
}
if !log_on && var("WATCHEXEC_LOG").is_ok() {
let subscriber =
FmtSubscriber::builder().with_env_filter(EnvFilter::from_env("WATCHEXEC_LOG"));
match subscriber.try_init() {
Ok(()) => {
warn!(WATCHEXEC_LOG=%var("WATCHEXEC_LOG").unwrap(), "logging configured from WATCHEXEC_LOG");
log_on = true;
}
Err(e) => {
eprintln!("Failed to initialise logging with WATCHEXEC_LOG, falling back\n{e}");
}
}
}
log_on
}
pub async fn postargs(args: &LoggingArgs) -> Result<Option<WorkerGuard>> {
if args.verbose == 0 {
return Ok(None);
}
let (log_writer, guard) = if let Some(file) = &args.log_file {
let is_dir = metadata(&file).await.map_or(false, |info| info.is_dir());
let (dir, filename) = if is_dir {
(
file.to_owned(),
PathBuf::from(format!(
"watchexec.{}.log",
chrono::Utc::now().format("%Y-%m-%dT%H-%M-%SZ")
)),
)
} else if let (Some(parent), Some(file_name)) = (file.parent(), file.file_name()) {
(parent.into(), PathBuf::from(file_name))
} else {
bail!("Failed to determine log file name");
};
non_blocking(rolling::never(dir, filename))
} else {
non_blocking(stderr())
};
let mut builder = tracing_subscriber::fmt().with_env_filter(match args.verbose {
0 => unreachable!("checked by if earlier"),
1 => "warn",
2 => "info",
3 => "debug",
_ => "trace",
});
if args.verbose > 2 {
use tracing_subscriber::fmt::format::FmtSpan;
builder = builder.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE);
}
match if args.log_file.is_some() {
builder.json().with_writer(log_writer).try_init()
} else if args.verbose > 3 {
builder.pretty().with_writer(log_writer).try_init()
} else {
builder.with_writer(log_writer).try_init()
} {
Ok(()) => info!("logging initialised"),
Err(e) => eprintln!("Failed to initialise logging, continuing with none\n{e}"),
}
Ok(Some(guard))
}
================================================
FILE: crates/cli/src/args/output.rs
================================================
use clap::{Parser, ValueEnum};
use miette::Result;
use super::OPTSET_OUTPUT;
#[derive(Debug, Clone, Parser)]
pub struct OutputArgs {
/// Clear screen before running command
///
/// If this doesn't completely clear the screen, try '--clear=reset'.
#[arg(
short = 'c',
long = "clear",
help_heading = OPTSET_OUTPUT,
num_args = 0..=1,
default_missing_value = "clear",
value_name
gitextract_c7s6gsdt/
├── .cargo/
│ └── config.toml
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── feature_request.md
│ │ └── regression.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── clippy.yml
│ ├── dist-manifest.jq
│ ├── release-cli.yml
│ └── tests.yml
├── .gitignore
├── .rustfmt.toml
├── CITATION.cff
├── CONTRIBUTING.md
├── Cargo.toml
├── LICENSE
├── README.md
├── bin/
│ ├── completions
│ ├── dates.mjs
│ ├── manpage
│ └── release-notes
├── cliff.toml
├── completions/
│ ├── bash
│ ├── elvish
│ ├── fish
│ ├── nu
│ ├── powershell
│ └── zsh
├── crates/
│ ├── bosion/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── examples/
│ │ │ ├── clap/
│ │ │ │ ├── Cargo.toml
│ │ │ │ ├── build.rs
│ │ │ │ └── src/
│ │ │ │ └── main.rs
│ │ │ ├── default/
│ │ │ │ ├── Cargo.toml
│ │ │ │ ├── build.rs
│ │ │ │ └── src/
│ │ │ │ ├── common.rs
│ │ │ │ └── main.rs
│ │ │ ├── no-git/
│ │ │ │ ├── Cargo.toml
│ │ │ │ ├── build.rs
│ │ │ │ └── src/
│ │ │ │ └── main.rs
│ │ │ ├── no-std/
│ │ │ │ ├── Cargo.toml
│ │ │ │ ├── build.rs
│ │ │ │ └── src/
│ │ │ │ └── main.rs
│ │ │ └── snapshots/
│ │ │ ├── build_date.txt
│ │ │ ├── build_datetime.txt
│ │ │ ├── crate_features.txt
│ │ │ ├── crate_version.txt
│ │ │ ├── default_long_version.txt
│ │ │ ├── default_long_version_with.txt
│ │ │ ├── git_commit_date.txt
│ │ │ ├── git_commit_datetime.txt
│ │ │ ├── git_commit_hash.txt
│ │ │ ├── git_commit_shorthash.txt
│ │ │ ├── no_git_long_version.txt
│ │ │ └── no_git_long_version_with.txt
│ │ ├── release.toml
│ │ ├── run-tests.sh
│ │ └── src/
│ │ ├── info.rs
│ │ └── lib.rs
│ ├── cli/
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── build.rs
│ │ ├── integration/
│ │ │ ├── env-unix.sh
│ │ │ ├── no-shell-unix.sh
│ │ │ ├── socket.sh
│ │ │ ├── stdin-quit-unix.sh
│ │ │ └── trailingargfile-unix.sh
│ │ ├── release.toml
│ │ ├── run-tests.sh
│ │ ├── src/
│ │ │ ├── args/
│ │ │ │ ├── command.rs
│ │ │ │ ├── events.rs
│ │ │ │ ├── filtering.rs
│ │ │ │ ├── logging.rs
│ │ │ │ └── output.rs
│ │ │ ├── args.rs
│ │ │ ├── config.rs
│ │ │ ├── dirs.rs
│ │ │ ├── emits.rs
│ │ │ ├── filterer/
│ │ │ │ ├── parse.rs
│ │ │ │ ├── proglib/
│ │ │ │ │ ├── file.rs
│ │ │ │ │ ├── hash.rs
│ │ │ │ │ ├── kv.rs
│ │ │ │ │ ├── macros.rs
│ │ │ │ │ └── output.rs
│ │ │ │ ├── proglib.rs
│ │ │ │ ├── progs.rs
│ │ │ │ └── syncval.rs
│ │ │ ├── filterer.rs
│ │ │ ├── lib.rs
│ │ │ ├── main.rs
│ │ │ ├── socket/
│ │ │ │ ├── fallback.rs
│ │ │ │ ├── parser.rs
│ │ │ │ ├── test.rs
│ │ │ │ ├── unix.rs
│ │ │ │ └── windows.rs
│ │ │ ├── socket.rs
│ │ │ └── state.rs
│ │ ├── tests/
│ │ │ ├── common/
│ │ │ │ └── mod.rs
│ │ │ └── ignore.rs
│ │ ├── watchexec-manifest.rc
│ │ └── watchexec.exe.manifest
│ ├── events/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── examples/
│ │ │ └── parse-and-print.rs
│ │ ├── release.toml
│ │ ├── src/
│ │ │ ├── event.rs
│ │ │ ├── fs.rs
│ │ │ ├── keyboard.rs
│ │ │ ├── lib.rs
│ │ │ ├── process.rs
│ │ │ ├── sans_notify.rs
│ │ │ └── serde_formats.rs
│ │ └── tests/
│ │ ├── json.rs
│ │ └── snapshots/
│ │ ├── array.json
│ │ ├── asymmetric.json
│ │ ├── completions.json
│ │ ├── metadata.json
│ │ ├── paths.json
│ │ ├── signals.json
│ │ ├── single.json
│ │ └── sources.json
│ ├── filterer/
│ │ ├── globset/
│ │ │ ├── CHANGELOG.md
│ │ │ ├── Cargo.toml
│ │ │ ├── README.md
│ │ │ ├── release.toml
│ │ │ ├── src/
│ │ │ │ └── lib.rs
│ │ │ └── tests/
│ │ │ ├── filtering.rs
│ │ │ └── helpers/
│ │ │ └── mod.rs
│ │ └── ignore/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── release.toml
│ │ ├── src/
│ │ │ └── lib.rs
│ │ └── tests/
│ │ ├── filtering.rs
│ │ ├── helpers/
│ │ │ └── mod.rs
│ │ └── ignores/
│ │ ├── allowlist
│ │ ├── folders
│ │ ├── globs
│ │ ├── negate
│ │ ├── none-allowed
│ │ ├── scopes-global
│ │ ├── scopes-local
│ │ ├── scopes-sublocal
│ │ └── self.ignore
│ ├── ignore-files/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── release.toml
│ │ ├── src/
│ │ │ ├── discover.rs
│ │ │ ├── error.rs
│ │ │ ├── filter.rs
│ │ │ └── lib.rs
│ │ └── tests/
│ │ ├── filtering.rs
│ │ ├── global/
│ │ │ ├── first
│ │ │ └── second
│ │ ├── helpers/
│ │ │ └── mod.rs
│ │ └── tree/
│ │ ├── base
│ │ └── branch/
│ │ └── inner
│ ├── lib/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── examples/
│ │ │ ├── only_commands.rs
│ │ │ ├── only_events.rs
│ │ │ ├── readme.rs
│ │ │ └── restart_run_on_successful_build.rs
│ │ ├── release.toml
│ │ ├── src/
│ │ │ ├── action/
│ │ │ │ ├── handler.rs
│ │ │ │ ├── quit.rs
│ │ │ │ ├── return.rs
│ │ │ │ └── worker.rs
│ │ │ ├── action.rs
│ │ │ ├── changeable.rs
│ │ │ ├── config.rs
│ │ │ ├── error/
│ │ │ │ ├── critical.rs
│ │ │ │ ├── runtime.rs
│ │ │ │ └── specialised.rs
│ │ │ ├── error.rs
│ │ │ ├── filter.rs
│ │ │ ├── id.rs
│ │ │ ├── late_join_set.rs
│ │ │ ├── lib.rs
│ │ │ ├── paths.rs
│ │ │ ├── sources/
│ │ │ │ ├── fs.rs
│ │ │ │ ├── keyboard.rs
│ │ │ │ └── signal.rs
│ │ │ ├── sources.rs
│ │ │ ├── watched_path.rs
│ │ │ └── watchexec.rs
│ │ └── tests/
│ │ ├── env_reporting.rs
│ │ └── error_handler.rs
│ ├── project-origins/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── examples/
│ │ │ └── find-origins.rs
│ │ ├── release.toml
│ │ └── src/
│ │ └── lib.rs
│ ├── signals/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── release.toml
│ │ └── src/
│ │ └── lib.rs
│ ├── supervisor/
│ │ ├── CHANGELOG.md
│ │ ├── Cargo.toml
│ │ ├── README.md
│ │ ├── release.toml
│ │ ├── src/
│ │ │ ├── command/
│ │ │ │ ├── conversions.rs
│ │ │ │ ├── program.rs
│ │ │ │ └── shell.rs
│ │ │ ├── command.rs
│ │ │ ├── errors.rs
│ │ │ ├── flag.rs
│ │ │ ├── job/
│ │ │ │ ├── job.rs
│ │ │ │ ├── messages.rs
│ │ │ │ ├── priority.rs
│ │ │ │ ├── state.rs
│ │ │ │ ├── task.rs
│ │ │ │ ├── test.rs
│ │ │ │ └── testchild.rs
│ │ │ ├── job.rs
│ │ │ └── lib.rs
│ │ └── tests/
│ │ └── programs.rs
│ └── test-socketfd/
│ ├── Cargo.toml
│ ├── README.md
│ └── src/
│ └── main.rs
└── doc/
├── packages.md
├── socket.md
├── watchexec.1
└── watchexec.1.md
SYMBOL INDEX (690 symbols across 104 files)
FILE: crates/bosion/examples/clap/build.rs
function main (line 1) | fn main() {
FILE: crates/bosion/examples/clap/src/main.rs
type Args (line 7) | struct Args {
function main (line 21) | fn main() {
FILE: crates/bosion/examples/default/build.rs
function main (line 1) | fn main() {
FILE: crates/bosion/examples/default/src/common.rs
function git_commit_info (line 2) | pub(crate) fn git_commit_info(format: &str) -> String {
FILE: crates/bosion/examples/default/src/main.rs
function main (line 4) | fn main() {}
FILE: crates/bosion/examples/no-git/build.rs
function main (line 1) | fn main() {
FILE: crates/bosion/examples/no-git/src/main.rs
function main (line 5) | fn main() {}
FILE: crates/bosion/examples/no-std/build.rs
function main (line 1) | fn main() {
FILE: crates/bosion/examples/no-std/src/main.rs
function panic (line 9) | fn panic(_panic: &PanicInfo<'_>) -> ! {
FILE: crates/bosion/src/info.rs
type Info (line 21) | pub struct Info {
method gather (line 71) | pub fn gather() -> Result<Self, String> {
method build_date (line 91) | fn build_date() -> Result<OffsetDateTime, String> {
method features (line 103) | fn features() -> Vec<String> {
method set_reruns (line 115) | pub(crate) fn set_reruns(&self) {
type ErrString (line 48) | trait ErrString<T> {
method err_string (line 49) | fn err_string(self) -> Result<T, String>;
function err_string (line 56) | fn err_string(self) -> Result<T, String> {
constant DATE_FORMAT (line 61) | const DATE_FORMAT: &[FormatItem<'static>] = format_description!("[year]-...
constant DATETIME_FORMAT (line 62) | const DATETIME_FORMAT: &[FormatItem<'static>] =
type GitInfo (line 129) | pub struct GitInfo {
method gather (line 155) | fn gather() -> Result<Self, String> {
method find_git_dir (line 176) | fn find_git_dir(start: &Path) -> Option<PathBuf> {
method resolve_head (line 198) | fn resolve_head(git_dir: &Path) -> Option<String> {
method resolve_ref (line 212) | fn resolve_ref(git_dir: &Path, ref_path: &str) -> Option<String> {
method read_commit_timestamp (line 238) | fn read_commit_timestamp(git_dir: &Path, hash: &str) -> Option<i64> {
method read_loose_commit_timestamp (line 248) | fn read_loose_commit_timestamp(git_dir: &Path, hash: &str) -> Option<i...
method read_packed_commit_timestamp (line 263) | fn read_packed_commit_timestamp(git_dir: &Path, hash: &str) -> Option<...
method hex_to_bytes (line 287) | fn hex_to_bytes(hex: &str) -> Option<[u8; 20]> {
method find_object_in_index (line 299) | fn find_object_in_index(idx_path: &Path, hash: &[u8; 20]) -> Option<u6...
method read_pack_object (line 394) | fn read_pack_object(pack_path: &Path, offset: u64) -> Option<Vec<u8>> {
method parse_commit_timestamp (line 449) | fn parse_commit_timestamp(data: &[u8]) -> Option<i64> {
FILE: crates/bosion/src/lib.rs
function gather (line 14) | pub fn gather() {
function gather_pub (line 22) | pub fn gather_pub() {
function gather_to (line 48) | pub fn gather_to(filename: &str, structname: &str, public: bool) {
function gather_to_env (line 202) | pub fn gather_to_env() {
function gather_to_env_with_prefix (line 222) | pub fn gather_to_env_with_prefix(prefix: &str) {
FILE: crates/cli/build.rs
function main (line 1) | fn main() {
FILE: crates/cli/src/args.rs
constant OPTSET_COMMAND (line 18) | const OPTSET_COMMAND: &str = "Command";
constant OPTSET_DEBUGGING (line 19) | const OPTSET_DEBUGGING: &str = "Debugging";
constant OPTSET_EVENTS (line 20) | const OPTSET_EVENTS: &str = "Events";
constant OPTSET_FILTERING (line 21) | const OPTSET_FILTERING: &str = "Filtering";
constant OPTSET_OUTPUT (line 22) | const OPTSET_OUTPUT: &str = "Output";
type Args (line 72) | pub struct Args {
type TimeSpan (line 171) | pub struct TimeSpan<const UNITLESS_NANOS_MULTIPLIER: u64 = { 1_000_000_0...
type Err (line 174) | type Err = humantime::DurationError;
method from_str (line 176) | fn from_str(s: &str) -> Result<Self, Self::Err> {
function expand_args_up_to_doubledash (line 191) | fn expand_args_up_to_doubledash() -> Result<Vec<OsString>, std::io::Erro...
type ShellCompletion (line 234) | pub enum ShellCompletion {
type Guards (line 244) | pub struct Guards {
function get_args (line 248) | pub async fn get_args() -> Result<(Args, Guards)> {
function verify_cli (line 279) | fn verify_cli() {
FILE: crates/cli/src/args/command.rs
type CommandArgs (line 21) | pub struct CommandArgs {
method normalise (line 269) | pub(crate) async fn normalise(&mut self) -> Result<()> {
type WrapMode (line 290) | pub enum WrapMode {
constant WRAP_DEFAULT (line 297) | pub const WRAP_DEFAULT: &str = if cfg!(target_os = "macos") {
type EnvVar (line 304) | pub struct EnvVar {
type EnvVarValueParser (line 310) | pub(crate) struct EnvVarValueParser;
type Value (line 313) | type Value = EnvVar;
method parse_ref (line 315) | fn parse_ref(
FILE: crates/cli/src/args/events.rs
type EventsArgs (line 14) | pub struct EventsArgs {
method normalise (line 310) | pub(crate) fn normalise(
type EmitEvents (line 358) | pub enum EmitEvents {
type OnBusyUpdate (line 369) | pub enum OnBusyUpdate {
type SignalMapping (line 378) | pub struct SignalMapping {
type SignalMappingValueParser (line 384) | struct SignalMappingValueParser;
type Value (line 387) | type Value = SignalMapping;
method parse_ref (line 389) | fn parse_ref(
FILE: crates/cli/src/args/filtering.rs
type FilteringArgs (line 21) | pub struct FilteringArgs {
method normalise (line 389) | pub(crate) async fn normalise(&mut self, command: &CommandArgs) -> Res...
type FsEvent (line 489) | pub enum FsEvent {
FILE: crates/cli/src/args/logging.rs
type LoggingArgs (line 13) | pub struct LoggingArgs {
function preargs (line 72) | pub fn preargs() -> bool {
function postargs (line 103) | pub async fn postargs(args: &LoggingArgs) -> Result<Option<WorkerGuard>> {
FILE: crates/cli/src/args/output.rs
type OutputArgs (line 7) | pub struct OutputArgs {
method normalise (line 86) | pub(crate) fn normalise(&mut self) -> Result<()> {
type ClearMode (line 97) | pub enum ClearMode {
type ColourMode (line 104) | pub enum ColourMode {
type NotifyMode (line 111) | pub enum NotifyMode {
method on_start (line 123) | pub fn on_start(self) -> bool {
method on_end (line 128) | pub fn on_end(self) -> bool {
FILE: crates/cli/src/config.rs
type OutputFlags (line 48) | struct OutputFlags {
type TimeoutConfig (line 57) | struct TimeoutConfig {
function make_config (line 66) | pub fn make_config(args: &Args, state: &State) -> Result<Config> {
function interpret_command_args (line 647) | fn interpret_command_args(args: &Args) -> Result<Arc<Command>> {
function setup_process (line 724) | fn setup_process(
function format_duration (line 817) | fn format_duration(duration: Duration) -> impl fmt::Display {
function end_of_process (line 829) | fn end_of_process(
function emit_events_to_command (line 930) | fn emit_events_to_command(
function reset_screen (line 1004) | pub fn reset_screen() {
FILE: crates/cli/src/dirs.rs
function project_origin (line 15) | pub async fn project_origin(
function vcs_types (line 76) | pub async fn vcs_types(origin: &Path) -> Vec<ProjectType> {
function ignores (line 86) | pub async fn ignores(args: &Args, vcs_types: &[ProjectType]) -> Result<V...
FILE: crates/cli/src/emits.rs
function emits_to_environment (line 9) | pub fn emits_to_environment(events: &[Event]) -> impl Iterator<Item = En...
function events_to_simple_format (line 18) | pub fn events_to_simple_format(events: &[Event]) -> Result<String> {
function emits_to_file (line 57) | pub fn emits_to_file(target: &RotatingTempFile, events: &[Event]) -> Res...
function emits_to_json_file (line 63) | pub fn emits_to_json_file(target: &RotatingTempFile, events: &[Event]) -...
FILE: crates/cli/src/filterer.rs
type WatchexecFilterer (line 26) | pub struct WatchexecFilterer {
method new (line 72) | pub async fn new(args: &Args) -> Result<Arc<Self>> {
method check_event (line 34) | fn check_event(&self, event: &Event, priority: Priority) -> Result<bool,...
function read_filter_file (line 161) | async fn read_filter_file(path: &Path) -> Result<Vec<(String, Option<Pat...
FILE: crates/cli/src/filterer/parse.rs
type FilterProgram (line 16) | pub enum FilterProgram {
method new_jaq_from_file (line 29) | pub(crate) async fn new_jaq_from_file(path: impl Into<PathBuf>) -> Res...
method new_jaq_from_arg (line 45) | pub(crate) fn new_jaq_from_arg(n: usize, arg: String) -> Result<Self> {
method new_jaq (line 51) | fn new_jaq(path: PathBuf, code: String) -> Result<Self> {
method run (line 78) | pub(crate) fn run(&self, event: &Event) -> Result<bool> {
method fmt (line 21) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
FILE: crates/cli/src/filterer/proglib.rs
function jaq_lib (line 9) | pub fn jaq_lib<'s>() -> Compiler<&'s str, Native<jaq_json::Val>> {
FILE: crates/cli/src/filterer/proglib/file.rs
function funs (line 16) | pub fn funs() -> [Filter<Native<jaq_json::Val>>; 3] {
function json_meta (line 106) | fn json_meta(meta: Metadata) -> Value {
function filetype_str (line 139) | fn filetype_str(filetype: FileType) -> &'static str {
function fs_time (line 175) | fn fs_time(time: std::io::Result<SystemTime>) -> Option<u64> {
FILE: crates/cli/src/filterer/proglib/hash.rs
function funs (line 10) | pub fn funs() -> [Filter<Native<jaq_json::Val>>; 2] {
FILE: crates/cli/src/filterer/proglib/kv.rs
type KvStore (line 13) | type KvStore = Arc<DashMap<String, SyncVal>>;
function kv_store (line 14) | fn kv_store() -> KvStore {
function funs (line 19) | pub fn funs() -> [Filter<Native<jaq_json::Val>>; 3] {
FILE: crates/cli/src/filterer/proglib/output.rs
function funs (line 23) | pub fn funs() -> [Filter<Native<jaq_json::Val>>; 3] {
FILE: crates/cli/src/filterer/progs.rs
constant BUFFER (line 14) | const BUFFER: usize = 128;
type FilterProgs (line 17) | pub struct FilterProgs {
method check (line 59) | pub fn check(&self, event: &Event) -> Result<bool, RuntimeError> {
method new (line 63) | pub fn new(args: &Args) -> miette::Result<Self> {
type Requester (line 22) | pub struct Requester<S, R> {
function new (line 32) | pub fn new(capacity: usize) -> (Self, mpsc::Receiver<(S, oneshot::Sender...
function call (line 43) | pub fn call(&self, value: S) -> Result<R, RuntimeError> {
FILE: crates/cli/src/filterer/syncval.rs
type SyncVal (line 8) | pub enum SyncVal {
method from (line 20) | fn from(val: &Val) -> Self {
method from (line 47) | fn from(val: &SyncVal) -> Self {
FILE: crates/cli/src/lib.rs
function run_watchexec (line 32) | async fn run_watchexec(args: Args, state: state::State) -> Result<()> {
function run_manpage (line 71) | async fn run_manpage() -> Result<()> {
function run_completions (line 116) | async fn run_completions(shell: ShellCompletion) -> Result<()> {
function run (line 136) | pub async fn run() -> Result<ExitCode> {
FILE: crates/cli/src/main.rs
function main (line 12) | fn main() -> miette::Result<ExitCode> {
FILE: crates/cli/src/socket.rs
type SocketType (line 28) | pub enum SocketType {
type SocketSpec (line 35) | pub struct SocketSpec {
type Sockets (line 40) | pub(crate) trait Sockets
method create (line 44) | async fn create(specs: &[SocketSpec]) -> Result<Self>;
method envs (line 45) | fn envs(&self) -> Vec<EnvVar>;
method serve (line 46) | fn serve(&mut self) {}
FILE: crates/cli/src/socket/fallback.rs
type SocketSet (line 8) | pub struct SocketSet;
method create (line 11) | async fn create(_: &[SocketSpec]) -> Result<Self> {
method envs (line 15) | fn envs(&self) -> Vec<EnvVar> {
FILE: crates/cli/src/socket/parser.rs
type SocketSpecValueParser (line 17) | pub(crate) struct SocketSpecValueParser;
type Value (line 20) | type Value = SocketSpec;
method parse_ref (line 22) | fn parse_ref(
FILE: crates/cli/src/socket/test.rs
function parse_port_only (line 11) | fn parse_port_only() {
function parse_addr_port_v4 (line 25) | fn parse_addr_port_v4() {
function parse_addr_port_v6 (line 39) | fn parse_addr_port_v6() {
function parse_port_only_explicit_tcp (line 58) | fn parse_port_only_explicit_tcp() {
function parse_addr_port_v4_explicit_tcp (line 72) | fn parse_addr_port_v4_explicit_tcp() {
function parse_addr_port_v6_explicit_tcp (line 86) | fn parse_addr_port_v6_explicit_tcp() {
function parse_port_only_explicit_udp (line 105) | fn parse_port_only_explicit_udp() {
function parse_addr_port_v4_explicit_udp (line 119) | fn parse_addr_port_v4_explicit_udp() {
function parse_addr_port_v6_explicit_udp (line 133) | fn parse_addr_port_v6_explicit_udp() {
function parse_bad_prefix (line 152) | fn parse_bad_prefix() {
function parse_bad_port_zero (line 164) | fn parse_bad_port_zero() {
function parse_bad_port_high (line 176) | fn parse_bad_port_high() {
function parse_bad_port_alpha (line 188) | fn parse_bad_port_alpha() {
FILE: crates/cli/src/socket/unix.rs
type SocketSet (line 15) | pub struct SocketSet {
method create (line 21) | async fn create(specs: &[SocketSpec]) -> Result<Self> {
method envs (line 31) | fn envs(&self) -> Vec<EnvVar> {
method create (line 46) | fn create(&self) -> Result<OwnedFd> {
FILE: crates/cli/src/socket/windows.rs
type SocketSet (line 24) | pub struct SocketSet {
method create (line 33) | async fn create(specs: &[SocketSpec]) -> Result<Self> {
method envs (line 52) | fn envs(&self) -> Vec<EnvVar> {
method serve (line 66) | fn serve(&mut self) {
function provide_sockets (line 82) | async fn provide_sockets(
function socket_to_payload (line 117) | fn socket_to_payload(socket: &OwnedSocket, pid: u32) -> std::io::Result<...
method create (line 141) | fn create(&self) -> Result<OwnedSocket> {
FILE: crates/cli/src/state.rs
type State (line 19) | pub type State = Arc<InnerState>;
function new (line 21) | pub async fn new(args: &Args) -> Result<State> {
type InnerState (line 39) | pub struct InnerState {
type RotatingTempFile (line 49) | pub struct RotatingTempFile(Mutex<Option<NamedTempFile>>);
method rotate (line 52) | pub fn rotate(&self) -> Result<()> {
method write (line 65) | pub fn write(&self, data: &[u8]) -> Result<()> {
method path (line 73) | pub fn path(&self) -> PathBuf {
FILE: crates/cli/tests/common/mod.rs
function get_placeholder_data (line 8) | fn get_placeholder_data() -> &'static str {
type GeneratedFileNesting (line 14) | pub enum GeneratedFileNesting {
type TestSubfolderConfiguration (line 23) | pub struct TestSubfolderConfiguration {
type GenerateTestFilesArgs (line 36) | pub struct GenerateTestFilesArgs {
function generate_test_files (line 48) | pub fn generate_test_files(args: GenerateTestFilesArgs) -> Result<Vec<Pa...
FILE: crates/cli/tests/ignore.rs
constant WATCH_DIR_NAME (line 18) | const WATCH_DIR_NAME: &str = "watch";
constant WATCH_TOKEN (line 21) | const WATCH_TOKEN: &str = "updated";
function e2e_ignore_many_files_200_000 (line 39) | async fn e2e_ignore_many_files_200_000() -> Result<()> {
function run_watchexec_cmd (line 126) | async fn run_watchexec_cmd(
FILE: crates/events/examples/parse-and-print.rs
function main (line 4) | fn main() -> Result<()> {
FILE: crates/events/src/event.rs
type Event (line 18) | pub struct Event {
method is_internal (line 170) | pub fn is_internal(&self) -> bool {
method is_empty (line 178) | pub fn is_empty(&self) -> bool {
method paths (line 183) | pub fn paths(&self) -> impl Iterator<Item = (&Path, Option<&FileType>)> {
method signals (line 191) | pub fn signals(&self) -> impl Iterator<Item = Signal> + '_ {
method completions (line 199) | pub fn completions(&self) -> impl Iterator<Item = Option<ProcessEnd>> ...
method fmt (line 208) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type Tag (line 31) | pub enum Tag {
method discriminant_name (line 67) | pub const fn discriminant_name(&self) -> &'static str {
type Source (line 89) | pub enum Source {
method fmt (line 110) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type Priority (line 135) | pub enum Priority {
method default (line 162) | fn default() -> Self {
FILE: crates/events/src/fs.rs
type FileType (line 28) | pub enum FileType {
method from (line 43) | fn from(ft: std::fs::FileType) -> Self {
method fmt (line 57) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
FILE: crates/events/src/keyboard.rs
type Keyboard (line 6) | pub enum Keyboard {
type KeyCode (line 29) | pub enum KeyCode {
type Modifiers (line 41) | pub struct Modifiers {
method is_empty (line 61) | pub fn is_empty(&self) -> bool {
function is_false (line 54) | fn is_false(b: &bool) -> bool {
FILE: crates/events/src/process.rs
type ProcessEnd (line 25) | pub enum ProcessEnd {
method from (line 53) | fn from(es: ExitStatus) -> Self {
method from (line 74) | fn from(es: ExitStatus) -> Self {
method from (line 83) | fn from(es: ExitStatus) -> Self {
method into_exitstatus (line 101) | pub fn into_exitstatus(self) -> ExitStatus {
method into_exitstatus (line 122) | pub fn into_exitstatus(self) -> ExitStatus {
method into_exitstatus (line 134) | pub fn into_exitstatus(self) -> ExitStatus {
FILE: crates/events/src/sans_notify.rs
type AccessMode (line 15) | pub enum AccessMode {
type AccessKind (line 37) | pub enum AccessKind {
type CreateKind (line 59) | pub enum CreateKind {
type DataChange (line 77) | pub enum DataChange {
type MetadataKind (line 95) | pub enum MetadataKind {
type RenameMode (line 125) | pub enum RenameMode {
type ModifyKind (line 150) | pub enum ModifyKind {
type RemoveKind (line 173) | pub enum RemoveKind {
type EventKind (line 195) | pub enum EventKind {
method is_access (line 244) | pub fn is_access(&self) -> bool {
method is_create (line 249) | pub fn is_create(&self) -> bool {
method is_modify (line 254) | pub fn is_modify(&self) -> bool {
method is_remove (line 259) | pub fn is_remove(&self) -> bool {
method is_other (line 264) | pub fn is_other(&self) -> bool {
method default (line 270) | fn default() -> Self {
FILE: crates/events/src/serde_formats.rs
type SerdeTag (line 19) | pub struct SerdeTag {
method from (line 106) | fn from(value: Tag) -> Self {
type TagKind (line 59) | pub enum TagKind {
type ProcessDisposition (line 73) | pub enum ProcessDisposition {
type FsEventKind (line 85) | pub enum FsEventKind {
method from (line 94) | fn from(value: EventKind) -> Self {
method from (line 182) | fn from(value: SerdeTag) -> Self {
type SerdeEvent (line 344) | pub struct SerdeEvent {
method from (line 354) | fn from(Event { tags, metadata }: Event) -> Self {
method from (line 363) | fn from(SerdeEvent { tags, metadata }: SerdeEvent) -> Self {
FILE: crates/events/tests/json.rs
function parse_file (line 12) | fn parse_file(path: &str) -> Vec<Event> {
function single (line 17) | fn single() {
function array (line 38) | fn array() {
function metadata (line 66) | fn metadata() {
function asymmetric (line 85) | fn asymmetric() {
function sources (line 119) | fn sources() {
function signals (line 148) | fn signals() {
function completions (line 176) | fn completions() {
function paths (line 207) | fn paths() {
FILE: crates/filterer/globset/src/lib.rs
type GlobsetFilterer (line 26) | pub struct GlobsetFilterer {
method fmt (line 38) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
method new (line 66) | pub async fn new(
method check_event (line 133) | fn check_event(&self, event: &Event, priority: Priority) -> Result<bool,...
FILE: crates/filterer/globset/tests/filtering.rs
function empty_filter_passes_everything (line 6) | async fn empty_filter_passes_everything() {
function exact_filename (line 26) | async fn exact_filename() {
function exact_filename_in_folder (line 39) | async fn exact_filename_in_folder() {
function exact_filename_in_hidden_folder (line 53) | async fn exact_filename_in_hidden_folder() {
function exact_filenames_multiple (line 67) | async fn exact_filenames_multiple() {
function glob_single_final_ext_star (line 84) | async fn glob_single_final_ext_star() {
function glob_star_trailing_slash (line 96) | async fn glob_star_trailing_slash() {
function glob_star_leading_slash (line 109) | async fn glob_star_leading_slash() {
function glob_leading_double_star (line 121) | async fn glob_leading_double_star() {
function glob_trailing_double_star (line 136) | async fn glob_trailing_double_star() {
function glob_middle_double_star (line 150) | async fn glob_middle_double_star() {
function glob_double_star_trailing_slash (line 165) | async fn glob_double_star_trailing_slash() {
function ignore_exact_filename (line 183) | async fn ignore_exact_filename() {
function ignore_exact_filename_in_folder (line 196) | async fn ignore_exact_filename_in_folder() {
function ignore_exact_filename_in_hidden_folder (line 210) | async fn ignore_exact_filename_in_hidden_folder() {
function ignore_exact_filenames_multiple (line 224) | async fn ignore_exact_filenames_multiple() {
function ignore_glob_single_final_ext_star (line 241) | async fn ignore_glob_single_final_ext_star() {
function ignore_glob_star_trailing_slash (line 253) | async fn ignore_glob_star_trailing_slash() {
function ignore_glob_star_leading_slash (line 266) | async fn ignore_glob_star_leading_slash() {
function ignore_glob_leading_double_star (line 278) | async fn ignore_glob_leading_double_star() {
function ignore_glob_trailing_double_star (line 293) | async fn ignore_glob_trailing_double_star() {
function ignore_glob_middle_double_star (line 312) | async fn ignore_glob_middle_double_star() {
function ignore_glob_double_star_trailing_slash (line 327) | async fn ignore_glob_double_star_trailing_slash() {
function ignores_take_precedence (line 345) | async fn ignores_take_precedence() {
function extensions_fail_dirs (line 365) | async fn extensions_fail_dirs() {
function extensions_fail_extensionless (line 376) | async fn extensions_fail_extensionless() {
function multipath_allow_on_any_one_pass (line 384) | async fn multipath_allow_on_any_one_pass() {
function extensions_and_filters_glob (line 413) | async fn extensions_and_filters_glob() {
function extensions_and_filters_slash (line 427) | async fn extensions_and_filters_slash() {
function leading_single_glob_file (line 437) | async fn leading_single_glob_file() {
function nonpath_event_passes (line 450) | async fn nonpath_event_passes() {
function ignore_folder_incorrectly_with_bare_match (line 480) | async fn ignore_folder_incorrectly_with_bare_match() {
function ignore_folder_incorrectly_with_bare_and_leading_slash (line 511) | async fn ignore_folder_incorrectly_with_bare_and_leading_slash() {
function ignore_folder_incorrectly_with_bare_and_trailing_slash (line 542) | async fn ignore_folder_incorrectly_with_bare_and_trailing_slash() {
function ignore_folder_incorrectly_with_only_double_double_glob (line 573) | async fn ignore_folder_incorrectly_with_only_double_double_glob() {
function ignore_folder_correctly_with_double_and_double_double_globs (line 604) | async fn ignore_folder_correctly_with_double_and_double_double_globs() {
function whitelist_overrides_ignore (line 633) | async fn whitelist_overrides_ignore() {
function whitelist_overrides_ignore_files (line 651) | async fn whitelist_overrides_ignore_files() {
function whitelist_overrides_ignore_files_nested (line 682) | async fn whitelist_overrides_ignore_files_nested() {
FILE: crates/filterer/globset/tests/helpers/mod.rs
type PathHarness (line 18) | pub trait PathHarness: Filterer {
method check_path (line 19) | fn check_path(
method path_pass (line 32) | fn path_pass(&self, path: &str, file_type: Option<FileType>, pass: boo...
method file_does_pass (line 60) | fn file_does_pass(&self, path: &str) {
method file_doesnt_pass (line 64) | fn file_doesnt_pass(&self, path: &str) {
method dir_does_pass (line 68) | fn dir_does_pass(&self, path: &str) {
method dir_doesnt_pass (line 72) | fn dir_doesnt_pass(&self, path: &str) {
method unk_does_pass (line 76) | fn unk_does_pass(&self, path: &str) {
method unk_doesnt_pass (line 80) | fn unk_doesnt_pass(&self, path: &str) {
function tracing_init (line 88) | fn tracing_init() {
function globset_filt (line 103) | pub async fn globset_filt(
FILE: crates/filterer/ignore/src/lib.rs
type IgnoreFilterer (line 24) | pub struct IgnoreFilterer(pub IgnoreFilter);
method check_event (line 31) | fn check_event(&self, event: &Event, _priority: Priority) -> Result<bool...
FILE: crates/filterer/ignore/tests/filtering.rs
function folders (line 8) | async fn folders() {
function folders_suite (line 32) | fn folders_suite(filterer: &IgnoreFilterer, name: &str) {
function globs (line 63) | async fn globs() {
function negate (line 184) | async fn negate() {
function allowlist (line 193) | async fn allowlist() {
function scopes (line 214) | async fn scopes() {
function self_ignored (line 253) | async fn self_ignored() {
function add_globs_without_any_ignore_file (line 261) | async fn add_globs_without_any_ignore_file() {
function add_globs_to_existing_ignore_file (line 274) | async fn add_globs_to_existing_ignore_file() {
function add_ignore_file_without_any_preexisting_ignore_file (line 290) | async fn add_ignore_file_without_any_preexisting_ignore_file() {
function add_ignore_file_to_existing_ignore_file (line 302) | async fn add_ignore_file_to_existing_ignore_file() {
FILE: crates/filterer/ignore/tests/helpers/mod.rs
type PathHarness (line 15) | pub trait PathHarness: Filterer {
method check_path (line 16) | fn check_path(
method path_pass (line 29) | fn path_pass(&self, path: &str, file_type: Option<FileType>, pass: boo...
method file_does_pass (line 57) | fn file_does_pass(&self, path: &str) {
method file_doesnt_pass (line 61) | fn file_doesnt_pass(&self, path: &str) {
method dir_does_pass (line 65) | fn dir_does_pass(&self, path: &str) {
method dir_doesnt_pass (line 69) | fn dir_doesnt_pass(&self, path: &str) {
method unk_does_pass (line 73) | fn unk_does_pass(&self, path: &str) {
method unk_doesnt_pass (line 77) | fn unk_doesnt_pass(&self, path: &str) {
function tracing_init (line 84) | fn tracing_init() {
function ignore_filt (line 99) | pub async fn ignore_filt(origin: &str, ignore_files: &[IgnoreFile]) -> I...
function ig_file (line 109) | pub fn ig_file(name: &str) -> IgnoreFile {
type Applies (line 119) | pub trait Applies {
method applies_globally (line 120) | fn applies_globally(self) -> Self;
method applies_in (line 121) | fn applies_in(self, origin: &str) -> Self;
method applies_globally (line 125) | fn applies_globally(mut self) -> Self {
method applies_in (line 130) | fn applies_in(mut self, origin: &str) -> Self {
FILE: crates/ignore-files/src/discover.rs
type IgnoreFilesFromOriginArgs (line 21) | pub struct IgnoreFilesFromOriginArgs {
method check (line 42) | pub fn check(&self) -> Result<()> {
method canonicalise (line 62) | pub async fn canonicalise(self) -> std::io::Result<Self> {
method new (line 73) | pub fn new(
method new_unchecked (line 91) | pub fn new_unchecked(
method from (line 105) | fn from(path: &Path) -> Self {
function from_origin (line 156) | pub async fn from_origin(
function from_environment (line 329) | pub async fn from_environment(appname: Option<&str>) -> (Vec<IgnoreFile>...
function discover_file (line 444) | pub async fn discover_file(
function find_file (line 473) | async fn find_file(path: PathBuf) -> Result<Option<PathBuf>, Error> {
type DirTourist (line 483) | struct DirTourist {
method new (line 500) | pub async fn new(
method next (line 537) | pub async fn next(&mut self) -> Visit {
method visit_path (line 547) | async fn visit_path(&mut self, path: PathBuf) -> Visit {
method skip (line 628) | pub fn skip(&mut self, path: PathBuf) {
method add_last_file_to_filter (line 634) | pub(crate) async fn add_last_file_to_filter(
method must_skip (line 646) | fn must_skip(&self, mut path: &Path) -> bool {
type Visit (line 493) | enum Visit {
FILE: crates/ignore-files/src/error.rs
type Error (line 8) | pub enum Error {
FILE: crates/ignore-files/src/filter.rs
type Ignore (line 16) | struct Ignore {
method fmt (line 23) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type IgnoreFilter (line 37) | pub struct IgnoreFilter {
method empty (line 46) | pub fn empty(origin: impl AsRef<Path>) -> Self {
method new (line 68) | pub async fn new(origin: impl AsRef<Path> + Send, files: &[IgnoreFile]...
method num_ignores (line 180) | pub fn num_ignores(&self) -> (u64, u64) {
method finish (line 191) | pub fn finish(&mut self) {
method add_file (line 203) | pub async fn add_file(&mut self, file: &IgnoreFile) -> Result<(), Erro...
method recompile (line 253) | fn recompile(&mut self, file: &IgnoreFile) -> Result<(), Error> {
method add_globs (line 295) | pub fn add_globs(&mut self, globs: &[&str], applies_in: Option<&PathBu...
method match_path (line 340) | pub fn match_path(&self, path: &Path, is_dir: bool) -> Match<&Glob> {
method check_dir (line 393) | pub fn check_dir(&self, path: &Path) -> bool {
function get_applies_in_path (line 419) | fn get_applies_in_path(origin: &Path, ignore_file: &IgnoreFile) -> PathB...
function prefix (line 430) | fn prefix<T: AsRef<Path>>(path: T) -> String {
function handle_relative_paths (line 450) | async fn handle_relative_paths() {
FILE: crates/ignore-files/src/lib.rs
type IgnoreFile (line 33) | pub struct IgnoreFile {
function simplify_path (line 44) | pub(crate) fn simplify_path(path: &Path) -> PathBuf {
FILE: crates/ignore-files/tests/filtering.rs
function globals (line 6) | async fn globals() {
function tree (line 25) | async fn tree() {
FILE: crates/ignore-files/tests/helpers/mod.rs
function drive_root (line 15) | fn drive_root() -> String {
function normalize_path (line 32) | fn normalize_path(path: &str) -> PathBuf {
type PathHarness (line 47) | pub trait PathHarness {
method check_path (line 48) | fn check_path(&self, path: &Path, is_dir: bool) -> Match<&Glob>;
method path_pass (line 50) | fn path_pass(&self, path: &str, is_dir: bool, pass: bool) {
method file_does_pass (line 84) | fn file_does_pass(&self, path: &str) {
method file_doesnt_pass (line 88) | fn file_doesnt_pass(&self, path: &str) {
method dir_does_pass (line 92) | fn dir_does_pass(&self, path: &str) {
method dir_doesnt_pass (line 96) | fn dir_doesnt_pass(&self, path: &str) {
method agnostic_pass (line 100) | fn agnostic_pass(&self, path: &str) {
method agnostic_fail (line 105) | fn agnostic_fail(&self, path: &str) {
method check_path (line 112) | fn check_path(&self, path: &Path, is_dir: bool) -> Match<&Glob> {
function tracing_init (line 117) | fn tracing_init() {
function ignore_filt (line 132) | pub async fn ignore_filt(origin: &str, ignore_files: &[IgnoreFile]) -> I...
function ig_file (line 140) | pub fn ig_file(name: &str) -> IgnoreFile {
type Applies (line 150) | pub trait Applies {
method applies_globally (line 151) | fn applies_globally(self) -> Self;
method applies_globally (line 155) | fn applies_globally(mut self) -> Self {
FILE: crates/lib/examples/only_commands.rs
function main (line 15) | async fn main() -> Result<()> {
FILE: crates/lib/examples/only_events.rs
function main (line 5) | async fn main() -> Result<()> {
FILE: crates/lib/examples/readme.rs
function main (line 16) | async fn main() -> Result<()> {
FILE: crates/lib/examples/restart_run_on_successful_build.rs
function main (line 13) | async fn main() -> Result<()> {
FILE: crates/lib/src/action/handler.rs
type Handler (line 52) | pub struct Handler {
method new (line 61) | pub(crate) fn new(events: Arc<[Event]>, jobs: HashMap<Id, Job>) -> Self {
method create_job (line 74) | pub fn create_job(&mut self, command: Arc<Command>) -> (Id, Job) {
method create_job_with_id (line 82) | fn create_job_with_id(&mut self, id: Id, command: Arc<Command>) -> Job {
method get_or_create_job (line 93) | pub fn get_or_create_job(&mut self, id: Id, command: impl Fn() -> Arc<...
method get_job (line 102) | pub fn get_job(&self, id: Id) -> Option<Job> {
method list_jobs (line 110) | pub fn list_jobs(&self) -> impl Iterator<Item = (Id, Job)> + '_ {
method quit (line 121) | pub fn quit(&mut self) {
method quit_gracefully (line 139) | pub fn quit_gracefully(&mut self, signal: Signal, grace: Duration) {
method signals (line 144) | pub fn signals(&self) -> impl Iterator<Item = Signal> + '_ {
method paths (line 154) | pub fn paths(&self) -> impl Iterator<Item = (&Path, Option<&FileType>)...
method completions (line 159) | pub fn completions(&self) -> impl Iterator<Item = Option<ProcessEnd>> ...
FILE: crates/lib/src/action/quit.rs
type QuitManner (line 6) | pub enum QuitManner {
FILE: crates/lib/src/action/return.rs
type ActionReturn (line 12) | pub enum ActionReturn {
FILE: crates/lib/src/action/worker.rs
function worker (line 29) | pub async fn worker(
function throttle_collect (line 113) | pub async fn throttle_collect(
FILE: crates/lib/src/changeable.rs
type Changeable (line 20) | pub struct Changeable<T>(Arc<RwLock<T>>);
function new (line 29) | pub fn new(value: T) -> Self {
function replace (line 36) | pub fn replace(&self, new: T) {
function get (line 44) | pub fn get(&self) -> T {
method default (line 53) | fn default() -> Self {
function fmt (line 60) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type ChangeableFn (line 73) | pub struct ChangeableFn<T, U>(Changeable<Arc<dyn (Fn(T) -> U) + Send + S...
function new (line 79) | pub(crate) fn new(f: impl (Fn(T) -> U) + Send + Sync + 'static) -> Self {
function replace (line 86) | pub fn replace(&self, new: impl (Fn(T) -> U) + Send + Sync + 'static) {
function call (line 93) | pub fn call(&self, data: T) -> U {
method clone (line 100) | fn clone(&self) -> Self {
method default (line 110) | fn default() -> Self {
function fmt (line 116) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
FILE: crates/lib/src/config.rs
type Config (line 32) | pub struct Config {
method signal_change (line 191) | pub fn signal_change(&self) -> &Self {
method watch (line 201) | pub(crate) fn watch(&self) -> ConfigWatched {
method fs_ready (line 212) | pub fn fs_ready(&self) -> watch::Receiver<()> {
method pathset (line 217) | pub fn pathset<I, P>(&self, pathset: I) -> &Self
method file_watcher (line 229) | pub fn file_watcher(&self, watcher: Watcher) -> &Self {
method keyboard_events (line 236) | pub fn keyboard_events(&self, enable: bool) -> &Self {
method throttle (line 243) | pub fn throttle(&self, throttle: impl Into<Duration>) -> &Self {
method filterer (line 251) | pub fn filterer(&self, filterer: impl Filterer + 'static) -> &Self {
method on_error (line 258) | pub fn on_error(&self, handler: impl Fn(ErrorHook) + Send + Sync + 'st...
method on_action (line 265) | pub fn on_action(
method on_action_async (line 276) | pub fn on_action_async(
method default (line 165) | fn default() -> Self {
type ConfigWatched (line 291) | pub(crate) struct ConfigWatched {
method new (line 297) | fn new(notify: Arc<Notify>) -> Self {
method next (line 307) | pub async fn next(&mut self) {
FILE: crates/lib/src/error/critical.rs
type CriticalError (line 12) | pub enum CriticalError {
FILE: crates/lib/src/error/runtime.rs
type RuntimeError (line 56) | pub enum RuntimeError {
FILE: crates/lib/src/error/specialised.rs
type FsWatcherError (line 9) | pub enum FsWatcherError {
type KeyboardWatcherError (line 69) | pub enum KeyboardWatcherError {
FILE: crates/lib/src/filter.rs
type Filterer (line 10) | pub trait Filterer: std::fmt::Debug + Send + Sync {
method check_event (line 21) | fn check_event(&self, event: &Event, priority: Priority) -> Result<boo...
method check_event (line 25) | fn check_event(&self, _event: &Event, _priority: Priority) -> Result<b...
method check_event (line 31) | fn check_event(&self, event: &Event, priority: Priority) -> Result<boo...
method check_event (line 50) | fn check_event(&self, event: &Event, priority: Priority) -> Result<boo...
type ChangeableFilterer (line 39) | pub struct ChangeableFilterer(Changeable<Arc<dyn Filterer>>);
method replace (line 44) | pub fn replace(&self, new: impl Filterer + 'static) {
method fmt (line 69) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method clone (line 57) | fn clone(&self) -> Self {
method default (line 63) | fn default() -> Self {
FILE: crates/lib/src/id.rs
type Id (line 6) | pub struct Id {
method default (line 16) | fn default() -> Self {
function threadid (line 27) | fn threadid() -> NonZeroU64 {
function test_threadid (line 60) | fn test_threadid() {
FILE: crates/lib/src/late_join_set.rs
type LateJoinSet (line 53) | pub struct LateJoinSet {
method spawn (line 67) | pub fn spawn(&self, task: impl Future<Output = ()> + Send + 'static) {
method insert (line 72) | pub fn insert(&self, task: JoinHandle<()>) {
method join_next (line 79) | pub async fn join_next(&mut self) -> Option<Result<(), JoinError>> {
method join_all (line 86) | pub async fn join_all(&mut self) {
method abort_all (line 95) | pub fn abort_all(&self) {
method drop (line 101) | fn drop(&mut self) {
FILE: crates/lib/src/lib.rs
type Readme (line 91) | pub struct Readme;
FILE: crates/lib/src/paths.rs
constant PATH_SEPARATOR (line 13) | pub const PATH_SEPARATOR: &str = ":";
constant PATH_SEPARATOR (line 16) | pub const PATH_SEPARATOR: &str = ";";
function common_prefix (line 23) | pub fn common_prefix<I, P>(paths: I) -> Option<PathBuf>
function summarise_events_to_env (line 74) | pub fn summarise_events_to_env<'events>(
FILE: crates/lib/src/sources/fs.rs
type Watcher (line 31) | pub enum Watcher {
method create (line 44) | fn create(
function worker (line 108) | pub async fn worker(
function notify_multi_path_errors (line 226) | fn notify_multi_path_errors(
function process_event (line 260) | fn process_event(
FILE: crates/lib/src/sources/keyboard.rs
function worker (line 24) | pub async fn worker(
type RawModeGuard (line 63) | pub struct RawModeGuard {
method enter (line 70) | pub fn enter() -> Option<Self> {
method enter (line 129) | pub fn enter() -> Option<Self> {
method drop (line 101) | fn drop(&mut self) {
type RawModeGuard (line 119) | pub struct RawModeGuard {
method enter (line 70) | pub fn enter() -> Option<Self> {
method enter (line 129) | pub fn enter() -> Option<Self> {
method drop (line 157) | fn drop(&mut self) {
function byte_to_keyboard (line 166) | fn byte_to_keyboard(byte: u8) -> Option<Keyboard> {
function watch_stdin (line 195) | async fn watch_stdin(
function send_event (line 276) | async fn send_event(
FILE: crates/lib/src/sources/signal.rs
function worker (line 39) | pub async fn worker(
function imp_worker (line 48) | async fn imp_worker(
function imp_worker (line 89) | async fn imp_worker(
function send_event (line 121) | async fn send_event(
FILE: crates/lib/src/watched_path.rs
type WatchedPath (line 7) | pub struct WatchedPath {
method from (line 13) | fn from(path: PathBuf) -> Self {
method from (line 22) | fn from(path: &str) -> Self {
method from (line 31) | fn from(path: String) -> Self {
method from (line 40) | fn from(path: &Path) -> Self {
method as_ref (line 61) | fn as_ref(&self) -> &Path {
method recursive (line 68) | pub fn recursive(path: impl Into<PathBuf>) -> Self {
method non_recursive (line 76) | pub fn non_recursive(path: impl Into<PathBuf>) -> Self {
method from (line 49) | fn from(path: WatchedPath) -> Self {
method from (line 55) | fn from(path: &WatchedPath) -> Self {
FILE: crates/lib/src/watchexec.rs
type Watchexec (line 34) | pub struct Watchexec {
method fmt (line 95) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method new (line 110) | pub fn new(
method new_async (line 121) | pub fn new_async(
method with_config (line 138) | pub fn with_config(config: Config) -> Result<Self, CriticalError> {
method send_event (line 211) | pub async fn send_event(&self, event: Event, priority: Priority) -> Re...
method main (line 222) | pub fn main(&self) -> JoinHandle<Result<(), CriticalError>> {
method default (line 89) | fn default() -> Self {
function error_hook (line 233) | async fn error_hook(
type ErrorHook (line 263) | pub struct ErrorHook {
method new (line 270) | fn new(error: RuntimeError) -> Self {
method handle_crit (line 277) | fn handle_crit(crit: Arc<OnceLock<CriticalError>>) -> Result<(), Criti...
method critical (line 297) | pub fn critical(self, critical: CriticalError) {
method elevate (line 304) | pub fn elevate(self) {
FILE: crates/lib/tests/env_reporting.rs
constant ENV_SEP (line 8) | const ENV_SEP: &str = ":";
constant ENV_SEP (line 10) | const ENV_SEP: &str = ";";
function ospath (line 12) | fn ospath(path: &str) -> OsString {
function event (line 22) | fn event(path: &str, kind: FileEventKind) -> Event {
function no_events_no_env (line 36) | fn no_events_no_env() {
function single_created (line 42) | fn single_created() {
function single_meta (line 54) | fn single_meta() {
function single_removed (line 69) | fn single_removed() {
function single_renamed (line 81) | fn single_renamed() {
function single_written (line 96) | fn single_written() {
function single_otherwise (line 111) | fn single_otherwise() {
function all_types_once (line 123) | fn all_types_once() {
function single_type_multipath (line 156) | fn single_type_multipath() {
function single_type_divergent_paths (line 187) | fn single_type_divergent_paths() {
function multitype_multipath (line 211) | fn multitype_multipath() {
function multiple_paths_in_one_event (line 249) | fn multiple_paths_in_one_event() {
function mixed_non_paths_events (line 277) | fn mixed_non_paths_events() {
function only_non_paths_events (line 303) | fn only_non_paths_events() {
function multipath_is_sorted (line 318) | fn multipath_is_sorted() {
function multipath_is_deduped (line 346) | fn multipath_is_deduped() {
FILE: crates/lib/tests/error_handler.rs
function main (line 8) | async fn main() -> Result<()> {
FILE: crates/project-origins/examples/find-origins.rs
function main (line 8) | async fn main() -> Result<()> {
FILE: crates/project-origins/src/lib.rs
type ProjectType (line 34) | pub enum ProjectType {
method is_vcs (line 158) | pub const fn is_vcs(self) -> bool {
method is_soft (line 172) | pub const fn is_soft(self) -> bool {
function origins (line 198) | pub async fn origins(path: impl AsRef<Path> + Send) -> HashSet<PathBuf> {
function types (line 290) | pub async fn types(path: impl AsRef<Path> + Send) -> HashSet<ProjectType> {
type DirList (line 332) | struct DirList(HashMap<PathBuf, FileType>);
method obtain (line 334) | async fn obtain(path: &Path) -> Self {
method is_empty (line 361) | fn is_empty(&self) -> bool {
method has_file (line 366) | fn has_file(&self, name: impl AsRef<Path>) -> bool {
method has_dir (line 372) | fn has_dir(&self, name: impl AsRef<Path>) -> bool {
method if_has_file (line 378) | fn if_has_file(&self, name: impl AsRef<Path>, project: ProjectType) ->...
method if_has_dir (line 387) | fn if_has_dir(&self, name: impl AsRef<Path>, project: ProjectType) -> ...
FILE: crates/signals/src/lib.rs
type Signal (line 35) | pub enum Signal {
method to_nix (line 134) | pub fn to_nix(self) -> Option<NixSignal> {
method from_nix (line 151) | pub fn from_nix(sig: NixSignal) -> Self {
method from (line 169) | fn from(raw: i32) -> Self {
method from_unix_str (line 203) | pub fn from_unix_str(s: &str) -> Result<Self, SignalParseError> {
method from_unix_str_impl (line 208) | fn from_unix_str_impl(s: &str) -> Result<Self, SignalParseError> {
method from_unix_str_impl (line 225) | fn from_unix_str_impl(s: &str) -> Result<Self, SignalParseError> {
method from_windows_str (line 262) | pub fn from_windows_str(s: &str) -> Result<Self, SignalParseError> {
method fmt (line 313) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method from (line 383) | fn from(signal: SerdeSignal) -> Self {
type Err (line 275) | type Err = SignalParseError;
method from_str (line 277) | fn from_str(s: &str) -> Result<Self, Self::Err> {
type SignalParseError (line 287) | pub struct SignalParseError {
method new (line 303) | pub fn new(src: &str, err: &str) -> Self {
type SerdeSignal (line 343) | pub enum SerdeSignal {
method from (line 368) | fn from(signal: Signal) -> Self {
type NamedSignal (line 350) | pub enum NamedSignal {
FILE: crates/supervisor/src/command.rs
type Command (line 25) | pub struct Command {
type SpawnOptions (line 54) | pub struct SpawnOptions {
FILE: crates/supervisor/src/command/conversions.rs
method to_spawnable (line 11) | pub fn to_spawnable(&self) -> CommandWrap {
method fmt (line 88) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method fmt (line 106) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
FILE: crates/supervisor/src/command/program.rs
type Program (line 7) | pub enum Program {
FILE: crates/supervisor/src/command/shell.rs
type Shell (line 5) | pub struct Shell {
method new (line 23) | pub fn new(name: impl Into<PathBuf>) -> Self {
method cmd (line 34) | pub fn cmd() -> Self {
FILE: crates/supervisor/src/errors.rs
type SyncIoError (line 9) | pub type SyncIoError = Arc<OnceLock<Error>>;
function sync_io_error (line 13) | pub fn sync_io_error(err: Error) -> SyncIoError {
FILE: crates/supervisor/src/flag.rs
type Inner (line 20) | struct Inner {
type Flag (line 26) | pub struct Flag(Arc<Inner>);
method new (line 35) | pub fn new(value: bool) -> Self {
method raised (line 42) | pub fn raised(&self) -> bool {
method raise (line 46) | pub fn raise(&self) {
method default (line 29) | fn default() -> Self {
type Output (line 53) | type Output = ();
method poll (line 55) | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
FILE: crates/supervisor/src/job/job.rs
type Job (line 45) | pub struct Job {
method command (line 58) | pub fn command(&self) -> Arc<Command> {
method is_dead (line 66) | pub fn is_dead(&self) -> bool {
method is_running (line 74) | pub fn is_running(&self) -> bool {
method prepare_control (line 78) | fn prepare_control(&self, control: Control) -> (Ticket, ControlMessage) {
method send_controls (line 89) | pub(crate) fn send_controls<const N: usize>(
method control (line 118) | pub fn control(&self, control: Control) -> Ticket {
method start (line 123) | pub fn start(&self) -> Ticket {
method stop (line 130) | pub fn stop(&self) -> Ticket {
method stop_with_signal (line 140) | pub fn stop_with_signal(&self, signal: Signal, grace: Duration) -> Tic...
method restart (line 149) | pub fn restart(&self) -> Ticket {
method restart_with_signal (line 159) | pub fn restart_with_signal(&self, signal: Signal, grace: Duration) -> ...
method try_restart (line 171) | pub fn try_restart(&self) -> Ticket {
method try_restart_with_signal (line 181) | pub fn try_restart_with_signal(&self, signal: Signal, grace: Duration)...
method signal (line 200) | pub fn signal(&self, sig: Signal) -> Ticket {
method delete (line 208) | pub fn delete(&self) -> Ticket {
method delete_now (line 216) | pub fn delete_now(&self) -> Ticket {
method to_wait (line 228) | pub fn to_wait(&self) -> Ticket {
method run (line 242) | pub fn run(&self, fun: impl FnOnce(&JobTaskContext<'_>) + Send + Sync ...
method run_async (line 302) | pub fn run_async(
method set_spawn_hook (line 317) | pub fn set_spawn_hook(
method set_spawn_async_hook (line 337) | pub fn set_spawn_async_hook(
method unset_spawn_hook (line 348) | pub fn unset_spawn_hook(&self) -> Ticket {
method set_spawn_fn (line 363) | pub fn set_spawn_fn(
method unset_spawn_fn (line 374) | pub fn unset_spawn_fn(&self) -> Ticket {
method set_error_handler (line 379) | pub fn set_error_handler(&self, fun: impl Fn(SyncIoError) + Send + Syn...
method set_async_error_handler (line 384) | pub fn set_async_error_handler(
method unset_error_handler (line 397) | pub fn unset_error_handler(&self) -> Ticket {
FILE: crates/supervisor/src/job/messages.rs
type Control (line 22) | pub enum Control {
method fmt (line 78) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
type ControlMessage (line 123) | pub struct ControlMessage {
type Ticket (line 136) | pub struct Ticket {
method cancelled (line 142) | pub(crate) fn cancelled() -> Self {
type Output (line 151) | type Output = ();
method poll (line 153) | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
FILE: crates/supervisor/src/job/priority.rs
type Priority (line 14) | pub enum Priority {
type PriorityReceiver (line 21) | pub struct PriorityReceiver {
method recv (line 53) | pub async fn recv(&mut self, stop_timer: &mut Option<Timer>) -> Option...
type PrioritySender (line 28) | pub struct PrioritySender {
method send (line 35) | pub fn send(&self, message: ControlMessage, priority: Priority) {
function new (line 85) | pub fn new() -> (PrioritySender, PriorityReceiver) {
type Timer (line 105) | pub struct Timer {
method stop (line 112) | pub fn stop(grace: Duration, done: Flag) -> Self {
method restart (line 120) | pub fn restart(grace: Duration, done: Flag) -> Self {
method to_sleep (line 128) | fn to_sleep(&self) -> Sleep {
method is_past (line 132) | fn is_past(&self) -> bool {
method to_control (line 136) | fn to_control(&self) -> ControlMessage {
FILE: crates/supervisor/src/job/state.rs
type CommandState (line 23) | pub enum CommandState {
method is_pending (line 59) | pub const fn is_pending(&self) -> bool {
method is_running (line 65) | pub const fn is_running(&self) -> bool {
method is_finished (line 71) | pub const fn is_finished(&self) -> bool {
method spawn (line 76) | pub(crate) fn spawn(
method reset (line 107) | pub(crate) fn reset(&mut self) -> Self {
method wait (line 139) | pub(crate) async fn wait(&mut self) -> std::io::Result<bool> {
FILE: crates/supervisor/src/job/task.rs
function start_job (line 35) | pub fn start_job(command: Arc<Command>) -> (Job, JoinHandle<()>) {
type JobTaskContext (line 421) | pub struct JobTaskContext<'task> {
type SyncFunc (line 434) | pub type SyncFunc = Box<dyn FnOnce(&JobTaskContext<'_>) + Send + Sync + ...
type AsyncFunc (line 435) | pub type AsyncFunc = Box<
type SyncSpawnHook (line 442) | pub type SyncSpawnHook = Arc<dyn Fn(&mut CommandWrap, &JobTaskContext<'_...
type AsyncSpawnHook (line 443) | pub type AsyncSpawnHook = Arc<
type SpawnFn (line 460) | pub type SpawnFn = Arc<
type SyncErrorHandler (line 469) | pub type SyncErrorHandler = Arc<dyn Fn(SyncIoError) + Send + Sync + 'sta...
type AsyncErrorHandler (line 470) | pub type AsyncErrorHandler = Arc<
function signal_child (line 478) | async fn signal_child(
FILE: crates/supervisor/src/job/test.rs
constant GRACE (line 25) | const GRACE: u64 = 10;
function erroring_command (line 27) | fn erroring_command() -> Arc<Command> {
function working_command (line 37) | fn working_command() -> Arc<Command> {
function ungraceful_command (line 47) | fn ungraceful_command() -> Arc<Command> {
function graceful_command (line 57) | fn graceful_command() -> Arc<Command> {
function sync_error_handler (line 68) | async fn sync_error_handler() {
function async_error_handler (line 91) | async fn async_error_handler() {
function unset_error_handler (line 117) | async fn unset_error_handler() {
function queue_ordering (line 142) | async fn queue_ordering() {
function sync_func (line 168) | async fn sync_func() {
function async_func (line 194) | async fn async_func() {
function refresh_state (line 224) | async fn refresh_state(job: &Job, state: &Arc<Mutex<Option<CommandState>...
function set_running_child_status (line 241) | async fn set_running_child_status(job: &Job, status: ExitStatus) {
function get_child (line 305) | async fn get_child(job: &Job) -> super::TestChild {
function start (line 317) | async fn start() {
function signal_unix (line 331) | async fn signal_unix() {
function stop (line 349) | async fn stop() {
function stop_when_running (line 374) | async fn stop_when_running() {
function stop_fail (line 391) | async fn stop_fail() {
function restart (line 420) | async fn restart() {
function graceful_stop (line 463) | async fn graceful_stop() {
function graceful_stop_with_job_dropped (line 499) | async fn graceful_stop_with_job_dropped() {
function graceful_restart (line 532) | async fn graceful_restart() {
function graceful_stop_beyond_grace (line 577) | async fn graceful_stop_beyond_grace() {
function graceful_restart_beyond_grace (line 617) | async fn graceful_restart_beyond_grace() {
function try_restart (line 679) | async fn try_restart() {
function try_graceful_restart (line 716) | async fn try_graceful_restart() {
function try_restart_beyond_grace (line 780) | async fn try_restart_beyond_grace() {
function try_graceful_restart_beyond_grace (line 831) | async fn try_graceful_restart_beyond_grace() {
FILE: crates/supervisor/src/job/testchild.rs
type TestChild (line 18) | pub struct TestChild {
method new (line 27) | pub fn new(command: Arc<Command>) -> std::io::Result<Self> {
method id (line 60) | pub fn id(&mut self) -> Option<u32> {
method kill (line 65) | pub fn kill(&mut self) -> Box<dyn Future<Output = Result<()>> + Send +...
method start_kill (line 70) | pub fn start_kill(&mut self) -> Result<()> {
method try_wait (line 75) | pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
method wait (line 99) | pub fn wait(&mut self) -> Pin<Box<dyn Future<Output = Result<ExitStatu...
method wait_with_output (line 136) | pub fn wait_with_output(self) -> Box<dyn Future<Output = Result<Output...
method signal (line 150) | pub fn signal(&self, sig: i32) -> Result<()> {
type TestChildCall (line 48) | pub enum TestChildCall {
FILE: crates/supervisor/tests/programs.rs
function unix_shell_none (line 5) | async fn unix_shell_none() -> Result<(), std::io::Error> {
function unix_shell_sh (line 23) | async fn unix_shell_sh() -> Result<(), std::io::Error> {
function unix_shell_alternate (line 42) | async fn unix_shell_alternate() -> Result<(), std::io::Error> {
function unix_shell_alternate_shopts (line 61) | async fn unix_shell_alternate_shopts() -> Result<(), std::io::Error> {
function windows_shell_none (line 83) | async fn windows_shell_none() -> Result<(), std::io::Error> {
function windows_shell_cmd (line 101) | async fn windows_shell_cmd() -> Result<(), std::io::Error> {
function windows_shell_powershell (line 120) | async fn windows_shell_powershell() -> Result<(), std::io::Error> {
FILE: crates/test-socketfd/src/main.rs
function main (line 8) | fn main() {
Condensed preview — 232 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (857K chars).
[
{
"path": ".cargo/config.toml",
"chars": 291,
"preview": "[target.armv7-unknown-linux-gnueabihf]\nlinker = \"arm-linux-gnueabihf-gcc\"\n\n[target.armv7-unknown-linux-musleabihf]\nlinke"
},
{
"path": ".editorconfig",
"chars": 328,
"preview": "root = true\n\n[*]\nindent_style = tab\nindent_size = 4\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\nins"
},
{
"path": ".gitattributes",
"chars": 80,
"preview": "Cargo.lock merge=binary\ndoc/watchexec.* merge=binary\ncompletions/* merge=binary\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 19,
"preview": "liberapay: passcod\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 455,
"preview": "---\nname: Bug report\nabout: Something is wrong\ntitle: ''\nlabels: bug, need-info\nassignees: ''\n\n---\n\nPlease delete this t"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 835,
"preview": "---\nname: Feature request\nabout: Something is missing\ntitle: ''\nlabels: feature\nassignees: ''\n\n---\n\n<!-- Please note tha"
},
{
"path": ".github/ISSUE_TEMPLATE/regression.md",
"chars": 658,
"preview": "---\nname: Regression\nabout: Something changed unexpectedly\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**What used to happe"
},
{
"path": ".github/dependabot.yml",
"chars": 1302,
"preview": "# Dependabot dependency version checks / updates\n\nversion: 2\nupdates:\n - package-ecosystem: \"github-actions\"\n # Work"
},
{
"path": ".github/workflows/clippy.yml",
"chars": 1344,
"preview": "name: Clippy\n\non:\n workflow_dispatch:\n pull_request:\n push:\n branches:\n - main\n tags-ignore:\n - \"*\"\n\n"
},
{
"path": ".github/workflows/dist-manifest.jq",
"chars": 961,
"preview": "{\n dist_version: \"0.0.2\",\n releases: [{\n app_name: \"watchexec\",\n app_version: $version,\n changelog_title: \"CL"
},
{
"path": ".github/workflows/release-cli.yml",
"chars": 9139,
"preview": "name: CLI Release\n\non:\n workflow_dispatch:\n push:\n tags:\n - \"v*.*.*\"\n\nenv:\n CARGO_TERM_COLOR: always\n CARGO_"
},
{
"path": ".github/workflows/tests.yml",
"chars": 5586,
"preview": "name: Test suite\n\non:\n workflow_dispatch:\n pull_request:\n types:\n - opened\n - reopened\n - synchroniz"
},
{
"path": ".gitignore",
"chars": 36,
"preview": "target\n/watchexec-*\nwatchexec.*.log\n"
},
{
"path": ".rustfmt.toml",
"chars": 17,
"preview": "hard_tabs = true\n"
},
{
"path": "CITATION.cff",
"chars": 465,
"preview": "cff-version: 1.2.0\nmessage: |\n If you use this software, please cite it using these metadata.\ntitle: \"Watchexec: a tool"
},
{
"path": "CONTRIBUTING.md",
"chars": 4308,
"preview": "# Contribution guidebook\n\n\nThis is a fairly free-form project, with low contribution traffic.\n\nMaintainers:\n\n- Félix Sap"
},
{
"path": "Cargo.toml",
"chars": 730,
"preview": "[workspace]\nresolver = \"2\"\nmembers = [\n\t\"crates/lib\",\n\t\"crates/cli\",\n\t\"crates/events\",\n\t\"crates/signals\",\n\t\"crates/super"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 4254,
"preview": "[](https://gith"
},
{
"path": "bin/completions",
"chars": 446,
"preview": "#!/bin/sh\ncargo run -p watchexec-cli $* -- --completions bash > completions/bash\ncargo run -p watchexec-cli $* -- --comp"
},
{
"path": "bin/dates.mjs",
"chars": 217,
"preview": "#!/usr/bin/env node\n\nconst id = Math.floor(Math.random() * 100);\nlet n = 0;\nconst m = 5;\nwhile (n < m) {\n\tn += 1;\n\tconso"
},
{
"path": "bin/manpage",
"chars": 123,
"preview": "#!/bin/sh\ncargo run -p watchexec-cli -- --manual > doc/watchexec.1\npandoc doc/watchexec.1 -t markdown > doc/watchexec.1."
},
{
"path": "bin/release-notes",
"chars": 95,
"preview": "#!/bin/sh\nexec git cliff --include-path '**/crates/cli/**/*' --count-tags 'v*' --unreleased $*\n"
},
{
"path": "cliff.toml",
"chars": 2203,
"preview": "[changelog]\ntrim = true\nheader = \"\"\nfooter = \"\"\nbody = \"\"\"\n{% if version %}\\\n\t## v{{ version | trim_start_matches(pat=\"v"
},
{
"path": "completions/bash",
"chars": 9135,
"preview": "_watchexec() {\n local i cur prev opts cmd\n COMPREPLY=()\n if [[ \"${BASH_VERSINFO[0]}\" -ge 4 ]]; then\n cur"
},
{
"path": "completions/elvish",
"chars": 5506,
"preview": "\nuse builtin;\nuse str;\n\nset edit:completion:arg-completer[watchexec] = {|@words|\n fn spaces {|n|\n builtin:repe"
},
{
"path": "completions/fish",
"chars": 5083,
"preview": "complete -c watchexec -l completions -d 'Generate a shell completions script' -r -f -a \"bash\\t''\nelvish\\t''\nfish\\t''\nnu\\"
},
{
"path": "completions/nu",
"chars": 5046,
"preview": "module completions {\n\n def \"nu-complete watchexec completions\" [] {\n [ \"bash\" \"elvish\" \"fish\" \"nu\" \"powershell\" \"zsh"
},
{
"path": "completions/powershell",
"chars": 11629,
"preview": "\nusing namespace System.Management.Automation\nusing namespace System.Management.Automation.Language\n\nRegister-ArgumentCo"
},
{
"path": "completions/zsh",
"chars": 6025,
"preview": "#compdef watchexec\n\nautoload -U is-at-least\n\n_watchexec() {\n typeset -A opt_args\n typeset -a _arguments_options\n "
},
{
"path": "crates/bosion/CHANGELOG.md",
"chars": 642,
"preview": "# Changelog\n\n## Next (YYYY-MM-DD)\n\n## v2.0.0 (2026-01-20)\n\n- Remove `GIT_COMMIT_DESCRIPTION`. In practice this had zero "
},
{
"path": "crates/bosion/Cargo.toml",
"chars": 1343,
"preview": "[package]\nname = \"bosion\"\nversion = \"2.0.0\"\n\nauthors = [\"Félix Saparelli <felix@passcod.name>\"]\nlicense = \"Apache-2.0 OR"
},
{
"path": "crates/bosion/README.md",
"chars": 4074,
"preview": "# Bosion\n\n_Gather build information for verbose versions flags._\n\n- **[API documentation][docs]**.\n- Licensed under [Apa"
},
{
"path": "crates/bosion/examples/clap/Cargo.toml",
"chars": 270,
"preview": "[package]\nname = \"bosion-example-clap\"\nversion = \"0.1.0\"\npublish = false\nedition = \"2021\"\n\n[workspace]\n\n[features]\ndefau"
},
{
"path": "crates/bosion/examples/clap/build.rs",
"chars": 33,
"preview": "fn main() {\n\tbosion::gather();\n}\n"
},
{
"path": "crates/bosion/examples/clap/src/main.rs",
"chars": 961,
"preview": "use clap::Parser;\n\ninclude!(env!(\"BOSION_PATH\"));\n\n#[derive(Parser)]\n#[clap(version, long_version = Bosion::LONG_VERSION"
},
{
"path": "crates/bosion/examples/default/Cargo.toml",
"chars": 356,
"preview": "[package]\nname = \"bosion-test-default\"\nversion = \"0.1.0\"\npublish = false\nedition = \"2021\"\n\n[workspace]\n\n[features]\ndefau"
},
{
"path": "crates/bosion/examples/default/build.rs",
"chars": 33,
"preview": "fn main() {\n\tbosion::gather();\n}\n"
},
{
"path": "crates/bosion/examples/default/src/common.rs",
"chars": 1697,
"preview": "#[cfg(test)]\npub(crate) fn git_commit_info(format: &str) -> String {\n\tlet output = std::process::Command::new(\"git\")\n\t\t."
},
{
"path": "crates/bosion/examples/default/src/main.rs",
"chars": 727,
"preview": "include!(env!(\"BOSION_PATH\"));\n\nmod common;\nfn main() {}\n\ntest_snapshot!(crate_version, Bosion::CRATE_VERSION);\n\ntest_sn"
},
{
"path": "crates/bosion/examples/no-git/Cargo.toml",
"chars": 399,
"preview": "[package]\nname = \"bosion-test-no-git\"\nversion = \"0.1.0\"\npublish = false\nedition = \"2021\"\n\n[workspace]\n\n[features]\ndefaul"
},
{
"path": "crates/bosion/examples/no-git/build.rs",
"chars": 33,
"preview": "fn main() {\n\tbosion::gather();\n}\n"
},
{
"path": "crates/bosion/examples/no-git/src/main.rs",
"chars": 511,
"preview": "include!(env!(\"BOSION_PATH\"));\n\n#[path = \"../../default/src/common.rs\"]\nmod common;\nfn main() {}\n\ntest_snapshot!(crate_v"
},
{
"path": "crates/bosion/examples/no-std/Cargo.toml",
"chars": 446,
"preview": "[package]\nname = \"bosion-test-no-std\"\nversion = \"0.1.0\"\npublish = false\nedition = \"2021\"\n\n[profile.dev]\npanic = \"abort\"\n"
},
{
"path": "crates/bosion/examples/no-std/build.rs",
"chars": 33,
"preview": "fn main() {\n\tbosion::gather();\n}\n"
},
{
"path": "crates/bosion/examples/no-std/src/main.rs",
"chars": 643,
"preview": "#![cfg_attr(not(test), no_main)]\n#![cfg_attr(not(test), no_std)]\n\n#[cfg(not(test))]\nuse core::panic::PanicInfo;\n\n#[cfg(n"
},
{
"path": "crates/bosion/examples/snapshots/build_date.txt",
"chars": 13,
"preview": "{today date}\n"
},
{
"path": "crates/bosion/examples/snapshots/build_datetime.txt",
"chars": 18,
"preview": "{today date} [..]\n"
},
{
"path": "crates/bosion/examples/snapshots/crate_features.txt",
"chars": 30,
"preview": "[\n \"default\",\n \"foo\",\n]\n"
},
{
"path": "crates/bosion/examples/snapshots/crate_version.txt",
"chars": 6,
"preview": "0.1.0\n"
},
{
"path": "crates/bosion/examples/snapshots/default_long_version.txt",
"chars": 150,
"preview": "0.1.0 ({git shorthash} {git date}) +foo\ncommit-hash: {git hash}\ncommit-date: {git date}\nbuild-date: {today date}\nrelease"
},
{
"path": "crates/bosion/examples/snapshots/default_long_version_with.txt",
"chars": 177,
"preview": "0.1.0 ({git shorthash} {git date}) +foo\ncommit-hash: {git hash}\ncommit-date: {git date}\nbuild-date: {today date}\nrelease"
},
{
"path": "crates/bosion/examples/snapshots/git_commit_date.txt",
"chars": 11,
"preview": "{git date}\n"
},
{
"path": "crates/bosion/examples/snapshots/git_commit_datetime.txt",
"chars": 15,
"preview": "{git datetime}\n"
},
{
"path": "crates/bosion/examples/snapshots/git_commit_hash.txt",
"chars": 11,
"preview": "{git hash}\n"
},
{
"path": "crates/bosion/examples/snapshots/git_commit_shorthash.txt",
"chars": 16,
"preview": "{git shorthash}\n"
},
{
"path": "crates/bosion/examples/snapshots/no_git_long_version.txt",
"chars": 88,
"preview": "0.1.0 ({today date}) +foo\nbuild-date: {today date}\nrelease: 0.1.0\nfeatures: default,foo\n"
},
{
"path": "crates/bosion/examples/snapshots/no_git_long_version_with.txt",
"chars": 115,
"preview": "0.1.0 ({today date}) +foo\nbuild-date: {today date}\nrelease: 0.1.0\nfeatures: default,foo\nextra: field\ncustom: 1.2.3\n"
},
{
"path": "crates/bosion/release.toml",
"chars": 423,
"preview": "pre-release-commit-message = \"release: bosion v{{version}}\"\ntag-prefix = \"bosion-\"\ntag-message = \"bosion {{version}}\"\n\n["
},
{
"path": "crates/bosion/run-tests.sh",
"chars": 185,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\nfor test in examples/*; do\n\techo \"Testing $test\"\n\tpushd $test\n\tif ! test -f Cargo.toml; "
},
{
"path": "crates/bosion/src/info.rs",
"chars": 13679,
"preview": "use std::{\n\tenv::var,\n\tpath::{Path, PathBuf},\n};\n\nuse time::{format_description::FormatItem, macros::format_description,"
},
{
"path": "crates/bosion/src/lib.rs",
"chars": 9376,
"preview": "#![doc = include_str!(\"../README.md\")]\n#![cfg_attr(not(test), warn(unused_crate_dependencies))]\n\nuse std::{env::var, fs:"
},
{
"path": "crates/cli/Cargo.toml",
"chars": 6026,
"preview": "[package]\nname = \"watchexec-cli\"\nversion = \"2.5.1\"\n\nauthors = [\"Félix Saparelli <felix@passcod.name>\", \"Matt Green <matt"
},
{
"path": "crates/cli/README.md",
"chars": 8250,
"preview": "# Watchexec CLI\n\nA simple standalone tool that watches a path and runs a command whenever it detects modifications.\n\nExa"
},
{
"path": "crates/cli/build.rs",
"chars": 246,
"preview": "fn main() {\n\tembed_resource::compile(\"watchexec-manifest.rc\", embed_resource::NONE)\n\t\t.manifest_optional()\n\t\t.unwrap();\n"
},
{
"path": "crates/cli/integration/env-unix.sh",
"chars": 123,
"preview": "#!/bin/bash\n\nset -euxo pipefail\n\nwatchexec=${WATCHEXEC_BIN:-watchexec}\n\n$watchexec -1 --env FOO=BAR echo '$FOO' | grep B"
},
{
"path": "crates/cli/integration/no-shell-unix.sh",
"chars": 123,
"preview": "#!/bin/bash\n\nset -euxo pipefail\n\nwatchexec=${WATCHEXEC_BIN:-watchexec}\n\n$watchexec -1 -n echo 'foo bar' | grep 'foo ba"
},
{
"path": "crates/cli/integration/socket.sh",
"chars": 502,
"preview": "#!/bin/bash\n\nset -euxo pipefail\n\nwatchexec=${WATCHEXEC_BIN:-watchexec}\ntest_socketfd=${TEST_SOCKETFD_BIN:-test-socketfd}"
},
{
"path": "crates/cli/integration/stdin-quit-unix.sh",
"chars": 136,
"preview": "#!/bin/bash\n\nset -euxo pipefail\n\nwatchexec=${WATCHEXEC_BIN:-watchexec}\n\ntimeout -s9 30s sh -c \"sleep 10 | $watchexec --s"
},
{
"path": "crates/cli/integration/trailingargfile-unix.sh",
"chars": 111,
"preview": "#!/bin/bash\n\nset -euxo pipefail\n\nwatchexec=${WATCHEXEC_BIN:-watchexec}\n\n$watchexec -1 -- echo @trailingargfile\n"
},
{
"path": "crates/cli/release.toml",
"chars": 669,
"preview": "pre-release-commit-message = \"release: cli v{{version}}\"\ntag-prefix = \"\"\ntag-message = \"watchexec {{version}}\"\n\npre-rele"
},
{
"path": "crates/cli/run-tests.sh",
"chars": 614,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\nexport WATCHEXEC_BIN=$(realpath ${WATCHEXEC_BIN:-$(which watchexec)})\nexport TEST_SOCKET"
},
{
"path": "crates/cli/src/args/command.rs",
"chars": 10903,
"preview": "use std::{\n\tffi::{OsStr, OsString},\n\tmem::take,\n\tpath::PathBuf,\n};\n\nuse clap::{\n\tbuilder::TypedValueParser,\n\terror::{Err"
},
{
"path": "crates/cli/src/args/events.rs",
"chars": 15206,
"preview": "use std::{ffi::OsStr, path::PathBuf};\n\nuse clap::{\n\tbuilder::TypedValueParser, error::ErrorKind, Arg, Command, CommandFa"
},
{
"path": "crates/cli/src/args/filtering.rs",
"chars": 17328,
"preview": "use std::{\n\tcollections::BTreeSet,\n\tmem::take,\n\tpath::{Path, PathBuf},\n};\n\nuse clap::{Parser, ValueEnum, ValueHint};\nuse"
},
{
"path": "crates/cli/src/args/logging.rs",
"chars": 4591,
"preview": "use std::{env::var, io::stderr, path::PathBuf};\n\nuse clap::{ArgAction, Parser, ValueHint};\nuse miette::{bail, Result};\nu"
},
{
"path": "crates/cli/src/args/output.rs",
"chars": 3090,
"preview": "use clap::{Parser, ValueEnum};\nuse miette::Result;\n\nuse super::OPTSET_OUTPUT;\n\n#[derive(Debug, Clone, Parser)]\npub struc"
},
{
"path": "crates/cli/src/args.rs",
"chars": 8676,
"preview": "use std::{\n\tffi::{OsStr, OsString},\n\tstr::FromStr,\n\ttime::Duration,\n};\n\nuse clap::{Parser, ValueEnum, ValueHint};\nuse mi"
},
{
"path": "crates/cli/src/config.rs",
"chars": 26848,
"preview": "use std::{\n\tborrow::Cow,\n\tcollections::HashMap,\n\tenv::var,\n\tffi::OsStr,\n\tfmt,\n\tfs::File,\n\tio::{IsTerminal, Write},\n\titer"
},
{
"path": "crates/cli/src/dirs.rs",
"chars": 5621,
"preview": "use std::{\n\tcollections::HashSet,\n\tpath::{Path, PathBuf},\n};\n\nuse ignore_files::{IgnoreFile, IgnoreFilesFromOriginArgs};"
},
{
"path": "crates/cli/src/emits.rs",
"chars": 1810,
"preview": "use std::{fmt::Write, path::PathBuf};\n\nuse miette::{IntoDiagnostic, Result};\nuse watchexec::paths::summarise_events_to_e"
},
{
"path": "crates/cli/src/filterer/parse.rs",
"chars": 2895,
"preview": "use std::{fmt::Debug, path::PathBuf};\n\nuse jaq_core::{\n\tload::{Arena, File, Loader},\n\tCtx, Filter, Native, RcIter,\n};\nus"
},
{
"path": "crates/cli/src/filterer/proglib/file.rs",
"chars": 4073,
"preview": "use std::{\n\tfs::{metadata, File, FileType, Metadata},\n\tio::{BufReader, Read},\n\titer::once,\n\ttime::{SystemTime, UNIX_EPOC"
},
{
"path": "crates/cli/src/filterer/proglib/hash.rs",
"chars": 1466,
"preview": "use std::{fs::File, io::Read, iter::once};\n\nuse jaq_core::{Error, Native};\nuse jaq_json::Val;\nuse jaq_std::{v, Filter};\n"
},
{
"path": "crates/cli/src/filterer/proglib/kv.rs",
"chars": 1068,
"preview": "use std::{\n\titer::once,\n\tsync::{Arc, OnceLock},\n};\n\nuse dashmap::DashMap;\nuse jaq_core::Native;\nuse jaq_json::Val;\nuse j"
},
{
"path": "crates/cli/src/filterer/proglib/macros.rs",
"chars": 127,
"preview": "macro_rules! return_err {\n\t($err:expr) => {\n\t\treturn Box::new(once($err.map_err(Into::into)))\n\t};\n}\npub(crate) use retur"
},
{
"path": "crates/cli/src/filterer/proglib/output.rs",
"chars": 1419,
"preview": "use std::iter::once;\n\nuse jaq_core::{Ctx, Error, Native};\nuse jaq_json::Val;\nuse jaq_std::{v, Filter};\nuse tracing::{deb"
},
{
"path": "crates/cli/src/filterer/proglib.rs",
"chars": 351,
"preview": "use jaq_core::{Compiler, Native};\n\nmod file;\nmod hash;\nmod kv;\nmod macros;\nmod output;\n\npub fn jaq_lib<'s>() -> Compiler"
},
{
"path": "crates/cli/src/filterer/progs.rs",
"chars": 2860,
"preview": "use std::marker::PhantomData;\n\nuse miette::miette;\nuse tokio::{\n\tsync::{mpsc, oneshot},\n\ttask::{block_in_place, spawn_bl"
},
{
"path": "crates/cli/src/filterer/syncval.rs",
"chars": 1719,
"preview": "/// Jaq's [Val](jaq_json::Val) uses Rc, but we want to use in Sync contexts. UGH!\nuse std::{rc::Rc, sync::Arc};\n\nuse ind"
},
{
"path": "crates/cli/src/filterer.rs",
"chars": 5112,
"preview": "use std::{\n\tffi::OsString,\n\tpath::{Path, PathBuf, MAIN_SEPARATOR},\n\tsync::Arc,\n};\n\nuse miette::{IntoDiagnostic, Result};"
},
{
"path": "crates/cli/src/lib.rs",
"chars": 3680,
"preview": "#![deny(rust_2018_idioms)]\n#![allow(clippy::missing_const_for_fn, clippy::future_not_send)]\n\nuse std::{\n\tio::{IsTerminal"
},
{
"path": "crates/cli/src/main.rs",
"chars": 519,
"preview": "#[cfg(feature = \"eyra\")]\nextern crate eyra;\n\nuse std::process::ExitCode;\n\nuse miette::IntoDiagnostic;\n\n#[cfg(target_env "
},
{
"path": "crates/cli/src/socket/fallback.rs",
"chars": 332,
"preview": "use miette::{bail, Result};\n\nuse crate::args::command::EnvVar;\n\nuse super::{SocketSpec, Sockets};\n\n#[derive(Debug)]\npub "
},
{
"path": "crates/cli/src/socket/parser.rs",
"chars": 1853,
"preview": "use std::{\n\tffi::OsStr,\n\tnet::{IpAddr, Ipv4Addr, SocketAddr},\n\tnum::{IntErrorKind, NonZero},\n\tstr::FromStr,\n};\n\nuse clap"
},
{
"path": "crates/cli/src/socket/test.rs",
"chars": 4082,
"preview": "use crate::args::Args;\n\nuse super::*;\nuse clap::{builder::TypedValueParser, CommandFactory};\nuse std::{\n\tffi::OsStr,\n\tne"
},
{
"path": "crates/cli/src/socket/unix.rs",
"chars": 1735,
"preview": "use std::os::fd::{AsRawFd, OwnedFd};\n\nuse miette::{IntoDiagnostic, Result};\nuse nix::sys::socket::{\n\tbind, listen, setso"
},
{
"path": "crates/cli/src/socket/windows.rs",
"chars": 4091,
"preview": "use std::{\n\tio::ErrorKind,\n\tnet::SocketAddr,\n\tos::windows::io::{AsRawSocket, OwnedSocket},\n\tstr::FromStr,\n\tsync::Arc,\n};"
},
{
"path": "crates/cli/src/socket.rs",
"chars": 919,
"preview": "// listen-fd code inspired by systemdfd source by @mitsuhiko (Apache-2.0)\n// https://github.com/mitsuhiko/systemfd/blob/"
},
{
"path": "crates/cli/src/state.rs",
"chars": 1725,
"preview": "use std::{\n\tenv::var_os,\n\tio::Write,\n\tpath::PathBuf,\n\tprocess::ExitCode,\n\tsync::{Arc, Mutex, OnceLock},\n};\n\nuse watchexe"
},
{
"path": "crates/cli/tests/common/mod.rs",
"chars": 3507,
"preview": "use std::path::PathBuf;\nuse std::{fs, sync::OnceLock};\n\nuse miette::{Context, IntoDiagnostic, Result};\nuse rand::Rng;\n\ns"
},
{
"path": "crates/cli/tests/ignore.rs",
"chars": 4733,
"preview": "use std::{\n\tpath::{Path, PathBuf},\n\tprocess::Stdio,\n\ttime::Duration,\n};\n\nuse miette::{IntoDiagnostic, Result, WrapErr};\n"
},
{
"path": "crates/cli/watchexec-manifest.rc",
"chars": 62,
"preview": "#define RT_MANIFEST 24\n1 RT_MANIFEST \"watchexec.exe.manifest\"\n"
},
{
"path": "crates/cli/watchexec.exe.manifest",
"chars": 1482,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersi"
},
{
"path": "crates/events/CHANGELOG.md",
"chars": 860,
"preview": "# Changelog\n\n## Next (YYYY-MM-DD)\n\n## v6.1.0 (2026-02-22)\n\n- Add `Keyboard::Key` to describe arbitrary single-key keyboa"
},
{
"path": "crates/events/Cargo.toml",
"chars": 1172,
"preview": "[package]\nname = \"watchexec-events\"\nversion = \"6.1.0\"\n\nauthors = [\"Félix Saparelli <felix@passcod.name>\"]\nlicense = \"Apa"
},
{
"path": "crates/events/README.md",
"chars": 1337,
"preview": "# watchexec-events\n\n_Watchexec's event types._\n\n- **[API documentation][docs]**.\n- Licensed under [Apache 2.0][license] "
},
{
"path": "crates/events/examples/parse-and-print.rs",
"chars": 198,
"preview": "use std::io::{stdin, Result};\nuse watchexec_events::Event;\n\nfn main() -> Result<()> {\n\tfor line in stdin().lines() {\n\t\tl"
},
{
"path": "crates/events/release.toml",
"chars": 302,
"preview": "pre-release-commit-message = \"release: events v{{version}}\"\ntag-prefix = \"watchexec-events-\"\ntag-message = \"watchexec-ev"
},
{
"path": "crates/events/src/event.rs",
"chars": 6213,
"preview": "use std::{\n\tcollections::HashMap,\n\tfmt,\n\tpath::{Path, PathBuf},\n};\n\nuse watchexec_signals::Signal;\n\n#[cfg(feature = \"ser"
},
{
"path": "crates/events/src/fs.rs",
"chars": 1550,
"preview": "use std::fmt;\n\n/// Re-export of the Notify file event types.\n#[cfg(feature = \"notify\")]\npub mod filekind {\n\tpub use noti"
},
{
"path": "crates/events/src/keyboard.rs",
"chars": 1706,
"preview": "#[derive(Debug, Clone, PartialEq, Eq)]\n#[cfg_attr(feature = \"serde\", derive(serde::Serialize, serde::Deserialize))]\n#[cf"
},
{
"path": "crates/events/src/lib.rs",
"chars": 377,
"preview": "#![doc = include_str!(\"../README.md\")]\n#![cfg_attr(not(test), warn(unused_crate_dependencies))]\n\n#[doc(inline)]\npub use "
},
{
"path": "crates/events/src/process.rs",
"chars": 4732,
"preview": "use std::{\n\tnum::{NonZeroI32, NonZeroI64},\n\tprocess::ExitStatus,\n};\n\nuse watchexec_signals::Signal;\n\n/// The end status "
},
{
"path": "crates/events/src/sans_notify.rs",
"chars": 9282,
"preview": "// This file is dual-licensed under the Artistic License 2.0 as per the\n// LICENSE.ARTISTIC file, and the Creative Commo"
},
{
"path": "crates/events/src/serde_formats.rs",
"chars": 11197,
"preview": "use std::{\n\tcollections::BTreeMap,\n\tnum::{NonZeroI32, NonZeroI64},\n\tpath::PathBuf,\n};\n\nuse serde::{Deserialize, Serializ"
},
{
"path": "crates/events/tests/json.rs",
"chars": 5622,
"preview": "#![cfg(feature = \"serde\")]\n\nuse std::num::{NonZeroI32, NonZeroI64};\n\nuse snapbox::{assert_data_eq, file};\nuse watchexec_"
},
{
"path": "crates/events/tests/snapshots/array.json",
"chars": 371,
"preview": "[\n {\n \"tags\": [\n {\n \"kind\": \"source\",\n \"source\": \"internal\"\n }\n ]\n },\n {\n \"tags\": [\n"
},
{
"path": "crates/events/tests/snapshots/asymmetric.json",
"chars": 282,
"preview": "[\n\t{\n\t\t\"tags\": [\n\t\t\t{\n\t\t\t\t\"kind\": \"path\",\n\t\t\t\t\"absolute\": \"/foo/bar/baz\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"kind\": \"fs\",\n\t\t\t\t\"simple\": \"cre"
},
{
"path": "crates/events/tests/snapshots/completions.json",
"chars": 805,
"preview": "[\n {\n \"tags\": [\n {\n \"kind\": \"completion\",\n \"disposition\": \"unknown\"\n },\n {\n \"kin"
},
{
"path": "crates/events/tests/snapshots/metadata.json",
"chars": 210,
"preview": "[\n {\n \"tags\": [\n {\n \"kind\": \"source\",\n \"source\": \"internal\"\n }\n ],\n \"metadata\": {\n "
},
{
"path": "crates/events/tests/snapshots/paths.json",
"chars": 879,
"preview": "[\n {\n \"tags\": [\n {\n \"kind\": \"path\",\n \"absolute\": \"/foo/bar/baz\",\n \"filetype\": \"symlink\"\n "
},
{
"path": "crates/events/tests/snapshots/signals.json",
"chars": 397,
"preview": "[\n {\n \"tags\": [\n {\n \"kind\": \"signal\",\n \"signal\": \"SIGINT\"\n },\n {\n \"kind\": \"signa"
},
{
"path": "crates/events/tests/snapshots/single.json",
"chars": 82,
"preview": "{\n \"tags\": [\n {\n \"kind\": \"source\",\n \"source\": \"internal\"\n }\n ]\n}"
},
{
"path": "crates/events/tests/snapshots/sources.json",
"chars": 479,
"preview": "[\n {\n \"tags\": [\n {\n \"kind\": \"source\",\n \"source\": \"filesystem\"\n },\n {\n \"kind\": \"s"
},
{
"path": "crates/filterer/globset/CHANGELOG.md",
"chars": 1011,
"preview": "# Changelog\n\n## Next (YYYY-MM-DD)\n\n## v8.0.0 (2025-05-15)\n\n## v7.0.0 (2025-02-09)\n\n## v6.0.0 (2024-10-14)\n\n- Deps: watch"
},
{
"path": "crates/filterer/globset/Cargo.toml",
"chars": 1134,
"preview": "[package]\nname = \"watchexec-filterer-globset\"\nversion = \"8.0.0\"\n\nauthors = [\"Matt Green <mattgreenrocks@gmail.com>\", \"Fé"
},
{
"path": "crates/filterer/globset/README.md",
"chars": 712,
"preview": "[](https://crates.io/crates/watchexec-filterer-"
},
{
"path": "crates/filterer/globset/release.toml",
"chars": 332,
"preview": "pre-release-commit-message = \"release: filterer-globset v{{version}}\"\ntag-prefix = \"watchexec-filterer-globset-\"\ntag-mes"
},
{
"path": "crates/filterer/globset/src/lib.rs",
"chars": 7322,
"preview": "//! A path-only Watchexec filterer based on globsets.\n//!\n//! This filterer mimics the behavior of the `watchexec` v1 fi"
},
{
"path": "crates/filterer/globset/tests/filtering.rs",
"chars": 25561,
"preview": "mod helpers;\nuse helpers::globset::*;\nuse std::io::Write;\n\n#[tokio::test]\nasync fn empty_filter_passes_everything() {\n\tl"
},
{
"path": "crates/filterer/globset/tests/helpers/mod.rs",
"chars": 2993,
"preview": "use std::{\n\tffi::OsString,\n\tpath::{Path, PathBuf},\n};\n\nuse ignore_files::IgnoreFile;\nuse watchexec::{error::RuntimeError"
},
{
"path": "crates/filterer/ignore/CHANGELOG.md",
"chars": 1271,
"preview": "# Changelog\n\n## Next (YYYY-MM-DD)\n\n## v7.0.0 (2025-05-15)\n\n- Deps: remove unused dependency `watchexec-signals` ([#930]("
},
{
"path": "crates/filterer/ignore/Cargo.toml",
"chars": 1055,
"preview": "[package]\nname = \"watchexec-filterer-ignore\"\nversion = \"7.0.0\"\n\nauthors = [\"Félix Saparelli <felix@passcod.name>\"]\nlicen"
},
{
"path": "crates/filterer/ignore/README.md",
"chars": 952,
"preview": "[](https://crates.io/crates/watchexec-filterer-i"
},
{
"path": "crates/filterer/ignore/release.toml",
"chars": 329,
"preview": "pre-release-commit-message = \"release: filterer-ignore v{{version}}\"\ntag-prefix = \"watchexec-filterer-ignore-\"\ntag-messa"
},
{
"path": "crates/filterer/ignore/src/lib.rs",
"chars": 2246,
"preview": "//! A Watchexec Filterer implementation for ignore files.\n//!\n//! This filterer is meant to be used as a backing filtere"
},
{
"path": "crates/filterer/ignore/tests/filtering.rs",
"chars": 10859,
"preview": "use ignore_files::IgnoreFilter;\nuse watchexec_filterer_ignore::IgnoreFilterer;\n\nmod helpers;\nuse helpers::ignore::*;\n\n#["
},
{
"path": "crates/filterer/ignore/tests/helpers/mod.rs",
"chars": 3189,
"preview": "use std::path::{Path, PathBuf};\n\nuse ignore_files::{IgnoreFile, IgnoreFilter};\nuse watchexec::{error::RuntimeError, filt"
},
{
"path": "crates/filterer/ignore/tests/ignores/allowlist",
"chars": 109,
"preview": "# from https://github.com/github/gitignore\n\n*\n\n!/.gitignore\n\n!*.go\n!go.sum\n!go.mod\n\n!README.md\n!LICENSE\n\n!*/\n"
},
{
"path": "crates/filterer/ignore/tests/ignores/folders",
"chars": 63,
"preview": "prunes\n/apricots\ncherries/\n**/grapes/**\n**/feijoa\n**/feijoa/**\n"
},
{
"path": "crates/filterer/ignore/tests/ignores/globs",
"chars": 114,
"preview": "Cargo.toml\npackage.json\n*.gemspec\ntest-*\n*.sw*\nsources.*/\n/output.*\n**/possum\nzebra/**\nelep/**/hant\nsong/**/bird/\n"
},
{
"path": "crates/filterer/ignore/tests/ignores/negate",
"chars": 14,
"preview": "nah\n!nah.yeah\n"
},
{
"path": "crates/filterer/ignore/tests/ignores/none-allowed",
"chars": 2,
"preview": "*\n"
},
{
"path": "crates/filterer/ignore/tests/ignores/scopes-global",
"chars": 9,
"preview": "global.*\n"
},
{
"path": "crates/filterer/ignore/tests/ignores/scopes-local",
"chars": 8,
"preview": "local.*\n"
},
{
"path": "crates/filterer/ignore/tests/ignores/scopes-sublocal",
"chars": 11,
"preview": "sublocal.*\n"
},
{
"path": "crates/filterer/ignore/tests/ignores/self.ignore",
"chars": 12,
"preview": "self.ignore\n"
},
{
"path": "crates/ignore-files/CHANGELOG.md",
"chars": 2126,
"preview": "# Changelog\n\n## Next (YYYY-MM-DD)\n\n## v3.0.5 (2026-01-20)\n\n- Deps: gix-config 0.50\n- Deps: radix-trie 0.3\n- Fix: match g"
},
{
"path": "crates/ignore-files/Cargo.toml",
"chars": 1258,
"preview": "[package]\nname = \"ignore-files\"\nversion = \"3.0.5\"\n\nauthors = [\"Félix Saparelli <felix@passcod.name>\"]\nlicense = \"Apache-"
},
{
"path": "crates/ignore-files/README.md",
"chars": 622,
"preview": "[](https://crates.io/crates/ignore-files)\n[]\n#[non_exhaus"
},
{
"path": "crates/ignore-files/src/filter.rs",
"chars": 11903,
"preview": "use std::path::{Path, PathBuf};\n\nuse futures::stream::{FuturesUnordered, StreamExt};\nuse ignore::{\n\tgitignore::{Gitignor"
},
{
"path": "crates/ignore-files/src/lib.rs",
"chars": 1505,
"preview": "//! Find, parse, and interpret ignore files.\n//!\n//! Ignore files are files that contain ignore patterns, often followin"
},
{
"path": "crates/ignore-files/tests/filtering.rs",
"chars": 2273,
"preview": "mod helpers;\n\nuse helpers::ignore_tests::*;\n\n#[tokio::test]\nasync fn globals() {\n\tlet filter = filt(\n\t\t\"tree\",\n\t\t&[\n\t\t\tf"
},
{
"path": "crates/ignore-files/tests/global/first",
"chars": 7,
"preview": "apples\n"
},
{
"path": "crates/ignore-files/tests/global/second",
"chars": 8,
"preview": "oranges\n"
},
{
"path": "crates/ignore-files/tests/helpers/mod.rs",
"chars": 3614,
"preview": "use std::path::{Path, PathBuf};\n\nuse ignore::{gitignore::Glob, Match};\nuse ignore_files::{IgnoreFile, IgnoreFilter};\n\npu"
},
{
"path": "crates/ignore-files/tests/tree/base",
"chars": 34,
"preview": "/apples\ncarrots\npineapples/grapes\n"
},
{
"path": "crates/ignore-files/tests/tree/branch/inner",
"chars": 39,
"preview": "/cauliflowers\nartichokes\nbananas/pears\n"
},
{
"path": "crates/lib/CHANGELOG.md",
"chars": 23807,
"preview": "# Changelog\n\n## Next (YYYY-MM-DD)\n\n## v8.2.0 (2026-03-02)\n\n- Feat: add `fs_ready` signal for watcher readiness ([#1024]("
},
{
"path": "crates/lib/Cargo.toml",
"chars": 1773,
"preview": "[package]\nname = \"watchexec\"\nversion = \"8.2.0\"\n\nauthors = [\"Félix Saparelli <felix@passcod.name>\", \"Matt Green <mattgree"
},
{
"path": "crates/lib/README.md",
"chars": 8739,
"preview": "[](https://crates.io/crates/watchexec)\n[ -> Result<()> {\n\tlet wx "
},
{
"path": "crates/lib/examples/readme.rs",
"chars": 3692,
"preview": "use std::{\n\tsync::{Arc, Mutex},\n\ttime::Duration,\n};\n\nuse miette::{IntoDiagnostic, Result};\nuse watchexec::{\n\tcommand::{C"
},
{
"path": "crates/lib/examples/restart_run_on_successful_build.rs",
"chars": 1762,
"preview": "use std::sync::Arc;\n\nuse miette::{IntoDiagnostic, Result};\nuse watchexec::{\n\tcommand::{Command, Program, SpawnOptions},\n"
},
{
"path": "crates/lib/release.toml",
"chars": 285,
"preview": "pre-release-commit-message = \"release: lib v{{version}}\"\ntag-prefix = \"watchexec-\"\ntag-message = \"watchexec {{version}}\""
},
{
"path": "crates/lib/src/action/handler.rs",
"chars": 6980,
"preview": "use std::{collections::HashMap, path::Path, sync::Arc, time::Duration};\nuse tokio::task::JoinHandle;\nuse watchexec_event"
},
{
"path": "crates/lib/src/action/quit.rs",
"chars": 408,
"preview": "use std::time::Duration;\nuse watchexec_signals::Signal;\n\n/// How the Watchexec instance should quit.\n#[derive(Clone, Cop"
},
{
"path": "crates/lib/src/action/return.rs",
"chars": 806,
"preview": "use std::future::Future;\n\nuse super::ActionHandler;\n\n/// The return type of an action.\n///\n/// This is the type returned"
},
{
"path": "crates/lib/src/action/worker.rs",
"chars": 5144,
"preview": "use std::{\n\tcollections::HashMap,\n\tmem::take,\n\tsync::Arc,\n\ttime::{Duration, Instant},\n};\n\nuse async_priority_channel as "
},
{
"path": "crates/lib/src/action.rs",
"chars": 336,
"preview": "//! Processor responsible for receiving events, filtering them, and scheduling actions in response.\n\n#[doc(inline)]\npub "
},
{
"path": "crates/lib/src/changeable.rs",
"chars": 3008,
"preview": "//! Changeable values.\n\nuse std::{\n\tany::type_name,\n\tfmt,\n\tsync::{Arc, RwLock},\n};\n\n/// A shareable value that doesn't k"
},
{
"path": "crates/lib/src/config.rs",
"chars": 11633,
"preview": "//! Configuration and builders for [`crate::Watchexec`].\n\nuse std::{future::Future, pin::pin, sync::Arc, time::Duration}"
},
{
"path": "crates/lib/src/error/critical.rs",
"chars": 2175,
"preview": "use miette::Diagnostic;\nuse thiserror::Error;\nuse tokio::{sync::mpsc, task::JoinError};\nuse watchexec_events::{Event, Pr"
},
{
"path": "crates/lib/src/error/runtime.rs",
"chars": 6223,
"preview": "use miette::Diagnostic;\nuse thiserror::Error;\nuse watchexec_events::{Event, Priority};\nuse watchexec_signals::Signal;\n\nu"
},
{
"path": "crates/lib/src/error/specialised.rs",
"chars": 2592,
"preview": "use std::path::PathBuf;\n\nuse miette::Diagnostic;\nuse thiserror::Error;\n\n/// Errors emitted by the filesystem watcher.\n#["
},
{
"path": "crates/lib/src/error.rs",
"chars": 219,
"preview": "//! Error types for critical, runtime, and specialised errors.\n\n#[doc(inline)]\npub use critical::*;\n#[doc(inline)]\npub u"
},
{
"path": "crates/lib/src/filter.rs",
"chars": 2438,
"preview": "//! The `Filterer` trait for event filtering.\n\nuse std::{fmt, sync::Arc};\n\nuse watchexec_events::{Event, Priority};\n\nuse"
},
{
"path": "crates/lib/src/id.rs",
"chars": 1370,
"preview": "use std::{cell::Cell, num::NonZeroU64};\n\n/// Unique opaque identifier.\n#[must_use]\n#[derive(Debug, Hash, PartialEq, Eq, "
},
{
"path": "crates/lib/src/late_join_set.rs",
"chars": 2769,
"preview": "use std::future::Future;\n\nuse futures::{stream::FuturesUnordered, StreamExt};\nuse tokio::task::{JoinError, JoinHandle};\n"
},
{
"path": "crates/lib/src/lib.rs",
"chars": 3012,
"preview": "//! Watchexec: a library for utilities and programs which respond to (file, signal, etc) events\n//! primarily by launchi"
},
{
"path": "crates/lib/src/paths.rs",
"chars": 4331,
"preview": "//! Utilities for paths and sets of paths.\n\nuse std::{\n\tcollections::{HashMap, HashSet},\n\tffi::OsString,\n\tpath::{Path, P"
},
{
"path": "crates/lib/src/sources/fs.rs",
"chars": 8091,
"preview": "//! Event source for changes to files and directories.\n\nuse std::{\n\tcollections::{HashMap, HashSet},\n\tfs::metadata,\n\tmem"
},
{
"path": "crates/lib/src/sources/keyboard.rs",
"chars": 8242,
"preview": "//! Event source for keyboard input and related events\nuse std::io::Read;\nuse std::sync::atomic::{AtomicBool, Ordering};"
},
{
"path": "crates/lib/src/sources/signal.rs",
"chars": 3909,
"preview": "//! Event source for signals / notifications sent to the main process.\n\nuse std::sync::Arc;\n\nuse async_priority_channel "
},
{
"path": "crates/lib/src/sources.rs",
"chars": 70,
"preview": "//! Sources of events.\n\npub mod fs;\npub mod keyboard;\npub mod signal;\n"
},
{
"path": "crates/lib/src/watched_path.rs",
"chars": 1445,
"preview": "use std::path::{Path, PathBuf};\n\n/// A path to watch.\n///\n/// Can be a recursive or non-recursive watch.\n#[derive(Clone,"
},
{
"path": "crates/lib/src/watchexec.rs",
"chars": 9488,
"preview": "use std::{\n\tfmt,\n\tfuture::Future,\n\tsync::{Arc, OnceLock},\n};\n\nuse async_priority_channel as priority;\nuse atomic_take::A"
},
{
"path": "crates/lib/tests/env_reporting.rs",
"chars": 8144,
"preview": "use std::{collections::HashMap, ffi::OsString, path::MAIN_SEPARATOR};\n\nuse notify::event::CreateKind;\nuse watchexec::pat"
},
{
"path": "crates/lib/tests/error_handler.rs",
"chars": 417,
"preview": "use std::time::Duration;\n\nuse miette::Result;\nuse tokio::time::sleep;\nuse watchexec::{ErrorHook, Watchexec};\n\n#[tokio::m"
},
{
"path": "crates/project-origins/CHANGELOG.md",
"chars": 941,
"preview": "# Changelog\n\n## Next (YYYY-MM-DD)\n\n## v1.4.2 (2025-05-15)\n\n## v1.4.1 (2025-02-09)\n\n## v1.4.0 (2024-04-28)\n\n- Add out-of-"
},
{
"path": "crates/project-origins/Cargo.toml",
"chars": 952,
"preview": "[package]\nname = \"project-origins\"\nversion = \"1.4.2\"\n\nauthors = [\"Félix Saparelli <felix@passcod.name>\"]\nlicense = \"Apac"
},
{
"path": "crates/project-origins/README.md",
"chars": 649,
"preview": "[](https://crates.io/crates/project-origins)\n[
About this extraction
This page contains the full source code of the mattgreen/watchexec GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 232 files (756.8 KB), approximately 218.5k tokens, and a symbol index with 690 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.