[
  {
    "path": ".dockerignore",
    "content": "target/\n.git/\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"cargo\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"daily\"\n    groups:\n      dependencies:\n        patterns:\n          - \"*\"\n  - package-ecosystem: \"github-actions\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"daily\"\n    groups:\n      dependencies:\n        patterns:\n          - \"*\"\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\n# GitHub recommends pinning actions to a commit SHA.\n# To get a newer version, you will need to update the SHA.\n# You can also reference a tag or branch, but the action may change without warning.\n\nname: Create and publish a Docker image\n\non:\n  push:\n    tags:\n      - gping-v*\n    branches:\n      - master\n  workflow_dispatch:\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\nconcurrency: build-docker-image\n\njobs:\n  build-and-push-image:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v6\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Log in to the Container registry\n        if: github.event_name == 'tag' || github.ref_name == 'master'\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          push: ${{ github.event_name == 'tag' || github.ref_name == 'master' }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: |\n            linux/arm64\n            linux/amd64\n            linux/arm/v7\n          provenance: true\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/homebrew.yml",
    "content": "name: Homebrew Bump\non:\n  push:\n    tags:\n      - 'gping-v*'\n      - '!gping-v*-post*'\n\njobs:\n  homebrew:\n    name: Bump Homebrew formula\n    runs-on: ubuntu-latest\n    steps:\n      - name: Extract version\n        id: extract-version\n        run: |\n          echo \"VERSION=${GITHUB_REF#refs/tags/gping-}\" >> \"$GITHUB_OUTPUT\"\n      - uses: mislav/bump-homebrew-formula-action@v3\n        with:\n          formula-name: gping\n          commit-message: |\n            gping ${{ steps.extract-version.outputs.VERSION }}\n\n            Created by https://github.com/mislav/bump-homebrew-formula-action\n        env:\n          COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "on:\n  push:\n    tags:\n      - gping-v*\n    branches:\n      - master\n  pull_request:\n  workflow_dispatch:\n\nname: CI\n\njobs:\n  cross_builds:\n    name: ${{ matrix.target }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - target: aarch64-apple-darwin\n            os: macos-latest\n          - target: x86_64-apple-darwin\n            os: macos-latest\n          - target: x86_64-pc-windows-msvc\n            os: windows-latest\n            archive: zip\n        os: [ 'ubuntu-24.04' ]\n        target:\n          - armv7-linux-androideabi\n          - armv7-unknown-linux-gnueabihf\n          - armv7-unknown-linux-musleabihf\n          - x86_64-unknown-linux-gnu\n          - x86_64-unknown-linux-musl\n          - aarch64-unknown-linux-gnu\n          - aarch64-unknown-linux-musl\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Install Rust\n        id: rust\n        uses: actions-rust-lang/setup-rust-toolchain@v1\n        with:\n          cache: 'false'\n          cache-on-failure: false\n          target: ${{ matrix.target }}\n\n      - name: Setup Rust Caching\n        uses: Swatinem/rust-cache@v2\n        with:\n          cache-on-failure: false\n          prefix-key: ${{ matrix.target }}\n          key: ${{ steps.rust.outputs.cachekey }}\n\n      - name: Test\n        uses: houseabsolute/actions-rust-cross@v1\n        with:\n          command: test\n          target: ${{ matrix.target }}\n          args: --locked\n\n      - name: Sanity check\n        if: matrix.target == 'x86_64-unknown-linux-gnu' || matrix.target == 'aarch64-apple-darwin' || matrix.target == 'x86_64-pc-windows-msvc'\n        run: cargo run --target ${{ matrix.target }} -- --help\n\n      - name: Build release\n        uses: houseabsolute/actions-rust-cross@v1\n        if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'\n        with:\n          command: build\n          target: ${{ matrix.target }}\n          args: --release --locked\n\n      - name: Publish artifacts and release\n        if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'\n        uses: houseabsolute/actions-rust-release@v1\n        with:\n          executable-name: gping\n          target: ${{ matrix.target }}\n          extra-files: gping.1\n          changes-file: \"\"\n\n  create_release:\n    name: Release\n    runs-on: ubuntu-latest\n    if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'\n    needs:\n      - cross_builds\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v6\n      - uses: actions/download-artifact@v7\n        with:\n          merge-multiple: true\n      - name: Publish\n        if: startsWith(github.ref, 'refs/tags/')\n        uses: softprops/action-gh-release@v2\n        with:\n          draft: false\n          files: |\n            gping.1\n            **/*.tar.gz\n            **/*.zip\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  checks:\n    name: Checks\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v6\n\n      - uses: actions/setup-python@v6\n        with:\n          python-version: '3.11'\n\n      - name: Install stable toolchain\n        uses: actions-rust-lang/setup-rust-toolchain@v1\n        with:\n          cache-on-failure: false\n          components: rustfmt,clippy\n\n      - name: Rustfmt Check\n        uses: actions-rust-lang/rustfmt@v1\n\n      - name: Run cargo check\n        if: success() || failure()\n        run: cargo check\n\n      - if: success() || failure()\n        run: cargo clippy --all-targets --all-features --locked -- -D warnings\n\n      - if: success() || failure()\n        uses: pre-commit/action@v3.0.1\n"
  },
  {
    "path": ".github/workflows/winget.yml",
    "content": "name: Publish to WinGet\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: Tag to release\n        required: true\n  release:\n    types: [ released ]\n\njobs:\n  publish:\n    runs-on: windows-latest\n    steps:\n      - uses: vedantmgoyal9/winget-releaser@main\n        with:\n          identifier: orf.gping\n          release-tag: '${{ github.event.release.tag_name || inputs.tag }}'\n          token: ${{ secrets.COMMITTER_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\n.idea/\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v5.0.0\n    hooks:\n      - id: check-yaml\n      - id: end-of-file-fixer\n      - id: trailing-whitespace\n        exclude: 'ping.1'\n  - repo: local\n    hooks:\n      - id: rustfmt\n        name: rustfmt\n        entry: cargo fmt -- --check\n        pass_filenames: false\n        language: system\n      - id: clippy\n        name: clippy\n        entry: cargo clippy --all-targets --all-features -- -D warnings\n        pass_filenames: false\n        language: system\n      - id: mangen\n        name: mangen\n        entry: env GENERATE_MANPAGE=\"gping.1\" cargo run\n        pass_filenames: false\n        language: system\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\n.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\n\nmembers = [\n    \"gping\",\n    \"pinger\"\n]\n\n[profile.release]\nlto = true\ncodegen-units = 1\n"
  },
  {
    "path": "Cross.toml",
    "content": "#[target.\"armv7-linux-androideabi\"]\n#pre-build = [\"apt-get update && apt-get install --assume-yes iputils-ping\"]\n#\n#[target.\"armv7-unknown-linux-gnueabihf\"]\n#pre-build = [\"apt-get update && apt-get install --assume-yes iputils-ping\"]\n#\n#[target.\"armv7-unknown-linux-musleabihf\"]\n#pre-build = [\"apt-get update && apt-get install --assume-yes iputils-ping\"]\n#\n#[target.\"aarch64-linux-android\"]\n#pre-build = [\"apt-get update && apt-get install --assume-yes iputils-ping\"]\n#\n#[target.\"aarch64-unknown-linux-gnu\"]\n#pre-build = [\"apt-get update && apt-get install --assume-yes iputils-ping\"]\n#\n#[target.\"aarch64-unknown-linux-musl\"]\n#pre-build = [\"apt-get update && apt-get install --assume-yes iputils-ping\"]\n#\n#[target.\"x86_64-unknown-linux-musl\"]\n#pre-build = [\"apt-get update && apt-get install --assume-yes iputils-ping\"]\n#\n\n[build]\npre-build = [\"apt-get update && apt-get install --assume-yes iputils-ping\"]\n\n[build.env]\npassthrough = [\"CI\", \"GITHUB_ACTIONS\"]\n"
  },
  {
    "path": "Dockerfile",
    "content": "# syntax=docker/dockerfile:1\n\nFROM rust:slim-bookworm AS builder\n\nWORKDIR /usr/src/gping\n\nCOPY gping/ gping/\nCOPY pinger/ pinger/\nCOPY Cargo.* ./\n\nRUN cargo install --locked --path ./gping\n\n\nFROM debian:bookworm-slim\n\nRUN apt-get update \\\n    && apt-get install -y iputils-ping \\\n    && rm -rf /var/lib/apt/lists/*\n\nCOPY --link --from=builder /usr/local/cargo/bin/gping /usr/local/bin/gping\n\nENTRYPOINT [\"gping\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Tom Forbes\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "gping/Cargo.toml",
    "content": "[package]\nname = \"gping\"\nversion = \"1.20.1\"\nauthors = [\"Tom Forbes <tom@tomforb.es>\"]\nedition = \"2018\"\nrepository = \"https://github.com/orf/gping\"\nlicense = \"MIT\"\ndescription = \"Ping, but with a graph.\"\nbuild = \"build.rs\"\nreadme = \"../readme.md\"\n\n[dependencies]\npinger = { version = \"^2.1.1\", path = \"../pinger\" }\ntui = { package = \"ratatui\", version = \"0.29.0\", features = [\"crossterm\"], default-features = false }\ncrossterm = \"0.29.0\"\nanyhow = \"1.0.101\"\nchrono = \"0.4.43\"\nitertools = \"0.14.0\"\nshadow-rs = { version = \"1.7.0\", default-features = false }\nconst_format = \"0.2.35\"\nclap = { version = \"4.5.57\", features = [\"derive\"] }\nclap_mangen = \"0.2.31\"\nidna = \"1.1.0\"\nclap-cargo = \"0.18.3\"\n\n[build-dependencies]\nshadow-rs = { version = \"1.7.0\"}\n"
  },
  {
    "path": "gping/build.rs",
    "content": "fn main() {\n    shadow_rs::ShadowBuilder::builder().build().unwrap();\n}\n"
  },
  {
    "path": "gping/src/colors.rs",
    "content": "use std::{iter::Iterator, ops::RangeFrom, str::FromStr};\n\nuse anyhow::{anyhow, Result};\nuse tui::style::Color;\n\npub struct Colors<T> {\n    already_used: Vec<Color>,\n    color_names: T,\n    indices: RangeFrom<u8>,\n}\n\nimpl<T> From<T> for Colors<T> {\n    fn from(color_names: T) -> Self {\n        Self {\n            already_used: Vec::new(),\n            color_names,\n            indices: 2..,\n        }\n    }\n}\n\nimpl<'a, T> Iterator for Colors<T>\nwhere\n    T: Iterator<Item = &'a String>,\n{\n    type Item = Result<Color>;\n\n    fn next(&mut self) -> Option<Self::Item> {\n        match self.color_names.next() {\n            Some(name) => match Color::from_str(name) {\n                Ok(color) => {\n                    if !self.already_used.contains(&color) {\n                        self.already_used.push(color);\n                    }\n                    Some(Ok(color))\n                }\n                error => Some(error.map_err(|err| {\n                    anyhow!(err).context(format!(\"Invalid color code: `{}`\", name))\n                })),\n            },\n            None => loop {\n                let index = unsafe { self.indices.next().unwrap_unchecked() };\n                let color = Color::Indexed(index);\n                if !self.already_used.contains(&color) {\n                    self.already_used.push(color);\n                    break Some(Ok(color));\n                }\n            },\n        }\n    }\n}\n"
  },
  {
    "path": "gping/src/main.rs",
    "content": "use crate::plot_data::PlotData;\nuse anyhow::{anyhow, bail, Context, Result};\nuse chrono::prelude::*;\nuse clap::{CommandFactory, Parser};\nuse crossterm::event::KeyModifiers;\nuse crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};\nuse crossterm::{\n    event::{self, Event as CEvent, KeyCode},\n    execute,\n    terminal::{disable_raw_mode, enable_raw_mode, SetSize},\n};\nuse itertools::{Itertools, MinMaxResult};\nuse pinger::{ping, PingOptions, PingResult};\nuse std::io;\nuse std::io::BufWriter;\nuse std::iter;\nuse std::net::{IpAddr, ToSocketAddrs};\nuse std::ops::Add;\nuse std::path::Path;\nuse std::process::{Command, ExitStatus, Stdio};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse std::sync::mpsc::Sender;\nuse std::sync::{mpsc, Arc};\nuse std::thread;\nuse std::thread::{sleep, JoinHandle};\nuse std::time::{Duration, Instant};\nuse tui::backend::{Backend, CrosstermBackend};\nuse tui::layout::{Constraint, Direction, Flex, Layout};\nuse tui::style::{Color, Style};\nuse tui::text::Span;\nuse tui::widgets::{Axis, Block, Borders, Chart, Dataset};\nuse tui::Terminal;\n\nmod colors;\nmod plot_data;\nmod region_map;\n\nuse colors::Colors;\nuse shadow_rs::{formatcp, shadow};\nuse tui::prelude::Position;\n\nshadow!(build);\n\nconst VERSION_INFO: &str = formatcp!(\n    r#\"{}\ncommit_hash: {}\nbuild_time: {}\nbuild_env: {},{}\"#,\n    build::PKG_VERSION,\n    build::SHORT_COMMIT,\n    build::BUILD_TIME,\n    build::RUST_VERSION,\n    build::RUST_CHANNEL\n);\n\n#[derive(Parser, Debug)]\n#[command(author, version=build::PKG_VERSION, name = \"gping\", about = \"Ping, but with a graph.\", long_version = VERSION_INFO, styles = clap_cargo::style::CLAP_STYLING\n)]\nstruct Args {\n    /// Graph the execution time for a list of commands rather than pinging hosts\n    #[arg(long)]\n    cmd: bool,\n\n    /// Watch interval seconds (provide partial seconds like '0.5'). Default for ping is 0.2, default for cmd is 0.5.\n    #[arg(short = 'n', long)]\n    watch_interval: Option<f32>,\n\n    /// Hosts or IPs to ping, or commands to run if --cmd is provided. Can use cloud shorthands like aws:eu-west-1.\n    #[arg(allow_hyphen_values = false)]\n    hosts_or_commands: Vec<String>,\n\n    /// Determines the number of seconds to display in the graph.\n    #[arg(short, long, default_value = \"30\")]\n    buffer: u64,\n    /// Resolve ping targets to IPv4 address\n    #[arg(short = '4', conflicts_with = \"ipv6\")]\n    ipv4: bool,\n    /// Resolve ping targets to IPv6 address\n    #[arg(short = '6', conflicts_with = \"ipv4\")]\n    ipv6: bool,\n\n    #[cfg(not(target_os = \"windows\"))]\n    /// Interface to use when pinging.\n    #[arg(short = 'i', long)]\n    interface: Option<String>,\n\n    /// Uses dot characters instead of braille\n    #[arg(short = 's', long, help = \"\")]\n    simple_graphics: bool,\n\n    /// Vertical margin around the graph (top and bottom)\n    #[arg(long, default_value = \"1\")]\n    vertical_margin: u16,\n\n    /// Horizontal margin around the graph (left and right)\n    #[arg(long, default_value = \"0\")]\n    horizontal_margin: u16,\n\n    #[arg(\n        name = \"color\",\n        short = 'c',\n        long = \"color\",\n        use_value_delimiter = true,\n        value_delimiter = ',',\n        help = r#\"Assign color to a graph entry.\n\nThis option can be defined more than once as a comma separated string, and the\norder which the colors are provided will be matched against the hosts or\ncommands passed to gping.\n\nHexadecimal RGB color codes are accepted in the form of '#RRGGBB' or the\nfollowing color names: 'black', 'red', 'green', 'yellow', 'blue', 'magenta',\n'cyan', 'gray', 'dark-gray', 'light-red', 'light-green', 'light-yellow',\n'light-blue', 'light-magenta', 'light-cyan', and 'white'\"#\n    )]\n    color_codes_or_names: Vec<String>,\n\n    /// Clear the graph from the terminal after closing the program\n    #[arg(name = \"clear\", long = \"clear\", action)]\n    clear: bool,\n\n    #[cfg(not(target_os = \"windows\"))]\n    /// Extra arguments to pass to `ping`. These are platform dependent.\n    #[arg(long, allow_hyphen_values = true, num_args = 0.., conflicts_with=\"cmd\")]\n    ping_args: Option<Vec<String>>,\n}\n\nstruct App {\n    data: Vec<PlotData>,\n    display_interval: chrono::Duration,\n    started: chrono::DateTime<Local>,\n}\n\nimpl App {\n    fn new(data: Vec<PlotData>, buffer: u64) -> Self {\n        App {\n            data,\n            display_interval: chrono::Duration::from_std(Duration::from_secs(buffer)).unwrap(),\n            started: Local::now(),\n        }\n    }\n\n    fn update(&mut self, host_idx: usize, item: Option<Duration>) {\n        let host = &mut self.data[host_idx];\n        host.update(item);\n    }\n\n    fn y_axis_bounds(&self) -> [f64; 2] {\n        // Find the Y axis bounds for our chart.\n        // This is trickier than the x-axis. We iterate through all our PlotData structs\n        // and find the min/max of all the values. Then we add a 10% buffer to them.\n        let (min, max) = match self\n            .data\n            .iter()\n            .flat_map(|b| b.data.as_slice())\n            .map(|v| v.1)\n            .filter(|v| !v.is_nan())\n            .minmax()\n        {\n            MinMaxResult::NoElements => (f64::INFINITY, 0_f64),\n            MinMaxResult::OneElement(elm) => (elm, elm),\n            MinMaxResult::MinMax(min, max) => (min, max),\n        };\n\n        // Add a 10% buffer to the top and bottom\n        let max_10_percent = (max * 10_f64) / 100_f64;\n        let min_10_percent = (min * 10_f64) / 100_f64;\n        [min - min_10_percent, max + max_10_percent]\n    }\n\n    fn x_axis_bounds(&self) -> [f64; 2] {\n        let now = Local::now();\n        let now_idx;\n        let before_idx;\n        if (now - self.started) < self.display_interval {\n            now_idx = (self.started + self.display_interval).timestamp_millis() as f64 / 1_000f64;\n            before_idx = self.started.timestamp_millis() as f64 / 1_000f64;\n        } else {\n            now_idx = now.timestamp_millis() as f64 / 1_000f64;\n            let before = now - self.display_interval;\n            before_idx = before.timestamp_millis() as f64 / 1_000f64;\n        }\n\n        [before_idx, now_idx]\n    }\n\n    fn x_axis_labels(&self, bounds: [f64; 2]) -> Vec<Span<'_>> {\n        let lower_utc = DateTime::<Utc>::from_timestamp(bounds[0] as i64, 0)\n            .expect(\"Error parsing x-axis bounds 0\");\n        let upper_utc = DateTime::<Utc>::from_timestamp(bounds[1] as i64, 0)\n            .expect(\"Error parsing x-asis bounds 1\");\n        let lower: DateTime<Local> = DateTime::from(lower_utc);\n        let upper: DateTime<Local> = DateTime::from(upper_utc);\n        let diff = (upper - lower) / 2;\n        let midpoint = lower + diff;\n        vec![\n            Span::raw(format!(\"{:?}\", lower.time())),\n            Span::raw(format!(\"{:?}\", midpoint.time())),\n            Span::raw(format!(\"{:?}\", upper.time())),\n        ]\n    }\n\n    fn y_axis_labels(&self, bounds: [f64; 2]) -> Vec<Span<'_>> {\n        // Create 7 labels for our y axis, based on the y-axis bounds we computed above.\n        let min = bounds[0];\n        let max = bounds[1];\n\n        let difference = max - min;\n        let num_labels = 7;\n        // Split difference into one chunk for each of the 7 labels\n        let increment = Duration::from_micros((difference / num_labels as f64) as u64);\n        let duration = Duration::from_micros(min as u64);\n\n        (0..num_labels)\n            .map(|i| Span::raw(format!(\"{:?}\", duration.add(increment * i))))\n            .collect()\n    }\n}\n\n#[derive(Debug)]\nenum Update {\n    Result(Duration),\n    Timeout,\n    Unknown,\n    Terminated(ExitStatus, String),\n}\n\nimpl From<PingResult> for Update {\n    fn from(result: PingResult) -> Self {\n        match result {\n            PingResult::Pong(duration, _) => Update::Result(duration),\n            PingResult::Timeout(_) => Update::Timeout,\n            PingResult::Unknown(_) => Update::Unknown,\n            PingResult::PingExited(e, stderr) => Update::Terminated(e, stderr),\n        }\n    }\n}\n\n#[derive(Debug)]\nenum Event {\n    Update(usize, Update),\n    Terminate,\n    Render,\n}\n\nfn start_render_thread(\n    kill_event: Arc<AtomicBool>,\n    cmd_tx: Sender<Event>,\n) -> JoinHandle<Result<()>> {\n    thread::spawn(move || {\n        while !kill_event.load(Ordering::Acquire) {\n            sleep(Duration::from_millis(250));\n            cmd_tx.send(Event::Render)?;\n        }\n        Ok(())\n    })\n}\n\nfn start_cmd_thread(\n    watch_cmd: &str,\n    host_id: usize,\n    watch_interval: Option<f32>,\n    cmd_tx: Sender<Event>,\n    kill_event: Arc<AtomicBool>,\n) -> JoinHandle<Result<()>> {\n    let mut words = watch_cmd.split_ascii_whitespace();\n    let cmd = words\n        .next()\n        .expect(\"Must specify a command to watch\")\n        .to_string();\n    let cmd_args = words.map(|w| w.to_string()).collect::<Vec<String>>();\n\n    let interval = Duration::from_millis((watch_interval.unwrap_or(0.5) * 1000.0) as u64);\n\n    // Pump cmd watches into the queue\n    thread::spawn(move || -> Result<()> {\n        while !kill_event.load(Ordering::Acquire) {\n            let start = Instant::now();\n            let mut child = Command::new(&cmd)\n                .args(&cmd_args)\n                .stderr(Stdio::null())\n                .stdout(Stdio::null())\n                .spawn()?;\n            let status = child.wait()?;\n            let duration = start.elapsed();\n            let update = if status.success() {\n                Update::Result(duration)\n            } else {\n                Update::Timeout\n            };\n            cmd_tx.send(Event::Update(host_id, update))?;\n            sleep(interval);\n        }\n        Ok(())\n    })\n}\n\nfn start_ping_thread(\n    options: PingOptions,\n    host_id: usize,\n    ping_tx: Sender<Event>,\n    kill_event: Arc<AtomicBool>,\n) -> Result<JoinHandle<Result<()>>> {\n    let stream = ping(options)?;\n    // Pump ping messages into the queue\n    Ok(thread::spawn(move || -> Result<()> {\n        while !kill_event.load(Ordering::Acquire) {\n            match stream.recv() {\n                Ok(v) => {\n                    ping_tx.send(Event::Update(host_id, v.into()))?;\n                }\n                Err(_) => {\n                    // Stream closed, just break\n                    return Ok(());\n                }\n            }\n        }\n        Ok(())\n    }))\n}\n\nfn get_host_ipaddr(host: &str, force_ipv4: bool, force_ipv6: bool) -> Result<String> {\n    let mut host = host.to_string();\n    if !host.is_ascii() {\n        let Ok(encoded_host) = idna::domain_to_ascii(&host) else {\n            bail!(\"Could not encode host {host} to punycode\")\n        };\n        host = encoded_host;\n    }\n    let ipaddr: Vec<_> = (host.as_str(), 80)\n        .to_socket_addrs()\n        .with_context(|| format!(\"Resolving {host}\"))?\n        .map(|s| s.ip())\n        .collect();\n    if ipaddr.is_empty() {\n        bail!(\"Could not resolve hostname {}\", host)\n    }\n    let ipaddr = if force_ipv4 {\n        ipaddr\n            .iter()\n            .find(|ip| matches!(ip, IpAddr::V4(_)))\n            .ok_or_else(|| anyhow!(\"Could not resolve '{}' to IPv4\", host))\n    } else if force_ipv6 {\n        ipaddr\n            .iter()\n            .find(|ip| matches!(ip, IpAddr::V6(_)))\n            .ok_or_else(|| anyhow!(\"Could not resolve '{}' to IPv6\", host))\n    } else {\n        ipaddr\n            .first()\n            .ok_or_else(|| anyhow!(\"Could not resolve '{}' to IP\", host))\n    };\n    Ok(ipaddr?.to_string())\n}\n\nfn generate_man_page(path: &Path) -> anyhow::Result<()> {\n    let man = clap_mangen::Man::new(Args::command().version(None).long_version(None));\n    let mut buffer: Vec<u8> = Default::default();\n    man.render(&mut buffer)?;\n\n    std::fs::write(path, buffer)?;\n    Ok(())\n}\n\nfn main() -> Result<()> {\n    if let Some(path) = std::env::var_os(\"GENERATE_MANPAGE\") {\n        return generate_man_page(Path::new(&path));\n    };\n    let args: Args = Args::parse();\n\n    if args.hosts_or_commands.is_empty() {\n        return Err(anyhow!(\"At least one host or command must be given (i.e gping google.com). Use --help for a full list of arguments.\"));\n    }\n\n    let mut data = vec![];\n\n    let colors = Colors::from(args.color_codes_or_names.iter());\n    let hosts_or_commands: Vec<String> = args\n        .hosts_or_commands\n        .clone()\n        .into_iter()\n        .map(|s| match region_map::try_host_from_cloud_region(&s) {\n            None => s,\n            Some(new_domain) => new_domain,\n        })\n        .collect();\n\n    for (host_or_cmd, color) in hosts_or_commands.iter().zip(colors) {\n        let color = color?;\n        let display = match args.cmd {\n            true => host_or_cmd.to_string(),\n            false => format!(\n                \"{} ({})\",\n                host_or_cmd,\n                get_host_ipaddr(host_or_cmd, args.ipv4, args.ipv6)?\n            ),\n        };\n        data.push(PlotData::new(\n            display,\n            args.buffer,\n            Style::default().fg(color),\n            args.simple_graphics,\n        ));\n    }\n\n    #[cfg(not(target_os = \"windows\"))]\n    let interface: Option<String> = args.interface.clone();\n    #[cfg(target_os = \"windows\")]\n    let interface: Option<String> = None;\n\n    #[cfg(not(target_os = \"windows\"))]\n    let ping_args: Option<Vec<String>> = args.ping_args.clone();\n    #[cfg(target_os = \"windows\")]\n    let ping_args: Option<Vec<String>> = None;\n\n    let (key_tx, rx) = mpsc::channel();\n\n    let mut threads = vec![];\n\n    let killed = Arc::new(AtomicBool::new(false));\n\n    for (host_id, host_or_cmd) in hosts_or_commands.iter().cloned().enumerate() {\n        if args.cmd {\n            let cmd_thread = start_cmd_thread(\n                &host_or_cmd,\n                host_id,\n                args.watch_interval,\n                key_tx.clone(),\n                std::sync::Arc::clone(&killed),\n            );\n            threads.push(cmd_thread);\n        } else {\n            let interval =\n                Duration::from_millis((args.watch_interval.unwrap_or(0.2) * 1000.0) as u64);\n\n            let mut ping_opts = if args.ipv4 {\n                PingOptions::new_ipv4(host_or_cmd, interval, interface.clone())\n            } else if args.ipv6 {\n                PingOptions::new_ipv6(host_or_cmd, interval, interface.clone())\n            } else {\n                PingOptions::new(host_or_cmd, interval, interface.clone())\n            };\n            if let Some(ping_args) = &ping_args {\n                ping_opts = ping_opts.with_raw_arguments(ping_args.clone());\n            }\n\n            threads.push(start_ping_thread(\n                ping_opts,\n                host_id,\n                key_tx.clone(),\n                std::sync::Arc::clone(&killed),\n            )?);\n        }\n    }\n    threads.push(start_render_thread(\n        std::sync::Arc::clone(&killed),\n        key_tx.clone(),\n    ));\n\n    let mut app = App::new(data, args.buffer);\n    enable_raw_mode()?;\n    let stdout = io::stdout();\n    let mut backend = CrosstermBackend::new(BufWriter::with_capacity(1024 * 1024 * 4, stdout));\n    let rect = backend.size()?;\n\n    if args.clear {\n        execute!(\n            backend,\n            SetSize(rect.width, rect.height),\n            EnterAlternateScreen,\n        )?;\n    } else {\n        execute!(backend, SetSize(rect.width, rect.height),)?;\n    }\n\n    let mut terminal = Terminal::new(backend)?;\n    terminal.clear()?;\n\n    // Pump keyboard messages into the queue\n    let killed_thread = std::sync::Arc::clone(&killed);\n    thread::spawn(move || -> Result<()> {\n        while !killed_thread.load(Ordering::Acquire) {\n            if event::poll(Duration::from_secs(5))? {\n                if let CEvent::Key(key) = event::read()? {\n                    match key.code {\n                        KeyCode::Char('q') | KeyCode::Esc => {\n                            key_tx.send(Event::Terminate)?;\n                            break;\n                        }\n                        KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {\n                            key_tx.send(Event::Terminate)?;\n                            break;\n                        }\n                        _ => {}\n                    }\n                }\n            }\n        }\n        Ok(())\n    });\n\n    loop {\n        match rx.recv()? {\n            Event::Update(host_id, update) => {\n                match update {\n                    Update::Result(duration) => app.update(host_id, Some(duration)),\n                    Update::Timeout => app.update(host_id, None),\n                    Update::Unknown => (),\n                    Update::Terminated(e, _) if e.success() => {\n                        break;\n                    }\n                    Update::Terminated(e, stderr) => {\n                        eprintln!(\"There was an error running ping: {e}\\nStderr: {stderr}\\n\");\n                        break;\n                    }\n                };\n            }\n            Event::Render => {\n                terminal.draw(|f| {\n                    let chunks = Layout::default()\n                        .flex(Flex::Legacy)\n                        .direction(Direction::Vertical)\n                        .vertical_margin(args.vertical_margin)\n                        .horizontal_margin(args.horizontal_margin)\n                        .constraints(\n                            std::iter::repeat_n(Constraint::Length(1), app.data.len())\n                                .chain(iter::once(Constraint::Percentage(10)))\n                                .collect::<Vec<_>>(),\n                        )\n                        .split(f.area());\n\n                    let total_chunks = chunks.len();\n\n                    let header_chunks = &chunks[0..total_chunks - 1];\n                    let chart_chunk = &chunks[total_chunks - 1];\n\n                    for (plot_data, chunk) in app.data.iter().zip(header_chunks) {\n                        let header_layout = Layout::default()\n                            .direction(Direction::Horizontal)\n                            .constraints(\n                                [\n                                    Constraint::Percentage(30),\n                                    Constraint::Percentage(10),\n                                    Constraint::Percentage(10),\n                                    Constraint::Percentage(10),\n                                    Constraint::Percentage(10),\n                                    Constraint::Percentage(10),\n                                    Constraint::Percentage(10),\n                                    Constraint::Percentage(10),\n                                ]\n                                .as_ref(),\n                            )\n                            .split(*chunk);\n\n                        for (area, paragraph) in header_layout.iter().zip(plot_data.header_stats())\n                        {\n                            f.render_widget(paragraph, *area);\n                        }\n                    }\n\n                    let datasets: Vec<Dataset> = app.data.iter().map(|d| d.into()).collect();\n\n                    let y_axis_bounds = app.y_axis_bounds();\n                    let x_axis_bounds = app.x_axis_bounds();\n\n                    let chart = Chart::new(datasets)\n                        .block(Block::default().borders(Borders::NONE))\n                        .x_axis(\n                            Axis::default()\n                                .style(Style::default().fg(Color::Gray))\n                                .bounds(x_axis_bounds)\n                                .labels(app.x_axis_labels(x_axis_bounds)),\n                        )\n                        .y_axis(\n                            Axis::default()\n                                .style(Style::default().fg(Color::Gray))\n                                .bounds(y_axis_bounds)\n                                .labels(app.y_axis_labels(y_axis_bounds)),\n                        );\n\n                    f.render_widget(chart, *chart_chunk)\n                })?;\n            }\n            Event::Terminate => {\n                killed.store(true, Ordering::Release);\n                break;\n            }\n        }\n    }\n    killed.store(true, Ordering::Relaxed);\n\n    disable_raw_mode()?;\n    execute!(terminal.backend_mut())?;\n    terminal.show_cursor()?;\n\n    let new_size = terminal.size()?;\n    terminal.set_cursor_position(Position {\n        x: new_size.width,\n        y: new_size.height,\n    })?;\n    for thread in threads {\n        thread.join().unwrap()?;\n    }\n\n    if args.clear {\n        execute!(terminal.backend_mut(), LeaveAlternateScreen)?;\n    };\n\n    Ok(())\n}\n"
  },
  {
    "path": "gping/src/plot_data.rs",
    "content": "use anyhow::Context;\nuse chrono::prelude::*;\nuse core::option::Option;\nuse core::option::Option::{None, Some};\nuse core::time::Duration;\nuse itertools::Itertools;\nuse tui::style::Style;\nuse tui::symbols;\nuse tui::widgets::{Dataset, GraphType, Paragraph};\n\npub struct PlotData {\n    pub display: String,\n    pub data: Vec<(f64, f64)>,\n    pub style: Style,\n    buffer: chrono::Duration,\n    simple_graphics: bool,\n}\n\nimpl PlotData {\n    pub fn new(display: String, buffer: u64, style: Style, simple_graphics: bool) -> PlotData {\n        PlotData {\n            display,\n            data: Vec::with_capacity(150),\n            style,\n            buffer: chrono::Duration::try_seconds(buffer as i64)\n                .with_context(|| format!(\"Error converting {buffer} to seconds\"))\n                .unwrap(),\n            simple_graphics,\n        }\n    }\n    pub fn update(&mut self, item: Option<Duration>) {\n        let now = Local::now();\n        let idx = now.timestamp_millis() as f64 / 1_000f64;\n        match item {\n            Some(dur) => self.data.push((idx, dur.as_micros() as f64)),\n            None => self.data.push((idx, f64::NAN)),\n        }\n        // Find the last index that we should remove.\n        let earliest_timestamp = (now - self.buffer).timestamp_millis() as f64 / 1_000f64;\n        let last_idx = self\n            .data\n            .iter()\n            .enumerate()\n            .filter(|(_, (timestamp, _))| *timestamp < earliest_timestamp)\n            .map(|(idx, _)| idx)\n            .next_back();\n        if let Some(idx) = last_idx {\n            self.data.drain(0..idx).for_each(drop)\n        }\n    }\n\n    pub fn header_stats(&self) -> Vec<Paragraph<'_>> {\n        let ping_header = Paragraph::new(self.display.clone()).style(self.style);\n        let items: Vec<&f64> = self\n            .data\n            .iter()\n            .filter(|(_, x)| !x.is_nan())\n            .map(|(_, v)| v)\n            .sorted_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))\n            .collect();\n        if items.is_empty() {\n            return vec![ping_header];\n        }\n\n        let min = **items.first().unwrap();\n        let max = **items.last().unwrap();\n        let avg = items.iter().copied().sum::<f64>() / items.len() as f64;\n        let jtr = items\n            .iter()\n            .zip(items.iter().skip(1))\n            .map(|(&prev, &curr)| (curr - prev).abs())\n            .sum::<f64>()\n            / (items.len() - 1) as f64;\n\n        let percentile_position = 0.95 * items.len() as f32;\n        let rounded_position = percentile_position.round() as usize;\n        let p95 = items.get(rounded_position).map(|i| **i).unwrap_or(0f64);\n\n        // count timeouts\n        let to = self.data.iter().filter(|(_, x)| x.is_nan()).count();\n\n        let last = self.data.last().unwrap_or(&(0f64, 0f64)).1;\n\n        vec![\n            ping_header,\n            Paragraph::new(format!(\"last {:?}\", Duration::from_micros(last as u64)))\n                .style(self.style),\n            Paragraph::new(format!(\"min {:?}\", Duration::from_micros(min as u64)))\n                .style(self.style),\n            Paragraph::new(format!(\"max {:?}\", Duration::from_micros(max as u64)))\n                .style(self.style),\n            Paragraph::new(format!(\"avg {:?}\", Duration::from_micros(avg as u64)))\n                .style(self.style),\n            Paragraph::new(format!(\"jtr {:?}\", Duration::from_micros(jtr as u64)))\n                .style(self.style),\n            Paragraph::new(format!(\"p95 {:?}\", Duration::from_micros(p95 as u64)))\n                .style(self.style),\n            Paragraph::new(format!(\"t/o {to:?}\")).style(self.style),\n        ]\n    }\n}\n\nimpl<'a> From<&'a PlotData> for Dataset<'a> {\n    fn from(plot: &'a PlotData) -> Self {\n        let slice = plot.data.as_slice();\n        Dataset::default()\n            .marker(if plot.simple_graphics {\n                symbols::Marker::Dot\n            } else {\n                symbols::Marker::Braille\n            })\n            .style(plot.style)\n            .graph_type(GraphType::Line)\n            .data(slice)\n    }\n}\n"
  },
  {
    "path": "gping/src/region_map.rs",
    "content": "type Host = String;\n\npub fn try_host_from_cloud_region(query: &str) -> Option<Host> {\n    match query.split_once(':') {\n        Some((\"aws\", region)) => Some(format!(\"ec2.{region}.amazonaws.com\")),\n        Some((\"gcp\", \"\")) => Some(\"cloud.google.com\".to_string()),\n        Some((\"gcp\", region)) => Some(format!(\"storage.{region}.rep.googleapis.com\")),\n        _ => None,\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn test_host_from_aws() {\n        assert_eq!(\n            try_host_from_cloud_region(\"aws:eu-west-1\"),\n            Some(\"ec2.eu-west-1.amazonaws.com\".to_string())\n        );\n    }\n\n    #[test]\n    fn test_host_from_gcp() {\n        assert_eq!(\n            try_host_from_cloud_region(\"gcp:me-central2\"),\n            Some(\"storage.me-central2.rep.googleapis.com\".to_string())\n        );\n        assert_eq!(\n            try_host_from_cloud_region(\"gcp:\"),\n            Some(\"cloud.google.com\".to_string())\n        );\n    }\n\n    #[test]\n    fn test_host_from_foo() {\n        assert_eq!(try_host_from_cloud_region(\"foo:bar\"), None);\n    }\n\n    #[test]\n    fn test_invalid_input() {\n        assert_eq!(try_host_from_cloud_region(\"foo\"), None);\n    }\n}\n"
  },
  {
    "path": "gping.1",
    "content": ".ie \\n(.g .ds Aq \\(aq\n.el .ds Aq '\n.TH gping 1  \"gping \" \n.SH NAME\ngping \\- Ping, but with a graph.\n.SH SYNOPSIS\n\\fBgping\\fR [\\fB\\-\\-cmd\\fR] [\\fB\\-n\\fR|\\fB\\-\\-watch\\-interval\\fR] [\\fB\\-b\\fR|\\fB\\-\\-buffer\\fR] [\\fB\\-4 \\fR] [\\fB\\-6 \\fR] [\\fB\\-i\\fR|\\fB\\-\\-interface\\fR] [\\fB\\-s\\fR|\\fB\\-\\-simple\\-graphics\\fR] [\\fB\\-\\-vertical\\-margin\\fR] [\\fB\\-\\-horizontal\\-margin\\fR] [\\fB\\-c\\fR|\\fB\\-\\-color\\fR] [\\fB\\-\\-clear\\fR] [\\fB\\-\\-ping\\-args\\fR] [\\fB\\-h\\fR|\\fB\\-\\-help\\fR] [\\fIHOSTS_OR_COMMANDS\\fR] \n.SH DESCRIPTION\nPing, but with a graph.\n.SH OPTIONS\n.TP\n\\fB\\-\\-cmd\\fR\nGraph the execution time for a list of commands rather than pinging hosts\n.TP\n\\fB\\-n\\fR, \\fB\\-\\-watch\\-interval\\fR \\fI<WATCH_INTERVAL>\\fR\nWatch interval seconds (provide partial seconds like \\*(Aq0.5\\*(Aq). Default for ping is 0.2, default for cmd is 0.5\n.TP\n\\fB\\-b\\fR, \\fB\\-\\-buffer\\fR \\fI<BUFFER>\\fR [default: 30]\nDetermines the number of seconds to display in the graph\n.TP\n\\fB\\-4\\fR\nResolve ping targets to IPv4 address\n.TP\n\\fB\\-6\\fR\nResolve ping targets to IPv6 address\n.TP\n\\fB\\-i\\fR, \\fB\\-\\-interface\\fR \\fI<INTERFACE>\\fR\nInterface to use when pinging\n.TP\n\\fB\\-s\\fR, \\fB\\-\\-simple\\-graphics\\fR\n\n.TP\n\\fB\\-\\-vertical\\-margin\\fR \\fI<VERTICAL_MARGIN>\\fR [default: 1]\nVertical margin around the graph (top and bottom)\n.TP\n\\fB\\-\\-horizontal\\-margin\\fR \\fI<HORIZONTAL_MARGIN>\\fR [default: 0]\nHorizontal margin around the graph (left and right)\n.TP\n\\fB\\-c\\fR, \\fB\\-\\-color\\fR \\fI<color>\\fR\nAssign color to a graph entry.\n\nThis option can be defined more than once as a comma separated string, and the\norder which the colors are provided will be matched against the hosts or\ncommands passed to gping.\n\nHexadecimal RGB color codes are accepted in the form of \\*(Aq#RRGGBB\\*(Aq or the\nfollowing color names: \\*(Aqblack\\*(Aq, \\*(Aqred\\*(Aq, \\*(Aqgreen\\*(Aq, \\*(Aqyellow\\*(Aq, \\*(Aqblue\\*(Aq, \\*(Aqmagenta\\*(Aq,\n\\*(Aqcyan\\*(Aq, \\*(Aqgray\\*(Aq, \\*(Aqdark\\-gray\\*(Aq, \\*(Aqlight\\-red\\*(Aq, \\*(Aqlight\\-green\\*(Aq, \\*(Aqlight\\-yellow\\*(Aq,\n\\*(Aqlight\\-blue\\*(Aq, \\*(Aqlight\\-magenta\\*(Aq, \\*(Aqlight\\-cyan\\*(Aq, and \\*(Aqwhite\\*(Aq\n.TP\n\\fB\\-\\-clear\\fR\nClear the graph from the terminal after closing the program\n.TP\n\\fB\\-\\-ping\\-args\\fR [\\fI<PING_ARGS>...\\fR]\nExtra arguments to pass to `ping`. These are platform dependent\n.TP\n\\fB\\-h\\fR, \\fB\\-\\-help\\fR\nPrint help\n.TP\n[\\fIHOSTS_OR_COMMANDS\\fR]\nHosts or IPs to ping, or commands to run if \\-\\-cmd is provided. Can use cloud shorthands like aws:eu\\-west\\-1\n.SH AUTHORS\nTom Forbes <tom@tomforb.es>\n"
  },
  {
    "path": "pinger/Cargo.toml",
    "content": "[package]\nname = \"pinger\"\nversion = \"2.1.1\"\nauthors = [\"Tom Forbes <tom@tomforb.es>\"]\nedition = \"2018\"\nlicense = \"MIT\"\ndescription = \"A small cross-platform library to execute the ping command and parse the output\"\nrepository = \"https://github.com/orf/gping/tree/master/pinger\"\n\n[dependencies]\nthiserror = \"2.0.18\"\nlazy-regex = \"3.5.1\"\nrand = { version = \"0.10.0\", optional = true }\n\n[target.'cfg(windows)'.dependencies]\nwinping = \"0.10.1\"\n\n[dev-dependencies]\nos_info = \"3.14.0\"\nntest = \"0.9.5\"\nanyhow = \"1.0.101\"\n\n[features]\ndefault = []\nfake-ping = [\"rand\"]\n"
  },
  {
    "path": "pinger/README.md",
    "content": "# pinger\n\n> A small cross-platform library to execute the ping command and parse the output.\n\nThis crate is primarily built for use with `gping`, but it can also be used as a\nstandalone library.\n\nThis allows you to reliably ping hosts without having to worry about process permissions,\nin a cross-platform manner on Windows, Linux and macOS.\n\n## Usage\n\nA full example of using the library can be found in the `examples/` directory, but the\ninterface is quite simple:\n\n```rust\nuse std::time::Duration;\nuse pinger::{ping, PingOptions};\n\nfn ping_google() {\n    let options = PingOptions::new(\"google.com\", Duration::from_secs(1), None);\n    let stream = ping(options).expect(\"Error pinging\");\n    for message in stream {\n        match message {\n            pinger::PingResult::Pong(duration, _) => {\n                println!(\"Duration: {:?}\", duration)\n            }\n            _ => {} // Handle errors, log ping timeouts, etc.\n        }\n    }\n}\n```\n\n## Adding pinger to your project.\n\n`cargo add pinger`\n"
  },
  {
    "path": "pinger/examples/simple-ping.rs",
    "content": "use pinger::{ping, PingOptions};\n\nconst LIMIT: usize = 3;\n\npub fn main() {\n    let target = \"tomforb.es\".to_string();\n    let interval = std::time::Duration::from_millis(500);\n    let options = PingOptions::new(target, interval, None);\n    let stream = ping(options).expect(\"Error pinging\");\n    for message in stream.into_iter().take(LIMIT) {\n        match message {\n            pinger::PingResult::Pong(duration, line) => {\n                println!(\"Duration: {:?}\\t\\t(raw: {:?})\", duration, line)\n            }\n            pinger::PingResult::Timeout(line) => println!(\"Timeout! (raw: {line:?})\"),\n            pinger::PingResult::Unknown(line) => println!(\"Unknown line: {:?}\", line),\n            pinger::PingResult::PingExited(code, stderr) => {\n                panic!(\"Ping exited! Code: {:?}. Stderr: {:?}\", code, stderr)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "pinger/src/bsd.rs",
    "content": "use crate::{extract_regex, PingCreationError, PingOptions, PingResult, Pinger};\nuse lazy_regex::*;\n\npub static RE: Lazy<Regex> = lazy_regex!(r\"time=(?:(?P<ms>[0-9]+).(?P<ns>[0-9]+)\\s+ms)\");\n\npub struct BSDPinger {\n    options: PingOptions,\n}\n\npub(crate) fn parse_bsd(line: String) -> Option<PingResult> {\n    if line.starts_with(\"PING \") {\n        return None;\n    }\n    if line.starts_with(\"Request timeout\") {\n        return Some(PingResult::Timeout(line));\n    }\n    extract_regex(&RE, line)\n}\n\nimpl Pinger for BSDPinger {\n    fn from_options(options: PingOptions) -> Result<Self, PingCreationError>\n    where\n        Self: Sized,\n    {\n        Ok(Self { options })\n    }\n\n    fn parse_fn(&self) -> fn(String) -> Option<PingResult> {\n        parse_bsd\n    }\n\n    fn ping_args(&self) -> (&str, Vec<String>) {\n        let mut args = vec![format!(\n            \"-i{:.1}\",\n            self.options.interval.as_millis() as f32 / 1_000_f32\n        )];\n        if let Some(interface) = &self.options.interface {\n            args.push(\"-I\".into());\n            args.push(interface.clone());\n        }\n        if let Some(raw_args) = &self.options.raw_arguments {\n            args.extend(raw_args.iter().cloned());\n        }\n        args.push(self.options.target.to_string());\n        (\"ping\", args)\n    }\n}\n"
  },
  {
    "path": "pinger/src/fake.rs",
    "content": "use crate::{PingCreationError, PingOptions, PingResult, Pinger};\nuse rand::prelude::*;\nuse rand::rng;\nuse std::sync::mpsc;\nuse std::sync::mpsc::Receiver;\nuse std::thread;\nuse std::time::Duration;\n\npub struct FakePinger {\n    options: PingOptions,\n}\n\nimpl Pinger for FakePinger {\n    fn from_options(options: PingOptions) -> Result<Self, PingCreationError>\n    where\n        Self: Sized,\n    {\n        Ok(Self { options })\n    }\n\n    fn parse_fn(&self) -> fn(String) -> Option<PingResult> {\n        unimplemented!(\"parse for FakeParser not implemented\")\n    }\n\n    fn ping_args(&self) -> (&str, Vec<String>) {\n        unimplemented!(\"ping_args not implemented for FakePinger\")\n    }\n\n    fn start(&self) -> Result<Receiver<PingResult>, PingCreationError> {\n        let (tx, rx) = mpsc::channel();\n        let sleep_time = self.options.interval;\n\n        thread::spawn(move || {\n            let mut random = rng();\n            loop {\n                let fake_seconds = random.random_range(50..150);\n                let ping_result = PingResult::Pong(\n                    Duration::from_millis(fake_seconds),\n                    format!(\"Fake ping line: {fake_seconds} ms\"),\n                );\n                if tx.send(ping_result).is_err() {\n                    break;\n                }\n\n                std::thread::sleep(sleep_time);\n            }\n        });\n\n        Ok(rx)\n    }\n}\n"
  },
  {
    "path": "pinger/src/lib.rs",
    "content": "/// Pinger\n/// This crate exposes a simple function to ping remote hosts across different operating systems.\n/// Example:\n/// ```no_run\n/// use std::time::Duration;\n/// use pinger::{ping, PingResult, PingOptions};\n/// let options = PingOptions::new(\"tomforb.es\".to_string(), Duration::from_secs(1), None);\n/// let stream = ping(options).expect(\"Error pinging\");\n/// for message in stream {\n///     match message {\n///         PingResult::Pong(duration, line) => println!(\"{:?} (line: {})\", duration, line),\n///         PingResult::Timeout(_) => println!(\"Timeout!\"),\n///         PingResult::Unknown(line) => println!(\"Unknown line: {}\", line),\n///         PingResult::PingExited(_code, _stderr) => {}\n///     }\n/// }\n/// ```\nuse lazy_regex::Regex;\nuse std::ffi::OsStr;\nuse std::fmt::{Debug, Formatter};\nuse std::io::{BufRead, BufReader};\nuse std::process::{Child, Command, ExitStatus, Stdio};\nuse std::sync::{mpsc, Arc};\nuse std::time::Duration;\nuse std::{fmt, io, thread};\nuse target::Target;\nuse thiserror::Error;\n\n#[cfg(unix)]\npub mod linux;\n#[cfg(unix)]\npub mod macos;\n#[cfg(windows)]\npub mod windows;\n\n#[cfg(unix)]\nmod bsd;\n#[cfg(feature = \"fake-ping\")]\nmod fake;\nmod target;\n#[cfg(test)]\nmod test;\n\n#[derive(Debug, Clone)]\npub struct PingOptions {\n    pub target: Target,\n    pub interval: Duration,\n    pub interface: Option<String>,\n    pub raw_arguments: Option<Vec<String>>,\n}\n\nimpl PingOptions {\n    pub fn with_raw_arguments(mut self, raw_arguments: Vec<impl ToString>) -> Self {\n        self.raw_arguments = Some(\n            raw_arguments\n                .into_iter()\n                .map(|item| item.to_string())\n                .collect(),\n        );\n        self\n    }\n}\n\nimpl PingOptions {\n    pub fn from_target(target: Target, interval: Duration, interface: Option<String>) -> Self {\n        Self {\n            target,\n            interval,\n            interface,\n            raw_arguments: None,\n        }\n    }\n    pub fn new(target: impl ToString, interval: Duration, interface: Option<String>) -> Self {\n        Self::from_target(Target::new_any(target), interval, interface)\n    }\n\n    pub fn new_ipv4(target: impl ToString, interval: Duration, interface: Option<String>) -> Self {\n        Self::from_target(Target::new_ipv4(target), interval, interface)\n    }\n\n    pub fn new_ipv6(target: impl ToString, interval: Duration, interface: Option<String>) -> Self {\n        Self::from_target(Target::new_ipv6(target), interval, interface)\n    }\n}\n\npub fn run_ping(\n    cmd: impl AsRef<OsStr> + Debug,\n    args: Vec<impl AsRef<OsStr> + Debug>,\n) -> Result<Child, PingCreationError> {\n    Ok(Command::new(cmd.as_ref())\n        .args(&args)\n        .stdout(Stdio::piped())\n        .stderr(Stdio::piped())\n        // Required to ensure that the output is formatted in the way we expect, not\n        // using locale specific delimiters.\n        .env(\"LANG\", \"C\")\n        .env(\"LC_ALL\", \"C\")\n        .spawn()?)\n}\n\npub(crate) fn extract_regex(regex: &Regex, line: String) -> Option<PingResult> {\n    let cap = regex.captures(&line)?;\n    let ms = cap\n        .name(\"ms\")\n        .expect(\"No capture group named 'ms'\")\n        .as_str()\n        .parse::<u64>()\n        .ok()?;\n    let ns = match cap.name(\"ns\") {\n        None => 0,\n        Some(cap) => {\n            let matched_str = cap.as_str();\n            let number_of_digits = matched_str.len() as u32;\n            let fractional_ms = matched_str.parse::<u64>().ok()?;\n            fractional_ms * (10u64.pow(6 - number_of_digits))\n        }\n    };\n    let duration = Duration::from_millis(ms) + Duration::from_nanos(ns);\n    Some(PingResult::Pong(duration, line))\n}\n\npub trait Pinger: Send + Sync {\n    fn from_options(options: PingOptions) -> std::result::Result<Self, PingCreationError>\n    where\n        Self: Sized;\n\n    fn parse_fn(&self) -> fn(String) -> Option<PingResult>;\n\n    fn ping_args(&self) -> (&str, Vec<String>);\n\n    fn start(&self) -> Result<mpsc::Receiver<PingResult>, PingCreationError> {\n        let (tx, rx) = mpsc::channel();\n        let (cmd, args) = self.ping_args();\n\n        let mut child = run_ping(cmd, args)?;\n        let stdout = child.stdout.take().expect(\"child did not have a stdout\");\n\n        let parse_fn = self.parse_fn();\n\n        thread::spawn(move || {\n            let reader = BufReader::new(stdout).lines();\n            for line in reader {\n                match line {\n                    Ok(msg) => {\n                        if let Some(result) = parse_fn(msg) {\n                            if tx.send(result).is_err() {\n                                break;\n                            }\n                        }\n                    }\n                    Err(_) => break,\n                }\n            }\n            let result = child.wait_with_output().expect(\"Child wasn't started?\");\n            let decoded_stderr = String::from_utf8(result.stderr).expect(\"Error decoding stderr\");\n            let _ = tx.send(PingResult::PingExited(result.status, decoded_stderr));\n        });\n\n        Ok(rx)\n    }\n}\n\n#[derive(Debug)]\npub enum PingResult {\n    Pong(Duration, String),\n    Timeout(String),\n    Unknown(String),\n    PingExited(ExitStatus, String),\n}\n\nimpl fmt::Display for PingResult {\n    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {\n        match &self {\n            PingResult::Pong(duration, _) => write!(f, \"{duration:?}\"),\n            PingResult::Timeout(_) => write!(f, \"Timeout\"),\n            PingResult::Unknown(_) => write!(f, \"Unknown\"),\n            PingResult::PingExited(status, stderr) => write!(f, \"Exited({status}, {stderr})\"),\n        }\n    }\n}\n\n#[derive(Error, Debug)]\npub enum PingCreationError {\n    #[error(\"Could not detect ping. Stderr: {stderr:?}\\nStdout: {stdout:?}\")]\n    UnknownPing {\n        stderr: Vec<String>,\n        stdout: Vec<String>,\n    },\n    #[error(\"Error spawning ping: {0}\")]\n    SpawnError(#[from] io::Error),\n\n    #[error(\"Installed ping is not supported: {alternative}\")]\n    NotSupported { alternative: String },\n\n    #[error(\"Invalid or unresolvable hostname {0}\")]\n    HostnameError(String),\n}\n\npub fn get_pinger(options: PingOptions) -> std::result::Result<Arc<dyn Pinger>, PingCreationError> {\n    #[cfg(feature = \"fake-ping\")]\n    if std::env::var(\"PINGER_FAKE_PING\")\n        .map(|e| e == \"1\")\n        .unwrap_or_default()\n    {\n        return Ok(Arc::new(fake::FakePinger::from_options(options)?));\n    }\n\n    #[cfg(windows)]\n    {\n        return Ok(Arc::new(windows::WindowsPinger::from_options(options)?));\n    }\n    #[cfg(unix)]\n    {\n        if cfg!(target_os = \"freebsd\")\n            || cfg!(target_os = \"dragonfly\")\n            || cfg!(target_os = \"openbsd\")\n            || cfg!(target_os = \"netbsd\")\n        {\n            Ok(Arc::new(bsd::BSDPinger::from_options(options)?))\n        } else if cfg!(target_os = \"macos\") {\n            Ok(Arc::new(macos::MacOSPinger::from_options(options)?))\n        } else {\n            Ok(Arc::new(linux::LinuxPinger::from_options(options)?))\n        }\n    }\n}\n\n/// Start pinging a an address. The address can be either a hostname or an IP address.\npub fn ping(\n    options: PingOptions,\n) -> std::result::Result<mpsc::Receiver<PingResult>, PingCreationError> {\n    let pinger = get_pinger(options)?;\n    pinger.start()\n}\n"
  },
  {
    "path": "pinger/src/linux.rs",
    "content": "use crate::{extract_regex, run_ping, PingCreationError, PingOptions, PingResult, Pinger};\nuse lazy_regex::*;\n\npub static UBUNTU_RE: Lazy<Regex> = lazy_regex!(r\"(?i-u)time=(?P<ms>\\d+)(?:\\.(?P<ns>\\d+))? *ms\");\n\n#[derive(Debug)]\npub enum LinuxPinger {\n    // Alpine\n    BusyBox(PingOptions),\n    // Debian, Ubuntu, etc\n    IPTools(PingOptions),\n}\n\nimpl LinuxPinger {\n    pub fn detect_platform_ping(options: PingOptions) -> Result<Self, PingCreationError> {\n        let child = run_ping(\"ping\", vec![\"-V\".to_string()])?;\n        let output = child.wait_with_output()?;\n        let stdout = String::from_utf8(output.stdout).expect(\"Error decoding ping stdout\");\n        let stderr = String::from_utf8(output.stderr).expect(\"Error decoding ping stderr\");\n\n        if stderr.contains(\"BusyBox\") {\n            Ok(LinuxPinger::BusyBox(options))\n        } else if stdout.contains(\"iputils\") {\n            Ok(LinuxPinger::IPTools(options))\n        } else if stdout.contains(\"inetutils\") {\n            Err(PingCreationError::NotSupported {\n                alternative: \"Please use iputils ping, not inetutils.\".to_string(),\n            })\n        } else {\n            let first_two_lines_stderr: Vec<String> =\n                stderr.lines().take(2).map(str::to_string).collect();\n            let first_two_lines_stout: Vec<String> =\n                stdout.lines().take(2).map(str::to_string).collect();\n            Err(PingCreationError::UnknownPing {\n                stdout: first_two_lines_stout,\n                stderr: first_two_lines_stderr,\n            })\n        }\n    }\n}\n\nimpl Pinger for LinuxPinger {\n    fn from_options(options: PingOptions) -> Result<Self, PingCreationError>\n    where\n        Self: Sized,\n    {\n        Self::detect_platform_ping(options)\n    }\n\n    fn parse_fn(&self) -> fn(String) -> Option<PingResult> {\n        |line| {\n            #[cfg(test)]\n            eprintln!(\"Got line {line}\");\n            if line.starts_with(\"64 bytes from\") {\n                return extract_regex(&UBUNTU_RE, line);\n            } else if line.starts_with(\"no answer yet\") {\n                return Some(PingResult::Timeout(line));\n            }\n            None\n        }\n    }\n\n    fn ping_args(&self) -> (&str, Vec<String>) {\n        match self {\n            // Alpine doesn't support timeout notifications, so we don't add the -O flag here.\n            LinuxPinger::BusyBox(options) => {\n                let cmd = if options.target.is_ipv6() {\n                    \"ping6\"\n                } else {\n                    \"ping\"\n                };\n\n                let mut args = vec![\n                    options.target.to_string(),\n                    format!(\"-i{:.1}\", options.interval.as_millis() as f32 / 1_000_f32),\n                ];\n\n                if let Some(raw_args) = &options.raw_arguments {\n                    args.extend(raw_args.iter().cloned());\n                }\n\n                (cmd, args)\n            }\n            LinuxPinger::IPTools(options) => {\n                let cmd = if options.target.is_ipv6() {\n                    \"ping6\"\n                } else {\n                    \"ping\"\n                };\n\n                // The -O flag ensures we \"no answer yet\" messages from ping\n                // See https://superuser.com/questions/270083/linux-ping-show-time-out\n                let mut args = vec![\n                    \"-O\".to_string(),\n                    format!(\"-i{:.1}\", options.interval.as_millis() as f32 / 1_000_f32),\n                ];\n                if let Some(interface) = &options.interface {\n                    args.push(\"-I\".into());\n                    args.push(interface.clone());\n                }\n                if let Some(raw_args) = &options.raw_arguments {\n                    args.extend(raw_args.iter().cloned());\n                }\n\n                args.push(options.target.to_string());\n                (cmd, args)\n            }\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    #[test]\n    #[cfg(target_os = \"linux\")]\n    fn test_linux_detection() {\n        use super::*;\n        use os_info::Type;\n        use std::time::Duration;\n\n        let platform = LinuxPinger::detect_platform_ping(PingOptions::new(\n            \"foo.com\".to_string(),\n            Duration::from_secs(1),\n            None,\n        ))\n        .unwrap();\n        match os_info::get().os_type() {\n            Type::Alpine => {\n                assert!(matches!(platform, LinuxPinger::BusyBox(_)))\n            }\n            Type::Ubuntu => {\n                assert!(matches!(platform, LinuxPinger::IPTools(_)))\n            }\n            _ => {}\n        }\n    }\n}\n"
  },
  {
    "path": "pinger/src/macos.rs",
    "content": "use crate::bsd::parse_bsd;\nuse crate::{PingCreationError, PingOptions, PingResult, Pinger};\nuse lazy_regex::*;\n\npub static RE: Lazy<Regex> = lazy_regex!(r\"time=(?:(?P<ms>[0-9]+).(?P<ns>[0-9]+)\\s+ms)\");\n\npub struct MacOSPinger {\n    options: PingOptions,\n}\n\nimpl Pinger for MacOSPinger {\n    fn from_options(options: PingOptions) -> Result<Self, PingCreationError>\n    where\n        Self: Sized,\n    {\n        Ok(Self { options })\n    }\n\n    fn parse_fn(&self) -> fn(String) -> Option<PingResult> {\n        parse_bsd\n    }\n\n    fn ping_args(&self) -> (&str, Vec<String>) {\n        let cmd = if self.options.target.is_ipv6() {\n            \"ping6\"\n        } else {\n            \"ping\"\n        };\n        let mut args = vec![\n            format!(\n                \"-i{:.1}\",\n                self.options.interval.as_millis() as f32 / 1_000_f32\n            ),\n            self.options.target.to_string(),\n        ];\n        if let Some(interface) = &self.options.interface {\n            args.push(\"-b\".into());\n            args.push(interface.clone());\n        }\n\n        if let Some(raw_args) = &self.options.raw_arguments {\n            args.extend(raw_args.iter().cloned());\n        }\n\n        (cmd, args)\n    }\n}\n"
  },
  {
    "path": "pinger/src/target.rs",
    "content": "use std::fmt;\nuse std::fmt::{Display, Formatter};\nuse std::net::{IpAddr, Ipv4Addr, Ipv6Addr};\n\n#[derive(Debug, Copy, Clone, Eq, PartialEq)]\npub enum IPVersion {\n    V4,\n    V6,\n    Any,\n}\n\n#[derive(Debug, Clone)]\npub enum Target {\n    IP(IpAddr),\n    Hostname { domain: String, version: IPVersion },\n}\n\nimpl Target {\n    pub fn is_ipv6(&self) -> bool {\n        match self {\n            Target::IP(ip) => ip.is_ipv6(),\n            Target::Hostname { version, .. } => *version == IPVersion::V6,\n        }\n    }\n\n    pub fn new_any(value: impl ToString) -> Self {\n        let value = value.to_string();\n        if let Ok(ip) = value.parse::<IpAddr>() {\n            return Self::IP(ip);\n        }\n        Self::Hostname {\n            domain: value,\n            version: IPVersion::Any,\n        }\n    }\n\n    pub fn new_ipv4(value: impl ToString) -> Self {\n        let value = value.to_string();\n        if let Ok(ip) = value.parse::<Ipv4Addr>() {\n            return Self::IP(IpAddr::V4(ip));\n        }\n        Self::Hostname {\n            domain: value.to_string(),\n            version: IPVersion::V4,\n        }\n    }\n\n    pub fn new_ipv6(value: impl ToString) -> Self {\n        let value = value.to_string();\n        if let Ok(ip) = value.parse::<Ipv6Addr>() {\n            return Self::IP(IpAddr::V6(ip));\n        }\n        Self::Hostname {\n            domain: value.to_string(),\n            version: IPVersion::V6,\n        }\n    }\n}\n\nimpl Display for Target {\n    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {\n        match self {\n            Target::IP(v) => Display::fmt(&v, f),\n            Target::Hostname { domain, .. } => Display::fmt(&domain, f),\n        }\n    }\n}\n"
  },
  {
    "path": "pinger/src/test.rs",
    "content": "#[cfg(test)]\nmod tests {\n    #[cfg(unix)]\n    use crate::bsd::BSDPinger;\n    #[cfg(unix)]\n    use crate::linux::LinuxPinger;\n    #[cfg(unix)]\n    use crate::macos::MacOSPinger;\n    #[cfg(windows)]\n    use crate::windows::WindowsPinger;\n    use crate::{PingOptions, PingResult, Pinger};\n    use anyhow::bail;\n    use ntest::timeout;\n    use std::time::Duration;\n\n    const IS_GHA: bool = option_env!(\"GITHUB_ACTIONS\").is_some();\n\n    #[test]\n    #[timeout(20_000)]\n    fn test_integration_any() {\n        run_integration_test(PingOptions::new(\n            \"tomforb.es\",\n            Duration::from_millis(500),\n            None,\n        ))\n        .unwrap();\n    }\n    #[test]\n    #[timeout(20_000)]\n    fn test_integration_ipv4() {\n        run_integration_test(PingOptions::new_ipv4(\n            \"tomforb.es\",\n            Duration::from_millis(500),\n            None,\n        ))\n        .unwrap();\n    }\n    #[test]\n    #[timeout(20_000)]\n    fn test_integration_ip6() {\n        let res = run_integration_test(PingOptions::new_ipv6(\n            \"tomforb.es\",\n            Duration::from_millis(500),\n            None,\n        ));\n        // ipv6 tests are allowed to fail on Gitlab CI, as it doesn't support ipv6, apparently.\n        if !IS_GHA {\n            res.unwrap();\n        }\n    }\n\n    fn run_integration_test(options: PingOptions) -> anyhow::Result<()> {\n        let stream = crate::ping(options.clone())?;\n\n        let mut success = 0;\n        let mut errors = 0;\n\n        for message in stream.into_iter().take(3) {\n            match message {\n                PingResult::Pong(_, m) | PingResult::Timeout(m) => {\n                    eprintln!(\"Message: {}\", m);\n                    success += 1;\n                }\n                PingResult::Unknown(line) => {\n                    eprintln!(\"Unknown line: {}\", line);\n                    errors += 1;\n                }\n                PingResult::PingExited(code, stderr) => {\n                    bail!(\"Ping exited with code: {}, stderr: {}\", code, stderr);\n                }\n            }\n        }\n        assert_eq!(success, 3, \"Success != 3 with opts {options:?}\");\n        assert_eq!(errors, 0, \"Errors != 0 with opts {options:?}\");\n        Ok(())\n    }\n\n    fn opts() -> PingOptions {\n        PingOptions::new(\"foo\".to_string(), Duration::from_secs(1), None)\n    }\n\n    fn test_parser<T: Pinger>(contents: &str) {\n        let pinger = T::from_options(opts()).unwrap();\n        run_parser_test(contents, &pinger);\n    }\n\n    fn run_parser_test(contents: &str, pinger: &impl Pinger) {\n        let parser = pinger.parse_fn();\n        let test_file: Vec<&str> = contents.split(\"-----\").collect();\n        let input = test_file[0].trim().split('\\n');\n        let expected: Vec<&str> = test_file[1].trim().split('\\n').collect();\n        let parsed: Vec<Option<PingResult>> = input.map(|l| parser(l.to_string())).collect();\n\n        assert_eq!(\n            parsed.len(),\n            expected.len(),\n            \"Parsed: {:?}, Expected: {:?}\",\n            &parsed,\n            &expected\n        );\n\n        for (idx, (output, expected)) in parsed.into_iter().zip(expected).enumerate() {\n            if let Some(value) = output {\n                assert_eq!(\n                    format!(\"{value}\").trim(),\n                    expected.trim(),\n                    \"Failed at idx {idx}\"\n                )\n            } else {\n                assert_eq!(\"None\", expected.trim(), \"Failed at idx {idx}\")\n            }\n        }\n    }\n\n    #[cfg(unix)]\n    #[test]\n    fn macos() {\n        test_parser::<MacOSPinger>(include_str!(\"tests/macos.txt\"));\n    }\n\n    #[cfg(unix)]\n    #[test]\n    fn freebsd() {\n        test_parser::<BSDPinger>(include_str!(\"tests/bsd.txt\"));\n    }\n\n    #[cfg(unix)]\n    #[test]\n    fn dragonfly() {\n        test_parser::<BSDPinger>(include_str!(\"tests/bsd.txt\"));\n    }\n\n    #[cfg(unix)]\n    #[test]\n    fn openbsd() {\n        test_parser::<BSDPinger>(include_str!(\"tests/bsd.txt\"));\n    }\n\n    #[cfg(unix)]\n    #[test]\n    fn netbsd() {\n        test_parser::<BSDPinger>(include_str!(\"tests/bsd.txt\"));\n    }\n\n    #[cfg(unix)]\n    #[test]\n    fn ubuntu() {\n        run_parser_test(\n            include_str!(\"tests/ubuntu.txt\"),\n            &LinuxPinger::IPTools(opts()),\n        );\n    }\n\n    #[cfg(unix)]\n    #[test]\n    fn debian() {\n        run_parser_test(\n            include_str!(\"tests/debian.txt\"),\n            &LinuxPinger::IPTools(opts()),\n        );\n    }\n\n    #[cfg(windows)]\n    #[test]\n    fn windows() {\n        test_parser::<WindowsPinger>(include_str!(\"tests/windows.txt\"));\n    }\n\n    #[cfg(unix)]\n    #[test]\n    fn android() {\n        run_parser_test(\n            include_str!(\"tests/android.txt\"),\n            &LinuxPinger::BusyBox(opts()),\n        );\n    }\n\n    #[cfg(unix)]\n    #[test]\n    fn alpine() {\n        run_parser_test(\n            include_str!(\"tests/alpine.txt\"),\n            &LinuxPinger::BusyBox(opts()),\n        );\n    }\n}\n"
  },
  {
    "path": "pinger/src/tests/alpine.txt",
    "content": "PING google.com (142.250.178.14): 56 data bytes\n64 bytes from 142.250.178.14: seq=0 ttl=37 time=19.236 ms\n64 bytes from 142.250.178.14: seq=1 ttl=37 time=19.319 ms\n64 bytes from 142.250.178.14: seq=2 ttl=37 time=17.944 ms\nping: sendto: Network unreachable\n-----\n\nNone\n19.236ms\n19.319ms\n17.944ms\nNone\n"
  },
  {
    "path": "pinger/src/tests/android.txt",
    "content": "PING google.com (172.217.173.46) 56(84) bytes of data.\n64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=1 ttl=110 time=106 ms\n64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=2 ttl=110 time=142 ms\n64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=3 ttl=110 time=244 ms\n64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=4 ttl=110 time=120 ms\n64 bytes from bog02s12-in-f14.1e100.net (172.217.173.46): icmp_seq=5 ttl=110 time=122 ms\n64 bytes from 172.217.173.46: icmp_seq=6 ttl=110 time=246 ms\n\n--- google.com ping statistics ---\n6 packets transmitted, 6 received, 0% packet loss, time 5018ms\nrtt min/avg/max/mdev = 106.252/163.821/246.851/58.823 ms\n\n-----\n\nNone\n106ms\n142ms\n244ms\n120ms\n122ms\n246ms\nNone\nNone\nNone\nNone\n"
  },
  {
    "path": "pinger/src/tests/bsd.txt",
    "content": "PING google.com (216.58.198.174): 56 data bytes\n64 bytes from 96.47.72.84: icmp_seq=0 ttl=50 time=111.525 ms\nping: sendto: Host is down\n64 bytes from 96.47.72.84: icmp_seq=1 ttl=50 time=110.395 ms\nping: sendto: No route to host\n\n-----\n\nNone\n111.525ms\nNone\n110.395ms\nNone\n"
  },
  {
    "path": "pinger/src/tests/debian.txt",
    "content": "PING google.com (216.58.209.78): 56 data bytes\n64 bytes from 216.58.209.78: icmp_seq=0 ttl=37 time=21.308 ms\n64 bytes from 216.58.209.78: icmp_seq=1 ttl=37 time=15.769 ms\n^C--- google.com ping statistics ---\n8 packets transmitted, 8 packets received, 0% packet loss\nround-trip min/avg/max/stddev = 15.282/20.347/41.775/8.344 ms\n\n-----\n\nNone\n21.308ms\n15.769ms\nNone\nNone\nNone\n"
  },
  {
    "path": "pinger/src/tests/macos.txt",
    "content": "PING google.com (216.58.209.78): 56 data bytes\n64 bytes from 216.58.209.78: icmp_seq=0 ttl=119 time=14.621 ms\n64 bytes from 216.58.209.78: icmp_seq=1 ttl=119 time=33.898 ms\n64 bytes from 216.58.209.78: icmp_seq=2 ttl=119 time=17.305 ms\n64 bytes from 216.58.209.78: icmp_seq=3 ttl=119 time=24.235 ms\n64 bytes from 216.58.209.78: icmp_seq=4 ttl=119 time=15.242 ms\n64 bytes from 216.58.209.78: icmp_seq=5 ttl=119 time=16.639 ms\nRequest timeout for icmp_seq 19\nRequest timeout for icmp_seq 20\nRequest timeout for icmp_seq 21\n64 bytes from 216.58.209.78: icmp_seq=30 ttl=119 time=16.943 ms\n\n-----\n\nNone\n14.621ms\n33.898ms\n17.305ms\n24.235ms\n15.242ms\n16.639ms\nTimeout\nTimeout\nTimeout\n16.943ms\n"
  },
  {
    "path": "pinger/src/tests/ubuntu.txt",
    "content": "PING google.com (216.58.209.78) 56(84) bytes of data.\n64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=1 ttl=37 time=25.1 ms\n64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=2 ttl=37 time=19.4 ms\n64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=3 ttl=37 time=14.9 ms\n64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=4 ttl=37 time=22.8 ms\n64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=5 ttl=37 time=13.9 ms\n64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=6 ttl=37 time=77.6 ms\n64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=7 ttl=37 time=158 ms\nno answer yet for icmp_seq=8\nno answer yet for icmp_seq=9\n64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=18 ttl=37 time=357 ms\n64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=19 ttl=37 time=85.2 ms\n64 bytes from mad07s22-in-f14.1e100.net (216.58.209.78): icmp_seq=20 ttl=37 time=17.8 ms\n\n-----\n\nNone\n25.1ms\n19.4ms\n14.9ms\n22.8ms\n13.9ms\n77.6ms\n158ms\nTimeout\nTimeout\n357ms\n85.2ms\n17.8ms\n"
  },
  {
    "path": "pinger/src/tests/windows.txt",
    "content": "pinging example.microsoft.com [192.168.239.132] with 32 bytes of data:\nReply from 192.168.239.132: bytes=32 time=101ms TTL=124\nReply from 192.168.239.132: bytes=32 time=100ms TTL=124\nReply from 192.168.239.132: bytes=32 time=120ms TTL=124\nReply from 192.168.239.132: bytes=32 time=120ms TTL=124\nRequest timed out.\nRequest timed out.\nReply from 192.168.239.132: bytes=32 time=120ms TTL=124\n\n-----\n\nNone\n101ms\n100ms\n120ms\n120ms\nTimeout\nTimeout\n120ms\n"
  },
  {
    "path": "pinger/src/windows.rs",
    "content": "use crate::target::{IPVersion, Target};\nuse crate::PingCreationError;\nuse crate::{extract_regex, PingOptions, PingResult, Pinger};\nuse lazy_regex::*;\nuse std::net::{IpAddr, ToSocketAddrs};\nuse std::sync::mpsc;\nuse std::thread;\nuse std::time::Duration;\nuse winping::{Buffer, Pinger as WinPinger};\n\npub static RE: Lazy<Regex> = lazy_regex!(r\"(?ix-u)time=(?P<ms>\\d+)(?:\\.(?P<ns>\\d+))?\");\n\npub struct WindowsPinger {\n    options: PingOptions,\n}\n\nimpl Pinger for WindowsPinger {\n    fn from_options(options: PingOptions) -> Result<Self, PingCreationError> {\n        Ok(Self { options })\n    }\n\n    fn parse_fn(&self) -> fn(String) -> Option<PingResult> {\n        |line| {\n            if line.contains(\"timed out\") || line.contains(\"failure\") {\n                return Some(PingResult::Timeout(line));\n            }\n            extract_regex(&RE, line)\n        }\n    }\n\n    fn ping_args(&self) -> (&str, Vec<String>) {\n        unimplemented!(\"ping_args for WindowsPinger is not implemented\")\n    }\n\n    fn start(&self) -> Result<mpsc::Receiver<PingResult>, PingCreationError> {\n        let interval = self.options.interval;\n        let parsed_ip = match &self.options.target {\n            Target::IP(ip) => ip.clone(),\n            Target::Hostname { domain, version } => {\n                let ips = (domain.as_str(), 0).to_socket_addrs()?;\n                let selected_ips: Vec<_> = if *version == IPVersion::Any {\n                    ips.collect()\n                } else {\n                    ips.into_iter()\n                        .filter(|addr| {\n                            if *version == IPVersion::V6 {\n                                matches!(addr.ip(), IpAddr::V6(_))\n                            } else {\n                                matches!(addr.ip(), IpAddr::V4(_))\n                            }\n                        })\n                        .collect()\n                };\n                if selected_ips.is_empty() {\n                    return Err(PingCreationError::HostnameError(domain.clone()).into());\n                }\n                selected_ips[0].ip()\n            }\n        };\n\n        let (tx, rx) = mpsc::channel();\n\n        thread::spawn(move || {\n            let pinger = WinPinger::new().expect(\"Failed to create a WinPinger instance\");\n            let mut buffer = Buffer::new();\n            loop {\n                match pinger.send(parsed_ip.clone(), &mut buffer) {\n                    Ok(rtt) => {\n                        if tx\n                            .send(PingResult::Pong(\n                                Duration::from_millis(rtt as u64),\n                                \"\".to_string(),\n                            ))\n                            .is_err()\n                        {\n                            break;\n                        }\n                    }\n                    Err(_) => {\n                        // Fuck it. All errors are timeouts. Why not.\n                        if tx.send(PingResult::Timeout(\"\".to_string())).is_err() {\n                            break;\n                        }\n                    }\n                }\n                thread::sleep(interval);\n            }\n        });\n\n        Ok(rx)\n    }\n}\n"
  },
  {
    "path": "readme.md",
    "content": "# gping 🚀\n\n[![Crates.io](https://img.shields.io/crates/v/gping.svg)](https://crates.io/crates/gping)\n[![Actions Status](https://github.com/orf/gping/workflows/CI/badge.svg)](https://github.com/orf/gping/actions)\n\nPing, but with a graph.\n\n![](./images/readme-example.gif)\n\nComes with the following super-powers:\n* Graph the ping time for multiple hosts\n* Graph the _execution time_ for commands via the `--cmd` flag\n* Custom colours\n* Windows, Mac and Linux support\n\nTable of Contents\n=================\n\n   * [Install :cd:](#install-cd)\n   * [Usage :saxophone:](#usage-saxophone)\n\n<a href=\"https://repology.org/project/gping/versions\">\n    <img src=\"https://repology.org/badge/vertical-allrepos/gping.svg\" alt=\"Packaging status\" align=\"right\">\n</a>\n\n# Install :cd:\n\n* macOS\n  * [Homebrew](https://formulae.brew.sh/formula/gping#default): `brew install gping`\n  * [MacPorts](https://ports.macports.org/port/gping/): `sudo port install gping`\n* Linux (Homebrew): `brew install gping`\n* CentOS (and other distributions with an old glibc): Download the MUSL build from the latest release\n* Windows/ARM:\n  * Scoop: `scoop install gping`\n  * Chocolatey: `choco install gping`\n  * Download the latest release from [the github releases page](https://github.com/orf/gping/releases)\n* Fedora ([COPR](https://copr.fedorainfracloud.org/coprs/atim/gping/)): `sudo dnf copr enable atim/gping -y && sudo dnf install gping`\n* Cargo (**This requires `rustc` version 1.67.0 or greater**): `cargo install gping`\n* Arch Linux: `pacman -S gping`\n* Alpine linux: `apk add gping`\n* Ubuntu >23.10/Debian >13: `apt install gping`\n* Ubuntu/Debian ([Azlux's repo](https://packages.azlux.fr/)):\n  ```bash\n  echo 'deb [signed-by=/usr/share/keyrings/azlux.gpg] https://packages.azlux.fr/debian/ bookworm main' | sudo tee /etc/apt/sources.list.d/azlux.list\n  sudo apt install gpg\n  curl -s https://azlux.fr/repo.gpg.key | gpg --dearmor | sudo tee /usr/share/keyrings/azlux.gpg > /dev/null\n  sudo apt update\n  sudo apt install gping\n  ```\n* Gentoo ([dm9pZCAq overlay](https://github.com/gentoo-mirror/dm9pZCAq)):\n  ```sh\n  sudo eselect repository enable dm9pZCAq\n  sudo emerge --sync dm9pZCAq\n  sudo emerge net-misc/gping::dm9pZCAq\n  ```\n* FreeBSD:\n  * [pkg](https://www.freshports.org/net-mgmt/gping/): `pkg install gping`\n  * [ports](https://cgit.freebsd.org/ports/tree/net-mgmt/gping) `cd /usr/ports/net-mgmt/gping; make install clean`\n* Docker:\n  ```sh\n  # Check all options\n  docker run --rm -ti --network host ghcr.io/orf/gping:gping-v1.15.1 --help\n  # Ping google.com\n  docker run --rm -ti --network host ghcr.io/orf/gping:gping-v1.15.1 google.com\n  ```\n* Flox:\n  ```sh\n  # Inside of a Flox environment\n  flox install gping\n  ```\n* [gah](https://github.com/marverix/gah):\n  ```sh\n  gah install gping\n  ```\n\n# Usage :saxophone:\n\nJust run `gping [host]`. `host` can be a command like `curl google.com` if the `--cmd` flag is used. You can also use\nshorthands like `aws:eu-west-1` or `aws:ca-central-1` to ping specific cloud regions. Only `aws` is currently supported.\n\n```bash\n$ gping --help\nPing, but with a graph.\n\nUsage: gping [OPTIONS] [HOSTS_OR_COMMANDS]...\n\nArguments:\n  [HOSTS_OR_COMMANDS]...  Hosts or IPs to ping, or commands to run if --cmd is provided. Can use cloud shorthands like aws:eu-west-1.\n\nOptions:\n      --cmd\n          Graph the execution time for a list of commands rather than pinging hosts\n  -n, --watch-interval <WATCH_INTERVAL>\n          Watch interval seconds (provide partial seconds like '0.5'). Default for ping is 0.2, default for cmd is 0.5.\n  -b, --buffer <BUFFER>\n          Determines the number of seconds to display in the graph. [default: 30]\n  -4\n          Resolve ping targets to IPv4 address\n  -6\n          Resolve ping targets to IPv6 address\n  -i, --interface <INTERFACE>\n          Interface to use when pinging\n  -s, --simple-graphics\n          Uses dot characters instead of braille\n      --vertical-margin <VERTICAL_MARGIN>\n          Vertical margin around the graph (top and bottom) [default: 1]\n      --horizontal-margin <HORIZONTAL_MARGIN>\n          Horizontal margin around the graph (left and right) [default: 0]\n  -c, --color <color>\n          Assign color to a graph entry. This option can be defined more than once as a comma separated string, and the order which the colors are provided will be matched against the hosts or commands passed to gping. Hexadecimal RGB color codes are accepted in the form of '#RRGGBB' or the following color names: 'black', 'red', 'green', 'yellow', 'blue', 'magenta','cyan', 'gray', 'dark-gray', 'light-red', 'light-green', 'light-yellow', 'light-blue', 'light-magenta', 'light-cyan', and 'white'\n  -h, --help\n          Print help information\n  -V, --version\n          Print version information\n      --clear\n          Clear the graph from the terminal after closing the program\n```\n"
  }
]