nutmeg-0.1.4/.cargo_vcs_info.json0000644000000001360000000000100123350ustar { "git": { "sha1": "8c83babf341a7a40dff29270cb6e903c61287999" }, "path_in_vcs": "" }nutmeg-0.1.4/.devcontainer/Dockerfile000064400000000000000000000010171046102023000156550ustar 00000000000000# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/rust/.devcontainer/base.Dockerfile # [Choice] Debian OS version (use bullseye on local arm64/Apple Silicon): buster, bullseye ARG VARIANT="buster" FROM mcr.microsoft.com/vscode/devcontainers/rust:0-${VARIANT} # [Optional] Uncomment this section to install additional packages. # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # && apt-get -y install --no-install-recommends nutmeg-0.1.4/.devcontainer/devcontainer.json000064400000000000000000000031661046102023000172460ustar 00000000000000// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/rust { "name": "Rust", "build": { "dockerfile": "Dockerfile", "args": { // Use the VARIANT arg to pick a Debian OS version: buster, bullseye // Use bullseye when on local on arm64/Apple Silicon. "VARIANT": "bullseye" } }, "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], // Configure tool-specific properties. "customizations": { // Configure properties specific to VS Code. "vscode": { // Set *default* container specific settings.json values on container create. "settings": { "lldb.executable": "/usr/bin/lldb", // VS Code don't watch files under ./target "files.watcherExclude": { "**/target/**": true }, "rust-analyzer.checkOnSave.command": "clippy" }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "vadimcn.vscode-lldb", "mutantdino.resourcemonitor", "rust-lang.rust-analyzer", "tamasfe.even-better-toml", "serayuzgur.crates" ] } }, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "rustc --version", // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", "features": { "git": "os-provided", "github-cli": "latest", "fish": "latest" } } nutmeg-0.1.4/.github/workflows/cargo-audit.yml000064400000000000000000000007011046102023000174420ustar 00000000000000name: cargo-audit on: schedule: - cron: '17 0 * * 2' push: paths: - '**/Cargo.toml' - '**/Cargo.lock' - .github/workflows/cargo-audit.yml jobs: cargo-audit: runs-on: ubuntu-latest steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - uses: actions-rs/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} nutmeg-0.1.4/.github/workflows/tests.yml000064400000000000000000000016561046102023000164170ustar 00000000000000name: Tests on: push: pull_request: # see https://matklad.github.io/2021/09/04/fast-rust-builds.html env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CI: 1 RUST_BACKTRACE: short RUSTFLAGS: "-W rust-2021-compatibility" RUSTUP_MAX_RETRIES: 10 jobs: build: strategy: matrix: os: [macOS-latest, ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - name: Show Cargo and rustc version run: | cargo --version rustc --version - name: Cache Cargo uses: actions/cache@v2 with: path: | ~/.cargo/registry ~/.cargo/git target key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} restore-keys: | cargo-${{ runner.os }}- - name: Build run: cargo build --all-targets - name: Test run: cargo test --workspace nutmeg-0.1.4/.gitignore000064400000000000000000000000401046102023000131070ustar 00000000000000/target Cargo.lock mutants.out* nutmeg-0.1.4/Cargo.lock0000644000000316520000000000100103170ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", "winapi", ] [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "cc" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "errno" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", "windows-sys", ] [[package]] name = "errno-dragonfly" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" dependencies = [ "cc", "libc", ] [[package]] name = "getrandom" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", "wasi", ] [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "hermit-abi" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.3", "libc", "windows-sys", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "linux-raw-sys" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", "winapi", ] [[package]] name = "nutmeg" version = "0.1.4" dependencies = [ "atty", "parking_lot", "rand", "terminal_size", "tracing", "tracing-subscriber", "yansi", ] [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets", ] [[package]] name = "pin-project-lite" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags", ] [[package]] name = "rustix" version = "0.37.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sharded-slab" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" dependencies = [ "lazy_static", ] [[package]] name = "smallvec" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "syn" version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "terminal_size" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ "rustix", "windows-sys", ] [[package]] name = "thread_local" version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "tracing" version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-log" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" dependencies = [ "lazy_static", "log", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ "nu-ansi-term", "sharded-slab", "smallvec", "thread_local", "tracing-core", "tracing-log", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[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-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[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_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[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_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[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_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" nutmeg-0.1.4/Cargo.toml0000644000000020400000000000100103270ustar # 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" name = "nutmeg" version = "0.1.4" description = "An unopinionated progress bar library" readme = "README.md" keywords = [ "progress", "progress-bar", "terminal", "ansi", ] license = "MIT" repository = "https://github.com/sourcefrog/nutmeg" [dependencies.atty] version = "0.2" [dependencies.parking_lot] version = "0.12" [dependencies.terminal_size] version = "0.2" [dependencies.yansi] version = "0.5" [dev-dependencies.rand] version = "0.8" [dev-dependencies.tracing] version = "0.1" [dev-dependencies.tracing-subscriber] version = "0.3" nutmeg-0.1.4/Cargo.toml.orig000064400000000000000000000010431046102023000140120ustar 00000000000000[package] name = "nutmeg" version = "0.1.4" edition = "2021" description = "An unopinionated progress bar library" license = "MIT" repository = "https://github.com/sourcefrog/nutmeg" keywords = ["progress", "progress-bar", "terminal", "ansi"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] atty = "0.2" parking_lot = "0.12" terminal_size = "0.2" yansi = "0.5" [dev-dependencies] rand = "0.8" tracing = "0.1" tracing-subscriber = "0.3" [workspace] members = ["examples/tracing"] nutmeg-0.1.4/LICENSE000064400000000000000000000020541046102023000121330ustar 00000000000000MIT License Copyright (c) 2021 Martin Pool Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. nutmeg-0.1.4/NEWS.md000064400000000000000000000102201046102023000122160ustar 00000000000000# Nutmeg Changelog ## 0.1.4 Released 2023-09-23 - New: Support and documentation for sending `tracing` updates through a Nutmeg view. - New: `View::new` is now `const`, so that a `static` view can be constructed in a global variable: you no longer need to wrap them in an `Arc`. See `examples/static_view`. ## 0.1.3 Released 2023-05-24 - New: [View::clear] temporarily removes the progress view if it is drawn, but allows it to pop back when the model is next updated. - New: [Options] can be constructed as a `static` value: there's a new `const fn` [Options::new] constructor and the functions to set fields are also `const`. - New: [View::message_bytes] as a convenience for callers who already have a `[u8]` or `Bytes` or similar. ## 0.1.2 Released 2022-07-27 - API change: Removed `View::new_stderr` and `View::write_to`. Instead, the view can be drawn on stderr or output can be captured using [Options::destination]. This is better aligned with the idea that programs might have a central function that constructs a [Options], as they will probably want to consistently write to either stdout or stderr. - New: Output can be captured for inspection in tests using [Options::destination], [Destination::Capture], and [View::captured_output]. - Improved: Nutmeg avoids redrawing if the model renders identical output to what is already displayed, to avoid flicker. ## 0.1.1 Released 2022-03-22 - API change: [View::message] takes the message as an `AsRef`, meaning it may be either a `&str` or `String`. This makes the common case where the message is the result of `format!` a little easier. ## 0.1.0 Released 2022-03-22 - API change: The "Write" type representing the destination is no longer part of the visible public signature of [View], to hide complexity and since it is not helpful to most callers. - API change: Renamed `View::to_stderr` to `View::new_stderr`. - New: [percent_done] and [estimate_remaining] functions to help in rendering progress bars. - New: The [models] mod provides some generally-useful basic models, specifically [models::StringPair], [models::UnboundedModel] and [models::LinearModel]. These build only on the public interface of Nutmeg, so also constitute examples of what can be done in application-defined models. - New: [View::finish] removes the progress bar (if painted) and returns the [Model]. [View::abandon] now also returns the model. - New: [Model::final_message] to let the model render a message to be printed when work is complete. - New: The callback to [View::update] may return a value, and this is passed back to the caller of [View::update]. - New: [models::BasicModel] allows simple cases to supply both an initial value and a render function inline in the [View] constructor call, avoiding any need to define a [Model] struct. - New: [View::inspect_model] gives its callback a `&mut` to the model. - New: Progress bars constructed by [View::new] and `View::new_stderr` are disabled when `$TERM=dumb`. ## 0.0.2 Released 2022-03-07 - API change: Renamed `nutmeg::ViewOptions` to just `nutmeg::Options`. - Fixed: A bug that caused leftover text when multi-line bars shrink in width. - Fixed: The output from bars created with [View::new] and `View::to_stderr` in Rust tests is captured with the test output rather than leaking through to cargo's output. - New method [View::message] to print a message to the terminal, as an alternative to using `write!()`. - New `example/multithreaded.rs` showing how a View and Model can be shared across threads. ## 0.0.1 - Rate-limit updates to the terminal, controlled by `ViewOptions::update_interval` and `ViewOptions::print_holdoff`. - Fix a bug where the bar was sometimes not correctly erased by [View::suspend]. - Change to [`parking_lot`](https://docs.rs/parking_lot) mutexes in the implementation. ## 0.0.0 - The application has complete control of styling, including coloring etc. - Draw and erase progress bars. - Write messages "under" the progress bar with `writeln!(view, ...)`. The bar is automatically suspended and restored. If the message has no final newline, the bar remains suspended until the line is completed. nutmeg-0.1.4/README.md000064400000000000000000000042531046102023000124100ustar 00000000000000# nutmeg - an unopinionated progress bar library [![Tests](https://github.com/sourcefrog/nutmeg/actions/workflows/tests.yml/badge.svg?branch=main&event=push)](https://github.com/sourcefrog/nutmeg/actions/workflows/tests.yml?query=branch%3Amain) [![docs.rs](https://docs.rs/nutmeg/badge.svg)](https://docs.rs/nutmeg) [![crates.io](https://img.shields.io/crates/v/nutmeg.svg)](https://crates.io/crates/nutmeg) [![libs.rs](https://img.shields.io/badge/libs.rs-nutmeg-blue)](https://lib.rs/crates/nutmeg) ![Maturity: Beta](https://img.shields.io/badge/maturity-beta-blue.svg) Nutmeg draws terminal progress indicators while giving the application complete control over their appearance and content. For more information: License: MIT ## Example From `examples/basic.rs`: ```rust use std::io::Write; // to support write!() // 1. Define a struct holding all the application state necessary to // render the progress bar. #[derive(Default)] struct Model { i: usize, total: usize, last_file_name: String, } // 2. Define how to render the progress bar as a String. impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("{}/{}: {}", self.i, self.total, self.last_file_name) } } fn main() -> std::io::Result<()> { // 3. Create a View when you want to draw a progress bar. let mut view = nutmeg::View::new(Model::default(), nutmeg::Options::default()); // 4. As the application runs, update the model via the view. let total_work = 100; view.update(|model| model.total = total_work); for i in 0..total_work { view.update(|model| { model.i += 1; model.last_file_name = format!("file{}.txt", i); }); // 5. Interleave text output lines by writing to the view. if i % 10 == 3 { writeln!(view, "reached {}", i)?; } std::thread::sleep(std::time::Duration::from_millis(100)); } // 5. The bar is automatically erased when dropped. Ok(()) } ``` [![asciicast](https://asciinema.org/a/oPI37ohOY8yhDxomTzHCsR4sw.svg)](https://asciinema.org/a/oPI37ohOY8yhDxomTzHCsR4sw) nutmeg-0.1.4/examples/abandon.rs000064400000000000000000000010041046102023000147060ustar 00000000000000//! Abandon a bar while drawn, without erasing it. use std::thread::sleep; use std::time::Duration; struct Model { i: usize, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("count: {}", self.i) } } fn main() { let options = nutmeg::Options::default(); let view = nutmeg::View::new(Model { i: 0 }, options); for _i in 1..=5 { view.update(|state| state.i += 1); sleep(Duration::from_millis(300)); } view.abandon(); } nutmeg-0.1.4/examples/basic.rs000064400000000000000000000024161046102023000143750ustar 00000000000000//! The overall example used in README and the library docstring. use std::io::Write; // to support write!() // 1. Define a struct holding all the application state necessary to // render the progress bar. #[derive(Default)] struct Model { i: usize, total: usize, last_file_name: String, } // 2. Define how to render the progress bar as a String. impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("{}/{}: {}", self.i, self.total, self.last_file_name) } } fn main() -> std::io::Result<()> { // 3. Create a View when you want to draw a progress bar. let mut view = nutmeg::View::new(Model::default(), nutmeg::Options::default()); // 4. As the application runs, update the model via the view. let total_work = 100; view.update(|model| model.total = total_work); for i in 0..total_work { view.update(|model| { model.i += 1; model.last_file_name = format!("file{i}.txt"); }); // 5. Interleave text output lines by writing to the view. if i % 10 == 3 { writeln!(view, "reached {i}")?; } std::thread::sleep(std::time::Duration::from_millis(100)); } // 5. The bar is automatically erased when dropped. Ok(()) } nutmeg-0.1.4/examples/basic_model.rs000064400000000000000000000005611046102023000155540ustar 00000000000000//! Use of [nutmeg::models::BasicModel]. pub fn main() { let view = nutmeg::View::new( nutmeg::models::BasicModel::new((0, 10), |(a, b)| format!("{a}/{b} complete")), nutmeg::Options::default(), ); for _i in 0..10 { view.update(|model| model.value.0 += 1); std::thread::sleep(std::time::Duration::from_millis(150)); } } nutmeg-0.1.4/examples/bench.rs000064400000000000000000000010101046102023000143600ustar 00000000000000//! See how fast we can send view updates. //! //! (Run this with `--release` to get a fair estimate.) use std::time::Instant; fn main() { let start = Instant::now(); let view = nutmeg::View::new(0u64, nutmeg::Options::default()); let n = 10_000_000; for i in 0..n { view.update(|model| *model = i); } view.message(format!( "{}ms to send {} updates; average {}ns/update", start.elapsed().as_millis(), n, start.elapsed().as_nanos() / n as u128, )); } nutmeg-0.1.4/examples/clear.rs000064400000000000000000000011461046102023000144010ustar 00000000000000//! Example of [nutmeg::View::clear]. use std::thread::sleep; use std::time::Duration; struct Model { i: usize, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("count: {}", self.i) } } fn main() { let options = nutmeg::Options::default(); let view = nutmeg::View::new(Model { i: 0 }, options); for _i in 1..=5 { view.update(|state| state.i += 1); sleep(Duration::from_millis(600)); // bar disappears, but will reappear on the next update. view.clear(); sleep(Duration::from_millis(600)); } } nutmeg-0.1.4/examples/colored.rs000064400000000000000000000017461046102023000147500ustar 00000000000000//! Example of colored progress bars and output. use std::io::Write; use std::thread; use std::time; use yansi::Color; use yansi::Paint; struct Model { i: usize, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("count: {}", Paint::yellow(self.i)) } } fn main() { let options = nutmeg::Options::default(); let mut view = nutmeg::View::new(Model { i: 0 }, options); for i in 1..=45 { view.update(|state| state.i += 1); if i % 5 == 0 { writeln!( view, "{}", Paint::new(format!( "{} {}", Paint::new("item").italic(), Paint::new(i).underline() )) .wrap() .fg(Color::White) .bold() .bg(Color::Blue) ) .unwrap(); } thread::sleep(time::Duration::from_millis(300)); } } nutmeg-0.1.4/examples/count_but_with_disabled_progress.rs000064400000000000000000000011221046102023000221150ustar 00000000000000//! Example that when the progress bar is disabled, the app can still //! call `View::update` but nothing is drawn. #[allow(unused_imports)] use std::io::Write; struct Model { i: usize, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("count: {}", self.i) } } fn main() { let options = nutmeg::Options::default().progress_enabled(false); let view = nutmeg::View::new(Model { i: 0 }, options); for _i in 1..=5 { view.update(|state| state.i += 1); } // Should show nothing because progress is disabled } nutmeg-0.1.4/examples/count_up.rs000064400000000000000000000007621046102023000151520ustar 00000000000000//! Example of a simple progress bar that counts up. use std::thread::sleep; use std::time::Duration; struct Model { i: usize, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("count: {}", self.i) } } fn main() { let options = nutmeg::Options::default(); let view = nutmeg::View::new(Model { i: 0 }, options); for _i in 1..=5 { view.update(|state| state.i += 1); sleep(Duration::from_millis(300)); } } nutmeg-0.1.4/examples/final_message.rs000064400000000000000000000011301046102023000161010ustar 00000000000000//! Example of a simple progress bar that counts up. use std::thread::sleep; use std::time::Duration; struct Model { i: usize, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("count: {}", self.i) } fn final_message(&mut self) -> String { format!("done, {} steps executed", self.i) } } fn main() { let options = nutmeg::Options::default(); let view = nutmeg::View::new(Model { i: 0 }, options); for _i in 1..=5 { view.update(|state| state.i += 1); sleep(Duration::from_millis(300)); } } nutmeg-0.1.4/examples/fizzbuzz.rs000064400000000000000000000014631046102023000152120ustar 00000000000000//! Example of mixing progress updates with printed output. use std::io::Write; use std::thread; use std::time; struct Model { i: usize, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("count: {}", self.i) } } fn main() { let options = nutmeg::Options::default(); let mut view = nutmeg::View::new(Model { i: 0 }, options); for i in 1..=25 { view.update(|state| state.i += 1); if i % 15 == 0 { view.message("fizzbuzz\n"); } else if i % 3 == 0 { view.message("fizz\n"); } else if i % 5 == 0 { // Alternatively, you can treat it as a destination for Write. writeln!(view, "buzz").unwrap(); } thread::sleep(time::Duration::from_millis(300)); } } nutmeg-0.1.4/examples/identical_updates_suppressed.rs000064400000000000000000000012571046102023000212540ustar 00000000000000//! This model repeatedly generates the same text: it's a counter that //! only shows the hundreds. //! //! Nutmeg avoids redrawing the bar on every update to avoid flickering //! (on terminals that don't handle this well themselves.) use std::thread::sleep; use std::time::Duration; struct Model { i: usize, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("count: {}", self.i / 100) } } fn main() { let options = nutmeg::Options::default(); let view = nutmeg::View::new(Model { i: 0 }, options); for _i in 1..=5000 { view.update(|state| state.i += 1); sleep(Duration::from_millis(5)); } } nutmeg-0.1.4/examples/linear_model.rs000064400000000000000000000005651046102023000157510ustar 00000000000000// Copyright 2022 Martin Pool pub fn main() { let total = 99; let progress = nutmeg::View::new( nutmeg::models::LinearModel::new("Counting raindrops", total), nutmeg::Options::default(), ); for _i in 0..=total { progress.update(|model| model.increment(1)); std::thread::sleep(std::time::Duration::from_millis(100)); } } nutmeg-0.1.4/examples/multiline.rs000064400000000000000000000020071046102023000153120ustar 00000000000000//! Multi-line progress. use std::thread::sleep; use std::time::{Duration, Instant}; use yansi::Paint; struct Model { i: usize, start: Instant, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { let long_text = self.i.to_string().repeat(40 - self.i); format!( " count: {}\n bar: {}\nelapsed: {:.1}s\n blink: {}\n long: {}", self.i, "*".repeat(self.i), self.start.elapsed().as_secs_f32(), if self.i % 2 == 0 { Paint::red("XXX") } else { Paint::yellow("XXX") }, long_text, ) } } fn main() { let options = nutmeg::Options::default().update_interval(Duration::from_millis(50)); let model = Model { i: 0, start: Instant::now(), }; let view = nutmeg::View::new(model, options); for _i in 1..=40 { view.update(|state| state.i += 1); sleep(Duration::from_millis(200)); } } nutmeg-0.1.4/examples/multithreaded.rs000064400000000000000000000043561046102023000161540ustar 00000000000000// Copyright 2022 Martin Pool. //! Demonstrate multiple threads writing to a single view. //! //! A single View is shared in an Arc across all threads. (A scoped thread //! would also work.) //! //! Each thread periodically updates the model, which will make it repaint //! subject to the update rate limit. use std::fmt::Write; use std::sync::Arc; use std::thread::{self, sleep}; use std::time::Duration; use rand::Rng; const THREAD_WORK_MAX: usize = 20; /// Per-thread progress. struct JobState { x: usize, complete: bool, } /// Overall task progress. struct Model { job_state: Vec, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { let mut s = String::new(); let n_jobs = self.job_state.len(); let n_complete = self.job_state.iter().filter(|j| j.complete).count(); writeln!(s, "{n_complete}/{n_jobs} complete").unwrap(); for (i, js) in self.job_state.iter().enumerate() { let remains = THREAD_WORK_MAX - js.x; writeln!(s, "{:3}: {}{}", i, "#".repeat(js.x), "_".repeat(remains)).unwrap(); } s } } fn work(i_thread: usize, arc_view: Arc>) { let mut rng = rand::thread_rng(); for j in 0..=THREAD_WORK_MAX { arc_view.update(|model| model.job_state[i_thread].x = j); sleep(Duration::from_millis(rng.gen_range(100..600))); } arc_view.update(|model| model.job_state[i_thread].complete = true); } fn main() { let model = Model { job_state: Vec::new(), }; let view = nutmeg::View::new(model, nutmeg::Options::default()); view.update(|_m| ()); let arc_view = Arc::new(view); let mut join_handles = Vec::new(); for i_thread in 0..20 { arc_view.update(|model| { model.job_state.push(JobState { x: 0, complete: false, }) }); sleep(Duration::from_millis(100)); let give_arc_view = arc_view.clone(); join_handles.push(thread::spawn(move || work(i_thread, give_arc_view))); } for join_handle in join_handles { arc_view.update(|_m| ()); join_handle.join().unwrap(); } arc_view.update(|_| ()); sleep(Duration::from_millis(500)); } nutmeg-0.1.4/examples/only_print.rs000064400000000000000000000007421046102023000155110ustar 00000000000000//! Example of the simplest case: just printing message, no actual //! progress bar. use std::io::Write; struct Model {} impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { unreachable!("Model::render should never be called, since the progress bar is disabled"); } } fn main() { let mut view = nutmeg::View::new(Model {}, nutmeg::Options::default()); for i in 1..=5 { writeln!(view, "write line {i}").unwrap(); } } nutmeg-0.1.4/examples/print_holdoff.rs000064400000000000000000000021061046102023000161450ustar 00000000000000//! Progress bar updates are delayed after printing. //! //! This example sets a very long print_holdoff to make //! the effect more noticeable. After each message, the updates //! to the model do take effect but they're not drawn to the //! screen. use std::io; use std::io::Write; use std::thread; use std::time; use std::time::Duration; fn main() -> io::Result<()> { let options = nutmeg::Options::default() .print_holdoff(Duration::from_millis(1000)) .update_interval(Duration::from_millis(0)); let mut view = nutmeg::View::new(0usize, options); for _i in 0..5 { for j in 0..4 { writeln!(view, "message {j}")?; thread::sleep(time::Duration::from_millis(100)); } for j in 0..20 { view.update(|state| { // Previous updates were applied even though // they may not have been painted. assert!(j == 0 || *state == (j - 1)); *state = j }); thread::sleep(time::Duration::from_millis(100)); } } Ok(()) } nutmeg-0.1.4/examples/print_partial_lines.rs000064400000000000000000000022771046102023000173630ustar 00000000000000//! Partial lines can be printed, and the progress bar does not overwrite them. use std::io::{self, Write}; use std::thread::sleep; use std::time::Duration; struct Model { i: usize, legal: bool, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { assert!(self.legal); format!("progress: {}", self.i) } } fn zz() { sleep(Duration::from_millis(300)); } fn main() -> io::Result<()> { let options = nutmeg::Options::default(); let model = Model { i: 0, legal: true }; let mut view = nutmeg::View::new(model, options); for i in 1..=5 { view.update(|model| model.i += 1); zz(); write!(view, "partial output {i}... ")?; zz(); view.update(|model| model.i += 1); zz(); write!(view, "more... ")?; zz(); view.update(|model| model.i += 1); zz(); write!(view, "more... ")?; zz(); view.update(|model| model.i += 1); zz(); writeln!(view, "done!")?; view.update(|model| model.i += 1); zz(); for _ in 0..4 { view.update(|model| model.i += 1); zz(); } } Ok(()) } nutmeg-0.1.4/examples/rate_limited.rs000064400000000000000000000013201046102023000157470ustar 00000000000000//! Fast updates to the model can be rate-limited in the display. use std::thread::sleep; use std::time::Duration; struct Model { i: usize, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("count: {}", self.i) } } fn main() { for update_interval in [20, 50, 100, 250, 1000] { println!("update_interval={update_interval}ms"); let options = nutmeg::Options::default().update_interval(Duration::from_millis(update_interval)); let view = nutmeg::View::new(Model { i: 0 }, options); for _i in 1..=500 { view.update(|state| state.i += 1); sleep(Duration::from_millis(5)); } } } nutmeg-0.1.4/examples/right_size_bar.rs000064400000000000000000000017411046102023000163070ustar 00000000000000//! The render function is passed the terminal width and can use it to make things //! fit nicely. use std::thread::sleep; use std::time::{Duration, Instant}; struct Model { i: usize, start_time: Instant, } impl nutmeg::Model for Model { fn render(&mut self, width: usize) -> String { let start = format!("i={} | ", self.i); let end = format!(" | {:.3}s", self.start_time.elapsed().as_secs_f64()); let fill_len = width - start.len() - end.len(); let mut fill: Vec = vec![b'.'; fill_len]; fill[self.i % fill_len] = b'~'; let fill: String = String::from_utf8(fill).unwrap(); format!("{start}{fill}{end}") } } fn main() { let options = nutmeg::Options::default(); let state = Model { i: 0, start_time: Instant::now(), }; let view = nutmeg::View::new(state, options); for _ in 1..=120 { view.update(|state| state.i += 1); sleep(Duration::from_millis(100)); } } nutmeg-0.1.4/examples/static_view.rs000064400000000000000000000024661046102023000156420ustar 00000000000000//! Demonstrates that you can have a View in a static global variable. //! //! This works even when the View is accessed by multiple threads, because //! it synchronizes internally. use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering::Relaxed; use std::thread::{self, sleep}; use std::time::Duration; // Note: The initial model must also be `const`, so you cannot call `Default::default()`. // Note: Similarly, you can call `nutmeg::Options::new()` but not `Options::default()`. static VIEW: nutmeg::View = nutmeg::View::new( Model { i: AtomicUsize::new(0), }, nutmeg::Options::new(), ); #[derive(Default)] struct Model { i: AtomicUsize, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("i={}", self.i.load(Relaxed)) } } fn main() -> std::io::Result<()> { thread::scope(|scope| { for tid in 0..3 { scope.spawn(move || { VIEW.message(format!("thread {} starting\n", tid)); for _i in 0..20 { VIEW.update(|model| model.i.fetch_add(1, Relaxed)); sleep(Duration::from_millis(rand::random::() % 200)); } VIEW.message(format!("thread {} done\n", tid)); }); } }); Ok(()) } nutmeg-0.1.4/examples/stderr.rs000064400000000000000000000017451046102023000146230ustar 00000000000000//! Draw to stderr. Try this with stdout redirected to a file. use std::thread::sleep; use std::time::{Duration, Instant}; struct Model { i: usize, start_time: Instant, } impl nutmeg::Model for Model { fn render(&mut self, width: usize) -> String { let start = format!("i={} | ", self.i); let end = format!(" | {:.3}s", self.start_time.elapsed().as_secs_f64()); let fill_len = width - start.len() - end.len(); let mut fill: Vec = vec![b'.'; fill_len]; fill[self.i % fill_len] = b'~'; let fill: String = String::from_utf8(fill).unwrap(); format!("{start}{fill}{end}") } } fn main() { let options = nutmeg::Options::default().destination(nutmeg::Destination::Stderr); let state = Model { i: 0, start_time: Instant::now(), }; let view = nutmeg::View::new(state, options); for _ in 1..=120 { view.update(|state| state.i += 1); sleep(Duration::from_millis(30)); } } nutmeg-0.1.4/examples/string_as_model.rs000064400000000000000000000007451046102023000164700ustar 00000000000000//! Use a simple String as a Model, with no need to `impl Model`. use std::fs::read_dir; use std::io; use std::thread::sleep; use std::time::Duration; fn main() -> io::Result<()> { let options = nutmeg::Options::default(); let view = nutmeg::View::new(String::new(), options); for p in read_dir(".")? { let dir_entry = p?; view.update(|model| *model = dir_entry.path().display().to_string()); sleep(Duration::from_millis(300)); } Ok(()) } nutmeg-0.1.4/examples/suspend.rs000064400000000000000000000011161046102023000147710ustar 00000000000000//! Suspend updates for a while. use std::thread::sleep; use std::time::Duration; struct Model { i: usize, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("count: {}", self.i) } } fn main() { let options = nutmeg::Options::default(); let view = nutmeg::View::new(Model { i: 0 }, options); for i in 1..=16 { if i == 4 { view.suspend(); } else if i == 10 { view.resume(); } view.update(|state| state.i = i); sleep(Duration::from_millis(300)); } } nutmeg-0.1.4/examples/unbounded_model.rs000064400000000000000000000005321046102023000164540ustar 00000000000000// Copyright 2022 Martin Pool pub fn main() { let progress = nutmeg::View::new( nutmeg::models::UnboundedModel::new("Counting raindrops"), nutmeg::Options::default(), ); for _i in 0..=99 { progress.update(|model| model.increment(1)); std::thread::sleep(std::time::Duration::from_millis(100)); } } nutmeg-0.1.4/examples/wide_progress_truncated.rs000064400000000000000000000017141046102023000202410ustar 00000000000000//! Example showing that progress bar that are too wide for the terminal are //! horizontally truncated, even if `State::render` ignores the advised width. use std::thread::sleep; use std::time::Duration; struct Model { i: usize, width: usize, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { let mut s = format!("i={} | ", self.i); let ii = self.i % self.width; for _ in 0..ii { s.push('_'); } s.push('🦀'); for _ in (ii + 1)..self.width { s.push('_'); } s } } fn main() { let options = nutmeg::Options::default().update_interval(Duration::from_millis(50)); let model = Model { i: 0, width: 120 }; let view = nutmeg::View::new(model, options); for _ in 1..=360 { view.update(|state| state.i += 1); sleep(Duration::from_millis(100)); } // Should show nothing because progress is disabled } nutmeg-0.1.4/src/ansi.rs000064400000000000000000000016511046102023000132170ustar 00000000000000// Copyright 2022 Martin Pool. //! Draw ANSI escape sequences. // References: // * #![allow(unused)] use std::borrow::Cow; pub(crate) const MOVE_TO_START_OF_LINE: &str = "\x1b[1G"; // https://vt100.net/docs/vt510-rm/DECAWM pub(crate) const DISABLE_LINE_WRAP: &str = "\x1b[?7l"; pub(crate) const ENABLE_LINE_WRAP: &str = "\x1b[?7h"; pub(crate) const CLEAR_TO_END_OF_LINE: &str = "\x1b[0K"; pub(crate) const CLEAR_CURRENT_LINE: &str = "\x1b[2K"; pub(crate) const CLEAR_TO_END_OF_SCREEN: &str = "\x1b[0J"; pub(crate) fn up_n_lines_and_home(n: usize) -> Cow<'static, str> { if n > 0 { format!("\x1b[{n}F").into() } else { MOVE_TO_START_OF_LINE.into() } } #[cfg(windows)] pub(crate) fn enable_windows_ansi() -> bool { crate::windows::enable_windows_ansi() } #[cfg(not(windows))] pub(crate) fn enable_windows_ansi() -> bool { true } nutmeg-0.1.4/src/destination.rs000064400000000000000000000027551046102023000146140ustar 00000000000000// Copyright 2022-2023 Martin Pool. use std::env; use std::result::Result; #[allow(unused)] // for docstrings use crate::View; use crate::{ansi, width}; /// Destinations for progress bar output. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Destination { /// Draw to stdout. Stdout, /// Draw to stderr. Stderr, /// Draw to an internal capture buffer, which can be retrieved with [View::captured_output]. /// /// This is intended for testing. /// /// A width of 80 columns is used. Capture, } impl Destination { /// Determine if this destination is possible, and, if necessary, enable Windows ANSI support. pub(crate) fn initalize(&self) -> Result<(), ()> { if match self { Destination::Stdout => { atty::is(atty::Stream::Stdout) && !is_dumb_term() && ansi::enable_windows_ansi() } Destination::Stderr => { atty::is(atty::Stream::Stderr) && !is_dumb_term() && ansi::enable_windows_ansi() } Destination::Capture => true, } { Ok(()) } else { Err(()) } } pub(crate) fn width(&self) -> Option { match self { Destination::Stdout => width::stdout_width(), Destination::Stderr => width::stderr_width(), Destination::Capture => Some(80), } } } fn is_dumb_term() -> bool { env::var("TERM").map_or(false, |s| s.eq_ignore_ascii_case("dumb")) } nutmeg-0.1.4/src/helpers.rs000064400000000000000000000026641046102023000137340ustar 00000000000000// Copyright 2022 Martin Pool //! Helpful functions for drawing progress bars. use std::time::{Duration, Instant}; fn duration_brief(d: Duration) -> String { let secs = d.as_secs(); if secs >= 120 { format!("{} min", secs / 60) } else { format!("{secs} sec") } } /// Estimate by linear extrapolation the time remaining for a task with a given /// start time, number of completed items and number of total items. /// /// The result is in the format "33 sec" or "12 min". This format may change in /// future releases before 1.0. /// /// If the remaining time is not estimatable, returns "??". pub fn estimate_remaining(start: &Instant, done: usize, total: usize) -> String { let elapsed = start.elapsed(); if total == 0 || done == 0 || elapsed.is_zero() || done > total { "??".into() } else { let done = done as f64; let total = total as f64; let estimate = Duration::from_secs_f64(elapsed.as_secs_f64() * (total / done - 1.0)); duration_brief(estimate) } } /// Return a string representation of the percentage of work completed. /// /// ``` /// use nutmeg::percent_done; /// assert_eq!(percent_done(6, 12), "50.0%"); /// assert_eq!(percent_done(0, 0), "??%"); /// ``` pub fn percent_done(done: usize, total: usize) -> String { if total == 0 || done > total { "??%".into() } else { format!("{:.1}%", done as f64 * 100.0 / total as f64) } } nutmeg-0.1.4/src/lib.rs000064400000000000000000001000051046102023000130240ustar 00000000000000// Copyright 2022-2023 Martin Pool. /*! Nutmeg draws multi-line terminal progress bars to an ANSI terminal. By contrast to other Rust progress-bar libraries, Nutmeg has no built-in concept of what the progress bar or indicator should look like: the application has complete control. # Concept Nutmeg has three key types: Model, View, and Options. ## Model A type implementing the [Model] trait holds whatever information is needed to draw the progress bars. This might be the start time of the operation, the number of things processed, the amount of data transmitted or received, the currently active tasks, whatever... The Model can be any of these things, from simplest to most powerful: 1. Any type that implements [std::fmt::Display], such as a String or integer. 2. One of the provided [models]. 3. An application-defined struct (or enum or other type) that implements [Model]. The model is responsible for rendering itself into a String, optionally with ANSI styling, by implementing [Model::render] (or [std::fmt::Display]). Applications might choose to use any of the Rust crates that can render ANSI control codes into a string, such as yansi. The application is responsible for deciding whether or not to color its output, for example by consulting `$CLICOLORS` or its own command line. Models can optionally provide a "final message" by implementing [Model::final_message], which will be left on the screen when the view is finished. If one overall operation represents several concurrent operations then the application can, for example, represent them in a collection within the Model, and render them into multiple lines, or multiple sections in a single line. (See `examples/multithreaded.rs`.) ## View To get the model on to the terminal the application must create a [View], typically with [View::new], passing the initial model. The view takes ownership of the model. The application then updates the model state via [View::update], which may decide to paint the view to the terminal, subject to rate-limiting and other constraints. The view has an internal mutex and is `Send` and `Sync`, so it can be shared freely across threads. The view automatically erases itself from the screen when it is dropped. While the view is on the screen, the application can print messages interleaved with the progress bar by either calling [View::message], or treating it as a [std::io::Write] destination, for example for [std::writeln]. Errors in writing to the terminal cause a panic. ## Options A small [Options] type, passed to the View constructor, allows turning progress bars off, setting rate limits, etc. In particular applications might choose to construct all [Options] from a single function that respects an application-level option for whether progress bars should be drawn. ## Utility functions This crate also provides a few free functions such as [estimate_remaining], that can be helpful in implementing [Model::render]. # Example ``` use std::io::Write; // to support write!() // 1. Define a struct holding all the application state necessary to // render the progress bar. #[derive(Default)] struct Model { i: usize, total: usize, last_file_name: String, } // 2. Define how to render the progress bar as a String. impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("{}/{}: {}", self.i, self.total, self.last_file_name) } } fn main() -> std::io::Result<()> { // 3. Create a View when you want to draw a progress bar. let mut view = nutmeg::View::new(Model::default(), nutmeg::Options::default()); // 4. As the application runs, update the model via the view. let total_work = 100; view.update(|model| model.total = total_work); for i in 0..total_work { view.update(|model| { model.i += 1; model.last_file_name = format!("file{}.txt", i); }); // 5. Interleave text output lines by writing messages to the view. if i % 10 == 3 { view.message(format!("reached {}", i)); } } // 5. The bar is automatically erased when dropped. Ok(()) } ``` See the `examples/` directory for more. # Performance Nutmeg's goal is that [View::update] is cheap enough that applications can call it fairly freely when there are small updates. The library takes care of rate-limiting updates to the terminal, as configured in the [Options]. Each call to [View::update] will take a `parking_lot` mutex and check the system time, in addition to running the callback and some function-call overhead. The model is only rendered to a string, and the string printed to a terminal, if sufficient time has passed since it was last painted. The `examples/bench.rs` sends updates as fast as possible to a model containing a single `u64`, from a single thread. As of 2022-03-22, on a 2019 Core i9 Macbook Pro, it takes about 500ms to send 10e6 updates, or 50ns/update. # Integration with `tracing` Nutmeg can be used to draw progress bars in a terminal interleaved with [tracing](https://docs.rs/tracing/) messages. The progress bar is automatically temporarily removed to show messages, and repainted after the next update, subject to rate limiting and the holdoff time configured in [Options]. `Arc>` implicitly implements [`tracing_subscriber::fmt::writer::MakeWriter`](https://docs.rs/tracing-subscriber/0.3.17/tracing_subscriber/fmt/writer/trait.MakeWriter.html) and so can be passed to `tracing_subscriber::fmt::layer().with_writer()`. For example: ```rust use std::sync::Arc; use tracing::Level; use tracing_subscriber::prelude::*; struct Model { count: usize } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { todo!() } } let model = Model { count: 0, }; let view = Arc::new(nutmeg::View::new(model, nutmeg::Options::new())); let layer = tracing_subscriber::fmt::layer() .with_ansi(true) .with_writer(Arc::clone(&view)) .with_target(false) .with_timer(tracing_subscriber::fmt::time::uptime()) .with_filter(tracing_subscriber::filter::LevelFilter::from_level( Level::INFO, )); tracing_subscriber::registry().with(layer).init(); for i in 0..10 { if i % 10 == 0 { tracing::info!(i, "cats adored"); } view.update(|m| m.count += 1); std::thread::sleep(std::time::Duration::from_millis(100)); } ``` See `examples/tracing` for a runnable example. # Project status Nutmeg is a young library. Although the API will not break gratuitously, it may evolve in response to experience and feedback in every pre-1.0 release. If the core ideas prove useful and the API remains stable for an extended period then the author intends to promote it to 1.0, after which the API will respect Rust stability conventions. Changes are described in the [changelog](#Changelog) in the top-level Rustdoc, below. Constructive feedback on integrations that work well, or that don't work well, is welcome. # Potential future features * Draw updates from a background thread, so that it will keep ticking even if not actively updated, and to better handle applications that send a burst of updates followed by a long pause. The background thread will eventually paint the last drawn update. * Also set the window title from the progress model, perhaps by a different render function? */ #![warn(missing_docs)] use std::fmt::Display; use std::io::{self, Write}; use std::sync::Arc; use std::time::{Duration, Instant}; use parking_lot::Mutex; mod ansi; mod destination; mod helpers; pub mod models; mod width; #[cfg(windows)] mod windows; pub mod _changelog { #![doc = include_str!("../NEWS.md")] #[allow(unused_imports)] use super::*; // so that hyperlinks work } pub use crate::helpers::*; pub use destination::Destination; /// An application-defined type that holds whatever state is relevant to the /// progress bar, and that can render it into one or more lines of text. pub trait Model { /// Render this model into a string to draw on the console. /// /// This is called by the View when it wants to repaint the screen /// after [View::update] was called. /// /// Future versions of this library may call this function from a different /// thread. /// /// The `width` argument advises the model rendering code of the width of /// the terminal. The `render` implementation may make us of this to, for /// example, draw a full-width progress bar, or to selectively truncate /// sections within the line. /// /// The model may also ignore the `width` parameter and return a string /// of any width, in which case it will be truncated to fit on the /// screen. /// /// The rendered version may contain ANSI escape sequences for coloring, /// etc, but should not move the cursor. /// /// Lines are separarated by `\n`. If there is a final `\n` it is ignored. /// /// # Example /// /// ``` /// struct Model { i: usize, total: usize } /// /// impl nutmeg::Model for Model { /// fn render(&mut self, _width: usize) -> String { /// format!("phase {}/{}", self.i, self.total) /// } /// } /// ``` fn render(&mut self, width: usize) -> String; /// Optionally render a final message when the view is finished. /// /// For example this could be used to print the amount of work done /// after the work is complete. /// /// By default this prints nothing. /// /// The final message may contain ANSI styling and may be multiple lines, /// but it should not have a final newline, unless a trailing blank line /// is desired. /// /// This is called by [View::finish] or when the view is dropped. /// The final message is not printed when the view is abandoned by /// [View::abandon]. fn final_message(&mut self) -> String { String::new() } } /// Blanket implementation of Model for Display. /// /// `self` is converted to a display string without regard for /// the terminal width. /// /// This allows direct use of e.g. a String or integer as a model /// for very basic progress indications. /// /// ``` /// use nutmeg::{Options, View}; /// /// let view = View::new(0, Options::default()); /// view.update(|model| *model += 1); /// ``` impl Model for T where T: Display, { fn render(&mut self, _width: usize) -> String { self.to_string() } } /// A view that draws and coordinates a progress bar on the terminal. /// /// There should be only one `View` active on a terminal at any time, and /// while it's in use it should be the only channel by which output is /// printed. /// /// The View may be shared freely across threads: it internally /// synchronizes updates. /// /// # Printing text lines /// /// The View implements [std::io::Write] and so can be used by e.g. /// [std::writeln] to print non-progress output lines. /// /// The progress bar is removed from the screen to make room /// for the printed output. /// /// Printed output is emitted even if the progress bar is not enabled. /// /// It is OK to print incomplete lines, i.e. without a final `\n` /// character. In this case the progress bar remains suspended /// until the line is completed. /// /// ## Static views /// /// Views can be constructed as static variables, and used from multiple threads. /// /// Note that `Default::default()` is not `const` so cannot be used to construct /// either your model or the `Options`. /// /// For example: /// ``` /// static VIEW: nutmeg::View = nutmeg::View::new(Model { i: 0 }, nutmeg::Options::new()); /// /// struct Model { /// i: usize, /// } /// /// impl nutmeg::Model for Model { /// fn render(&mut self, _width: usize) -> String { /// format!("i={}", self.i) /// } /// } /// /// fn main() -> std::io::Result<()> { /// for i in 0..20 { /// VIEW.update(|model| model.i = i); /// if i % 5 == 0 { /// // Note: You cannot use writeln!() here, because its argument must be /// // `&mut`, but you can send messages. /// VIEW.message(&format!("message: i={i}\n")); /// } /// std::thread::sleep(std::time::Duration::from_millis(20)); /// } /// Ok(()) /// } /// /// ``` pub struct View { /// The real state of the view. /// /// The contents are always Some unless the View has been explicitly destroyed, /// in which case this makes Drop a no-op. inner: Mutex>>, } impl View { /// Construct a new progress view, drawn to stdout. /// /// `model` is the application-defined initial model. The View takes /// ownership of the model, after which the application can update /// it through [View::update]. /// /// `options` can typically be `Options::default`. /// /// On Windows, this enables use of ANSI sequences for styling stdout. /// /// Even if progress bars are enabled in the [Options], they will be /// disabled under some conditions: /// * If stdout is not a tty, /// * On Windows, if ANSI sequences cannot be enabled. /// * If the `$TERM` environment variable is `DUMB`. /// /// This constructor arranges that output from the progress view will be /// captured by the Rust test framework and not leak to stdout, but /// detection of whether to show progress bars may not work correctly. pub const fn new(model: M, options: Options) -> View { View { inner: Mutex::new(Some(InnerView::new(model, options))), } } /// Stop using this progress view. /// /// If the progress bar is currently visible, it will be left behind on the /// screen. /// /// Returns the model. pub fn abandon(self) -> M { // Mark it as not drawn (even if it is) so that Drop will not try to // hide it. self.inner .lock() .take() .expect("inner state is still present") .abandon() .unwrap() } /// Erase the model from the screen (if drawn), destroy it, and return the model. pub fn finish(self) -> M { self.inner .lock() .take() .expect("inner state is still present") .finish() } /// Update the model, and possibly redraw the screen to reflect the /// update. /// /// The progress bar may be repainted with the results of the update, /// if all these conditions are true: /// /// * The view is not suspended (by [View::suspend]). /// * Progress bars are enabled by [Options::progress_enabled]. /// * The terminal seems capable of drawing progress bars. /// * The progress bar was not drawn too recently, as controlled by /// [Options::update_interval]. /// * A message was not printed too recently, as controlled by /// [Options::print_holdoff]. /// * An incomplete message line isn't pending: in other words the /// last message written to the view, if any, had a final newline. /// /// If the view decides to repaint the progress bar it will call /// [Model::render]. In a future release redrawing may be done on a /// different thread. /// /// The `update_fn` may return a value, and this is returned from /// `update`. pub fn update(&self, update_fn: U) -> R where U: FnOnce(&mut M) -> R, { self.inner.lock().as_mut().unwrap().update(update_fn) } /// Hide the progress bar if it's currently drawn, and leave it /// hidden until [View::resume] is called. pub fn suspend(&self) { self.inner.lock().as_mut().unwrap().suspend().unwrap() } /// Remove the progress bar if it's currently drawn, but allow it /// to be redrawn when the model is next updated. pub fn clear(&self) { self.inner.lock().as_mut().unwrap().clear().unwrap() } /// Allow the progress bar to be drawn again, reversing the effect /// of [View::suspend]. pub fn resume(&self) { self.inner.lock().as_mut().unwrap().resume().unwrap() } /// Set the value of the fake clock, for testing. /// /// Panics if [Options::fake_clock] was not previously set. /// /// Moving the clock backwards in time may cause a panic. pub fn set_fake_clock(&self, fake_clock: Instant) { self.inner .lock() .as_mut() .unwrap() .set_fake_clock(fake_clock) } /// Inspect the view's model. /// /// The function `f` is applied to the model, and then the result /// of `f` is returned by `inspect_model`. /// /// ``` /// use nutmeg::{Options, View}; /// /// let view = View::new(10, Options::default()); /// view.update(|model| *model += 3); /// assert_eq!(view.inspect_model(|m| *m), 13); /// ``` pub fn inspect_model(&self, f: F) -> R where F: FnOnce(&mut M) -> R, { f(&mut self.inner.lock().as_mut().unwrap().model) } /// Print a message to the view. /// /// The progress bar, if present, is removed to print the message /// and then remains off for a time controlled by [Options::print_holdoff]. /// /// The message may contain ANSI control codes for styling. /// /// The message may contain multiple lines. /// /// Typically the message should end with `\n`. /// /// If the last character of the message is *not* `\n` then the incomplete /// line remains on the terminal, and the progress bar will not be painted /// until it is completed by a message finishing in `\n`. /// /// This is equivalent to `write!(view, ...)` except: /// * [std::io::Write::write] requires a `&mut View`, while `message` /// can be called on a `&View`. /// * `message` panics on an error writing to the terminal; `write!` requires /// the caller to handle a `Result`. /// * `write!` integrates string formatting; `message` does not, and typically /// would be called with the results of `format!()`. /// /// ``` /// use nutmeg::{Options, View}; /// /// let view = View::new(0, Options::default()); /// // ... /// view.message(format!("{} splines reticulated\n", 42)); /// ``` pub fn message>(&self, message: S) { self.inner .lock() .as_mut() .unwrap() .write(message.as_ref().as_bytes()) .expect("writing message"); } /// Print a message from a byte buffer. /// /// This is the same as [View::message] but takes an `AsRef<[u8]>`, such as a slice. /// /// Most destinations will expect the contents to be UTF-8. /// /// ``` /// use nutmeg::{Options, View}; /// /// let view = View::new("model content", Options::default()); /// view.message_bytes(b"hello crow\n"); /// ``` pub fn message_bytes>(&self, message: S) { self.inner .lock() .as_mut() .unwrap() .write(message.as_ref()) .expect("writing message"); } /// If the view's destination is [Destination::Capture], returns the buffer /// of captured output. /// /// Panics if the destination is not [Destination::Capture]. /// /// The buffer is returned in an Arc so that it remains valid after the View /// is dropped. /// /// This is intended for use in testing. /// /// # Example /// /// ``` /// use nutmeg::{Destination, Options, View}; /// /// let view = View::new(0, Options::default().destination(Destination::Capture)); /// let output = view.captured_output(); /// view.message("Captured message\n"); /// drop(view); /// assert_eq!(output.lock().as_str(), "Captured message\n"); /// ``` pub fn captured_output(&self) -> Arc> { self.inner.lock().as_mut().unwrap().captured_output() } } impl io::Write for &View { fn write(&mut self, buf: &[u8]) -> io::Result { if buf.is_empty() { return Ok(0); } self.inner.lock().as_mut().unwrap().write(buf) } fn flush(&mut self) -> io::Result<()> { Ok(()) } } impl io::Write for View { fn write(&mut self, buf: &[u8]) -> io::Result { if buf.is_empty() { return Ok(0); } self.inner.lock().as_mut().unwrap().write(buf) } fn flush(&mut self) -> io::Result<()> { Ok(()) } } impl Drop for View { fn drop(&mut self) { // Only try lock here: don't hang if it's locked or panic // if it's poisoned. And, do nothing if the View has already been // finished, in which case the contents of the Mutex will be None. if let Some(mut inner_guard) = self.inner.try_lock() { if let Some(inner) = Option::take(&mut inner_guard) { inner.finish(); } } } } /// The real contents of a View, inside a mutex. struct InnerView { /// Current application model. model: M, /// True if the progress bar is suspended, and should not be drawn. suspended: bool, /// Whether the progress bar is drawn, etc. state: State, options: Options, /// The current time on the fake clock, if it is enabled. fake_clock: Option, /// Captured output, if active. capture_buffer: Option>>, } #[derive(Debug, PartialEq, Eq, Clone)] enum State { /// Nothing has ever been painted, and the screen has not yet been initialized. New, /// Progress is not visible and nothing was recently printed. None, /// Progress bar is currently displayed. ProgressDrawn { /// Last time it was drawn. last_drawn_time: Instant, /// Number of lines the cursor is below the line where the progress bar /// should next be drawn. cursor_y: usize, /// The rendered string last drawn. last_drawn_string: String, }, /// Messages were written, and the progress bar is not visible. Printed { last_printed: Instant }, /// An incomplete message line has been printed, so the progress bar can't /// be drawn until it's removed. IncompleteLine, } impl InnerView { const fn new(model: M, options: Options) -> InnerView { InnerView { capture_buffer: None, fake_clock: None, model, options, state: State::New, suspended: false, } } fn finish(mut self) -> M { let _ = self.clear(); let final_message = self.model.final_message(); if !final_message.is_empty() { self.write_output(&format!("{final_message}\n")); } self.model } fn abandon(mut self) -> io::Result { match self.state { State::ProgressDrawn { .. } => { self.write_output("\n"); } State::New | State::IncompleteLine | State::None | State::Printed { .. } => (), } self.state = State::None; // so that drop does not attempt to erase Ok(self.model) } /// Return the real or fake clock. fn clock(&self) -> Instant { self.fake_clock.unwrap_or_else(Instant::now) } fn init_destination(&mut self) { if self.state == State::New { if self.options.destination.initalize().is_err() { // This destination doesn't want to draw progress bars, so stay off forever. self.options.progress_enabled = false; } self.state = State::None; } } fn paint_progress(&mut self) -> io::Result<()> { self.init_destination(); if !self.options.progress_enabled || self.suspended { return Ok(()); } let now = self.clock(); match self.state { State::IncompleteLine => return Ok(()), State::New | State::None => (), State::Printed { last_printed } => { if now - last_printed < self.options.print_holdoff { return Ok(()); } } State::ProgressDrawn { last_drawn_time, .. } => { if now - last_drawn_time < self.options.update_interval { return Ok(()); } } } if let Some(width) = self.options.destination.width() { let mut rendered = self.model.render(width); if rendered.ends_with('\n') { // Handle models that incorrectly add a trailing newline, rather than // leaving a blank line. (Maybe we should just let them fix it, and // be simpler?) rendered.pop(); } let mut buf = String::new(); if let State::ProgressDrawn { ref last_drawn_string, cursor_y, .. } = self.state { if *last_drawn_string == rendered { return Ok(()); } buf.push_str(&ansi::up_n_lines_and_home(cursor_y)); } buf.push_str(ansi::DISABLE_LINE_WRAP); buf.push_str(ansi::CLEAR_TO_END_OF_SCREEN); buf.push_str(&rendered); self.write_output(&buf); let cursor_y = rendered.as_bytes().iter().filter(|b| **b == b'\n').count(); self.state = State::ProgressDrawn { last_drawn_time: now, last_drawn_string: rendered, cursor_y, }; } Ok(()) } /// Hide the progress bar and leave it hidden until it is resumed. fn suspend(&mut self) -> io::Result<()> { self.suspended = true; self.clear() } fn resume(&mut self) -> io::Result<()> { self.suspended = false; self.paint_progress() } /// Clear the progress bars off the screen, leaving it ready to /// print other output. fn clear(&mut self) -> io::Result<()> { match self.state { State::ProgressDrawn { cursor_y, .. } => { self.write_output(&format!( "{}{}{}", ansi::up_n_lines_and_home(cursor_y), ansi::CLEAR_TO_END_OF_SCREEN, ansi::ENABLE_LINE_WRAP, )); self.state = State::None; } State::None | State::New | State::IncompleteLine | State::Printed { .. } => {} } Ok(()) } fn update(&mut self, update_fn: U) -> R where U: FnOnce(&mut M) -> R, { let r = update_fn(&mut self.model); self.paint_progress().unwrap(); r } fn write(&mut self, buf: &[u8]) -> io::Result { if buf.is_empty() { return Ok(0); } self.init_destination(); self.clear()?; self.state = if buf.ends_with(b"\n") { State::Printed { last_printed: self.clock(), } } else { State::IncompleteLine }; self.write_output(std::str::from_utf8(buf).expect("message is not UTF-8")); Ok(buf.len()) } /// Set the value of the fake clock, for testing. fn set_fake_clock(&mut self, fake_clock: Instant) { assert!(self.options.fake_clock, "Options.fake_clock is not enabled"); self.fake_clock = Some(fake_clock); } fn write_output(&mut self, buf: &str) { match &mut self.options.destination { Destination::Stdout => { print!("{buf}"); io::stdout().flush().unwrap(); } Destination::Stderr => { eprint!("{buf}"); io::stderr().flush().unwrap(); } Destination::Capture => { self.capture_buffer .get_or_insert_with(|| Arc::new(Mutex::new(String::new()))) .lock() .push_str(buf); } } } fn captured_output(&mut self) -> Arc> { self.capture_buffer .get_or_insert_with(|| Arc::new(Mutex::new(String::new()))) .clone() } } /// Options controlling a View. /// /// These are supplied to a constructor like [View::new], and cannot be changed after the view is created. /// /// The default options created by [Options::default] should be reasonable /// for most applications. /// /// # Example /// /// ``` /// let options = nutmeg::Options::default() /// .progress_enabled(false); // Don't draw bars, only print. /// ``` /// /// Options can be constructed as a static or constant value, using [Options::new]. /// /// ``` /// use std::time::Duration; /// use nutmeg::Options; /// /// static NUTMEG_OPTIONS: Options = Options::new() /// .update_interval(Duration::from_millis(100)) /// .progress_enabled(true) /// .destination(nutmeg::Destination::Stderr); /// ``` #[derive(Debug, Clone)] pub struct Options { /// Target interval to repaint the progress bar. update_interval: Duration, /// How long to wait after printing output before drawing the progress bar again. print_holdoff: Duration, /// Is the progress bar drawn at all? progress_enabled: bool, /// Use a fake clock for testing. fake_clock: bool, /// Write progress and messages to stdout, stderr, or a capture buffer for tests? destination: Destination, } impl Options { /// Return some reasonable default options. /// /// The update interval and print holdoff are 100ms, the progress bar is enabled, /// and output is sent to stdout. pub const fn new() -> Options { Options { update_interval: Duration::from_millis(100), print_holdoff: Duration::from_millis(100), progress_enabled: true, fake_clock: false, destination: Destination::Stdout, } } /// Set whether the progress bar will be drawn. /// /// By default it is drawn, except that this value will be ignored by [View::new] if stdout is not a terminal. pub const fn progress_enabled(self, progress_enabled: bool) -> Options { Options { progress_enabled, ..self } } /// Set the minimal interval to repaint the progress bar. /// /// `Duration::ZERO` can be used to cause the bar to repaint on every update. pub const fn update_interval(self, update_interval: Duration) -> Options { Options { update_interval, ..self } } /// Set the minimal interval between printing a message and painting /// the progress bar. /// /// This is used to avoid the bar flickering if the application is /// repeatedly printing messages at short intervals. /// /// `Duration::ZERO` can be used to disable this behavior. pub const fn print_holdoff(self, print_holdoff: Duration) -> Options { Options { print_holdoff, ..self } } /// Enable use of a fake clock, for testing. /// /// When true, all calculations of when to repaint use the fake /// clock rather than the real system clock. /// /// The fake clock begins at [Instant::now()] when the [View] is /// constructed. /// /// If this is enabled the fake clock can be updated with /// [View::set_fake_clock]. pub const fn fake_clock(self, fake_clock: bool) -> Options { Options { fake_clock, ..self } } /// Set whether progress bars are drawn to stdout, stderr, or an internal capture buffer. /// /// [Destination::Stdout] is the default. /// /// [Destination::Stderr] may be useful for programs that expect stdout to be redirected /// to a file and that want to draw progress output that is not captured by the /// redirection. pub const fn destination(self, destination: Destination) -> Options { Options { destination, ..self } } } impl Default for Options { /// Create default reasonable view options. /// /// This is the same as [Options::new]. fn default() -> Options { Options::new() } } nutmeg-0.1.4/src/models.rs000064400000000000000000000167551046102023000135630ustar 00000000000000// Copyright 2022 Martin Pool //! Generally reusable models for Nutmeg. //! //! These are provided because they may be easy to use for many applications //! that do not (yet) want to customize the progress display. There is no //! requirement to use them: they only implement the public [Model] interface. use std::borrow::Cow; use std::time::{Duration, Instant}; #[allow(unused)] // For docstrings use crate::View; use crate::{estimate_remaining, percent_done, Model}; /// A Nutmeg progress model that concatenates a pair of strings to render /// the progress bar. /// /// For example, the prefix could be a description of the operation, and the /// suffix could be the name of the file or object that's being processed. pub struct StringPair { prefix: Cow<'static, str>, suffix: Cow<'static, str>, } impl StringPair { /// Construct a new StringPair model, providing initial values for the /// two strings. /// /// ``` /// let progress_bar = nutmeg::View::new( /// nutmeg::models::StringPair::new("Copying: ",""), /// nutmeg::Options::default(), /// ); /// // ... /// progress_bar.update(|model| model.set_suffix("/etc/hostname")); /// ``` pub fn new(prefix: S1, suffix: S2) -> StringPair where S1: Into>, S2: Into>, { StringPair { prefix: prefix.into(), suffix: suffix.into(), } } /// Update the second string. /// /// Typically this should be called from a callback passed to [View::update]. pub fn set_suffix(&mut self, suffix: S) where S: Into>, { self.suffix = suffix.into(); } } impl Model for StringPair { fn render(&mut self, _width: usize) -> String { format!("{}{}", self.prefix, self.suffix) } } /// A model for completion of a number of approximately equal-sized tasks, /// with a percentage completion and extrapolated time to completion. /// /// The rendered result looks like this: /// /// ```text /// Counting raindrops: 68/99, 68.7%, 3 sec remaining /// ``` /// /// /// Run `cargo run --examples linear_model` in the Nutmeg source tree to see this in action. /// /// # Example /// /// ``` /// let total = 99; /// let progress = nutmeg::View::new( /// nutmeg::models::LinearModel::new("Counting raindrops", total), /// nutmeg::Options::default(), /// ); /// for i in 1..=total { /// progress.update(|model| model.increment(1)); /// } /// ``` pub struct LinearModel { done: usize, total: usize, message: Cow<'static, str>, start: Instant, } impl LinearModel { /// Construct a new model with a prefix string and number of total work items. pub fn new>>(message: S, total: usize) -> LinearModel { LinearModel { done: 0, total, message: message.into(), start: Instant::now(), } } /// Update the total amount of expected work. pub fn set_total(&mut self, total: usize) { self.total = total } /// Update the amount of work done. /// /// This should normally be called from a callback passed to [View::update]. pub fn set_done(&mut self, done: usize) { self.done = done } /// Update the amount of work done by an increment (typically 1). /// /// This should normally be called from a callback passed to [View::update]. /// pub fn increment(&mut self, i: usize) { self.done += i } } impl Model for LinearModel { fn render(&mut self, _width: usize) -> String { format!( "{}: {}/{}, {}, {} remaining", self.message, self.done, self.total, percent_done(self.done, self.total), estimate_remaining(&self.start, self.done, self.total) ) } } /// A model that counts up the amount of work done, with no known total, showing the elapsed time. /// /// Run `cargo run --examples unbounded_model` in the Nutmeg source tree to see this in action. /// /// # Example /// ``` /// let progress = nutmeg::View::new( /// nutmeg::models::UnboundedModel::new("Counting raindrops"), /// nutmeg::Options::default(), /// ); /// for _i in 0..=99 { /// progress.update(|model| model.increment(1)); /// } /// ``` pub struct UnboundedModel { message: Cow<'static, str>, done: usize, start: Instant, } impl UnboundedModel { /// Construct a model with a message describing the type of work being done. pub fn new>>(message: S) -> UnboundedModel { UnboundedModel { done: 0, message: message.into(), start: Instant::now(), } } /// Update the amount of work done. /// /// This should normally be called from a callback passed to [View::update]. pub fn set_done(&mut self, done: usize) { self.done = done } /// Update the amount of work done by an increment (typically 1). /// /// This should normally be called from a callback passed to [View::update]. /// pub fn increment(&mut self, i: usize) { self.done += i } } impl Model for UnboundedModel { fn render(&mut self, _width: usize) -> String { format!( "{}: {} in {}", self.message, self.done, format_duration(self.start.elapsed()) ) } } fn format_duration(d: Duration) -> String { let elapsed_secs = d.as_secs(); if elapsed_secs >= 3600 { format!( "{}:{:02}:{:02}", elapsed_secs / 3600, (elapsed_secs / 60) % 60, elapsed_secs % 60 ) } else { format!("{}:{:02}", (elapsed_secs / 60) % 60, elapsed_secs % 60) } } /// A model that stores any user-provided type, and renders by calling a function /// provided in the constructor. /// /// For many simple cases this avoids any need to explicitly declare a model /// class: instead the [View::new] call can, in-line, construct a BasicView /// giving an initial value and a render function. /// /// # Example /// ``` /// let view = nutmeg::View::new( /// nutmeg::models::BasicModel::new((0, 10), |(a, b)| format!("{}/{} complete", a, b)), /// nutmeg::Options::default(), /// ); /// for _i in 0..10 { /// // Note that the callback should update `model.value`, which is the user-defined /// // type. /// view.update(|model| model.value.0 += 1); /// // ... /// } /// ``` pub struct BasicModel where R: FnMut(&mut T) -> String, { /// The current inner value of the model. /// /// The type `T` and initial value are set by the first parameter to /// [BasicModel::new]. /// /// The functions passed to [View::update] take a `model` as a parameter /// and should typically act on `model.value`. pub value: T, render_fn: R, } impl BasicModel where R: FnMut(&mut T) -> String, { /// Construct a new BasicModel. /// /// `value` is the initial inner value of the model. It may be any type /// but might typically be an integer, a string, or a tuple of simple /// values. /// /// `render_fn` takes an `&mut T` and renders it to a string to be /// drawn in the progress bar. pub fn new(value: T, render_fn: R) -> BasicModel { BasicModel { value, render_fn } } } impl Model for BasicModel where R: FnMut(&mut T) -> String, { fn render(&mut self, _width: usize) -> String { (self.render_fn)(&mut self.value) } } nutmeg-0.1.4/src/width.rs000064400000000000000000000014321046102023000134010ustar 00000000000000// Copyright 2022-2023 Martin Pool //! Measure terminal width. use terminal_size::Width; #[cfg(unix)] pub(crate) fn stdout_width() -> Option { terminal_size::terminal_size_using_fd(1).map(|(Width(w), _)| w as usize) } #[cfg(windows)] pub(crate) fn stdout_width() -> Option { // TODO: We could get the handle for stderr to make this more precise... terminal_size::terminal_size().map(|(Width(w), _)| w as usize) } #[cfg(unix)] pub(crate) fn stderr_width() -> Option { terminal_size::terminal_size_using_fd(2).map(|(Width(w), _)| w as usize) } #[cfg(windows)] pub(crate) fn stderr_width() -> Option { // TODO: We could get the handle for stderr to make this more precise... terminal_size::terminal_size().map(|(Width(w), _)| w as usize) } nutmeg-0.1.4/src/windows.rs000064400000000000000000000010201046102023000137450ustar 00000000000000use std::sync::atomic::{AtomicBool, Ordering}; static WINDOWS_TRIED: AtomicBool = AtomicBool::new(false); static WINDOWS_SUCCEEDED: AtomicBool = AtomicBool::new(false); pub(crate) fn enable_windows_ansi() -> bool { if WINDOWS_TRIED.load(Ordering::SeqCst) { WINDOWS_SUCCEEDED.load(Ordering::SeqCst) } else { let succeeded = yansi::Paint::enable_windows_ascii(); WINDOWS_TRIED.store(true, Ordering::SeqCst); WINDOWS_SUCCEEDED.store(succeeded, Ordering::SeqCst); succeeded } } nutmeg-0.1.4/tests/api/identical_output_suppressed.rs000064400000000000000000000016401046102023000212400ustar 00000000000000//! Test that Nutmeg avoids redrawing the same text repeatedly. use std::time::Duration; use nutmeg::{Destination, Options, View}; struct Hundreds(usize); impl nutmeg::Model for Hundreds { fn render(&mut self, _width: usize) -> String { format!("hundreds={}", self.0 / 100) } } #[test] fn identical_output_suppressed() { let options = Options::default() .destination(Destination::Capture) .update_interval(Duration::ZERO); let view = View::new(Hundreds(0), options); let output = view.captured_output(); for i in 0..200 { // We change the model, but not in a way that will change what's displayed. view.update(|model| model.0 = i); } view.abandon(); // No erasure commands, just a newline after the last painted view. assert_eq!( output.lock().as_str(), "\x1b[?7l\x1b[0Jhundreds=0\x1b[1G\x1b[?7l\x1b[0Jhundreds=1\n" ); } nutmeg-0.1.4/tests/api/main.rs000064400000000000000000000141731046102023000143400ustar 00000000000000// Copyright 2022-2023 Martin Pool. //! API tests for Nutmeg. use std::io::Write; use std::thread::sleep; use std::time::{Duration, Instant}; use nutmeg::{Destination, Options, View}; mod identical_output_suppressed; struct MultiLineModel { i: usize, } // You can construct options as a static using const fns. static _SOME_OPTIONS: Options = Options::new() .update_interval(Duration::ZERO) .print_holdoff(Duration::from_millis(20)) .destination(Destination::Stderr) .fake_clock(false) .progress_enabled(true); // Just the default options are also OK. static _DEFAULT_OPTIONS: Options = Options::new(); impl nutmeg::Model for MultiLineModel { fn render(&mut self, _width: usize) -> String { format!(" count: {}\n bar: {}\n", self.i, "*".repeat(self.i),) } } #[test] fn draw_progress_once() { let model = MultiLineModel { i: 0 }; let options = Options::default().destination(Destination::Capture); let view = nutmeg::View::new(model, options); let output = view.captured_output(); view.update(|model| model.i = 1); drop(view); assert_eq!( output.lock().as_str(), "\x1b[?7l\x1b[0J count: 1\n bar: *\x1b[1F\x1b[0J\x1b[?7h" ); } #[test] fn abandoned_bar_is_not_erased() { let model = MultiLineModel { i: 0 }; let view = View::new(model, Options::default().destination(Destination::Capture)); let output = view.captured_output(); view.update(|model| model.i = 1); view.abandon(); // No erasure commands, just a newline after the last painted view. assert_eq!( output.lock().as_str(), "\x1b[?7l\x1b[0J count: 1\n bar: *\n" ); } #[test] fn suspend_and_resume() { struct Model(usize); impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { format!("XX: {}", self.0) } } let model = Model(0); let options = Options::default() .destination(Destination::Capture) .update_interval(Duration::ZERO); let view = nutmeg::View::new(model, options); let output = view.captured_output(); for i in 0..=4 { if i == 1 { view.suspend(); } else if i == 3 { view.resume(); } view.update(|model| model.0 = i); } view.abandon(); // No erasure commands, just a newline after the last painted view. // * 0 is painted before it's suspended. // * the bar is then erased // * 1 is never painted because the bar is suspended. // * 2 is also updated into the model while the bar is suspended, but then // it's resumed, so 2 is then painted. // * 3 and 4 are painted in the usual way. assert_eq!( output.lock().as_str(), "\x1b[?7l\x1b[0JXX: 0\ \x1b[1G\x1b[0J\x1b[?7h\ \x1b[?7l\x1b[0JXX: 2\ \x1b[1G\x1b[?7l\x1b[0JXX: 3\ \x1b[1G\x1b[?7l\x1b[0JXX: 4\n" ); } #[test] fn disabled_progress_is_not_drawn() { let model = MultiLineModel { i: 0 }; let options = Options::default() .destination(Destination::Capture) .progress_enabled(false); let view = nutmeg::View::new(model, options); let output = view.captured_output(); for i in 0..10 { view.update(|model| model.i = i); } drop(view); assert_eq!(output.lock().as_str(), ""); } #[test] fn disabled_progress_does_not_block_print() { let model = MultiLineModel { i: 0 }; let options = Options::default() .destination(Destination::Capture) .progress_enabled(false); let mut view = nutmeg::View::new(model, options); let output = view.captured_output(); for i in 0..2 { view.update(|model| model.i = i); writeln!(view, "print line {i}").unwrap(); } drop(view); assert_eq!(output.lock().as_str(), "print line 0\nprint line 1\n"); } /// If output is redirected, it should not be affected by the width of /// wherever stdout is pointing. #[test] fn default_width_when_not_on_stdout() { struct Model(); impl nutmeg::Model for Model { fn render(&mut self, width: usize) -> String { assert_eq!(width, 80); format!("width={width}") } } let model = Model(); let options = Options::default().destination(Destination::Capture); let view = nutmeg::View::new(model, options); let output = view.captured_output(); view.update(|_model| ()); drop(view); assert_eq!( output.lock().as_str(), "\x1b[?7l\x1b[0Jwidth=80\x1b[1G\x1b[0J\x1b[?7h" ); } #[test] fn rate_limiting_with_fake_clock() { struct Model { draw_count: usize, update_count: usize, } impl nutmeg::Model for Model { fn render(&mut self, _width: usize) -> String { self.draw_count += 1; format!("update:{} draw:{}", self.update_count, self.draw_count) } } let model = Model { draw_count: 0, update_count: 0, }; let options = Options::default() .destination(Destination::Capture) .fake_clock(true) .update_interval(Duration::from_millis(1)); let mut fake_clock = Instant::now(); let view = nutmeg::View::new(model, options); view.set_fake_clock(fake_clock); let output = view.captured_output(); // Any number of updates, but until the clock ticks only one will be drawn. for _i in 0..10 { view.update(|model| model.update_count += 1); sleep(Duration::from_millis(10)); } assert_eq!(view.inspect_model(|m| m.draw_count), 1); assert_eq!(view.inspect_model(|m| m.update_count), 10); // Time passes... fake_clock += Duration::from_secs(1); view.set_fake_clock(fake_clock); // Another burst of updates, and just one of them will be drawn. for _i in 0..10 { view.update(|model| model.update_count += 1); sleep(Duration::from_millis(10)); } assert_eq!(view.inspect_model(|m| m.draw_count), 2); assert_eq!(view.inspect_model(|m| m.update_count), 20); drop(view); assert_eq!( output.lock().as_str(), "\x1b[?7l\x1b[0Jupdate:1 draw:1\ \x1b[1G\ \x1b[?7l\x1b[0Jupdate:11 draw:2\ \x1b[1G\x1b[0J\x1b[?7h" ); } nutmeg-0.1.4/tests/captured_in_tests.rs000064400000000000000000000017171046102023000163620ustar 00000000000000// Copyright 2022 Martin Pool //! Test that output from the main View constructors is captured inside //! unit tests. //! //! These tests are not expcted to fail, themselves, but in older //! versions of nutmeg they would leak to the stdout of `cargo test`. //! //! `test_output_captured` runs these tests in a subprocess and //! checks that they don't leak. use std::io::Write; #[test] fn view_stdout_captured() { let mut view = nutmeg::View::new(String::new(), nutmeg::Options::default()); view.update(|model| *model = "stdout progress should be captured".into()); writeln!(view, "stdout message should be captured").unwrap(); } #[test] fn view_stderr_captured() { let mut view = nutmeg::View::new( String::new(), nutmeg::Options::default().destination(nutmeg::Destination::Stderr), ); view.update(|model| *model = "stderr progress should be captured".into()); writeln!(view, "stderr message should be captured").unwrap(); } nutmeg-0.1.4/tests/test_output_captured.rs000064400000000000000000000014021046102023000171200ustar 00000000000000// Copyright 2022 Martin Pool //! Test that Nutmeg output is captured within Rust tests. use std::env; use std::process::Command; /// Run the tests in a subprocess and check we don't see leakage on stdout. #[test] fn view_in_test_does_not_leak() { let cargo = env::var("CARGO").expect("$CARGO isn't set"); let output = Command::new(cargo) .args(["test", "--test", "captured_in_tests"]) .output() .expect("failed to spawn cargo"); let stdout_str = String::from_utf8_lossy(&output.stdout); let stderr_str = String::from_utf8_lossy(&output.stderr); println!("stdout:\n{stdout_str}\nstderr:\n{stderr_str}\n",); assert!(!stdout_str.contains("should be captured")); assert!(!stderr_str.contains("should be captured")); }