bacon-3.12.0/.cargo_vcs_info.json0000644000000001360000000000100122010ustar { "git": { "sha1": "14f86c0ae2af18088e8721dcf3ef1441b87df182" }, "path_in_vcs": "" }bacon-3.12.0/.github/FUNDING.yml000064400000000000000000000000201046102023000141360ustar 00000000000000github: [Canop] bacon-3.12.0/.gitignore000064400000000000000000000001301046102023000127530ustar 00000000000000/target bacon.log /*deploy.sh /website/site .bacon-locations bacon-analysis.json result bacon-3.12.0/CHANGELOG.md000064400000000000000000000513731046102023000126130ustar 00000000000000 ### v3.12.0 - 2025/03/14 - cargo-json analyzer (for bacon-ls): fix invalid spans for errors from proc-macros - Fix #332 - Thanks @Leandros - `back` action, usually mapped to the `esc` key, no longer quits on first job. If you want the old behavior, add this binding: `esc = "back-or-quit"`. Fix #338 - Thanks @ian-h-chamberlain ### v3.11.0 - 2025/03/03 - hit `:` then type an integer to go to a diagnostic by number - Fix #104 - standard test analyzer: fix stack overflow not detected - Fix #326 - Thanks @gmorenz #### Dynamic Completion The script providing location aware completion needs to be sourced once. This can be done for example in your .profile with ```bash source <(COMPLETE=bash bacon) ``` (adapt for your shell) This feature is still experimental. Please give feedback, positive or negative, in the chat. Thanks @bryceberger ### v3.10.0 - 2025/02/09 - all job parameters can now be specified at the root (to be applied to all jobs) - `grace_period`, `show_change_count`, `sound.enabled`, and `sound.base_volume` can now be specified at job level - `no-op` (no operation) internal, which can be used to disable a previously set binding ### v3.9.1 - 2025/01/28 - as compilation of Alsa can be a problem on some systems, the "sound" feature is now disabled by default. You can enable it by compiling with `--features "sound"` - Fix #319 ### v3.9.0 - 2025/01/26 - sound can be enabled with `sound.enabled = true`, which allows adding job parameters such as `on_success = "play-sound(name=90s-game-ui-6,volume=50)"` and `on_failure = "play-sound(name=beep-warning)"` - Fix #303 - fix freeze on mac on config change - Fix #306 - Thanks @irh - fix race condition on config reload when saved by helix - Fix #310 ### v3.8.0 - 2025/01/17 - with `--headless`, bacon runs without TUI - Fix #293 - `--config-toml` argument - Fix #284 - fix workspace level Cargo.toml file not watched - `copy-unstyled-output` internal that you can bind with eg `ctrl-c = "copy-unstyled-output"`. It's currently gated by the `"clipboard"` feature, please give feedback regarding compilation and usage - Fix #282 - Thanks @letsgetrusty - list of loaded config files displayed in help page ### v3.7.0 - 2024/12/27 - search with the / key - Fix #224 - fix nextest analyzer not capturing test output with nextest 0.9.86+ - Fix #280 - show an error if the command fails to spawn - Thanks @jyn514 ### v3.6.0 - 2024/12/15 - support for cpp (gcc & clang) with `analyzer = "cpp"` - Thanks @bryceberger - removal of the `--path` argument, replaced with `--project` and `--watch` (overrides the list of watched files). The path to the project can also be given as trailing argument as today. - Fix #274 - the `cargo_json` analyzer can now be leveraged to export data from the cargo metadata `Diagnostic` and `DiagnosticSpan` structs - Fix #249 ### v3.5.0 - 2024/12/05 - support for biome with `analyzer = "biome"` - support for ruff with `analyzer = "python_ruff"` - read bacon.toml in workspace/.config and package/.config - Fix #268 - read `workspace.metadata.bacon` and `package.metadata.bacon` config elements in `Cargo.toml` files - Fix #241 - fix locations export when launching bacon inside a rust workspace but with a non cargo tool ### v3.4.0 - 2024/11/30 - new analyzer framework, make it possible for bacon to call more tools - Python Pytest analyzer - Analyzer for `cargo check --message-format json-diagnostic-rendered-ansi` - see #269 - allow specifying scroll-pages action with a floating point number - Fix #264 ### v3.3.0 - 2024/11/16 - bacon can now be launched without Cargo.toml file - eslint analyzer (set `analyzer = "eslint"` in your job definition) - Python Unittest analyzer (set `analyzer = "python_unittest"` in your job definition) - fix Miri output seen as wrong when there's only warnings - allow defining environment vars for all jobs - Thanks @joshka - set `env.CARGO_TERM_COLOR = "always"` in default conf, thus making `"--color", "always"` useless in all cargo based job definition - Thanks @joshka - new `ignore` job parameter, accepts a list of glob patterns - more lenient detection of warnings and errors due to 'miri run' not supporting `--color` - Fix #251 ### v3.2.0 - 2024/11/04 - allow defining `default_watch` and `watch` at global level, so that they apply to all jobs unless overridden. ### v3.1.2 - 2024/10/29 - "config loaded" message always automatically disappears after a few seconds ### v3.1.1 - 2024/10/18 #### Major feature: hot reload of config files When a configuration file is modified, bacon automatically reloads its config. So you don't need to quit/relaunch when you add a new job, add a key-binding, change the allowed lints of clippy, etc. - Fix #29 ### v3.0.0 - 2024/10/09 #### Major feature: nextest support Hit `n` to launch the nextest job. It's a default job, but you may define your own one by specifying `analyzer = "nextest"` in the job entry. Internally, this is supported by a new analyzer framework which will allow easier analysis updates or addition of analysis for other tools (or languages). Fix #196 #### Major feature: scope test job to failure If you're running a test or nextest job and you want only the failing test to be retried, hit `f`. If you want all tests to be executed again, hit `esc`. Fix #214 #### Other features: - grace period (by default 5ms) after a file event before the real launch of the command and during which other file events may be disregarded. Helps when saving a file changes several ones (eg backup then rename). - new `exports` structure in configuration. New `analysis` export bound by default to `ctrl-e`. The old syntax defining locations export is still supported but won't appear in documentations anymore. - recognize panic location in test - Fix #208 - lines to ignore can be specified as a set of regular expressions in a `ignored_lines` field either in the job or at the top of the prefs or bacon.toml - Fix #223 - `toggle-backtrace` accepts an optional level: `toggle-backtrace(1)` or `toggle-backtrace(full)` - Experimental - Fix #210 - configuration paths can be passed in `BACON_PREFS` and `BACON_CONFIG` env vars - Fix #76 #### Fixes: - fix changing wrapping mode not always working in raw output mode - Fix #234 ### v2.21.0 - 2024/09/14 With `show_changes_count=true`, you can see the number of file changes that occurred since last job start. #### Major change: the `on_change_strategy` setting and a new default strategy * With `on_change_strategy = "kill_then_restart"`, the current job is immediately killed and a new job restarted. This is the behavior that bacon had before this PR. It has the downside of never allowing any job to complete if you're always changing files and the job is just a little too long to finish between changes. * With `on_change_strategy = "wait_then_restart"` (which is the new default, so you can omit it), bacon waits for the job to finish before restarting it. This is probably much better when the jobs aren't instant and you want to continue changing files while it's computing. The on_change_strategy can be defined in the global prefs, in the project settings, and even for a specific job. ### v2.20.0 - 2024/08/13 - until now, when there was no `bacon.toml` file, the default one was applied, overriding the settings of `prefs.toml`. This is no longer the case: this default file is now applied before `prefs.toml` (which overrides it) - Fix #157 - `kill` job parameter - Thanks @pcapriotti ### v2.19.0 - 2024/08/07 - `extraneous_args` job parameter - Thanks @TheTollingBell - pause/unpause bound to 'p' - Fix #194 ### v2.18.2 - 2024/05/31 - fix failure to recognize location in test compilation output - Fix #190 ### v2.18.1 - 2024/05/21 - update dependencies (especially locked ones) to fix compilation on nightly ### v2.18.0 - 2024/05/20 - new `{context}` possible part for exported locations, originally designed for [bacon-ls](https://github.com/crisidev/bacon-ls) but available for other purposes - Thanks @crisidev ### v2.17.0 - 2024/05/05 - default binding for 'c' in bacon.toml is now the new 'clippy-all' job which does what the old 'clippy' job was doing. 'clippy' job changed to not run on all targets. Default bacon.toml explain how to bind 'c' to clippy instead of 'clippy-all' - Fix #167 - expand env vars in job command unless the job specifies `expand_env_vars = false` - Fix #181 - some file events filtered out from watch (feedback welcome, especially if you notice some failures to recompute) - parse test results even when tests are run with `-q`/`--quiet` - Thanks @narpfel ### v2.16.0 - 2024/03/30 - `on_success` triggered with warning or errors depending on `allow_warnings` and `allow_failures` - Fix #179 - `--no-help-line` option. This is experimental and may be removed depending on feedback and future additions to this line - Thanks @danielwolbach ### v2.15.0 - 2024/03/05 - insert features related arguments before the -- when there's some - Fix #171 - fix offset in Windows terminal - Fix #175 - better `--help` with examples and main shortcuts - rewriten execution engine ### v2.14.2 - 2024/02/10 - update dependencies to fix bacon not compiling on nightly - Fix #168 ### v2.14.1 - 2023/12/24 - fix output not cleared when cargo is in quiet mode and there's nothing - Fix #131 ### v2.14.0 - 2023/10/04 - F5 now clears the output before running the job (now bound to `refresh` internal) - new optional `background` job parameter, should be set to `false` for never ending jobs - Fix #161 ### v2.13.0 - 2023/09/15 - fix mouse wheel scrolling not working on Windows - Fix #153 - Thanks @Adham-A - detect locations in test failures, thus enabling jumps to those failures - more relevant suggestions in help line - add a default job for running examples ### v2.12.1 - 2023/07/22 - fix some scroll problem, especially in reverse - Fix #86 ### v2.12.0 - 2023/07/20 - better `--help` ### v2.11.1 - 2023/07/13 - fix warning summary sometimes confused with a warning - Fix #149 ### v2.11.0 - 2023/06/30 - allow defining env vars for jobs - Fix #145 ### v2.10.0 - 2023/06/27 - accept bacon.toml file at workspace level - Fix #141 ### v2.9.0 - 2023/06/19 - export format and path can now be configured - default export format includes error/warning summary (nvim-bacon has been updated in parallel) - Fix #127 - fix output non scrollable when non parsable - fix test non parsed when styled and sent to stdout instead of stderr - Fix #137 ### v2.8.1 - 2023/04/22 - color rendering of cargo test - Fix #124 ### v2.8.0 - 2023/03/23 - By default, "src", "tests", "benches", "examples" are now watched - Fix #119 - `default_watch` bool job parameter - Fix #92 ### v2.7.0 - 2023/03/14 - watch "examples" directory in default run job - fix warnings not recognized on Windows - Fix #70 - Thanks @crillon ### v2.6.3 - 2023/03/09 - remove keybindings from default bacon.toml - Fix #116 ### v2.6.2 - 2023/03/03 - more consistent "pass!" - Thanks @zolrath ### v2.6.1 - 2023/02/22 - fix a dependency compilation problem - Fix #112 ### v2.6.0 - 2023/02/21 - change default value of 'wrap' setting to true - `--offline` experimental launch argument, prevents bacon (but not jobs) from accessing the network. Downside is a potentially less relevant list of watched files and directories - Fix #110 ### v2.5.0 - 2023/01/19 - new `allow_failures` job parameter - Fix #99 - `rerun` internal bound by default to F5 - Fix #105 ### v2.4.0 - 2023/01/12 Major feature: The global prefs.toml and the local bacon.toml file now have the same properties, the local bacon.toml overriding the global prefs.toml file. Among the consequences: you can have a list of default global jobs; you can set a different preferences (eg wrapping, summary, etc.) for a specific repository. The default configuration files and the recommended best practices are unchanged - Fix #101 ### v2.3.0 - 2022/12/30 - doesn't launch job when the modified file is excluded by gitignore rules - Fix #32 ### v2.2.8 - 2022/12/15 - remove double-dash from default run configuration - Fix #96 ### v2.2.7 - 2022/12/14 - capture output of "should panic" tests - Fix #95 ### v2.2.6 - 2022/12/08 - fix a compilation problem - Fix #94 ### v2.2.5 - 2022/10/08 - fix wrong scrollbar in several cases of wrapping ### v2.2.4 - 2022/10/05 - fix inability to scroll to last line sometimes ### v2.2.3 - 2022/09/17 - fix a compilation problem on Window - Thanks @Stargateur - Fix #87 ### v2.2.2 - 2022/08/28 - define a new `allow_warnings` job setting. When it's true, the job is considered successful even when there are warnings. This is default on the `run` job, which means the `cargo run` output is displayed even when there are warnings - Fix #81 - allow `cargo --prefs` to be ran from outside cargo projects - Fix #84 ### v2.2.1 - 2022/05/12 - update some dependencies ### v2.2.0 - 2022/05/12 - Locations exported in .bacon-locations now made absolute so that IDE plugins don't have to know the package's root - job cancelling now works on unresponsive jobs too. This is a quite heavy change as the current implementation involves bringing in async and it's not 100% clean but it solves a major problem, further improvements could be welcome - Fix #78 - Thanks @nolanderc - you can refer to cargo aliases by prefixing jobs with `alias:`, either when setting up keybindings, defaults, or when launching bacon. Example: `bacon alias:q` to launch the cargo task aliased as `q` - Fix #77 ### v2.1.0 - 2022/03/26 Major feature: The `export-locations` argument (shortened in `-e`) generates a `.bacon-locations` file which can be used by IDE plugins. A plugin has been made for neovim: [nvim-bacon](https://github.com/Canop/nvim-bacon) and other ones would be welcome. Minor changes: - wrapping now applies to all outputs, even non interpreted ones like the output of `cargo run`. ### v2.0.1 - 2022/02/18 - fix summary of warnings counted as warning ### v2.0.0 - 2022/02/16 #### Major features: - It's now possible to configure key bindings in the prefs.toml file. Those key bingings can trigger internal actions (scrolling, toggling, quitting) or jobs (for example you can launch `cargo test` on the `t` key. - Fix #52 #### Other changes: - help page, listing all key-bindings - a job is said to be *successful* when there's no error, test failure or warning. When a job is successful, its output is displayed by bacon. This makes it possible to have a `cargo run` job, for example. - it's possible to define an *action* to run when a job is successful. For example you can launch a `cargo doc --open` job on a key, and have bacon switch to the previous job with the `on_success = "back` trigger so that you don't open a browser page on every change - arguments given after `--` are given to the job - Fix #67 - there's a web documentation site now, you should have a look: https://dystroy.org/bacon Minor changes: - fix character being lost behind scrollbar on wrapping - replaced argh with clap for launch arg parsing. The `--help` presentation is thus different. `bacon -h` now supported. ### v1.2.5 - 2022/01/29 - fix missing output of "no_run" doctests - Fix #64 - restrict naming of jobs to [\w-]+ regex (you were unlikely to use other chars due to the TOML format anyway) ### v1.2.4 - 2021/11/27 - fix inability to deal with some inter-member dependencies on Windows - Fix #59 - Thanks @jDomantas - fix compilation broken due to change in anyhow 1.0.49 - Fix #63 ### v1.2.3 - 2021/11/15 - add the "clippy-all" default job - Thanks @rukai - alpha sort the table outputted by `bacon --list-jobs` - Thanks @rukai ### v1.2.2 - 2021/10/18 - solve a dependency build problem - Fix #55 ### v1.2.1 - 2021/10/03 - propose to toggle backtraces when suggestion is found in cargo's output ### v1.1.8 - 2021/07/31 - move to more recent versions of some crates - Fix #51 - `bacon --list-jobs` (or `bacon -l`) lists all available jobs ### v1.1.7 - 2021/07/11 - recognize doc test output - Fix #49 - display 4 spaces for tabs - Fix #50 ### v1.1.6 - 2021/06/22 - the default conf now contains a [doc] job - `--all-features` launch option ### v1.1.5 - 2021/02/27 - fix wrong version number in bacon.log ### v1.1.4 - 2021/02/10 It's possible to define directories to watch in the bacon.toml config file. For example, by default the `test` job watches the `tests` directory if it exists - Thanks @SafariMonkey ### v1.1.3 - 2021/01/29 * `check-all` target now checks all - Fix #27 * `--no-default-features` and `--features` - Fix #31 ### v1.1.2 - 2021/01/05 Revert standard job to ignore tests because compilation with them is too slow. A new default job is added. ### v1.1.1 - 2021/01/03 Don't consider test fails as command fails (ie display the count of test fails in `bacon test` instead of command error) ### v1.1.0 - 2020/12/26 If the job's command returns an error code and no error was read in the output, bacon now displays the output and the error code instead of letting the user think there's no error ### v1.0.1 - 2020/11/21 * vim key bindings can be enabled in prefs * default job is now `cargo check --tests` to check the code for tests compiles too (without running them) ### v1.0.0 - 2020/11/19 * nothing new... so it's stable enough to be tagged 1.0 ### v0.6.0 - 2020/11/15 * `bacon test` shows test failures - Fix #3 - Note that you need to remove then rebuild your bacon.toml file to use this new job ### v0.5.3 - 2020/11/14 * "reverse" option allows having the focus on bottom - Fix #19 * initial compilation autoscroll based on scroll position - Fix #22 * remove flickering ### v0.5.2 - 2020/11/14 * fix bacon ending with an error when prefs file is missing ### v0.5.1 - 2020/11/13 * `bacon --prefs` shows or creates a prefs file which can be changed to defined default display settings (currently "summary" and "wrap") ### v0.5.0 - 2020/11/12 * `bacon --init` creates a default `bacon.toml` file which can be customized to add jobs or change the standard ones * bacon launch arguments changed to ease use of customized jobs ### v0.4.3 - 2020/11/11 * fix report only taking the first package into account (for workspaces) ### v0.4.2 - 2020/11/11 * fix some regressions in error and warning detection ### v0.4.1 - 2020/11/10 * reduce useless redraws during computation ### v0.4.0 - 2020/11/10 * make it possible to watch only part of the sources: the passed directory (or the current one), when not a package directory (i.e. not containing a Cargo.toml file), will be the one watched - Thanks @nikhilmitrax and @jyn514 for their help * logo - Thanks @petervaro * line wrapping (and rewrapping on resize) ### v0.3.2 - 2020/11/08 * when quitting bacon, kill `cargo check` if running ### v0.3.1 - 2020/11/06 * better scroll position after toggling summary mode or resizing * space key now usable for page down ### v0.3.0 - 2020/11/06 * keep lines with location in summary mode - Fix #11 * allow scrolling the report (arrow keys, page keys, home & end keys, mouse wheel) - Fix #6 * log file renamed to 'bacon.log' to avoid collisions * initial execution is displayed raw before report computation - Fix #8 * initial execution can be interrupted, scrolled - Fix #12 ### v0.2.0 - 2020-10-01 * add the summary mode ### v0.1.1 - 2020-09-29 * also watches Cargo.toml ### v0.1.0 - 2020-09-29 Initial version bacon-3.12.0/Cargo.lock0000644000002404440000000000100101640ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "once_cell", "version_check", "zerocopy", ] [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alsa" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", "bitflags 2.9.0", "cfg-if", "libc", ] [[package]] name = "alsa-sys" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" dependencies = [ "libc", "pkg-config", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", "once_cell", "windows-sys 0.59.0", ] [[package]] name = "anyhow" version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "arboard" version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df099ccb16cd014ff054ac1bf392c67feeef57164b05c42f037cd40f5d4357f4" dependencies = [ "clipboard-win", "log", "objc2", "objc2-app-kit", "objc2-foundation", "parking_lot", "x11rb", ] [[package]] name = "arc-swap" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bacon" version = "3.12.0" dependencies = [ "anyhow", "arboard", "cargo_metadata", "clap", "clap-help", "clap_complete", "cli-log", "crokey", "ctrlc", "directories-next", "gix", "glob", "iq", "lazy-regex", "notify", "rodio", "rustc-hash 2.1.1", "serde", "serde_json", "termimad", "toml", "unicode-width 0.2.0", "vte", ] [[package]] name = "bindgen" version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ "bitflags 2.9.0", "cexpr", "clang-sys", "itertools", "proc-macro2", "quote", "regex", "rustc-hash 1.1.0", "shlex", "syn 2.0.100", ] [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ "objc2", ] [[package]] name = "bstr" version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "regex-automata", "serde", ] [[package]] name = "bumpalo" version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "camino" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" dependencies = [ "serde", ] [[package]] name = "cargo-platform" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" dependencies = [ "serde", ] [[package]] name = "cargo_metadata" version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", "semver", "serde", "serde_json", "thiserror 2.0.12", ] [[package]] name = "cc" version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ "jobserver", "libc", "shlex", ] [[package]] name = "cesu8" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-link", ] [[package]] name = "clang-sys" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", "libloading", ] [[package]] name = "clap" version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap-help" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc01b70b5fd7e87b2ae778cfd151120355002f44ab1504e9943151a52cae8171" dependencies = [ "clap", "termimad", "terminal-light", ] [[package]] name = "clap_builder" version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", ] [[package]] name = "clap_complete" version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5c5508ea23c5366f77e53f5a0070e5a84e51687ec3ef9e0464c86dc8d13ce98" dependencies = [ "clap", "clap_lex", "is_executable", "shlex", ] [[package]] name = "clap_derive" version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck", "proc-macro2", "quote", "syn 2.0.100", ] [[package]] name = "clap_lex" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cli-log" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e220aa46e5395cd473a054f8e7e52403108ce147a4eb68c001afb01672a4e046" dependencies = [ "chrono", "file-size", "log", "proc-status", ] [[package]] name = "clipboard-win" version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", ] [[package]] name = "clru" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "combine" version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", ] [[package]] name = "coolor" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "691defa50318376447a73ced869862baecfab35f6aabaa91a4cd726b315bfe1a" dependencies = [ "crossterm", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "coreaudio-rs" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" dependencies = [ "bitflags 1.3.2", "core-foundation-sys", "coreaudio-sys", ] [[package]] name = "coreaudio-sys" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" dependencies = [ "bindgen", ] [[package]] name = "cpal" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" dependencies = [ "alsa", "core-foundation-sys", "coreaudio-rs", "dasp_sample", "jni", "js-sys", "libc", "mach2", "ndk", "ndk-context", "oboe", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", "windows", ] [[package]] name = "crc32fast" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crokey" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520e83558f4c008ac06fa6a86e5c1d4357be6f994cce7434463ebcdaadf47bb1" dependencies = [ "crokey-proc_macros", "crossterm", "once_cell", "serde", "strict", ] [[package]] name = "crokey-proc_macros" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "370956e708a1ce65fe4ac5bb7185791e0ece7485087f17736d54a23a0895049f" dependencies = [ "crossterm", "proc-macro2", "quote", "strict", "syn 1.0.109", ] [[package]] name = "crossbeam" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", "crossbeam-queue", "crossbeam-utils", ] [[package]] name = "crossbeam-channel" version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ "bitflags 2.9.0", "crossterm_winapi", "mio", "parking_lot", "rustix 0.38.44", "signal-hook", "signal-hook-mio", "winapi", ] [[package]] name = "crossterm_winapi" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ "winapi", ] [[package]] name = "ctrlc" version = "3.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" dependencies = [ "nix", "windows-sys 0.59.0", ] [[package]] name = "dasp_sample" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] name = "directories-next" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" dependencies = [ "cfg-if", "dirs-sys-next", ] [[package]] name = "dirs-sys-next" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", "winapi", ] [[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", "syn 2.0.100", ] [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encoding_rs" version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "error-code" version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "faster-hex" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" dependencies = [ "serde", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "file-size" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9544f10105d33957765016b8a9baea7e689bf1f0f2f32c2fa2f568770c38d2b3" [[package]] name = "filetime" version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", "libredox", "windows-sys 0.59.0", ] [[package]] name = "flate2" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "fsevent-sys" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" dependencies = [ "libc", ] [[package]] name = "gethostname" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ "libc", "windows-targets 0.48.5", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ "cfg-if", "libc", "wasi 0.13.3+wasi-0.2.2", "windows-targets 0.52.6", ] [[package]] name = "gix" version = "0.67.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7d3e78ddac368d3e3bfbc2862bc2aafa3d89f1b15fed898d9761e1ec6f3f17f" dependencies = [ "gix-actor", "gix-commitgraph", "gix-config", "gix-date", "gix-diff", "gix-discover", "gix-features", "gix-fs", "gix-glob", "gix-hash", "gix-hashtable", "gix-ignore", "gix-index", "gix-lock", "gix-object", "gix-odb", "gix-pack", "gix-path", "gix-ref", "gix-refspec", "gix-revision", "gix-revwalk", "gix-sec", "gix-tempfile", "gix-trace", "gix-traverse", "gix-url", "gix-utils", "gix-validate", "gix-worktree", "once_cell", "smallvec", "thiserror 1.0.69", ] [[package]] name = "gix-actor" version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20018a1a6332e065f1fcc8305c1c932c6b8c9985edea2284b3c79dc6fa3ee4b2" dependencies = [ "bstr", "gix-date", "gix-utils", "itoa", "thiserror 2.0.12", "winnow 0.6.26", ] [[package]] name = "gix-bitmap" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1db9765c69502650da68f0804e3dc2b5f8ccc6a2d104ca6c85bc40700d37540" dependencies = [ "thiserror 2.0.12", ] [[package]] name = "gix-chunk" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b1f1d8764958699dc764e3f727cef280ff4d1bd92c107bbf8acd85b30c1bd6f" dependencies = [ "thiserror 2.0.12", ] [[package]] name = "gix-commitgraph" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8da6591a7868fb2b6dabddea6b09988b0b05e0213f938dbaa11a03dd7a48d85" dependencies = [ "bstr", "gix-chunk", "gix-features", "gix-hash", "memmap2", "thiserror 2.0.12", ] [[package]] name = "gix-config" version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bedd1bf1c7b994be9d57207e8e0de79016c05e2e8701d3015da906e65ac445e" dependencies = [ "bstr", "gix-config-value", "gix-features", "gix-glob", "gix-path", "gix-ref", "gix-sec", "memchr", "once_cell", "smallvec", "thiserror 1.0.69", "unicode-bom", "winnow 0.6.26", ] [[package]] name = "gix-config-value" version = "0.14.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11365144ef93082f3403471dbaa94cfe4b5e72743bdb9560719a251d439f4cee" dependencies = [ "bitflags 2.9.0", "bstr", "gix-path", "libc", "thiserror 2.0.12", ] [[package]] name = "gix-date" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c57c477b645ee248b173bb1176b52dd528872f12c50375801a58aaf5ae91113f" dependencies = [ "bstr", "itoa", "jiff", "thiserror 2.0.12", ] [[package]] name = "gix-diff" version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9850fd0c15af113db6f9e130d13091ba0d3754e570a2afdff9e2f3043da260e" dependencies = [ "bstr", "gix-hash", "gix-object", "thiserror 1.0.69", ] [[package]] name = "gix-discover" version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c522e31f458f50af09dfb014e10873c5378f702f8049c96f508989aad59671f6" dependencies = [ "bstr", "dunce", "gix-fs", "gix-hash", "gix-path", "gix-ref", "gix-sec", "thiserror 1.0.69", ] [[package]] name = "gix-features" version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d85d673f2e022a340dba4713bed77ef2cf4cd737d2f3e0f159d45e0935fd81f" dependencies = [ "crc32fast", "crossbeam-channel", "flate2", "gix-hash", "gix-trace", "gix-utils", "libc", "once_cell", "parking_lot", "prodash", "sha1_smol", "thiserror 2.0.12", "walkdir", ] [[package]] name = "gix-fs" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3d4fac505a621f97e5ce2c69fdc425742af00c0920363ca4074f0eb48b1db9" dependencies = [ "fastrand", "gix-features", "gix-utils", ] [[package]] name = "gix-glob" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaf69a6bec0a3581567484bf99a4003afcaf6c469fd4214352517ea355cf3435" dependencies = [ "bitflags 2.9.0", "bstr", "gix-features", "gix-path", ] [[package]] name = "gix-hash" version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b5eccc17194ed0e67d49285e4853307e4147e95407f91c1c3e4a13ba9f4e4ce" dependencies = [ "faster-hex", "thiserror 2.0.12", ] [[package]] name = "gix-hashtable" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef65b256631078ef733bc5530c4e6b1c2e7d5c2830b75d4e9034ab3997d18fe" dependencies = [ "gix-hash", "hashbrown 0.14.5", "parking_lot", ] [[package]] name = "gix-ignore" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6b1fb24d2a4af0aa7438e2771d60c14a80cf2c9bd55c29cf1712b841f05bb8a" dependencies = [ "bstr", "gix-glob", "gix-path", "gix-trace", "unicode-bom", ] [[package]] name = "gix-index" version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27619009ca1ea33fd885041273f5fa5a09163a5c1d22a913b28d7b985e66fe29" dependencies = [ "bitflags 2.9.0", "bstr", "filetime", "fnv", "gix-bitmap", "gix-features", "gix-fs", "gix-hash", "gix-lock", "gix-object", "gix-traverse", "gix-utils", "gix-validate", "hashbrown 0.14.5", "itoa", "libc", "memmap2", "rustix 0.38.44", "smallvec", "thiserror 1.0.69", ] [[package]] name = "gix-lock" version = "15.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd3ab68a452db63d9f3ebdacb10f30dba1fa0d31ac64f4203d395ed1102d940" dependencies = [ "gix-tempfile", "gix-utils", "thiserror 2.0.12", ] [[package]] name = "gix-object" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a77b6e7753d298553d9ae8b1744924481e7a49170983938bb578dccfbc6fc1a" dependencies = [ "bstr", "gix-actor", "gix-date", "gix-features", "gix-hash", "gix-hashtable", "gix-utils", "gix-validate", "itoa", "smallvec", "thiserror 1.0.69", "winnow 0.6.26", ] [[package]] name = "gix-odb" version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb86aadf7f1b2f980601b4fc94309706f9700f8008f935dc512d556c9e60f61" dependencies = [ "arc-swap", "gix-date", "gix-features", "gix-fs", "gix-hash", "gix-hashtable", "gix-object", "gix-pack", "gix-path", "gix-quote", "parking_lot", "tempfile", "thiserror 1.0.69", ] [[package]] name = "gix-pack" version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "363e6e59a855ba243672408139db68e2478126cdcfeabb420777df4a1f20026b" dependencies = [ "clru", "gix-chunk", "gix-features", "gix-hash", "gix-hashtable", "gix-object", "gix-path", "memmap2", "smallvec", "thiserror 1.0.69", ] [[package]] name = "gix-path" version = "0.10.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c40f12bb65a8299be0cfb90fe718e3be236b7a94b434877012980863a883a99f" dependencies = [ "bstr", "gix-trace", "home", "once_cell", "thiserror 2.0.12", ] [[package]] name = "gix-quote" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e49357fccdb0c85c0d3a3292a9f6db32d9b3535959b5471bb9624908f4a066c6" dependencies = [ "bstr", "gix-utils", "thiserror 2.0.12", ] [[package]] name = "gix-ref" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a47385e71fa2d9da8c35e642ef4648808ddf0a52bc93425879088c706dfeaea2" dependencies = [ "gix-actor", "gix-features", "gix-fs", "gix-hash", "gix-lock", "gix-object", "gix-path", "gix-tempfile", "gix-utils", "gix-validate", "memmap2", "thiserror 1.0.69", "winnow 0.6.26", ] [[package]] name = "gix-refspec" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0022038a09d80d9abf773be8efcbb502868d97f6972b8633bfb52ab6edaac442" dependencies = [ "bstr", "gix-hash", "gix-revision", "gix-validate", "smallvec", "thiserror 1.0.69", ] [[package]] name = "gix-revision" version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee8eb4088fece3562af4a5d751e069f90e93345524ad730512185234c4b55f1" dependencies = [ "bstr", "gix-commitgraph", "gix-date", "gix-hash", "gix-object", "gix-revwalk", "thiserror 1.0.69", ] [[package]] name = "gix-revwalk" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c9a9496da98d36ff19063a8576bf09a87425583b709a56dc5594fffa9d39b2" dependencies = [ "gix-commitgraph", "gix-date", "gix-hash", "gix-hashtable", "gix-object", "smallvec", "thiserror 1.0.69", ] [[package]] name = "gix-sec" version = "0.10.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d84dae13271f4313f8d60a166bf27e54c968c7c33e2ffd31c48cafe5da649875" dependencies = [ "bitflags 2.9.0", "gix-path", "libc", "windows-sys 0.52.0", ] [[package]] name = "gix-tempfile" version = "15.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2feb86ef094cc77a4a9a5afbfe5de626897351bbbd0de3cb9314baf3049adb82" dependencies = [ "gix-fs", "libc", "once_cell", "parking_lot", "tempfile", ] [[package]] name = "gix-trace" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c396a2036920c69695f760a65e7f2677267ccf483f25046977d87e4cb2665f7" [[package]] name = "gix-traverse" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f20f1b13cc4fa6ba92b24e6aa0c2fb6a34beb4458ef88c6300212db504e818df" dependencies = [ "bitflags 2.9.0", "gix-commitgraph", "gix-date", "gix-hash", "gix-hashtable", "gix-object", "gix-revwalk", "smallvec", "thiserror 1.0.69", ] [[package]] name = "gix-url" version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d096fb733ba6bd3f5403dba8bd72bdd8809fe2b347b57844040b8f49c93492d9" dependencies = [ "bstr", "gix-features", "gix-path", "percent-encoding", "thiserror 2.0.12", "url", ] [[package]] name = "gix-utils" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff08f24e03ac8916c478c8419d7d3c33393da9bb41fa4c24455d5406aeefd35f" dependencies = [ "fastrand", "unicode-normalization", ] [[package]] name = "gix-validate" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eaa01c3337d885617c0a42e92823922a2aea71f4caeace6fe87002bdcadbd90" dependencies = [ "bstr", "thiserror 2.0.12", ] [[package]] name = "gix-worktree" version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d345e5b523550fe4fa0e912bf957de752011ccfc87451968fda1b624318f29c" dependencies = [ "bstr", "gix-features", "gix-fs", "gix-glob", "gix-hash", "gix-ignore", "gix-index", "gix-object", "gix-path", ] [[package]] name = "glob" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", ] [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "home" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "iana-time-zone" version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core 0.52.0", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "icu_collections" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_locid" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", "tinystr", "writeable", "zerovec", ] [[package]] name = "icu_locid_transform" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" dependencies = [ "displaydoc", "icu_locid", "icu_locid_transform_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_locid_transform_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" [[package]] name = "icu_normalizer" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", "icu_provider", "smallvec", "utf16_iter", "utf8_iter", "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" [[package]] name = "icu_properties" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", "icu_collections", "icu_locid_transform", "icu_properties_data", "icu_provider", "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" [[package]] name = "icu_provider" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", "icu_locid", "icu_provider_macros", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", "zerovec", ] [[package]] name = "icu_provider_macros" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", "syn 2.0.100", ] [[package]] name = "idna" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", "utf8_iter", ] [[package]] name = "idna_adapter" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", ] [[package]] name = "indexmap" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "hashbrown 0.15.2", ] [[package]] name = "inotify" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" dependencies = [ "bitflags 1.3.2", "inotify-sys", "libc", ] [[package]] name = "inotify-sys" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" dependencies = [ "libc", ] [[package]] name = "instant" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] [[package]] name = "iq" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5965c1d0f04558bc506736debf41699d807a1c6e1a7963efca7d85871257ee91" dependencies = [ "lazy-regex", "serde", "serde_json", ] [[package]] name = "is_executable" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2" dependencies = [ "winapi", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c04ef77ae73f3cf50510712722f0c4e8b46f5aaa1bf5ffad2ae213e6495e78e5" dependencies = [ "jiff-tzdb-platform", "log", "portable-atomic", "portable-atomic-util", "serde", "windows-sys 0.59.0", ] [[package]] name = "jiff-tzdb" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "962e1dfe9b2d75a84536cf5bf5eaaa4319aa7906c7160134a22883ac316d5f31" [[package]] name = "jiff-tzdb-platform" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a63c62e404e7b92979d2792352d885a7f8f83fd1d0d31eea582d77b2ceca697e" dependencies = [ "jiff-tzdb", ] [[package]] name = "jni" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", "cfg-if", "combine", "jni-sys", "log", "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] [[package]] name = "jni-sys" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "js-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "kqueue" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" dependencies = [ "kqueue-sys", "libc", ] [[package]] name = "kqueue-sys" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" dependencies = [ "bitflags 1.3.2", "libc", ] [[package]] name = "lazy-regex" version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126" dependencies = [ "lazy-regex-proc_macros", "once_cell", "regex", ] [[package]] name = "lazy-regex-proc_macros" version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1" dependencies = [ "proc-macro2", "quote", "regex", "syn 2.0.100", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libloading" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets 0.52.6", ] [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.0", "libc", "redox_syscall", ] [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" [[package]] name = "litemap" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "mach2" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" dependencies = [ "libc", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" dependencies = [ "libc", ] [[package]] name = "minimad" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2" dependencies = [ "once_cell", ] [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", ] [[package]] name = "mio" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] [[package]] name = "ndk" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ "bitflags 2.9.0", "jni-sys", "log", "ndk-sys", "num_enum", "thiserror 1.0.69", ] [[package]] name = "ndk-context" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "ndk-sys" version = "0.5.0+25.2.9519653" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" dependencies = [ "jni-sys", ] [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.9.0", "cfg-if", "cfg_aliases", "libc", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "notify" version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" dependencies = [ "bitflags 2.9.0", "filetime", "fsevent-sys", "inotify", "kqueue", "libc", "log", "mio", "notify-types", "walkdir", "windows-sys 0.52.0", ] [[package]] name = "notify-types" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" dependencies = [ "instant", ] [[package]] name = "num-derive" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", "syn 2.0.100", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_enum" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.100", ] [[package]] name = "objc-sys" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" [[package]] name = "objc2" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ "objc-sys", "objc2-encode", ] [[package]] name = "objc2-app-kit" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.9.0", "block2", "libc", "objc2", "objc2-core-data", "objc2-core-image", "objc2-foundation", "objc2-quartz-core", ] [[package]] name = "objc2-core-data" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.9.0", "block2", "objc2", "objc2-foundation", ] [[package]] name = "objc2-core-image" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2", "objc2", "objc2-foundation", "objc2-metal", ] [[package]] name = "objc2-encode" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.9.0", "block2", "libc", "objc2", ] [[package]] name = "objc2-metal" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.9.0", "block2", "objc2", "objc2-foundation", ] [[package]] name = "objc2-quartz-core" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.9.0", "block2", "objc2", "objc2-foundation", "objc2-metal", ] [[package]] name = "oboe" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" dependencies = [ "jni", "ndk", "ndk-context", "num-derive", "num-traits", "oboe-sys", ] [[package]] name = "oboe-sys" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" dependencies = [ "cc", ] [[package]] name = "once_cell" version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "portable-atomic-util" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "proc-macro-crate" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "proc-status" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0e0c0ac915e7b76b47850ba4ffc377abde6c6ff9eeace61d0a89623db449712" dependencies = [ "thiserror 1.0.69", ] [[package]] name = "prodash" version = "29.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ee7ce24c980b976607e2d6ae4aae92827994d23fed71659c3ede3f92528b58b" dependencies = [ "log", "parking_lot", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ "bitflags 2.9.0", ] [[package]] name = "redox_users" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom 0.2.15", "libredox", "thiserror 1.0.69", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rodio" version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" dependencies = [ "cpal", "symphonia", ] [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.9.0", "errno", "libc", "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] [[package]] name = "rustix" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" dependencies = [ "bitflags 2.9.0", "errno", "libc", "linux-raw-sys 0.9.3", "windows-sys 0.59.0", ] [[package]] name = "rustversion" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", ] [[package]] name = "serde" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", "syn 2.0.100", ] [[package]] name = "serde_json" version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] [[package]] name = "sha1_smol" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-mio" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", "mio", "signal-hook", ] [[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "smallvec" version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strict" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "symphonia" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" dependencies = [ "lazy_static", "symphonia-bundle-mp3", "symphonia-core", "symphonia-metadata", ] [[package]] name = "symphonia-bundle-mp3" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" dependencies = [ "lazy_static", "log", "symphonia-core", "symphonia-metadata", ] [[package]] name = "symphonia-core" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" dependencies = [ "arrayvec", "bitflags 1.3.2", "bytemuck", "lazy_static", "log", ] [[package]] name = "symphonia-metadata" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" dependencies = [ "encoding_rs", "lazy_static", "log", "symphonia-core", ] [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "synstructure" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", "syn 2.0.100", ] [[package]] name = "tempfile" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" dependencies = [ "fastrand", "getrandom 0.3.1", "once_cell", "rustix 1.0.2", "windows-sys 0.59.0", ] [[package]] name = "termimad" version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8e19c6dbf107bec01d0e216bb8219485795b7d75328e4fa5ef2756c1be4f8dc" dependencies = [ "coolor", "crokey", "crossbeam", "lazy-regex", "minimad", "serde", "thiserror 1.0.69", "unicode-width 0.1.14", ] [[package]] name = "terminal-light" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9474484d1a0c031cd7d065c6f027a376859c9fedb32c94df3d7a797218bbb7" dependencies = [ "coolor", "crossterm", "thiserror 1.0.69", "xterm-query", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl 2.0.12", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn 2.0.100", ] [[package]] name = "thiserror-impl" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", "syn 2.0.100", ] [[package]] name = "tinystr" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", ] [[package]] name = "tinyvec" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow 0.7.4", ] [[package]] name = "unicode-bom" version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-normalization" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "url" version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] [[package]] name = "utf16_iter" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" [[package]] name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vte" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" dependencies = [ "arrayvec", "memchr", ] [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" version = "0.13.3+wasi-0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn 2.0.100", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", "syn 2.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" dependencies = [ "windows-core 0.54.0", "windows-targets 0.52.6", ] [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-core" version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ "windows-result", "windows-targets 0.52.6", ] [[package]] name = "windows-link" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" [[package]] name = "windows-result" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28" dependencies = [ "memchr", ] [[package]] name = "winnow" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen-rt" version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ "bitflags 2.9.0", ] [[package]] name = "write16" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] name = "writeable" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "x11rb" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", "rustix 0.38.44", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xterm-query" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fc10a48747cba9264d985322216216cdafd823080cc8af5488f2afc8dc776c" dependencies = [ "nix", "thiserror 1.0.69", ] [[package]] name = "yoke" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", "yoke-derive", "zerofrom", ] [[package]] name = "yoke-derive" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", "syn 2.0.100", "synstructure", ] [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", "syn 2.0.100", ] [[package]] name = "zerofrom" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", "syn 2.0.100", "synstructure", ] [[package]] name = "zerovec" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", "syn 2.0.100", ] bacon-3.12.0/Cargo.toml0000644000000046520000000000100102060ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.76" name = "bacon" version = "3.12.0" authors = ["dystroy "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "background rust compiler" readme = "README.md" keywords = [ "rust", "background", "compiler", "watch", "inotify", ] categories = [ "command-line-utilities", "development-tools", ] license = "AGPL-3.0" repository = "https://github.com/Canop/bacon" [features] clipboard = ["arboard"] default = [] sound = ["rodio"] [lib] name = "bacon" path = "src/lib.rs" [[bin]] name = "bacon" path = "src/main.rs" [dependencies.anyhow] version = "1.0" [dependencies.arboard] version = "3.4" optional = true default-features = false [dependencies.cargo_metadata] version = "0.19" [dependencies.clap] version = "4.5" features = [ "derive", "cargo", ] [dependencies.clap-help] version = "1.3.2" [dependencies.clap_complete] version = "4.5.44" features = ["unstable-dynamic"] [dependencies.cli-log] version = "2.1" [dependencies.crokey] version = "1.1" [dependencies.ctrlc] version = "3.4" [dependencies.directories-next] version = "2.0.0" [dependencies.gix] version = "0.67" features = [ "index", "excludes", "parallel", ] default-features = false [dependencies.glob] version = "0.3" [dependencies.iq] version = "0.2" features = ["template"] [dependencies.lazy-regex] version = "3.4.1" [dependencies.notify] version = "7.0" [dependencies.rodio] version = "0.20" features = ["mp3"] optional = true default-features = false [dependencies.rustc-hash] version = "2" [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.serde_json] version = "1.0" [dependencies.termimad] version = "0.31.1" [dependencies.toml] version = "0.8" [dependencies.unicode-width] version = "0.2" [dependencies.vte] version = "0.15" [profile.release] lto = "fat" codegen-units = 1 debug = 0 strip = "symbols" bacon-3.12.0/Cargo.toml.orig0000644000000030450000000000100111400ustar [package] name = "bacon" version = "3.12.0" authors = ["dystroy "] repository = "https://github.com/Canop/bacon" description = "background rust compiler" edition = "2021" keywords = ["rust", "background", "compiler", "watch", "inotify"] license = "AGPL-3.0" categories = ["command-line-utilities", "development-tools"] readme = "README.md" rust-version = "1.76" [features] default = [] clipboard = ["arboard"] sound = ["rodio"] [dependencies] anyhow = "1.0" arboard = { version = "3.4", default-features = false, optional = true } cargo_metadata = "0.19" clap = { version = "4.5", features = ["derive", "cargo"] } clap-help = "1.3.2" clap_complete = { version = "4.5.44", features = ["unstable-dynamic"] } cli-log = "2.1" crokey = "1.1" ctrlc = "3.4" directories-next = "2.0.0" gix = { version = "0.67", default-features = false, features = ["index", "excludes", "parallel"] } glob = "0.3" iq = { version = "0.2", features = ["template"] } lazy-regex = "3.4.1" notify = "7.0" rodio = { version = "0.20", optional = true, default-features = false, features = ["mp3"] } rustc-hash = "2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" termimad = "0.31.1" toml = "0.8" unicode-width = "0.2" vte = "0.15" [profile.release] debug = false lto = "fat" strip = "symbols" codegen-units = 1 [patch.crates-io] # clap-help = { path = "../clap-help" } # termimad = { path = "../termimad" } # crokey = { path = "../crokey" } # coolor = { path = "../coolor" } # iq = { path = "../iq" } # lazy-regex = { path = "../lazy-regex" } bacon-3.12.0/Cargo.toml.orig000064400000000000000000000030451046102023000136620ustar 00000000000000[package] name = "bacon" version = "3.12.0" authors = ["dystroy "] repository = "https://github.com/Canop/bacon" description = "background rust compiler" edition = "2021" keywords = ["rust", "background", "compiler", "watch", "inotify"] license = "AGPL-3.0" categories = ["command-line-utilities", "development-tools"] readme = "README.md" rust-version = "1.76" [features] default = [] clipboard = ["arboard"] sound = ["rodio"] [dependencies] anyhow = "1.0" arboard = { version = "3.4", default-features = false, optional = true } cargo_metadata = "0.19" clap = { version = "4.5", features = ["derive", "cargo"] } clap-help = "1.3.2" clap_complete = { version = "4.5.44", features = ["unstable-dynamic"] } cli-log = "2.1" crokey = "1.1" ctrlc = "3.4" directories-next = "2.0.0" gix = { version = "0.67", default-features = false, features = ["index", "excludes", "parallel"] } glob = "0.3" iq = { version = "0.2", features = ["template"] } lazy-regex = "3.4.1" notify = "7.0" rodio = { version = "0.20", optional = true, default-features = false, features = ["mp3"] } rustc-hash = "2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" termimad = "0.31.1" toml = "0.8" unicode-width = "0.2" vte = "0.15" [profile.release] debug = false lto = "fat" strip = "symbols" codegen-units = 1 [patch.crates-io] # clap-help = { path = "../clap-help" } # termimad = { path = "../termimad" } # crokey = { path = "../crokey" } # coolor = { path = "../coolor" } # iq = { path = "../iq" } # lazy-regex = { path = "../lazy-regex" } bacon-3.12.0/LICENSE000064400000000000000000001033331046102023000120010ustar 00000000000000 GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . bacon-3.12.0/README.md000064400000000000000000000064561046102023000122630ustar 00000000000000![bacon][logo] [logo]: img/logo-text.png?raw=true "bacon" [![Latest Version][s1]][l1] [![site][s4]][l4] [![Chat on Miaou][s2]][l2] [![License: AGPL v3][s3]][l3] [s1]: https://img.shields.io/crates/v/bacon.svg [l1]: https://crates.io/crates/bacon [s2]: https://dystroy.org/chat-shield.svg [l2]: https://miaou.dystroy.org/4683?bacon [s3]: https://img.shields.io/badge/License-AGPL_v3-blue.svg [l3]: https://www.gnu.org/licenses/agpl-3.0 [s4]: https://dystroy.org/dystroy-doc-pink-shield.svg [l4]: https://dystroy.org/bacon **bacon** is a background code checker. It's designed for minimal interaction so that you can just let it run, alongside your editor, and be notified of warnings, errors, or test failures in your Rust code. ![screenshot](doc/screenshot.png) # Documentation The **[bacon website](https://dystroy.org/bacon)** is a complete guide. Below is only a short overview. ## install cargo install --locked bacon Run this command too if you want to update bacon. Configuration has always been retro-compatible so you won't lose anything. Some features are disabled by default. You may enable them with cargo install --features "clipboard sound" ## check the current project bacon That's how you'll usually launch bacon, because other jobs like `test`, `clippy`, `doc`, your own ones, are just a key away: You'll hit c to see Clippy warnings, t for the tests, d to open the documentation, etc. ## check another project bacon --path ../broot or bacon ../broot ## check all targets (tests, examples, benches, etc) bacon --job check-all When there's no ambiguity, you may omit the `--job` part: bacon check-all ## run clippy instead of cargo check bacon clippy This will run against all targets like `check-all` does. ## run tests bacon test or `bacon nextest` if you're a nextest user. ![bacon test](doc/test.png) When there's a failure, hit f to restrict the job to the failing test. Hit esc to get back to all tests. ## define your own jobs First create a `bacon.toml` file by running bacon --init This file already contains some standard jobs. Add your own, for example ```toml [jobs.check-win] command = ["cargo", "check", "--target", "x86_64-pc-windows-gnu"] ``` or ```toml [jobs.check-examples] command = ["cargo", "check", "--examples"] watch = ["examples"] # src is implicitly included ``` and run bacon check-win or bacon check-examples The `bacon.toml` file may evolve with the features and settings of your project and should be added to source control. ## Optional features Some bacon features can be disabled or enabled at compilation: * `"clipboard"` - disabled by default : necessary for the `copy-unstyled-output` internal * `"sound"` - enabled by default : necessary for the `play-sound` internal ## Licences Bacon is licenced under [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.en.html). You're free to use it to compile the Rust projects of your choice, even commercial. The logo is designed by [Peter Varo][pv] and licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License][cc-lic]. [![license][cc-img]][cc-lic] [pv]: https://petervaro.com [cc-lic]: https://creativecommons.org/licenses/by-sa/4.0 [cc-img]: https://i.creativecommons.org/l/by-sa/4.0/80x15.png bacon-3.12.0/bacon.toml000064400000000000000000000036671046102023000127640ustar 00000000000000# a bacon.toml file dedicated to the bacon tool default_job = "check-all" env.CARGO_TERM_COLOR = "always" # uncomment for sound #sound.enabled = true #sound.base_volume = "20%" [jobs] # This alternative to 'check' demonstrates the use of the JSON output # of cargo check - see https://github.com/Canop/bacon/issues/249 [jobs.json-check] command = [ "cargo", "check", "--message-format", "json-diagnostic-rendered-ansi", ] need_stdout = true analyzer = "cargo_json" [jobs.check] command = ["cargo", "check"] need_stdout = false default_watch = false [jobs.check-all] command = [ "cargo", "check", "--all-targets", ] need_stdout = false [jobs.fmt] command = ["cargo", "+nightly", "fmt"] [jobs.nightly] command = [ "cargo", "+nightly", "check", "--all-targets", ] need_stdout = false [jobs.win] command = [ "cross", "build", "--target", "x86_64-pc-windows-gnu", ] [jobs.test] command = ["cargo", "test"] need_stdout = true on_success = "play-sound(name=90s-game-ui-6,volume=50)" on_failure = "play-sound(name=beep-warning,volume=100)" [jobs.doc] command = ["cargo", "doc", "--no-deps"] need_stdout = false # If the doc compiles, then it opens in your browser and bacon switches # to the previous job [jobs.doc-open] command = ["cargo", "doc", "--no-deps", "--open"] need_stdout = false on_success = "back" # so that we don't open the browser at each change [jobs.clippy-all] command = [ "cargo", "clippy", "--", "-A", "clippy::bool_to_int_with_if", "-A", "clippy::collapsible_else_if", "-A", "clippy::collapsible_if", "-A", "clippy::derive_partial_eq_without_eq", "-A", "clippy::get_first", "-A", "clippy::if_same_then_else", "-A", "clippy::len_without_is_empty", "-A", "clippy::map_entry", "-A", "clippy::while_let_on_iterator", ] need_stdout = false [keybindings] c = "job:clippy-all" ctrl-c = "copy-unstyled-output" #n = "next-match" #shift-n = "previous-match" #h = "play-sound(name=car-horn)" #b = "play-sound(name=beep-6)" f = "job:fmt" bacon-3.12.0/defaults/default-bacon.toml000064400000000000000000000064131046102023000162050ustar 00000000000000# This is a configuration file for the bacon tool # # Complete help on configuration: https://dystroy.org/bacon/config/ # # You may check the current default at # https://github.com/Canop/bacon/blob/main/defaults/default-bacon.toml default_job = "check" env.CARGO_TERM_COLOR = "always" [jobs.check] command = ["cargo", "check"] need_stdout = false [jobs.check-all] command = ["cargo", "check", "--all-targets"] need_stdout = false # Run clippy on the default target [jobs.clippy] command = ["cargo", "clippy"] need_stdout = false # Run clippy on all targets # To disable some lints, you may change the job this way: # [jobs.clippy-all] # command = [ # "cargo", "clippy", # "--all-targets", # "--", # "-A", "clippy::bool_to_int_with_if", # "-A", "clippy::collapsible_if", # "-A", "clippy::derive_partial_eq_without_eq", # ] # need_stdout = false [jobs.clippy-all] command = ["cargo", "clippy", "--all-targets"] need_stdout = false # This job lets you run # - all tests: bacon test # - a specific test: bacon test -- config::test_default_files # - the tests of a package: bacon test -- -- -p config [jobs.test] command = ["cargo", "test"] need_stdout = true [jobs.nextest] command = [ "cargo", "nextest", "run", "--hide-progress-bar", "--failure-output", "final" ] need_stdout = true analyzer = "nextest" [jobs.doc] command = ["cargo", "doc", "--no-deps"] need_stdout = false # If the doc compiles, then it opens in your browser and bacon switches # to the previous job [jobs.doc-open] command = ["cargo", "doc", "--no-deps", "--open"] need_stdout = false on_success = "back" # so that we don't open the browser at each change # You can run your application and have the result displayed in bacon, # if it makes sense for this crate. [jobs.run] command = [ "cargo", "run", # put launch parameters for your program behind a `--` separator ] need_stdout = true allow_warnings = true background = true # Run your long-running application (eg server) and have the result displayed in bacon. # For programs that never stop (eg a server), `background` is set to false # to have the cargo run output immediately displayed instead of waiting for # program's end. # 'on_change_strategy' is set to `kill_then_restart` to have your program restart # on every change (an alternative would be to use the 'F5' key manually in bacon). # If you often use this job, it makes sense to override the 'r' key by adding # a binding `r = job:run-long` at the end of this file . [jobs.run-long] command = [ "cargo", "run", # put launch parameters for your program behind a `--` separator ] need_stdout = true allow_warnings = true background = false on_change_strategy = "kill_then_restart" # This parameterized job runs the example of your choice, as soon # as the code compiles. # Call it as # bacon ex -- my-example [jobs.ex] command = ["cargo", "run", "--example"] need_stdout = true allow_warnings = true # You may define here keybindings that would be specific to # a project, for example a shortcut to launch a specific job. # Shortcuts to internal functions (scrolling, toggling, etc.) # should go in your personal global prefs.toml file instead. [keybindings] # alt-m = "job:my-job" c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target bacon-3.12.0/defaults/default-prefs.toml000064400000000000000000000063251046102023000162440ustar 00000000000000# This is a preferences file for the bacon tool # More info at https://github.com/Canop/bacon # Uncomment and change the value (true/false) to # specify whether bacon should start in summary mode # # summary = true # Uncomment and change the value (true/false) to # specify whether bacon should start with lines wrapped # # wrap = false # In "reverse" mode, the focus is at the bottom, item # order is reversed, and the status bar is on top # # reverse = true # The grace period is a delay after a file event before the real # task is launched and during which other events will be ignored. # This is mostly useful when your editor does several operations # when saving a file and the state is temporarily wrong (eg it # moves the file to a backup name before recreating the right one) # You can set it to "none" if it's useless for you. # # grace_period = "15ms" # Uncomment and change the value (true/false) to # specify whether bacon should show a help line. # # help_line = false # Uncomment and change the value (true/false) to # set whether to display the count of changes since last job start # # show_changes_count = false # Uncomment one of those lines if you don't want the default # behavior triggered by a file change. This property can also # be set directly in a specific job. # # on_change_strategy = "kill_then_restart" # on_change_strategy = "wait_then_restart" # Exporting "locations" (by setting its 'auto' to true) lets you use # them in an external tool, for example as a list of jump locations # in an IDE or in a language server. # (See https://dystroy.org/bacon/config/#export-locations), # # Possible line_format parts: # - kind: warning|error|test # - path: complete absolute path to the file # - line: 1-based line number # - column: 1-based column # - message: description of the item # - context: unstyled lines of output, separated with escaped newlines (`\\n`) [exports.locations] auto = false exporter = "locations" path = ".bacon-locations" line_format = "{kind} {path}:{line}:{column} {message}" # If you want some job to emit a beep on success or on failure, # you need to globally enable sound, and you may set up the max volume here # # With sound enabled, you may set up sound on a job with eg # on_success = "play-sound(name=90s-game-ui-6,volume=50)" # on_failure = "play-sound(name=beep-warning,volume=100)" [sound] enabled = false # set true to allow sound base_volume = "100%" # global volume multiplier # Uncomment and change the key-bindings you want to define # (some of those ones are the defaults and are just here for illustration) [keybindings] # esc = "back" # g = "scroll-to-top" # shift-g = "scroll-to-bottom" # k = "scroll-lines(-1)" # j = "scroll-lines(1)" # ctrl-c = "quit" # ctrl-q = "quit" # q = "quit" # F5 = "rerun" # alt-s = "toggle-summary" # alt-w = "toggle-wrap" # alt-b = "toggle-backtrace" # Home = "scroll-to-top" # End = "scroll-to-bottom" # Up = "scroll-lines(-1)" # Down = "scroll-lines(1)" # PageUp = "scroll-pages(-1)" # PageDown = "scroll-pages(1)" # Space = "scroll-pages(1)" # a = "job:check-all" # i = "job:initial" # c = "job:clippy" # c = "job:clippy-all" # d = "job:doc-open" # t = "job:test" # r = "job:run" # ctrl-e = "export:analysis" # ctrl-c = "copy-unstyled-output" bacon-3.12.0/flake.lock000064400000000000000000000045161046102023000127330ustar 00000000000000{ "nodes": { "flake-utils": { "inputs": { "systems": "systems" }, "locked": { "lastModified": 1731533236, "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { "owner": "numtide", "repo": "flake-utils", "type": "github" } }, "naersk": { "inputs": { "nixpkgs": "nixpkgs" }, "locked": { "lastModified": 1736429655, "narHash": "sha256-BwMekRuVlSB9C0QgwKMICiJ5EVbLGjfe4qyueyNQyGI=", "owner": "nix-community", "repo": "naersk", "rev": "0621e47bd95542b8e1ce2ee2d65d6a1f887a13ce", "type": "github" }, "original": { "owner": "nix-community", "repo": "naersk", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1736320768, "narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=", "owner": "NixOS", "repo": "nixpkgs", "rev": "4bc9c909d9ac828a039f288cf872d16d38185db8", "type": "github" }, "original": { "id": "nixpkgs", "type": "indirect" } }, "nixpkgs_2": { "locked": { "lastModified": 1736320768, "narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=", "owner": "NixOS", "repo": "nixpkgs", "rev": "4bc9c909d9ac828a039f288cf872d16d38185db8", "type": "github" }, "original": { "owner": "NixOS", "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "flake-utils": "flake-utils", "naersk": "naersk", "nixpkgs": "nixpkgs_2" } }, "systems": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "owner": "nix-systems", "repo": "default", "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { "owner": "nix-systems", "repo": "default", "type": "github" } } }, "root": "root", "version": 7 } bacon-3.12.0/flake.nix000064400000000000000000000017741046102023000126040ustar 00000000000000{ inputs = { flake-utils.url = "github:numtide/flake-utils"; naersk.url = "github:nix-community/naersk"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; }; outputs = { self, flake-utils, naersk, nixpkgs, }: flake-utils.lib.eachDefaultSystem ( system: let pkgs = (import nixpkgs) { inherit system; }; naersk' = pkgs.callPackage naersk { }; bacon = naersk'.buildPackage { buildInputs = if pkgs.stdenv.isLinux then [ pkgs.alsa-lib pkgs.pkg-config ] else []; src = ./.; }; in { # For `nix build` & `nix run`: defaultPackage = bacon; # For `nix develop`: devShell = pkgs.mkShell { nativeBuildInputs = with pkgs; [ rustc cargo ]; }; # Overlay for package usage in other Nix configurations overlay = final: prev: { bacon = bacon; }; } ); } bacon-3.12.0/resources/README.md000064400000000000000000000006411046102023000142630ustar 00000000000000Resources here are embedded in bacon, depending on features enabled at compilation. All mp3 files in this directory come from https://pixabay.com/ Their name in this directory is the name in pixabay, to ease retrieval. They're free to use. You may propose other sounds for inclusion in bacon, provided that their usage isn't limited and that they're light enough. See src/sound/play_sound.rs for more information. bacon-3.12.0/rustfmt.toml000064400000000000000000000001761046102023000133760ustar 00000000000000edition = "2021" style_edition = "2024" imports_granularity = "One" imports_layout = "Vertical" fn_params_layout = "Vertical" bacon-3.12.0/src/analysis/analyzer.rs000064400000000000000000000034551046102023000156050ustar 00000000000000use { super::{ biome, cargo_json, cpp, eslint, nextest, python, standard, }, crate::*, serde::{ Deserialize, Serialize, }, }; /// A stateless operator building a report from a list of command output lines. /// /// Implementation routing will probably change at some point #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AnalyzerRef { #[default] Standard, CargoJson, Nextest, Eslint, Biome, PythonPytest, PythonRuff, PythonUnittest, Cpp, CppDoctest, } impl AnalyzerRef { pub fn create_analyzer(self) -> Box { match self { Self::Standard => Box::new(standard::StandardAnalyzer::default()), Self::Nextest => Box::new(nextest::NextestAnalyzer::default()), Self::Eslint => Box::new(eslint::EslintAnalyzer::default()), Self::Biome => Box::new(biome::BiomeAnalyzer::default()), Self::PythonUnittest => Box::new(python::unittest::UnittestAnalyzer::default()), Self::PythonPytest => Box::new(python::pytest::PytestAnalyzer::default()), Self::PythonRuff => Box::new(python::ruff::RuffAnalyzer::default()), Self::CargoJson => Box::new(cargo_json::CargoJsonAnalyzer::default()), Self::Cpp => Box::new(cpp::CppAnalyzer::default()), Self::CppDoctest => Box::new(cpp::CppDoctestAnalyzer::default()), } } } pub trait Analyzer { fn start( &mut self, mission: &Mission, ); fn receive_line( &mut self, line: CommandOutputLine, command_output: &mut CommandOutput, ); fn build_report(&mut self) -> anyhow::Result; } bacon-3.12.0/src/analysis/biome/mod.rs000064400000000000000000000062401046102023000156250ustar 00000000000000//! An analyzer for biome ( / ) use { super::*, crate::*, anyhow::Result, lazy_regex::*, }; #[derive(Debug, Default)] pub struct BiomeAnalyzer { lines: Vec, } #[derive(Debug)] struct LocationCode<'l> { location: &'l str, // path with line and column code: &'l str, // eg "lint/complexity/noForEach" tag: Option<&'l TString>, // eg "FIXABLE" } #[derive(Debug)] enum BiomeLine<'l> { LocationCode(LocationCode<'l>), Other, } impl Analyzer for BiomeAnalyzer { fn start( &mut self, _mission: &Mission, ) { self.lines.clear(); } fn receive_line( &mut self, line: CommandOutputLine, command_output: &mut CommandOutput, ) { self.lines.push(line.clone()); command_output.push(line); } fn build_report(&mut self) -> Result { build_report(&self.lines) } } fn recognize_line(tline: &TLine) -> BiomeLine { if let Some(lc) = recognize_location_code(tline) { return BiomeLine::LocationCode(lc); } BiomeLine::Other } fn recognize_location_code(tline: &TLine) -> Option { if let Some(s) = tline.if_unstyled() { // untagged if let Some((_, location, code)) = regex_captures!(r"([^\s:]+:\d+:\d+) (\S+) ━+$", s) { let tag = None; return Some(LocationCode { location, code, tag, }); } } let mut strings = tline.strings.iter(); let a = strings.next(); let b = strings.next(); let c = strings.next(); if let (Some(a), Some(b), Some(c)) = (a, b, c) { if a.is_unstyled() && c.is_unstyled() && regex_is_match!("^ ━+$", &c.raw) { if let Some((_, location, code)) = regex_captures!(r"([^\s:]+:\d+:\d+) (\S+) ", &a.raw) { let tag = Some(b); return Some(LocationCode { location, code, tag, }); } } } None } /// Build a report from the output of biome pub fn build_report(cmd_lines: &[CommandOutputLine]) -> anyhow::Result { let mut items = ItemAccumulator::default(); let mut last_is_blank = true; for cmd_line in cmd_lines { let bline = recognize_line(&cmd_line.content); if let BiomeLine::LocationCode(lc) = bline { let mut error_line = burp::error_line(lc.code); if let Some(tag) = lc.tag { error_line.strings.push(TString::new("", " ")); error_line.strings.push(tag.clone()); } items.push_error_title(error_line); items.push_line( LineType::Location, burp::location_line(lc.location.to_string()), ); last_is_blank = false; } else { let is_blank = cmd_line.content.is_blank(); if !(is_blank && last_is_blank) { items.push_line(LineType::Normal, cmd_line.content.clone()); } last_is_blank = is_blank; } } Ok(items.report()) } bacon-3.12.0/src/analysis/cargo_json/cargo_json_export.rs000064400000000000000000000027761046102023000216360ustar 00000000000000use { crate::*, cargo_metadata::diagnostic::{ Diagnostic, DiagnosticSpan, }, serde::Serialize, }; /// An export in progress for the cargo_json analyzer pub struct CargoJsonExport { pub name: String, /// The written data to write to the file pub export: String, pub line_template: iq::Template, } /// The data provided to the template, once per span #[derive(Debug, Clone, Serialize)] struct OnSpanData<'d> { diagnostic: &'d Diagnostic, span: &'d DiagnosticSpan, } impl CargoJsonExport { pub fn new( name: String, settings: &ExportSettings, ) -> Self { Self { name, export: String::new(), line_template: iq::Template::new(&settings.line_format), } } pub fn receive_diagnostic( &mut self, diagnostic: &Diagnostic, ) { for span in &diagnostic.spans { let data = { // This is a diagnostic that originates from a proc-macro. if let Some(expansion) = &span.expansion { OnSpanData { diagnostic, span: &expansion.span, } } else { OnSpanData { diagnostic, span } } }; let line = self.line_template.render(&data); if !line.is_empty() { self.export.push_str(&line); self.export.push('\n'); } } } } bacon-3.12.0/src/analysis/cargo_json/mod.rs000064400000000000000000000076461046102023000166710ustar 00000000000000mod cargo_json_export; use { super::*, crate::{ analysis::standard, *, }, anyhow::Result, cargo_json_export::*, cargo_metadata::{ Message, diagnostic::Diagnostic, }, }; /// An analyzer able to read the output /// of `cargo check --message-format=json-diagnostic-rendered-ansi` /// /// In order to be useful, this analyzer would have to read more than /// just the 'rendered' field of the diagnostic. It would have to read /// for example the spans, detect what's a suggestion, etc. /// See https://github.com/Canop/bacon/issues/249 /// /// There are so many problems with this approach, though, that I'm /// not sure this is worth it. #[derive(Default)] pub struct CargoJsonAnalyzer { lines: Vec, exports: Vec, } impl Analyzer for CargoJsonAnalyzer { fn start( &mut self, mission: &Mission, ) { self.lines.clear(); self.exports.clear(); for (name, export_settings) in &mission.settings.exports.exports { if export_settings.exporter == Exporter::Analyser { let export = CargoJsonExport::new(name.clone(), export_settings); self.exports.push(export); } } } fn receive_line( &mut self, cmd_line: CommandOutputLine, command_output: &mut CommandOutput, ) { let Some(content) = cmd_line.content.if_unstyled() else { return; // right now we're not expecting styled output }; match serde_json::from_str::(content) { Ok(message) => { self.receive_cargo_message(message, cmd_line.origin, command_output); } Err(err) => { let line = TLine::from_tty(&format!("Error parsing JSON: {}", err)); let cmd_line = CommandOutputLine { content: line, origin: cmd_line.origin, }; command_output.push(cmd_line); } } } fn build_report(&mut self) -> Result { let line_analyzer = standard::StandardLineAnalyzer {}; let mut report = standard::build_report(&self.lines, line_analyzer)?; for export in self.exports.drain(..) { report.analyzer_exports.insert(export.name, export.export); } Ok(report) } } impl CargoJsonAnalyzer { fn receive_cargo_message( &mut self, message: Message, origin: CommandStream, command_output: &mut CommandOutput, ) { match message { Message::CompilerArtifact(_) => {} Message::CompilerMessage(compiler_message) => { self.receive_diagnostic(compiler_message.message, origin, command_output); } Message::BuildScriptExecuted(_) => {} Message::BuildFinished(_) => {} Message::TextLine(_) => {} _ => { // non exhaustive enum } } } fn receive_diagnostic( &mut self, diagnostic: Diagnostic, origin: CommandStream, command_output: &mut CommandOutput, ) { for export in &mut self.exports { export.receive_diagnostic(&diagnostic); } let Diagnostic { //message, //code, //level, //spans, children, //suggestion, rendered, .. } = diagnostic; if let Some(rendered) = rendered { for line in rendered.trim().lines() { let content = TLine::from_tty(line); let cmd_line = CommandOutputLine { content, origin }; command_output.push(cmd_line.clone()); self.lines.push(cmd_line); } } for child in children { self.receive_diagnostic(child, origin, command_output); } } } bacon-3.12.0/src/analysis/cpp.rs000064400000000000000000000156561046102023000145500ustar 00000000000000use { crate::*, anyhow::Result, std::{ borrow::Cow, iter::once, }, }; #[derive(Debug, Default)] pub struct CppAnalyzer { lines: Vec, } impl Analyzer for CppAnalyzer { fn start( &mut self, _mission: &Mission, ) { self.lines.clear() } fn receive_line( &mut self, line: CommandOutputLine, command_output: &mut CommandOutput, ) { self.lines.push(line.clone()); command_output.push(line); } fn build_report(&mut self) -> Result { build_report(&self.lines) } } struct Diagnostic<'l> { level: Kind, path: &'l str, line: &'l str, column: &'l str, message: Cow<'l, [TString]>, } fn build_report(lines: &[CommandOutputLine]) -> Result { let mut items = ItemAccumulator::default(); for line in lines { let diag = recognize_diagnostic(&line.content); if let Some(Diagnostic { level, path, line, column, message, }) = diag { let content = match level { Kind::Error => burp::error_line_ts(&message), Kind::Warning => burp::warning_line_ts(&message), _ => unreachable!(), }; items.start_item(level); items.push_line(LineType::Title(level), content); items.push_line( LineType::Location, burp::location_line(format!("{path}:{line}:{column}")), ); } else { items.push_line(LineType::Normal, line.content.clone()) } } Ok(items.report()) } /// Expect that diagnostics look like the following: /// /// ```text /// [path/to/source:line:column:] [level:] [message...] /// ``` /// /// ... where each part is contained within a potentially styled section. fn recognize_diagnostic(line: &TLine) -> Option { let (pos, level, kind_end) = line.strings.iter().enumerate().find_map(|(idx, section)| { if let Some(found) = section.raw.find("error:") { let remaining = (found + "error: ".len()).min(section.raw.len()); Some((idx, Kind::Error, remaining)) } else if let Some(found) = section.raw.find("warning:") { let remaining = (found + "warning: ".len()).min(section.raw.len()); Some((idx, Kind::Warning, remaining)) } else { None } })?; let (path, loc_line, loc_col) = line.strings.iter().take(pos + 1).find_map(|section| { let mut it = section.raw.split(':'); Some((it.next()?.trim(), it.next()?.trim(), it.next()?.trim())) })?; let message = if kind_end == 0 { // if the "warning:" section only contains the warning text, // take all remaining sections as the message Cow::Borrowed(line.strings.get(pos + 1..).unwrap_or_default()) } else { // otherwise, we need to extract whatever is left in the type section // specifically: // - raw[kind_end..] -> the rest of the type section // - strings[pos+1..] -> all remaining sections let level_section = &line.strings[pos]; let first = TString { csi: level_section.csi.clone(), raw: String::from(level_section.raw[kind_end..].trim_start_matches(' ')), }; let rest = line.strings.get(pos + 1..).unwrap_or_default(); let sections = once(first).chain(rest.iter().cloned()); Cow::Owned(sections.collect()) }; Some(Diagnostic { level, path, line: loc_line, column: loc_col, message, }) } #[derive(Debug, Default)] pub struct CppDoctestAnalyzer { lines: Vec, } impl Analyzer for CppDoctestAnalyzer { fn start( &mut self, _mission: &Mission, ) { self.lines.clear() } fn receive_line( &mut self, line: CommandOutputLine, command_output: &mut CommandOutput, ) { self.lines.push(line.clone()); command_output.push(line); } fn build_report(&mut self) -> Result { build_doctest_report(&self.lines) } } fn build_doctest_report(lines: &[CommandOutputLine]) -> Result { let mut items = ItemAccumulator::default(); let mut current_test_case = String::from("(unknown test)"); let mut empty_count = 0; for line in lines { match recognize_doctest_line(¤t_test_case, &line.content) { Some(DoctestDiagnostic::StartTestCase(tc)) => current_test_case = tc.into(), Some(DoctestDiagnostic::Failure { level, location, first_line, }) => { let content = match level { Kind::Error => burp::error_line_ts(&first_line), Kind::Warning => burp::warning_line_ts(&first_line), _ => unreachable!(), }; items.push_failure_title(content); items.push_line(LineType::Location, burp::location_line(location)); empty_count = 0; } Some(DoctestDiagnostic::StartOrEnd) => current_test_case = "(unknown test)".into(), None => { if empty_count < 1 { items.push_line(LineType::Normal, line.content.clone()); } if line.content.strings.iter().all(|l| l.raw.is_empty()) { empty_count += 1; } } } } Ok(items.report()) } enum DoctestDiagnostic<'a> { StartTestCase(&'a str), Failure { location: &'a str, level: Kind, first_line: Vec, }, StartOrEnd, } fn recognize_doctest_line<'l>( current_test_case: &str, line: &'l TLine, ) -> Option> { let get = |idx: usize| line.strings.get(idx).map(|l| l.raw.as_str()); if let Some("===============================================================================") = get(0) { return Some(DoctestDiagnostic::StartOrEnd); } if let Some("TEST CASE: ") = get(0) { return Some(DoctestDiagnostic::StartTestCase( line.strings.get(1).map_or("", |l| &l.raw), )); } if let Some(s @ ("WARNING: " | "ERROR: " | "FATAL ERROR: ")) = get(1) { let level = match s { "WARNING: " => Kind::Warning, "ERROR: " | "FATAL ERROR: " => Kind::Error, _ => unreachable!(), }; let rest_of_line = once(TString::new("", current_test_case)) .chain(once(TString::new("", " "))) .chain(line.strings.get(2..).unwrap_or_default().iter().cloned()) .collect(); return Some(DoctestDiagnostic::Failure { level, location: line.strings.first().unwrap().raw.as_str().trim(), first_line: rest_of_line, }); } None } bacon-3.12.0/src/analysis/eslint/eslint_line_analyzer.rs000064400000000000000000000077341046102023000214740ustar 00000000000000//! An analyzer for eslint ( ) use { super::*, crate::*, lazy_regex::*, }; const CSI_LOCATION_PATH: &str = "\u{1b}[4m"; const CSI_LINE_COL: &str = "\u{1b}[2m"; const CSI_ERROR: &str = "\u{1b}[31m"; const CSI_WARNING: &str = "\u{1b}[33m"; const CSI_SUM: &str = "\u{1b}[31m\u{1b}[1m"; #[derive(Debug, Default)] pub struct EslintLineAnalyzer; impl LineAnalyzer for EslintLineAnalyzer { fn analyze_line( &self, cmd_line: &CommandOutputLine, ) -> LineAnalysis { if is_line_col_error_line(&cmd_line.content) { LineAnalysis { line_type: LineType::Title(Kind::Error), key: None, } } else if is_line_col_warning_line(&cmd_line.content) { LineAnalysis { line_type: LineType::Title(Kind::Warning), key: None, } } else if get_location_path(&cmd_line.content).is_some() { LineAnalysis { line_type: LineType::Location, key: None, } } else if is_sum(&cmd_line.content) { LineAnalysis { line_type: LineType::Title(Kind::Sum), key: None, } } else { LineAnalysis { line_type: LineType::Normal, key: None, } } } } /// Return true when the line is like /// "67:52 error Unnecessary escape character: \/ no-useless-escape" fn is_line_col_error_line(content: &TLine) -> bool { let mut strings = content.strings.iter(); let (Some(first), Some(second), Some(third), Some(fourth)) = ( strings.next(), strings.next(), strings.next(), strings.next(), ) else { return false; }; first.is_blank() && second.csi == CSI_LINE_COL && regex_is_match!(r"^\d+:\d+$", &second.raw) && third.is_blank() && fourth.csi == CSI_ERROR && fourth.raw == "error" } /// Return true when the line is like /// "67:52 warning bla bla" fn is_line_col_warning_line(content: &TLine) -> bool { let mut strings = content.strings.iter(); let (Some(first), Some(second), Some(third), Some(fourth)) = ( strings.next(), strings.next(), strings.next(), strings.next(), ) else { return false; }; first.is_blank() && second.csi == CSI_LINE_COL && regex_is_match!(r"^\d+:\d+$", &second.raw) && third.is_blank() && fourth.csi == CSI_WARNING && fourth.raw == "warning" } fn is_sum(content: &TLine) -> bool { let mut strings = content.strings.iter(); let Some(first) = strings.next() else { return false; }; if !(first.csi == CSI_SUM && regex_is_match!(r"^✖ \d+ problems \(\d+ errors, \d+ warnings\)$", &first.raw)) { return false; } for string in strings { if !string.is_blank() { return false; } } true } pub fn get_location_path(content: &TLine) -> Option { let mut strings = content.strings.iter(); let first = strings.next()?; if first.csi != CSI_LOCATION_PATH { return None; } // trying to recognize a path, I might make some wrong assumptions here, // especially for windows... if !regex_is_match!(r"^\s*/\S+\.\w+s\s*$", &first.raw) { return None; } Some(first.raw.to_string()) } pub fn cleaned_tline(content: &TLine) -> TLine { let mut tline = TLine::default(); let mut last_is_blank = true; for ts in &content.strings { if ts.csi == CSI_LINE_COL && regex_is_match!(r"^\d+:\d+$", &ts.raw) { continue; // useless line:col at start of title } let raw = regex_replace_all!(r"\s+", &ts.raw, " ").to_string(); let is_blank = raw.trim().is_empty(); if !(is_blank && last_is_blank) { tline.strings.push(TString::new(&ts.csi, raw)); } last_is_blank = is_blank; } tline } bacon-3.12.0/src/analysis/eslint/mod.rs000064400000000000000000000054321046102023000160320ustar 00000000000000//! An analyzer for eslint ( ) mod eslint_line_analyzer; use { super::*, crate::*, anyhow::Result, eslint_line_analyzer::*, }; #[derive(Debug, Default)] pub struct EslintAnalyzer { lines: Vec, } impl Analyzer for EslintAnalyzer { fn start( &mut self, _mission: &Mission, ) { self.lines.clear(); } fn receive_line( &mut self, line: CommandOutputLine, command_output: &mut CommandOutput, ) { self.lines.push(line.clone()); command_output.push(line); } fn build_report(&mut self) -> Result { build_report(&self.lines) } } /// Build a report from the output of eslint /// /// The main specificity of eslint is that the path of a file with error is given /// before the errors of the file, each error coming with the line and column /// in the file. pub fn build_report(cmd_lines: &[CommandOutputLine]) -> anyhow::Result { let line_analyzer = EslintLineAnalyzer {}; let mut items = ItemAccumulator::default(); let mut last_location_path = None; for cmd_line in cmd_lines { let line_analysis = line_analyzer.analyze_line(cmd_line); let line_type = line_analysis.line_type; match line_type { LineType::Garbage => { continue; } LineType::Title(kind) => { items.start_item(kind); } LineType::Normal => {} LineType::Location => { let path = get_location_path(&cmd_line.content); if let Some(path) = path { last_location_path = Some(path); continue; } else { warn!("unconsistent line parsing"); } } _ => {} } items.push_line(line_type, cleaned_tline(&cmd_line.content)); if matches!(line_type, LineType::Title(_)) { // We just added the title, we must now add the location // As it's something which isn't present in eslint output, we're free // to choose the format we want so we're choosing the BURP one let line_col: Option<&str> = cmd_line.content.strings.get(1).map(|s| s.raw.as_ref()); let Some(line_col) = line_col else { warn!("unconsistent line parsing"); continue; }; let Some(location_path) = last_location_path.as_ref() else { warn!("no location given before error"); continue; }; items.push_line( LineType::Location, burp::location_line(format!("{location_path}:{line_col}")), ); } } Ok(items.report()) } bacon-3.12.0/src/analysis/item_accumulator.rs000064400000000000000000000043101046102023000173040ustar 00000000000000use crate::*; /// Receives lines and accumulates them into items, used /// at end to build a sorted list of lines. /// /// This is a small optional utility for report makers #[derive(Default)] pub struct ItemAccumulator { curr_kind: Option, errors: Vec, test_fails: Vec, warnings: Vec, } impl ItemAccumulator { pub fn start_item( &mut self, kind: Kind, ) { self.curr_kind = Some(kind); } pub fn close_item(&mut self) { self.curr_kind = None; } pub fn push_line( &mut self, line_type: LineType, content: TLine, ) { let line = Line { item_idx: 0, // will be filled later line_type, content, }; match self.curr_kind { Some(Kind::Warning) => self.warnings.push(line), Some(Kind::Error) => self.errors.push(line), Some(Kind::TestFail) => self.test_fails.push(line), _ => {} // before warnings and errors, or in a sum } } pub fn push_error_title( &mut self, content: TLine, ) { self.curr_kind = Some(Kind::Error); self.push_line(LineType::Title(Kind::Error), content); } pub fn push_failure_title( &mut self, content: TLine, ) { self.curr_kind = Some(Kind::TestFail); self.push_line(LineType::Title(Kind::TestFail), content); } pub fn lines(mut self) -> Vec { let mut lines = self.errors; lines.append(&mut self.test_fails); lines.append(&mut self.warnings); let mut item_idx = 0; for line in &mut lines { if matches!(line.line_type, LineType::Title(_)) { item_idx += 1; } line.item_idx = item_idx; } lines } pub fn report(self) -> Report { let lines = self.lines(); let stats = Stats::from(&lines); info!("stats: {:#?}", &stats); Report { lines, stats, suggest_backtrace: false, output: Default::default(), failure_keys: Vec::new(), analyzer_exports: Default::default(), } } } bacon-3.12.0/src/analysis/line_analysis.rs000064400000000000000000000022401046102023000166010ustar 00000000000000use { crate::*, serde::{ Deserialize, Serialize, }, }; /// result of the "parsing" of the line #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct LineAnalysis { pub line_type: LineType, #[serde(default, skip_serializing_if = "Option::is_none")] pub key: Option, } impl LineAnalysis { pub fn of_type(line_type: LineType) -> Self { Self { line_type, key: None, } } pub fn normal() -> Self { Self::of_type(LineType::Normal) } pub fn garbage() -> Self { Self::of_type(LineType::Garbage) } pub fn title_key( kind: Kind, key: String, ) -> Self { Self { line_type: LineType::Title(kind), key: Some(key), } } pub fn fail>(key: S) -> Self { Self { line_type: LineType::Title(Kind::TestFail), key: Some(key.into()), } } pub fn test_result( key: String, pass: bool, ) -> Self { Self { line_type: LineType::TestResult(pass), key: Some(key), } } } bacon-3.12.0/src/analysis/line_analyzer.rs000064400000000000000000000003711046102023000166060ustar 00000000000000use crate::*; // an utility for those analyzers that can work with the LineAnalysis struct pub trait LineAnalyzer { /// this function will disappear fn analyze_line( &self, line: &CommandOutputLine, ) -> LineAnalysis; } bacon-3.12.0/src/analysis/line_type.rs000064400000000000000000000060651046102023000157500ustar 00000000000000use { crate::*, anyhow::*, serde::{ Deserialize, Serialize, }, std::io::Write, termimad::crossterm::style::Stylize, }; /// a kind of section #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum Kind { /// a warning Warning, /// an error Error, /// a test failure TestFail, /// a sum of errors and/or warnings, typically occuring /// at the end of the compilation of a package Sum, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum LineType { /// the start of a section Title(Kind), /// the end of a section (not part of the section) SectionEnd, /// a line locating the problem Location, /// the line saying if a test was passed TestResult(bool), /// a suggestion to try with backtrace BacktraceSuggestion, /// a line we know is useless noise Garbage, /// Raw line, unclassified Raw(CommandStream), /// Continuation of a previous line Continuation { /// offset to count back to get to first (starting at 1) offset: usize, /// whether the line is a summary summary: bool, }, /// any other line Normal, } impl LineType { /// Width on screen for the specific prefix of line of this type pub fn cols(self) -> usize { match self { Self::Title(_) => 3, _ => 0, } } pub fn at_index_in( idx: usize, lines: &[Line], ) -> Option { let line = lines.get(idx)?; match line.line_type { Self::Continuation { offset, .. } => { if offset > idx { error!("unconsistent offset in continuation line"); return None; } let idx = idx - offset; let line = lines.get(idx)?; Some(line.line_type) } line_type => Some(line_type), } } pub fn is_summary(self) -> bool { match self { Self::Normal | Self::Raw(_) => false, Self::Continuation { summary, .. } => summary, _ => true, } } pub fn matches( self, summary: bool, ) -> bool { !summary || self.is_summary() } pub fn draw( self, w: &mut W, item_idx: usize, ) -> Result<()> { match self { Self::Title(Kind::Error) => { write!(w, "{}", format!("{:^3}", item_idx).black().bold().on_red())?; } Self::Title(Kind::TestFail) => { write!( w, "\u{1b}[1m\u{1b}[38;5;235m\u{1b}[48;5;208m{:^3}\u{1b}[0m\u{1b}[0m", item_idx )?; } Self::Title(Kind::Warning) => { write!( w, "{}", format!("{:^3}", item_idx).black().bold().on_yellow() )?; } _ => {} } Ok(()) } } bacon-3.12.0/src/analysis/mod.rs000064400000000000000000000004771046102023000145400ustar 00000000000000mod analyzer; mod biome; mod cargo_json; mod cpp; mod eslint; mod item_accumulator; mod line_analysis; mod line_analyzer; mod line_type; mod nextest; mod python; mod standard; mod stats; pub use { analyzer::*, item_accumulator::*, line_analysis::*, line_analyzer::*, line_type::*, stats::*, }; bacon-3.12.0/src/analysis/nextest/mod.rs000064400000000000000000000013621046102023000162240ustar 00000000000000mod nextest_line_analyser; use { crate::*, anyhow::Result, nextest_line_analyser::NextestLineAnalyzer, }; #[derive(Debug, Default)] pub struct NextestAnalyzer { lines: Vec, } impl Analyzer for NextestAnalyzer { fn start( &mut self, _mission: &Mission, ) { self.lines.clear(); } fn receive_line( &mut self, line: CommandOutputLine, command_output: &mut CommandOutput, ) { self.lines.push(line.clone()); command_output.push(line); } fn build_report(&mut self) -> Result { let line_analyzer = NextestLineAnalyzer::default(); crate::analysis::standard::build_report(&self.lines, line_analyzer) } } bacon-3.12.0/src/analysis/nextest/nextest_line_analyser.rs000064400000000000000000000174051046102023000220510ustar 00000000000000use { crate::{ analysis::standard::StandardLineAnalyzer, *, }, lazy_regex::*, }; const CSI_TITLE: &str = "\u{1b}[35;1m"; const CSI_PASS: &str = "\u{1b}[32;1m"; const CSI_ERROR: &str = "\u{1b}[31;1m"; #[derive(Debug, Default)] pub struct NextestLineAnalyzer { default_analyzer: StandardLineAnalyzer, } impl LineAnalyzer for NextestLineAnalyzer { fn analyze_line( &self, cmd_line: &CommandOutputLine, ) -> LineAnalysis { let content = &cmd_line.content; if let Some(key) = title_key(content).or_else(|| title_key_v2(content)) { return LineAnalysis::title_key(Kind::TestFail, key); } if let Some((key, pass)) = as_test_result(content) { return LineAnalysis::test_result(key, pass); } if is_canceling(content) { return LineAnalysis::of_type(LineType::SectionEnd); } if is_error_test_run_failed(content) { return LineAnalysis::of_type(LineType::Garbage); } if let Some(content) = cmd_line.content.if_unstyled() { if regex_is_match!(r"^running \d+ tests?$", content) { return LineAnalysis::of_type(LineType::Garbage); } if content == "------------" { return LineAnalysis::of_type(LineType::SectionEnd); } if content == "────────────" { return LineAnalysis::of_type(LineType::SectionEnd); } } // compilation warnings and errors are still composed with the standard cargo tool self.default_analyzer.analyze_line(cmd_line) } } fn title_key(content: &TLine) -> Option { title_key_v1(content).or_else(|| title_key_v2(content)) } /// Return the key when the line is like "--- STD(OUT|ERR): somekey ---" fn title_key_v1(content: &TLine) -> Option { let mut strings = content.strings.iter(); let first = strings.next()?; if !regex_is_match!(r"^--- STD(OUT|ERR):\s*", &first.raw) { return None; } extract_key_after_crate_name_v1(strings) } /// Return the key when the line is like "──── STD(OUT|ERR): cratename some::key" fn title_key_v2(content: &TLine) -> Option { let mut strings = content.strings.iter(); let ts = strings.next()?; if ts.csi != CSI_ERROR || ts.raw != "────" { return None; } let ts = strings.find(|ts| ts.raw != " ")?; if !regex_is_match!(r"^STD(OUT|ERR):\s*", &ts.raw) { return None; } extract_key_after_crate_name_v2(strings) } fn extract_key_after_crate_name_v1(mut strings: std::slice::Iter<'_, TString>) -> Option { let _ = strings.next(); // skip blank let mut key = String::new(); for s in &mut strings { if s.csi.is_empty() { continue; } if s.raw == " ---" || s.csi == CSI_TITLE { break; } key.push_str(&s.raw); } if strings.next().is_some() { return None; } if key.is_empty() { None } else { Some(key) } } fn extract_key_after_crate_name_v2(mut strings: std::slice::Iter<'_, TString>) -> Option { // skip blank and crate name let ts = strings.find(|ts| ts.csi != CSI_TITLE && ts.raw != " ")?; let mut key = String::new(); key.push_str(&ts.raw); for s in &mut strings { if s.csi.is_empty() || s.csi == CSI_TITLE { break; } key.push_str(&s.raw); } if key.is_empty() { None } else { Some(key) } } fn is_error_test_run_failed(content: &TLine) -> bool { let mut strings = content.strings.iter(); let (Some(first), Some(second), None) = (strings.next(), strings.next(), strings.next()) else { return false; }; first.csi == CSI_ERROR && first.raw.trim() == "error" && second.raw.trim() == ": test run failed" } fn is_canceling(content: &TLine) -> bool { let Some(first) = content.strings.first() else { return false; }; first.csi == CSI_ERROR && first.raw.trim() == "Canceling" } /// return the key and whether the tests passes, when the line is a test /// result (like " PASS [ 0.003s] bacon tests::failing_test3") /// /// In the future, we might want to return the duration too. fn as_test_result(content: &TLine) -> Option<(String, bool)> { as_test_result_v1(content).or_else(|| as_test_result_v2(content)) } fn as_test_result_v1(content: &TLine) -> Option<(String, bool)> { let mut strings = content.strings.iter(); let first = strings.next()?; let pass = match (first.csi.as_str(), first.raw.trim()) { (CSI_PASS, "PASS") => true, (CSI_ERROR, "FAIL") => false, _ => return None, }; let _duration = match strings.next() { Some(s) if s.csi.is_empty() => s.raw.trim(), _ => return None, }; let key = extract_key_after_crate_name_v1(strings)?; Some((key, pass)) } /// return the key and whether the tests passes, when the line is a test /// result (like " PASS [ 0.003s] bacon tests::failing_test3") /// /// In the future, we might want to return the duration too. fn as_test_result_v2(content: &TLine) -> Option<(String, bool)> { let mut strings = content.strings.iter(); let first = strings.next()?; let pass = match (first.csi.as_str(), first.raw.trim()) { (CSI_PASS, "PASS") => true, (CSI_ERROR, "FAIL") => false, _ => return None, }; let _duration = match strings.next() { Some(s) if s.csi.is_empty() => s.raw.trim(), _ => return None, }; let key = extract_key_after_crate_name_v1(strings)?; Some((key, pass)) } #[test] fn test_title_key_v1() { let content = TLine { strings: vec![ TString::new("\u{1b}[35;1m", "--- STDOUT: bacon-test"), TString::new("", " "), TString::new("\u{1b}[36m", "tests::"), TString::new("\u{1b}[34;1m", "failing_test3"), TString::new("\u{1b}[35;1m", " ---"), ], }; assert_eq!( title_key_v1(&content), Some("tests::failing_test3".to_string()) ); let content = TLine { strings: vec![ TString::new("\u{1b}[31;1m", "--- STDERR: bacon"), TString::new("", " "), TString::new("\u{1b}[36m", "analysis::nextest_analyzer::"), TString::new("\u{1b}[34;1m", "test_as_test_result"), TString::new("\u{1b}[31;1m", " ---"), ], }; assert_eq!( title_key_v1(&content), Some("analysis::nextest_analyzer::test_as_test_result".to_string()) ); } #[test] fn test_canceling() { let content = TLine { strings: vec![ TString::new("\u{1b}[31;1m", " Canceling"), TString::new("", " due to "), TString::new("\u{1b}[31;1m", "test failure"), TString::new("", ": "), TString::new("\u{1b}[1m", "1"), TString::new("", " test still running"), ], }; assert_eq!(is_canceling(&content), true); } #[test] fn test_as_test_result() { let content = TLine { strings: vec![ TString::new("\u{1b}[32;1m", " PASS"), TString::new("", " [ 0.003s] "), TString::new("\u{1b}[35;1m", "bacon"), TString::new("", " "), TString::new("\u{1b}[36m", "analysis::nextest_analyzer::test_canceling"), ], }; assert_eq!( as_test_result(&content), Some(( "analysis::nextest_analyzer::test_canceling".to_string(), true )) ); } #[test] fn test_recognize_test_run_failed() { let content = TLine { strings: vec![ TString::new("\u{1b}[31;1m", "error"), TString::new("", ": test run failed"), ], }; assert!(is_error_test_run_failed(&content)); } bacon-3.12.0/src/analysis/python/mod.rs000064400000000000000000000000601046102023000160450ustar 00000000000000pub mod pytest; pub mod ruff; pub mod unittest; bacon-3.12.0/src/analysis/python/pytest.rs000064400000000000000000000065221046102023000166270ustar 00000000000000//! An analyzer for Python's Pytest test framework. use { crate::*, anyhow::Result, lazy_regex::*, }; #[derive(Debug, Default)] pub struct PytestAnalyzer { lines: Vec, } enum PytLineFormat<'l> { H1(&'l str), // big title with `=` H2(&'l str), // smaller title with `_` Location { path: &'l str, line: &'l str }, Other, } enum Section { Errors, Failures, Other, } impl Analyzer for PytestAnalyzer { fn start( &mut self, _mission: &Mission, ) { self.lines.clear(); } fn receive_line( &mut self, line: CommandOutputLine, command_output: &mut CommandOutput, ) { self.lines.push(line.clone()); command_output.push(line); } fn build_report(&mut self) -> Result { build_report(&self.lines) } } fn recognize_format(content: &str) -> PytLineFormat<'_> { regex_switch!(content, r"^(?:={2,39}) (?.+) (?:={2,39})$" => PytLineFormat::H1(title), r"^(?:_{2,39}) (?<title>.+) (?:_{2,39})$" => PytLineFormat::H2(title), r"^file (?<path>\S+\.py), line (?<line>\d{1,8})$" => PytLineFormat::Location { path, line }, r"^(?<path>\S+\.py):(?<line>\d{1,8})" => PytLineFormat::Location { path, line }, ) .unwrap_or(PytLineFormat::Other) } /// Build a report from the output of Python unittest /// /// The main special thing here is transforming the location line in /// a BURP location line. pub fn build_report(cmd_lines: &[CommandOutputLine]) -> anyhow::Result<Report> { let mut current_section = Section::Other; let mut items = ItemAccumulator::default(); let mut last_location_in_item = None; // to deduplicate locations for cmd_line in cmd_lines { let Some(content) = cmd_line.content.if_unstyled() else { continue; // right now we're not expecting styled output }; let format = recognize_format(content); match format { PytLineFormat::H1(title) => { current_section = match title { "ERRORS" => Section::Errors, "FAILURES" => Section::Failures, _ => Section::Other, }; items.close_item(); } PytLineFormat::H2(title) => match current_section { Section::Errors => { items.push_error_title(burp::error_line(title)); last_location_in_item = None; } Section::Failures => { items.push_failure_title(burp::failure_line(title)); last_location_in_item = None; } _ => {} }, PytLineFormat::Location { path, line } => { if let Some(last_location) = last_location_in_item { if last_location == (path, line) { continue; } } last_location_in_item = Some((path, line)); items.push_line( LineType::Location, burp::location_line(format!("{path}:{line}")), ); } PytLineFormat::Other => { items.push_line(LineType::Normal, cmd_line.content.clone()); } } } Ok(items.report()) } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/analysis/python/ruff.rs������������������������������������������������������������0000644�0000000�0000000�00000005765�10461020230�0016251�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! An analyzer for ruff ( <https://docs.astral.sh/ruff/> ) use { crate::*, anyhow::Result, lazy_regex::*, }; #[derive(Debug, Default)] pub struct RuffAnalyzer { lines: Vec<CommandOutputLine>, } #[derive(Debug)] struct LocationTitle<'l> { path: &'l str, line: &'l str, column: &'l str, message: &'l [TString], } #[derive(Debug)] enum RuffLine<'l> { LocationTitle(LocationTitle<'l>), Other, } impl Analyzer for RuffAnalyzer { fn start( &mut self, _mission: &Mission, ) { self.lines.clear(); } fn receive_line( &mut self, line: CommandOutputLine, command_output: &mut CommandOutput, ) { self.lines.push(line.clone()); command_output.push(line); } fn build_report(&mut self) -> Result<Report> { build_report(&self.lines) } } fn recognize_line(tline: &TLine) -> RuffLine { if let Some(lt) = recognize_location_message(tline) { return RuffLine::LocationTitle(lt); } RuffLine::Other } fn recognize_location_message(tline: &TLine) -> Option<LocationTitle> { let tstrings = &tline.strings; if tstrings.len() < 8 { return None; } if tstrings[0].csi != "\u{1b}[1m" || tstrings[1].raw.is_empty() // path || tstrings[1].csi != "\u{1b}[36m" || tstrings[1].raw != ":" || tstrings[2].is_styled() || !regex_is_match!(r"^\d+$", &tstrings[2].raw) // line || tstrings[3].csi != "\u{1b}[36m" || tstrings[3].raw != ":" || tstrings[4].is_styled() || !regex_is_match!(r"^\d+$", &tstrings[4].raw) // column || tstrings[5].csi != "\u{1b}[36m" || tstrings[5].raw != ":" || tstrings[6].is_styled() || tstrings[6].raw != " " { return None; } Some(LocationTitle { path: &tstrings[0].raw, line: &tstrings[2].raw, column: &tstrings[4].raw, message: &tstrings[7..], }) } /// Build a report from the output of biome pub fn build_report(cmd_lines: &[CommandOutputLine]) -> anyhow::Result<Report> { let mut items = ItemAccumulator::default(); let mut last_is_blank = true; let mut i = 0; for cmd_line in cmd_lines { if i < 5 { info!("cmd_line: {:#?}", &cmd_line); i += 1; } let bline = recognize_line(&cmd_line.content); if let RuffLine::LocationTitle(LocationTitle { path, line, column, message, }) = bline { items.push_error_title(burp::error_line_ts(message)); items.push_line( LineType::Location, burp::location_line(format!("{}:{}:{}", path, line, column)), ); last_is_blank = false; } else { let is_blank = cmd_line.content.is_blank(); if !(is_blank && last_is_blank) { items.push_line(LineType::Normal, cmd_line.content.clone()); } last_is_blank = is_blank; } } Ok(items.report()) } �����������bacon-3.12.0/src/analysis/python/unittest.rs��������������������������������������������������������0000644�0000000�0000000�00000005776�10461020230�0017170�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! An analyzer for Python unittest use { crate::*, anyhow::Result, lazy_regex::*, }; #[derive(Debug, Default)] pub struct UnittestAnalyzer { lines: Vec<CommandOutputLine>, } impl Analyzer for UnittestAnalyzer { fn start( &mut self, _mission: &Mission, ) { self.lines.clear(); } fn receive_line( &mut self, line: CommandOutputLine, command_output: &mut CommandOutput, ) { self.lines.push(line.clone()); command_output.push(line); } fn build_report(&mut self) -> Result<Report> { build_report(&self.lines) } } pub fn analyze_line(cmd_line: &CommandOutputLine) -> LineAnalysis { // we're not expecting styled output for unittest (it's probable // some users use decorators, but I don't know those today) let Some(content) = cmd_line.content.if_unstyled() else { return LineAnalysis::normal(); }; regex_switch!(content, r"^FAIL:\s+\S+\s+\((?<key>.+)\)" => LineAnalysis::fail(key), r#"^\s+File ".+", line \d+"# => LineAnalysis::of_type(LineType::Location), "^={50,}$" => LineAnalysis::garbage(), "^-{50,}$" => LineAnalysis::garbage(), r"^Traceback \(most recent call last\)" => LineAnalysis::garbage(), ) .unwrap_or_else(LineAnalysis::normal) } /// Build a report from the output of Python unittest /// /// The main special thing here is transforming the location line in /// a BURP location line. pub fn build_report(cmd_lines: &[CommandOutputLine]) -> anyhow::Result<Report> { let mut items = ItemAccumulator::default(); let mut item_location_written = false; for cmd_line in cmd_lines { let line_analysis = analyze_line(cmd_line); let line_type = line_analysis.line_type; match line_type { LineType::Garbage => { continue; } LineType::Title(kind) => { items.start_item(kind); item_location_written = false; } LineType::Normal => {} LineType::Location => { if !item_location_written { if let Some(content) = cmd_line.content.if_unstyled() { // we rewrite the location as a BURP location if let Some((_, path, line)) = regex_captures!(r#"\s+File "(.+)", line (\d+)"#, content,) { items.push_line( LineType::Location, burp::location_line(format!("{path}:{line}")), ); item_location_written = true; } else { warn!("unconsistent line parsing"); } continue; } } } _ => {} } items.push_line(line_type, cmd_line.content.clone()); } Ok(items.report()) } ��bacon-3.12.0/src/analysis/standard/mod.rs�����������������������������������������������������������0000644�0000000�0000000�00000001455�10461020230�0016335�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mod standard_line_analyser; mod standard_report_building; pub use { standard_line_analyser::StandardLineAnalyzer, standard_report_building::build_report, }; use { crate::*, anyhow::Result, }; #[derive(Debug, Default)] pub struct StandardAnalyzer { lines: Vec<CommandOutputLine>, } impl Analyzer for StandardAnalyzer { fn start( &mut self, _mission: &Mission, ) { self.lines.clear(); } fn receive_line( &mut self, line: CommandOutputLine, command_output: &mut CommandOutput, ) { self.lines.push(line.clone()); command_output.push(line); } fn build_report(&mut self) -> Result<Report> { let line_analyzer = StandardLineAnalyzer {}; build_report(&self.lines, line_analyzer) } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/analysis/standard/standard_line_analyser.rs����������������������������������������0000644�0000000�0000000�00000020256�10461020230�0022263�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, lazy_regex::*, }; #[cfg(not(windows))] const CSI_ERROR_BODY: &str = CSI_BOLD; #[cfg(windows)] const CSI_ERROR_BODY: &str = CSI_BOLD_WHITE; #[derive(Debug, Default)] pub struct StandardLineAnalyzer; impl LineAnalyzer for StandardLineAnalyzer { fn analyze_line( &self, cmd_line: &CommandOutputLine, ) -> LineAnalysis { analyze_line(cmd_line) } } fn analyze_line(cmd_line: &CommandOutputLine) -> LineAnalysis { let content = &cmd_line.content; //debug!("content: {:?}", &content); let mut key = None; let line_type = if cmd_line.content.is_blank() { LineType::Normal } else if let Some(content) = cmd_line.content.if_unstyled() { if let Some((k, r)) = as_test_result(content) { key = Some(k.to_string()); LineType::TestResult(r) } else if let Some(k) = as_fail_result_title(content) { key = Some(k.to_string()); LineType::Title(Kind::TestFail) } else if let Some(k) = as_stack_overflow_message(content) { key = Some(k.to_string()); LineType::Title(Kind::TestFail) } else if cmd_line.origin == CommandStream::StdErr && regex_is_match!("^error: ", content) && !regex_is_match!("^error: aborting due to", content) { // This recognizes the error in case there's no styling (eg miri run, see issue #251). // If there proves to be too many false positives, this might be moved to an "unstyled" // analyzer dedicated to those cases when we can't get any styling information. LineType::Title(Kind::Error) } else if cmd_line.origin == CommandStream::StdErr && regex_is_match!("^warning: ", content) && !regex_is_match!(r"generated \d+ warnings?$", content) { // This recognizes the warning in case there's no styling (eg miri run, see issue #251) LineType::Title(Kind::Warning) } else if regex_is_match!("^failures:$", content) { // this isn't very discriminant... LineType::Title(Kind::Sum) } else if regex_is_match!("[Rr]un with (`)?RUST_BACKTRACE=", content) { LineType::BacktraceSuggestion } else if regex_is_match!(r#", [^:\s'"]+:\d+:\d+$"#, content) { // this kind of location comes up in test failures LineType::Location } else if regex_is_match!(r#"^\s+--> [^:\s'"]+:\d+:\d+$"#, content) { // this comes up in test failures to compile LineType::Location } else if regex_is_match!(r#"^thread '.+' panicked at [^:\s'"]+:\d+:\d+:$"#, content) { // this comes up in test failures LineType::Location } else { LineType::Normal } } else { let ts0 = content.strings.get(0); let ts1 = content.strings.get(1); match (ts0, ts1) { (Some(title), Some(body)) => { match ( title.csi.as_ref(), title.raw.as_ref(), body.csi.as_ref(), body.raw.as_ref(), ) { (CSI_BOLD_RED, "error", CSI_ERROR_BODY, body_raw) if body_raw.starts_with(": aborting due to") => { LineType::Title(Kind::Sum) } (CSI_BOLD_RED, title_raw, CSI_ERROR_BODY, _) if title_raw.starts_with("error") => { LineType::Title(Kind::Error) } #[cfg(not(windows))] (CSI_BOLD_YELLOW, "warning", _, body_raw) => { determine_warning_type(body_raw, content) } #[cfg(windows)] (CSI_BOLD_YELLOW | CSI_BOLD_4BIT_YELLOW, "warning", _, body_raw) => { determine_warning_type(body_raw, content) } ("", title_raw, CSI_BOLD_BLUE, "--> ") if is_spaces(title_raw) => { LineType::Location } ("", title_raw, CSI_BOLD_BLUE, "::: ") if is_spaces(title_raw) => { LineType::Location } ("", k, CSI_BOLD_RED | CSI_RED, "FAILED") if content.strings.len() == 2 => { if let Some(k) = as_test_name(k) { key = Some(k.to_string()); LineType::TestResult(false) } else { LineType::Normal } } ("", k, CSI_GREEN, "ok") => { if let Some(k) = as_test_name(k) { key = Some(k.to_string()); LineType::TestResult(true) } else { LineType::Normal } } _ => LineType::Normal, } } (Some(content), None) => { if regex_is_match!( r#"^thread '.+' panicked at [^:\s'"]+:\d+:\d+:$"#, &content.raw ) { // this comes up in nextest failures LineType::Location } else { LineType::Normal } } _ => LineType::Normal, // empty line } }; LineAnalysis { line_type, key } } fn determine_warning_type( body_raw: &str, content: &TLine, ) -> LineType { if is_n_warnings_emitted(body_raw) || is_generated_n_warnings(&content.strings) || is_build_failed(content.strings.get(2)) { LineType::Title(Kind::Sum) } else { LineType::Title(Kind::Warning) } } fn is_spaces(s: &str) -> bool { s.chars().all(|c| c.is_ascii_whitespace()) } /// check if the string starts with something like ": 15 warnings emitted" fn is_n_warnings_emitted(s: &str) -> bool { regex_is_match!(r#"^: \d+ warnings? emitted"#, s) } fn is_generated_n_warnings(ts: &[TString]) -> bool { ts.iter() .any(|ts| regex_is_match!(r#"generated \d+ warnings?"#, &ts.raw)) } fn is_build_failed(ts: Option<&TString>) -> bool { ts.is_some_and(|ts| regex_is_match!(r#"^\s*build failed"#, &ts.raw)) } /// similar to as_test_result but without the FAILED|ok part /// This is used in case of styled output (because the FAILED|ok /// part is in another TString) fn as_test_name(s: &str) -> Option<&str> { regex_captures!( r#"^(?:test\s+)?(.+?)(?: - should panic\s*)?(?: - compile\s*)?\s+...\s*$"#, s ) .map(|(_, key)| key) } /// return Some when the line is the non detailled /// result of a test, for example /// /// "test str_fit::fitting_count_tests::test_count_fitting ... FAILED" /// or /// "test wrap::wrap_tests::check_space_removing ... ok" /// or /// "test src/lib.rs - (line 6) ... FAILED" /// or /// "test src/lib.rs - (line 10) ... ok" /// or /// "test src/mode.rs - mode::Mode::new (line 121) - compile ... FAILED" /// (in this case, the " - compile" part isn't in the key, see #64) /// or /// "test tests::another - should panic ... FAILED" /// (in this case, the " - should panic" part isn't in the key, see #95) fn as_test_result(s: &str) -> Option<(&str, bool)> { regex_captures!( r#"^(?:test\s+)?(.+?)(?: - should panic\s*)?(?: - compile\s*)?\s+...\s+(\w+)$"#, s ) .and_then(|(_, key, outcome)| match outcome { "ok" => Some((key, true)), "FAILED" => Some((key, false)), other => { warn!("unrecognized doctest outcome: {:?}", other); None } }) } /// return Some(key) when the line is like this: /// "---- key stdout ----" fn as_fail_result_title(s: &str) -> Option<&str> { regex_captures!(r#"^---- (.+) stdout ----$"#, s).map(|(_, key)| key) } /// Returns Some(key) when the line is like this: /// thread 'key' has overflowed its stack fn as_stack_overflow_message(s: &str) -> Option<&str> { regex_captures!("^thread '(.+)' has overflowed its stack$", s).map(|(_, key)| key) } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/analysis/standard/standard_report_building.rs��������������������������������������0000644�0000000�0000000�00000013032�10461020230�0022620�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, rustc_hash::FxHashMap, }; pub fn build_report<L: LineAnalyzer>( cmd_lines: &[CommandOutputLine], line_analyzer: L, ) -> anyhow::Result<Report> { #[derive(Debug, Default)] struct Failure { has_title: bool, } // we first accumulate warnings, test fails and errors in separate vectors let mut warnings = Vec::new(); let mut errors = Vec::new(); let mut fails = Vec::new(); let mut failures: FxHashMap<String, Failure> = Default::default(); let mut passed_tests = 0; let mut cur_err_kind = None; // the current kind among stderr lines let mut is_in_out_fail = false; let mut suggest_backtrace = false; for cmd_line in cmd_lines { let line_analysis = line_analyzer.analyze_line(cmd_line); let line_type = line_analysis.line_type; let mut line = Line { item_idx: 0, // will be filled later line_type, content: cmd_line.content.clone(), }; match (line_type, line_analysis.key) { (LineType::Garbage, _) => { continue; } (LineType::TestResult(r), Some(key)) => { if r { passed_tests += 1; } else if !failures.contains_key(&key) { // we should receive the test failure section later, // right now we just whitelist it failures.insert(key, Failure::default()); } } (LineType::Title(Kind::TestFail), Some(key)) => { let failure = failures.entry(key.clone()).or_default(); is_in_out_fail = true; cur_err_kind = Some(Kind::TestFail); if failure.has_title { // we already have a title for this failure // (for nextest, we have a title for stdout and one for stderr) continue; } failure.has_title = true; line.content = TLine::failed(&key); fails.push(line); } (LineType::Normal, None) => { if line.content.is_blank() { if cur_err_kind == Some(Kind::TestFail) { // beautification: we remove some blank lines if let Some(last) = fails.last() { if last.line_type != LineType::Normal || last.content.is_blank() { continue; } } } else { is_in_out_fail = false; } } if is_in_out_fail { fails.push(line); } else { match cur_err_kind { Some(Kind::Warning) => warnings.push(line), Some(Kind::Error) => errors.push(line), _ => {} } } } (LineType::Title(Kind::Sum), None) => { // we're not interested in this section cur_err_kind = None; is_in_out_fail = false; } (LineType::SectionEnd, None) => { cur_err_kind = None; is_in_out_fail = false; } (LineType::Title(kind), _) => { cur_err_kind = Some(kind); match cur_err_kind { Some(Kind::Warning) => warnings.push(line), Some(Kind::Error) => errors.push(line), _ => {} // before warnings and errors, or in a sum } } (LineType::BacktraceSuggestion, _) => { suggest_backtrace = true; } (LineType::Location, _) => { match cur_err_kind { Some(Kind::Warning) => warnings.push(line), Some(Kind::Error) => errors.push(line), Some(Kind::TestFail) => fails.push(line), _ => {} // before warnings and errors, or in a sum } } _ => {} } } for (key, failure) in &failures { // if we know of a failure but there was no content, we add some if failure.has_title { continue; } fails.push(Line { item_idx: 0, // will be filled later line_type: LineType::Title(Kind::TestFail), content: TLine::failed(key), }); fails.push(Line { item_idx: 0, line_type: LineType::Normal, content: TLine::italic("no output".to_string()), }); } // we now build a common vector, with errors first let mut lines = errors; lines.append(&mut fails); lines.append(&mut warnings); // and we assign the indexes let mut item_idx = 0; for line in &mut lines { if matches!(line.line_type, LineType::Title(_)) { item_idx += 1; } line.item_idx = item_idx; } // we compute the stats at end because some lines may // have been read but not added (at start or end) let mut stats = Stats::from(&lines); stats.passed_tests = passed_tests; debug!("stats: {:#?}", &stats); let failure_keys = failures.keys().cloned().collect(); let report = Report { lines, stats, suggest_backtrace, output: Default::default(), failure_keys, analyzer_exports: Default::default(), }; Ok(report) } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/analysis/stats.rs������������������������������������������������������������������0000644�0000000�0000000�00000002544�10461020230�0015114�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, serde::{ Deserialize, Serialize, }, }; /// number of lines per type in a report #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct Stats { pub warnings: usize, pub errors: usize, pub test_fails: usize, pub passed_tests: usize, pub location_lines: usize, pub normal_lines: usize, } impl From<&Vec<Line>> for Stats { fn from(lines: &Vec<Line>) -> Self { lines.iter().fold(Stats::default(), |mut stats, line| { match line.line_type { LineType::Title(Kind::Error) => stats.errors += 1, LineType::Title(Kind::Warning) => stats.warnings += 1, LineType::Title(Kind::TestFail) => stats.test_fails += 1, LineType::Location => stats.location_lines += 1, _ => stats.normal_lines += 1, } stats }) } } impl Stats { pub fn lines( &self, summary: bool, ) -> usize { let mut sum = self.warnings + self.errors + self.test_fails + self.location_lines; if !summary { sum += self.normal_lines; } sum } pub fn items(&self) -> usize { self.warnings + self.errors + self.test_fails } pub fn can_scope_tests(&self) -> bool { self.passed_tests > 0 && self.test_fails > 0 } } ������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/burp/mod.rs������������������������������������������������������������������������0000644�0000000�0000000�00000003253�10461020230�0013660�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������//! Utilities related to BURP //! See <https://dystroy.org/blog/bacon-everything-roadmap/#introduce-burp> use crate::*; /// Make a BURP compliant location line pub fn location_line<S: Into<String>>(location: S) -> TLine { let mut line = TLine::default(); line.strings .push(TString::new("\u{1b}[1m\u{1b}[38;5;12m", " --> ")); line.strings.push(TString::new("", location)); line } /// Make a BURP compliant error line (title) pub fn error_line(error: &str) -> TLine { let mut line = TLine::default(); line.strings.push(TString::new(CSI_BOLD_RED, "error")); line.strings.push(TString::new("", ": ")); line.strings.push(TString::new("", error.to_string())); line } /// Make a BURP compliant error line (title) from a [TString] pub fn error_line_ts(error: &[TString]) -> TLine { let mut line = TLine::default(); line.strings.push(TString::new(CSI_BOLD_RED, "error")); line.strings.push(TString::new("", ": ")); line.strings.extend(error.iter().cloned()); line } /// Make a BURP compliant warning line (title) from a [TString] pub fn warning_line_ts(warning: &[TString]) -> TLine { let mut line = TLine::default(); line.strings.push(TString::new(CSI_BOLD_YELLOW, "warning")); line.strings.push(TString::new("", ": ")); line.strings.extend(warning.iter().cloned()); line } /// Make a BURP compliant test failure line (title) /// (this one isn't based on cargo) pub fn failure_line(error: &str) -> TLine { let mut line = TLine::default(); line.strings.push(TString::new(CSI_BOLD_YELLOW, "failure")); line.strings.push(TString::new("", ": ")); line.strings.push(TString::new("", error.to_string())); line } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/cli/args.rs������������������������������������������������������������������������0000644�0000000�0000000�00000013377�10461020230�0013644�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::{ Result, bail, }, clap::{ CommandFactory, Parser, }, clap_complete::ArgValueCandidates, termimad::ansi, }; static INTRO: &str = " **bacon** watches your project and runs jobs in background. Use shortcuts to: * switch job: *t* for `test`, *c* for `clippy`, *d* to open rust doc, etc. * toggle display: *s* for summary, *w* for wrapped lines, etc. * search: */* * see all shortcuts: *?* Complete documentation at https://dystroy.org/bacon "; /// Launch arguments #[derive(Debug, Parser)] #[command( author, about, version, disable_version_flag = true, disable_help_flag = true )] pub struct Args { /// Print help information #[arg(long)] pub help: bool, /// Print the version #[arg(long)] pub version: bool, /// Print the path to the prefs file, create it if it doesn't exist #[clap(long)] pub prefs: bool, /// Run without user interface: just run the default job on change #[clap(long)] pub headless: bool, /// Start in summary mode #[clap(short = 's', long)] pub summary: bool, /// Start in full mode (not summary) #[clap(short = 'S', long)] pub no_summary: bool, /// Start with lines wrapped #[clap(short = 'w', long)] pub wrap: bool, /// Start with lines not wrapped #[clap(short = 'W', long)] pub no_wrap: bool, /// Start with gui vertical order reversed #[clap(long)] pub reverse: bool, /// Start with standard gui order #[clap(long)] pub no_reverse: bool, /// Display a help line #[clap(long)] pub help_line: bool, /// Do not display a help line #[clap(long)] pub no_help_line: bool, /// List available jobs #[clap(short = 'l', long)] pub list_jobs: bool, #[clap(long, hide = true)] pub completion_list_jobs: bool, /// Don't access the network #[clap(long)] pub offline: bool, /// Create a bacon.toml file, ready to be customized #[clap(long)] pub init: bool, /// Job to launch: `check`, `clippy`, custom ones... #[clap(short = 'j', long, value_name = "job", add = ArgValueCandidates::new(crate::cli::completions::list_jobs))] pub job: Option<ConcreteJobRef>, /// Ignore features of both the package and the bacon job #[clap(long)] pub no_default_features: bool, /// Comma separated list of features /// (if the job defines some, they're merged) #[clap(long, value_name = "features")] pub features: Option<String>, /// Activate all available features #[clap(long)] pub all_features: bool, /// Export locations in `.bacon-locations` file #[clap(short = 'e', long)] pub export_locations: bool, /// Don't export locations #[clap(short = 'E', long)] pub no_export_locations: bool, /// Path to watch (overriding what's normally computed from the project's /// type, bacon.toml file, etc.) #[clap(long, value_name = "watch", value_hint = clap::ValueHint::FilePath)] pub watch: Option<String>, /// Project to run jobs on, and use as working directory #[clap(long, value_name = "project", value_hint = clap::ValueHint::DirPath)] pub project: Option<String>, /// Configuration passed as a TOML string #[clap(long)] pub config_toml: Option<String>, #[clap(add = ArgValueCandidates::new(crate::cli::completions::list_jobs))] /// What to do: either a job, or a path, or both pub args: Vec<String>, #[clap(last = true)] /// Arguments given to the job pub additional_job_args: Vec<String>, } impl Args { /// positional arguments in bacon command are a convenience /// allowing to skip writing `-j`, `-p`, or both. /// To be used, they must be copied to the `job` or /// `path` values. pub fn fix(&mut self) -> Result<()> { let mut args = self.args.drain(..); match ( args.next(), args.next(), self.job.is_none(), self.project.is_none(), ) { (Some(a), b, true, true) => { if a.contains('.') || a.contains('/') { // a is a path, it can't be job self.project = Some(a); self.job = b.map(|b| b.as_str().into()); } else { self.job = Some(a.as_str().into()); self.project = b; } } (Some(_), Some(_), _, _) => { bail!("Too many arguments"); } (Some(a), None, true, false) => { self.job = Some(a.as_str().into()); } (Some(a), None, false, true) => { self.project = Some(a); } (Some(a), None, false, false) => { bail!("Unexpected argument {:?}", a); } _ => {} } Ok(()) } pub fn print_help(&self) { let mut printer = clap_help::Printer::new(Args::command()) .with("introduction", INTRO) .with("options", clap_help::TEMPLATE_OPTIONS_MERGED_VALUE) .without("author"); let skin = printer.skin_mut(); skin.headers[0].compound_style.set_fg(ansi(204)); skin.bold.set_fg(ansi(204)); skin.italic = termimad::CompoundStyle::with_fg(ansi(204)); // 2, 81, 73, 38 printer.template_keys_mut().push("examples"); printer.set_template("examples", EXAMPLES_TEMPLATE); for (i, example) in EXAMPLES.iter().enumerate() { printer .expander_mut() .sub("examples") .set("example-number", i + 1) .set("example-title", example.title) .set("example-cmd", example.cmd); } printer.print_help(); } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/cli/completions.rs�����������������������������������������������������������������0000644�0000000�0000000�00000001160�10461020230�0015227�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { clap_complete::CompletionCandidate, std::process::Command, }; fn with_self_command( f: impl FnOnce(Command) -> Option<Vec<CompletionCandidate>> ) -> Vec<CompletionCandidate> { std::env::current_exe() .ok() .and_then(|command| f(Command::new(command))) .unwrap_or_default() } pub fn list_jobs() -> Vec<CompletionCandidate> { with_self_command(|mut c| { let output = c.arg("--completion-list-jobs").output().ok()?; let output: String = String::from_utf8(output.stdout).ok()?; Some(output.split('\0').map(CompletionCandidate::new).collect()) }) } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/cli/mod.rs�������������������������������������������������������������������������0000644�0000000�0000000�00000005761�10461020230�0013465�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mod args; mod completions; pub use args::*; use { crate::*, anyhow::anyhow, clap::{ CommandFactory, Parser, }, std::{ fs, io::Write, }, termimad::crossterm::{ QueueableCommand, cursor, terminal::{ EnterAlternateScreen, LeaveAlternateScreen, }, }, }; #[cfg(windows)] use termimad::crossterm::event::{ DisableMouseCapture, EnableMouseCapture, }; /// The Write type used by all GUI writing functions pub type W = std::io::BufWriter<std::io::Stdout>; /// return the writer used by the application pub fn writer() -> W { std::io::BufWriter::new(std::io::stdout()) } pub fn run() -> anyhow::Result<()> { if std::env::var_os("COMPLETE").is_some() { clap_complete::CompleteEnv::with_factory(Args::command).complete(); } let mut args: Args = Args::parse(); args.fix()?; info!("args: {:#?}", &args); let headless = args.headless; if args.help { args.print_help(); return Ok(()); } if args.version { println!("bacon {}", env!("CARGO_PKG_VERSION")); return Ok(()); } if args.prefs { let prefs_path = bacon_prefs_path().ok_or(anyhow!("No preferences location known for this system."))?; if !prefs_path.exists() { fs::create_dir_all(prefs_path.parent().unwrap())?; fs::write(&prefs_path, DEFAULT_PREFS.trim_start())?; // written to stderr to allow initialization with commands like // $EDITOR "$(bacon --prefs)" eprintln!("Preferences file written."); } println!("{}", prefs_path.to_string_lossy()); return Ok(()); } let context = Context::new(&args)?; debug!("mission context: {:#?}", &context); if args.init { let package_config_path = context.package_config_path(); if !package_config_path.exists() { fs::write(&package_config_path, DEFAULT_PACKAGE_CONFIG.trim_start())?; eprintln!("bacon project configuration file written."); } else { eprintln!("bacon configuration file already exists."); } println!("{}", package_config_path.to_string_lossy()); return Ok(()); } let settings = Settings::read(&args, &context)?; if args.list_jobs { print_jobs(&settings); return Ok(()); } if args.completion_list_jobs { for job in settings.jobs.keys() { print!("{job}\0"); } return Ok(()); } let mut w = writer(); if !headless { w.queue(EnterAlternateScreen)?; w.queue(cursor::Hide)?; #[cfg(windows)] w.queue(EnableMouseCapture)?; w.flush()?; } let result = tui::app::run(&mut w, settings, &args, context, headless); if !headless { #[cfg(windows)] w.queue(DisableMouseCapture)?; w.queue(cursor::Show)?; w.queue(LeaveAlternateScreen)?; } w.flush()?; result } ���������������bacon-3.12.0/src/conf/action.rs���������������������������������������������������������������������0000644�0000000�0000000�00000012521�10461020230�0014331�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, lazy_regex::*, serde::{ Deserialize, Deserializer, Serialize, Serializer, de, }, std::{ fmt, str::FromStr, }, }; /// An action that can be mapped to a key /// /// While it's not really specified, export names, just like /// job names, must be gentle enough so as to be correctly parsed /// (if not, they won't go very far in the system). #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Action { Export(String), Internal(Internal), Job(JobRef), } impl Action { /// Return the action description to show in doc/help pub fn doc(&self) -> String { match self { Self::Export(export_name) => format!("run *{export_name}* export"), Self::Internal(internal) => internal.doc(), Self::Job(job_name) => format!("start the *{job_name}* job"), } } } #[derive(Debug, PartialEq)] pub enum ParseActionError { UnknownAction(String), UnknowCategory(String), UnknownInternal(String), } impl fmt::Display for ParseActionError { fn fmt( &self, f: &mut fmt::Formatter, ) -> fmt::Result { match self { Self::UnknownAction(s) => { write!( f, "Action not understood: {s:?} (did you mean \"job:{s}\"?)" ) } Self::UnknowCategory(s) => { write!(f, "Unknown category: {s:?}") } Self::UnknownInternal(s) => { write!(f, "Internal not understood: {s:?}") } } } } impl std::error::Error for ParseActionError {} impl fmt::Display for Action { fn fmt( &self, f: &mut fmt::Formatter, ) -> fmt::Result { match self { Self::Export(name) => write!(f, "export:{}", name), Self::Internal(internal) => internal.fmt(f), Self::Job(job_ref) => write!(f, "job:{}", job_ref), } } } impl FromStr for Action { type Err = ParseActionError; fn from_str(s: &str) -> Result<Self, ParseActionError> { if let Some((_, cat, con)) = regex_captures!(r#"^(\w+)\s*:\s*(.+)$"#, s) { match cat { "export" => Ok(Self::Export(con.into())), "internal" => { // this prefix is optional if let Ok(internal) = Internal::from_str(con) { Ok(Self::Internal(internal)) } else { Err(ParseActionError::UnknownInternal(con.to_string())) } } "job" => Ok(Self::Job(con.into())), _ => Err(ParseActionError::UnknowCategory(cat.to_string())), } } else if let Ok(internal) = Internal::from_str(s) { Ok(Self::Internal(internal)) } else { Err(ParseActionError::UnknownAction(s.to_string())) } } } impl From<Internal> for Action { fn from(i: Internal) -> Self { Self::Internal(i) } } impl From<JobRef> for Action { fn from(jr: JobRef) -> Self { Self::Job(jr) } } impl Serialize for Action { fn serialize<S>( &self, serializer: S, ) -> Result<S::Ok, S::Error> where S: Serializer, { serializer.collect_str(self) } } impl<'de> Deserialize<'de> for Action { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; FromStr::from_str(&s).map_err(de::Error::custom) } } #[test] fn test_action_string_round_trip() { let actions = vec![ Action::Job(JobRef::Default), Action::Job(JobRef::Initial), Action::Job(JobRef::PreviousOrQuit), Action::Job(JobRef::Previous), Action::Job(JobRef::Concrete(ConcreteJobRef { name_or_alias: NameOrAlias::Name("run".to_string()), scope: Scope::default(), })), Action::Job(JobRef::Concrete(ConcreteJobRef { name_or_alias: NameOrAlias::Name("nextest".to_string()), scope: Scope { tests: vec!["first::test".to_string(), "second_test".to_string()], }, })), Action::Job(JobRef::Concrete(ConcreteJobRef { name_or_alias: NameOrAlias::Alias("my-check".to_string()), scope: Scope::default(), })), Action::Job(JobRef::Concrete(ConcreteJobRef { name_or_alias: NameOrAlias::Alias("my-test".to_string()), scope: Scope { tests: vec!["abc".to_string()], }, })), Action::Job(JobRef::Concrete(ConcreteJobRef { name_or_alias: NameOrAlias::Alias("a forbidden name!".to_string()), scope: Scope { tests: vec!["abc".to_string()], }, })), Action::Internal(Internal::Help), Action::Internal(Internal::Scroll(ScrollCommand::MilliPages(1500))), Action::Internal(Internal::Scroll(ScrollCommand::MilliPages(-500))), Action::Internal(Internal::Scroll(ScrollCommand::MilliPages(-2000))), Action::Export("my export".to_string()), ]; for action in actions { println!("action: {}", action.to_string()); assert_eq!(action.to_string().parse(), Ok(action)); } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/conf/auto_refresh.rs���������������������������������������������������������������0000644�0000000�0000000�00000000564�10461020230�0015546�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#[derive(Debug, Clone, Copy, PartialEq)] pub enum AutoRefresh { /// Don't rerun the job on file changes. Paused, /// Run the job on file changes. Enabled, } impl AutoRefresh { pub fn is_enabled(self) -> bool { matches!(self, AutoRefresh::Enabled) } pub fn is_paused(self) -> bool { matches!(self, AutoRefresh::Paused) } } ��������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/conf/cargo_wrapped_config.rs�������������������������������������������������������0000644�0000000�0000000�00000002275�10461020230�0017223�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { super::Config, anyhow::*, serde::Deserialize, std::path::Path, }; #[derive(Deserialize)] struct CargoWrappedConfig { workspace: Option<WrappedMetadata>, package: Option<WrappedMetadata>, } #[derive(Deserialize)] struct WrappedMetadata { metadata: Option<WrappedConfig>, } #[derive(Deserialize)] struct WrappedConfig { bacon: Option<Config>, } pub fn load_config_from_cargo_toml(cargo_file_path: &Path) -> Result<Vec<Config>> { if !cargo_file_path.exists() { return Ok(Vec::default()); } let cargo_toml = std::fs::read_to_string(cargo_file_path)?; let mut cargo: CargoWrappedConfig = toml::from_str(&cargo_toml)?; let mut configs = Vec::new(); let worskpace_config = cargo .workspace .take() .and_then(|workspace| workspace.metadata) .and_then(|metadata| metadata.bacon); if let Some(config) = worskpace_config { configs.push(config); } let worskpace_config = cargo .package .take() .and_then(|package| package.metadata) .and_then(|metadata| metadata.bacon); if let Some(config) = worskpace_config { configs.push(config); } Ok(configs) } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/conf/config.rs���������������������������������������������������������������������0000644�0000000�0000000�00000006040�10461020230�0014320�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::*, lazy_regex::regex_is_match, serde::Deserialize, std::{ collections::HashMap, fs, path::Path, }, }; /// A configuration item which may be stored in various places, eg as `bacon.toml` /// along a `Cargo.toml` file or as `prefs.toml` in the xdg config directory. /// /// Leaf values are options (and not Default) so that they don't /// override previously set values when applied to settings. #[derive(Debug, Clone, Deserialize)] pub struct Config { pub additional_alias_args: Option<Vec<String>>, pub default_job: Option<ConcreteJobRef>, /// Default config for a job #[serde(flatten)] pub all_jobs: Job, /// locations export #[deprecated(since = "2.22.0", note = "use exports.locations")] pub export: Option<ExportConfig>, #[deprecated(since = "2.9.0", note = "use exports.locations.auto")] pub export_locations: Option<bool>, #[serde(default)] pub exports: HashMap<String, ExportConfig>, pub help_line: Option<bool>, #[serde(default)] pub jobs: HashMap<String, Job>, pub keybindings: Option<KeyBindings>, pub reverse: Option<bool>, pub summary: Option<bool>, #[deprecated(since = "2.0.0", note = "use keybindings")] pub vim_keys: Option<bool>, pub wrap: Option<bool>, } impl Config { /// Load from zero to two configuration items from the provided path which /// must be in TOML format but may not exist. /// /// Expected structures are either bacon config or a cargo.toml file (which /// may contain a workspace.metadata.bacon key and a package.metadata.bacon key). pub fn from_path_detect(path: &Path) -> Result<Vec<Self>> { if !path.exists() { return Ok(Vec::default()); } let file_name = path.file_name().and_then(|f| f.to_str()); if file_name == Some("Cargo.toml") { load_config_from_cargo_toml(path) } else { Ok(vec![Self::from_path(path)?]) } } /// Load a configuration item filling the provided path in TOML pub fn from_path(path: &Path) -> Result<Self> { let conf = toml::from_str::<Self>(&fs::read_to_string(path)?) .with_context(|| format!("Failed to parse configuration file at {:?}", path))?; for (name, job) in &conf.jobs { if !regex_is_match!(r#"^[\w-]+$"#, name) { bail!("Invalid configuration : Illegal job name : {:?}", name); } if job.command.is_empty() { bail!("Invalid configuration : empty command for job {:?}", name); } } Ok(conf) } pub fn default_package_config() -> Self { toml::from_str(DEFAULT_PACKAGE_CONFIG).unwrap() } pub fn default_prefs() -> Self { toml::from_str(DEFAULT_PREFS).unwrap() } } #[test] fn test_default_files() { let mut settings = Settings::default(); settings.apply_config(&Config::default_prefs()); settings.apply_config(&Config::default_package_config()); settings.check().unwrap(); } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/conf/defaults.rs�������������������������������������������������������������������0000644�0000000�0000000�00000000262�10461020230�0014662�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������pub static DEFAULT_PREFS: &str = include_str!("../../defaults/default-prefs.toml"); pub static DEFAULT_PACKAGE_CONFIG: &str = include_str!("../../defaults/default-bacon.toml"); ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/conf/keybindings.rs����������������������������������������������������������������0000644�0000000�0000000�00000014250�10461020230�0015363�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, crokey::*, serde::Deserialize, std::{ collections::{ HashMap, hash_map, }, fmt, }, }; /// A mapping from key combinations to actions. /// /// Several key combinations can go to the same action. #[derive(Clone, Deserialize)] pub struct KeyBindings { #[serde(flatten)] map: HashMap<KeyCombination, Action>, } impl Default for KeyBindings { fn default() -> Self { let mut bindings = Self { map: HashMap::default(), }; bindings.set(key!('?'), Internal::Help); bindings.set(key!(h), Internal::Help); bindings.set(key!(ctrl - c), Internal::Quit); bindings.set(key!(ctrl - q), Internal::Quit); bindings.set(key!(q), Internal::Quit); bindings.set(key!(F5), Internal::Refresh); bindings.set(key!(s), Internal::ToggleSummary); bindings.set(key!(w), Internal::ToggleWrap); bindings.set(key!(b), Internal::ToggleBacktrace("1")); bindings.set(key!(Home), Internal::Scroll(ScrollCommand::Top)); bindings.set(key!(End), Internal::Scroll(ScrollCommand::Bottom)); bindings.set(key!(Up), Internal::Scroll(ScrollCommand::Lines(-1))); bindings.set(key!(Down), Internal::Scroll(ScrollCommand::Lines(1))); bindings.set(key!(PageUp), Internal::Scroll(ScrollCommand::pages(-1))); bindings.set(key!(PageDown), Internal::Scroll(ScrollCommand::pages(1))); bindings.set( key!(Space), Internal::Scroll(ScrollCommand::MilliPages(800)), ); bindings.set(key!(f), Internal::ScopeToFailures); bindings.set(key!(esc), Internal::Back); bindings.set(key!(ctrl - d), JobRef::Default); bindings.set(key!(i), JobRef::Initial); bindings.set(key!(p), Internal::TogglePause); bindings.set(key!('/'), Internal::FocusSearch); bindings.set(key!(':'), Internal::FocusGoto); bindings.set(key!(enter), Internal::Validate); bindings.set(key!(tab), Internal::NextMatch); bindings.set(key!(backtab), Internal::PreviousMatch); bindings.set(key!(shift - backtab), Internal::PreviousMatch); // keybindings for some common jobs bindings.set(key!(a), JobRef::from_job_name("check-all")); bindings.set(key!(c), JobRef::from_job_name("clippy")); bindings.set(key!(d), JobRef::from_job_name("doc-open")); bindings.set(key!(t), JobRef::from_job_name("test")); bindings.set(key!(n), JobRef::from_job_name("nextest")); bindings.set(key!(r), JobRef::from_job_name("run")); bindings } } impl KeyBindings { pub fn set<A: Into<Action>>( &mut self, ck: KeyCombination, action: A, ) { self.map.insert(ck, action.into()); } pub fn add_vim_keys(&mut self) { self.set(key!(g), Internal::Scroll(ScrollCommand::Top)); self.set(key!(shift - g), Internal::Scroll(ScrollCommand::Bottom)); self.set(key!(k), Internal::Scroll(ScrollCommand::Lines(-1))); self.set(key!(j), Internal::Scroll(ScrollCommand::Lines(1))); } pub fn add_all( &mut self, other: &KeyBindings, ) { for (ck, action) in other.map.iter() { self.map.insert(*ck, action.clone()); } } pub fn get( &self, key: KeyCombination, ) -> Option<&Action> { self.map.get(&key) } /// return the shortest key.to_string for the internal, if any pub fn shortest_action_key<F>( &self, filter: F, ) -> Option<String> where F: Fn(&Action) -> bool, { let mut shortest: Option<String> = None; for (ck, action) in &self.map { if filter(action) { let s = ck.to_string(); match &shortest { Some(previous) if previous.len() < s.len() => {} _ => { shortest = Some(s); } } } } shortest } /// return the shortest key.to_string for the internal, if any pub fn shortest_internal_key( &self, internal: Internal, ) -> Option<String> { let internal_action = Action::Internal(internal); self.shortest_action_key(|action| action == &internal_action) } /// build and return a map from actions to all the possible shortcuts pub fn build_reverse_map(&self) -> HashMap<&Action, Vec<KeyCombination>> { let mut reverse_map = HashMap::new(); for (ck, action) in &self.map { reverse_map.entry(action).or_insert_with(Vec::new).push(*ck); } reverse_map } } impl<'a> IntoIterator for &'a KeyBindings { type Item = (&'a KeyCombination, &'a Action); type IntoIter = hash_map::Iter<'a, KeyCombination, Action>; fn into_iter(self) -> Self::IntoIter { self.map.iter() } } impl fmt::Debug for KeyBindings { fn fmt( &self, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { let mut ds = f.debug_struct("KeyBindings"); for (kc, action) in &self.map { ds.field(&kc.to_string(), &action.to_string()); } ds.finish() } } #[test] fn test_deserialize_keybindings() { #[derive(Deserialize)] struct Config { keybindings: KeyBindings, } let toml = r#" [keybindings] Ctrl-U = "internal:scroll-pages(-.5)" Ctrl-d = "internal:scroll-page(1)" alt-q = "internal:quit" alt-p = "job:previous" "#; let conf = toml::from_str::<Config>(toml).unwrap(); assert_eq!( conf.keybindings.get(key!(ctrl - u)), Some(&Action::Internal(Internal::Scroll( ScrollCommand::MilliPages(-500) ))), ); assert_eq!( conf.keybindings.get(key!(ctrl - d)), Some(&Action::Internal(Internal::Scroll( ScrollCommand::MilliPages(1000) ))), ); assert_eq!(conf.keybindings.get(key!(z)), None,); assert_eq!( conf.keybindings.get(key!(alt - q)), Some(&Action::Internal(Internal::Quit)), ); assert_eq!( conf.keybindings.get(key!(alt - p)), Some(&Action::Job(JobRef::Previous)), ); } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/conf/mod.rs������������������������������������������������������������������������0000644�0000000�0000000�00000002331�10461020230�0013631�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mod action; mod auto_refresh; mod cargo_wrapped_config; mod config; mod defaults; mod keybindings; mod settings; pub use { action::*, auto_refresh::*, cargo_wrapped_config::*, config::*, defaults::*, keybindings::*, settings::*, }; use std::path::{ Path, PathBuf, }; /// If the system can manage application preferences, return the /// canonical path to the bacon preferences file pub fn bacon_prefs_path() -> Option<std::path::PathBuf> { directories_next::ProjectDirs::from("org", "dystroy", "bacon") .map(|project_dir| project_dir.config_dir().join("prefs.toml")) } /// Return the path given by the env var, if it exists (doesn't check whether /// it's a correct configuration file) pub fn config_path_from_env(env_var_name: &str) -> Option<PathBuf> { let path = std::env::var_os(env_var_name)?; let path = Path::new(&path); if path.exists() { Some(path.to_path_buf()) } else { // some users may want to use an env var to point to a file that may not always exist // so we don't throw an error here warn!( "Env var {:?} points to file {:?} which does not exist", env_var_name, path ); None } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/conf/settings.rs�������������������������������������������������������������������0000644�0000000�0000000�00000016436�10461020230�0014725�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::{ Result, bail, }, std::{ collections::HashMap, path::PathBuf, }, }; /// The settings used in the application. /// /// They're immutable during the execution of the missions. #[derive(Debug, Clone)] pub struct Settings { pub additional_alias_args: Option<Vec<String>>, pub additional_job_args: Vec<String>, pub all_features: bool, pub arg_job: Option<ConcreteJobRef>, /// Path of the files which were used to build the settings /// (note that not all settings come from files) pub config_files: Vec<PathBuf>, pub default_job: ConcreteJobRef, pub exports: ExportsSettings, pub features: Option<String>, // comma separated list pub help_line: bool, pub jobs: HashMap<String, Job>, pub keybindings: KeyBindings, pub no_default_features: bool, pub reverse: bool, pub summary: bool, pub wrap: bool, pub all_jobs: Job, } impl Default for Settings { fn default() -> Self { Self { arg_job: Default::default(), additional_job_args: Default::default(), additional_alias_args: Default::default(), summary: false, wrap: true, reverse: false, help_line: true, no_default_features: Default::default(), all_features: Default::default(), features: Default::default(), keybindings: Default::default(), jobs: Default::default(), default_job: Default::default(), exports: Default::default(), config_files: Default::default(), all_jobs: Default::default(), } } } impl Settings { /// Read the settings from all configuration files and arguments. /// /// /// Hardcoded defaults are overridden by the following configuration elements, in order: /// * the default `bacon.toml` file (embedded in the binary) /// * the global `prefs.toml`, from user config directory /// * the file whose path is in environment variable `BACON_PREFS` /// * the workspace.metadata.bacon config in the workspace level `Cargo.toml` file /// * the workspace level `bacon.toml` file in workspace-root/bacon.toml /// * the workspace level `bacon.toml` file in workspace-root/.config/.bacon.toml /// * the package.metadata.bacon config in the package level `Cargo.toml` file /// * the package level `bacon.toml` file in package-root/.bacon.toml /// * the package level `bacon.toml` file in package-root/.config/.bacon.toml /// * the file whose path is in environment variable `BACON_CONFIG` /// * the content of the `--config-toml` argument /// * args given as arguments, coming from the cli call pub fn read( args: &Args, context: &Context, ) -> Result<Self> { let mut settings = Settings::default(); let default_package_config = Config::default_package_config(); settings.apply_config(&default_package_config); let paths = vec![ bacon_prefs_path(), config_path_from_env("BACON_PREFS"), context.workspace_cargo_path(), context.workspace_config_path(), context.workspace_dot_config_path(), Some(context.package_cargo_path()), Some(context.package_config_path()), Some(context.package_dot_config_path()), config_path_from_env("BACON_CONFIG"), ]; for path in paths.into_iter().flatten() { if path.exists() { let configs = Config::from_path_detect(&path)?; if !configs.is_empty() { info!("config loaded from {:?}", path); settings.register_config_file(path.clone()); for config in configs { settings.apply_config(&config); } } } } if let Some(toml) = &args.config_toml { let config = toml::from_str(toml)?; info!("config loaded from --config-toml: {:#?}", &config); settings.apply_config(&config); } settings.apply_args(args); settings.check()?; info!("settings: {:#?}", &settings); Ok(settings) } pub fn register_config_file( &mut self, path: PathBuf, ) { self.config_files.push(path); } /// Apply one of the configuration elements, overriding /// defaults and previously applied configuration elements pub fn apply_config( &mut self, config: &Config, ) { self.all_jobs.apply(&config.all_jobs); if let Some(b) = config.summary { self.summary = b; } if let Some(b) = config.wrap { self.wrap = b; } if let Some(b) = config.reverse { self.reverse = b; } if let Some(b) = config.help_line { self.help_line = b; } #[allow(deprecated)] // for compatibility if config.vim_keys == Some(true) { self.keybindings.add_vim_keys(); } if let Some(keybindings) = config.keybindings.as_ref() { self.keybindings.add_all(keybindings); } if config.additional_alias_args.is_some() { self.additional_alias_args .clone_from(&config.additional_alias_args); } for (name, job) in &config.jobs { self.jobs.insert(name.clone(), job.clone()); } if let Some(default_job) = &config.default_job { self.default_job = default_job.clone(); } self.exports.apply_config(config); } pub fn apply_args( &mut self, args: &Args, ) { if let Some(job) = &args.job { self.arg_job = Some(job.clone()); } if args.no_summary { self.summary = false; } if args.summary { self.summary = true; } if args.no_wrap { self.wrap = false; } if args.wrap { self.wrap = true; } if args.no_reverse { self.reverse = false; } if args.help_line { self.help_line = true; } if args.no_help_line { self.help_line = false; } if args.export_locations { self.exports.set_locations_export_auto(true); } if args.no_export_locations { self.exports.set_locations_export_auto(false); } if args.reverse { self.reverse = true; } if args.no_default_features { self.no_default_features = true; } if args.all_features { self.all_features = true; } if args.features.is_some() { self.features.clone_from(&args.features); } self.additional_job_args .clone_from(&args.additional_job_args); } pub fn check(&self) -> Result<()> { if self.jobs.is_empty() { bail!("Invalid configuration : no job found"); } if let NameOrAlias::Name(name) = &self.default_job.name_or_alias { if !self.jobs.contains_key(name) { bail!("Invalid configuration : default job ({name:?}) not found in jobs"); } } Ok(()) } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/context.rs�������������������������������������������������������������������������0000644�0000000�0000000�00000020577�10461020230�0013625�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::{ Result, bail, }, cargo_metadata::MetadataCommand, std::{ env, fmt, path::{ Path, PathBuf, }, }, }; static DEFAULT_WATCHES: &[&str] = &[ "Cargo.toml", "src", "tests", "benches", "examples", "build.rs", ]; /// information on the paths which are relevant for a mission #[derive(Debug)] pub struct Context { pub name: String, pub nature: ContextNature, /// The current package/project pub package_directory: PathBuf, /// The root of the workspace, only defined when it makes sense /// and it's different from the package directory. /// /// Today it's only obtained from cargo metadata but in the future /// it could be obtained from other kind of sources. pub workspace_root: Option<PathBuf>, cargo_mission_location: Option<CargoContext>, /// An optional path to watch, given at launch and overriding the settings /// of the Cargo.toml file, bacon.toml file, etc. pub path_to_watch: Option<PathBuf>, } /// Specific data for a cargo related mission struct CargoContext { pub cargo_toml_file: PathBuf, pub packages: Vec<cargo_metadata::Package>, } impl fmt::Debug for CargoContext { fn fmt( &self, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { f.debug_struct("CargoContext") .field("cargo_toml_file", &self.cargo_toml_file) .finish_non_exhaustive() } } impl Context { pub fn new(args: &Args) -> Result<Self> { let package_directory = args .project .as_ref() .map_or_else(|| env::current_dir().unwrap(), PathBuf::from); if !package_directory.exists() || !package_directory.is_dir() { bail!("The project path must be a directory"); } let name = package_directory .file_name() .unwrap_or(package_directory.as_os_str()) .to_string_lossy() .to_string(); let path_to_watch = args.watch.as_ref().map(PathBuf::from); // A cargo project is one directly containing a Cargo.toml file. // When the project is a Cargo project, some additional rules apply and // the Cargo.toml file(s) is/are used to determine the paths to watch. let mut cargo_toml_file = package_directory.join("Cargo.toml"); let nature = if cargo_toml_file.exists() && cargo_toml_file.is_file() { ContextNature::Cargo } else { ContextNature::Other }; let mut workspace_root = None; let mut cargo_mission_location = None; if nature == ContextNature::Cargo { let metadata = if args.offline { MetadataCommand::new() .current_dir(&package_directory) .no_deps() .other_options(["--frozen".to_string(), "--offline".to_string()]) .exec() } else { MetadataCommand::new() .current_dir(&package_directory) .exec() }; let metadata = metadata?; if let Some(resolved_root) = metadata.resolve.and_then(|resolve| resolve.root) { cargo_toml_file = metadata .packages .iter() .find(|p| p.id == resolved_root) .map(|p| p.manifest_path.as_std_path().to_path_buf()) .expect("resolved manifest was not in package list"); if metadata.workspace_root.as_std_path() != package_directory { workspace_root = Some(metadata.workspace_root.as_std_path().to_path_buf()); } } cargo_mission_location = Some(CargoContext { cargo_toml_file, packages: metadata.packages, }); } Ok(Self { name, nature, package_directory, workspace_root, cargo_mission_location, path_to_watch, }) } pub fn mission<'s>( &self, concrete_job_ref: ConcreteJobRef, leaf_job: &Job, // the raw job as defined, without using root settings settings: &'s Settings, ) -> Result<Mission<'s>> { // the real job used in the mission is built from settings.all_jobs // on which the provided leaf job is applied let mut job = settings.all_jobs.clone(); job.apply(leaf_job); let location_name = self.name.clone(); let mut paths_to_watch: Vec<PathBuf> = Vec::new(); if let Some(path_to_watch) = &self.path_to_watch { paths_to_watch.push(path_to_watch.clone()); } else { // Automatically watch all kinds of source files. // "watches", at this point, aren't full path, they still must be joined // with the right path which may depend on the let mut watches = Vec::new(); if let Some(v) = &job.watch { for watch in v.iter() { watches.push(watch.as_str()); } } let add_default = job.default_watch.unwrap_or(true); if add_default { for watch in DEFAULT_WATCHES { if !watches.contains(watch) { watches.push(watch); } } } debug!("watches: {watches:?}"); add_to_paths_to_watch(&watches, &self.package_directory, &mut paths_to_watch); if let Some(workspace_root) = &self.workspace_root { // there's usually not much src at the workspace level but we must // at least watch the Cargo.toml file add_to_paths_to_watch(&watches, workspace_root, &mut paths_to_watch); } if let Some(location) = &self.cargo_mission_location { for item in &location.packages { // if it's a local package if item.source.is_none() { let item_path = item .manifest_path .parent() .expect("parent of a target folder is a root folder"); add_to_paths_to_watch( &watches, item_path.as_std_path(), &mut paths_to_watch, ); if item.manifest_path.exists() { paths_to_watch.push(item.manifest_path.clone().into()); } else { warn!("missing manifest file: {:?}", item.manifest_path); } } } } } let execution_directory = self.package_directory.clone(); Ok(Mission { location_name, concrete_job_ref, execution_directory, package_directory: self.package_directory.clone(), workspace_directory: self.workspace_root.clone(), job, paths_to_watch, settings, }) } pub fn workspace_cargo_path(&self) -> Option<PathBuf> { self.workspace_root.as_ref().map(|p| p.join("Cargo.toml")) } /// return the location of the workspace level bacon.toml file /// (if it's different from the package level bacon.toml file) pub fn workspace_config_path(&self) -> Option<PathBuf> { self.workspace_root.as_ref().map(|p| p.join("bacon.toml")) } pub fn workspace_dot_config_path(&self) -> Option<PathBuf> { self.workspace_root .as_ref() .map(|p| p.join(".config/bacon.toml")) } pub fn package_cargo_path(&self) -> PathBuf { self.package_directory.join("Cargo.toml") } pub fn package_config_path(&self) -> PathBuf { self.package_directory.join("bacon.toml") } pub fn package_dot_config_path(&self) -> PathBuf { self.package_directory.join(".config/bacon.toml") } } fn add_to_paths_to_watch( watches: &[&str], base_path: &Path, paths_to_watch: &mut Vec<PathBuf>, ) { for watch in watches { let full_path = base_path.join(watch); if !paths_to_watch.contains(&full_path) && full_path.exists() { paths_to_watch.push(full_path); } } } ���������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/context_nature.rs������������������������������������������������������������������0000644�0000000�0000000�00000000271�10461020230�0015170�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/// The kind of projec/context, as it impacts computing features, /// files to watch, etc. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ContextNature { Cargo, Other, } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/exec/command_builder.rs������������������������������������������������������������0000644�0000000�0000000�00000005051�10461020230�0016177�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::{ collections::HashMap, ffi::{ OsStr, OsString, }, path::{ Path, PathBuf, }, process::{ Command, Stdio, }, }; #[derive(Debug, Clone)] pub struct CommandBuilder { exe: String, current_dir: Option<PathBuf>, args: Vec<OsString>, with_stdout: bool, envs: HashMap<OsString, OsString>, } impl CommandBuilder { pub fn new(exe: &str) -> Self { Self { exe: exe.to_string(), current_dir: None, args: Vec::new(), with_stdout: false, envs: Default::default(), } } pub fn build(&self) -> Command { let mut command = Command::new(&self.exe); if let Some(dir) = &self.current_dir { command.current_dir(dir); } command.args(&self.args); command.envs(&self.envs); command .envs(&self.envs) .stdin(Stdio::null()) .stderr(Stdio::piped()) .stdout(if self.with_stdout { Stdio::piped() } else { Stdio::null() }); command } pub fn with_stdout( &mut self, b: bool, ) -> &mut Self { self.with_stdout = b; self } pub fn is_with_stdout(&self) -> bool { self.with_stdout } pub fn current_dir<P: AsRef<Path>>( &mut self, dir: P, ) -> &mut Self { self.current_dir = Some(dir.as_ref().to_path_buf()); self } pub fn arg<S: AsRef<OsStr>>( &mut self, arg: S, ) -> &mut Self { self.args.push(arg.as_ref().to_os_string()); self } pub fn args<I, S>( &mut self, args: I, ) -> &mut Self where I: IntoIterator<Item = S>, S: AsRef<OsStr>, { for arg in args { self.args.push(arg.as_ref().to_os_string()); } self } pub fn env<K, V>( &mut self, key: K, val: V, ) -> &mut Self where K: AsRef<OsStr>, V: AsRef<OsStr>, { self.envs .insert(key.as_ref().to_os_string(), val.as_ref().to_os_string()); self } pub fn envs<I, K, V>( &mut self, vars: I, ) -> &mut Self where I: IntoIterator<Item = (K, V)>, K: AsRef<OsStr>, V: AsRef<OsStr>, { for (k, v) in vars { self.envs .insert(k.as_ref().to_os_string(), v.as_ref().to_os_string()); } self } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/exec/executor.rs�������������������������������������������������������������������0000644�0000000�0000000�00000022442�10461020230�0014714�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, std::{ io::{ self, BufRead, BufReader, }, process::{ Child, Command, }, thread, time::Instant, }, termimad::crossbeam::channel::{ self, Receiver, Sender, }, }; /// an executor calling a cargo (or similar) command in a separate /// thread when asked to and sending the lines of output in a channel, /// and finishing by None. /// Channel sizes are designed to avoid useless computations. pub struct MissionExecutor { command_builder: CommandBuilder, kill_command: Option<Vec<String>>, line_sender: Sender<CommandExecInfo>, pub line_receiver: Receiver<CommandExecInfo>, } /// Dedicated to one execution of the job (so there's usually /// several task executors during the lifetime of a mission executor) pub struct TaskExecutor { /// the thread running the current task child_thread: thread::JoinHandle<()>, stop_sender: Sender<StopMessage>, grace_period_start: Option<Instant>, // forgotten at end of grace period grace_period: Period, } /// A message sent to the child_thread on end #[derive(Clone, Copy)] enum StopMessage { SendStatus, // process already finished, just get status Kill, // kill the process, don't bother about the status } impl TaskExecutor { /// Interrupt the process pub fn interrupt(self) { let _ = self.stop_sender.send(StopMessage::Kill); } /// Kill the process, and wait until it finished pub fn die(self) { if let Err(e) = self.stop_sender.send(StopMessage::Kill) { debug!("failed to send 'die' signal: {e}"); } if self.child_thread.join().is_err() { warn!("child_thread.join() failed"); // should not happen } } pub fn is_in_grace_period(&mut self) -> bool { if let Some(grace_period_start) = self.grace_period_start { if grace_period_start.elapsed() < self.grace_period.duration { return true; } self.grace_period_start = None; } false } } impl MissionExecutor { /// Prepare the executor (no task/process/thread is started at this point) pub fn new(mission: &Mission) -> anyhow::Result<Self> { let command_builder = mission.get_command()?; let kill_command = mission.kill_command(); let (line_sender, line_receiver) = channel::unbounded(); Ok(Self { command_builder, kill_command, line_sender, line_receiver, }) } /// Start the job's command, once, with the given settings pub fn start( &mut self, task: Task, ) -> anyhow::Result<TaskExecutor> { info!("start task {task:?}"); let grace_period = task.grace_period; let grace_period_start = if grace_period.is_zero() { None } else { Some(Instant::now()) }; let mut command_builder = self.command_builder.clone(); command_builder.env("RUST_BACKTRACE", task.backtrace.unwrap_or("0")); let kill_command = self.kill_command.clone(); let with_stdout = command_builder.is_with_stdout(); let line_sender = self.line_sender.clone(); let (stop_sender, stop_receiver) = channel::bounded(1); let err_stop_sender = stop_sender.clone(); // Global task executor thread let child_thread = thread::spawn(move || { // before starting the command, we wait some time, so that a bunch // of quasi-simultaneous file events can be finished before the command // starts (during this time, no other command is started by bacon in app.rs) if !grace_period.is_zero() { thread::sleep(grace_period.duration); } let mut cmd = command_builder.build(); let mut child = match cmd.spawn() { Ok(child) => child, Err(e) => { let _ = line_sender.send(CommandExecInfo::Error( anyhow::anyhow!(e).context(format!("failed to spawn {cmd:?}")), )); return; } }; // thread piping the stdout lines if with_stdout { let sender = line_sender.clone(); let Some(stdout) = child.stdout.take() else { warn!("process has no stdout"); // unlikely return; }; let mut buf_reader = BufReader::new(stdout); thread::spawn(move || { let mut line = String::new(); loop { match buf_reader.read_line(&mut line) { Err(e) => { warn!("error : {e}"); } Ok(0) => { // there won't be anything more, quitting break; } Ok(_) => { let response = CommandExecInfo::Line(RawCommandOutputLine { content: line.clone(), origin: CommandStream::StdOut, }); if sender.send(response).is_err() { break; // channel closed } } } line.clear(); } }); } // starting a thread to handle stderr lines until program // ends (then ask the child_thread to send status) let err_line_sender = line_sender.clone(); let stderr = child.stderr.take().expect("child missing stderr"); let mut buf_reader = BufReader::new(stderr); thread::spawn(move || { let mut line = String::new(); loop { match buf_reader.read_line(&mut line) { Err(e) => { warn!("error : {e}"); } Ok(0) => { if let Err(e) = err_stop_sender.send(StopMessage::SendStatus) { warn!("sending stop message failed: {e}"); } break; } Ok(_) => { let response = CommandExecInfo::Line(RawCommandOutputLine { content: line.clone(), origin: CommandStream::StdErr, }); if err_line_sender.send(response).is_err() { break; // channel closed } } } line.clear(); } }); // now waiting for the stop event match stop_receiver.recv() { Ok(stop) => match stop { StopMessage::SendStatus => { let status = child.try_wait(); if let Ok(status) = status { let _ = line_sender.send(CommandExecInfo::End { status }); } } StopMessage::Kill => { debug!("explicit interrupt received"); kill(kill_command.as_deref(), &mut child); } }, Err(e) => { debug!("recv error: {e}"); // probably just the executor dropped kill(kill_command.as_deref(), &mut child); } } if let Err(e) = child.wait() { warn!("waiting for child failed: {e}"); } }); Ok(TaskExecutor { child_thread, stop_sender, grace_period_start, grace_period, }) } } /// kill the child process, either by using a specific command or by /// using the default platform kill method if the specific command /// failed or wasn't provided. fn kill( kill_command: Option<&[String]>, child: &mut Child, ) { if let Some(kill_command) = kill_command { info!("launch specific kill command {kill_command:?}"); let Err(e) = run_kill_command(kill_command, child) else { return; }; warn!("specific kill command failed: {e}"); } child.kill().expect("command couldn't be killed") } fn run_kill_command( kill_command: &[String], child: &mut Child, ) -> io::Result<()> { let (exe, args) = kill_command .split_first() .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "empty kill command"))?; let mut kill = Command::new(exe); kill.args(args); kill.arg(child.id().to_string()); let mut proc = kill.spawn()?; let status = proc.wait()?; if !status.success() { return Err(io::Error::new( io::ErrorKind::Other, format!("kill command returned nonzero status: {status}"), )); } child.wait()?; Ok(()) } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/exec/mod.rs������������������������������������������������������������������������0000644�0000000�0000000�00000000317�10461020230�0013632�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mod command_builder; mod executor; mod on_change_strategy; mod period; mod task; pub use { command_builder::CommandBuilder, executor::*, on_change_strategy::*, period::*, task::Task, }; �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/exec/on_change_strategy.rs���������������������������������������������������������0000644�0000000�0000000�00000000273�10461020230�0016717�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use serde::Deserialize; #[derive(Debug, Clone, Copy, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum OnChangeStrategy { KillThenRestart, WaitThenRestart, } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/exec/period.rs���������������������������������������������������������������������0000644�0000000�0000000�00000003036�10461020230�0014336�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { anyhow::anyhow, lazy_regex::*, serde::{ Deserialize, Deserializer, de, }, std::{ str::FromStr, time::Duration, }, }; /// A small wrapper over time::Duration, to allow reading from a string in /// config. There's no symetric serialization and the input format is /// quite crude (eg "25ms" or "254ns" or "none") #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Period { pub duration: Duration, } impl Period { pub const fn is_zero(&self) -> bool { self.duration.is_zero() } pub fn sleep(&self) { std::thread::sleep(self.duration); } } impl From<Duration> for Period { fn from(duration: Duration) -> Self { Self { duration } } } impl FromStr for Period { type Err = anyhow::Error; fn from_str(s: &str) -> Result<Self, Self::Err> { let duration = regex_switch!(s, r"^(?<n>\d+)\s*ns$" => Duration::from_nanos(n.parse()?), r"^(?<n>\d+)\s*ms$" => Duration::from_millis(n.parse()?), r"^(?<n>\d+)\s*s$" => Duration::from_secs(n.parse()?), r"^[^1-9]*$" => Duration::new(0, 0), // eg "none", "0", "off" ) .ok_or_else(|| anyhow!("Invalid period: {}", s))?; Ok(Self { duration }) } } impl<'de> Deserialize<'de> for Period { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; FromStr::from_str(&s).map_err(de::Error::custom) } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/exec/task.rs�����������������������������������������������������������������������0000644�0000000�0000000�00000000335�10461020230�0014015�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use crate::Period; /// Settings for one execution of a job's command #[derive(Debug, Clone, Copy, PartialEq)] pub struct Task { pub backtrace: Option<&'static str>, // ("1" or "full") pub grace_period: Period, } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/export/export_config.rs������������������������������������������������������������0000644�0000000�0000000�00000000561�10461020230�0016317�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, serde::Deserialize, std::path::PathBuf, }; /// A generic configuration for all exports, whatever the exporter. #[derive(Debug, Clone, Deserialize)] pub struct ExportConfig { pub exporter: Option<Exporter>, #[serde(alias = "enabled")] pub auto: Option<bool>, pub path: Option<PathBuf>, pub line_format: Option<String>, } �����������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/export/export_settings.rs����������������������������������������������������������0000644�0000000�0000000�00000003061�10461020230�0016710�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, std::{ fs::File, path::PathBuf, }, }; /// Settings for one export #[derive(Debug, Clone)] pub struct ExportSettings { pub exporter: Exporter, pub auto: bool, pub path: PathBuf, pub line_format: String, } impl ExportSettings { pub fn do_export( &self, name: &str, state: &AppState<'_>, ) -> anyhow::Result<()> { let path = if self.path.is_relative() { state.mission.package_directory.join(&self.path) } else { self.path.to_path_buf() }; info!("exporting to {:?}", path); let Some(report) = state.cmd_result.report() else { info!("No report to export"); return Ok(()); }; match self.exporter { Exporter::Analyser => { if let Some(export) = report.analyzer_exports.get(name) { std::fs::write(&path, export)?; } else { info!("Analyzer didn't build export {:?}", name); } } Exporter::Analysis => { error!("Aanlysis export not currently implemented"); } Exporter::JsonReport => { let json = serde_json::to_string_pretty(&report)?; std::fs::write(&path, json)?; } Exporter::Locations => { let mut file = File::create(path)?; report.write_locations(&mut file, &state.mission, &self.line_format)?; } } Ok(()) } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/export/exporter.rs�����������������������������������������������������������������0000644�0000000�0000000�00000000627�10461020230�0015324�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use serde::Deserialize; #[derive(Debug, Clone, Copy, PartialEq, Deserialize)] #[serde(rename_all = "snake_case")] pub enum Exporter { /// The analyzer is tasked with doing an export while analyzing the /// command output #[serde(alias = "analyzer")] Analyser, /// This exporter doesn't exist at the moment #[serde(alias = "analyzis")] Analysis, JsonReport, Locations, } ���������������������������������������������������������������������������������������������������������bacon-3.12.0/src/export/exports_settings.rs���������������������������������������������������������0000644�0000000�0000000�00000016231�10461020230�0017076�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, std::{ collections::HashMap, path::PathBuf, }, }; /// Settings for all exports #[derive(Debug, Clone, Default)] pub struct ExportsSettings { pub exports: HashMap<String, ExportSettings>, } impl ExportsSettings { pub fn set_locations_export_auto( &mut self, enabled: bool, ) { let locations_export = self .exports .entry("locations".to_string()) .or_insert_with(default_locations_export_settings); locations_export.auto = enabled; } pub fn do_auto_exports( &self, state: &AppState<'_>, ) { for (name, export) in &self.exports { if export.auto { info!("doing auto export {:?}", name); if let Err(e) = export.do_export(name, state) { error!("error while exporting {:?}: {:?}", name, e); } } } } pub fn do_named_export( &self, requested_name: &str, state: &AppState<'_>, ) { if let Some(export) = self.exports.get(requested_name) { if let Err(e) = export.do_export(requested_name, state) { error!("error while exporting {:?}: {:?}", requested_name, e); } } else { warn!("no export named {:?}", requested_name); } } /// We apply different parts of the config, matching /// different generations of the config format: /// /// - the exports map (current) /// - the export object (recently deprecated since 2.22.0) /// - the export_locations field (deprecated since 2.9.0) /// /// FIXME Should we prevent having two exporters with the /// same path ? pub fn apply_config( &mut self, config: &Config, ) { // normal [exports] map for (name, ec) in &config.exports { if let Some(e) = self.exports.get_mut(name) { if let Some(exporter) = ec.exporter { e.exporter = exporter; } if let Some(b) = ec.auto { e.auto = b; } if let Some(p) = &ec.path { e.path = p.clone(); } if let Some(lf) = &ec.line_format { e.line_format = lf.clone(); } continue; } let exporter = match ec.exporter { Some(e) => e, None => match name.as_str() { "analysis" => Exporter::Analysis, "json-report" => Exporter::JsonReport, "locations" => Exporter::Locations, _ => { warn!( "Exporter not specified for export {:?}, using 'locations'", name ); Exporter::Locations } }, }; let auto = ec.auto.unwrap_or(true); let path = ec.path.clone().unwrap_or_else(|| match exporter { Exporter::Analyser => default_analyser_path(), Exporter::Analysis => default_analysis_path(), Exporter::Locations => default_locations_path(), Exporter::JsonReport => default_json_report_path(), }); let line_format = ec.line_format.clone().unwrap_or_else(|| match exporter { Exporter::Locations => default_locations_line_format().to_string(), _ => "".to_string(), }); self.exports.insert( name.clone(), ExportSettings { exporter, auto, path, line_format, }, ); } // [export] object #[allow(deprecated)] // for compatibility if let Some(ec) = &config.export { match ec.exporter { Some(Exporter::Analysis) => { let analysis_export = self .exports .entry("analysis".to_string()) .or_insert_with(default_analysis_export_settings); if let Some(b) = ec.auto { analysis_export.auto = b; } if let Some(p) = &ec.path { analysis_export.path = p.clone(); } } Some(Exporter::JsonReport) => { let json_report_export = self .exports .entry("json-report".to_string()) .or_insert_with(default_json_report_export_settings); if let Some(b) = ec.auto { json_report_export.auto = b; } if let Some(p) = &ec.path { json_report_export.path = p.clone(); } } _ => { let locations_export = self .exports .entry("locations".to_string()) .or_insert_with(default_locations_export_settings); if let Some(b) = ec.auto { locations_export.auto = b; } if let Some(p) = &ec.path { locations_export.path = p.clone(); } if let Some(lf) = &ec.line_format { locations_export.line_format = lf.clone(); } } } } #[allow(deprecated)] // for compatibility if let Some(b) = config.export_locations { let locations_export = self .exports .entry("locations".to_string()) .or_insert_with(default_locations_export_settings); locations_export.auto = b; } } } fn default_analysis_export_settings() -> ExportSettings { ExportSettings { exporter: Exporter::Analysis, auto: true, path: default_analysis_path(), line_format: "".to_string(), // not used } } fn default_json_report_export_settings() -> ExportSettings { ExportSettings { exporter: Exporter::JsonReport, auto: true, path: default_json_report_path(), line_format: "".to_string(), // not used } } fn default_locations_export_settings() -> ExportSettings { ExportSettings { exporter: Exporter::Locations, auto: true, path: default_locations_path(), line_format: default_locations_line_format().to_string(), } } pub fn default_locations_line_format() -> &'static str { "{kind} {path}:{line}:{column} {message}" } pub fn default_analyser_path() -> PathBuf { PathBuf::from("bacon-analyser.json") } pub fn default_analysis_path() -> PathBuf { PathBuf::from("bacon-analysis.json") } pub fn default_json_report_path() -> PathBuf { PathBuf::from("bacon-report.json") } pub fn default_locations_path() -> PathBuf { PathBuf::from(".bacon-locations") } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/export/mod.rs����������������������������������������������������������������������0000644�0000000�0000000�00000000262�10461020230�0014226�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mod export_config; mod export_settings; mod exporter; mod exports_settings; pub use { export_config::*, export_settings::*, exporter::*, exports_settings::*, }; ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/help/examples.rs�������������������������������������������������������������������0000644�0000000�0000000�00000001503�10461020230�0014673�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/// A bacon launch example to display in the --help message pub struct Example { pub title: &'static str, pub cmd: &'static str, } pub static EXAMPLES_TEMPLATE: &str = " **Examples:** ${examples *${example-number})* ${example-title}: `${example-cmd}` } "; /// Examples to display in the --help message pub static EXAMPLES: &[Example] = &[ Example { title: "Start with the default job", cmd: "bacon", }, Example { title: "Start with a specific job", cmd: "bacon clippy", }, Example { title: "Start with features", cmd: "bacon --features clipboard", }, Example { title: "Start a specific job on another path", cmd: "bacon ../broot test", }, Example { title: "Start in summary mode", cmd: "bacon -s", }, ]; ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/help/help_line.rs������������������������������������������������������������������0000644�0000000�0000000�00000013013�10461020230�0015013�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use crate::*; pub struct HelpLine { quit: String, toggle_summary: Option<String>, wrap: Option<String>, not_wrap: Option<String>, toggle_backtrace: Option<String>, help: Option<String>, close_help: Option<String>, pause: Option<String>, unpause: Option<String>, scope: Option<String>, search: Option<String>, next_match: Option<String>, previous_match: Option<String>, clear_search: Option<String>, validate_search: Option<String>, } impl HelpLine { pub fn new(settings: &Settings) -> Self { let kb = &settings.keybindings; let quit = kb .shortest_internal_key(Internal::Quit) .map(|k| format!("*{k}* to quit")) .expect("the app to be quittable"); let toggle_summary = kb .shortest_internal_key(Internal::ToggleSummary) .map(|k| format!("*{k}* to toggle summary mode")); let wrap = kb .shortest_internal_key(Internal::ToggleWrap) .map(|k| format!("*{k}* to wrap lines")); let not_wrap = kb .shortest_internal_key(Internal::ToggleWrap) .map(|k| format!("*{k}* to not wrap lines")); let toggle_backtrace = kb .shortest_action_key(|action| { matches!(action, Action::Internal(Internal::ToggleBacktrace(_))) }) .map(|k| format!("*{k}* to toggle backtraces")); let help = kb .shortest_internal_key(Internal::Help) .map(|k| format!("*{k}* for help")); let close_help = kb .shortest_internal_key(Internal::Back) .or_else(|| kb.shortest_internal_key(Internal::Help)) .map(|k| format!("*{k}* to close this help")); let pause = kb .shortest_internal_key(Internal::Pause) .or(kb.shortest_internal_key(Internal::TogglePause)) .map(|k| format!("*{k}* to pause")); let unpause = kb .shortest_internal_key(Internal::Unpause) .or(kb.shortest_internal_key(Internal::TogglePause)) .map(|k| format!("*{k}* to unpause")); let scope = kb .shortest_internal_key(Internal::ScopeToFailures) .map(|k| format!("*{k}* to scope to failures")); let search = kb .shortest_internal_key(Internal::FocusSearch) .map(|k| format!("*{k}* to search")); let next_match = kb .shortest_internal_key(Internal::NextMatch) .map(|k| format!("*{k}* for next match")); let previous_match = kb .shortest_internal_key(Internal::PreviousMatch) .map(|k| format!("*{k}* for previous match")); let clear_search = kb .shortest_internal_key(Internal::Back) .map(|k| format!("*{k}* to clear")); let validate_search = kb .shortest_internal_key(Internal::Validate) .map(|k| format!("*{k}* to validate")); Self { quit, toggle_summary, wrap, not_wrap, toggle_backtrace, help, close_help, pause, unpause, scope, search, next_match, previous_match, clear_search, validate_search, } } fn applicable_parts( &self, state: &AppState, ) -> Vec<&str> { let mut parts: Vec<&str> = Vec::new(); if state.is_help() { parts.push(&self.quit); if let Some(s) = &self.close_help { parts.push(s); } return parts; } if state.has_search() { if let Some(s) = &self.next_match { parts.push(s); } if let Some(s) = &self.previous_match { parts.push(s); } if let Some(s) = &self.clear_search { parts.push(s); } if state.search.focused() { if let Some(s) = &self.validate_search { parts.push(s); } } return parts; } if state.can_be_scoped() { if let Some(s) = &self.scope { parts.push(s); } } if state.auto_refresh.is_paused() { if let Some(s) = &self.unpause { parts.push(s); } } if state.cmd_result.suggest_backtrace() { if let Some(s) = &self.toggle_backtrace { parts.push(s); } } if let Some(s) = &self.search { parts.push(s); } if let CommandResult::Report(report) = &state.cmd_result { if !state.mission.is_success(report) { if let Some(s) = &self.toggle_summary { parts.push(s); } } } if let Some(s) = &self.help { parts.push(s); } if state.wrap { if let Some(s) = &self.not_wrap { parts.push(s); } } else { if let Some(s) = &self.wrap { parts.push(s); } } if state.auto_refresh.is_enabled() { if let Some(s) = &self.pause { parts.push(s); } } parts.push(&self.quit); parts } pub fn markdown( &self, state: &AppState, ) -> String { let parts = self.applicable_parts(state); format!("Hit {}", parts.join(", ")) } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/help/help_page.rs������������������������������������������������������������������0000644�0000000�0000000�00000007372�10461020230�0015013�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::Result, termimad::{ Area, CompoundStyle, FmtText, MadSkin, TextView, crossterm::style::{ Attribute, Color::*, }, minimad::{ Alignment, OwningTemplateExpander, TextTemplate, }, }, }; static TEMPLATE: &str = r#" # bacon ${version} **bac*o*n** is a background compiler, watching your sources and executing your cargo jobs on change. See *https://dystroy.org/bacon* for a complete guide. |:-:|:-: |**action**|**shortcut** |:-|:-: ${keybindings |${action}|${keys} } |-: Key bindings, jobs, and other preferences were loaded from these files: * internal default configuration files ${config_files * ${config_file_path} } "#; pub struct HelpPage { area: Area, skin: MadSkin, expander: OwningTemplateExpander<'static>, template: TextTemplate<'static>, scroll: usize, } impl HelpPage { pub fn new(settings: &Settings) -> Self { let mut skin = MadSkin::default(); skin.paragraph.align = Alignment::Center; skin.italic = CompoundStyle::new(Some(AnsiValue(204)), None, Attribute::Bold.into()); skin.table.align = Alignment::Center; skin.bullet.set_fg(AnsiValue(204)); let mut expander = OwningTemplateExpander::new(); expander.set("version", env!("CARGO_PKG_VERSION")); let mut bindings: Vec<(String, String)> = settings .keybindings .build_reverse_map() .into_iter() .map(|(action, cks)| { let cks: Vec<String> = cks.iter().map(|ck| format!("*{ck}*")).collect(); let cks = cks.join(" or "); (action.doc(), cks) }) .collect(); bindings.sort_by(|a, b| a.0.cmp(&b.0)); for (action, key) in bindings.drain(..) { expander .sub("keybindings") .set_md("keys", key) .set_md("action", action); } for config_file in &settings.config_files { expander .sub("config_files") .set_md("config_file_path", config_file.to_string_lossy()); } let template = TextTemplate::from(TEMPLATE); Self { area: Area::default(), skin, expander, template, scroll: 0, } } /// draw the state on the whole terminal pub fn draw( &mut self, w: &mut W, area: Area, ) -> Result<()> { self.area = area; let text = self.expander.expand(&self.template); let fmt_text = FmtText::from_text(&self.skin, text, Some((self.area.width - 1) as usize)); let mut text_view = TextView::from(&self.area, &fmt_text); self.scroll = text_view.set_scroll(self.scroll); Ok(text_view.write_on(w)?) } pub fn apply_scroll_command( &mut self, cmd: ScrollCommand, ) { let text = self.expander.expand(&self.template); let fmt_text = FmtText::from_text(&self.skin, text, Some((self.area.width - 1) as usize)); let mut text_view = TextView::from(&self.area, &fmt_text); text_view.set_scroll(self.scroll); match cmd { ScrollCommand::Top => { text_view.scroll = 0; } ScrollCommand::Bottom => { text_view.set_scroll(text_view.content_height()); } ScrollCommand::Lines(lines) => { text_view.try_scroll_lines(lines); } ScrollCommand::MilliPages(milli_pages) => { text_view.try_scroll_pages(milli_pages as f64 / 1000f64); } } self.scroll = text_view.scroll; } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/help/list_jobs.rs������������������������������������������������������������������0000644�0000000�0000000�00000001547�10461020230�0015055�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, termimad::{ MadSkin, minimad::{ OwningTemplateExpander, TextTemplate, }, }, }; pub fn print_jobs(settings: &Settings) { static MD: &str = r#" |:-:|:-| |**job**|**command**| |:-:|:-| ${jobs |${job_name}|${job_command}| } |-|-| default job: ${default_job} "#; let mut expander = OwningTemplateExpander::new(); let mut jobs: Vec<_> = settings.jobs.iter().collect(); jobs.sort_by_key(|(name, _)| name.to_string()); for (name, job) in &jobs { expander .sub("jobs") .set("job_name", name) .set("job_command", job.command.join(" ")); } expander.set("default_job", &settings.default_job); let skin = MadSkin::default(); skin.print_owning_expander(&expander, &TextTemplate::from(MD)); } ���������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/help/mod.rs������������������������������������������������������������������������0000644�0000000�0000000�00000000220�10461020230�0013627�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mod examples; mod help_line; mod help_page; mod list_jobs; pub use { examples::*, help_line::*, help_page::*, list_jobs::*, }; ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/ignorer/git_ignorer.rs�������������������������������������������������������������0000644�0000000�0000000�00000004567�10461020230�0016117�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::{ Context, Result, }, gix::{ self as git, Repository, }, std::path::Path, }; /// An object able to tell whether a file is excluded /// by gitignore rules pub struct GitIgnorer { repo: Repository, } impl GitIgnorer { /// Create an Ignorer from any directory path: the closest /// surrounding git repository will be found (if there's one) /// and its gitignore rules used. /// /// root_path is assumed to exist and be a directory pub(crate) fn new(root_path: &Path) -> Result<Self> { let repo = git::discover(root_path)?; Ok(Self { repo }) } } impl Ignorer for GitIgnorer { fn excludes( &mut self, paths: &Path, ) -> Result<bool> { self.excludes_all_paths(&[paths]) } } impl GitIgnorer { fn excludes_all_paths( &mut self, paths: &[&Path], ) -> Result<bool> { let worktree = self.repo.worktree().context("a worktree should exist")?; // The "Cache" is the structure allowing checking exclusion. // Building it is the most expensive operation, and we could store it // in the Ignorer instead of the repo (by having the repo in the mission), // but it's still about just 1ms and I'm not sure we know if it always // stays valid. let mut cache = time!(Debug, worktree.excludes(None)?); for path in paths { // cache.at_path panics if not provided a path relative // to the work directory, so we compute the relative path let Some(work_dir) = self.repo.work_dir() else { return Ok(false); }; let Ok(relative_path) = path.strip_prefix(work_dir) else { return Ok(false); }; // cache.at_path panics if the relative path is empty, so // we must check that if relative_path.as_os_str().is_empty() { return Ok(true); }; if path.is_dir() { // we're not interested in directories (we should not receive them anyway) return Ok(false); } let platform = cache.at_path(relative_path, Some(gix::index::entry::Mode::FILE))?; if !platform.is_excluded() { return Ok(false); } } Ok(true) } } �����������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/ignorer/glob_ignorer.rs������������������������������������������������������������0000644�0000000�0000000�00000002103�10461020230�0016237�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::Result, std::path::Path, }; #[derive(Default)] pub struct GlobIgnorer { globs: Vec<glob::Pattern>, } impl GlobIgnorer { pub fn add( &mut self, pattern: &str, root: &Path, ) -> Result<()> { if pattern.starts_with('/') { self.globs.push(glob::Pattern::new(pattern)?); // it's probably a path relative to the root of the package let pattern = root.join(pattern); let pattern = pattern.to_string_lossy(); self.globs.push(glob::Pattern::new(&pattern)?); } else { // as glob doesn't work with non absolute paths, we make it absolute self.globs .push(glob::Pattern::new(&format!("/**/{}", pattern))?); } Ok(()) } } impl Ignorer for GlobIgnorer { fn excludes( &mut self, paths: &Path, ) -> Result<bool> { for glob in &self.globs { if glob.matches_path(paths) { return Ok(true); } } Ok(false) } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/ignorer/mod.rs���������������������������������������������������������������������0000644�0000000�0000000�00000002504�10461020230�0014353�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { anyhow::Result, std::path::{ Path, PathBuf, }, }; mod git_ignorer; mod glob_ignorer; pub use { git_ignorer::GitIgnorer, glob_ignorer::GlobIgnorer, }; pub trait Ignorer { /// Tell whether all given paths are excluded according to /// either the global gitignore rules or the ones of the repository. /// /// Return Ok(false) when at least one file is included (i.e. we should /// execute the job) fn excludes( &mut self, paths: &Path, ) -> Result<bool>; } /// A set of ignorers #[derive(Default)] pub struct IgnorerSet { ignorers: Vec<Box<dyn Ignorer + Send>>, } impl IgnorerSet { pub fn add( &mut self, ignorer: Box<dyn Ignorer + Send>, ) { self.ignorers.push(ignorer); } pub fn excludes_all_pathbufs( &mut self, paths: &[PathBuf], ) -> Result<bool> { if self.ignorers.is_empty() { return Ok(false); } for path in paths { let mut excluded = false; for ignorer in &mut self.ignorers { if ignorer.excludes(path)? { excluded = true; break; } } if !excluded { return Ok(false); } } Ok(true) } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/internal.rs������������������������������������������������������������������������0000644�0000000�0000000�00000023505�10461020230�0013747�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::{ PlaySoundCommand, ScrollCommand, Volume, }, lazy_regex::*, serde::{ Deserialize, Deserializer, Serialize, Serializer, de, }, std::{ fmt, str::FromStr, }, }; /// one of the hardcoded actions that can be mapped /// to a key or ran after a successful job #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Internal { Back, // leave help, clear search, go to previous job, leave, etc. BackOrQuit, // same as Back but quits if there is nothing to go back to CopyUnstyledOutput, FocusSearch, FocusGoto, Help, NextMatch, NoOp, // no operation, can be used to clear a binding Pause, PlaySound(PlaySoundCommand), PreviousMatch, Quit, ReRun, Refresh, // clear and rerun ReloadConfig, ScopeToFailures, Scroll(ScrollCommand), ToggleBacktrace(&'static str), TogglePause, // either pause or unpause ToggleRawOutput, ToggleSummary, ToggleWrap, Unpause, Validate, // validate search entry } impl Internal { /// Return the action description to show in doc/help pub fn doc(&self) -> String { match self { Self::Back => "back to previous page or job".to_string(), Self::BackOrQuit => { "back to previous page or job, quitting if there is none".to_string() } Self::CopyUnstyledOutput => "copy current job's output".to_string(), Self::FocusSearch => "focus search".to_string(), Self::FocusGoto => "focus goto".to_string(), Self::Help => "help".to_string(), Self::NextMatch => "next match".to_string(), Self::NoOp => "no operation".to_string(), Self::Pause => "pause".to_string(), Self::PlaySound(_) => "play sound".to_string(), Self::PreviousMatch => "previous match".to_string(), Self::Quit => "quit".to_string(), Self::ReRun => "run current job again".to_string(), Self::Refresh => "clear then run current job again".to_string(), Self::ReloadConfig => "reload configuration files".to_string(), Self::ScopeToFailures => "scope to failures".to_string(), Self::Scroll(scroll_command) => scroll_command.doc(), Self::ToggleBacktrace(level) => format!("toggle backtrace ({level})"), Self::TogglePause => "toggle pause".to_string(), Self::ToggleRawOutput => "toggle raw output".to_string(), Self::ToggleSummary => "toggle summary".to_string(), Self::ToggleWrap => "toggle wrap".to_string(), Self::Unpause => "unpause".to_string(), Self::Validate => "validate".to_string(), } } } impl fmt::Display for Internal { fn fmt( &self, f: &mut fmt::Formatter, ) -> fmt::Result { match self { Self::Back => write!(f, "back"), Self::BackOrQuit => write!(f, "back-or-quit"), Self::CopyUnstyledOutput => write!(f, "copy-unstyled-output"), Self::Help => write!(f, "help"), Self::NoOp => write!(f, "no-op"), Self::Pause => write!(f, "pause"), Self::Quit => write!(f, "quit"), Self::ReRun => write!(f, "rerun"), Self::Refresh => write!(f, "refresh"), Self::ReloadConfig => write!(f, "reload-config"), Self::ScopeToFailures => write!(f, "scope-to-failures"), Self::Scroll(scroll_command) => scroll_command.fmt(f), Self::ToggleBacktrace(level) => write!(f, "toggle-backtrace({level})"), Self::TogglePause => write!(f, "toggle-pause"), Self::ToggleRawOutput => write!(f, "toggle-raw-output"), Self::ToggleSummary => write!(f, "toggle-summary"), Self::ToggleWrap => write!(f, "toggle-wrap"), Self::Unpause => write!(f, "unpause"), Self::FocusSearch => write!(f, "focus-search"), Self::FocusGoto => write!(f, "focus-goto"), Self::Validate => write!(f, "validate"), Self::NextMatch => write!(f, "next-match"), Self::PreviousMatch => write!(f, "previous-match"), Self::PlaySound(PlaySoundCommand { name, volume }) => { write!(f, "play-sound(")?; if let Some(name) = name { write!(f, "name={},", name)?; } write!(f, "volume={})", volume) } } } } impl std::str::FromStr for Internal { type Err = String; fn from_str(s: &str) -> Result<Self, Self::Err> { if let Ok(scroll_command) = ScrollCommand::from_str(s) { return Ok(Self::Scroll(scroll_command)); } match s { "back" => Ok(Self::Back), "back-or-quit" => Ok(Self::BackOrQuit), "help" => Ok(Self::Help), "quit" => Ok(Self::Quit), "refresh" => Ok(Self::Refresh), "reload-config" => Ok(Self::ReloadConfig), "rerun" => Ok(Self::ReRun), "scope-to-failures" => Ok(Self::ScopeToFailures), "toggle-raw-output" => Ok(Self::ToggleRawOutput), "toggle-backtrace" => Ok(Self::ToggleBacktrace("1")), "toggle-backtrace(1)" => Ok(Self::ToggleBacktrace("1")), "toggle-backtrace(2)" => Ok(Self::ToggleBacktrace("2")), "toggle-backtrace(full)" => Ok(Self::ToggleBacktrace("full")), "toggle-summary" => Ok(Self::ToggleSummary), "toggle-wrap" => Ok(Self::ToggleWrap), "noop" | "no-op" | "no-operation" => Ok(Self::NoOp), "pause" => Ok(Self::Pause), "unpause" => Ok(Self::Unpause), "toggle-pause" => Ok(Self::TogglePause), "focus-search" => Ok(Self::FocusSearch), "focus-goto" => Ok(Self::FocusGoto), "validate" => Ok(Self::Validate), "next-match" => Ok(Self::NextMatch), "previous-match" => Ok(Self::PreviousMatch), "copy-unstyled-output" => Ok(Self::CopyUnstyledOutput), "play-sound" => Ok(Self::PlaySound(PlaySoundCommand::default())), _ => { if let Some((_, props)) = regex_captures!(r"^play[_-]sound\((.*)\)$", s) { let iter = regex_captures_iter!(r"([^=,]+)=([^=,]+)", props); let mut volume = Volume::default(); let mut name = None; for (_, [prop_name, prop_value]) in iter.map(|c| c.extract()) { let prop_value = prop_value.trim(); match prop_name.trim() { "name" => { name = Some(prop_value.to_string()); } "volume" => { volume = prop_value.parse()?; } _ => { return Err("invalid play-sound parameter: {prop_name}".to_string()); } } } return Ok(Self::PlaySound(PlaySoundCommand { name, volume })); } Err("invalid internal".to_string()) } } } } impl Serialize for Internal { fn serialize<S>( &self, serializer: S, ) -> Result<S::Ok, S::Error> where S: Serializer, { serializer.collect_str(self) } } impl<'de> Deserialize<'de> for Internal { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; Self::from_str(&s).map_err(de::Error::custom) } } #[test] fn test_internal_string_round_trip() { use crate::Volume; let internals = [ Internal::Back, Internal::BackOrQuit, Internal::FocusSearch, Internal::Help, Internal::NoOp, Internal::Pause, Internal::Quit, Internal::ReRun, Internal::ReloadConfig, Internal::ScopeToFailures, Internal::Scroll(ScrollCommand::MilliPages(-3000)), Internal::Scroll(ScrollCommand::MilliPages(-350)), Internal::Scroll(ScrollCommand::MilliPages(1561)), Internal::Scroll(ScrollCommand::Top), Internal::ToggleBacktrace("1"), Internal::ToggleBacktrace("full"), Internal::TogglePause, Internal::ToggleSummary, Internal::ToggleWrap, Internal::Unpause, Internal::Validate, Internal::NextMatch, Internal::PreviousMatch, Internal::PlaySound(PlaySoundCommand::default()), Internal::PlaySound(PlaySoundCommand { name: None, volume: Volume::new(50), }), Internal::PlaySound(PlaySoundCommand { name: Some("beep-beep".to_string()), volume: Volume::new(100), }), Internal::PlaySound(PlaySoundCommand { name: None, volume: Volume::new(0), }), ]; for internal in internals { println!("testing {:?}", internal.to_string()); assert_eq!(internal.to_string().parse(), Ok(internal)); } } /// Check that white space is allowed around play-sound parameters /// See https://github.com/Canop/bacon/issues/322 #[test] fn test_play_sound_parsing_with_space() { use crate::Action; let strings = [ "play-sound(name=car-horn,volume=5)", "play-sound(name=car-horn, volume=5)", "play-sound( name = car-horn , volume = 5 )", ]; let psc = PlaySoundCommand { name: Some("car-horn".to_string()), volume: Volume::new(5), }; for string in &strings { let action: Action = string.parse().unwrap(); assert_eq!(action, Action::Internal(Internal::PlaySound(psc.clone()))); } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/jobs/concrete_job_ref.rs�����������������������������������������������������������0000644�0000000�0000000�00000006523�10461020230�0016361�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, lazy_regex::*, serde::{ Deserialize, Deserializer, Serialize, Serializer, de, }, std::{ fmt, str::FromStr, }, }; /// A "concrete" job ref is one which can be used from the start, without /// referring to the job stack #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ConcreteJobRef { pub name_or_alias: NameOrAlias, pub scope: Scope, } impl ConcreteJobRef { pub fn from_job_name<S: Into<String>>(s: S) -> Self { Self { name_or_alias: NameOrAlias::Name(s.into()), scope: Default::default(), } } pub fn badge_label(&self) -> String { let mut s = String::new(); match &self.name_or_alias { NameOrAlias::Name(name) => { s.push_str(name); } NameOrAlias::Alias(alias) => { s.push_str(alias); } } if self.scope.has_tests() { s.push_str(" (scoped)"); } s } pub fn with_scope( mut self, scope: Scope, ) -> Self { self.scope = scope; self } } impl Default for ConcreteJobRef { fn default() -> Self { Self { name_or_alias: NameOrAlias::Name("check".to_string()), scope: Default::default(), } } } impl fmt::Display for ConcreteJobRef { fn fmt( &self, f: &mut fmt::Formatter, ) -> fmt::Result { match &self.name_or_alias { NameOrAlias::Alias(alias) => write!(f, "alias:{alias}")?, NameOrAlias::Name(name) => write!(f, "{name}")?, } if self.scope.has_tests() { write!(f, "({})", self.scope.tests.join(","))?; } Ok(()) } } impl FromStr for ConcreteJobRef { type Err = &'static str; fn from_str(s: &str) -> Result<Self, Self::Err> { if s.is_empty() { Err("empty job name") } else { Ok(s.into()) } } } impl Serialize for ConcreteJobRef { fn serialize<S>( &self, serializer: S, ) -> Result<S::Ok, S::Error> where S: Serializer, { serializer.serialize_str(&self.to_string()) } } impl<'de> Deserialize<'de> for ConcreteJobRef { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; FromStr::from_str(&s).map_err(de::Error::custom) } } impl From<&str> for ConcreteJobRef { fn from(str_entry: &str) -> Self { let Some((_, alias_prefix, name_or_alias, scope)) = regex_captures!(r"^(alias:)?([^\(\)]+)(?:\(([^\)]+)\))?$", str_entry,) else { warn!("unexpected job ref: {:?}", str_entry); return Self::from_job_name(str_entry.to_string()); }; let name_or_alias = if alias_prefix.is_empty() { NameOrAlias::Name(name_or_alias.to_string()) } else { NameOrAlias::Alias(name_or_alias.to_string()) }; let scope = Scope { tests: scope .split(',') .filter(|t| !t.trim().is_empty()) .map(|s| s.to_string()) .collect(), }; Self { name_or_alias, scope, } } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/jobs/job.rs������������������������������������������������������������������������0000644�0000000�0000000�00000021304�10461020230�0013635�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, serde::Deserialize, std::collections::HashMap, }; /// One of the possible jobs that bacon can run /// One of the possible job that bacon can run #[derive(Debug, Default, Clone, Deserialize, PartialEq)] pub struct Job { /// Whether to consider that we can have a success /// when we have test failures pub allow_failures: Option<bool>, /// Whether to consider that we can have a success /// when we have warnings. This is especially useful /// for "cargo run" jobs pub allow_warnings: Option<bool>, /// The analyzer interpreting the output of the command, the /// standard cargo dedicated one if not provided pub analyzer: Option<AnalyzerRef>, /// Whether gitignore rules must be applied pub apply_gitignore: Option<bool>, /// Whether to wait for the computation to finish before /// to display it on screen /// /// This is true by default. Set it to false if you want /// the previous computation result to be replaced with /// the new one as soon as it starts. pub background: Option<bool>, /// The tokens making the command to execute (first one /// is the executable). #[serde(default)] pub command: Vec<String>, /// Whether to apply the default watch list, which is /// `["src", "tests", "benches", "examples", "build.rs"]` /// /// This is true by default. Set it to false if you want /// to watch nothing, or only the directories you set in /// `watch`. pub default_watch: Option<bool>, /// Env vars to set for this job execution #[serde(default)] pub env: HashMap<String, String>, /// Whether to expand environment variables in the command pub expand_env_vars: Option<bool>, /// Whether to insert extraneous arguments provided by bacon or end users /// /// Eg: --all-features or anything after -- in bacon incantation pub extraneous_args: Option<bool>, /// A list of glob patterns to ignore #[serde(default)] pub ignore: Vec<String>, /// Patterns of lines which should be ignored. Patterns of /// the prefs or bacon.toml can be overridden at the job pub ignored_lines: Option<Vec<LinePattern>>, /// A kill command. If not provided, SIGKILL is used. pub kill: Option<Vec<String>>, /// Whether we need to capture stdout too (stderr is /// always captured) pub need_stdout: Option<bool>, /// How to handle changes: either immediately kill the current job /// then restart it, or wait for the current job to finish before /// restarting it. pub on_change_strategy: Option<OnChangeStrategy>, /// The optional action to run when there's no /// error, warning or test failures /// (depending on whether allow_warnings is true or false) /// /// Could be made a vec in the future but that would involve /// explaining subtleties like the fact that those actions stop /// after the first one ending the mission or doing a refresh #[serde(default)] pub on_success: Option<Action>, pub grace_period: Option<Period>, /// The optional action to run when it's not a success #[serde(default)] pub on_failure: Option<Action>, /// A list of directories that will be watched if the job /// is run on a package. /// src, examples, tests, and benches are implicitly included /// unless you `set default_watch` to false. pub watch: Option<Vec<String>>, pub show_changes_count: Option<bool>, #[serde(default)] pub sound: SoundConfig, } static DEFAULT_ARGS: &[&str] = &["--color", "always"]; impl Job { /// Build a `Job` for a cargo alias pub fn from_alias( alias_name: &str, settings: &Settings, ) -> Self { let mut command = vec!["cargo".to_string(), alias_name.to_string()]; if let Some(additional_args) = settings.additional_alias_args.as_ref() { for arg in additional_args { command.push(arg.to_string()) } } else { for arg in DEFAULT_ARGS { command.push(arg.to_string()) } } Self { command, ..Default::default() } } pub fn allow_failures(&self) -> bool { self.allow_failures.unwrap_or(false) } pub fn allow_warnings(&self) -> bool { self.allow_warnings.unwrap_or(false) } pub fn background(&self) -> bool { self.background.unwrap_or(true) } pub fn default_watch(&self) -> bool { self.default_watch.unwrap_or(true) } pub fn expand_env_vars(&self) -> bool { self.expand_env_vars.unwrap_or(true) } pub fn need_stdout(&self) -> bool { self.need_stdout.unwrap_or(false) } pub fn extraneous_args(&self) -> bool { self.extraneous_args.unwrap_or(true) } pub fn show_changes_count(&self) -> bool { self.show_changes_count.unwrap_or(false) } pub fn grace_period(&self) -> Period { self.grace_period .unwrap_or(std::time::Duration::from_millis(15).into()) } pub fn on_change_strategy(&self) -> OnChangeStrategy { self.on_change_strategy .unwrap_or(OnChangeStrategy::WaitThenRestart) } pub fn apply( &mut self, job: &Job, ) { if let Some(b) = job.allow_failures { self.allow_failures = Some(b); } if let Some(b) = job.allow_warnings { self.allow_warnings = Some(b); } if let Some(v) = job.analyzer { self.analyzer = Some(v); } if let Some(b) = job.apply_gitignore { self.apply_gitignore = Some(b); } if let Some(b) = job.background { self.background = Some(b); } if !job.command.is_empty() { self.command.clone_from(&job.command); } if let Some(b) = job.default_watch { self.default_watch = Some(b); } for (k, v) in &job.env { self.env.insert(k.clone(), v.clone()); } if let Some(b) = job.expand_env_vars { self.expand_env_vars = Some(b); } if let Some(b) = job.extraneous_args { self.extraneous_args = Some(b); } for v in &job.ignore { if !self.ignore.contains(v) { self.ignore.push(v.clone()); } } if let Some(v) = job.ignored_lines.as_ref() { self.ignored_lines = Some(v.clone()); } if let Some(v) = job.kill.as_ref() { self.kill = Some(v.clone()); } if let Some(b) = job.need_stdout { self.need_stdout = Some(b); } if let Some(v) = job.on_change_strategy { self.on_change_strategy = Some(v); } if let Some(v) = job.on_success.as_ref() { self.on_success = Some(v.clone()); } if let Some(v) = job.grace_period { self.grace_period = Some(v); } if let Some(v) = job.on_failure.as_ref() { self.on_failure = Some(v.clone()); } if let Some(v) = job.watch.as_ref() { self.watch = Some(v.clone()); } if let Some(b) = job.show_changes_count { self.show_changes_count = Some(b); } self.sound.apply(&job.sound); } } #[test] fn test_job_apply() { use std::str::FromStr; let mut base_job = Job::default(); let job_to_apply = Job { allow_failures: Some(true), allow_warnings: Some(false), analyzer: Some(AnalyzerRef::Nextest), apply_gitignore: Some(false), background: Some(false), command: vec!["cargo".to_string(), "test".to_string()], default_watch: Some(false), env: vec![("RUST_LOG".to_string(), "debug".to_string())] .into_iter() .collect(), expand_env_vars: Some(false), extraneous_args: Some(false), ignore: vec!["special-target".to_string(), "generated".to_string()], ignored_lines: Some(vec![LinePattern::from_str("half-error.*").unwrap()]), kill: Some(vec!["die".to_string()]), need_stdout: Some(true), grace_period: Some(Period::from_str("20ms").unwrap()), on_change_strategy: Some(OnChangeStrategy::KillThenRestart), on_success: Some(Action::from_str("refresh").unwrap()), on_failure: Some(Action::from_str("play-sound(name=car-horn)").unwrap()), watch: Some(vec!["src".to_string(), "tests".to_string()]), show_changes_count: Some(true), sound: SoundConfig { enabled: Some(true), base_volume: Some(Volume::from_str("50").unwrap()), }, }; base_job.apply(&job_to_apply); dbg!(&base_job); assert_eq!(&base_job, &job_to_apply); } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/jobs/job_ref.rs��������������������������������������������������������������������0000644�0000000�0000000�00000006375�10461020230�0014504�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, lazy_regex::*, std::fmt, }; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum JobRef { Default, Initial, Previous, PreviousOrQuit, Concrete(ConcreteJobRef), Scope(Scope), } impl JobRef { pub fn from_job_name<S: Into<String>>(s: S) -> Self { Self::Concrete(ConcreteJobRef::from_job_name(s)) } } impl From<Scope> for JobRef { fn from(scope: Scope) -> Self { Self::Scope(scope) } } impl From<ConcreteJobRef> for JobRef { fn from(concrete: ConcreteJobRef) -> Self { Self::Concrete(concrete) } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum NameOrAlias { Name(String), Alias(String), } impl fmt::Display for JobRef { fn fmt( &self, f: &mut fmt::Formatter, ) -> fmt::Result { match self { Self::Default => write!(f, "default"), Self::Initial => write!(f, "initial"), Self::Previous => write!(f, "previous"), Self::PreviousOrQuit => write!(f, "previous-or-quit"), Self::Scope(Scope { tests }) => write!(f, "scope:{}", tests.join(",")), Self::Concrete(concrete) => write!(f, "{}", concrete), } } } impl From<&str> for JobRef { fn from(s: &str) -> Self { regex_switch!(s, "^default$"i => Self::Default, "^initial$"i => Self::Initial, "^previous$"i => Self::Previous, "^previous-or-quit$"i => Self::PreviousOrQuit, "^scope:(?<tests>.+)$"i => Self::Scope(Scope { tests: tests .split(',') .filter(|t| !t.trim().is_empty()) .map(|s| s.to_string()) .collect(), }), ) .unwrap_or_else(|| Self::Concrete(ConcreteJobRef::from(s))) } } #[test] fn test_job_ref_string_round_trip() { let job_refs = vec![ JobRef::Default, JobRef::Initial, JobRef::Previous, JobRef::PreviousOrQuit, JobRef::Concrete(ConcreteJobRef { name_or_alias: NameOrAlias::Name("run".to_string()), scope: Scope::default(), }), JobRef::Concrete(ConcreteJobRef { name_or_alias: NameOrAlias::Name("nextest".to_string()), scope: Scope { tests: vec!["first::test".to_string(), "second_test".to_string()], }, }), JobRef::Concrete(ConcreteJobRef { name_or_alias: NameOrAlias::Alias("my-check".to_string()), scope: Scope::default(), }), JobRef::Concrete(ConcreteJobRef { name_or_alias: NameOrAlias::Alias("my-test".to_string()), scope: Scope { tests: vec!["abc".to_string()], }, }), JobRef::Concrete(ConcreteJobRef { name_or_alias: NameOrAlias::Name("nextest".to_string()), scope: Scope { tests: vec!["abc".to_string()], }, }), JobRef::Scope(Scope { tests: vec!["first::test".to_string(), "second_test".to_string()], }), ]; for job_ref in job_refs { let s = job_ref.to_string(); let job_ref2 = JobRef::from(s.as_str()); assert_eq!(job_ref, job_ref2); } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/jobs/job_stack.rs������������������������������������������������������������������0000644�0000000�0000000�00000005554�10461020230�0015033�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::{ Result, anyhow, }, }; /// The stack of jobs that bacon ran, allowing to get back to the previous one, /// or to scope the current one #[derive(Default)] pub struct JobStack { entries: Vec<ConcreteJobRef>, } impl JobStack { /// Apply the job ref instruction to determine the job to run, updating the stack. /// /// When no job is returned, the application is supposed to quit. pub fn pick_job( &mut self, job_ref: &JobRef, settings: &Settings, ) -> Result<Option<(ConcreteJobRef, Job)>> { debug!("picking job {job_ref:?}"); let concrete = match job_ref { JobRef::Default => settings.default_job.clone(), JobRef::Initial => settings .arg_job .as_ref() .unwrap_or(&settings.default_job) .clone(), JobRef::Previous | JobRef::PreviousOrQuit => { let current = self.entries.pop(); match self.entries.pop() { Some(concrete) => concrete, None if current .as_ref() .is_some_and(|current| current.scope.has_tests()) => { // rather than quitting, we assume the user wants to "unscope" ConcreteJobRef { name_or_alias: current.unwrap().name_or_alias, scope: Scope::default(), } } None if *job_ref == JobRef::PreviousOrQuit => { return Ok(None); } None => { let Some(current) = current else { error!("no current job"); // job stack was misused return Ok(None); }; current } } } JobRef::Concrete(concrete) => concrete.clone(), JobRef::Scope(scope) => match self.entries.last() { Some(concrete) => ConcreteJobRef { name_or_alias: concrete.name_or_alias.clone(), scope: scope.clone(), }, None => { return Ok(None); } }, }; let job = match &concrete.name_or_alias { NameOrAlias::Alias(alias) => Job::from_alias(alias, settings), NameOrAlias::Name(name) => settings .jobs .get(name) .ok_or_else(|| anyhow!("job not found: {:?}", name))? .clone(), }; if self.entries.last() != Some(&concrete) { self.entries.push(concrete.clone()); } Ok(Some((concrete, job))) } } ����������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/jobs/mod.rs������������������������������������������������������������������������0000644�0000000�0000000�00000000251�10461020230�0013640�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mod concrete_job_ref; mod job; mod job_ref; mod job_stack; mod scope; pub use { concrete_job_ref::*, job::*, job_ref::*, job_stack::*, scope::*, }; �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/jobs/scope.rs����������������������������������������������������������������������0000644�0000000�0000000�00000000355�10461020230�0014177�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/// A dynamic reduction of a job execution #[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] pub struct Scope { pub tests: Vec<String>, } impl Scope { pub fn has_tests(&self) -> bool { !self.tests.is_empty() } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/lib.rs�����������������������������������������������������������������������������0000644�0000000�0000000�00000001042�10461020230�0012671�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mod analysis; pub mod burp; mod cli; mod conf; mod context; mod context_nature; mod exec; mod export; mod help; mod ignorer; mod internal; mod jobs; mod mission; mod result; mod search; mod sound; mod tty; mod tui; mod watcher; pub use { analysis::*, cli::*, conf::*, context::*, context_nature::*, exec::*, export::*, help::*, ignorer::*, internal::*, jobs::*, mission::*, result::*, search::*, sound::*, tty::*, tui::*, watcher::*, }; #[macro_use] extern crate cli_log; ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/main.rs����������������������������������������������������������������������������0000644�0000000�0000000�00000000222�10461020230�0013046�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/// Knowledge is power fn main() -> anyhow::Result<()> { cli_log::init_cli_log!(); bacon::run()?; cli_log::info!("bye"); Ok(()) } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/mission.rs�������������������������������������������������������������������������0000644�0000000�0000000�00000022013�10461020230�0013605�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, lazy_regex::regex_replace_all, rustc_hash::FxHashSet, std::{ collections::HashMap, path::PathBuf, }, }; /// the description of the mission of bacon /// after analysis of the args, env, and surroundings #[derive(Debug)] pub struct Mission<'s> { pub location_name: String, pub concrete_job_ref: ConcreteJobRef, pub execution_directory: PathBuf, pub package_directory: PathBuf, pub workspace_directory: Option<PathBuf>, pub job: Job, pub paths_to_watch: Vec<PathBuf>, pub settings: &'s Settings, } impl Mission<'_> { /// Return an Ignorer according to the job's settings pub fn ignorer(&self) -> IgnorerSet { let mut set = IgnorerSet::default(); if self.job.apply_gitignore != Some(false) { match GitIgnorer::new(&self.package_directory) { Ok(git_ignorer) => { set.add(Box::new(git_ignorer)); } Err(e) => { // might be normal, eg not in a git repo debug!("Failed to initialise git ignorer: {e}"); } } } if !self.job.ignore.is_empty() { let mut glob_ignorer = GlobIgnorer::default(); for pattern in &self.job.ignore { if let Err(e) = glob_ignorer.add(pattern, &self.package_directory) { warn!("Failed to add ignore pattern {pattern}: {e}"); } } set.add(Box::new(glob_ignorer)); } set } pub fn is_success( &self, report: &Report, ) -> bool { report.is_success(self.job.allow_warnings(), self.job.allow_failures()) } pub fn make_absolute( &self, path: PathBuf, ) -> PathBuf { if path.is_absolute() { return path; } // There's a small mess here. Cargo tends to make paths relative // not to the package or work directory but to the workspace, contrary // to any sane tool. We have to guess. if let Some(workspace) = &self.workspace_directory { let workspace_joined = workspace.join(&path); if workspace_joined.exists() { return workspace_joined; } } self.package_directory.join(&path) } /// build (and doesn't call) the external cargo command pub fn get_command(&self) -> anyhow::Result<CommandBuilder> { let mut command = if self.job.expand_env_vars() { self.job .command .iter() .map(|token| { regex_replace_all!(r"\$([A-Z0-9a-z_]+)", token, |whole: &str, name| { match std::env::var(name) { Ok(value) => value, Err(_) => { warn!("variable {whole} not found in env"); whole.to_string() } } }) .to_string() }) .collect() } else { self.job.command.clone() }; if command.is_empty() { anyhow::bail!( "Empty command in job {}", self.concrete_job_ref.badge_label() ); } let scope = &self.concrete_job_ref.scope; if scope.has_tests() && command.len() > 2 { let tests = if command[0] == "cargo" && command[1] == "test" { // Here we're going around a limitation of the vanilla cargo test: // it can only be scoped to one test &scope.tests[..1] } else { &scope.tests }; for test in tests { command.push(test.to_string()); } } let mut tokens = command.iter(); let mut command = CommandBuilder::new( tokens.next().unwrap(), // implies a check in the job ); command.with_stdout(self.job.need_stdout()); let envs: HashMap<&String, &String> = self .settings .all_jobs .env .iter() .chain(self.job.env.iter()) .collect(); if !self.job.extraneous_args() { command.args(tokens); command.current_dir(&self.execution_directory); command.envs(envs); debug!("command: {:#?}", &command); return Ok(command); } let mut no_default_features_done = false; let mut features_done = false; let mut last_is_features = false; let mut tokens = tokens.chain(&self.settings.additional_job_args); let mut has_double_dash = false; for arg in tokens.by_ref() { if arg == "--" { // we'll defer addition of the following arguments to after // the addition of the features stuff, so that the features // arguments are given to the cargo command. has_double_dash = true; break; } if last_is_features { if self.settings.all_features { debug!("ignoring features given along --all-features"); } else { features_done = true; // arg is expected there to be the list of features match (&self.settings.features, self.settings.no_default_features) { (Some(features), false) => { // we take the features of both the job and the args command.arg("--features"); command.arg(merge_features(arg, features)); } (Some(features), true) => { // arg add features and remove the job ones command.arg("--features"); command.arg(features); } (None, true) => { // we pass no feature } (None, false) => { // nothing to change command.arg("--features"); command.arg(arg); } } } last_is_features = false; } else if arg == "--no-default-features" { no_default_features_done = true; last_is_features = false; command.arg(arg); } else if arg == "--features" { last_is_features = true; } else { command.arg(arg); } } if self.settings.no_default_features && !no_default_features_done { command.arg("--no-default-features"); } if self.settings.all_features { command.arg("--all-features"); } if !features_done { if let Some(features) = &self.settings.features { if self.settings.all_features { debug!("not using features because of --all-features"); } else { command.arg("--features"); command.arg(features); } } } if has_double_dash { command.arg("--"); for arg in tokens { command.arg(arg); } } command.current_dir(&self.execution_directory); command.envs(envs); debug!("command builder: {:#?}", &command); Ok(command) } pub fn kill_command(&self) -> Option<Vec<String>> { self.job.kill.clone() } /// whether we need stdout and not just stderr pub fn need_stdout(&self) -> bool { self.job .need_stdout .or(self.settings.all_jobs.need_stdout) .unwrap_or(false) } pub fn analyzer(&self) -> AnalyzerRef { self.job.analyzer.unwrap_or_default() } pub fn ignored_lines_patterns(&self) -> Option<&Vec<LinePattern>> { self.job .ignored_lines .as_ref() .or(self.settings.all_jobs.ignored_lines.as_ref()) .filter(|p| !p.is_empty()) } pub fn sound_player_if_needed(&self) -> Option<SoundPlayer> { if self.job.sound.is_enabled() { match SoundPlayer::new(self.job.sound.get_base_volume()) { Ok(sound_player) => Some(sound_player), Err(e) => { warn!("Failed to initialise sound player: {e}"); None } } } else { None } } } fn merge_features( a: &str, b: &str, ) -> String { let mut features = FxHashSet::default(); for feature in a.split(',') { features.insert(feature); } for feature in b.split(',') { features.insert(feature); } features.iter().copied().collect::<Vec<&str>>().join(",") } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/result/command_output.rs�����������������������������������������������������������0000644�0000000�0000000�00000003221�10461020230�0016500�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, serde::{ Deserialize, Serialize, }, std::process::ExitStatus, }; #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Eq)] pub enum CommandStream { StdOut, StdErr, } /// a line coming either from stdout or from stderr, before TTY parsing #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct RawCommandOutputLine { pub content: String, pub origin: CommandStream, } /// a line coming either from stdout or from stderr #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct CommandOutputLine { pub content: TLine, pub origin: CommandStream, } /// some output lines #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct CommandOutput { pub lines: Vec<Line>, } /// a piece of information about the execution of a command #[derive(Debug)] pub enum CommandExecInfo { /// Command ended End { status: Option<ExitStatus> }, /// Bacon killed the command Interruption, /// Execution failed Error(anyhow::Error), /// Here's a line of output (coming from stderr or stdout) Line(RawCommandOutputLine), } impl CommandOutput { pub fn reverse(&mut self) { self.lines.reverse() } pub fn push<L: Into<Line>>( &mut self, line: L, ) { self.lines.push(line.into()); } pub fn len(&self) -> usize { self.lines.len() } } impl From<RawCommandOutputLine> for CommandOutputLine { fn from(raw: RawCommandOutputLine) -> Self { CommandOutputLine { content: TLine::from_tty(&raw.content), origin: raw.origin, } } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/result/command_result.rs�����������������������������������������������������������0000644�0000000�0000000�00000006100�10461020230�0016455�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::*, serde::{ Deserialize, Serialize, }, std::process::ExitStatus, }; /// what we get from the execution of a command #[derive(Debug, Clone, Serialize, Deserialize)] pub enum CommandResult { /// a trustable report with errors and warnings computed Report(Report), /// we don't have a proper report Failure(Failure), /// not yet computed None, } impl CommandResult { pub fn build( output: CommandOutput, exit_status: Option<ExitStatus>, mut report: Report, ) -> Result<Self> { let error_code = exit_status.and_then(|s| s.code()).filter(|&c| c != 0); debug!("report stats: {:?}", &report.stats); if let Some(error_code) = error_code { let stats = &report.stats; if stats.errors + stats.test_fails + stats.warnings == 0 { // Report shows no error while the command exe reported // an error, so the report can't be trusted. // Note that some tools return an error on warnings (eg // miri), some don't. let suggest_backtrace = report.suggest_backtrace; return Ok(Self::Failure(Failure { error_code, output, suggest_backtrace, })); } } report.output = output; // report looks valid Ok(Self::Report(report)) } pub fn output(&self) -> Option<&CommandOutput> { match self { Self::Report(report) => Some(&report.output), Self::Failure(failure) => Some(&failure.output), Self::None => None, } } pub fn report(&self) -> Option<&Report> { match self { Self::Report(report) => Some(report), _ => None, } } pub fn suggest_backtrace(&self) -> bool { match self { Self::Report(report) => report.suggest_backtrace, Self::Failure(failure) => failure.suggest_backtrace, _ => false, } } /// return true when the report has been computed and there's been no /// error, warning, or test failures /// /// This is different from the is_success that a mission can compute /// from a report using its own settings (eg allow_warnings) pub fn is_success(&self) -> bool { match self { Self::Report(report) => { report.stats.errors + report.stats.warnings + report.stats.test_fails == 0 } _ => false, } } pub fn reverse(&mut self) { match self { Self::Report(report) => { report.reverse(); } Self::Failure(failure) => { failure.output.reverse(); } Self::None => {} } } pub fn lines_len(&self) -> usize { match self { Self::Report(report) => report.lines.len(), Self::Failure(failure) => failure.output.lines.len(), Self::None => 0, } } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/result/failure.rs������������������������������������������������������������������0000644�0000000�0000000�00000000421�10461020230�0015070�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, serde::{ Deserialize, Serialize, }, }; /// data of a failed command #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Failure { pub error_code: i32, pub output: CommandOutput, pub suggest_backtrace: bool, } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/result/line.rs���������������������������������������������������������������������0000644�0000000�0000000�00000005110�10461020230�0014370�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, lazy_regex::regex_captures, serde::{ Deserialize, Serialize, }, std::path::PathBuf, }; /// A report line #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Line { /// the index among items /// (all lines having the same index belong to the same error, warning, or test item, /// or just the same unclassified soup) pub item_idx: usize, pub line_type: LineType, pub content: TLine, } impl Line { /// If the line is a title, get its message pub fn title_message(&self) -> Option<&str> { let title = match self.line_type { LineType::Title(_) => { if let Some(content) = self.content.if_unstyled() { Some(content) } else { self.content .strings .get(1) .map(|ts| ts.raw.as_str()) .map(|s| s.trim_start_matches(|c: char| c.is_whitespace() || c == ':')) } } _ => None, }; title } pub fn matches( &self, summary: bool, ) -> bool { !summary || self.line_type.is_summary() } /// Return the location as given by cargo /// It's usually relative and may contain the line and column pub fn location(&self) -> Option<&str> { match self.line_type { LineType::Location => { // the location part is a string at end like src/truc:15:3 // or src/truc self.content .strings .last() .and_then(|ts| regex_captures!(r"(\S+)$", ts.raw.as_str())) .map(|(_, path)| path) } _ => None, } } /// Return the absolute path to the error/warning/test location pub fn location_path( &self, mission: &Mission, ) -> Option<PathBuf> { let location_path = self.location()?; let mut location_path = PathBuf::from(location_path); if !location_path.is_absolute() { location_path = mission.package_directory.join(location_path); } Some(location_path) } pub fn is_continuation(&self) -> bool { matches!(self.line_type, LineType::Continuation { .. }) } } impl From<CommandOutputLine> for Line { fn from(col: CommandOutputLine) -> Self { Line { item_idx: 0, content: col.content, line_type: LineType::Raw(col.origin), } } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/result/mod.rs����������������������������������������������������������������������0000644�0000000�0000000�00000000500�10461020230�0014216�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mod command_output; mod command_result; mod failure; mod line; mod report; mod report_maker; mod wrapped_command_output; mod wrapped_report; pub use { command_output::*, command_result::*, failure::*, line::*, report::*, report_maker::*, wrapped_command_output::*, wrapped_report::*, }; ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/result/report.rs�������������������������������������������������������������������0000644�0000000�0000000�00000010415�10461020230�0014760�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::Result, lazy_regex::*, serde::{ Deserialize, Serialize, }, std::{ collections::HashMap, io, path::PathBuf, }, }; /// the usable content of cargo watch's output, /// lightly analyzed #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Report { pub lines: Vec<Line>, pub stats: Stats, pub suggest_backtrace: bool, pub output: CommandOutput, pub failure_keys: Vec<String>, /// the exports that the analyzers have done, by name pub analyzer_exports: HashMap<String, String>, } impl Report { /// change the order of the lines so that items are in reverse order /// (but keep the order of lines of a given item) pub fn reverse(&mut self) { self.lines .sort_by_key(|line| std::cmp::Reverse(line.item_idx)); } /// A successful report is one with nothing to tell: no warning, /// no error, no test failure pub fn is_success( &self, allow_warnings: bool, allow_failures: bool, ) -> bool { !(self.stats.errors != 0 || (!allow_failures && self.stats.test_fails != 0) || (!allow_warnings && self.stats.warnings != 0)) } /// Extract all the diagnostic context, that is all the normal lines /// which have the same item index as the given line. /// Those lines are taken without style and joined with an escaped newline. fn extract_raw_diagnostic_context( &self, line: &Line, ) -> String { self.lines .iter() .filter(|l| l.line_type == LineType::Normal && l.item_idx == line.item_idx) .map(|l| l.content.to_raw()) .collect::<Vec<String>>() .join("\\n") } /// export the report in a file, as the "locations" format pub fn write_locations<W: io::Write>( &self, w: &mut W, mission: &Mission, // used to get the workspace root to normalize locations line_format: &str, ) -> Result<(), io::Error> { let mut last_kind = "???"; let mut message = None; let format_has_context = line_format.contains("{context}"); for line in &self.lines { match line.line_type { LineType::Title(Kind::Warning) => { last_kind = "warning"; message = line.title_message(); } LineType::Title(Kind::Error) => { last_kind = "error"; message = line.title_message(); } LineType::Title(Kind::TestFail) => { last_kind = "test"; message = line.title_message(); } _ => {} } let Some(location) = line.location() else { continue; }; let (_, path, file_line, mut file_column) = regex_captures!(r#"^([^:\s]+):(\d+)(?:\:(\d+))?$"#, location) .unwrap_or(("", location, "", "")); // we need to make sure the path is absolute let path_buf = PathBuf::from(path); let path_buf = mission.make_absolute(path_buf); let path = path_buf.to_string_lossy().to_string(); let extracted_context; let context = if format_has_context { extracted_context = self.extract_raw_diagnostic_context(line); &extracted_context } else { "" }; if file_column.is_empty() { file_column = "1"; // by default, first column in file } let exported = regex_replace_all!(r#"\{([^\s}]+)\}"#, line_format, |_, key| { match key { "column" => file_column, "context" => context, "kind" => last_kind, "line" => file_line, "message" => message.unwrap_or(""), "path" => &path, _ => { debug!("unknown export key: {key:?}"); "" } } }); writeln!(w, "{}", exported)?; } debug!("exported locations"); Ok(()) } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/result/report_maker.rs�������������������������������������������������������������0000644�0000000�0000000�00000003257�10461020230�0016145�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::*, std::process::ExitStatus, }; /// Dedicated to a mission, the report maker receives the command /// output lines and builds a report at end, complete with starts. pub struct ReportMaker { ignored_lines_patterns: Option<Vec<LinePattern>>, analyzer: Box<dyn Analyzer>, } impl ReportMaker { pub fn new(mission: &Mission) -> Self { let ignored_lines_patterns = mission.ignored_lines_patterns().cloned(); let analyzer_ref = mission.analyzer(); let analyzer = analyzer_ref.create_analyzer(); Self { ignored_lines_patterns, analyzer, } } pub fn start( &mut self, mission: &Mission, ) { self.analyzer.start(mission); } pub fn receive_line( &mut self, cmd_line: CommandOutputLine, command_output: &mut CommandOutput, ) { if let Some(patterns) = self.ignored_lines_patterns.as_ref() { let raw_line = cmd_line.content.to_raw(); // FIXME could be made more efficient if patterns.iter().any(|p| p.raw_line_is_match(&raw_line)) { debug!("ignoring line: {}", &raw_line); return; } } self.analyzer.receive_line(cmd_line, command_output); } pub fn build_report(&mut self) -> Result<Report> { self.analyzer.build_report() } pub fn build_result( &mut self, output: CommandOutput, exit_status: Option<ExitStatus>, ) -> Result<CommandResult> { let report = self.analyzer.build_report()?; let result = CommandResult::build(output, exit_status, report)?; Ok(result) } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/result/wrapped_command_output.rs���������������������������������������������������0000644�0000000�0000000�00000002420�10461020230�0020222�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use crate::*; /// A wrapped cmd_output, only valid for the cmd_output it was computed for, /// contains references to the start and end of lines wrapped for a /// given width pub struct WrappedCommandOutput { pub sub_lines: Vec<Line>, /// in order to allow partial wrapping, and assuming the wrapped part /// didn't change, we store the count of lines which were wrapped so /// that we may update starting from there pub wrapped_lines_count: usize, } impl WrappedCommandOutput { /// compute a new wrapped cmd_output for a width and cmd_output. /// /// width is the total area width, including the scrollbar. pub fn new( cmd_output: &CommandOutput, width: u16, ) -> Self { let sub_lines = wrap(&cmd_output.lines, width); Self { sub_lines, wrapped_lines_count: cmd_output.len(), } } /// Assuming the width is the same and the lines already handled /// didn't change, wrap and add the lines which weren't. pub fn update( &mut self, cmd_output: &CommandOutput, width: u16, ) { self.sub_lines .extend(wrap(&cmd_output.lines[self.wrapped_lines_count..], width)); self.wrapped_lines_count = cmd_output.lines.len(); } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/result/wrapped_report.rs�����������������������������������������������������������0000644�0000000�0000000�00000001260�10461020230�0016500�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use crate::*; pub struct WrappedReport { pub sub_lines: Vec<Line>, /// number of summary lines after wrapping pub summary_height: usize, } impl WrappedReport { /// compute a new wrapped report for a width and report. /// /// width is the total area width, including the scrollbar. pub fn new( report: &Report, width: u16, ) -> Self { debug!("wrapping report"); let sub_lines = wrap(&report.lines, width); let summary_height = sub_lines .iter() .filter(|sl| sl.line_type.is_summary()) .count(); Self { sub_lines, summary_height, } } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/search/goto_idx.rs�����������������������������������������������������������������0000644�0000000�0000000�00000001210�10461020230�0015201�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use crate::*; pub fn search_item_idx<'i, I>( idx: usize, lines: I, ) -> Vec<Found> where I: IntoIterator<Item = &'i Line>, { for (line_idx, line) in lines.into_iter().enumerate() { if line.item_idx == idx && !line.content.strings.is_empty() { let end_byte_in_string = line.content.strings[0].raw.len(); return vec![Found { line_idx, trange: TRange { string_idx: 0, start_byte_in_string: 0, end_byte_in_string, }, continued: None, }]; } } vec![] } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/search/line_pattern.rs�������������������������������������������������������������0000644�0000000�0000000�00000002163�10461020230�0016061�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { lazy_regex::regex::Regex, serde::{ Deserialize, Deserializer, de, }, std::str::FromStr, }; /// A pattern dedicated to line matching. /// /// In the future, this may become more complex (eg filtering by style or origin) #[derive(Debug, Clone)] pub struct LinePattern { pub regex: Regex, } impl LinePattern { pub fn raw_line_is_match( &self, line: &str, ) -> bool { self.regex.is_match(line) } } impl FromStr for LinePattern { type Err = String; fn from_str(s: &str) -> Result<Self, Self::Err> { let regex = Regex::new(s).map_err(|e| format!("invalid regex: {}", e))?; Ok(Self { regex }) } } impl<'de> Deserialize<'de> for LinePattern { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; FromStr::from_str(&s).map_err(de::Error::custom) } } impl PartialEq for LinePattern { fn eq( &self, other: &Self, ) -> bool { self.regex.as_str() == other.regex.as_str() } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/search/mod.rs����������������������������������������������������������������������0000644�0000000�0000000�00000002244�10461020230�0014154�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mod goto_idx; mod line_pattern; mod search_pattern; pub use { goto_idx::*, line_pattern::*, search_pattern::*, }; use crate::*; #[derive(Debug, Clone, Copy, PartialEq)] pub enum SearchMode { Pattern, ItemIdx, } /// position in a [TLine] of a found pattern #[derive(Debug, PartialEq, Eq)] pub struct Found { /// The index of the first line containing the pattern pub line_idx: usize, /// The range of the pattern in the line pub trange: TRange, /// If the pattern goes over a line wrap, the range of the pattern in the next line pub continued: Option<TRange>, } pub const CSI_FOUND: &str = "\u{1b}[1m\u{1b}[38;5;208m"; // bold, orange foreground pub const CSI_FOUND_SELECTED: &str = "\u{1b}[1m\u{1b}[30m\u{1b}[48;5;208m"; // bold, orange background pub enum Search { Pattern(Pattern), ItemIdx(usize), } impl Search { pub fn search_lines<'i, I>( &self, lines: I, ) -> Vec<Found> where I: IntoIterator<Item = &'i Line>, { match self { Self::Pattern(pattern) => pattern.search_lines(lines), Self::ItemIdx(idx) => search_item_idx(*idx, lines), } } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/search/search_pattern.rs�����������������������������������������������������������0000644�0000000�0000000�00000006432�10461020230�0016402�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use crate::*; pub struct Pattern { pub pattern: String, // might change later } impl Pattern { // Current limitations: // - a match can't span over more than 2 lines. This is probably fine. pub fn search_lines<'i, I>( &self, lines: I, ) -> Vec<Found> where I: IntoIterator<Item = &'i Line>, { let lines = lines.into_iter(); let pattern = &self.pattern; let len = pattern.len(); let mut founds = Vec::new(); let mut previous_line: Option<&Line> = None; for (line_idx, line) in lines.enumerate() { if line.is_continuation() { if let Some(previous_line) = previous_line { // we check for a match broken by wrapping if !previous_line.content.strings.is_empty() && !line.content.strings.is_empty() { let previous_line_string_idx = previous_line.content.strings.len() - 1; let previous_last_raw = &previous_line.content.strings[previous_line_string_idx].raw; if let Some(cut) = find_cut_pattern( pattern, previous_last_raw, &line.content.strings[0].raw, ) { let found = Found { line_idx: line_idx - 1, trange: TRange { string_idx: previous_line_string_idx, start_byte_in_string: previous_last_raw.len() - cut, end_byte_in_string: previous_last_raw.len(), }, continued: Some(TRange { string_idx: 0, start_byte_in_string: 0, end_byte_in_string: len - cut, }), }; founds.push(found); } } } } previous_line = Some(line); for (string_idx, tstring) in line.content.strings.iter().enumerate() { let mut offset = 0; while offset + len < tstring.raw.len() { let haystack = &tstring.raw[offset..]; let Some(pos) = haystack.find(pattern) else { break; }; let found = Found { line_idx, trange: TRange { string_idx, start_byte_in_string: pos + offset, end_byte_in_string: pos + offset + pattern.len(), }, continued: None, }; founds.push(found); offset += pos + pattern.len(); } } } founds } } fn find_cut_pattern( pattern: &str, a: &str, b: &str, ) -> Option<usize> { let len = pattern.len(); (1..len).find(|&i| a.ends_with(&pattern[..i]) && b.starts_with(&pattern[i..])) } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/sound/mod.rs�����������������������������������������������������������������������0000644�0000000�0000000�00000001001�10461020230�0014025�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#[cfg(not(feature = "sound"))] mod no_sound; #[cfg(feature = "sound")] mod play_sound; mod sound_config; #[cfg(feature = "sound")] mod sound_player; mod volume; #[cfg(not(feature = "sound"))] pub use no_sound::*; #[cfg(feature = "sound")] pub use { play_sound::*, sound_player::*, }; pub use { sound_config::*, volume::*, }; /// A command to play a sound #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] pub struct PlaySoundCommand { pub name: Option<String>, pub volume: Volume, } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/sound/no_sound.rs������������������������������������������������������������������0000644�0000000�0000000�00000000653�10461020230�0015106�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use super::*; /// A dummy sound player which does nothing pub struct SoundPlayer {} impl SoundPlayer { pub fn new(_base_volume: Volume) -> anyhow::Result<Self> { Err(anyhow::anyhow!( "Bacon is compiled without the sound feature" )) } pub fn play( &self, _beep: PlaySoundCommand, ) { // should never be called as the sound player is not instanciated } } �������������������������������������������������������������������������������������bacon-3.12.0/src/sound/play_sound.rs����������������������������������������������������������������0000644�0000000�0000000�00000011534�10461020230�0015437�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { super::*, rodio::OutputStream, std::{ fmt, io::Cursor, time::Duration, }, termimad::crossbeam::channel::Receiver, }; struct Sound { bytes: &'static [u8], duration: Duration, } /// Get a sound by name, or the default sound if name is None /// /// Names here are as near as possible from the file names in the /// reources directory but without the number, syntax unconsistency and /// redundancy. Resource file names are kept identical to their original /// names to ease retrival for attribution). fn get_sound(name: Option<&str>) -> Result<Sound, SoundError> { let name = name.unwrap_or("store-scanner"); let sound = match name { "2" => Sound { bytes: include_bytes!("../../resources/2-100419.mp3"), duration: Duration::from_millis(2000), }, "90s-game-ui-6" => Sound { bytes: include_bytes!("../../resources/90s-game-ui-6-185099.mp3"), duration: Duration::from_millis(1300), }, "beep-6" => Sound { bytes: include_bytes!("../../resources/beep-6-96243.mp3"), duration: Duration::from_millis(1000), }, "beep-beep" => Sound { bytes: include_bytes!("../../resources/beep-beep-6151.mp3"), duration: Duration::from_millis(1200), }, "beep-warning" => Sound { bytes: include_bytes!("../../resources/beep-warning-6387.mp3"), duration: Duration::from_millis(1200), }, "bell-chord" => Sound { bytes: include_bytes!("../../resources/bell-chord1-83260.mp3"), duration: Duration::from_millis(1900), }, "car-horn" => Sound { bytes: include_bytes!("../../resources/car-horn-beepsmp3-14659.mp3"), duration: Duration::from_millis(1700), }, "convenience-store-ring" => Sound { bytes: include_bytes!("../../resources/conveniencestorering-96090.mp3"), duration: Duration::from_millis(1700), }, "cow-bells" => Sound { bytes: include_bytes!("../../resources/cow_bells_01-98236.mp3"), duration: Duration::from_millis(1400), }, "pickup" => Sound { bytes: include_bytes!("../../resources/pickup-sound-46472.mp3"), duration: Duration::from_millis(500), }, "positive-beeps" => Sound { bytes: include_bytes!("../../resources/positive_beeps-85504.mp3"), duration: Duration::from_millis(600), }, "short-beep-tone" => Sound { bytes: include_bytes!("../../resources/short-beep-tone-47916.mp3"), duration: Duration::from_millis(400), }, "slash" => Sound { bytes: include_bytes!("../../resources/slash1-94367.mp3"), duration: Duration::from_millis(800), }, "store-scanner" => Sound { bytes: include_bytes!("../../resources/store-scanner-beep-90395.mp3"), duration: Duration::from_millis(250), }, "success" => Sound { bytes: include_bytes!("../../resources/success-48018.mp3"), duration: Duration::from_millis(2000), }, _ => { return Err(SoundError::UnknownSoundName(name.to_string())); } }; Ok(sound) } #[derive(Debug)] pub enum SoundError { Interrupted, UnknownSoundName(String), RodioStream(rodio::StreamError), RodioPlay(rodio::PlayError), } impl From<rodio::StreamError> for SoundError { fn from(e: rodio::StreamError) -> Self { SoundError::RodioStream(e) } } impl From<rodio::PlayError> for SoundError { fn from(e: rodio::PlayError) -> Self { SoundError::RodioPlay(e) } } impl fmt::Display for SoundError { fn fmt( &self, f: &mut fmt::Formatter, ) -> fmt::Result { match self { SoundError::Interrupted => write!(f, "sound interrupted"), SoundError::UnknownSoundName(name) => write!(f, "unknown sound name: {}", name), SoundError::RodioStream(e) => write!(f, "rodio stream error: {}", e), SoundError::RodioPlay(e) => write!(f, "rodio play error: {}", e), } } } impl std::error::Error for SoundError {} /// Play the requested sound, sleeps for its duration (until interrupted) pub fn play_sound( psc: &PlaySoundCommand, interrupt: Receiver<()>, ) -> Result<(), SoundError> { debug!("play sound: {:#?}", psc); let Sound { bytes, duration } = get_sound(psc.name.as_deref())?; let (_stream, stream_handle) = OutputStream::try_default()?; let sound = Cursor::new(bytes); let sink = stream_handle.play_once(sound)?; sink.set_volume(psc.volume.as_part()); if interrupt.recv_timeout(duration).is_ok() { info!("sound interrupted"); Err(SoundError::Interrupted) } else { Ok(()) } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/sound/sound_config.rs��������������������������������������������������������������0000644�0000000�0000000�00000001221�10461020230�0015727�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, serde::Deserialize, }; #[derive(Debug, Clone, Default, Deserialize, PartialEq)] pub struct SoundConfig { pub enabled: Option<bool>, pub base_volume: Option<Volume>, } impl SoundConfig { pub fn apply( &mut self, sc: &SoundConfig, ) { if let Some(b) = sc.enabled { self.enabled = Some(b); } if let Some(bv) = sc.base_volume { self.base_volume = Some(bv); } } pub fn is_enabled(&self) -> bool { self.enabled.unwrap_or(false) } pub fn get_base_volume(&self) -> Volume { self.base_volume.unwrap_or_default() } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/sound/sound_player.rs��������������������������������������������������������������0000644�0000000�0000000�00000006573�10461020230�0015775�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { super::*, std::thread, termimad::crossbeam::channel::{ self, Sender, select, }, }; /// The maximum number of sounds that can be queued, as we don't want /// a long list of sounds for events that are triggered in a loop /// (on quit, the queue will be interrupted anyway). const MAX_QUEUED_SOUNDS: usize = 2; /// Manage a thread to play sounds without blocking bacon pub struct SoundPlayer { thread: Option<thread::JoinHandle<()>>, s_die: Option<Sender<()>>, s_sound: Sender<PlaySoundCommand>, } impl SoundPlayer { pub fn new(base_volume: Volume) -> anyhow::Result<Self> { let (s_sound, r_sound) = channel::bounded::<PlaySoundCommand>(MAX_QUEUED_SOUNDS); let (s_die, r_die) = channel::bounded(1); let thread = thread::spawn(move || { loop { select! { recv(r_die) -> _ => { info!("sound player thread is stopping"); break; } recv(r_sound) -> ps => { match ps { Ok(mut ps) => { if !r_die.is_empty() { continue; } ps.volume = ps.volume * base_volume; match play_sound(&ps, r_die.clone()) { Ok(()) => { debug!("sound played"); } Err(SoundError::Interrupted) => { // only reason is sound player is dying info!("sound interrupted"); break; } Err(e) => { error!("sound error: {}", e); } } } Err(e) => { error!("sound player channel error: {}", e); break; } } } } } }); Ok(Self { thread: Some(thread), s_die: Some(s_die), s_sound, }) } /// Requests a sound, unless too many of them are already queued pub fn play( &self, sound_command: PlaySoundCommand, ) { if self.s_sound.try_send(sound_command).is_err() { warn!("Too many sounds in the queue, dropping one"); } } /// Make the beeper thread synchronously stop /// (interrupting the current sound if any) pub fn die(&mut self) { if let Some(sender) = self.s_die.take() { if let Err(e) = sender.send(()) { warn!("failed to send 'kill' signal: {e}"); } } if let Some(thread) = self.thread.take() { if thread.join().is_err() { warn!("child_thread.join() failed"); // should not happen } else { info!("SoundPlayer gracefully stopped"); } } } } impl Drop for SoundPlayer { fn drop(&mut self) { self.die(); } } �������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/sound/volume.rs��������������������������������������������������������������������0000644�0000000�0000000�00000003624�10461020230�0014572�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { serde::{ Deserialize, Deserializer, Serialize, Serializer, de, }, std::{ ops::Mul, str::FromStr, }, }; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Volume { /// Volume in [0, 100] percent: u16, // u16 ensures we can multiply it without overflow } impl Default for Volume { fn default() -> Self { Self { percent: 100 } } } impl Mul for Volume { type Output = Self; fn mul( self, rhs: Self, ) -> Self { Self { percent: self.percent * rhs.percent / 100, } } } impl Volume { pub fn new(percent: u16) -> Self { Self { percent: percent.clamp(0, 100), } } /// Return the volume in [0, 100] pub fn as_percent(self) -> u16 { self.percent } /// Return the volume in [0, 1] pub fn as_part(self) -> f32 { self.percent as f32 / 100f32 } } impl FromStr for Volume { type Err = &'static str; fn from_str(s: &str) -> Result<Self, Self::Err> { let s = s.trim_end_matches('%'); let percent: u16 = s.parse().map_err(|_| "number in 0-100 expected")?; Ok(Self { percent: percent.clamp(0, 100), }) } } impl std::fmt::Display for Volume { fn fmt( &self, f: &mut std::fmt::Formatter<'_>, ) -> std::fmt::Result { write!(f, "{}%", self.percent) } } impl Serialize for Volume { fn serialize<S>( &self, serializer: S, ) -> Result<S::Ok, S::Error> where S: Serializer, { serializer.collect_str(self) } } impl<'de> Deserialize<'de> for Volume { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; Self::from_str(&s).map_err(de::Error::custom) } } ������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/tty/mod.rs�������������������������������������������������������������������������0000644�0000000�0000000�00000002475�10461020230�0013535�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mod tline; mod tline_builder; mod trange; mod tstring; pub const CSI_RESET: &str = "\u{1b}[0m\u{1b}[0m"; pub const CSI_BOLD: &str = "\u{1b}[1m"; pub const CSI_ITALIC: &str = "\u{1b}[3m"; pub const CSI_GREEN: &str = "\u{1b}[32m"; pub const CSI_RED: &str = "\u{1b}[31m"; pub const CSI_BOLD_RED: &str = "\u{1b}[1m\u{1b}[38;5;9m"; pub const CSI_BOLD_ORANGE: &str = "\u{1b}[1m\u{1b}[38;5;208m"; /// Used for "Blocking" pub const CSI_BLUE: &str = "\u{1b}[1m\u{1b}[36m"; #[cfg(windows)] pub const CSI_BOLD_YELLOW: &str = "\u{1b}[1m\u{1b}[38;5;11m"; #[cfg(not(windows))] pub const CSI_BOLD_YELLOW: &str = "\u{1b}[1m\u{1b}[33m"; #[cfg(windows)] pub const CSI_BOLD_BLUE: &str = "\u{1b}[1m\u{1b}[38;5;14m"; #[cfg(not(windows))] pub const CSI_BOLD_BLUE: &str = "\u{1b}[1m\u{1b}[38;5;12m"; #[cfg(windows)] pub const CSI_BOLD_4BIT_YELLOW: &str = "\u{1b}[1m\u{1b}[33m"; #[cfg(windows)] pub const CSI_BOLD_WHITE: &str = "\u{1b}[1m\u{1b}[38;5;15m"; static TAB_REPLACEMENT: &str = " "; use { crate::W, anyhow::Result, std::io::Write, }; pub use { tline::*, tline_builder::*, trange::*, tstring::*, }; pub fn draw( w: &mut W, csi: &str, raw: &str, ) -> Result<()> { if csi.is_empty() { write!(w, "{}", raw)?; } else { write!(w, "{}{}{}", csi, raw, CSI_RESET,)?; } Ok(()) } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/tty/tline.rs�����������������������������������������������������������������������0000644�0000000�0000000�00000012263�10461020230�0014065�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { super::*, crate::*, anyhow::*, serde::{ Deserialize, Serialize, }, }; /// a simple representation of a line made of homogeneous parts. /// /// Note that this only manages CSI and SGR components /// and isn't a suitable representation for an arbitrary /// terminal input or output. /// I recommend you to NOT try to reuse this hack in another /// project unless you perfectly understand it. #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TLine { pub strings: Vec<TString>, } impl TLine { pub fn change_range_style( &mut self, trange: TRange, new_csi: String, ) { let TRange { mut string_idx, start_byte_in_string, end_byte_in_string, } = trange; if string_idx >= self.strings.len() { return; } let has_before = start_byte_in_string > 0; let has_after = end_byte_in_string < self.strings[string_idx].raw.len(); if has_after { self.strings.insert( string_idx + 1, TString { csi: self.strings[string_idx].csi.clone(), raw: self.strings[string_idx].raw[end_byte_in_string..].to_string(), }, ); } if has_before { self.strings.insert( string_idx, TString { csi: self.strings[string_idx].csi.clone(), raw: self.strings[string_idx].raw[..start_byte_in_string].to_string(), }, ); string_idx += 1; } self.strings[string_idx].csi = new_csi; if has_before { self.strings[string_idx].raw = self.strings[string_idx].raw[start_byte_in_string..end_byte_in_string].to_string(); } else { // we can just truncate the string self.strings[string_idx].raw.truncate(end_byte_in_string); } } pub fn from_tty(tty: &str) -> Self { let tty_str: String; let tty = if tty.contains('\t') { tty_str = tty.replace('\t', TAB_REPLACEMENT); &tty_str } else { tty }; let mut builder = TLineBuilder::default(); builder.read(tty); builder.build() } pub fn from_raw(raw: String) -> Self { Self { strings: vec![TString { csi: " ".to_string(), raw, }], } } pub fn to_raw(&self) -> String { let mut s = String::new(); for ts in &self.strings { s.push_str(&ts.raw); } s } pub fn bold(raw: String) -> Self { Self { strings: vec![TString { csi: CSI_BOLD.to_string(), raw, }], } } pub fn italic(raw: String) -> Self { Self { strings: vec![TString { csi: CSI_ITALIC.to_string(), raw, }], } } pub fn failed(key: &str) -> Self { let mut strings = Vec::with_capacity(4); strings.push(TString::new(CSI_BOLD_ORANGE, "failed")); strings.push(TString::new("", ": ")); if let Some((module, function)) = key.rsplit_once("::") { strings.push(TString { csi: "".to_string(), raw: format!("{module}::"), }); strings.push(TString::new(CSI_BOLD_ORANGE, function)); } else { strings.push(TString::new(CSI_BOLD_ORANGE, key)); } TLine { strings } } pub fn add_badge( &mut self, badge: TString, ) { self.strings.push(badge); self.strings.push(TString { csi: "".to_string(), raw: " ".to_string(), }); } pub fn add_tstring<C: Into<String>, R: Into<String>>( &mut self, csi: C, raw: R, ) { self.strings.push(TString { csi: csi.into(), raw: raw.into(), }); } pub fn draw( &self, w: &mut W, ) -> Result<()> { for ts in &self.strings { ts.draw(w)?; } Ok(()) } /// draw the line but without taking more than cols_max cols. /// Return the number of cols written pub fn draw_in( &self, w: &mut W, cols_max: usize, ) -> Result<usize> { let mut cols = 0; for ts in &self.strings { if cols >= cols_max { break; } cols += ts.draw_in(w, cols_max - cols)?; } Ok(cols) } pub fn is_blank(&self) -> bool { self.strings.iter().all(|s| s.raw.trim().is_empty()) } // if this line has no style, return its content pub fn if_unstyled(&self) -> Option<&str> { if self.strings.len() == 1 { self.strings .get(0) .filter(|s| s.csi.is_empty()) .map(|s| s.raw.as_str()) } else { None } } pub fn has( &self, part: &str, ) -> bool { self.strings.iter().any(|s| s.raw.contains(part)) } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/tty/tline_builder.rs���������������������������������������������������������������0000644�0000000�0000000�00000004364�10461020230�0015576�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use super::*; /// A builder consuming a string assumed to contain TTY sequences and building a TLine. #[derive(Debug, Default)] pub struct TLineBuilder { cur: Option<TString>, strings: Vec<TString>, } impl TLineBuilder { pub fn read( &mut self, s: &str, ) { let mut parser = vte::Parser::new(); parser.advance(self, s.as_bytes()); } pub fn build(mut self) -> TLine { self.take_tstring(); TLine { strings: self.strings, } } fn take_tstring(&mut self) { if let Some(cur) = self.cur.take() { self.push_tstring(cur); } } fn push_tstring( &mut self, tstring: TString, ) { if let Some(last) = self.strings.last_mut() { if last.csi == tstring.csi { last.raw.push_str(&tstring.raw); return; } } self.strings.push(tstring); } } impl vte::Perform for TLineBuilder { fn print( &mut self, c: char, ) { self.cur.get_or_insert_with(TString::default).raw.push(c); } fn csi_dispatch( &mut self, params: &vte::Params, _intermediates: &[u8], _ignore: bool, action: char, ) { if params.len() == 1 && params.iter().next() == Some(&[0]) { self.take_tstring(); return; } if let Some(cur) = self.cur.as_mut() { if cur.raw.is_empty() { cur.push_csi(params, action); return; } } self.take_tstring(); let mut cur = TString::default(); cur.push_csi(params, action); self.cur = Some(cur); } fn execute( &mut self, _byte: u8, ) { } fn hook( &mut self, _params: &vte::Params, _intermediates: &[u8], _ignore: bool, _action: char, ) { } fn put( &mut self, _byte: u8, ) { } fn unhook(&mut self) {} fn osc_dispatch( &mut self, _params: &[&[u8]], _bell_terminated: bool, ) { } fn esc_dispatch( &mut self, _intermediates: &[u8], _ignore: bool, _byte: u8, ) { } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/tty/trange.rs����������������������������������������������������������������������0000644�0000000�0000000�00000000300�10461020230�0014217�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������/// A position in a tline #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TRange { pub string_idx: usize, pub start_byte_in_string: usize, pub end_byte_in_string: usize, } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/tty/tstring.rs���������������������������������������������������������������������0000644�0000000�0000000�00000006176�10461020230�0014452�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { super::*, crate::*, anyhow::*, serde::{ Deserialize, Serialize, }, std::{ fmt::Write as _, io::Write, }, termimad::StrFit, }; /// a simple representation of a colored and styled string. /// /// Note that this works because of a few properties of /// cargo's output: /// - styles and colors are always reset on changes /// - they're always in the same order (bold then fg color) /// /// A more generic parsing would have to: /// - parse the csi params (it's simple enough to map but takes code) /// - use a simple state machine to keep style (bold, italic, etc.), /// foreground color, and background color across tstrings #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct TString { pub csi: String, pub raw: String, } impl TString { pub fn new<S1: Into<String>, S2: Into<String>>( csi: S1, raw: S2, ) -> Self { Self { csi: csi.into(), raw: raw.into(), } } /// colors are 8bits ansi values pub fn badge( con: &str, fg: u8, bg: u8, ) -> Self { Self { csi: format!("\u{1b}[1m\u{1b}[38;5;{}m\u{1b}[48;5;{}m", fg, bg), raw: format!(" {} ", con), } } pub fn num_badge( num: usize, cat: &str, fg: u8, bg: u8, ) -> Self { let raw = if num < 2 { format!(" {} {} ", num, cat) } else { format!(" {} {}s ", num, cat) }; Self::badge(&raw, fg, bg) } pub fn push_csi( &mut self, params: &vte::Params, action: char, ) { self.csi.push('\u{1b}'); self.csi.push('['); for (idx, param) in params.iter().enumerate() { for p in param { let _ = write!(self.csi, "{}", p); } if idx < params.len() - 1 { self.csi.push(';'); } } self.csi.push(action); } pub fn draw( &self, w: &mut W, ) -> Result<()> { draw(w, &self.csi, &self.raw) } /// draw the string but without taking more than cols_max cols. /// Return the number of cols written pub fn draw_in( &self, w: &mut W, cols_max: usize, ) -> Result<usize> { let fit = StrFit::make_cow(&self.raw, cols_max); if self.csi.is_empty() { write!(w, "{}", &fit.0)?; } else { write!(w, "{}{}{}", &self.csi, &fit.0, CSI_RESET)?; } Ok(fit.1) } pub fn starts_with( &self, csi: &str, raw: &str, ) -> bool { self.csi == csi && self.raw.starts_with(raw) } pub fn split_off( &mut self, at: usize, ) -> Self { Self { csi: self.csi.clone(), raw: self.raw.split_off(at), } } pub fn is_blank(&self) -> bool { self.raw.chars().all(char::is_whitespace) } pub fn is_styled(&self) -> bool { !self.csi.is_empty() } pub fn is_unstyled(&self) -> bool { self.csi.is_empty() } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/tui/app.rs�������������������������������������������������������������������������0000644�0000000�0000000�00000036765�10461020230�0013530�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::Result, crokey::*, std::{ io::Write, time::Duration, }, termimad::{ EventSource, EventSourceOptions, Ticker, crossbeam::channel::select, crossterm::event::Event, }, }; #[cfg(windows)] use { crokey::key, termimad::crossterm::event::{ MouseEvent, MouseEventKind, }, }; enum DoAfterMission { NextJob(JobRef), ReloadConfig, Quit, } impl From<JobRef> for DoAfterMission { fn from(job_ref: JobRef) -> Self { Self::NextJob(job_ref) } } /// Run the application until the user quits pub fn run( w: &mut W, mut settings: Settings, args: &Args, location: Context, headless: bool, ) -> Result<()> { let event_source = if headless { // in headless mode, in some contexts, ctrl-c might not be enough to kill // bacon so we add this handler ctrlc::set_handler(move || { eprintln!("bye"); std::process::exit(0); }) .expect("Error setting Ctrl-C handler"); None } else { Some(EventSource::with_options(EventSourceOptions { combine_keys: false, ..Default::default() })?) }; let mut job_stack = JobStack::default(); let mut next_job = JobRef::Initial; let mut message = None; loop { let Some((concrete_job_ref, job)) = job_stack.pick_job(&next_job, &settings)? else { break; }; let mission = location.mission(concrete_job_ref, &job, &settings)?; let do_after = app::run_mission(w, mission, event_source.as_ref(), message.take(), headless)?; match do_after { DoAfterMission::NextJob(job_ref) => { next_job = job_ref; } DoAfterMission::ReloadConfig => match Settings::read(args, &location) { Ok(new_settings) => { settings = new_settings; message = Some(Message::short("Config reloaded")); } Err(e) => { message = Some(Message::short(format!("Invalid config: {e}"))); } }, DoAfterMission::Quit => { break; } } } Ok(()) } /// Run the mission and return what to do afterwards fn run_mission( w: &mut W, mission: Mission, event_source: Option<&EventSource>, message: Option<Message>, headless: bool, ) -> Result<DoAfterMission> { let keybindings = mission.settings.keybindings.clone(); let grace_period = mission.job.grace_period(); let sound_player = mission.sound_player_if_needed(); // build the watcher detecting and transmitting mission file changes let ignorer = time!(Info, mission.ignorer()); let mission_watcher = Watcher::new(&mission.paths_to_watch, ignorer)?; // create the watcher for config file changes let config_watcher = Watcher::new(&mission.settings.config_files, IgnorerSet::default())?; // create the executor, mission, and state let mut executor = MissionExecutor::new(&mission)?; let on_change_strategy = mission.job.on_change_strategy(); let mut state = AppState::new(mission, headless)?; if let Some(message) = message { state.messages.push(message); } state.computation_starts(); if !headless { state.draw(w)?; } let mut task_executor = executor.start(state.new_task())?; // first computation // A very low frequency tick generator, to ensure "config loaded" message doesn't stick // too long on the screen let mut ticker = Ticker::new(); ticker.tick_infinitely((), Duration::from_secs(5)); let _dummy_sender; let user_events = if let Some(event_source) = event_source { event_source.receiver() } else { let (sender, receiver) = termimad::crossbeam::channel::unbounded(); _dummy_sender = sender; receiver }; let mut mission_end = None; // loop on events #[allow(unused_mut)] loop { // The actions to execute in response to the event. // While it's a vec, action execution will stop at the first one quitting the // mission or requesting a task execution, and the rest of the vec will be dropped. let mut actions: Vec<Action> = Vec::new(); select! { recv(ticker.tick_receiver) -> _ => { // just redraw } recv(mission_watcher.receiver) -> _ => { debug!("watch event received"); if task_executor.is_in_grace_period() { debug!("ignoring notify event in grace period"); continue; } state.receive_watch_event(); if state.auto_refresh.is_enabled() { if !state.is_computing() || on_change_strategy == OnChangeStrategy::KillThenRestart { actions.push(Action::Internal(Internal::ReRun)); } } } recv(config_watcher.receiver) -> _ => { info!("config watch event received"); grace_period.sleep(); // Fix #310 actions.push(Action::Internal(Internal::ReloadConfig)); } recv(executor.line_receiver) -> info => { if let Ok(info) = info { match info { CommandExecInfo::Line(line) => { if headless { match line.origin { CommandStream::StdOut => print!("{}", line.content), CommandStream::StdErr => eprint!("{}", line.content), } } let line = line.into(); state.add_line(line); } CommandExecInfo::End { status } => { // computation finished info!("execution finished with status: {:?}", status); state.finish_task(status)?; if headless { for badge in state.job_badges() { badge.draw(w)?; } writeln!(w)?; w.flush()?; } if state.is_success() { if let Some(action) = &state.mission.job.on_success { actions.push(action.clone()); } } if state.is_failure() { if let Some(action) = &state.mission.job.on_failure { actions.push(action.clone()); } } if state.changes_since_last_job_start > 0 && state.auto_refresh.is_enabled() { // will be ignored if a on_success or on_failures ends the mission // or does a rerun already actions.push(Action::Internal(Internal::ReRun)) } } CommandExecInfo::Error(e) => { state.computation_stops(); return Err(e.context(format!("error in computation for job '{}'", state.mission.concrete_job_ref.badge_label()))); } CommandExecInfo::Interruption => { debug!("command was interrupted (by us)"); } } } } recv(user_events) -> user_event => { match user_event?.event { Event::Resize(mut width, mut height) => { state.resize(width, height); } Event::Key(key_event) => { let key_combination = KeyCombination::from(key_event); debug!("key combination pressed: {}", key_combination); if !state.apply_key_combination(key_combination) { let action = keybindings.get(key_combination); if let Some(action) = action { actions.push(action.clone()); } } } #[cfg(windows)] Event::Mouse(MouseEvent { kind: MouseEventKind::ScrollDown, .. }) => { let action = keybindings.get(key!(down)); if let Some(action) = action { actions.push(action.clone()); } } #[cfg(windows)] Event::Mouse(MouseEvent { kind: MouseEventKind::ScrollUp, .. }) => { let action = keybindings.get(key!(up)); if let Some(action) = action { actions.push(action.clone()); } } _ => {} } if let Some(event_source) = event_source { event_source.unblock(false); } } } for action in actions.drain(..) { info!("requested action: {action:?}"); match action { Action::Export(export_name) => { let export_name = export_name.to_string(); state .mission .settings .exports .do_named_export(&export_name, &state); state .messages .push(Message::short(format!("Export *{}* done", &export_name))); } Action::Internal(internal) => match internal { Internal::Back => { if !state.back() { mission_end = Some(DoAfterMission::NextJob(JobRef::Previous)); } } Internal::BackOrQuit => { if !state.back() { mission_end = Some(DoAfterMission::NextJob(JobRef::PreviousOrQuit)); } } Internal::CopyUnstyledOutput => { state.copy_unstyled_output(); } Internal::NextMatch => { state.next_match(); } Internal::PreviousMatch => { state.previous_match(); } Internal::FocusSearch => { state.focus_search(); } Internal::FocusGoto => { state.focus_goto(); } Internal::Help => { state.toggle_help(); } Internal::NoOp => {} Internal::Pause => { state.auto_refresh = AutoRefresh::Paused; } Internal::PlaySound(play_sound_command) => { if let Some(sound_player) = &sound_player { sound_player.play(play_sound_command.clone()); } else { debug!("sound not enabled"); } } Internal::Quit => { mission_end = Some(DoAfterMission::Quit); break; } Internal::ReRun => { task_executor.die(); task_executor = state.start_computation(&mut executor)?; break; // drop following actions } Internal::Refresh => { state.clear(); task_executor.die(); task_executor = state.start_computation(&mut executor)?; break; // drop following actions } Internal::ReloadConfig => { mission_end = Some(DoAfterMission::ReloadConfig); break; } Internal::ScopeToFailures => { if let Some(scope) = state.failures_scope() { info!("scoping to failures: {scope:#?}"); mission_end = Some(JobRef::from(scope).into()); break; } else { warn!("no available failures scope"); } } Internal::Scroll(scroll_command) => { state.apply_scroll_command(scroll_command); } Internal::ToggleBacktrace(level) => { state.toggle_backtrace(level); task_executor.die(); task_executor = state.start_computation(&mut executor)?; break; // drop following actions } Internal::TogglePause => match state.auto_refresh { AutoRefresh::Enabled => { state.auto_refresh = AutoRefresh::Paused; } AutoRefresh::Paused => { if state.changes_since_last_job_start > 0 { state.clear(); task_executor.die(); task_executor = state.start_computation(&mut executor)?; break; // drop following actions } state.auto_refresh = AutoRefresh::Enabled; } }, Internal::ToggleRawOutput => { state.toggle_raw_output(); } Internal::ToggleSummary => { state.toggle_summary_mode(); } Internal::ToggleWrap => { state.toggle_wrap_mode(); } Internal::Unpause => { if state.changes_since_last_job_start > 0 { state.clear(); task_executor.die(); task_executor = state.start_computation(&mut executor)?; break; // drop following actions } state.auto_refresh = AutoRefresh::Enabled; } Internal::Validate => { state.validate(); } }, Action::Job(job_ref) => { mission_end = Some(job_ref.clone().into()); break; } } } if !headless { state.draw(w)?; } if let Some(mission_end) = mission_end { task_executor.die(); return Ok(mission_end); } } } �����������bacon-3.12.0/src/tui/app_state.rs�������������������������������������������������������������������0000644�0000000�0000000�00000071736�10461020230�0014725�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::Result, crokey::KeyCombination, std::{ io::Write, process::ExitStatus, time::Instant, }, termimad::{ Area, CompoundStyle, MadSkin, crossterm::{ cursor, execute, style::{ Attribute, Color::*, Print, }, }, minimad::{ Alignment, Composite, }, }, }; /// Currently rendered state of the TUI application pub struct AppState<'s> { /// the mission to run, with settings pub mission: Mission<'s>, report_maker: ReportMaker, /// the lines of a computation in progress output: Option<CommandOutput>, /// wrapped output for the width of the console wrapped_output: Option<WrappedCommandOutput>, /// result of a command, hopefully a report pub cmd_result: CommandResult, /// a report wrapped for the size of the console wrapped_report: Option<WrappedReport>, /// screen width width: u16, /// screen height height: u16, /// whether a computation is in progress computing: bool, /// whether the user wants wrapped lines pub wrap: bool, /// the optional RUST_BACKTRACE env var to set pub backtrace: Option<&'static str>, /// whether we should display only titles and locations summary: bool, /// whether we display the gui bottom-to-top reverse: bool, /// colors and styles used for status bar status_skin: MadSkin, /// number of lines hidden on top due to scroll scroll: usize, /// item_idx of the item which was on top on last draw top_item_idx: usize, /// the tool building the help line help_line: Option<HelpLine>, /// the help page displayed over the rest, if any help_page: Option<HelpPage>, /// display the raw output instead of the report raw_output: bool, /// whether auto-refresh is enabled pub auto_refresh: AutoRefresh, /// How many watch events were received since last job start pub changes_since_last_job_start: usize, /// whether to display the count of changes pub show_changes_count: bool, /// messages to display to the user for a short duration pub messages: Vec<Message>, /// the search state pub search: SearchState, } impl<'s> AppState<'s> { pub fn new( mission: Mission<'s>, headless: bool, ) -> Result<Self> { let report_maker = ReportMaker::new(&mission); let mut status_skin = MadSkin::default(); status_skin .paragraph .set_fgbg(AnsiValue(252), AnsiValue(239)); status_skin.italic = CompoundStyle::new(Some(AnsiValue(204)), None, Attribute::Bold.into()); let (width, height) = if headless { (50, 50) } else { termimad::terminal_size() }; let help_line = mission .settings .help_line .then(|| HelpLine::new(mission.settings)); Ok(Self { report_maker, output: None, wrapped_output: None, cmd_result: CommandResult::None, wrapped_report: None, width, height, computing: true, summary: mission.settings.summary, wrap: mission.settings.wrap, backtrace: None, reverse: mission.settings.reverse, show_changes_count: mission.job.show_changes_count(), status_skin, scroll: 0, top_item_idx: 0, help_line, help_page: None, mission, raw_output: false, auto_refresh: AutoRefresh::Enabled, changes_since_last_job_start: 0, messages: Vec::new(), search: Default::default(), }) } pub fn focus_search(&mut self) { self.search.focus_with_mode(SearchMode::Pattern); self.show_selected_found(); } pub fn focus_goto(&mut self) { self.search.focus_with_mode(SearchMode::ItemIdx); self.show_selected_found(); } // Handle the "back" operation, return true if it did (thus consuming the action) pub fn back(&mut self) -> bool { if self.search.focused() { self.search.unfocus_and_clear(); true } else if self.help_page.is_some() { self.help_page = None; true } else if self.search.input_has_content() { self.search.clear(); true } else { false } } pub fn copy_unstyled_output(&mut self) { let message = { #[cfg(feature = "clipboard")] match arboard::Clipboard::new() { Ok(mut clipboard) => { let mut content = String::new(); for line in self.lines_to_draw() { content.push_str(&line.content.to_raw()); content.push('\n'); } let _ = clipboard.set_text(content); "Output copied to clipboard" } Err(e) => { error!("Failed to copy output to clipboard: {}", e); "Clipboard error - nothing copied" } } #[cfg(not(feature = "clipboard"))] "clipboard feature not enabled : nothing copied" }; self.messages.push(Message::short(message)); } pub fn next_match(&mut self) { self.search.next_match(); self.show_selected_found(); } pub fn previous_match(&mut self) { self.search.previous_match(); self.show_selected_found(); } // Handle the "validate" operation, return true if it did (thus consuming the action) pub fn validate(&mut self) -> bool { if self.search.focused() { self.search.unfocus(); true } else { false } } pub fn has_search(&self) -> bool { self.search.focused() || self.search.input_has_content() } /// handle a raw, uninterpreted key combination (in an input if there's one /// focused), return true if the key was consumed (if not, keybindings will /// be computed) pub fn apply_key_combination( &mut self, key: KeyCombination, ) -> bool { if self.search.apply_key_combination(key) { self.update_search(); self.show_selected_found(); return true; } false } pub fn update_search(&mut self) { if self.search.is_up_to_date() { return; } let founds = if self.search.input_has_content() { let search = self.search.search(); let lines = self.lines_to_draw(); search.search_lines(lines) } else { Vec::new() }; self.search.set_founds(founds); } /// Do a partial update for some potential added lines pub fn update_search_from_line( &mut self, line_count_before: usize, ) { if !self.search.input_has_content() { return; } // it's probably fine now to search without taking filtering into // account as we're only adding lines in the raw output where there's // no filtering let lines = self.lines_to_draw_unfiltered(); let search = self.search.search(); let new_founds = search.search_lines(&lines[line_count_before..]); self.search.extend_founds(new_founds); } pub fn add_line( &mut self, line: CommandOutputLine, ) { let auto_scroll = self.is_scroll_at_bottom(); let line_count_before = self.lines_to_draw_unfiltered().len(); if let Some(output) = self.output.as_mut() { self.report_maker.receive_line(line, output); if self.wrap { self.update_wrap(self.width - 1); } if auto_scroll { // if the user never scrolled, we'll stick to the bottom self.scroll_to_bottom(); } } else { self.wrapped_output = None; self.output = { let mut output = CommandOutput::default(); self.report_maker.receive_line(line, &mut output); Some(output) }; self.scroll = 0; self.fix_scroll(); } self.update_search_from_line(line_count_before); } pub fn new_task(&self) -> Task { Task { backtrace: self.backtrace, grace_period: self.mission.job.grace_period(), } } pub fn take_output(&mut self) -> Option<CommandOutput> { self.search.touch(); self.wrapped_output = None; self.output.take() } pub fn has_report(&self) -> bool { matches!(self.cmd_result, CommandResult::Report(_)) } pub fn can_be_scoped(&self) -> bool { self.cmd_result .report() .is_some_and(|report| report.stats.can_scope_tests()) } pub fn failures_scope(&self) -> Option<Scope> { if !self.can_be_scoped() { return None; } self.cmd_result.report().map(|report| Scope { tests: report.failure_keys.clone(), }) } pub fn toggle_raw_output(&mut self) { self.raw_output ^= true; if self.wrapped_output.is_some() { self.wrapped_output = None; } if self.wrap { self.update_wrap(self.width - 1); } self.search.touch(); } pub fn finish_task( &mut self, exit_status: Option<ExitStatus>, ) -> Result<()> { let output = self.take_output().unwrap_or_default(); let result = self.report_maker.build_result(output, exit_status)?; self.set_result(result); Ok(()) } fn set_result( &mut self, mut cmd_result: CommandResult, ) { self.search.touch(); if self.reverse { cmd_result.reverse(); } match &cmd_result { CommandResult::Report(report) => { debug!("Got report"); info!("Stats: {:#?}", report.stats); } CommandResult::Failure(_) => { debug!("Got failure"); } CommandResult::None => { debug!("GOT NONE ???"); } } if let CommandResult::Report(ref mut report) = cmd_result { // if the last line is empty, we remove it, to // avoid a useless empty line at the end if report .lines .last() .is_some_and(|line| line.content.is_blank()) { report.lines.pop(); } } // we keep the scroll when the number of lines didn't change let reset_scroll = self.cmd_result.lines_len() != cmd_result.lines_len(); self.wrapped_report = None; self.wrapped_output = None; self.cmd_result = cmd_result; self.computing = false; if reset_scroll { self.reset_scroll(); } self.raw_output = false; if self.wrap { self.update_wrap(self.width - 1); } // we do all exports which are set to auto self.mission.settings.exports.do_auto_exports(self); } pub fn is_computing(&self) -> bool { self.computing } pub fn clear(&mut self) { debug!("state.clear"); self.take_output(); self.cmd_result = CommandResult::None; self.search.touch(); } /// Start a new task on the current mission pub fn start_computation( &mut self, executor: &mut MissionExecutor, ) -> Result<TaskExecutor> { debug!("state.start_computation"); self.computation_starts(); executor.start(self.new_task()) } /// Called when a task has started pub fn computation_starts(&mut self) { if !self.mission.job.background() { self.clear(); } self.report_maker.start(&self.mission); self.computing = true; self.changes_since_last_job_start = 0; self.search.touch(); } pub fn computation_stops(&mut self) { self.computing = false; } pub fn receive_watch_event(&mut self) { self.changes_since_last_job_start += 1; } fn scroll_to_top(&mut self) { self.scroll = 0; self.top_item_idx = 0; } fn scroll_to_bottom(&mut self) { let ch = self.content_height(); let ph = self.page_height(); self.scroll = ch.saturating_sub(ph); // we don't set top_item_idx - does it matter? } fn is_scroll_at_bottom(&self) -> bool { self.scroll + self.page_height() + 1 >= self.content_height() } fn reset_scroll(&mut self) { if self.reverse { self.scroll_to_bottom(); } else { self.scroll_to_top(); } } fn fix_scroll(&mut self) { self.scroll = fix_scroll(self.scroll, self.content_height(), self.page_height()); } /// get the scroll value needed to go to the last item (if any) fn get_last_item_scroll(&self) -> usize { let lines = self.lines_to_draw(); for (row_idx, line) in lines.enumerate() { if line.item_idx == self.top_item_idx { return row_idx; } } 0 } pub fn keybindings(&self) -> &KeyBindings { &self.mission.settings.keybindings } fn try_scroll_to_last_top_item(&mut self) { self.scroll = self.get_last_item_scroll(); self.fix_scroll(); } fn show_line( &mut self, line_idx: usize, ) { let page_height = self.page_height(); if line_idx < self.scroll || line_idx >= self.scroll + page_height { self.scroll = (line_idx - (page_height / 2).min(line_idx)) .min(self.content_height().max(page_height) - page_height); } } fn show_selected_found(&mut self) { if let Some(selected_line) = self.search.selected_found_line() { self.show_line(selected_line); } } /// close the help and return true if it was open, /// return false otherwise pub fn close_help(&mut self) -> bool { if self.help_page.is_some() { self.help_page = None; true } else { false } } pub fn is_help(&self) -> bool { self.help_page.is_some() } pub fn toggle_help(&mut self) { self.help_page = match self.help_page { Some(_) => None, None => Some(HelpPage::new(self.mission.settings)), }; } pub fn toggle_summary_mode(&mut self) { self.summary ^= true; self.try_scroll_to_last_top_item(); self.search.touch(); self.update_search(); self.show_selected_found(); } pub fn toggle_backtrace( &mut self, level: &'static str, ) { self.backtrace = if self.backtrace == Some(level) { None } else { Some(level) }; } pub fn toggle_wrap_mode(&mut self) { self.wrap ^= true; if self.wrapped_output.is_some() { self.wrapped_output = None; } if self.wrap { self.update_wrap(self.width - 1); } if self.wrapped_report.is_some() { self.try_scroll_to_last_top_item(); } self.search.touch(); } fn content_height(&self) -> usize { let lines = self.lines_to_draw(); lines.count() } fn page_height(&self) -> usize { self.height.max(3) as usize - 3 } pub fn resize( &mut self, width: u16, height: u16, ) { if self.width != width { self.wrapped_report = None; self.wrapped_output = None; } self.width = width; self.height = height; if self.wrap { self.update_wrap(self.width - 1); } self.try_scroll_to_last_top_item(); self.search.touch(); } pub fn apply_scroll_command( &mut self, cmd: ScrollCommand, ) { if let Some(help_page) = self.help_page.as_mut() { help_page.apply_scroll_command(cmd); } else { debug!("content_height: {}", self.content_height()); debug!("page_height: {}", self.page_height()); self.scroll = cmd.apply(self.scroll, self.content_height(), self.page_height()); } } /// draw the grey line containing the keybindings indications fn draw_status_line( &mut self, w: &mut W, y: u16, ) -> Result<()> { let mut help_start = 0; // Search input if self.search.must_be_drawn() { let search_width = (self.width / 4).clamp(9, 27); self.search.draw_prefixed_input(w, 0, y, search_width)?; help_start += search_width; } goto(w, help_start, y)?; // Help line if let Some(help_line) = &self.help_line { let markdown = help_line.markdown(self); if self.height > 1 { let help_width = self.width - help_start; self.status_skin.write_composite_fill( w, Composite::from_inline(&markdown), help_width.into(), Alignment::Left, )?; } } else { clear_line(w)?; } Ok(()) } pub fn job_badges(&self) -> Vec<TString> { let mut badges = Vec::new(); let project_name = &self.mission.location_name; badges.push(TString::badge(project_name, 255, 240)); let job_label = self.mission.concrete_job_ref.badge_label(); badges.push(TString::badge(&job_label, 235, 204)); if let CommandResult::Report(report) = &self.cmd_result { let stats = &report.stats; if stats.errors > 0 { badges.push(TString::num_badge(stats.errors, "error", 235, 9)); } if stats.test_fails > 0 { badges.push(TString::num_badge(stats.test_fails, "fail", 235, 208)); } else if stats.passed_tests > 0 { badges.push(TString::badge("pass!", 254, 2)); } if stats.warnings > 0 { badges.push(TString::num_badge(stats.warnings, "warning", 235, 11)); } } else if let CommandResult::Failure(failure) = &self.cmd_result { badges.push(TString::badge( &format!("Command error code: {}", failure.error_code), 235, 9, )); } badges } /// draw the line of colored badges, usually on top pub fn draw_badges( &mut self, w: &mut W, y: u16, ) -> Result<usize> { goto_line(w, y)?; let mut t_line = TLine::default(); for badge in self.job_badges() { t_line.add_badge(badge); } if self.show_changes_count { t_line.add_badge(TString::num_badge( self.changes_since_last_job_start, "change", 235, 6, )); } self.search.add_summary_tstring(&mut t_line); let width = self.width as usize; let cols = t_line.draw_in(w, width)?; clear_line(w)?; Ok(cols) } /// draw "computing...", the error code if any, or a blank line pub fn draw_computing( &mut self, w: &mut W, y: u16, ) -> Result<()> { goto_line(w, y)?; let width = self.width as usize; if self.computing { write!( w, "\u{1b}[38;5;235m\u{1b}[48;5;204m{:^w$}\u{1b}[0m", "computing...", w = width )?; } else { clear_line(w)?; } Ok(()) } /// draw message pub fn draw_message( &mut self, w: &mut W, y: u16, ) -> Result<()> { let Some(message) = self.messages.first_mut() else { return Ok(()); }; if let Some(start) = message.display_start { if start.elapsed() > message.display_duration { self.messages.remove(0); return Ok(()); } } else { message.display_start = Some(Instant::now()); } goto_line(w, y)?; let markdown = format!(" {}", message.markdown); self.status_skin.write_composite_fill( w, Composite::from_inline(&markdown), self.width.into(), Alignment::Left, )?; Ok(()) } pub fn is_success(&self) -> bool { match &self.cmd_result { CommandResult::Report(report) => self.mission.is_success(report), _ => false, } } pub fn is_failure(&self) -> bool { match &self.cmd_result { CommandResult::Report(report) => !self.mission.is_success(report), CommandResult::Failure(_) => true, CommandResult::None => false, } } /// Return the (unfiltered) set of lines to draw, depending on whether we wrap or not /// and whether we have a report or not. fn lines_to_draw_unfiltered(&self) -> &[Line] { if let Some(report) = self.report_to_draw() { match (self.wrap, self.wrapped_report.as_ref()) { (true, Some(wrapped_report)) => { // wrapped report &wrapped_report.sub_lines } _ => { // unwrapped report &report.lines } } } else if let Some(output) = self.cmd_result.output().or(self.output.as_ref()) { match (self.wrap, self.wrapped_output.as_ref()) { (true, Some(wrapped_output)) => { // wrapped raw command output &wrapped_output.sub_lines } _ => { // unwrapped raw command output &output.lines } } } else { // nothing yet &[] } } fn lines_to_draw(&self) -> impl Iterator<Item = &Line> { self.lines_to_draw_unfiltered().iter().filter(|line| { // if this command failed, always show the output matches!(self.cmd_result, CommandResult::Failure(..)) || line.matches(self.summary) }) } fn report_to_draw(&self) -> Option<&Report> { self.cmd_result .report() .filter(|_| !self.raw_output) .filter(|report| !self.mission.is_success(report)) } fn update_wrap( &mut self, width: u16, ) { if let Some(report) = self.report_to_draw() { if self.wrapped_report.is_none() { self.wrapped_report = Some(WrappedReport::new(report, width)); self.scroll = self.get_last_item_scroll(); } } else if let Some(output) = self.cmd_result.output().or(self.output.as_ref()) { match self.wrapped_output.as_mut() { None => { self.wrapped_output = Some(WrappedCommandOutput::new(output, width)); self.reset_scroll(); } Some(wo) => { wo.update(output, width); } } } } /// draw the report or the lines of the current computation, between /// y and self.page_height() pub fn draw_content( &mut self, w: &mut W, y: u16, ) -> Result<()> { if self.height < 4 { return Ok(()); } let area = Area::new(0, y, self.width - 1, self.page_height() as u16); let content_height = self.content_height(); let scrollbar = area.scrollbar(self.scroll, content_height); let mut top_item_idx = None; let top = if self.reverse && self.page_height() > content_height { self.page_height() - content_height } else { 0 }; let top = area.top + top as u16; for y in area.top..top { goto_line(w, y)?; clear_line(w)?; } let width = self.width as usize; let lines = self.lines_to_draw(); let mut lines = lines.enumerate().skip(self.scroll); let mut found_idx = 0; #[derive(Debug)] struct PendingContinuation { trange: TRange, style: &'static str, } let mut pending_continuation = None; for row_idx in 0..area.height { let y = row_idx + top; goto_line(w, y)?; if let Some((line_idx, line)) = lines.next() { top_item_idx.get_or_insert(line.item_idx); line.line_type.draw(w, line.item_idx)?; write!(w, " ")?; if width > line.line_type.cols() + 1 { let mut tline = &line.content; // search for the optional founds related to that line let mut line_founds = Vec::new(); let found_idx_before_line = found_idx; let founds = self.search.founds(); while found_idx < founds.len() { let found = &founds[found_idx]; if found.line_idx > line_idx { break; } if found.line_idx == line_idx { line_founds.push(found); } found_idx += 1; } // apply the modification on the tline let mut modified; let previous_continuation = pending_continuation.take(); if previous_continuation.is_some() || !line_founds.is_empty() { modified = tline.clone(); // We iterate on founds in reverse, so that we change the tline from // the end, so that the tstring index in the founds stay valid when // tstrings are added by the change_range_style method. for (in_line_idx, found) in line_founds.iter().enumerate().rev() { let cur_idx = found_idx_before_line + in_line_idx; let style = if self.search.selected_found() == cur_idx { CSI_FOUND_SELECTED } else { CSI_FOUND }; modified.change_range_style(found.trange, style.to_string()); if let Some(continued) = &found.continued { pending_continuation = Some(PendingContinuation { trange: *continued, style, }); info!("pending_continuation: {:?}", &pending_continuation); } } if let Some(continuation) = previous_continuation { info!("APPLY continuation {:#?}", &continuation); modified.change_range_style( continuation.trange, continuation.style.to_string(), ); info!(" -> modified: {:#?}", &modified); } tline = &modified; } tline.draw_in(w, width - 1 - line.line_type.cols())?; } } clear_line(w)?; if is_thumb(y.into(), scrollbar) { execute!(w, cursor::MoveTo(area.width, y), Print('▐'.to_string()))?; } } Ok(()) } /// draw the state on the whole terminal pub fn draw( &mut self, w: &mut W, ) -> Result<()> { self.update_search(); if self.reverse { self.draw_status_line(w, 0)?; if let Some(help_page) = self.help_page.as_mut() { help_page.draw(w, Area::new(0, 1, self.width, self.height - 1))?; } else { self.draw_content(w, 1)?; self.draw_computing(w, self.height - 2)?; self.draw_message(w, self.height - 2)?; self.draw_badges(w, self.height - 1)?; } } else { if let Some(help_page) = self.help_page.as_mut() { help_page.draw(w, Area::new(0, 0, self.width, self.height - 1))?; } else { self.draw_badges(w, 0)?; self.draw_computing(w, 1)?; self.draw_message(w, 1)?; // drawn over the "computing..." line self.draw_content(w, 2)?; } self.draw_status_line(w, self.height - 1)?; } w.flush()?; Ok(()) } } ����������������������������������bacon-3.12.0/src/tui/drawing.rs���������������������������������������������������������������������0000644�0000000�0000000�00000001216�10461020230�0014362�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::Result, termimad::crossterm::{ cursor, execute, terminal, }, }; /// Move the curstor to the x, y position pub fn goto( w: &mut W, x: u16, y: u16, ) -> Result<()> { execute!(w, cursor::MoveTo(x, y))?; Ok(()) } /// Move the curstor to the start of the provided line pub fn goto_line( w: &mut W, y: u16, ) -> Result<()> { execute!(w, cursor::MoveTo(0, y))?; Ok(()) } /// Clear from the current position to the end of the line pub fn clear_line(w: &mut W) -> Result<()> { execute!(w, terminal::Clear(terminal::ClearType::UntilNewLine))?; Ok(()) } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/tui/messages.rs��������������������������������������������������������������������0000644�0000000�0000000�00000001237�10461020230�0014541�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::time::{ Duration, Instant, }; /// A message to be displayed to the user, one line max pub struct Message { pub markdown: String, /// when the message was first displayed pub display_start: Option<Instant>, /// minimal duration to display the message pub display_duration: Duration, } impl Message { /// build a short message, typically to answer to a user action /// (thus when the user is looking at bacon) pub fn short<S: Into<String>>(markdown: S) -> Self { Self { markdown: markdown.into(), display_start: None, display_duration: Duration::from_secs(5), } } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/tui/mod.rs�������������������������������������������������������������������������0000644�0000000�0000000�00000000321�10461020230�0013502�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������pub mod app; mod app_state; mod drawing; mod messages; mod scroll; mod search_state; mod wrap; pub use { app_state::*, drawing::*, messages::*, scroll::*, search_state::*, wrap::*, }; ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/tui/scroll.rs����������������������������������������������������������������������0000644�0000000�0000000�00000014006�10461020230�0014226�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { lazy_regex::*, serde::{ Deserialize, Deserializer, Serialize, Serializer, de, }, std::{ fmt, str::FromStr, }, }; /// A scroll related command #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ScrollCommand { Top, Bottom, Lines(i32), MilliPages(i32), } impl ScrollCommand { pub fn pages(n: i32) -> Self { Self::MilliPages(n * 1000) } fn to_lines( self, content_height: usize, page_height: usize, ) -> i32 { match self { Self::Top => -(content_height as i32), Self::Bottom => content_height as i32, Self::Lines(n) => n, Self::MilliPages(n) => { let lines = n as f64 * page_height as f64 / 1000.0; let lines = if lines < 0.0 { lines.floor() } else { lines.ceil() }; lines as i32 } } } /// Return the action description to show in doc/help pub fn doc(&self) -> String { fn txt( n: i32, thing: &str, way: &str, ) -> String { let p = if n > 1 { "s" } else { "" }; format!("scroll {n} {thing}{p} {way}") } match self { Self::Top => "scroll to top".to_string(), Self::Bottom => "scroll to bottom".to_string(), Self::Lines(lines) => { if *lines > 0 { txt(*lines, "line", "down") } else { txt(-lines, "line", "up") } } Self::MilliPages(n) => { if n % 1000 == 0 { let pages = n / 1000; if pages > 0 { txt(pages, "page", "down") } else { txt(-pages, "page", "up") } } else { let pages: f64 = *n as f64 / 1000.0; let s = format!("{pages:.3}"); let s = s.trim_matches('0'); format!("scroll {s} pages") } } } } /// compute the new scroll value pub fn apply( self, scroll: usize, content_height: usize, page_height: usize, ) -> usize { if content_height > page_height { (scroll as i32 + self.to_lines(content_height, page_height)) .min((content_height - page_height) as i32) .max(0) as usize } else { 0 } } } impl fmt::Display for ScrollCommand { fn fmt( &self, f: &mut fmt::Formatter, ) -> fmt::Result { match self { Self::Top => write!(f, "scroll-to-top"), Self::Bottom => write!(f, "scroll-to-bottom"), Self::Lines(n) => write!(f, "scroll-lines({n})"), Self::MilliPages(n) => { if n % 1000 == 0 { let n = n / 1000; write!(f, "scroll-pages({n})") } else { let n: f64 = *n as f64 / 1000.0; let s = format!("{n:.3}"); let s = s.trim_matches('0'); write!(f, "scroll-pages({s})") } } } } } impl std::str::FromStr for ScrollCommand { type Err = &'static str; fn from_str(s: &str) -> Result<Self, Self::Err> { regex_switch!(s, "^scroll[-_]?to[-_]?top$"i => Self::Top, "^scroll[-_]?to[-_]?bottom$"i => Self::Bottom, r#"^scroll[-_]?lines?\((?<n>[+-]?\d{1,4})\)$"#i => Self::Lines( n.parse().unwrap() // can't fail because [+-]?\d{1,4} ), r#"^scroll[-_]?pages?\((?<n>[+-]?\d{1,4})\)$"#i => Self::MilliPages( n.parse::<i32>().unwrap() * 1000 // can't fail because [+-]?\d{1,4} ), r#"^scroll[-_]?pages?\((?<n>[+-]?\d*\.\d{1,3})\)$"#i => { let n: f64 = n.parse().unwrap(); // can't fail let n: i32 = (n * 1000.0).round() as i32; Self::MilliPages(n) } ) .ok_or("not a valid scroll command") } } impl Serialize for ScrollCommand { fn serialize<S>( &self, serializer: S, ) -> Result<S::Ok, S::Error> where S: Serializer, { serializer.collect_str(self) } } impl<'de> Deserialize<'de> for ScrollCommand { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; Self::from_str(&s).map_err(de::Error::custom) } } pub fn is_thumb( y: usize, scrollbar: Option<(u16, u16)>, ) -> bool { scrollbar.is_some_and(|(sctop, scbottom)| { let y = y as u16; sctop <= y && y <= scbottom }) } pub fn fix_scroll( scroll: usize, content_height: usize, page_height: usize, ) -> usize { if content_height > page_height { scroll.min(content_height - page_height - 1) } else { 0 } } #[test] fn test_scroll_command_string_round_trip() { let commands = [ ScrollCommand::Lines(3), ScrollCommand::Lines(-12), ScrollCommand::MilliPages(1000), ScrollCommand::MilliPages(-2000), ScrollCommand::Top, ScrollCommand::Bottom, ]; for command in commands { assert_eq!(command.to_string().parse(), Ok(command)); } } #[test] fn test_scroll_command_string_alternative_writings() { assert_eq!("SCROLL-TO-TOP".parse(), Ok(ScrollCommand::Top)); assert_eq!("ScrollLines(5)".parse(), Ok(ScrollCommand::Lines(5))); assert_eq!("scroll-lines(+12)".parse(), Ok(ScrollCommand::Lines(12))); assert_eq!( "scroll_pages(-2)".parse(), Ok(ScrollCommand::MilliPages(-2000)) ); assert_eq!( "scroll_pages(-.2)".parse(), Ok(ScrollCommand::MilliPages(-200)) ); } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/tui/search_state.rs����������������������������������������������������������������0000644�0000000�0000000�00000012752�10461020230�0015403�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::Result, crokey::KeyCombination, termimad::InputField, }; /// Search related state, part of the app state pub struct SearchState { mode: SearchMode, /// the search input field input: InputField, /// whether the app state is up to date with the search up_to_date: bool, /// Locations matching the search_input content founds: Vec<Found>, /// The selection to show (index among the founds) selected_found: usize, } impl Default for SearchState { fn default() -> Self { let mut search_input = InputField::default(); search_input.set_focus(false); let founds = Default::default(); Self { mode: SearchMode::Pattern, input: search_input, up_to_date: true, founds, selected_found: 0, } } } impl SearchState { pub fn is_up_to_date(&self) -> bool { self.up_to_date } /// tell the search state that it's not up to date with the app state pub fn touch(&mut self) { self.up_to_date = false; } pub fn selected_found(&self) -> usize { self.selected_found } pub fn has_founds(&self) -> bool { !self.founds.is_empty() } pub fn must_be_drawn(&self) -> bool { self.focused() || !self.input.is_empty() } pub fn focus_with_mode( &mut self, mode: SearchMode, ) { if mode != self.mode { self.input.clear(); } self.mode = mode; self.input.set_focus(true); } pub fn unfocus(&mut self) { self.input.set_focus(false); } pub fn focused(&self) -> bool { self.input.focused() } pub fn input_has_content(&self) -> bool { !self.input.is_empty() } pub fn unfocus_and_clear(&mut self) { self.input.clear(); self.up_to_date = false; self.input.set_focus(false); } pub fn clear(&mut self) { self.input.clear(); self.up_to_date = false; } pub fn next_match(&mut self) { if self.founds.is_empty() { return; } self.selected_found = (self.selected_found + 1) % self.founds.len(); } pub fn previous_match(&mut self) { if self.founds.is_empty() { return; } self.selected_found = (self.selected_found + self.founds.len() - 1) % self.founds.len(); } /// handle a raw, uninterpreted key combination (in an input if there's one /// focused), return true if the key was consumed (if not, keybindings will /// be computed) pub fn apply_key_combination( &mut self, key: KeyCombination, ) -> bool { if self.input.focused() { if self.input.apply_key_combination(key) { self.up_to_date = false; return true; } } false } pub fn search(&self) -> Search { match self.mode { SearchMode::Pattern => Search::Pattern(Pattern { pattern: self.input.get_content(), }), SearchMode::ItemIdx => Search::ItemIdx(self.input.get_content().parse().unwrap_or(0)), } } pub fn is_invalid(&self) -> bool { match self.mode { SearchMode::Pattern => false, SearchMode::ItemIdx => { if self.input.is_empty() { false } else { let n = self.input.get_content().parse::<usize>(); n.is_err() } } } } pub fn set_founds( &mut self, founds: Vec<Found>, ) { let old_selected_line = self.selected_found_line(); self.founds = founds; let new_selected_line = self.selected_found_line(); if old_selected_line != new_selected_line { self.selected_found = 0; } self.up_to_date = true; } pub fn extend_founds( &mut self, new_founds: Vec<Found>, ) { self.founds.extend(new_founds); } /// if there are search results, return the line index of the currently selected one pub fn selected_found_line(&self) -> Option<usize> { self.founds .get(self.selected_found) .map(|found| found.line_idx) } pub fn founds(&self) -> &[Found] { &self.founds } /// Draw at the given position, with the specified width pub fn draw_prefixed_input( &mut self, w: &mut W, x: u16, y: u16, width: u16, // must be > 1 ) -> Result<()> { goto_line(w, y)?; draw( w, CSI_FOUND, if self.mode == SearchMode::ItemIdx { ":" } else { "/" }, )?; self.input.change_area(x + 1, y, width - 1); self.input.display_on(w)?; Ok(()) } pub fn add_summary_tstring( &self, t_line: &mut TLine, ) { if self.input_has_content() { if self.founds.is_empty() { if self.mode == SearchMode::ItemIdx && self.is_invalid() { t_line.add_tstring(CSI_FOUND, "integer expected"); } else { t_line.add_tstring(CSI_FOUND, "no match"); } } else { t_line.add_tstring( CSI_FOUND, format!("{}/{}", self.selected_found + 1, self.founds.len(),), ); } } } } ����������������������bacon-3.12.0/src/tui/wrap.rs������������������������������������������������������������������������0000644�0000000�0000000�00000004374�10461020230�0013710�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, unicode_width::UnicodeWidthChar, }; /// Wrap lines into sublines containing positions in the original lines pub fn wrap( lines: &[Line], width: u16, ) -> Vec<Line> { let cols = width as usize - 1; // -1 for the probable scrollbar let mut sub_lines = Vec::new(); for line in lines.iter() { let summary = line.line_type.is_summary(); sub_lines.push(Line { item_idx: line.item_idx, content: TLine::default(), line_type: line.line_type, }); let mut sub_cols = line.line_type.cols(); let mut wrap_idx = 0; // 1 for first continuation, etc. for string in &line.content.strings { sub_lines .last_mut() .unwrap() .content .strings .push(string.clone()); // might be truncated later let mut byte_offset = 0; for (byte_idx, c) in string.raw.char_indices() { let char_cols = c.width().unwrap_or(0); if sub_cols + char_cols > cols && sub_cols > 0 { let last_string = sub_lines .last_mut() .unwrap() .content .strings .last_mut() .unwrap(); let after_cut = TString::new( last_string.csi.clone(), last_string.raw[byte_idx - byte_offset..].to_string(), ); last_string.raw.truncate(byte_idx - byte_offset); byte_offset = byte_idx; sub_lines.push(Line { item_idx: line.item_idx, content: TLine { strings: vec![after_cut], }, line_type: LineType::Continuation { offset: wrap_idx, summary, }, }); wrap_idx += 1; sub_cols = char_cols; } else { sub_cols += char_cols; } } } } sub_lines } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/src/watcher.rs�������������������������������������������������������������������������0000644�0000000�0000000�00000006417�10461020230�0013573�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::*, anyhow::Result, notify::{ RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher, event::{ AccessKind, AccessMode, DataChange, EventKind, ModifyKind, }, }, std::path::PathBuf, termimad::crossbeam::channel::{ Receiver, bounded, }, }; /// A file watcher, providing a channel to receive notifications pub struct Watcher { pub receiver: Receiver<()>, _notify_watcher: RecommendedWatcher, } impl Watcher { pub fn new( paths_to_watch: &[PathBuf], mut ignorer: IgnorerSet, ) -> Result<Self> { info!("watcher on {:#?}", paths_to_watch); let (sender, receiver) = bounded(0); let mut notify_watcher = notify::recommended_watcher(move |res: notify::Result<notify::Event>| match res { Ok(we) => { match we.kind { EventKind::Modify(ModifyKind::Metadata(_)) => { debug!("ignoring metadata change"); return; // useless event } EventKind::Modify(ModifyKind::Data(DataChange::Any)) => { debug!("ignoring 'any' data change"); return; // probably useless event with no real change } EventKind::Access(AccessKind::Close(AccessMode::Write)) => { info!("close write event: {we:?}"); } EventKind::Access(_) => { debug!("ignoring access event: {we:?}"); return; // probably useless event } _ => { info!("notify event: {we:?}"); } } match time!(Info, ignorer.excludes_all_pathbufs(&we.paths)) { Ok(true) => { debug!("all excluded"); return; } Ok(false) => { debug!("at least one is included"); } Err(e) => { warn!("exclusion check failed: {e}"); } } if let Err(e) = sender.send(()) { debug!("error when notifying on notify event: {}", e); } } Err(e) => warn!("watch error: {:?}", e), })?; for path in paths_to_watch { if !path.exists() { warn!("watch path doesn't exist: {:?}", path); continue; } if path.is_dir() { debug!("add watch dir {:?}", path); notify_watcher.watch(path, RecursiveMode::Recursive)?; } else if path.is_file() { debug!("add watch file {:?}", path); notify_watcher.watch(path, RecursiveMode::NonRecursive)?; } } Ok(Self { receiver, _notify_watcher: notify_watcher, }) } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/website/.gitignore���������������������������������������������������������������������0000644�0000000�0000000�00000000005�10461020230�0014416�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������site ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/website/README.md����������������������������������������������������������������������0000644�0000000�0000000�00000000352�10461020230�0013712�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ Bacon's website is live at https://dystroy.org/bacon It's built using [mkdocs](https://www.mkdocs.org/) (minimal version: 1.0.4). To test it locally, cd to the website directory then mkdocs serve To build it, do mkdocs build ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/website/docs/analyzers.md��������������������������������������������������������������0000644�0000000�0000000�00000012215�10461020230�0015716�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������While Bacon was initially developped for the Rust language, it covers more and more tools and language. For most of them, a dedicated `analyzer` must be specified in the [job settings](../config#jobs). This page is an overview of the supported tools and how bacon can be configured for them. # Summary Analyzer | Languages | Tool -|-|- [standard](#rust) (*default*) | Rust | [cargo](https://doc.rust-lang.org/cargo/) `check`, `build`, `test`, `clippy`, `doc`, `run`, `miri` [nextest](#nextest)| Rust | [cargo-nextest](https://nexte.st/) [cargo_json](#cargojson)| Rust | cargo with `--message-format json-diagnostic-rendered-ansi` [python_unittest](#unittest) | Python | [Unittest](https://docs.python.org/3/library/unittest.html) [python_pytest](#pytest)| Python | [pytest](https://docs.pytest.org/) [python_ruff](#ruff)| Python | [ruff](https://docs.astral.sh/ruff/) [eslint](#eslint)| JS/TS/CSS | [ESLint](https://eslint.org/) [biome](#biome)| JS/TS/CSS | [Biome](https://biomejs.dev/) [cpp](#gcc-clang)| C++ | Clang and GCC cpp_doctest| C++ | [doctest](https://github.com/doctest/doctest). # Rust Rust specific support of bacon includes reading Cargo.toml files to identify all source directories, and help managing cargo features. ## Cargo build, clippy, test, doc, run **Status: <span style="background-color:green;color:white;padding:3px">mature</span>** These tools produce warnings, errors, test failures, with the same representation. Bacon comes with preconfigured modifiable jobs for them, and you can add some with no explicit analyzer according to your specific needs, for example ```TOML [jobs.nightly-clippy] command = [ "cargo", "+nightly", "clippy", "--all-targets", "--", "-A", "clippy::bool_to_int_with_if", ] ``` ## Cargo/JSON **Status: <span style="background-color:green;color:white;padding:3px">mature</span>** Cargo can be configured to output JSON. ```TOML [jobs.json-check] command = [ "cargo", "check", "--message-format", "json-diagnostic-rendered-ansi", ] need_stdout = true analyzer = "cargo_json" ``` With the `cargo_json` analyzer, the visible result in bacon is the same, but using this analyzer makes it possible to export from bacon more detailed data to use in other tools, eg [bacon-ls](https://github.com/crisidev/bacon-ls). ## Miri **Status: <span style="background-color:green;color:white;padding:3px">mature</span>** [miri](https://github.com/rust-lang/miri) is supported with the default analyzer. Bacon isn't preconfigured for miri but you can add a job with ```TOML [jobs.miri] command = ["cargo", "+nightly", "miri", "run"] need_stdout = true ``` ## Nextest **Status: <span style="background-color:green;color:white;padding:3px">mature</span>** [nextest](https://nexte.st/) It doesn't use the standard analyzer but bacon comes preconfigured with a nextest job so that you can launch `bacon nextest` or simply hit <kbd>n</kbd> while in bacon. # Python Support of Python is just starting, and Python developpers should raise their hand if they want to see progress here. ## Unittest **Status: <span style="background-color:orange;color:white;padding:3px">young</span>** Support for the [Unittest](https://docs.python.org/3/library/unittest.html) framework seems to work, but lacks testers and users. Exemple configuration: ```TOML [jobs.unittest] command = [ "python3", "unitest_runner.py", ] need_stdout = true analyzer = "python_unittest" watch = ["."] ``` ## Pytest **Status: <span style="background-color:orange;color:white;padding:3px">young</span>** [pytest](https://docs.pytest.org/en/stable/) It's configured with ```TOML [jobs.pytest] command = [ "pytest" ] need_stdout = true analyzer = "python_pytest" watch = ["."] ``` ## Ruff **Status: <span style="background-color:orange;color:white;padding:3px">young</span>** [ruff](https://docs.astral.sh/ruff/) Exemple configuration: ```TOML [jobs.ruff] env.FORCE_COLOR = "1" command = [ "ruff", "check", ] need_stdout = true analyzer = "python_ruff" watch = ["."] ``` # JavaScript / TypeScript ## Eslint **Status: <span style="background-color:orange;color:white;padding:3px">young</span>** [ESLint](https://eslint.org/) ```TOML [jobs.lint] command = ["npx", "eslint", "--color", "libs/*"] need_stdout = true analyzer = "eslint" watch = ["libs"] ``` ## Biome **Status: <span style="background-color:green;color:white;padding:3px">mature</span>** [Biome](https://biomejs.dev/) Example configuration (for a `./libs` folder) with some lint rules skipped: ```TOML [jobs.biome-libs] env.RAYON_NUM_THREADS = "1" # for constant ordering of items command = [ "npx", "@biomejs/biome", "lint", "--colors", "force", "./libs", "--skip", "complexity/useArrowFunction", "--skip", "style/useTemplate", ] need_stdout = true analyzer = "biome" watch = ["libs"] ``` # C++ ## GCC / Clang **Status: <span style="background-color:orange;color:white;padding:3px">young</span>** ```TOML [jobs.gcc] command = [ "g++", "-Wall", "src/main.cpp", ] watch = ["src"] need_stdout = true analyzer = "cpp" ``` ## # Other tools What's not here, you should probably ask for it, either on [GitHub](https://github.com/Canop/bacon) or on [the Miaou chat](https://miaou.dystroy.org/4683). �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/website/docs/community/FAQ.md����������������������������������������������������������0000644�0000000�0000000�00000004074�10461020230�0016345�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ # What does it exactly do ? Bacon watches the content of your source directories and launches `cargo check` or other commands on changes. Watching and computations are done on background threads to prevent any blocking. The screen isn't cleaned until the compilation is finished to prevent useful information from being replaced by the lines of an unfinished computation. Errors and test failures are displayed before warnings because you usually want to fix them first. Rendering is adapted to the dimensions of the terminal to ensure you get a proper usable report. And bacon manages rewrapping on resize. # Parallel bacon ? It's perfectly OK to have several bacon running in paralle, and can be useful to check several compilation targets. Similarly you don't have to stop bacon when you want to use cargo to build the application, or when you're just working on something else. You may have a dozen bacon running without problem. Bacon is efficient and doesn't work when there's no notification. # Supported platforms It works on all decent terminals on Linux, Max OSX and Windows. # Vim & Neovim support (Neo)Vim is perfectly supported but you may have had a problem, depending on your installation, with bacon sometimes not recomputing on file changes. The default write strategy of vim makes successive savings of the same file not always detectable by inotify. A solution is to add this to your init.vim file: set nowritebackup This doesn't prevent vim from keeping copies during editions, it just changes the behavior of the write operation and has no practical downside. # Licences Bacon is licenced under [AGPL-3.0](https://www.gnu.org/licenses/agpl-3.0.en.html). You're free to use it to compile the Rust projects of your choice, even commercial. The logo is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0). # Why "bacon" ? * It's a **bac**kground **con**piler. * It comes from France and, as you know, France is bacon. It's just a name. You don't have to eat meat to use the software. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/website/docs/community/bacon-dev.md����������������������������������������������������0000644�0000000�0000000�00000004532�10461020230�0017573�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ **Bacon** is developed by **Denys Séguret**, also known as [Canop](https://github.com/Canop) or [dystroy](https://dystroy.org). Major updates are announced on my Mastodon account: [@dystroy@mastodon.dystroy.org](https://mastodon.dystroy.org/@dystroy). The logo has been designed by [Peter Varo](https://petervaro.com). # Sponsorship **Bacon** is free for all uses. If it helps your company make money, consider helping me find time to add features and to develop new free open-source software. <div class=sponsorship> <!-- I don't think anybody even thought about using this <script src="https://liberapay.com/dystroy/widgets/button.js"></script> <noscript><a href="https://liberapay.com/dystroy/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a></noscript> --> <iframe src="https://github.com/sponsors/Canop/button" title="Sponsor Canop" height="35" width="114" style="border: 0; border-radius: 6px;margin-bottom: 20px;"></iframe> </div> I'm also available for consulting or custom development. Head to [https://dystroy.org](https://dystroy.org) for references. # Chat The best place to chat about bacon, to talk about features or bugs, is the Miaou chat. [Bacon room on Miaou](https://miaou.dystroy.org/4683?bacon) # Issues We use [GitHub's issue manager](https://github.com/Canop/bacon/issues). Before posting a new issue, check your problem hasn't already been raised and in case of doubt **please come first discuss it on the chat**. Looking into an issue may require bacon to be launched with log enabled (eg `BACON_LOG=DEBUG bacon`) which produces a `bacon.log` file. If bacon didn't understand correctly the output of a cargo tool, it may also be useful to have a look at the analysis export, which you normally find in a `bacon-analysis.json` file on hitting `ctrl-e`. # Contribute If you think you might help, as a tester or coder, you're welcome, but please read [Contributing to my FOSS projects](https://dystroy.org/blog/contributing/) before starting a PR. **Don't open a PR without discussing the design before**, either in the chat or in an issue, unless you're just fixing a typo. Coding is the easy part. Determining the exact requirement and how we want it to be done is the hard part. This is especially important if you plan to add a dependency or to change the visible parts, eg the launch arguments. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/website/docs/community/bacon-ls.md�����������������������������������������������������0000644�0000000�0000000�00000015673�10461020230�0017443�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[Bacon-ls](https://github.com/crisidev/bacon-ls) is a [language server](https://en.wikipedia.org/wiki/Language_Server_Protocol) implementation specifically designed to work with Bacon projects, exposing [textDocument/diagnostic](https://microsoft.github.io/language-server-protocol/specification#textDocument_diagnostic) and [workspace/diagnostic](https://microsoft.github.io/language-server-protocol/specification#workspace_diagnostic) capabilities to editors. ![bacon-ls](../img/bacon-ls.png) **NOTE: Bacon-ls requires Bacon 3.8+ to work properly.** **NOTE: Bacon-ls is not part of Bacon, it's a third-party tool developed to work WITH Bacon.** # Features * Read diagnostics from produced by Bacon. * Push diagnostics to the LSP client on certain events like saving or files changes. * Precise diagnostics positions. * Ability to react to changes over document saves and changes that can be configured. * Replacement code actions as suggested by `clippy`. * Automatic validation of `bacon` preferences to ensure `bacon-ls` can work with them. * Start `bacon` in background based on user preferences (requires `bacon` 3.8.0). * Synchronize diagnostics for all open files. # Installation ## VSCode First, install [Bacon](../../#installation). The VSCode extension is available on both VSCE and OVSX: * `VSCE` [https://marketplace.visualstudio.com/items?itemName=MatteoBigoi.bacon-ls-vscode](https://marketplace.visualstudio.com/items?itemName=MatteoBigoi.bacon-ls-vscode) * `OVSX` [https://open-vsx.org/extension/MatteoBigoi/bacon-ls-vscode](https://open-vsx.org/extension/MatteoBigoi/bacon-ls-vscode) ## Mason.nvim Both Bacon and Bacon-ls are installable via [mason.nvim](https://github.com/williamboman/mason.nvim): ```vim :MasonInstall bacon bacon-ls ``` ## Manual First, install [Bacon](../../#installation) and Bacon-ls ```bash ❯❯❯ cargo install --locked bacon bacon-ls ❯❯❯ bacon --version bacon 3.8.0 # make sure you have at least 3.8.0 ❯❯❯ bacon-ls --version 0.10.0 # make sure you have at least 0.10.0 ``` # Configuration Configure Bacon export settings with `bacon-ls` 🐽 export format and proper span support in the `bacon` preference file. To find where the file should be saved, you can use the command `bacon --prefs`: ```toml [jobs.bacon-ls] command = [ "cargo", "clippy", "--tests", "--all-targets", "--all-features", "--message-format", "json-diagnostic-rendered-ansi" ] analyzer = "cargo_json" need_stdout = true [exports.cargo-json-spans] auto = true exporter = "analyzer" line_format = "{diagnostic.level}|:|{span.file_name}|:|{span.line_start}|:|{span.line_end}|:|{span.column_start}|:|{span.column_end}|:|{diagnostic.message}|:|{span.suggested_replacement}" path = ".bacon-locations" ``` **NOTE: Bacon MUST be running to generate the export locations with the Bacon-ls job: `bacon -j bacon-ls`. From `bacon-ls` 0.10.0, this is done automatically if the option `runBaconInBackground` is set to true.** The language server can be configured using the appropriate LSP protocol and supports the following values: - `locationsFile` Bacon export filename (default: `.bacon-locations`). - `updateOnSave` Try to update diagnostics every time the file is saved (default: true). - `updateOnSaveWaitMillis` How many milliseconds to wait before updating diagnostics after a save (default: 1000). - `updateOnChange` Try to update diagnostics every time the file changes (default: true). - `validateBaconPreferences`: Try to validate that `bacon` preferences are setup correctly to work with `bacon-ls` (default: true). - `createBaconPreferencesFile`: If no `bacon` preferences file is found, create a new preferences file with the `bacon-ls` job definition (default: true). - `runBaconInBackground`: Run `bacon` in background for the `bacon-ls` job (default: true) - `runBaconInBackgroundCommandArguments`: Command line arguments to pass to `bacon` running in background (default "--headless -j bacon-ls") - `synchronizeAllOpenFilesWaitMillis`: How many milliseconds to wait between background diagnostics check to synchronize all open files (default: 2000). ## Neovim - LazyVim Bacon-ls is already integrated with [LazyVim](https://lazyvim.org) with [PR #3112](https://github.com/LazyVim/LazyVim/pull/3212): ```lua vim.g.lazyvim_rust_diagnostics = "bacon-ls" ``` ## Neovim - Manual NeoVim requires [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig/) to be configured and [rust-analyzer](https://rust-analyzer.github.io/) diagnostics must be turned off for Bacon-ls to properly function. Bacon-ls is part of nvim-lspconfig from commit [6d2ae9f](https://github.com/neovim/nvim-lspconfig/commit/6d2ae9fdc3111a6e8fd5db2467aca11737195a30) and it can be configured like any other LSP server works best when [vim.diagnostics.opts.update_in_insert](https://neovim.io/doc/user/diagnostic.html#vim.diagnostic.Opts) is set to `true`. ```lua require("lspconfig").bacon_ls.setup({ init_options = { updateOnSave = true, updateOnSaveWaitMillis = 1000, updateOnChange = false, } }) ``` For `rust-analyzer`, these 2 options must be turned off: ```lua rust-analyzer.checkOnSave.enable = false rust-analyzer.diagnostics.enable = false ``` ## VSCode The extension can be configured using the VSCode settings interface. **It is very important that rust-analyzer `Check On Save` and `Diagnostics` are disabled for `bacon-ls` to work properly:** * Untick `Rust-analyzer -> general -> Check On Save` * Untick `Rust-analyzer -> diagnostics -> Enable` ## Coc.nvim ```vim call coc#config('languageserver', { \ 'bacon-ls': { \ 'command': '~/.cargo/bin/bacon-ls', \ 'filetypes': ['rust'], \ 'rootPatterns': ['.git/', 'Cargo.lock', 'Cargo.toml'], \ 'initializationOptions': { \ 'updateOnSave': v:true, \ 'updateOnSaveWaitMillis': 1000, \ 'updateOnChange': v:false \ }, \ 'settings': {} \ } \ }) ``` # Troubleshooting Bacon-ls can produce a log file in the folder where its running by exporting the `RUST_LOG` variable in the shell: ## Vim / Neovim ```bash ❯❯❯ export RUST_LOG=debug ❯❯❯ nvim src/some-file.rs # or vim src/some-file.rs # the variable can also be exported for the current command and not for the whole shell ❯❯❯ RUST_LOG=debug nvim src/some-file.rs # or RUST_LOG=debug vim src/some-file.rs ❯❯❯ tail -F ./bacon-ls.log ``` ## VSCode Enable debug logging in the extension options. ```bash ❯❯❯ tail -F ./bacon-ls.log ``` # How does it work? Bacon-ls reads the diagnostics location list generated by [Bacon's export-locations](../../config/#exports) and exposes them on STDIO over the LSP protocol to be consumed by the client diagnostics. It requires Bacon to be running alongside to ensure regular updates of the export locations. The LSP client reads them as response to `textDocument/diagnostic` and `workspace/diagnostic`. # More info Please visit [https://github.com/crisidev/bacon-ls](https://github.com/crisidev/bacon-ls) for more information. ���������������������������������������������������������������������bacon-3.12.0/website/docs/community/nvim-bacon.md���������������������������������������������������0000644�0000000�0000000�00000002270�10461020230�0017763�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ # Purpose [nvim-bacon](https://github.com/Canop/nvim-bacon) is a neovim plugin, which, combined with bacon, lets you navigate between errors and warnings without leaving your editor, just hitting a key. # How it works At every job end, bacon writes a `.bacon-locations` file with all items (errors, warnings, test failures, etc.) and for each of them its label, file path, line and column. In nvim, on predefined shortcuts, the nvim-bacon plugin may jump to the next item's position, or display all items to let you choose one. The plugin reads the `.bacon-locations` file every time you hit one of its shortcuts. Nothing in this design is Rust related. This plugin can thus be used whatever the ecosystem(s) you program in. # Bacon configuration The configuration instructing bacon to export the locations at every job should be defined in your global `bacon/prefs.toml`: ```TOML [exports.locations] auto = true path = ".bacon-locations" line_format = "{kind} {path}:{line}:{column} {message}" ``` # Installation & Usage How to install nvim-bacon, how to configure it, and how to use it, are described in its own page: [https://github.com/Canop/nvim-bacon](https://github.com/Canop/nvim-bacon). ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/website/docs/config.md�����������������������������������������������������������������0000644�0000000�0000000�00000031143�10461020230�0015154�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ # Configuration Files All configuration files are optional but you'll probably need specific jobs for your targets, examples, etc. All accept the same properties (preferences, keybindings, jobs, etc.). Bacon loads in order: * its default internal configuration (which includes the default bacon.toml) * the global `prefs.toml` ([global preferences](#global-preferences)) * the file whose path is in environment variable `BACON_PREFS` * the `workspace.metadata.bacon` entry in the workspace's `Cargo.toml` * the workspace level `bacon.toml` file ([project settings](#project-settings)) * the `bacon.toml` file in `workspace-root/.config/` * the `package.metadata.bacon` entry in the package's `Cargo.toml` * the `bacon.toml` file in `package-root/` * the `bacon.toml` file in `package-root/.config/` * the file whose path is in environment variable `BACON_CONFIG` * the content of the `--config-toml` argument Each configuration file overrides the properties of previously loaded ones. But you don't *need* so many files. It's usually enough to have a global `prefs.toml` file and a project specific `bacon.toml`. When you modified those files and bacon evolved since, you may want to have a look at the current default ones: * [Current default prefs.toml](https://raw.githubusercontent.com/Canop/bacon/main/defaults/default-prefs.toml) * [Current default bacon.toml](https://raw.githubusercontent.com/Canop/bacon/main/defaults/default-bacon.toml) Bacon watches those files and reload them when necessary, so you don't have to relaunch it if you add a key-binding, or a job, or [an allowed lint](../cookbook/#configure-clippy-lints) in your clippy job. ## Global Preferences `bacon --prefs` creates the preferences file if it doesn't exist and returns its path (which is system dependent). You may run `$EDITOR "$(bacon --prefs)"` to edit it directly. The default configuration file contains already possible entries that you may uncomment and modify. ## Project Settings `bacon --init` creates the `bacon.toml` file if it doesn't exist. This file usually contains project specific jobs and shortcuts and should be saved and shared using your version control system. It's a good idea to put here the triggers for specific jobs. The [default bacon.toml](https://raw.githubusercontent.com/Canop/bacon/main/defaults/default-bacon.toml) is used when you don't create a file. # Jobs A job is a command which is ran by bacon in background, and whose result is analyzed and displayed on end. There's always an active job in bacon, be it the default one or one you selected at launch or using a bound key. A job declaration in a TOML file looks like this: ```TOML [jobs.clippy-all] command = [ "cargo", "clippy", "--", "-A", "clippy::derive_partial_eq_without_eq", "-A", "clippy::len_without_is_empty", "-A", "clippy::map_entry", ] need_stdout = false ``` ## Job Properties The job is defined by the following fields: field | meaning | default :-|:-|:- allow_failures | if `true`, the action is considered a success even when there are test failures | `false` allow_warnings | if `true`, the action is considered a success even when there are warnings | `false` analyzer | command output parser, see below | `"standard"` apply_gitignore | if `true` the job isn't triggered when the modified file is excluded by gitignore rules | `true` background | compute in background and display only on end | `true` command | the tokens making the command to execute (first one is the executable) | default_watch | whether to watch default files (`src`, `tests`, `examples`, `build.rs`, and `benches`). When it's set to `false`, only the files in your `watch` parameter are watched | `true` env | a map of environment vars, for example `env.LOG_LEVEL="die"` | kill | a command replacing the default job interruption (platform dependant, `SIGKILL` on unix). For example `kill = ["kill", "-s", "INT"]` | ignore | list of glob patterns for files to ignore | ignored_lines | regular expressions for lines to ignore | extraneous_args | if `false`, the action is run "as is" from `bacon.toml`, eg: no `--all-features` or `--features` inclusion | `true` need_stdout |whether we need to capture stdout too (stderr is always captured) | `false` on_change_strategy | `wait_then_restart` or `kill_then_restart` | on_success | the action to run when there's no error, warning or test failures | watch | a list of files and directories that will be watched if the job is run on a package. Usual source directories are implicitly included unless `default_watch` is set to false | All these properties can also be defined before jobs and will apply to all of them unless overriden. Beware of job references in `on_success`: you must avoid loops with 2 jobs calling themselves mutually, which would make bacon run all the time. Example: ```TOML [jobs.exs] command = ["cargo", "run", "--example", "simple"] need_stdout = true ``` Note: Some tools detect that their output is piped and don't add style information unless you add a parameter which usually looks like `--color always`. This isn't normally necessary for cargo because bacon, by default, sets the `CARGO_TERM_COLOR` environment variable. ## Analyzers The output of the standard cargo tools is understood by bacon's standard analyzer. For other tools, a specific analyzer may be configured with, eg, `analyzer = "nextest"`. For the list of analyzers and configuration examples, see [Analyzers](../analyzers). ## Default Job The default job is the one which is launched when you don't specify one in argument to the bacon command (ie `bacon test`). It's also the one you can run with the `job:default` action. You can set the default job by setting the `default_job` key in your `bacon.toml` file. # Key Bindings This section lets you change the key combinations to use to trigger [actions](#actions). For example: ```TOML [keybindings] h = "job:clippy" shift-F9 = "toggle-backtrace(1)" ctrl-r = "toggle-raw-output" ``` Note that you may have keybindings for jobs which aren't defined in your project, this isn't an error, and it's convenient to help keep define your personal keybindings in one place. Another example, if you want vim-like shortcuts: ```TOML [keybindings] esc = "back" g = "scroll-to-top" shift-g = "scroll-to-bottom" k = "scroll-lines(-1)" j = "scroll-lines(1)" ctrl-u = "scroll-page(-1)" ctrl-d = "scroll-page(1)" ``` Your operating system and console intercept many key combinations. If you want to know which one are available, and the key syntax to use, you may find [print_key](https://github.com/Canop/print_key) useful. # Actions Actions are launched * on key presses, depending on key-binding * when triggered by a job ending success Actions are parsed from strings, for example `quit` (long form: `internal:quit`) is the action of quitting bacon and can be bound to a key. An action is either an *internal*, based on a hardcoded behavior of bacon, a *job reference*, or an *export*. An export action is defined as `export:` followed by the export name. ## Internals internal | default binding | meaning :-|:-|:- back | <kbd>Esc</kbd> | get back to the previous page or job, or cancel search back-or-quit | | back to previous page or job, quitting if there is none copy-unstyled-output | | write the currently displayed job output to the clipboard focus-search | <kbd>/</kbd> | focus the search input help | <kbd>h</kbd> or <kbd>?</kbd> | open the help page next-match | <kbd>tab</kbd> | go to next search match no-op | | do nothing (may be used to disable a previously set binding) pause | | disable automatic job execution on change play-sound | | play a sound, eg `play-sound(volume=100%)` previous-match | <kbd>backtab</kbd> | go to previous search match quit | <kbd>q</kbd> or <kbd>ctrl</kbd><kbd>q</kbd> or <kbd>ctrl</kbd><kbd>c</kbd> | quit refresh | <kbd>F5</kbd> | clear output then run current job again reload-config | | reload all configuration files rerun | | run current job again scope-to-failures | <kbd>f</kbd> | restrict job to test failure(s) scroll-lines(-1) | <kbd>↑</kbd> | move one line up scroll-lines(1) | <kbd>↓</kbd> | move one line down scroll-pages(-1) | <kbd>PageUp</kbd> | move one page up scroll-pages(1) | <kbd>PageDown</kbd> | move one page down scroll-to-bottom | <kbd>End</kbd> | scroll to bottom scroll-to-top | <kbd>Home</kbd> | scroll to top toggle pause | <kbd>p</kbd> | toggle pause toggle-backtrace(level) | <kbd>b</kbd> | enable rust backtrace, level is either `1` or `full` toggle-raw-output | | display the untransformed command output toggle-summary | <kbd>s</kbd> | display results as abstracts toggle-wrap | <kbd>w</kbd> | toggle line wrapping unpause | | enable automatic job execution on change validate | <kbd>enter</kbd> | unfocus the input, keeping the search The `scroll-lines` and `scroll-pages` internals are parameterized. You can for example define a shortcut to move down half a page: ```toml ctrl-d = "scroll-pages(.5)" ``` ## Job References Job references are useful as actions, which can be bound to key combinations. They're either role based or name based. To refer to the job called `test`, you use a name based reference: `job:test`. To refer to a job based on a [cargo alias](https://doc.rust-lang.org/cargo/reference/config.html#alias), add `alias:`, for example `job:alias:r`. Role based job references are the following ones: job reference | meaning -|- `job:default` | the job defined as *default* in the bacon.toml file `job:initial` | the job specified as argument, or the default one if there was none explicit `job:previous` | the job which ran before, if any. The `back` internal has usually the same effect `job:previous-or-quit` | same as `job:previous`, but will quit if there was no job. The `back-or-quit` internal has usually the same effect # Exports If necessary, exports can be defined to write files either on end of task or on key presses. Following are 3 typical configurations. ## Locations export This is the best way to ensure you can list warnings/errors/failures and navigate between them in your IDE, for example with a plugin such as [nvim-bacon](https://github.com/Canop/nvim-bacon). With the following configuration, locations are exported on each job execution. ```TOML [exports.locations] auto = true path = ".bacon-locations" line_format = "{kind} {path}:{line}:{column} {message}" ``` This export works for any tool and any job. ## Cargo Spans export When using the `cargo_json` analyzer, more detailed information is available than what is printed on screen and this analyzer can provide that data with a configuration such as this one: ```TOML [jobs.bacon-ls] command = [ "cargo", "clippy", "--message-format", "json-diagnostic-rendered-ansi" ] analyzer = "cargo_json" need_stdout = true [exports.cargo-json-spans] auto = true exporter = "analyzer" line_format = "{diagnostic.level}:{span.file_name}:{span.line_start}:{span.line_end}:{diagnostic.message}" ``` The exported data come from the [Diagnostic](https://docs.rs/cargo_metadata/0.19.1/cargo_metadata/diagnostic/struct.Diagnostic.html) and [DiagnosticSpan](https://docs.rs/cargo_metadata/0.19.1/cargo_metadata/diagnostic/struct.DiagnosticSpan.html) structures. ## Report export This is an example of exporting a report on a key press, when required: ```TOML [exports.json-report] auto = false [keybindings] ctrl-e = "export:json-report" ``` # Other config properties Have a look, at least once, at the default configuration files. They contain many other properties, commented out, that you may find useful. ## summary, wrap, reverse You can change the `summary`, `wrapping`, and `reverse` mode at launch (see `bacon --help`), in the application using keys, and you may set the initial values in this preferences file: ```TOML # Uncomment and change the value (true/false) to # specify whether bacon should start in summary mode # # summary = true # Uncomment and change the value (true/false) to # specify whether bacon should start with lines wrapped # # wrap = true # In "reverse" mode, the focus is at the bottom, item # order is reversed, and the status bar is on top # # reverse = true ``` ## Sound You may have audio notifications on job success or failures. This requires sound to be enabled, either at root level or in a specific job: ```TOML [sound] enabled = true base_volume = "100%" # global volume multiplier ``` Sound being enabled, you can add `play-sound` callbacks to jobs, eg ```TOML on_success = "play-sound(name=90s-game-ui-6,volume=50)" on_failure = "play-sound(name=beep-warning,volume=100)" ``` Sound name can be omitted. Possible values are `2`, `90s-game-ui-6`, `beep-6`, `beep-beep`, `beep-warning`, `bell-chord`, `car-horn`, `convenience-store-ring`, `cow-bells`, `pickup`, `positive-beeps`, `short-beep-tone`, `slash`, `store-scanner`, `success`. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/website/docs/cookbook.md���������������������������������������������������������������0000644�0000000�0000000�00000013076�10461020230�0015522�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ Those few recipes illustrate the logic of using bacon, and may serve as inspiration for your own practices. Tell me about your recipes so that I may share them there. # Open your lib's doc Let's first assume your `bacon.toml` file has been generated by a recent version of bacon. Then you can launch bacon on its default task, then when you want to check the doc, hit the <kbd>d</kbd> key: as soon as the doc compiles, it's opened in your browser and bacon switches back to the previous job. If you want to complete an old `bacon.toml` file, or just understand how it works, here's the relevant configuration: The job: ```TOML [jobs.doc-open] command = ["cargo", "doc", "--no-deps", "--open"] need_stdout = false on_success = "back" # so that we don't open the browser at each change ``` And the key binding: ```TOML [keybindings] d = "job:doc-open" ``` # Configure Clippy lints Jobs in the `bacon.toml` file are specific to your projects, there's no reason not to adapt them for its specificities. You may for example want to tune the clippy rules: ```TOML [jobs.clippy] command = [ "cargo", "clippy", "--", "-A", "clippy::collapsible_else_if", "-A", "clippy::collapsible_if", "-A", "clippy::field_reassign_with_default", "-A", "clippy::match_like_matches_macro", ] need_stdout = false ``` You may also add some modifiers on spot sessions, eg ```bash bacon clippy -- -W clippy::pedantic ``` Note that bacon doesn't need to be killed and relaunched when you change the job config. # Check for other platforms You may define specific jobs for specific targets: ```toml [jobs.check-win] command = ["cargo", "check", "--target", "x86_64-pc-windows-gnu"] ``` An habit I suggest: use <kbd>alt</kbd> keybindings for *alternative* platforms: ```toml [keybindings] alt-w = "job:check-win" ``` # Run binaries and examples If you configure a `cargo run` job, you'll get the usual warnings and errors until there's none, at which point you'll have the output of your binary (assuming its terminal output is interesting). ```toml [jobs.exs] command = ["cargo", "run", "--example", "simple"] allow_warnings = true need_stdout = true ``` You may add `on_success = "back"` if you don't want the executable to run again on changes. The `allow_warnings = true` line tells bacon to run the executable even when there are warnings. The excutable's output would come below warnings. Some libraries and programs test whether they run in a TTY and remove style in such case. Most usually, those applications provide a way to bypass this test with a launch argument. Depending on the desired output, you would have to add a setting to the run job, for example ([more on this](https://github.com/Canop/bacon/issues/89#issuecomment-1257752297)): ```toml command = ["cargo", "run", "--", "--color", "yes"] ``` # Long running programs If your program never stops (e.g. a server), you may set `background = false` to have the output of `cargo run` immediately displayed instead of waiting for the program's end. If you want your program to restart at every change, use `on_change_strategy = "kill_then_restart"`. You may also want to change the way your program is killed if it should release resources. In this case, you can replace the standard interrupution by specifying your own `kill` command. Combining all those changes would give you something like this: ```toml [jobs.webserver] command = ["cargo", "run", "--", "--serve"] need_stdout = true background = false on_change_strategy = "kill_then_restart" kill = ["kill", "-s", "INT"] ``` Of course you don't have to take them all, depending on your precise case. # Variable arguments Launch arguments after the `--` aren't interpreted by bacon but sent unchanged to the job commands. This may be useful to add an argument only for one run without changing the `bacon.toml` file. For example ```bash bacon -- --target x86_64-pc-windows-gnu ``` Be careful that some programs already require `--` so you may have to double it. For example, to run `cargo test` with a single thread, you'll need ```bash bacon test -- -- --test-threads=1 ``` Another use case, a job which needs a complementary argument: ```toml [jobs.ex] command = ["cargo", "run", "--example"] need_stdout = true ``` You would call it with ```bash bacon ex -- example4578 ``` # Specific Rust toolchain Bacon calls `cargo` under the hood to analyze the workspace, before even trying to run a job. So bacon itself must sometimes be called with the toolchain already set in order to correctly understand the `Cargo.toml` files. A simple solution is to set it up with the `RUSTUP_TOOLCHAIN` env var. For example: ```bash RUSTUP_TOOLCHAIN=beta bacon test ``` # Bacon CLI snippets You may share a command-line snippet without requiring a bacon.toml file, using the `--config-toml` argument: ```bash bacon -j cli-test --config-toml ' [jobs.cli-test] command = [ "sh", "-c", "echo \"hello $(date +%H-%M-%S)\"; cargo run", ] need_stdout = true allow_warnings = true background = false on_change_strategy = "kill_then_restart" kill = ["pkill", "-TERM", "-P"]' ``` Notes: * wrap the inline configuration item in single quotes so that you may use double-quotes inside * this configuration is simply added to the other ones and it may refer to them * if you add a job this way and want it executed, either define it as `default_job` in the same inline TOML or use the `--job`/`-j` argument as in the example above # Run headless Sometimes, you may want to run bacon without a TUI, for example in a container, or when you run bacon to run an application on change and you want to keep all outputs. Try the headless mode: `bacon --headless` ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/website/docs/index.md������������������������������������������������������������������0000644�0000000�0000000�00000005364�10461020230�0015024�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ <p class=logo> <img class=logo width=640px src="img/logo.svg"> </p> **bacon** is a background code checker. It's designed for minimal interaction so that you can just let it run, alongside your editor, and be notified of warnings, errors, or test failures in your Rust code. It conveys the information you need even in a small terminal so that you can keep more screen estate for your other tasks. It shows you errors before warnings, and the first errors before the last ones, so you don't have to scroll up to find what's relevant. ![vi-and-bacon](img/vi-and-bacon.png) You don't have to remember commands: the essential ones are listed on bottom and the few other ones are shown on a hit on the <kbd>h</kbd> key. # Installation Run ```bash cargo install --locked bacon ``` Run this command too if you want to update bacon. Configuration has always been retro-compatible so you won't lose anything. Some features are disabled by default. You may enable them with cargo install --locked bacon --features "clipboard sound" # Usage Launch bacon in a terminal you'll keep visible ```bash bacon ``` This launches the default job, usually based on `cargo check` : Bacon will watch the source directories and shows you the errors and warnings found by the cargo command. You may decide to launch and watch tests by either hitting the <kbd>t</kbd> key, or by launching bacon with ```bash bacon test ``` or `bacon nextest` if you're a nextest user. ![test](img/test.png) When there's a failure, hit <kbd>f</kbd> to restrict the job to the failing test. Hit <kbd>esc</kbd> to get back to all tests. While in bacon, you can see Clippy warnings by hitting the <kbd>c</kbd> key. And you get back to your previous job with <kbd>esc</kbd> You may also open the `cargo doc` in your browser with the <kbd>d</kbd> key. You can configure and launch the jobs of your choice: tests, specific target compilations, examples, etc. and look at the results while you code. Run `bacon --help` to see all launch arguments, and read the [cookbook](cookbook). # Configuration See [config](config) for details, but here's the crust: ## Global Preferences The `prefs.toml` file lets you define key bindings, or always start in summary mode or with lines wrapped. To create a default preferences file, use `bacon --prefs`. Shortcut: $EDITOR "$(bacon --prefs)" ## Project Settings You'll define in the `bacon.toml` file the jobs you need, perhaps an example to check, a run with special parameters, or the settings of clippy, as well as shortcuts to run those jobs. Create a `bacon.toml` file by running bacon --init This file already contains some standard jobs. Add your own, for example ```toml [jobs.check-win] command = ["cargo", "check", "--target", "x86_64-pc-windows-gnu"] ``` ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������bacon-3.12.0/website/mkdocs.yml���������������������������������������������������������������������0000644�0000000�0000000�00000001673�10461020230�0014445�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������site_name: Bacon site_description: 'Bacon, a background compiler for Rust' site_url: https://dystroy.org/bacon repo_url: https://github.com/Canop/bacon edit_uri: '' nav: - Overview: index.md - Config: config.md - Analyzers: analyzers.md - Cookbook: cookbook.md - Community: - Bacon Dev: community/bacon-dev.md - FAQ: community/FAQ.md - bacon-ls: community/bacon-ls.md - nvim-bacon: community/nvim-bacon.md extra_css: - css/extra.css - css/link-to-dystroy.css extra_javascript: - js/link-to-dystroy.js theme: name: mkdocs favicon: /favicon.png nav_style: dark custom_dir: custom_theme highlightjs: true hljs_style: Dracula hljs_languages: - yaml - ini - css - rust markdown_extensions: - admonition - def_list �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������