The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .github
    ├── dependabot.yaml
    ├── version_check.py
    └── workflows
    │   ├── ci.yml
    │   └── release.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── images
    ├── somo-compact-example.png
    ├── somo-example.png
    ├── somo-kill-example.png
    └── somo-logo.png
├── nix
    ├── flake.lock
    ├── flake.nix
    └── package.nix
└── src
    ├── cli.rs
    ├── connections
        ├── common.rs
        ├── linux.rs
        ├── macos.rs
        └── mod.rs
    ├── macros.rs
    ├── main.rs
    ├── schemas.rs
    ├── table.rs
    └── utils.rs


/.github/dependabot.yaml:
--------------------------------------------------------------------------------
 1 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#package-ecosystem-
 2 | 
 3 | version: 2
 4 | updates:
 5 |   - package-ecosystem: cargo
 6 |     directory: /
 7 |     schedule:
 8 |       interval: weekly
 9 |   - package-ecosystem: github-actions
10 |     directory: /
11 |     schedule:
12 |       interval: weekly
13 | 


--------------------------------------------------------------------------------
/.github/version_check.py:
--------------------------------------------------------------------------------
 1 | import sys
 2 | import os
 3 | import re
 4 | from enum import Enum
 5 | 
 6 | 
 7 | class BumpType(Enum):
 8 |     MAJOR = "major"
 9 |     MINOR = "minor"
10 |     PATCH = "patch"
11 | 
12 | 
13 | def is_valid_semver(version: str) -> bool:
14 |     semver_regex = r"^\d+\.\d+\.\d+
quot;
15 |     return bool(re.match(semver_regex, version))
16 | 
17 | 
18 | def raise_semver_exception():
19 |     raise ValueError("Invalid version - must follow SEMVER convention (major.minor.patch)!")
20 | 
21 | 
22 | def validate_semver_bump(previous_version: str, next_version: str) -> BumpType:
23 |     if not (is_valid_semver(previous_version) and is_valid_semver(next_version)):
24 |         raise_semver_exception()
25 | 
26 |     if previous_version == next_version:
27 |         raise ValueError("Version was not bumped!")
28 |     
29 |     print(f"Comparing next version {next_version} with previous version {previous_version}.")
30 |     
31 |     previous_version = list(map(int, previous_version.split(".")))
32 |     next_version = list(map(int, next_version.split(".")))
33 | 
34 |     semver_parts = [BumpType.MAJOR, BumpType.MINOR, BumpType.PATCH]
35 |     for idx in range(len(semver_parts)):
36 |         if next_version[idx] != previous_version[idx]:
37 |             correct_bump = next_version[idx] - 1 == previous_version[idx]
38 |             lower_levels_reset = all(next_version[jdx] == 0 for jdx in range(idx + 1, len(semver_parts)))
39 | 
40 |             if correct_bump and lower_levels_reset:
41 |                 print(f"{semver_parts[idx].value} version bump.")
42 |                 return semver_parts[idx]
43 |             
44 |             raise_semver_exception()
45 |         
46 | 
47 | if __name__ == "__main__":
48 |     previous_version = sys.argv[1].removeprefix("v")
49 |     next_version = sys.argv[2].removeprefix("v")
50 | 
51 |     bump_type = validate_semver_bump(previous_version, next_version)
52 |     with open(os.getenv("GITHUB_OUTPUT"), "a") as output:
53 |         output.write(f"bump_type={bump_type.value}\n")


--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
 1 | name: Continuous Integration
 2 | 
 3 | on:
 4 |   push:
 5 |     paths-ignore:
 6 |       - "**/README.md"
 7 |     branches:
 8 |       - master
 9 |   pull_request:
10 |     branches: ["**"]
11 |   workflow_dispatch:
12 |   workflow_call:
13 | 
14 | env:
15 |   CARGO_TERM_COLOR: always
16 |   RUST_BACKTRACE: 1
17 | 
18 | jobs:
19 |   build:
20 |     runs-on: ${{ matrix.os }}
21 |     strategy:
22 |       matrix:
23 |         os: [ubuntu-latest, macos-latest]
24 | 
25 |     steps:
26 |     - name: Setup | Rust
27 |       uses: dtolnay/rust-toolchain@stable
28 |       with:
29 |         components: rustfmt, clippy, rust-src
30 | 
31 |     - name: Setup | Checkout
32 |       uses: actions/checkout@v4
33 | 
34 |     - name: Build | Compile
35 |       run: cargo build
36 | 
37 | 
38 |   check-format:
39 |     runs-on: ubuntu-latest
40 |     steps:
41 |     - name: Setup | Rust
42 |       uses: dtolnay/rust-toolchain@stable
43 | 
44 |     - name: Setup | Checkout
45 |       uses: actions/checkout@v4
46 | 
47 |     - name: Check | Fmt Check
48 |       run: cargo fmt -- --check
49 | 
50 | 
51 |   check-clippy:
52 |     runs-on: ubuntu-latest
53 |     steps:
54 |     - name: Setup | Rust
55 |       uses: dtolnay/rust-toolchain@stable
56 | 
57 |     - name: Setup | Checkout
58 |       uses: actions/checkout@v4
59 | 
60 |     - name: Check | Clippy
61 |       run: cargo clippy --no-deps -- -Dwarnings
62 | 
63 | 
64 |   test:
65 |     runs-on: ${{ matrix.os }}
66 |     strategy:
67 |       matrix:
68 |         os: [ubuntu-latest, macos-latest]
69 |     steps:
70 |     - name: Setup | Rust
71 |       uses: dtolnay/rust-toolchain@stable
72 | 
73 |     - name: Setup | Checkout
74 |       uses: actions/checkout@v4
75 | 
76 |     - name: Test | Run
77 |       run: cargo test
78 | 
79 |   nix:
80 |     runs-on: ubuntu-latest
81 |     steps:
82 |       - uses: actions/checkout@v4
83 |       - uses: DeterminateSystems/nix-installer-action@90bb610b90bf290cad97484ba341453bd1cbefea # v19
84 |       - run: nix run 'path:.?dir=nix'
85 | 


--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
  1 | name: Release
  2 | 
  3 | on:
  4 |   workflow_dispatch:
  5 |     inputs:
  6 |       release_type:
  7 |         description: "Type of release"
  8 |         required: true
  9 |         type: choice
 10 |         options:
 11 |           - patch
 12 |           - minor
 13 |           - major
 14 | 
 15 | jobs:
 16 |   ci:
 17 |     uses: ./.github/workflows/ci.yml
 18 | 
 19 |   version-check:
 20 |     needs: ci
 21 |     runs-on: ubuntu-latest
 22 | 
 23 |     outputs:
 24 |       bump_type: ${{ steps.check_version.outputs.bump_type }}
 25 | 
 26 |     steps:
 27 |       - name: Checkout repository
 28 |         uses: actions/checkout@v4
 29 |         with:
 30 |           fetch-depth: 0
 31 | 
 32 |       - name: Get new version
 33 |         id: new_version
 34 |         run: echo ::set-output name=version::$(grep -Po '^version = \"\K[^\"]+' Cargo.toml)
 35 | 
 36 |       - name: Get previous version
 37 |         id: previous_version
 38 |         run: echo ::set-output name=version::$(git describe --tags --abbrev=0 || echo '0.0.0')
 39 | 
 40 |       - name: Check version
 41 |         id: check_version
 42 |         run: |
 43 |           python .github/version_check.py ${{ steps.previous_version.outputs.version }} ${{ steps.new_version.outputs.version }}
 44 |           echo "${{ github.event_name }} ${{ github.ref }}"
 45 | 
 46 |       - name: Validate selected release type against version bump
 47 |         run: |
 48 |           echo "Expected release type: ${{ github.event.inputs.release_type }}"
 49 |           echo "Actual release type: ${{ steps.check_version.outputs.bump_type }}"
 50 |           if [ "${{ github.event.inputs.release_type }}" != "${{ steps.check_version.outputs.bump_type }}" ]; then
 51 |             echo "Mismatch between selected release type and actual version bump. Aborting release."
 52 |             exit 1
 53 |           fi
 54 |     
 55 |   build-and-release:
 56 |     needs: version-check
 57 | 
 58 |     runs-on: ubuntu-latest
 59 |     steps:
 60 |     - name: Checkout code
 61 |       uses: actions/checkout@v4
 62 | 
 63 |     - name: Extract version from Cargo.toml
 64 |       id: version
 65 |       run: echo "version=$(grep -Po '^version = \"\K[^\"]+' Cargo.toml)" >> "$GITHUB_OUTPUT"
 66 | 
 67 |     - name: Extract changelog entry
 68 |       id: changelog
 69 |       run: |
 70 |         version=${{ steps.version.outputs.version }}
 71 |         content=$(awk -v ver="$version" '
 72 |           $0 ~ "^## \\["ver"\\]" { found = 1; print; next }
 73 |           found && /^---$/ { exit }
 74 |           found { print }
 75 |         ' CHANGELOG.md)
 76 | 
 77 |         echo "changelog<<EOF" >> $GITHUB_OUTPUT
 78 |         echo "$content" >> $GITHUB_OUTPUT
 79 |         echo "EOF" >> $GITHUB_OUTPUT
 80 | 
 81 |     - name: Build and package
 82 |       run: |
 83 |         cargo build --release
 84 |         cargo install cargo-deb
 85 |         cargo deb --output target/debian/somo-${{ steps.version.outputs.version }}.deb
 86 | 
 87 |     - name: Upload Artifact
 88 |       uses: actions/upload-artifact@v4
 89 |       with:
 90 |         name: somo-${{ steps.version.outputs.version }}-${{ github.run_number }}-${{ github.sha }}.deb
 91 |         path: target/debian/somo-${{ steps.version.outputs.version }}.deb
 92 | 
 93 |     - name: Publish to Crates.io
 94 |       run: cargo publish
 95 |       env:
 96 |         CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
 97 | 
 98 |     - name: Create Release
 99 |       uses: actions/create-release@v1
100 |       env:
101 |         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
102 |       with:
103 |         tag_name: v${{ steps.version.outputs.version }}
104 |         release_name: 🎉 Somo Release ${{ steps.version.outputs.version }}
105 |         body: ${{ steps.changelog.outputs.changelog }}
106 |         draft: false
107 |         prerelease: false
108 |       id: create_release
109 | 
110 |     - name: Upload Release Asset
111 |       uses: actions/upload-release-asset@v1
112 |       env:
113 |         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
114 |       with:
115 |         upload_url: ${{ steps.create_release.outputs.upload_url }}
116 |         asset_path: target/debian/somo-${{ steps.version.outputs.version }}.deb
117 |         asset_name: somo-${{ steps.version.outputs.version }}-${{ github.run_number }}-${{ github.sha }}.deb
118 |         asset_content_type: application/x-debian-package
119 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /.idea
3 | 
4 | # Nix
5 | /result
6 | 


--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
 1 | # Changelog
 2 | 
 3 | ## [1.1.0] - 09.07.2025
 4 | - **add** macOS support, by @belingud
 5 | - **add** sorting by column, by @gerelef
 6 | - **fix** panicking when being piped, by @gerelef
 7 | - **add** compact table view, by @theopfr
 8 | - **add** format and clippy check to CI, by @jerry1098
 9 | - **update** CLI arguments with `--tcp`, `--udp` flags, by @celeo
10 | - **add** shell completions, by @polponline
11 | - **add** Nix packaging support, by @kachick
12 | - **update** kill functionality to use SIGTERM, by @rongyi
13 | - **add** JSON and custom formattable output, by @aptypp @gerelef
14 | - **fix** typos in README, by @cma5 @robinhutty
15 | - **add** MIT license, by @theopfr
16 | 
17 | ---
18 | 
19 | ## [1.0.0] - 04.06.2025
20 | - **update** flags, by @theopfr
21 |     - move ``--local-port`` to ``--port``
22 |     - move ``--port`` to ``--remote-port``
23 |     - add ``--listen``
24 | - **remove** abuseipdb scanning, by @theopfr
25 | - **add** logo, by @theopfr
26 | - **add** tests, by @theopfr
27 | 
28 | ---


--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
   1 | # This file is automatically @generated by Cargo.
   2 | # It is not intended for manual editing.
   3 | version = 4
   4 | 
   5 | [[package]]
   6 | name = "adler2"
   7 | version = "2.0.1"
   8 | source = "registry+https://github.com/rust-lang/crates.io-index"
   9 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
  10 | 
  11 | [[package]]
  12 | name = "aho-corasick"
  13 | version = "1.1.3"
  14 | source = "registry+https://github.com/rust-lang/crates.io-index"
  15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
  16 | dependencies = [
  17 |  "memchr",
  18 | ]
  19 | 
  20 | [[package]]
  21 | name = "android-tzdata"
  22 | version = "0.1.1"
  23 | source = "registry+https://github.com/rust-lang/crates.io-index"
  24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
  25 | 
  26 | [[package]]
  27 | name = "android_system_properties"
  28 | version = "0.1.5"
  29 | source = "registry+https://github.com/rust-lang/crates.io-index"
  30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
  31 | dependencies = [
  32 |  "libc",
  33 | ]
  34 | 
  35 | [[package]]
  36 | name = "anstream"
  37 | version = "0.6.19"
  38 | source = "registry+https://github.com/rust-lang/crates.io-index"
  39 | checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
  40 | dependencies = [
  41 |  "anstyle",
  42 |  "anstyle-parse",
  43 |  "anstyle-query",
  44 |  "anstyle-wincon",
  45 |  "colorchoice",
  46 |  "is_terminal_polyfill",
  47 |  "utf8parse",
  48 | ]
  49 | 
  50 | [[package]]
  51 | name = "anstyle"
  52 | version = "1.0.11"
  53 | source = "registry+https://github.com/rust-lang/crates.io-index"
  54 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
  55 | 
  56 | [[package]]
  57 | name = "anstyle-parse"
  58 | version = "0.2.7"
  59 | source = "registry+https://github.com/rust-lang/crates.io-index"
  60 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
  61 | dependencies = [
  62 |  "utf8parse",
  63 | ]
  64 | 
  65 | [[package]]
  66 | name = "anstyle-query"
  67 | version = "1.1.3"
  68 | source = "registry+https://github.com/rust-lang/crates.io-index"
  69 | checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
  70 | dependencies = [
  71 |  "windows-sys 0.59.0",
  72 | ]
  73 | 
  74 | [[package]]
  75 | name = "anstyle-wincon"
  76 | version = "3.0.9"
  77 | source = "registry+https://github.com/rust-lang/crates.io-index"
  78 | checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
  79 | dependencies = [
  80 |  "anstyle",
  81 |  "once_cell_polyfill",
  82 |  "windows-sys 0.59.0",
  83 | ]
  84 | 
  85 | [[package]]
  86 | name = "anyhow"
  87 | version = "1.0.98"
  88 | source = "registry+https://github.com/rust-lang/crates.io-index"
  89 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
  90 | 
  91 | [[package]]
  92 | name = "autocfg"
  93 | version = "1.5.0"
  94 | source = "registry+https://github.com/rust-lang/crates.io-index"
  95 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
  96 | 
  97 | [[package]]
  98 | name = "bindgen"
  99 | version = "0.70.1"
 100 | source = "registry+https://github.com/rust-lang/crates.io-index"
 101 | checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
 102 | dependencies = [
 103 |  "bitflags 2.9.1",
 104 |  "cexpr",
 105 |  "clang-sys",
 106 |  "itertools",
 107 |  "proc-macro2",
 108 |  "quote",
 109 |  "regex",
 110 |  "rustc-hash 1.1.0",
 111 |  "shlex",
 112 |  "syn 2.0.104",
 113 | ]
 114 | 
 115 | [[package]]
 116 | name = "bindgen"
 117 | version = "0.71.1"
 118 | source = "registry+https://github.com/rust-lang/crates.io-index"
 119 | checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
 120 | dependencies = [
 121 |  "bitflags 2.9.1",
 122 |  "cexpr",
 123 |  "clang-sys",
 124 |  "itertools",
 125 |  "log",
 126 |  "prettyplease",
 127 |  "proc-macro2",
 128 |  "quote",
 129 |  "regex",
 130 |  "rustc-hash 2.1.1",
 131 |  "shlex",
 132 |  "syn 2.0.104",
 133 | ]
 134 | 
 135 | [[package]]
 136 | name = "bitflags"
 137 | version = "1.3.2"
 138 | source = "registry+https://github.com/rust-lang/crates.io-index"
 139 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 140 | 
 141 | [[package]]
 142 | name = "bitflags"
 143 | version = "2.9.1"
 144 | source = "registry+https://github.com/rust-lang/crates.io-index"
 145 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
 146 | 
 147 | [[package]]
 148 | name = "block-buffer"
 149 | version = "0.10.4"
 150 | source = "registry+https://github.com/rust-lang/crates.io-index"
 151 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
 152 | dependencies = [
 153 |  "generic-array",
 154 | ]
 155 | 
 156 | [[package]]
 157 | name = "bumpalo"
 158 | version = "3.19.0"
 159 | source = "registry+https://github.com/rust-lang/crates.io-index"
 160 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
 161 | 
 162 | [[package]]
 163 | name = "byteorder"
 164 | version = "1.5.0"
 165 | source = "registry+https://github.com/rust-lang/crates.io-index"
 166 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 167 | 
 168 | [[package]]
 169 | name = "bytes"
 170 | version = "1.10.1"
 171 | source = "registry+https://github.com/rust-lang/crates.io-index"
 172 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
 173 | 
 174 | [[package]]
 175 | name = "cc"
 176 | version = "1.2.29"
 177 | source = "registry+https://github.com/rust-lang/crates.io-index"
 178 | checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362"
 179 | dependencies = [
 180 |  "shlex",
 181 | ]
 182 | 
 183 | [[package]]
 184 | name = "cexpr"
 185 | version = "0.6.0"
 186 | source = "registry+https://github.com/rust-lang/crates.io-index"
 187 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
 188 | dependencies = [
 189 |  "nom",
 190 | ]
 191 | 
 192 | [[package]]
 193 | name = "cfg-if"
 194 | version = "1.0.1"
 195 | source = "registry+https://github.com/rust-lang/crates.io-index"
 196 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
 197 | 
 198 | [[package]]
 199 | name = "cfg_aliases"
 200 | version = "0.2.1"
 201 | source = "registry+https://github.com/rust-lang/crates.io-index"
 202 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
 203 | 
 204 | [[package]]
 205 | name = "chrono"
 206 | version = "0.4.41"
 207 | source = "registry+https://github.com/rust-lang/crates.io-index"
 208 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
 209 | dependencies = [
 210 |  "android-tzdata",
 211 |  "iana-time-zone",
 212 |  "num-traits",
 213 |  "windows-link",
 214 | ]
 215 | 
 216 | [[package]]
 217 | name = "clang-sys"
 218 | version = "1.8.1"
 219 | source = "registry+https://github.com/rust-lang/crates.io-index"
 220 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
 221 | dependencies = [
 222 |  "glob",
 223 |  "libc",
 224 |  "libloading",
 225 | ]
 226 | 
 227 | [[package]]
 228 | name = "clap"
 229 | version = "4.5.41"
 230 | source = "registry+https://github.com/rust-lang/crates.io-index"
 231 | checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9"
 232 | dependencies = [
 233 |  "clap_builder",
 234 |  "clap_derive",
 235 | ]
 236 | 
 237 | [[package]]
 238 | name = "clap_builder"
 239 | version = "4.5.41"
 240 | source = "registry+https://github.com/rust-lang/crates.io-index"
 241 | checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
 242 | dependencies = [
 243 |  "anstream",
 244 |  "anstyle",
 245 |  "clap_lex",
 246 |  "strsim",
 247 | ]
 248 | 
 249 | [[package]]
 250 | name = "clap_complete"
 251 | version = "4.5.55"
 252 | source = "registry+https://github.com/rust-lang/crates.io-index"
 253 | checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
 254 | dependencies = [
 255 |  "clap",
 256 | ]
 257 | 
 258 | [[package]]
 259 | name = "clap_derive"
 260 | version = "4.5.41"
 261 | source = "registry+https://github.com/rust-lang/crates.io-index"
 262 | checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
 263 | dependencies = [
 264 |  "heck",
 265 |  "proc-macro2",
 266 |  "quote",
 267 |  "syn 2.0.104",
 268 | ]
 269 | 
 270 | [[package]]
 271 | name = "clap_lex"
 272 | version = "0.7.5"
 273 | source = "registry+https://github.com/rust-lang/crates.io-index"
 274 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
 275 | 
 276 | [[package]]
 277 | name = "colorchoice"
 278 | version = "1.0.4"
 279 | source = "registry+https://github.com/rust-lang/crates.io-index"
 280 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
 281 | 
 282 | [[package]]
 283 | name = "convert_case"
 284 | version = "0.7.1"
 285 | source = "registry+https://github.com/rust-lang/crates.io-index"
 286 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
 287 | dependencies = [
 288 |  "unicode-segmentation",
 289 | ]
 290 | 
 291 | [[package]]
 292 | name = "coolor"
 293 | version = "1.1.0"
 294 | source = "registry+https://github.com/rust-lang/crates.io-index"
 295 | checksum = "980c2afde4af43d6a05c5be738f9eae595cff86dce1f38f88b95058a98c027f3"
 296 | dependencies = [
 297 |  "crossterm 0.29.0",
 298 | ]
 299 | 
 300 | [[package]]
 301 | name = "core-foundation-sys"
 302 | version = "0.8.7"
 303 | source = "registry+https://github.com/rust-lang/crates.io-index"
 304 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 305 | 
 306 | [[package]]
 307 | name = "cpufeatures"
 308 | version = "0.2.17"
 309 | source = "registry+https://github.com/rust-lang/crates.io-index"
 310 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
 311 | dependencies = [
 312 |  "libc",
 313 | ]
 314 | 
 315 | [[package]]
 316 | name = "crc32fast"
 317 | version = "1.5.0"
 318 | source = "registry+https://github.com/rust-lang/crates.io-index"
 319 | checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
 320 | dependencies = [
 321 |  "cfg-if",
 322 | ]
 323 | 
 324 | [[package]]
 325 | name = "crokey"
 326 | version = "1.2.0"
 327 | source = "registry+https://github.com/rust-lang/crates.io-index"
 328 | checksum = "5282b45c96c5978c8723ea83385cb9a488b64b7d175733f48d07bf9da514a863"
 329 | dependencies = [
 330 |  "crokey-proc_macros",
 331 |  "crossterm 0.29.0",
 332 |  "once_cell",
 333 |  "serde",
 334 |  "strict",
 335 | ]
 336 | 
 337 | [[package]]
 338 | name = "crokey-proc_macros"
 339 | version = "1.2.0"
 340 | source = "registry+https://github.com/rust-lang/crates.io-index"
 341 | checksum = "2ea0218d3fedf0797fa55676f1964ef5d27103d41ed0281b4bbd2a6e6c3d8d28"
 342 | dependencies = [
 343 |  "crossterm 0.29.0",
 344 |  "proc-macro2",
 345 |  "quote",
 346 |  "strict",
 347 |  "syn 2.0.104",
 348 | ]
 349 | 
 350 | [[package]]
 351 | name = "crossbeam"
 352 | version = "0.8.4"
 353 | source = "registry+https://github.com/rust-lang/crates.io-index"
 354 | checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
 355 | dependencies = [
 356 |  "crossbeam-channel",
 357 |  "crossbeam-deque",
 358 |  "crossbeam-epoch",
 359 |  "crossbeam-queue",
 360 |  "crossbeam-utils",
 361 | ]
 362 | 
 363 | [[package]]
 364 | name = "crossbeam-channel"
 365 | version = "0.5.15"
 366 | source = "registry+https://github.com/rust-lang/crates.io-index"
 367 | checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
 368 | dependencies = [
 369 |  "crossbeam-utils",
 370 | ]
 371 | 
 372 | [[package]]
 373 | name = "crossbeam-deque"
 374 | version = "0.8.6"
 375 | source = "registry+https://github.com/rust-lang/crates.io-index"
 376 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
 377 | dependencies = [
 378 |  "crossbeam-epoch",
 379 |  "crossbeam-utils",
 380 | ]
 381 | 
 382 | [[package]]
 383 | name = "crossbeam-epoch"
 384 | version = "0.9.18"
 385 | source = "registry+https://github.com/rust-lang/crates.io-index"
 386 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
 387 | dependencies = [
 388 |  "crossbeam-utils",
 389 | ]
 390 | 
 391 | [[package]]
 392 | name = "crossbeam-queue"
 393 | version = "0.3.12"
 394 | source = "registry+https://github.com/rust-lang/crates.io-index"
 395 | checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
 396 | dependencies = [
 397 |  "crossbeam-utils",
 398 | ]
 399 | 
 400 | [[package]]
 401 | name = "crossbeam-utils"
 402 | version = "0.8.21"
 403 | source = "registry+https://github.com/rust-lang/crates.io-index"
 404 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
 405 | 
 406 | [[package]]
 407 | name = "crossterm"
 408 | version = "0.25.0"
 409 | source = "registry+https://github.com/rust-lang/crates.io-index"
 410 | checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
 411 | dependencies = [
 412 |  "bitflags 1.3.2",
 413 |  "crossterm_winapi",
 414 |  "libc",
 415 |  "mio 0.8.11",
 416 |  "parking_lot",
 417 |  "signal-hook",
 418 |  "signal-hook-mio",
 419 |  "winapi",
 420 | ]
 421 | 
 422 | [[package]]
 423 | name = "crossterm"
 424 | version = "0.29.0"
 425 | source = "registry+https://github.com/rust-lang/crates.io-index"
 426 | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
 427 | dependencies = [
 428 |  "bitflags 2.9.1",
 429 |  "crossterm_winapi",
 430 |  "derive_more",
 431 |  "document-features",
 432 |  "mio 1.0.4",
 433 |  "parking_lot",
 434 |  "rustix 1.0.8",
 435 |  "signal-hook",
 436 |  "signal-hook-mio",
 437 |  "winapi",
 438 | ]
 439 | 
 440 | [[package]]
 441 | name = "crossterm_winapi"
 442 | version = "0.9.1"
 443 | source = "registry+https://github.com/rust-lang/crates.io-index"
 444 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
 445 | dependencies = [
 446 |  "winapi",
 447 | ]
 448 | 
 449 | [[package]]
 450 | name = "crypto-common"
 451 | version = "0.1.6"
 452 | source = "registry+https://github.com/rust-lang/crates.io-index"
 453 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 454 | dependencies = [
 455 |  "generic-array",
 456 |  "typenum",
 457 | ]
 458 | 
 459 | [[package]]
 460 | name = "darling"
 461 | version = "0.20.11"
 462 | source = "registry+https://github.com/rust-lang/crates.io-index"
 463 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
 464 | dependencies = [
 465 |  "darling_core",
 466 |  "darling_macro",
 467 | ]
 468 | 
 469 | [[package]]
 470 | name = "darling_core"
 471 | version = "0.20.11"
 472 | source = "registry+https://github.com/rust-lang/crates.io-index"
 473 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
 474 | dependencies = [
 475 |  "fnv",
 476 |  "ident_case",
 477 |  "proc-macro2",
 478 |  "quote",
 479 |  "strsim",
 480 |  "syn 2.0.104",
 481 | ]
 482 | 
 483 | [[package]]
 484 | name = "darling_macro"
 485 | version = "0.20.11"
 486 | source = "registry+https://github.com/rust-lang/crates.io-index"
 487 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
 488 | dependencies = [
 489 |  "darling_core",
 490 |  "quote",
 491 |  "syn 2.0.104",
 492 | ]
 493 | 
 494 | [[package]]
 495 | name = "derive_builder"
 496 | version = "0.20.2"
 497 | source = "registry+https://github.com/rust-lang/crates.io-index"
 498 | checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
 499 | dependencies = [
 500 |  "derive_builder_macro",
 501 | ]
 502 | 
 503 | [[package]]
 504 | name = "derive_builder_core"
 505 | version = "0.20.2"
 506 | source = "registry+https://github.com/rust-lang/crates.io-index"
 507 | checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
 508 | dependencies = [
 509 |  "darling",
 510 |  "proc-macro2",
 511 |  "quote",
 512 |  "syn 2.0.104",
 513 | ]
 514 | 
 515 | [[package]]
 516 | name = "derive_builder_macro"
 517 | version = "0.20.2"
 518 | source = "registry+https://github.com/rust-lang/crates.io-index"
 519 | checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
 520 | dependencies = [
 521 |  "derive_builder_core",
 522 |  "syn 2.0.104",
 523 | ]
 524 | 
 525 | [[package]]
 526 | name = "derive_more"
 527 | version = "2.0.1"
 528 | source = "registry+https://github.com/rust-lang/crates.io-index"
 529 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
 530 | dependencies = [
 531 |  "derive_more-impl",
 532 | ]
 533 | 
 534 | [[package]]
 535 | name = "derive_more-impl"
 536 | version = "2.0.1"
 537 | source = "registry+https://github.com/rust-lang/crates.io-index"
 538 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
 539 | dependencies = [
 540 |  "convert_case",
 541 |  "proc-macro2",
 542 |  "quote",
 543 |  "syn 2.0.104",
 544 | ]
 545 | 
 546 | [[package]]
 547 | name = "digest"
 548 | version = "0.10.7"
 549 | source = "registry+https://github.com/rust-lang/crates.io-index"
 550 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 551 | dependencies = [
 552 |  "block-buffer",
 553 |  "crypto-common",
 554 | ]
 555 | 
 556 | [[package]]
 557 | name = "document-features"
 558 | version = "0.2.11"
 559 | source = "registry+https://github.com/rust-lang/crates.io-index"
 560 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
 561 | dependencies = [
 562 |  "litrs",
 563 | ]
 564 | 
 565 | [[package]]
 566 | name = "dyn-clone"
 567 | version = "1.0.19"
 568 | source = "registry+https://github.com/rust-lang/crates.io-index"
 569 | checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
 570 | 
 571 | [[package]]
 572 | name = "either"
 573 | version = "1.15.0"
 574 | source = "registry+https://github.com/rust-lang/crates.io-index"
 575 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
 576 | 
 577 | [[package]]
 578 | name = "errno"
 579 | version = "0.3.13"
 580 | source = "registry+https://github.com/rust-lang/crates.io-index"
 581 | checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
 582 | dependencies = [
 583 |  "libc",
 584 |  "windows-sys 0.60.2",
 585 | ]
 586 | 
 587 | [[package]]
 588 | name = "flate2"
 589 | version = "1.1.2"
 590 | source = "registry+https://github.com/rust-lang/crates.io-index"
 591 | checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
 592 | dependencies = [
 593 |  "crc32fast",
 594 |  "miniz_oxide",
 595 | ]
 596 | 
 597 | [[package]]
 598 | name = "fnv"
 599 | version = "1.0.7"
 600 | source = "registry+https://github.com/rust-lang/crates.io-index"
 601 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 602 | 
 603 | [[package]]
 604 | name = "fuzzy-matcher"
 605 | version = "0.3.7"
 606 | source = "registry+https://github.com/rust-lang/crates.io-index"
 607 | checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
 608 | dependencies = [
 609 |  "thread_local",
 610 | ]
 611 | 
 612 | [[package]]
 613 | name = "fxhash"
 614 | version = "0.2.1"
 615 | source = "registry+https://github.com/rust-lang/crates.io-index"
 616 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
 617 | dependencies = [
 618 |  "byteorder",
 619 | ]
 620 | 
 621 | [[package]]
 622 | name = "generic-array"
 623 | version = "0.14.7"
 624 | source = "registry+https://github.com/rust-lang/crates.io-index"
 625 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
 626 | dependencies = [
 627 |  "typenum",
 628 |  "version_check",
 629 | ]
 630 | 
 631 | [[package]]
 632 | name = "glob"
 633 | version = "0.3.2"
 634 | source = "registry+https://github.com/rust-lang/crates.io-index"
 635 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
 636 | 
 637 | [[package]]
 638 | name = "handlebars"
 639 | version = "6.3.2"
 640 | source = "registry+https://github.com/rust-lang/crates.io-index"
 641 | checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098"
 642 | dependencies = [
 643 |  "derive_builder",
 644 |  "log",
 645 |  "num-order",
 646 |  "pest",
 647 |  "pest_derive",
 648 |  "serde",
 649 |  "serde_json",
 650 |  "thiserror 2.0.12",
 651 | ]
 652 | 
 653 | [[package]]
 654 | name = "heck"
 655 | version = "0.5.0"
 656 | source = "registry+https://github.com/rust-lang/crates.io-index"
 657 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 658 | 
 659 | [[package]]
 660 | name = "hex"
 661 | version = "0.4.3"
 662 | source = "registry+https://github.com/rust-lang/crates.io-index"
 663 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
 664 | 
 665 | [[package]]
 666 | name = "iana-time-zone"
 667 | version = "0.1.63"
 668 | source = "registry+https://github.com/rust-lang/crates.io-index"
 669 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
 670 | dependencies = [
 671 |  "android_system_properties",
 672 |  "core-foundation-sys",
 673 |  "iana-time-zone-haiku",
 674 |  "js-sys",
 675 |  "log",
 676 |  "wasm-bindgen",
 677 |  "windows-core",
 678 | ]
 679 | 
 680 | [[package]]
 681 | name = "iana-time-zone-haiku"
 682 | version = "0.1.2"
 683 | source = "registry+https://github.com/rust-lang/crates.io-index"
 684 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
 685 | dependencies = [
 686 |  "cc",
 687 | ]
 688 | 
 689 | [[package]]
 690 | name = "ident_case"
 691 | version = "1.0.1"
 692 | source = "registry+https://github.com/rust-lang/crates.io-index"
 693 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 694 | 
 695 | [[package]]
 696 | name = "inquire"
 697 | version = "0.7.5"
 698 | source = "registry+https://github.com/rust-lang/crates.io-index"
 699 | checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a"
 700 | dependencies = [
 701 |  "bitflags 2.9.1",
 702 |  "crossterm 0.25.0",
 703 |  "dyn-clone",
 704 |  "fuzzy-matcher",
 705 |  "fxhash",
 706 |  "newline-converter",
 707 |  "once_cell",
 708 |  "unicode-segmentation",
 709 |  "unicode-width",
 710 | ]
 711 | 
 712 | [[package]]
 713 | name = "is_terminal_polyfill"
 714 | version = "1.70.1"
 715 | source = "registry+https://github.com/rust-lang/crates.io-index"
 716 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
 717 | 
 718 | [[package]]
 719 | name = "itertools"
 720 | version = "0.13.0"
 721 | source = "registry+https://github.com/rust-lang/crates.io-index"
 722 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
 723 | dependencies = [
 724 |  "either",
 725 | ]
 726 | 
 727 | [[package]]
 728 | name = "itoa"
 729 | version = "1.0.15"
 730 | source = "registry+https://github.com/rust-lang/crates.io-index"
 731 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
 732 | 
 733 | [[package]]
 734 | name = "js-sys"
 735 | version = "0.3.77"
 736 | source = "registry+https://github.com/rust-lang/crates.io-index"
 737 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
 738 | dependencies = [
 739 |  "once_cell",
 740 |  "wasm-bindgen",
 741 | ]
 742 | 
 743 | [[package]]
 744 | name = "lazy-regex"
 745 | version = "3.4.1"
 746 | source = "registry+https://github.com/rust-lang/crates.io-index"
 747 | checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126"
 748 | dependencies = [
 749 |  "lazy-regex-proc_macros",
 750 |  "once_cell",
 751 |  "regex",
 752 | ]
 753 | 
 754 | [[package]]
 755 | name = "lazy-regex-proc_macros"
 756 | version = "3.4.1"
 757 | source = "registry+https://github.com/rust-lang/crates.io-index"
 758 | checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1"
 759 | dependencies = [
 760 |  "proc-macro2",
 761 |  "quote",
 762 |  "regex",
 763 |  "syn 2.0.104",
 764 | ]
 765 | 
 766 | [[package]]
 767 | name = "libc"
 768 | version = "0.2.174"
 769 | source = "registry+https://github.com/rust-lang/crates.io-index"
 770 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
 771 | 
 772 | [[package]]
 773 | name = "libloading"
 774 | version = "0.8.8"
 775 | source = "registry+https://github.com/rust-lang/crates.io-index"
 776 | checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
 777 | dependencies = [
 778 |  "cfg-if",
 779 |  "windows-targets 0.53.2",
 780 | ]
 781 | 
 782 | [[package]]
 783 | name = "libproc"
 784 | version = "0.14.10"
 785 | source = "registry+https://github.com/rust-lang/crates.io-index"
 786 | checksum = "e78a09b56be5adbcad5aa1197371688dc6bb249a26da3bca2011ee2fb987ebfb"
 787 | dependencies = [
 788 |  "bindgen 0.70.1",
 789 |  "errno",
 790 |  "libc",
 791 | ]
 792 | 
 793 | [[package]]
 794 | name = "linux-raw-sys"
 795 | version = "0.4.15"
 796 | source = "registry+https://github.com/rust-lang/crates.io-index"
 797 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
 798 | 
 799 | [[package]]
 800 | name = "linux-raw-sys"
 801 | version = "0.9.4"
 802 | source = "registry+https://github.com/rust-lang/crates.io-index"
 803 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
 804 | 
 805 | [[package]]
 806 | name = "litrs"
 807 | version = "0.4.1"
 808 | source = "registry+https://github.com/rust-lang/crates.io-index"
 809 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
 810 | 
 811 | [[package]]
 812 | name = "lock_api"
 813 | version = "0.4.13"
 814 | source = "registry+https://github.com/rust-lang/crates.io-index"
 815 | checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
 816 | dependencies = [
 817 |  "autocfg",
 818 |  "scopeguard",
 819 | ]
 820 | 
 821 | [[package]]
 822 | name = "log"
 823 | version = "0.4.27"
 824 | source = "registry+https://github.com/rust-lang/crates.io-index"
 825 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
 826 | 
 827 | [[package]]
 828 | name = "memchr"
 829 | version = "2.7.5"
 830 | source = "registry+https://github.com/rust-lang/crates.io-index"
 831 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
 832 | 
 833 | [[package]]
 834 | name = "minimad"
 835 | version = "0.13.1"
 836 | source = "registry+https://github.com/rust-lang/crates.io-index"
 837 | checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2"
 838 | dependencies = [
 839 |  "once_cell",
 840 | ]
 841 | 
 842 | [[package]]
 843 | name = "minimal-lexical"
 844 | version = "0.2.1"
 845 | source = "registry+https://github.com/rust-lang/crates.io-index"
 846 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 847 | 
 848 | [[package]]
 849 | name = "miniz_oxide"
 850 | version = "0.8.9"
 851 | source = "registry+https://github.com/rust-lang/crates.io-index"
 852 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
 853 | dependencies = [
 854 |  "adler2",
 855 | ]
 856 | 
 857 | [[package]]
 858 | name = "mio"
 859 | version = "0.8.11"
 860 | source = "registry+https://github.com/rust-lang/crates.io-index"
 861 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
 862 | dependencies = [
 863 |  "libc",
 864 |  "log",
 865 |  "wasi",
 866 |  "windows-sys 0.48.0",
 867 | ]
 868 | 
 869 | [[package]]
 870 | name = "mio"
 871 | version = "1.0.4"
 872 | source = "registry+https://github.com/rust-lang/crates.io-index"
 873 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
 874 | dependencies = [
 875 |  "libc",
 876 |  "log",
 877 |  "wasi",
 878 |  "windows-sys 0.59.0",
 879 | ]
 880 | 
 881 | [[package]]
 882 | name = "netlink-packet-core"
 883 | version = "0.7.0"
 884 | source = "registry+https://github.com/rust-lang/crates.io-index"
 885 | checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4"
 886 | dependencies = [
 887 |  "anyhow",
 888 |  "byteorder",
 889 |  "netlink-packet-utils",
 890 | ]
 891 | 
 892 | [[package]]
 893 | name = "netlink-packet-sock-diag"
 894 | version = "0.4.2"
 895 | source = "registry+https://github.com/rust-lang/crates.io-index"
 896 | checksum = "a495cb1de50560a7cd12fdcf023db70eec00e340df81be31cedbbfd4aadd6b76"
 897 | dependencies = [
 898 |  "anyhow",
 899 |  "bitflags 1.3.2",
 900 |  "byteorder",
 901 |  "libc",
 902 |  "netlink-packet-core",
 903 |  "netlink-packet-utils",
 904 |  "smallvec",
 905 | ]
 906 | 
 907 | [[package]]
 908 | name = "netlink-packet-utils"
 909 | version = "0.5.2"
 910 | source = "registry+https://github.com/rust-lang/crates.io-index"
 911 | checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34"
 912 | dependencies = [
 913 |  "anyhow",
 914 |  "byteorder",
 915 |  "paste",
 916 |  "thiserror 1.0.69",
 917 | ]
 918 | 
 919 | [[package]]
 920 | name = "netlink-sys"
 921 | version = "0.8.7"
 922 | source = "registry+https://github.com/rust-lang/crates.io-index"
 923 | checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23"
 924 | dependencies = [
 925 |  "bytes",
 926 |  "libc",
 927 |  "log",
 928 | ]
 929 | 
 930 | [[package]]
 931 | name = "netstat2"
 932 | version = "0.11.1"
 933 | source = "registry+https://github.com/rust-lang/crates.io-index"
 934 | checksum = "6422b6a8c7635e8a82323e4cdf07a90e91901e07f4c1f0f3a245d54b4637e55c"
 935 | dependencies = [
 936 |  "bindgen 0.71.1",
 937 |  "bitflags 2.9.1",
 938 |  "byteorder",
 939 |  "netlink-packet-core",
 940 |  "netlink-packet-sock-diag",
 941 |  "netlink-packet-utils",
 942 |  "netlink-sys",
 943 |  "num-derive",
 944 |  "num-traits",
 945 |  "thiserror 2.0.12",
 946 | ]
 947 | 
 948 | [[package]]
 949 | name = "newline-converter"
 950 | version = "0.3.0"
 951 | source = "registry+https://github.com/rust-lang/crates.io-index"
 952 | checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f"
 953 | dependencies = [
 954 |  "unicode-segmentation",
 955 | ]
 956 | 
 957 | [[package]]
 958 | name = "nix"
 959 | version = "0.30.1"
 960 | source = "registry+https://github.com/rust-lang/crates.io-index"
 961 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
 962 | dependencies = [
 963 |  "bitflags 2.9.1",
 964 |  "cfg-if",
 965 |  "cfg_aliases",
 966 |  "libc",
 967 | ]
 968 | 
 969 | [[package]]
 970 | name = "nom"
 971 | version = "7.1.3"
 972 | source = "registry+https://github.com/rust-lang/crates.io-index"
 973 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
 974 | dependencies = [
 975 |  "memchr",
 976 |  "minimal-lexical",
 977 | ]
 978 | 
 979 | [[package]]
 980 | name = "num-derive"
 981 | version = "0.3.3"
 982 | source = "registry+https://github.com/rust-lang/crates.io-index"
 983 | checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
 984 | dependencies = [
 985 |  "proc-macro2",
 986 |  "quote",
 987 |  "syn 1.0.109",
 988 | ]
 989 | 
 990 | [[package]]
 991 | name = "num-modular"
 992 | version = "0.6.1"
 993 | source = "registry+https://github.com/rust-lang/crates.io-index"
 994 | checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
 995 | 
 996 | [[package]]
 997 | name = "num-order"
 998 | version = "1.2.0"
 999 | source = "registry+https://github.com/rust-lang/crates.io-index"
1000 | checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
1001 | dependencies = [
1002 |  "num-modular",
1003 | ]
1004 | 
1005 | [[package]]
1006 | name = "num-traits"
1007 | version = "0.2.19"
1008 | source = "registry+https://github.com/rust-lang/crates.io-index"
1009 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
1010 | dependencies = [
1011 |  "autocfg",
1012 | ]
1013 | 
1014 | [[package]]
1015 | name = "once_cell"
1016 | version = "1.21.3"
1017 | source = "registry+https://github.com/rust-lang/crates.io-index"
1018 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
1019 | 
1020 | [[package]]
1021 | name = "once_cell_polyfill"
1022 | version = "1.70.1"
1023 | source = "registry+https://github.com/rust-lang/crates.io-index"
1024 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
1025 | 
1026 | [[package]]
1027 | name = "parking_lot"
1028 | version = "0.12.4"
1029 | source = "registry+https://github.com/rust-lang/crates.io-index"
1030 | checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
1031 | dependencies = [
1032 |  "lock_api",
1033 |  "parking_lot_core",
1034 | ]
1035 | 
1036 | [[package]]
1037 | name = "parking_lot_core"
1038 | version = "0.9.11"
1039 | source = "registry+https://github.com/rust-lang/crates.io-index"
1040 | checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
1041 | dependencies = [
1042 |  "cfg-if",
1043 |  "libc",
1044 |  "redox_syscall",
1045 |  "smallvec",
1046 |  "windows-targets 0.52.6",
1047 | ]
1048 | 
1049 | [[package]]
1050 | name = "paste"
1051 | version = "1.0.15"
1052 | source = "registry+https://github.com/rust-lang/crates.io-index"
1053 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
1054 | 
1055 | [[package]]
1056 | name = "pest"
1057 | version = "2.8.1"
1058 | source = "registry+https://github.com/rust-lang/crates.io-index"
1059 | checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
1060 | dependencies = [
1061 |  "memchr",
1062 |  "thiserror 2.0.12",
1063 |  "ucd-trie",
1064 | ]
1065 | 
1066 | [[package]]
1067 | name = "pest_derive"
1068 | version = "2.8.1"
1069 | source = "registry+https://github.com/rust-lang/crates.io-index"
1070 | checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
1071 | dependencies = [
1072 |  "pest",
1073 |  "pest_generator",
1074 | ]
1075 | 
1076 | [[package]]
1077 | name = "pest_generator"
1078 | version = "2.8.1"
1079 | source = "registry+https://github.com/rust-lang/crates.io-index"
1080 | checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
1081 | dependencies = [
1082 |  "pest",
1083 |  "pest_meta",
1084 |  "proc-macro2",
1085 |  "quote",
1086 |  "syn 2.0.104",
1087 | ]
1088 | 
1089 | [[package]]
1090 | name = "pest_meta"
1091 | version = "2.8.1"
1092 | source = "registry+https://github.com/rust-lang/crates.io-index"
1093 | checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
1094 | dependencies = [
1095 |  "pest",
1096 |  "sha2",
1097 | ]
1098 | 
1099 | [[package]]
1100 | name = "prettyplease"
1101 | version = "0.2.35"
1102 | source = "registry+https://github.com/rust-lang/crates.io-index"
1103 | checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a"
1104 | dependencies = [
1105 |  "proc-macro2",
1106 |  "syn 2.0.104",
1107 | ]
1108 | 
1109 | [[package]]
1110 | name = "proc-macro2"
1111 | version = "1.0.95"
1112 | source = "registry+https://github.com/rust-lang/crates.io-index"
1113 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
1114 | dependencies = [
1115 |  "unicode-ident",
1116 | ]
1117 | 
1118 | [[package]]
1119 | name = "procfs"
1120 | version = "0.17.0"
1121 | source = "registry+https://github.com/rust-lang/crates.io-index"
1122 | checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f"
1123 | dependencies = [
1124 |  "bitflags 2.9.1",
1125 |  "chrono",
1126 |  "flate2",
1127 |  "hex",
1128 |  "procfs-core",
1129 |  "rustix 0.38.44",
1130 | ]
1131 | 
1132 | [[package]]
1133 | name = "procfs-core"
1134 | version = "0.17.0"
1135 | source = "registry+https://github.com/rust-lang/crates.io-index"
1136 | checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec"
1137 | dependencies = [
1138 |  "bitflags 2.9.1",
1139 |  "chrono",
1140 |  "hex",
1141 | ]
1142 | 
1143 | [[package]]
1144 | name = "quote"
1145 | version = "1.0.40"
1146 | source = "registry+https://github.com/rust-lang/crates.io-index"
1147 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
1148 | dependencies = [
1149 |  "proc-macro2",
1150 | ]
1151 | 
1152 | [[package]]
1153 | name = "redox_syscall"
1154 | version = "0.5.13"
1155 | source = "registry+https://github.com/rust-lang/crates.io-index"
1156 | checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
1157 | dependencies = [
1158 |  "bitflags 2.9.1",
1159 | ]
1160 | 
1161 | [[package]]
1162 | name = "regex"
1163 | version = "1.11.1"
1164 | source = "registry+https://github.com/rust-lang/crates.io-index"
1165 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
1166 | dependencies = [
1167 |  "aho-corasick",
1168 |  "memchr",
1169 |  "regex-automata",
1170 |  "regex-syntax",
1171 | ]
1172 | 
1173 | [[package]]
1174 | name = "regex-automata"
1175 | version = "0.4.9"
1176 | source = "registry+https://github.com/rust-lang/crates.io-index"
1177 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
1178 | dependencies = [
1179 |  "aho-corasick",
1180 |  "memchr",
1181 |  "regex-syntax",
1182 | ]
1183 | 
1184 | [[package]]
1185 | name = "regex-syntax"
1186 | version = "0.8.5"
1187 | source = "registry+https://github.com/rust-lang/crates.io-index"
1188 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
1189 | 
1190 | [[package]]
1191 | name = "rustc-hash"
1192 | version = "1.1.0"
1193 | source = "registry+https://github.com/rust-lang/crates.io-index"
1194 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
1195 | 
1196 | [[package]]
1197 | name = "rustc-hash"
1198 | version = "2.1.1"
1199 | source = "registry+https://github.com/rust-lang/crates.io-index"
1200 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
1201 | 
1202 | [[package]]
1203 | name = "rustix"
1204 | version = "0.38.44"
1205 | source = "registry+https://github.com/rust-lang/crates.io-index"
1206 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
1207 | dependencies = [
1208 |  "bitflags 2.9.1",
1209 |  "errno",
1210 |  "libc",
1211 |  "linux-raw-sys 0.4.15",
1212 |  "windows-sys 0.59.0",
1213 | ]
1214 | 
1215 | [[package]]
1216 | name = "rustix"
1217 | version = "1.0.8"
1218 | source = "registry+https://github.com/rust-lang/crates.io-index"
1219 | checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
1220 | dependencies = [
1221 |  "bitflags 2.9.1",
1222 |  "errno",
1223 |  "libc",
1224 |  "linux-raw-sys 0.9.4",
1225 |  "windows-sys 0.60.2",
1226 | ]
1227 | 
1228 | [[package]]
1229 | name = "rustversion"
1230 | version = "1.0.21"
1231 | source = "registry+https://github.com/rust-lang/crates.io-index"
1232 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
1233 | 
1234 | [[package]]
1235 | name = "ryu"
1236 | version = "1.0.20"
1237 | source = "registry+https://github.com/rust-lang/crates.io-index"
1238 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
1239 | 
1240 | [[package]]
1241 | name = "scopeguard"
1242 | version = "1.2.0"
1243 | source = "registry+https://github.com/rust-lang/crates.io-index"
1244 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
1245 | 
1246 | [[package]]
1247 | name = "serde"
1248 | version = "1.0.219"
1249 | source = "registry+https://github.com/rust-lang/crates.io-index"
1250 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
1251 | dependencies = [
1252 |  "serde_derive",
1253 | ]
1254 | 
1255 | [[package]]
1256 | name = "serde_derive"
1257 | version = "1.0.219"
1258 | source = "registry+https://github.com/rust-lang/crates.io-index"
1259 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
1260 | dependencies = [
1261 |  "proc-macro2",
1262 |  "quote",
1263 |  "syn 2.0.104",
1264 | ]
1265 | 
1266 | [[package]]
1267 | name = "serde_json"
1268 | version = "1.0.140"
1269 | source = "registry+https://github.com/rust-lang/crates.io-index"
1270 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
1271 | dependencies = [
1272 |  "itoa",
1273 |  "memchr",
1274 |  "ryu",
1275 |  "serde",
1276 | ]
1277 | 
1278 | [[package]]
1279 | name = "sha2"
1280 | version = "0.10.9"
1281 | source = "registry+https://github.com/rust-lang/crates.io-index"
1282 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
1283 | dependencies = [
1284 |  "cfg-if",
1285 |  "cpufeatures",
1286 |  "digest",
1287 | ]
1288 | 
1289 | [[package]]
1290 | name = "shlex"
1291 | version = "1.3.0"
1292 | source = "registry+https://github.com/rust-lang/crates.io-index"
1293 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
1294 | 
1295 | [[package]]
1296 | name = "signal-hook"
1297 | version = "0.3.18"
1298 | source = "registry+https://github.com/rust-lang/crates.io-index"
1299 | checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
1300 | dependencies = [
1301 |  "libc",
1302 |  "signal-hook-registry",
1303 | ]
1304 | 
1305 | [[package]]
1306 | name = "signal-hook-mio"
1307 | version = "0.2.4"
1308 | source = "registry+https://github.com/rust-lang/crates.io-index"
1309 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
1310 | dependencies = [
1311 |  "libc",
1312 |  "mio 0.8.11",
1313 |  "mio 1.0.4",
1314 |  "signal-hook",
1315 | ]
1316 | 
1317 | [[package]]
1318 | name = "signal-hook-registry"
1319 | version = "1.4.5"
1320 | source = "registry+https://github.com/rust-lang/crates.io-index"
1321 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
1322 | dependencies = [
1323 |  "libc",
1324 | ]
1325 | 
1326 | [[package]]
1327 | name = "smallvec"
1328 | version = "1.15.1"
1329 | source = "registry+https://github.com/rust-lang/crates.io-index"
1330 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
1331 | 
1332 | [[package]]
1333 | name = "somo"
1334 | version = "1.1.0"
1335 | dependencies = [
1336 |  "clap",
1337 |  "clap_complete",
1338 |  "handlebars",
1339 |  "inquire",
1340 |  "libproc",
1341 |  "netstat2",
1342 |  "nix",
1343 |  "procfs",
1344 |  "serde",
1345 |  "serde_json",
1346 |  "termimad",
1347 | ]
1348 | 
1349 | [[package]]
1350 | name = "strict"
1351 | version = "0.2.0"
1352 | source = "registry+https://github.com/rust-lang/crates.io-index"
1353 | checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006"
1354 | 
1355 | [[package]]
1356 | name = "strsim"
1357 | version = "0.11.1"
1358 | source = "registry+https://github.com/rust-lang/crates.io-index"
1359 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
1360 | 
1361 | [[package]]
1362 | name = "syn"
1363 | version = "1.0.109"
1364 | source = "registry+https://github.com/rust-lang/crates.io-index"
1365 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
1366 | dependencies = [
1367 |  "proc-macro2",
1368 |  "quote",
1369 |  "unicode-ident",
1370 | ]
1371 | 
1372 | [[package]]
1373 | name = "syn"
1374 | version = "2.0.104"
1375 | source = "registry+https://github.com/rust-lang/crates.io-index"
1376 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
1377 | dependencies = [
1378 |  "proc-macro2",
1379 |  "quote",
1380 |  "unicode-ident",
1381 | ]
1382 | 
1383 | [[package]]
1384 | name = "termimad"
1385 | version = "0.33.0"
1386 | source = "registry+https://github.com/rust-lang/crates.io-index"
1387 | checksum = "b23646cdde4c2e9d6f2621424751b48653e5755f50163b2ff5ce3b32ebf17104"
1388 | dependencies = [
1389 |  "coolor",
1390 |  "crokey",
1391 |  "crossbeam",
1392 |  "lazy-regex",
1393 |  "minimad",
1394 |  "serde",
1395 |  "thiserror 2.0.12",
1396 |  "unicode-width",
1397 | ]
1398 | 
1399 | [[package]]
1400 | name = "thiserror"
1401 | version = "1.0.69"
1402 | source = "registry+https://github.com/rust-lang/crates.io-index"
1403 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
1404 | dependencies = [
1405 |  "thiserror-impl 1.0.69",
1406 | ]
1407 | 
1408 | [[package]]
1409 | name = "thiserror"
1410 | version = "2.0.12"
1411 | source = "registry+https://github.com/rust-lang/crates.io-index"
1412 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
1413 | dependencies = [
1414 |  "thiserror-impl 2.0.12",
1415 | ]
1416 | 
1417 | [[package]]
1418 | name = "thiserror-impl"
1419 | version = "1.0.69"
1420 | source = "registry+https://github.com/rust-lang/crates.io-index"
1421 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
1422 | dependencies = [
1423 |  "proc-macro2",
1424 |  "quote",
1425 |  "syn 2.0.104",
1426 | ]
1427 | 
1428 | [[package]]
1429 | name = "thiserror-impl"
1430 | version = "2.0.12"
1431 | source = "registry+https://github.com/rust-lang/crates.io-index"
1432 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
1433 | dependencies = [
1434 |  "proc-macro2",
1435 |  "quote",
1436 |  "syn 2.0.104",
1437 | ]
1438 | 
1439 | [[package]]
1440 | name = "thread_local"
1441 | version = "1.1.9"
1442 | source = "registry+https://github.com/rust-lang/crates.io-index"
1443 | checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
1444 | dependencies = [
1445 |  "cfg-if",
1446 | ]
1447 | 
1448 | [[package]]
1449 | name = "typenum"
1450 | version = "1.18.0"
1451 | source = "registry+https://github.com/rust-lang/crates.io-index"
1452 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
1453 | 
1454 | [[package]]
1455 | name = "ucd-trie"
1456 | version = "0.1.7"
1457 | source = "registry+https://github.com/rust-lang/crates.io-index"
1458 | checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
1459 | 
1460 | [[package]]
1461 | name = "unicode-ident"
1462 | version = "1.0.18"
1463 | source = "registry+https://github.com/rust-lang/crates.io-index"
1464 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
1465 | 
1466 | [[package]]
1467 | name = "unicode-segmentation"
1468 | version = "1.12.0"
1469 | source = "registry+https://github.com/rust-lang/crates.io-index"
1470 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
1471 | 
1472 | [[package]]
1473 | name = "unicode-width"
1474 | version = "0.1.14"
1475 | source = "registry+https://github.com/rust-lang/crates.io-index"
1476 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
1477 | 
1478 | [[package]]
1479 | name = "utf8parse"
1480 | version = "0.2.2"
1481 | source = "registry+https://github.com/rust-lang/crates.io-index"
1482 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1483 | 
1484 | [[package]]
1485 | name = "version_check"
1486 | version = "0.9.5"
1487 | source = "registry+https://github.com/rust-lang/crates.io-index"
1488 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
1489 | 
1490 | [[package]]
1491 | name = "wasi"
1492 | version = "0.11.1+wasi-snapshot-preview1"
1493 | source = "registry+https://github.com/rust-lang/crates.io-index"
1494 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
1495 | 
1496 | [[package]]
1497 | name = "wasm-bindgen"
1498 | version = "0.2.100"
1499 | source = "registry+https://github.com/rust-lang/crates.io-index"
1500 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
1501 | dependencies = [
1502 |  "cfg-if",
1503 |  "once_cell",
1504 |  "rustversion",
1505 |  "wasm-bindgen-macro",
1506 | ]
1507 | 
1508 | [[package]]
1509 | name = "wasm-bindgen-backend"
1510 | version = "0.2.100"
1511 | source = "registry+https://github.com/rust-lang/crates.io-index"
1512 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
1513 | dependencies = [
1514 |  "bumpalo",
1515 |  "log",
1516 |  "proc-macro2",
1517 |  "quote",
1518 |  "syn 2.0.104",
1519 |  "wasm-bindgen-shared",
1520 | ]
1521 | 
1522 | [[package]]
1523 | name = "wasm-bindgen-macro"
1524 | version = "0.2.100"
1525 | source = "registry+https://github.com/rust-lang/crates.io-index"
1526 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
1527 | dependencies = [
1528 |  "quote",
1529 |  "wasm-bindgen-macro-support",
1530 | ]
1531 | 
1532 | [[package]]
1533 | name = "wasm-bindgen-macro-support"
1534 | version = "0.2.100"
1535 | source = "registry+https://github.com/rust-lang/crates.io-index"
1536 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
1537 | dependencies = [
1538 |  "proc-macro2",
1539 |  "quote",
1540 |  "syn 2.0.104",
1541 |  "wasm-bindgen-backend",
1542 |  "wasm-bindgen-shared",
1543 | ]
1544 | 
1545 | [[package]]
1546 | name = "wasm-bindgen-shared"
1547 | version = "0.2.100"
1548 | source = "registry+https://github.com/rust-lang/crates.io-index"
1549 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
1550 | dependencies = [
1551 |  "unicode-ident",
1552 | ]
1553 | 
1554 | [[package]]
1555 | name = "winapi"
1556 | version = "0.3.9"
1557 | source = "registry+https://github.com/rust-lang/crates.io-index"
1558 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1559 | dependencies = [
1560 |  "winapi-i686-pc-windows-gnu",
1561 |  "winapi-x86_64-pc-windows-gnu",
1562 | ]
1563 | 
1564 | [[package]]
1565 | name = "winapi-i686-pc-windows-gnu"
1566 | version = "0.4.0"
1567 | source = "registry+https://github.com/rust-lang/crates.io-index"
1568 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1569 | 
1570 | [[package]]
1571 | name = "winapi-x86_64-pc-windows-gnu"
1572 | version = "0.4.0"
1573 | source = "registry+https://github.com/rust-lang/crates.io-index"
1574 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1575 | 
1576 | [[package]]
1577 | name = "windows-core"
1578 | version = "0.61.2"
1579 | source = "registry+https://github.com/rust-lang/crates.io-index"
1580 | checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
1581 | dependencies = [
1582 |  "windows-implement",
1583 |  "windows-interface",
1584 |  "windows-link",
1585 |  "windows-result",
1586 |  "windows-strings",
1587 | ]
1588 | 
1589 | [[package]]
1590 | name = "windows-implement"
1591 | version = "0.60.0"
1592 | source = "registry+https://github.com/rust-lang/crates.io-index"
1593 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
1594 | dependencies = [
1595 |  "proc-macro2",
1596 |  "quote",
1597 |  "syn 2.0.104",
1598 | ]
1599 | 
1600 | [[package]]
1601 | name = "windows-interface"
1602 | version = "0.59.1"
1603 | source = "registry+https://github.com/rust-lang/crates.io-index"
1604 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
1605 | dependencies = [
1606 |  "proc-macro2",
1607 |  "quote",
1608 |  "syn 2.0.104",
1609 | ]
1610 | 
1611 | [[package]]
1612 | name = "windows-link"
1613 | version = "0.1.3"
1614 | source = "registry+https://github.com/rust-lang/crates.io-index"
1615 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
1616 | 
1617 | [[package]]
1618 | name = "windows-result"
1619 | version = "0.3.4"
1620 | source = "registry+https://github.com/rust-lang/crates.io-index"
1621 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
1622 | dependencies = [
1623 |  "windows-link",
1624 | ]
1625 | 
1626 | [[package]]
1627 | name = "windows-strings"
1628 | version = "0.4.2"
1629 | source = "registry+https://github.com/rust-lang/crates.io-index"
1630 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
1631 | dependencies = [
1632 |  "windows-link",
1633 | ]
1634 | 
1635 | [[package]]
1636 | name = "windows-sys"
1637 | version = "0.48.0"
1638 | source = "registry+https://github.com/rust-lang/crates.io-index"
1639 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
1640 | dependencies = [
1641 |  "windows-targets 0.48.5",
1642 | ]
1643 | 
1644 | [[package]]
1645 | name = "windows-sys"
1646 | version = "0.59.0"
1647 | source = "registry+https://github.com/rust-lang/crates.io-index"
1648 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
1649 | dependencies = [
1650 |  "windows-targets 0.52.6",
1651 | ]
1652 | 
1653 | [[package]]
1654 | name = "windows-sys"
1655 | version = "0.60.2"
1656 | source = "registry+https://github.com/rust-lang/crates.io-index"
1657 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
1658 | dependencies = [
1659 |  "windows-targets 0.53.2",
1660 | ]
1661 | 
1662 | [[package]]
1663 | name = "windows-targets"
1664 | version = "0.48.5"
1665 | source = "registry+https://github.com/rust-lang/crates.io-index"
1666 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
1667 | dependencies = [
1668 |  "windows_aarch64_gnullvm 0.48.5",
1669 |  "windows_aarch64_msvc 0.48.5",
1670 |  "windows_i686_gnu 0.48.5",
1671 |  "windows_i686_msvc 0.48.5",
1672 |  "windows_x86_64_gnu 0.48.5",
1673 |  "windows_x86_64_gnullvm 0.48.5",
1674 |  "windows_x86_64_msvc 0.48.5",
1675 | ]
1676 | 
1677 | [[package]]
1678 | name = "windows-targets"
1679 | version = "0.52.6"
1680 | source = "registry+https://github.com/rust-lang/crates.io-index"
1681 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
1682 | dependencies = [
1683 |  "windows_aarch64_gnullvm 0.52.6",
1684 |  "windows_aarch64_msvc 0.52.6",
1685 |  "windows_i686_gnu 0.52.6",
1686 |  "windows_i686_gnullvm 0.52.6",
1687 |  "windows_i686_msvc 0.52.6",
1688 |  "windows_x86_64_gnu 0.52.6",
1689 |  "windows_x86_64_gnullvm 0.52.6",
1690 |  "windows_x86_64_msvc 0.52.6",
1691 | ]
1692 | 
1693 | [[package]]
1694 | name = "windows-targets"
1695 | version = "0.53.2"
1696 | source = "registry+https://github.com/rust-lang/crates.io-index"
1697 | checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
1698 | dependencies = [
1699 |  "windows_aarch64_gnullvm 0.53.0",
1700 |  "windows_aarch64_msvc 0.53.0",
1701 |  "windows_i686_gnu 0.53.0",
1702 |  "windows_i686_gnullvm 0.53.0",
1703 |  "windows_i686_msvc 0.53.0",
1704 |  "windows_x86_64_gnu 0.53.0",
1705 |  "windows_x86_64_gnullvm 0.53.0",
1706 |  "windows_x86_64_msvc 0.53.0",
1707 | ]
1708 | 
1709 | [[package]]
1710 | name = "windows_aarch64_gnullvm"
1711 | version = "0.48.5"
1712 | source = "registry+https://github.com/rust-lang/crates.io-index"
1713 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
1714 | 
1715 | [[package]]
1716 | name = "windows_aarch64_gnullvm"
1717 | version = "0.52.6"
1718 | source = "registry+https://github.com/rust-lang/crates.io-index"
1719 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
1720 | 
1721 | [[package]]
1722 | name = "windows_aarch64_gnullvm"
1723 | version = "0.53.0"
1724 | source = "registry+https://github.com/rust-lang/crates.io-index"
1725 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
1726 | 
1727 | [[package]]
1728 | name = "windows_aarch64_msvc"
1729 | version = "0.48.5"
1730 | source = "registry+https://github.com/rust-lang/crates.io-index"
1731 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
1732 | 
1733 | [[package]]
1734 | name = "windows_aarch64_msvc"
1735 | version = "0.52.6"
1736 | source = "registry+https://github.com/rust-lang/crates.io-index"
1737 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
1738 | 
1739 | [[package]]
1740 | name = "windows_aarch64_msvc"
1741 | version = "0.53.0"
1742 | source = "registry+https://github.com/rust-lang/crates.io-index"
1743 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
1744 | 
1745 | [[package]]
1746 | name = "windows_i686_gnu"
1747 | version = "0.48.5"
1748 | source = "registry+https://github.com/rust-lang/crates.io-index"
1749 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
1750 | 
1751 | [[package]]
1752 | name = "windows_i686_gnu"
1753 | version = "0.52.6"
1754 | source = "registry+https://github.com/rust-lang/crates.io-index"
1755 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
1756 | 
1757 | [[package]]
1758 | name = "windows_i686_gnu"
1759 | version = "0.53.0"
1760 | source = "registry+https://github.com/rust-lang/crates.io-index"
1761 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
1762 | 
1763 | [[package]]
1764 | name = "windows_i686_gnullvm"
1765 | version = "0.52.6"
1766 | source = "registry+https://github.com/rust-lang/crates.io-index"
1767 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
1768 | 
1769 | [[package]]
1770 | name = "windows_i686_gnullvm"
1771 | version = "0.53.0"
1772 | source = "registry+https://github.com/rust-lang/crates.io-index"
1773 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
1774 | 
1775 | [[package]]
1776 | name = "windows_i686_msvc"
1777 | version = "0.48.5"
1778 | source = "registry+https://github.com/rust-lang/crates.io-index"
1779 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
1780 | 
1781 | [[package]]
1782 | name = "windows_i686_msvc"
1783 | version = "0.52.6"
1784 | source = "registry+https://github.com/rust-lang/crates.io-index"
1785 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
1786 | 
1787 | [[package]]
1788 | name = "windows_i686_msvc"
1789 | version = "0.53.0"
1790 | source = "registry+https://github.com/rust-lang/crates.io-index"
1791 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
1792 | 
1793 | [[package]]
1794 | name = "windows_x86_64_gnu"
1795 | version = "0.48.5"
1796 | source = "registry+https://github.com/rust-lang/crates.io-index"
1797 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
1798 | 
1799 | [[package]]
1800 | name = "windows_x86_64_gnu"
1801 | version = "0.52.6"
1802 | source = "registry+https://github.com/rust-lang/crates.io-index"
1803 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
1804 | 
1805 | [[package]]
1806 | name = "windows_x86_64_gnu"
1807 | version = "0.53.0"
1808 | source = "registry+https://github.com/rust-lang/crates.io-index"
1809 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
1810 | 
1811 | [[package]]
1812 | name = "windows_x86_64_gnullvm"
1813 | version = "0.48.5"
1814 | source = "registry+https://github.com/rust-lang/crates.io-index"
1815 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
1816 | 
1817 | [[package]]
1818 | name = "windows_x86_64_gnullvm"
1819 | version = "0.52.6"
1820 | source = "registry+https://github.com/rust-lang/crates.io-index"
1821 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
1822 | 
1823 | [[package]]
1824 | name = "windows_x86_64_gnullvm"
1825 | version = "0.53.0"
1826 | source = "registry+https://github.com/rust-lang/crates.io-index"
1827 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
1828 | 
1829 | [[package]]
1830 | name = "windows_x86_64_msvc"
1831 | version = "0.48.5"
1832 | source = "registry+https://github.com/rust-lang/crates.io-index"
1833 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
1834 | 
1835 | [[package]]
1836 | name = "windows_x86_64_msvc"
1837 | version = "0.52.6"
1838 | source = "registry+https://github.com/rust-lang/crates.io-index"
1839 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1840 | 
1841 | [[package]]
1842 | name = "windows_x86_64_msvc"
1843 | version = "0.53.0"
1844 | source = "registry+https://github.com/rust-lang/crates.io-index"
1845 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
1846 | 


--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
 1 | [package]
 2 | name = "somo"
 3 | version = "1.1.0"
 4 | edition = "2021"
 5 | authors = ["theopfr"]
 6 | description = "A human-friendly alternative to netstat for socket and port monitoring on Linux and macOS."
 7 | license = "MIT"
 8 | readme = "./README.md"
 9 | repository = "https://github.com/theopfr/somo/"
10 | keywords = ["netstat", "socket-monitoring", "port-checker"]
11 | categories = ["command-line-utilities"]
12 | 
13 | [dependencies]
14 | clap = { version = "4.5.40", features = ["derive"] }
15 | clap_complete = "4.5.54"
16 | inquire = "0.7.5"
17 | termimad = "0.33.0"
18 | serde = { version = "1.0.196", features = ["derive"] }
19 | serde_json = "1.0.96"
20 | handlebars = "6.3.2"
21 | nix = {version = "0.30.1", features = ["process", "signal"]}
22 | 
23 | [target.'cfg(target_os = "linux")'.dependencies]
24 | procfs = "0.17.0"
25 | 
26 | [target.'cfg(target_os = "macos")'.dependencies]
27 | netstat2 = "0.11.1"
28 | libproc = "0.14.10"
29 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2025 Theodor Peifer
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | <p align="center">
  2 |   <img style="width: 250px" src="./images/somo-logo.png" />
  3 | </p>
  4 | 
  5 | 
  6 | ### A human-friendly alternative to netstat for socket and port monitoring on Linux and macOS.
  7 | 
  8 | > [!NOTE]  
  9 | > The master branch code and readme may include features that are not yet released. For the official, stable version and its documentation, please refer to the [crates.io](https://crates.io/crates/somo) page.
 10 | 
 11 | ## ✨ Highlights:
 12 | - pleasing to the eye thanks to a nice table view
 13 | - filterable and sortable output
 14 | - interactive killing of processes
 15 | - JSON and custom formattable output
 16 | - from ``netstat -tulpn`` to ``somo -l``
 17 | - cross-platform support for Linux and macOS
 18 | - you can find all features further down
 19 | 
 20 | <br />
 21 | 
 22 | <p align="center">
 23 |   <img src="./images/somo-example.png" />
 24 | </p>
 25 | 
 26 | ## ⬇️ Installation:
 27 | 
 28 | ### Option 1 - Debian:
 29 | If you use a Debian OS go to [releases](https://github.com/theopfr/somo/releases) and download the latest .deb release.
 30 | 
 31 | ### Option 2 - crates.io:
 32 | ```sh
 33 | cargo install somo
 34 | ```
 35 | Most of the time you will want to run this in ``sudo`` mode to see all processes and ports. By default, this is not possible when installed via cargo. But you can create a symlink so the binary can be run as root:
 36 | ```sh
 37 | sudo ln -s ~/.cargo/bin/somo /usr/local/bin/somo
 38 | sudo somo   # this works now
 39 | ```
 40 | 
 41 | ### Option 3 - GitHub (Development Version):
 42 | *Warning:* This is the cutting-edge development version and may be unstable or contain incomplete features. You can install it via cargo directly from the GitHub repository.
 43 | 
 44 | ```sh
 45 | cargo install --git https://github.com/theopfr/somo
 46 | ```
 47 | 
 48 | ### Option 4 - Nix (Development Version):
 49 | You can build and use the development version using Nix with Flakes.
 50 | ```sh
 51 | nix build 'github:theopfr/somo?dir=nix'
 52 | sudo ./result/bin/somo
 53 | ```
 54 | 
 55 | ---
 56 | 
 57 | ## 🏃‍♀️ Running somo:
 58 | To run somo just type: 
 59 | ```sh
 60 | sudo somo
 61 | ```
 62 | 
 63 | Somo supports the following features:
 64 | 
 65 | ### ✨ Filtering:
 66 | You can use the following flags to filter based on different attributes:
 67 | | filter flag | description | value |
 68 | | :------------- |:------------- | :----- |
 69 | | ```--tcp, -t``` | filter by TCP connections | - |
 70 | | ```--udp, -u``` | filter by UDP connections  | - | 
 71 | | ```--proto``` | deprecated – use ``--tcp`` / ``--udp`` instead | ``tcp`` or ``udp`` | 
 72 | | ```--port, -p``` | filter by a local port | port number, e.g ``5433`` |
 73 | | ```--remote-port``` | filter by a remote port | port number, e.g ``443`` |
 74 | | ```--ip``` | filter by a remote IP | IP address e.g ``0.0.0.0`` |
 75 | | ```--program``` | filter by a client program | program name e.g ``chrome`` |
 76 | | ```--pid``` | filter by a PID | PID number, e.g ``10000`` |
 77 | | ```--open, -o``` | filter by open connections | - |
 78 | | ```--listen, -l``` | filter by listening connections | - |
 79 | | ```--exclude-ipv6``` | don't list IPv6 connections | - |
 80 | 
 81 | ### ✨ Compact table view:
 82 | To get a smaller, more compact table use the ``--compact, -c`` flag.
 83 | 
 84 | <img style="width: 75%" src="./images/somo-compact-example.png" />
 85 | 
 86 | ### ✨ Process killing:
 87 | With the ``--kill, -k`` flag you can choose to kill a process after inspecting the connections using an interactive selection option.
 88 | 
 89 | <img style="width: 75%" src="./images/somo-kill-example.png" />
 90 | 
 91 | ### ✨ JSON and custom output format:
 92 | Using the ``--json`` flag you can choose to retrieve the connection data in JSON format. <br />
 93 | You can also define a custom output format using the ``--format`` flag, for example:
 94 | ```sh
 95 | somo --format "PID: {{pid}}, Protocol: {{proto}}, Remote Address: {{remote_address}}" # attributes must be specified in snake_case
 96 | ```
 97 | In the format-string, the attributes have to be specified in *snake_case*.
 98 | 
 99 | ### ✨ Sorting by columns:
100 | The ``--sort, -s`` flag can be used to sort the table after a specific column ascending. For example:
101 | ```sh
102 | somo --sort pid   # column names must be specified in snake_case
103 | ```
104 | To get a descending order, you can use the ``--reverse, -r`` flag.
105 | 
106 | ---
107 | 
108 | ## 🐚 Shell Completions:
109 | Somo supports shell completions for bash, zsh, fish, and elvish. Choose your shell:
110 | 
111 | #### Bash
112 | ```bash
113 | mkdir -p ~/.local/share/bash-completion/completions
114 | somo generate-completions bash > ~/.local/share/bash-completion/completions/somo
115 | ```
116 | 
117 | #### Zsh
118 | ```zsh
119 | mkdir -p ~/.local/share/zsh/site-functions
120 | somo generate-completions zsh > ~/.local/share/zsh/site-functions/_somo
121 | echo 'fpath=(~/.local/share/zsh/site-functions $fpath)' >> ~/.zshrc
122 | echo 'autoload -U compinit && compinit' >> ~/.zshrc
123 | ```
124 | 
125 | #### Fish
126 | ```fish
127 | mkdir -p ~/.config/fish/completions
128 | somo generate-completions fish > ~/.config/fish/completions/somo.fish
129 | ```
130 | 
131 | #### Elvish
132 | ```bash
133 | mkdir -p ~/.config/elvish/lib
134 | somo generate-completions elvish > ~/.config/elvish/lib/somo.elv
135 | echo 'use somo' >> ~/.config/elvish/rc.elv
136 | ```
137 | 
138 | ---
139 | 
140 | ## 🖥️ Platform Support:
141 | Somo currently supports:
142 | - Linux: Full support using the [procfs](https://crates.io/crates/procfs) crate
143 | - macOS: Full support using [netstat2](https://crates.io/crates/netstat2) and [libproc](https://crates.io/crates/libproc/0.13.0) crates
144 | 


--------------------------------------------------------------------------------
/images/somo-compact-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theopfr/somo/752419df33470c35e4f3f78f2e66e7de87d409d2/images/somo-compact-example.png


--------------------------------------------------------------------------------
/images/somo-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theopfr/somo/752419df33470c35e4f3f78f2e66e7de87d409d2/images/somo-example.png


--------------------------------------------------------------------------------
/images/somo-kill-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theopfr/somo/752419df33470c35e4f3f78f2e66e7de87d409d2/images/somo-kill-example.png


--------------------------------------------------------------------------------
/images/somo-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theopfr/somo/752419df33470c35e4f3f78f2e66e7de87d409d2/images/somo-logo.png


--------------------------------------------------------------------------------
/nix/flake.lock:
--------------------------------------------------------------------------------
 1 | {
 2 |   "nodes": {
 3 |     "nixpkgs": {
 4 |       "locked": {
 5 |         "lastModified": 1752596105,
 6 |         "narHash": "sha256-lFNVsu/mHLq3q11MuGkMhUUoSXEdQjCHvpReaGP1S2k=",
 7 |         "owner": "NixOS",
 8 |         "repo": "nixpkgs",
 9 |         "rev": "dab3a6e781554f965bde3def0aa2fda4eb8f1708",
10 |         "type": "github"
11 |       },
12 |       "original": {
13 |         "owner": "NixOS",
14 |         "ref": "nixpkgs-unstable",
15 |         "repo": "nixpkgs",
16 |         "type": "github"
17 |       }
18 |     },
19 |     "root": {
20 |       "inputs": {
21 |         "nixpkgs": "nixpkgs"
22 |       }
23 |     }
24 |   },
25 |   "root": "root",
26 |   "version": 7
27 | }
28 | 


--------------------------------------------------------------------------------
/nix/flake.nix:
--------------------------------------------------------------------------------
 1 | {
 2 |   inputs = {
 3 |     nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
 4 |   };
 5 | 
 6 |   outputs =
 7 |     {
 8 |       nixpkgs,
 9 |       ...
10 |     }:
11 |     let
12 |       lib = nixpkgs.lib;
13 |       forAllSystems = lib.genAttrs lib.systems.flakeExposed;
14 |     in
15 |     {
16 |       formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.nixfmt-tree);
17 | 
18 |       packages = forAllSystems (
19 |         system:
20 |         let
21 |           pkgs = nixpkgs.legacyPackages.${system};
22 |         in
23 |         {
24 |           default = pkgs.callPackage ./package.nix { };
25 |         }
26 |       );
27 |     };
28 | }
29 | 


--------------------------------------------------------------------------------
/nix/package.nix:
--------------------------------------------------------------------------------
 1 | {
 2 |   lib,
 3 |   rustPlatform,
 4 |   versionCheckHook,
 5 | }:
 6 | 
 7 | let
 8 |   mainProgram = "somo";
 9 | in
10 | rustPlatform.buildRustPackage (finalAttrs: {
11 |   pname = "somo";
12 |   version = with builtins; (fromTOML (readFile ../Cargo.toml)).package.version;
13 | 
14 |   src = lib.fileset.toSource {
15 |     root = ./..;
16 |     fileset = lib.fileset.unions [
17 |       ../src
18 |       ../Cargo.toml
19 |       ../Cargo.lock
20 |     ];
21 |   };
22 | 
23 |   cargoLock.lockFile = ../Cargo.lock;
24 | 
25 |   nativeInstallCheckInputs = [
26 |     versionCheckHook
27 |   ];
28 |   doInstallCheck = true;
29 |   versionCheckProgram = "${placeholder "out"}/bin/${mainProgram}";
30 |   versionCheckProgramArg = "--version";
31 | 
32 |   meta = {
33 |     inherit mainProgram;
34 |     description = "Human-friendly alternative to netstat for socket and port monitoring";
35 |     homepage = "https://github.com/theopfr/somo";
36 |     license = lib.licenses.mit;
37 |     platforms = lib.platforms.linux ++ lib.platforms.darwin;
38 |   };
39 | })
40 | 


--------------------------------------------------------------------------------
/src/cli.rs:
--------------------------------------------------------------------------------
  1 | use clap::{Parser, Subcommand};
  2 | use clap_complete::{generate, Generator, Shell};
  3 | use inquire::InquireError;
  4 | use inquire::Select;
  5 | use nix::sys::signal;
  6 | use nix::unistd::Pid;
  7 | use std::str::FromStr;
  8 | use std::{io, string::String};
  9 | 
 10 | use crate::schemas::{Connection, Protocol, Protocols};
 11 | use crate::utils;
 12 | 
 13 | /// Used for parsing all the flag values provided by the user in the CLI.
 14 | #[derive(Debug, Default)]
 15 | pub struct Flags {
 16 |     pub kill: bool,
 17 |     pub proto: Option<String>,
 18 |     pub tcp: bool,
 19 |     pub udp: bool,
 20 |     pub ip: Option<String>,
 21 |     pub remote_port: Option<String>,
 22 |     pub port: Option<String>,
 23 |     pub program: Option<String>,
 24 |     pub pid: Option<String>,
 25 |     pub format: Option<String>,
 26 |     pub json: bool,
 27 |     pub open: bool,
 28 |     pub listen: bool,
 29 |     pub exclude_ipv6: bool,
 30 |     pub compact: bool,
 31 |     pub sort: Option<SortField>,
 32 |     pub reverse: bool,
 33 | }
 34 | 
 35 | /// Represents all possible flags which can be provided by the user in the CLI.
 36 | #[derive(Parser, Debug)]
 37 | #[command(author, version, about, long_about = None)]
 38 | pub struct Args {
 39 |     #[command(subcommand)]
 40 |     command: Option<Commands>,
 41 | 
 42 |     /// Display an interactive selection option after inspecting connections
 43 |     #[arg(short = 'k', long, default_value = None)]
 44 |     kill: bool,
 45 | 
 46 |     /// Deprecated. Use '--tcp' and '--udp' instead.
 47 |     #[arg(long, default_value = None)]
 48 |     proto: Option<String>,
 49 | 
 50 |     /// Include TCP connections
 51 |     #[arg(short, long, default_value = None)]
 52 |     tcp: bool,
 53 | 
 54 |     /// Include UDP connections
 55 |     #[arg(short, long, default_value = None)]
 56 |     udp: bool,
 57 | 
 58 |     /// Filter connections by remote IP address
 59 |     #[arg(long, default_value = None)]
 60 |     ip: Option<String>,
 61 | 
 62 |     /// Filter connections by remote port
 63 |     #[arg(long, default_value = None)]
 64 |     remote_port: Option<String>,
 65 | 
 66 |     /// Filter connections by local port
 67 |     #[arg(short = 'p', long, default_value = None)]
 68 |     port: Option<String>,
 69 | 
 70 |     /// Filter connections by program name
 71 |     #[arg(long, default_value = None)]
 72 |     program: Option<String>,
 73 | 
 74 |     /// Filter connections by PID
 75 |     #[arg(long, default_value = None)]
 76 |     pid: Option<String>,
 77 | 
 78 |     /// Format the output in a certain way, e.g., `somo --format "PID: {{pid}}, Protocol: {{proto}}, Remote Address: {{remote_address}}"`
 79 |     #[arg(long, default_value = None)]
 80 |     format: Option<String>,
 81 | 
 82 |     /// Output in JSON
 83 |     #[arg(long, default_value_t = false)]
 84 |     json: bool,
 85 | 
 86 |     /// Filter by open connections
 87 |     #[arg(short = 'o', long, default_value_t = false)]
 88 |     open: bool,
 89 | 
 90 |     /// Filter by listening connections
 91 |     #[arg(short = 'l', long, default_value_t = false)]
 92 |     listen: bool,
 93 | 
 94 |     /// Exclude IPv6 connections
 95 |     #[arg(long, default_value_t = false)]
 96 |     exclude_ipv6: bool,
 97 | 
 98 |     #[arg(short = 'c', long, default_value_t = false)]
 99 |     compact: bool,
100 | 
101 |     /// Reverse order of the table
102 |     #[arg(short = 'r', long, default_value_t = false)]
103 |     reverse: bool,
104 | 
105 |     /// Sort by column name
106 |     #[arg(short = 's', long, default_value = None)]
107 |     sort: Option<SortField>,
108 | }
109 | 
110 | #[derive(Subcommand, Debug)]
111 | pub enum Commands {
112 |     /// Generate shell completions
113 |     GenerateCompletions {
114 |         /// The shell to generate completions for
115 |         #[arg(value_enum)]
116 |         shell: Shell,
117 |     },
118 | }
119 | 
120 | pub enum CliCommand {
121 |     Run(Flags),
122 |     Subcommand(Commands),
123 | }
124 | 
125 | #[derive(clap::ValueEnum, Clone, Copy, Debug)]
126 | #[clap(rename_all = "snake_case")]
127 | pub enum SortField {
128 |     Proto,
129 |     LocalPort,
130 |     RemoteAddress,
131 |     RemotePort,
132 |     Program,
133 |     Pid,
134 |     State,
135 | }
136 | 
137 | /// Gets all flag values provided by the user in the CLI using the "clap" crate.
138 | ///
139 | /// # Arguments
140 | /// None
141 | ///
142 | /// # Returns
143 | /// A `CliCommand` enum which contains either the `Run` variant with the parsed flags or the `Subcommand` variant with a specific command.
144 | pub fn cli() -> CliCommand {
145 |     let args = Args::parse();
146 | 
147 |     match args.command {
148 |         Some(cmd) => CliCommand::Subcommand(cmd),
149 |         None => CliCommand::Run(Flags {
150 |             kill: args.kill,
151 |             proto: args.proto,
152 |             tcp: args.tcp,
153 |             udp: args.udp,
154 |             ip: args.ip,
155 |             remote_port: args.remote_port,
156 |             port: args.port,
157 |             program: args.program,
158 |             pid: args.pid,
159 |             format: args.format,
160 |             json: args.json,
161 |             open: args.open,
162 |             listen: args.listen,
163 |             exclude_ipv6: args.exclude_ipv6,
164 |             compact: args.compact,
165 |             sort: args.sort,
166 |             reverse: args.reverse,
167 |         }),
168 |     }
169 | }
170 | 
171 | pub fn sort_connections(all_connections: &mut [Connection], field: SortField) {
172 |     all_connections.sort_by(|our, other| match field {
173 |         SortField::Proto => our.proto.to_lowercase().cmp(&other.proto.to_lowercase()),
174 |         SortField::LocalPort => our
175 |             .local_port
176 |             .parse::<u32>()
177 |             .unwrap_or(0)
178 |             .cmp(&other.local_port.parse::<u32>().unwrap_or(0)),
179 |         SortField::RemoteAddress => our.ipvx_raw.cmp(&other.ipvx_raw),
180 |         SortField::RemotePort => our
181 |             .remote_port
182 |             .parse::<u32>()
183 |             .unwrap_or(0)
184 |             .cmp(&other.remote_port.parse::<u32>().unwrap_or(0)),
185 |         SortField::Program => our
186 |             .program
187 |             .to_lowercase()
188 |             .cmp(&other.program.to_lowercase()),
189 |         SortField::Pid => our.pid.cmp(&other.pid),
190 |         SortField::State => our.state.to_lowercase().cmp(&other.state.to_lowercase()),
191 |     });
192 | }
193 | 
194 | /// Determines which protocols to include based on CLI flags.
195 | ///
196 | /// The `--tcp` and `--udp` flags take precedence over the deprecated `--proto` flag.
197 | /// If either `--tcp` or `--udp` is set, `--proto` is ignored.
198 | /// If no relevant flags are set, both TCP and UDP are enabled by default.
199 | ///
200 | /// # Arguments
201 | /// * `args`: Parsed CLI flags (of interest: `--tcp`, `--udp`, and optionally `--proto`)
202 | ///
203 | /// # Returns
204 | /// A `Protocols` struct indicating whether to include TCP, UDP, or both.
205 | pub fn resolve_protocols(args: &Flags) -> Protocols {
206 |     let mut protocols = Protocols::default();
207 |     if args.tcp || args.udp {
208 |         protocols.tcp = args.tcp;
209 |         protocols.udp = args.udp;
210 |     } else if let Some(arg) = &args.proto {
211 |         // Support the deprecated '--proto' argument
212 |         if let Ok(matching) = Protocol::from_str(arg) {
213 |             match matching {
214 |                 Protocol::Tcp => protocols.tcp = true,
215 |                 Protocol::Udp => protocols.udp = true,
216 |             }
217 |         }
218 |     } else {
219 |         protocols.tcp = true;
220 |         protocols.udp = true;
221 |     }
222 |     protocols
223 | }
224 | 
225 | /// Generates and prints shell completions to stdout.
226 | ///
227 | /// # Arguments
228 | /// * `gen` - The shell to generate completions for
229 | /// * `cmd` - The clap command to generate completions for
230 | ///
231 | /// # Returns
232 | /// None
233 | pub fn print_completions<G: Generator>(gen: G, cmd: &mut clap::Command) {
234 |     generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout());
235 | }
236 | 
237 | /// Kills a process by its PID.
238 | ///
239 | /// # Argument
240 | /// * `pid`: The PID value as a string.
241 | ///
242 | /// # Returns
243 | /// None
244 | pub fn kill_process(pid_num: i32) {
245 |     let pid = Pid::from_raw(pid_num);
246 | 
247 |     match signal::kill(pid, signal::Signal::SIGTERM) {
248 |         Ok(_) => utils::pretty_print_info(&format!("Killed process with PID {pid}.")),
249 |         Err(_) => utils::pretty_print_error(&format!("Failed to kill process with PID {pid}.")),
250 |     }
251 | }
252 | 
253 | /// Starts an interactive selection process in the console for choosing a process to kill using the "inquire" crate.
254 | ///
255 | /// # Argument
256 | /// * `connections`: A vector containing all connections which themselves contain a PID value.
257 | ///
258 | /// # Returns
259 | /// None
260 | pub fn interactive_process_kill(connections: &[Connection]) {
261 |     let selection: Result<u32, InquireError> = Select::new(
262 |         "Which process to kill (search or type index)?",
263 |         (1..=connections.len() as u32).collect(),
264 |     )
265 |     .prompt();
266 | 
267 |     match selection {
268 |         Ok(choice) => {
269 |             let pid_str = &connections[choice as usize - 1].pid;
270 |             let pid_num = match pid_str.parse::<i32>() {
271 |                 Ok(pid) => pid,
272 |                 Err(_) => {
273 |                     utils::pretty_print_error("Couldn't find PID.");
274 |                     return;
275 |                 }
276 |             };
277 |             kill_process(pid_num)
278 |         }
279 |         Err(_) => {
280 |             utils::pretty_print_error("Process selection cancelled.");
281 |         }
282 |     }
283 | }
284 | 
285 | #[cfg(test)]
286 | mod tests {
287 |     use std::{net::IpAddr, str::FromStr};
288 | 
289 |     use crate::{
290 |         cli::{resolve_protocols, sort_connections, SortField},
291 |         schemas::AddressType,
292 |     };
293 | 
294 |     use super::{Args, Commands, Flags};
295 |     use clap::Parser;
296 | 
297 |     #[test]
298 |     fn test_all_flags_parsing() {
299 |         let args = Args::parse_from([
300 |             "test-bin",
301 |             "-k",
302 |             "--proto",
303 |             "udp",
304 |             "--tcp",
305 |             "--udp",
306 |             "--ip",
307 |             "192.168.0.1",
308 |             "--remote-port",
309 |             "53",
310 |             "-p",
311 |             "8080",
312 |             "--program",
313 |             "nginx",
314 |             "--pid",
315 |             "1234",
316 |             "-o",
317 |             "-l",
318 |             "--exclude-ipv6",
319 |         ]);
320 | 
321 |         assert!(args.kill);
322 |         assert_eq!(args.proto.as_deref(), Some("udp"));
323 |         assert!(args.tcp);
324 |         assert!(args.udp);
325 |         assert_eq!(args.ip.as_deref(), Some("192.168.0.1"));
326 |         assert_eq!(args.remote_port.as_deref(), Some("53"));
327 |         assert_eq!(args.port.as_deref(), Some("8080"));
328 |         assert_eq!(args.program.as_deref(), Some("nginx"));
329 |         assert_eq!(args.pid.as_deref(), Some("1234"));
330 |         assert!(args.open);
331 |         assert!(args.listen);
332 |         assert!(args.exclude_ipv6);
333 |     }
334 | 
335 |     #[test]
336 |     fn test_default_values() {
337 |         let args = Args::parse_from(["test-bin"]);
338 | 
339 |         assert!(!args.kill);
340 |         assert!(args.proto.is_none());
341 |         assert!(!args.tcp);
342 |         assert!(!args.udp);
343 |         assert!(args.ip.is_none());
344 |         assert!(args.remote_port.is_none());
345 |         assert!(args.port.is_none());
346 |         assert!(args.program.is_none());
347 |         assert!(args.pid.is_none());
348 |         assert!(!args.open);
349 |         assert!(!args.listen);
350 |         assert!(!args.exclude_ipv6);
351 |     }
352 | 
353 |     #[test]
354 |     fn test_flag_short_and_long_equivalence() {
355 |         let short = Args::parse_from(["test-bin", "-k", "-p", "80", "-o", "-l"]);
356 |         let long = Args::parse_from(["test-bin", "--kill", "--port", "80", "--open", "--listen"]);
357 | 
358 |         assert_eq!(short.kill, long.kill);
359 |         assert_eq!(short.port, long.port);
360 |         assert_eq!(short.open, long.open);
361 |         assert_eq!(short.listen, long.listen);
362 |         assert_eq!(short.exclude_ipv6, long.exclude_ipv6);
363 |     }
364 | 
365 |     #[test]
366 |     fn test_resolve_protocols() {
367 |         // Test deprecated --proto
368 |         let flags = Flags {
369 |             tcp: false,
370 |             udp: false,
371 |             proto: Some("tcp".into()),
372 |             ..Default::default()
373 |         };
374 |         let result = resolve_protocols(&flags);
375 |         assert!(result.tcp);
376 |         assert!(!result.udp);
377 | 
378 |         // Test precendence of --tcp/--udp over --proto
379 |         let flags = Flags {
380 |             tcp: true,
381 |             udp: false,
382 |             proto: Some("udp".into()),
383 |             ..Default::default()
384 |         };
385 |         let result = resolve_protocols(&flags);
386 |         assert!(result.tcp);
387 |         assert!(!result.udp);
388 | 
389 |         // Test default with no protocol flags
390 |         let flags = Flags {
391 |             tcp: false,
392 |             udp: false,
393 |             proto: None,
394 |             ..Default::default()
395 |         };
396 |         let result = resolve_protocols(&flags);
397 |         assert!(result.tcp);
398 |         assert!(result.udp);
399 | 
400 |         // Test both --tcp and --udp set
401 |         let flags = Flags {
402 |             tcp: true,
403 |             udp: true,
404 |             proto: Some("tcp".into()),
405 |             ..Default::default()
406 |         };
407 |         let result = resolve_protocols(&flags);
408 |         assert!(result.tcp);
409 |         assert!(result.udp);
410 |     }
411 | 
412 |     #[test]
413 |     fn test_generate_completions_subcommand() {
414 |         let args = Args::parse_from(["test-bin", "generate-completions", "bash"]);
415 | 
416 |         match args.command {
417 |             Some(Commands::GenerateCompletions { shell }) => {
418 |                 assert_eq!(shell.to_string(), "bash");
419 |             }
420 |             _ => panic!("Expected GenerateCompletions command"),
421 |         }
422 |     }
423 | 
424 |     #[test]
425 |     fn test_generate_completions_all_shells() {
426 |         let shells = ["bash", "zsh", "fish", "elvish"];
427 | 
428 |         for shell in &shells {
429 |             let args = Args::parse_from(["test-bin", "generate-completions", shell]);
430 | 
431 |             match args.command {
432 |                 Some(Commands::GenerateCompletions {
433 |                     shell: parsed_shell,
434 |                 }) => {
435 |                     assert_eq!(parsed_shell.to_string(), *shell);
436 |                 }
437 |                 _ => panic!("Expected GenerateCompletions command for {shell}"),
438 |             }
439 |         }
440 |     }
441 | 
442 |     #[test]
443 |     fn test_cli_returns_none_for_subcommands() {
444 |         // Mock the Args parsing by directly testing the logic
445 |         // This test ensures that when a subcommand is present, cli() returns None
446 | 
447 |         // We can't easily test the full cli() function without actually running the completion
448 |         // generation, so we test the Args parsing logic instead
449 |         let args = Args::parse_from(["test-bin", "generate-completions", "bash"]);
450 | 
451 |         // Verify that a subcommand is present
452 |         assert!(args.command.is_some());
453 | 
454 |         // Verify that flags are still parsed correctly even with subcommands
455 |         assert!(!args.kill);
456 |         assert!(args.proto.is_none());
457 |     }
458 | 
459 |     #[test]
460 |     fn test_sort_connections() {
461 |         use crate::schemas::Connection;
462 | 
463 |         fn build_connection(
464 |             proto: &str,
465 |             local_port: &str,
466 |             remote: &str,
467 |             remote_port: &str,
468 |             program: &str,
469 |             pid: &str,
470 |             state: &str,
471 |         ) -> Connection {
472 |             Connection {
473 |                 proto: proto.to_string(),
474 |                 local_port: local_port.to_string(),
475 |                 remote_port: remote_port.to_string(),
476 |                 ipvx_raw: IpAddr::from_str(remote).unwrap(),
477 |                 program: program.to_string(),
478 |                 pid: pid.to_string(),
479 |                 state: state.to_string(),
480 |                 remote_address: remote.to_string(),
481 |                 address_type: AddressType::Extern,
482 |             }
483 |         }
484 | 
485 |         let mut connections = vec![
486 |             build_connection("TCP", "443", "9.9.9.9", "443", "nginx", "1", "ESTABLISHED"),
487 |             build_connection("UDP", "53", "8.8.8.8", "8080", "apache", "2", "CLOSE_WAIT"),
488 |             build_connection("TCP", "80", "0.0.0.0", "80", "postgres", "3", "LISTEN"),
489 |         ];
490 | 
491 |         // Maps a sort key to the expected order of the connections (represented by their PIDs) after sort
492 |         let sort_scenarios = vec![
493 |             (SortField::Pid, ["1", "2", "3"]),
494 |             (SortField::RemoteAddress, ["3", "2", "1"]),
495 |             (SortField::State, ["2", "1", "3"]),
496 |         ];
497 | 
498 |         for scenario in sort_scenarios {
499 |             sort_connections(&mut connections, scenario.0);
500 |             let result_pids: Vec<&str> = connections.iter().map(|c| c.pid.as_str()).collect();
501 |             assert_eq!(result_pids, scenario.1);
502 |         }
503 |     }
504 | }
505 | 


--------------------------------------------------------------------------------
/src/connections/common.rs:
--------------------------------------------------------------------------------
  1 | use crate::schemas::{AddressType, Connection, FilterOptions};
  2 | 
  3 | /// Checks if a connection should be filtered out based on options provided by the user.
  4 | ///
  5 | /// # Arguments
  6 | /// * `connection_details`: The connection to check for filtering.
  7 | /// * `filter_options`: The filter options provided by the user.
  8 | ///
  9 | /// # Returns
 10 | /// `true` if the connection should be filtered out, `false` if not.
 11 | pub fn filter_out_connection(
 12 |     connection_details: &Connection,
 13 |     filter_options: &FilterOptions,
 14 | ) -> bool {
 15 |     match &filter_options.by_remote_port {
 16 |         Some(filter_remote_port) if &connection_details.remote_port != filter_remote_port => {
 17 |             return true
 18 |         }
 19 |         _ => {}
 20 |     }
 21 |     match &filter_options.by_local_port {
 22 |         Some(filter_local_port) if &connection_details.local_port != filter_local_port => {
 23 |             return true
 24 |         }
 25 |         _ => {}
 26 |     }
 27 |     match &filter_options.by_remote_address {
 28 |         Some(filter_remote_address)
 29 |             if &connection_details.remote_address != filter_remote_address =>
 30 |         {
 31 |             return true
 32 |         }
 33 |         _ => {}
 34 |     }
 35 |     match &filter_options.by_program {
 36 |         Some(filter_program) if &connection_details.program != filter_program => return true,
 37 |         _ => {}
 38 |     }
 39 |     match &filter_options.by_pid {
 40 |         Some(filter_pid) if &connection_details.pid != filter_pid => return true,
 41 |         _ => {}
 42 |     }
 43 |     if filter_options.by_listen && connection_details.state != "listen" {
 44 |         return true;
 45 |     }
 46 |     if filter_options.by_open && connection_details.state == "close" {
 47 |         return true;
 48 |     }
 49 | 
 50 |     false
 51 | }
 52 | 
 53 | /// Checks if a given IP address is either "unspecified", localhost or an extern address.
 54 | ///
 55 | /// * `0.0.0.0` or `[::]` -> unspecified
 56 | /// * `127.0.0.1` or `[::1]` -> localhost
 57 | /// * else -> extern address
 58 | ///
 59 | /// # Arguments
 60 | /// * `remote_address`: The address to be checked.
 61 | ///
 62 | /// # Returns
 63 | /// The address-type as an AddressType enum.
 64 | pub fn get_address_type(remote_address: &str) -> AddressType {
 65 |     if remote_address == "127.0.0.1" || remote_address == "[::1]" || remote_address == "::1" {
 66 |         return AddressType::Localhost;
 67 |     } else if remote_address == "0.0.0.0" || remote_address == "[::]" || remote_address == "::" {
 68 |         return AddressType::Unspecified;
 69 |     }
 70 |     AddressType::Extern
 71 | }
 72 | 
 73 | #[cfg(test)]
 74 | mod tests {
 75 |     use super::*;
 76 |     use std::net::Ipv4Addr;
 77 | 
 78 |     #[test]
 79 |     fn test_get_address_type() {
 80 |         use crate::schemas::AddressType;
 81 | 
 82 |         assert_eq!(get_address_type("127.0.0.1"), AddressType::Localhost);
 83 |         assert_eq!(get_address_type("[::1]"), AddressType::Localhost);
 84 |         assert_eq!(get_address_type("0.0.0.0"), AddressType::Unspecified);
 85 |         assert_eq!(get_address_type("[::]"), AddressType::Unspecified);
 86 |         assert_eq!(get_address_type("8.8.8.8"), AddressType::Extern);
 87 |     }
 88 | 
 89 |     #[test]
 90 |     fn test_filter_out_connection_by_port() {
 91 |         use crate::schemas::{AddressType, Connection, FilterOptions};
 92 | 
 93 |         let conn = Connection {
 94 |             proto: "tcp".to_string(),
 95 |             local_port: "8080".to_string(),
 96 |             remote_port: "443".to_string(),
 97 |             remote_address: "8.8.8.8".to_string(),
 98 |             program: "nginx".to_string(),
 99 |             pid: "123".to_string(),
100 |             state: "established".to_string(),
101 |             address_type: AddressType::Extern,
102 |             ipvx_raw: Ipv4Addr::new(8, 8, 8, 8).into(),
103 |         };
104 | 
105 |         let filter_by_matching_port = FilterOptions {
106 |             by_local_port: Some("8080".to_string()),
107 |             ..Default::default()
108 |         };
109 |         assert!(!filter_out_connection(&conn, &filter_by_matching_port));
110 | 
111 |         let filter_by_non_matching_port = FilterOptions {
112 |             by_local_port: Some("8181".to_string()),
113 |             ..Default::default()
114 |         };
115 |         assert!(filter_out_connection(&conn, &filter_by_non_matching_port));
116 |     }
117 | 
118 |     #[test]
119 |     fn test_filter_out_connection_by_state() {
120 |         use crate::schemas::{AddressType, Connection, FilterOptions};
121 | 
122 |         let mut conn = Connection {
123 |             proto: "udp".to_string(),
124 |             local_port: "8080".to_string(),
125 |             remote_port: "443".to_string(),
126 |             remote_address: "8.8.8.8".to_string(),
127 |             program: "nginx".to_string(),
128 |             pid: "123".to_string(),
129 |             state: "close".to_string(),
130 |             address_type: AddressType::Extern,
131 |             ipvx_raw: Ipv4Addr::new(8, 8, 8, 8).into(),
132 |         };
133 | 
134 |         let filter_by_open_state = FilterOptions {
135 |             by_open: true,
136 |             ..Default::default()
137 |         };
138 |         assert!(filter_out_connection(&conn, &filter_by_open_state));
139 | 
140 |         let no_active_open_filter = FilterOptions {
141 |             by_open: false,
142 |             ..Default::default()
143 |         };
144 |         assert!(!filter_out_connection(&conn, &no_active_open_filter));
145 | 
146 |         conn.state = "listen".to_string();
147 | 
148 |         let filter_by_listen_state = FilterOptions {
149 |             by_listen: true,
150 |             ..Default::default()
151 |         };
152 |         assert!(!filter_out_connection(&conn, &filter_by_listen_state));
153 | 
154 |         let no_active_listen_filter = FilterOptions {
155 |             by_listen: false,
156 |             ..Default::default()
157 |         };
158 |         assert!(!filter_out_connection(&conn, &no_active_listen_filter));
159 |     }
160 | 
161 |     #[test]
162 |     fn test_filter_out_connection_by_pid_and_program() {
163 |         use crate::schemas::{AddressType, Connection, FilterOptions};
164 | 
165 |         let conn = Connection {
166 |             proto: "tcp".to_string(),
167 |             local_port: "8080".to_string(),
168 |             remote_port: "443".to_string(),
169 |             remote_address: "8.8.8.8".to_string(),
170 |             program: "nginx".to_string(),
171 |             pid: "123".to_string(),
172 |             state: "close".to_string(),
173 |             address_type: AddressType::Extern,
174 |             ipvx_raw: Ipv4Addr::new(8, 8, 8, 8).into(),
175 |         };
176 | 
177 |         let filter_by_open_state = FilterOptions {
178 |             by_pid: Some("123".to_string()),
179 |             ..Default::default()
180 |         };
181 |         assert!(!filter_out_connection(&conn, &filter_by_open_state));
182 | 
183 |         let no_active_open_filter = FilterOptions {
184 |             by_program: Some("postgres".to_string()),
185 |             ..Default::default()
186 |         };
187 |         assert!(filter_out_connection(&conn, &no_active_open_filter));
188 |     }
189 | 
190 |     #[test]
191 |     fn test_filter_out_connection_by_multiple_conditions() {
192 |         use crate::schemas::{AddressType, Connection, FilterOptions};
193 | 
194 |         let mut conn = Connection {
195 |             proto: "tcp".to_string(),
196 |             local_port: "8080".to_string(),
197 |             remote_port: "443".to_string(),
198 |             remote_address: "8.8.8.8".to_string(),
199 |             program: "python".to_string(),
200 |             pid: "123".to_string(),
201 |             state: "listen".to_string(),
202 |             address_type: AddressType::Extern,
203 |             ipvx_raw: Ipv4Addr::new(8, 8, 8, 8).into(),
204 |         };
205 | 
206 |         let filter_by_multiple_conditions = FilterOptions {
207 |             by_local_port: Some("8080".to_string()),
208 |             by_pid: Some("123".to_string()),
209 |             by_program: Some("python".to_string()),
210 |             by_listen: true,
211 |             ..Default::default()
212 |         };
213 |         assert!(!filter_out_connection(
214 |             &conn,
215 |             &filter_by_multiple_conditions
216 |         ));
217 | 
218 |         conn.state = "close".to_string();
219 |         assert!(filter_out_connection(&conn, &filter_by_multiple_conditions));
220 |     }
221 | }
222 | 


--------------------------------------------------------------------------------
/src/connections/linux.rs:
--------------------------------------------------------------------------------
  1 | use crate::connections::common::{filter_out_connection, get_address_type};
  2 | use crate::schemas::{Connection, FilterOptions};
  3 | use procfs::process::FDTarget;
  4 | use procfs::process::Stat;
  5 | use std::collections::HashMap;
  6 | use std::net::SocketAddr;
  7 | 
  8 | /// General struct type for TCP and UDP entries.
  9 | #[derive(Debug)]
 10 | pub struct NetEntry {
 11 |     pub protocol: String,
 12 |     pub local_address: SocketAddr,
 13 |     pub remote_address: SocketAddr,
 14 |     pub state: String,
 15 |     pub inode: u64,
 16 | }
 17 | 
 18 | /// Splits a string combined of an IP address and port with a ":" delimiter into two parts.
 19 | ///
 20 | /// # Arguments
 21 | /// * `address`: The combination of address and port joined by a ":", e.g. "127.0.0.1:5432"
 22 | ///
 23 | /// # Example
 24 | /// ```
 25 | /// let address_port_1 = "127.0.0.1:5432".to_string();
 26 | /// assert_eq!(split_address(address_port_1), Some(("5432", "127.0.0.1")));
 27 | ///
 28 | /// let address_port_2 = "fails.com".to_string();
 29 | /// assert_eq!(split_address(address_port_2), None);
 30 | /// ```
 31 | ///
 32 | /// # Returns
 33 | /// If the string can be successfully split,
 34 | /// it will return a tuple containing the address and the port, if not `None`.
 35 | fn split_address(address: &str) -> Option<(&str, &str)> {
 36 |     static DELIMITER: &str = ":";
 37 | 
 38 |     let mut address_parts = address.rsplitn(2, DELIMITER);
 39 |     match (address_parts.next(), address_parts.next()) {
 40 |         (Some(first), Some(second)) => Some((second, first)),
 41 |         _ => None,
 42 |     }
 43 | }
 44 | 
 45 | /// Handles the output of the `split_address` function by replacing the port with a "-" if the string couldn't be split.
 46 | /// ###### TODO: maybe combine it with the `split_address` function.
 47 | ///
 48 | /// # Arguments
 49 | /// * `address`: The address-port combination which should be split.
 50 | ///
 51 | /// # Example
 52 | /// ```
 53 | /// let address_port_1 = "127.0.0.1:5432".to_string();
 54 | /// assert_eq!(get_address_parts(address_port_1), ("5432", "127.0.0.1"));
 55 | ///
 56 | /// let address_port_2 = "fails.com".to_string();
 57 | /// assert_eq!(get_address_parts(address_port_1), ("-", "127.0.0.1"));
 58 | /// ```
 59 | ///
 60 | /// # Returns
 61 | /// A tuple containing the address and port or just the address and a "-" if there wasn't a port.
 62 | fn get_address_parts(address: &str) -> (String, String) {
 63 |     split_address(address)
 64 |         .map(|(a, p)| (a.to_string(), p.to_string()))
 65 |         .unwrap_or((address.to_string(), "-".to_string()))
 66 | }
 67 | 
 68 | /// Gets all running processes on the system using the "procfs" crate.
 69 | /// This code is taken from the "procfs" crate documentation.
 70 | ///
 71 | /// # Arguments
 72 | /// None
 73 | ///
 74 | /// # Returns
 75 | /// A map of all current processes.
 76 | fn get_processes() -> HashMap<u64, Stat> {
 77 |     let all_procs = procfs::process::all_processes().unwrap();
 78 | 
 79 |     let mut map: HashMap<u64, Stat> = HashMap::new();
 80 |     for p in all_procs {
 81 |         let process = p.unwrap();
 82 |         if let (Ok(stat), Ok(fds)) = (process.stat(), process.fd()) {
 83 |             for fd in fds {
 84 |                 if let FDTarget::Socket(inode) = fd.unwrap().target {
 85 |                     map.insert(inode, stat.clone());
 86 |                 }
 87 |             }
 88 |         }
 89 |     }
 90 |     map
 91 | }
 92 | 
 93 | fn get_connection_data(net_entry: NetEntry, all_processes: &HashMap<u64, Stat>) -> Connection {
 94 |     let local_address_full = format!("{}", net_entry.local_address);
 95 |     let (_, local_port) = get_address_parts(&local_address_full);
 96 | 
 97 |     let remote_address_full = format!("{}", net_entry.remote_address);
 98 |     let (remote_address, remote_port) = get_address_parts(&remote_address_full);
 99 |     let state = net_entry.state;
100 | 
101 |     let (program, pid) = all_processes
102 |         .get(&net_entry.inode)
103 |         .map(|stat| (stat.comm.to_string(), stat.pid.to_string()))
104 |         .unwrap_or(("-".to_string(), "-".to_string()));
105 | 
106 |     let address_type = get_address_type(&remote_address);
107 | 
108 |     let connection: Connection = Connection {
109 |         proto: net_entry.protocol,
110 |         local_port,
111 |         remote_address,
112 |         remote_port,
113 |         program,
114 |         pid,
115 |         state,
116 |         address_type,
117 |         ipvx_raw: net_entry.remote_address.ip(),
118 |     };
119 | 
120 |     connection
121 | }
122 | 
123 | /// Gets all currently open TCP connections using the "procfs" crate and processes them.
124 | ///
125 | /// # Arguments
126 | /// * `all_processes`: A map of all running processes on the system.
127 | /// * `filter_options`: The filter options provided by the user.
128 | ///
129 | /// # Returns
130 | /// All processed and filtered TCP connections as a `Connection` struct in a vector.
131 | fn get_tcp_connections(
132 |     all_processes: &HashMap<u64, Stat>,
133 |     filter_options: &FilterOptions,
134 | ) -> Vec<Connection> {
135 |     let mut tcp_entries = procfs::net::tcp().unwrap();
136 |     if !filter_options.exclude_ipv6 {
137 |         tcp_entries.extend(procfs::net::tcp6().unwrap());
138 |     }
139 | 
140 |     tcp_entries
141 |         .iter()
142 |         .filter_map(|entry| {
143 |             let tcp_entry: NetEntry = NetEntry {
144 |                 protocol: "tcp".to_string(),
145 |                 local_address: entry.local_address,
146 |                 remote_address: entry.remote_address,
147 |                 state: format!("{:?}", entry.state).to_ascii_lowercase(),
148 |                 inode: entry.inode,
149 |             };
150 |             let connection = get_connection_data(tcp_entry, all_processes);
151 | 
152 |             let filter_connection: bool = filter_out_connection(&connection, filter_options);
153 |             if !filter_connection {
154 |                 Some(connection)
155 |             } else {
156 |                 None
157 |             }
158 |         })
159 |         .collect()
160 | }
161 | 
162 | /// Gets all currently open UDP connections using the "procfs" crate and processes them.
163 | ///
164 | /// # Arguments
165 | /// * `all_processes`: A map of all running processes on the system.
166 | /// * `filter_options`: The filter options provided by the user.
167 | ///
168 | /// # Returns
169 | /// All processed and filtered UDP connections as a `Connection` struct in a vector.
170 | fn get_udp_connections(
171 |     all_processes: &HashMap<u64, Stat>,
172 |     filter_options: &FilterOptions,
173 | ) -> Vec<Connection> {
174 |     let mut udp_entries = procfs::net::udp().unwrap();
175 |     if !filter_options.exclude_ipv6 {
176 |         udp_entries.extend(procfs::net::udp6().unwrap());
177 |     }
178 | 
179 |     udp_entries
180 |         .iter()
181 |         .filter_map(|entry| {
182 |             let udp_entry: NetEntry = NetEntry {
183 |                 protocol: "udp".to_string(),
184 |                 local_address: entry.local_address,
185 |                 remote_address: entry.remote_address,
186 |                 state: format!("{:?}", entry.state).to_ascii_lowercase(),
187 |                 inode: entry.inode,
188 |             };
189 |             let connection: Connection = get_connection_data(udp_entry, all_processes);
190 | 
191 |             let filter_connection: bool = filter_out_connection(&connection, filter_options);
192 |             if !filter_connection {
193 |                 Some(connection)
194 |             } else {
195 |                 None
196 |             }
197 |         })
198 |         .collect()
199 | }
200 | 
201 | /// Gets both TCP and UDP connections and combines them based on protocol filter options.
202 | ///
203 | /// # Arguments
204 | /// * `filter_options`: The filter options provided by the user.
205 | ///
206 | /// # Returns
207 | /// All processed and filtered TCP/UDP connections as a `Connection` struct in a vector.
208 | pub fn get_connections(filter_options: &FilterOptions) -> Vec<Connection> {
209 |     let all_processes = get_processes();
210 | 
211 |     let mut connections = Vec::new();
212 |     if filter_options.by_proto.tcp {
213 |         connections.extend(get_tcp_connections(&all_processes, filter_options))
214 |     }
215 |     if filter_options.by_proto.udp {
216 |         connections.extend(get_udp_connections(&all_processes, filter_options))
217 |     }
218 | 
219 |     connections
220 | }
221 | 
222 | #[cfg(test)]
223 | mod tests {
224 |     use super::*;
225 | 
226 |     #[test]
227 |     fn test_split_address_valid() {
228 |         let addr = "127.0.0.1:5432";
229 |         assert_eq!(split_address(addr), Some(("127.0.0.1", "5432")));
230 | 
231 |         let addr = "[::1]:8080";
232 |         assert_eq!(split_address(addr), Some(("[::1]", "8080")));
233 |     }
234 | 
235 |     #[test]
236 |     fn test_split_address_invalid() {
237 |         let addr = "localhost";
238 |         assert_eq!(split_address(addr), None);
239 |         let addr = "192.168.0.1";
240 |         assert_eq!(split_address(addr), None);
241 |     }
242 | 
243 |     #[test]
244 |     fn test_get_address_parts_valid() {
245 |         let addr = "192.168.0.1:80";
246 |         let (address, port) = get_address_parts(addr);
247 |         assert_eq!(address, "192.168.0.1");
248 |         assert_eq!(port, "80");
249 |     }
250 | 
251 |     #[test]
252 |     fn test_get_address_parts_invalid() {
253 |         let addr = "example.com";
254 |         let (address, port) = get_address_parts(addr);
255 |         assert_eq!(address, "example.com");
256 |         assert_eq!(port, "-");
257 |     }
258 | }
259 | 


--------------------------------------------------------------------------------
/src/connections/macos.rs:
--------------------------------------------------------------------------------
  1 | use crate::connections::common::{filter_out_connection, get_address_type};
  2 | use crate::schemas::{Connection, FilterOptions};
  3 | use libproc::libproc::proc_pid;
  4 | use netstat2::{
  5 |     get_sockets_info, AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo as NetstatSocketInfo,
  6 |     SocketInfo,
  7 | };
  8 | use std::collections::HashSet;
  9 | 
 10 | /// Retrieves the name of a process given its PID on macOS using the libproc library.
 11 | ///
 12 | /// # Arguments
 13 | /// * `pid`: The process ID for which to obtain the process name.
 14 | ///
 15 | /// # Returns
 16 | /// A string containing the process name if found, or "-" if the name cannot be retrieved.
 17 | fn get_process_name(pid: i32) -> String {
 18 |     match proc_pid::name(pid) {
 19 |         Ok(name) => name,
 20 |         Err(_) => "-".to_string(),
 21 |     }
 22 | }
 23 | 
 24 | /// Parses and filters TCP and/or UDP connections using socket information.
 25 | ///
 26 | /// # Arguments
 27 | /// * `sockets_info`: List of socket information coming from the netstat2 crate
 28 | /// * `filter_options`: The filter options provided by the user.
 29 | ///
 30 | /// # Returns
 31 | /// All filtered TCP/UDP connections as a `Connection` struct in a vector.
 32 | fn parse_connections(
 33 |     sockets_info: &[SocketInfo],
 34 |     filter_options: &FilterOptions,
 35 | ) -> Vec<Connection> {
 36 |     let mut af_flags = AddressFamilyFlags::empty();
 37 |     if !filter_options.exclude_ipv6 {
 38 |         af_flags |= AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6;
 39 |     } else {
 40 |         af_flags |= AddressFamilyFlags::IPV4;
 41 |     }
 42 | 
 43 |     let mut proto_flags = ProtocolFlags::empty();
 44 |     if filter_options.by_proto.tcp {
 45 |         proto_flags |= ProtocolFlags::TCP;
 46 |     }
 47 |     if filter_options.by_proto.udp {
 48 |         proto_flags |= ProtocolFlags::UDP;
 49 |     }
 50 | 
 51 |     // Temporary storage for connections, for deduplication
 52 |     let mut seen_connections = HashSet::new();
 53 | 
 54 |     // Convert the socket information to our Connection type
 55 |     sockets_info
 56 |         .iter()
 57 |         .filter_map(|si| {
 58 |             let (proto, local_port, remote_address, remote_port, state) =
 59 |                 match &si.protocol_socket_info {
 60 |                     NetstatSocketInfo::Tcp(tcp_si) => {
 61 |                         let state = format!("{}", tcp_si.state).to_ascii_lowercase();
 62 |                         (
 63 |                             "tcp".to_string(),
 64 |                             tcp_si.local_port.to_string(),
 65 |                             tcp_si.remote_addr.to_string(),
 66 |                             tcp_si.remote_port.to_string(),
 67 |                             state,
 68 |                         )
 69 |                     }
 70 |                     NetstatSocketInfo::Udp(udp_si) => (
 71 |                         "udp".to_string(),
 72 |                         udp_si.local_port.to_string(),
 73 |                         "0.0.0.0".to_string(),
 74 |                         "-".to_string(),
 75 |                         "-".to_string(),
 76 |                     ),
 77 |                 };
 78 | 
 79 |             let (program, pid) = if let Some(first_pid) = si.associated_pids.first() {
 80 |                 let proc_name = get_process_name(*first_pid as i32);
 81 |                 (proc_name, first_pid.to_string())
 82 |             } else {
 83 |                 ("-".to_string(), "-".to_string())
 84 |             };
 85 | 
 86 |             // Create a unique key for deduplication
 87 |             let connection_key =
 88 |                 format!("{proto}:{local_port}:{remote_address}:{remote_port}:{state}:{pid}");
 89 | 
 90 |             // If the connection has already been processed, skip it
 91 |             if !seen_connections.insert(connection_key) {
 92 |                 return None;
 93 |             }
 94 | 
 95 |             let conn = Connection {
 96 |                 proto,
 97 |                 local_port,
 98 |                 remote_address: remote_address.clone(),
 99 |                 remote_port,
100 |                 program,
101 |                 pid,
102 |                 state,
103 |                 address_type: get_address_type(&remote_address),
104 |                 ipvx_raw: si.local_addr(),
105 |             };
106 | 
107 |             if filter_out_connection(&conn, filter_options) {
108 |                 None
109 |             } else {
110 |                 Some(conn)
111 |             }
112 |         })
113 |         .collect()
114 | }
115 | 
116 | /// Gets and filters TCP and/or UDP connections using socket information from the netstat2 crate.
117 | ///
118 | /// # Arguments
119 | /// * `filter_options`: The filter options provided by the user.
120 | ///
121 | /// # Returns
122 | /// All processed and filtered TCP/UDP connections as a `Connection` struct in a vector.
123 | pub fn get_connections(filter_options: &FilterOptions) -> Vec<Connection> {
124 |     let mut af_flags = AddressFamilyFlags::empty();
125 |     if !filter_options.exclude_ipv6 {
126 |         af_flags |= AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6;
127 |     } else {
128 |         af_flags |= AddressFamilyFlags::IPV4;
129 |     }
130 | 
131 |     let mut proto_flags = ProtocolFlags::empty();
132 |     if filter_options.by_proto.tcp {
133 |         proto_flags |= ProtocolFlags::TCP;
134 |     }
135 |     if filter_options.by_proto.udp {
136 |         proto_flags |= ProtocolFlags::UDP;
137 |     }
138 | 
139 |     let sockets_info = match get_sockets_info(af_flags, proto_flags) {
140 |         Ok(sockets) => sockets,
141 |         Err(_) => return Vec::new(),
142 |     };
143 | 
144 |     parse_connections(&sockets_info, filter_options)
145 | }
146 | 
147 | #[cfg(test)]
148 | mod tests {
149 |     use super::*;
150 |     use crate::schemas::Protocols;
151 |     use netstat2::{ProtocolSocketInfo, SocketInfo, TcpSocketInfo, TcpState};
152 |     use std::net::{IpAddr, Ipv4Addr};
153 | 
154 |     #[test]
155 |     fn test_parse_connections_tcp() {
156 |         let mock_socket = SocketInfo {
157 |             protocol_socket_info: ProtocolSocketInfo::Tcp(TcpSocketInfo {
158 |                 local_port: 8080,
159 |                 remote_port: 443,
160 |                 local_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
161 |                 remote_addr: IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34)),
162 |                 state: TcpState::Established,
163 |             }),
164 |             associated_pids: vec![1234],
165 |         };
166 | 
167 |         let filter_options = FilterOptions {
168 |             exclude_ipv6: false,
169 |             by_proto: Protocols {
170 |                 tcp: true,
171 |                 udp: false,
172 |             },
173 |             ..Default::default()
174 |         };
175 | 
176 |         let connections = parse_connections(&vec![mock_socket], &filter_options);
177 | 
178 |         assert_eq!(connections.len(), 1);
179 |         let conn = &connections[0];
180 |         assert_eq!(conn.proto, "tcp");
181 |         assert_eq!(conn.local_port, "8080");
182 |         assert_eq!(conn.remote_port, "443");
183 |         assert_eq!(conn.remote_address, "93.184.216.34");
184 |         assert_eq!(conn.state, "established");
185 |         assert_eq!(conn.pid, "1234");
186 |     }
187 | 
188 |     #[test]
189 |     fn test_parse_connections_udp() {
190 |         let mock_socket = SocketInfo {
191 |             protocol_socket_info: ProtocolSocketInfo::Udp(netstat2::UdpSocketInfo {
192 |                 local_port: 53,
193 |                 local_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
194 |             }),
195 |             associated_pids: vec![5678],
196 |         };
197 | 
198 |         let filter_options = FilterOptions {
199 |             by_proto: Protocols {
200 |                 tcp: false,
201 |                 udp: true,
202 |             },
203 |             ..Default::default()
204 |         };
205 | 
206 |         let connections = parse_connections(&vec![mock_socket], &filter_options);
207 | 
208 |         assert_eq!(connections.len(), 1);
209 |         let conn = &connections[0];
210 |         assert_eq!(conn.proto, "udp");
211 |         assert_eq!(conn.local_port, "53");
212 |         assert_eq!(conn.remote_port, "-");
213 |         assert_eq!(conn.remote_address, "0.0.0.0");
214 |         assert_eq!(conn.state, "-");
215 |         assert_eq!(conn.pid, "5678");
216 |     }
217 | }
218 | 


--------------------------------------------------------------------------------
/src/connections/mod.rs:
--------------------------------------------------------------------------------
 1 | pub mod common;
 2 | 
 3 | #[cfg(target_os = "linux")]
 4 | mod linux;
 5 | #[cfg(target_os = "macos")]
 6 | mod macos;
 7 | 
 8 | use crate::schemas::Connection;
 9 | use crate::schemas::FilterOptions;
10 | 
11 | /// Retrieves all TCP/UDP network connections based on the current operating system (Linux or macOS).
12 | ///
13 | /// # Arguments
14 | /// * `filter_options`: The filter options provided by the user.
15 | ///
16 | /// # Returns
17 | /// All processed and filtered TCP/UDP connections as a `Connection` struct in a vector.
18 | pub fn get_all_connections(filter_options: &FilterOptions) -> Vec<Connection> {
19 |     #[cfg(target_os = "linux")]
20 |     {
21 |         linux::get_connections(filter_options)
22 |     }
23 | 
24 |     #[cfg(target_os = "macos")]
25 |     {
26 |         macos::get_connections(filter_options)
27 |     }
28 | }
29 | 


--------------------------------------------------------------------------------
/src/macros.rs:
--------------------------------------------------------------------------------
 1 | /// Pipe-safe stdout printing -- alternative to print!.
 2 | /// This macro exists because there is no way to handle pipes ending abruptly with print!
 3 | /// For more information, read:
 4 | /// - https://users.rust-lang.org/t/why-does-the-pipe-cause-the-panic-in-the-standard-library/107222/4
 5 | /// - rust-lang/rust#97889
 6 | ///
 7 | /// # Usage
 8 | /// Same arguments & expected behaviour as print!, with graceful handling of (expected) IO errors.
 9 | #[macro_export]
10 | macro_rules! sout {
11 |     ($($arg:tt)*) => {{
12 |         use std::io::Write;
13 | 
14 |         match write!(std::io::stdout(), $($arg)*) {
15 |             Ok(_) => (),
16 |             Err(broken_pipe) if broken_pipe.kind() == std::io::ErrorKind::BrokenPipe => (),
17 |             Err(err) => panic!("Unknown error occurred while writing to stdout {:?}", err.kind()),
18 |         }
19 |     }};
20 | }
21 | 
22 | /// Pipe-safe stdout printing -- alternative to println!.
23 | /// This macro exists because there is no way to handle pipes ending abruptly with println!
24 | /// For more information, read:
25 | /// - https://users.rust-lang.org/t/why-does-the-pipe-cause-the-panic-in-the-standard-library/107222/4
26 | /// - rust-lang/rust#97889
27 | ///
28 | /// # Usage
29 | /// Same arguments & expected behaviour as println!, with graceful handling of (expected) IO errors.
30 | #[macro_export]
31 | macro_rules! soutln {
32 |     () => {{
33 |         $crate::sout!("\n");
34 |     }};
35 |     ($($arg:tt)*) => {{
36 |         $crate::sout!("{}\n", format!($($arg)*));
37 |     }};
38 | }
39 | 


--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
 1 | mod cli;
 2 | mod connections;
 3 | mod macros;
 4 | mod schemas;
 5 | mod table;
 6 | mod utils;
 7 | 
 8 | use clap::CommandFactory;
 9 | use cli::{print_completions, Args, CliCommand, Commands};
10 | use schemas::{Connection, FilterOptions};
11 | 
12 | use crate::cli::sort_connections;
13 | 
14 | fn main() {
15 |     let args = match cli::cli() {
16 |         CliCommand::Subcommand(Commands::GenerateCompletions { shell }) => {
17 |             let mut cmd = Args::command();
18 |             print_completions(shell, &mut cmd);
19 |             return;
20 |         }
21 |         CliCommand::Run(flags) => flags,
22 |     };
23 | 
24 |     let filter_options: FilterOptions = FilterOptions {
25 |         by_proto: cli::resolve_protocols(&args),
26 |         by_remote_address: args.ip,
27 |         by_remote_port: args.remote_port,
28 |         by_local_port: args.port,
29 |         by_program: args.program,
30 |         by_pid: args.pid,
31 |         by_open: args.open,
32 |         by_listen: args.listen,
33 |         exclude_ipv6: args.exclude_ipv6,
34 |     };
35 | 
36 |     let mut all_connections: Vec<Connection> = connections::get_all_connections(&filter_options);
37 | 
38 |     if let Some(sort) = args.sort {
39 |         sort_connections(&mut all_connections, sort);
40 |     }
41 | 
42 |     if args.reverse {
43 |         all_connections.reverse();
44 |     }
45 | 
46 |     if args.json {
47 |         let result = table::get_connections_json(&all_connections);
48 |         soutln!("{}", result);
49 |     } else if args.format.is_some() {
50 |         let result = table::get_connections_formatted(&all_connections, &args.format.unwrap());
51 |         soutln!("{}", result);
52 |     } else {
53 |         table::print_connections_table(&all_connections, args.compact);
54 |     }
55 | 
56 |     if args.kill {
57 |         cli::interactive_process_kill(&all_connections);
58 |     }
59 | }
60 | 


--------------------------------------------------------------------------------
/src/schemas.rs:
--------------------------------------------------------------------------------
 1 | use std::net::IpAddr;
 2 | 
 3 | /// Represents the type of an IP address.
 4 | ///
 5 | /// # Variants
 6 | /// * `Localhost`: Represents the localhost/127.0.0.1 address.
 7 | /// * `Unspecified`: Represents an unspecified or wildcard address.
 8 | /// * `Extern`: Represents an external address.
 9 | #[derive(Debug, PartialEq, serde::Serialize)]
10 | pub enum AddressType {
11 |     Localhost,
12 |     Unspecified,
13 |     Extern,
14 | }
15 | 
16 | /// Contains which type(s) of protocols the user wants to see.
17 | #[derive(Debug, Default)]
18 | pub struct Protocols {
19 |     pub tcp: bool,
20 |     pub udp: bool,
21 | }
22 | 
23 | /// Represents a processed socket connection with all its attributes.
24 | #[derive(Debug, serde::Serialize)]
25 | #[serde(rename_all = "snake_case")]
26 | pub struct Connection {
27 |     pub proto: String,
28 |     pub local_port: String,
29 |     pub remote_address: String,
30 |     pub remote_port: String,
31 |     pub program: String,
32 |     pub pid: String,
33 |     pub state: String,
34 |     pub address_type: AddressType,
35 | 
36 |     /// Internal variable used only for ordering operations of raw ipv4/6 addresses
37 |     #[serde(skip_serializing)]
38 |     pub ipvx_raw: IpAddr,
39 | }
40 | 
41 | /// Contains options for filtering a `Connection`.
42 | #[derive(Debug, Default)]
43 | pub struct FilterOptions {
44 |     pub by_proto: Protocols,
45 |     pub by_program: Option<String>,
46 |     pub by_pid: Option<String>,
47 |     pub by_remote_address: Option<String>,
48 |     pub by_remote_port: Option<String>,
49 |     pub by_local_port: Option<String>,
50 |     pub by_open: bool,
51 |     pub by_listen: bool,
52 |     pub exclude_ipv6: bool,
53 | }
54 | 
55 | #[derive(Debug, Clone, Copy, PartialEq, Eq)]
56 | pub enum Protocol {
57 |     Tcp,
58 |     Udp,
59 | }
60 | 
61 | impl std::str::FromStr for Protocol {
62 |     type Err = ();
63 | 
64 |     fn from_str(input: &str) -> Result<Self, Self::Err> {
65 |         match input.to_lowercase().as_str() {
66 |             "tcp" => Ok(Protocol::Tcp),
67 |             "udp" => Ok(Protocol::Udp),
68 |             _ => Err(()),
69 |         }
70 |     }
71 | }
72 | 


--------------------------------------------------------------------------------
/src/table.rs:
--------------------------------------------------------------------------------
  1 | use handlebars::{Handlebars, RenderErrorReason};
  2 | use termimad::crossterm::style::{Attribute::*, Color::*};
  3 | use termimad::*;
  4 | 
  5 | use crate::schemas::{AddressType, Connection};
  6 | use crate::utils::pretty_print_syntax_error;
  7 | use crate::{soutln, utils};
  8 | 
  9 | /// Uses the termimad crate to create a custom appearance for Markdown text in the console.
 10 | ///
 11 | /// # Appearance
 12 | /// * **bold** text -> bold and cyan
 13 | /// * *italic* text -> italic and light gray
 14 | /// * ~~strikeout~~ text -> not struck out, red and blinking
 15 | /// * `inline code` text -> not code formatted, yellow
 16 | ///
 17 | /// # Arguments
 18 | /// None
 19 | ///
 20 | /// # Returns
 21 | /// A custom markdown "skin".
 22 | fn create_table_style(use_compact_mode: bool) -> MadSkin {
 23 |     let mut skin = MadSkin::default();
 24 |     skin.bold.set_fg(Cyan);
 25 |     skin.italic.set_fg(gray(11));
 26 |     skin.strikeout = CompoundStyle::new(Some(Red), None, RapidBlink.into());
 27 |     skin.paragraph.align = Alignment::Left;
 28 |     skin.table.align = if use_compact_mode {
 29 |         Alignment::Left
 30 |     } else {
 31 |         Alignment::Center
 32 |     };
 33 |     skin.inline_code = CompoundStyle::new(Some(Yellow), None, Encircled.into());
 34 | 
 35 |     skin
 36 | }
 37 | 
 38 | /// Marks localhost and unspecified IP addresses (i.e., 0.0.0.0) using Markdown formatting
 39 | ///
 40 | /// * `address_type` == Localhost -> *italic* + "localhost"
 41 | /// * `address_type` == Unspecified -> *italic*
 42 | /// * `address_type` == Extern -> not formatted
 43 | ///
 44 | /// # Arguments
 45 | /// * `remote_address`: The remote address.
 46 | /// * `address_type`: The address type as an AddressType enum.
 47 | ///
 48 | /// # Example
 49 | /// ```
 50 | /// let address = "127.0.0.1".to_string();
 51 | /// let address_type = AddressType::Localhost;
 52 | /// let formatted = format_known_address(&address, &address_type);
 53 | /// assert_eq!(formatted, "*127.0.0.1 localhost*");
 54 | /// ```
 55 | ///
 56 | /// # Returns
 57 | /// A Markdown formatted string based on the address-type.
 58 | fn format_known_address(remote_address: &String, address_type: &AddressType) -> String {
 59 |     match address_type {
 60 |         AddressType::Unspecified => {
 61 |             format!("*{remote_address}*")
 62 |         }
 63 |         AddressType::Localhost => {
 64 |             format!("*{remote_address} localhost*")
 65 |         }
 66 |         AddressType::Extern => remote_address.to_string(),
 67 |     }
 68 | }
 69 | 
 70 | /// Creates a Markdown table row with just empty characters with the width of the terminal window.
 71 | ///
 72 | /// # Argument
 73 | /// * `terminal_width`: The current width of the terminal.
 74 | /// * `max_column_spaces`: An array in which the values represent the max-width of each of the 7 Markdown table rows.
 75 | ///
 76 | /// # Returns
 77 | /// A Markdown table row string in which each column is filled with as many empty characters needed to fit in content and as well fill out the terminal width.
 78 | fn fill_terminal_width(terminal_width: u16, max_column_spaces: [u16; 7]) -> String {
 79 |     let total_column_spaces: u16 = max_column_spaces.iter().sum();
 80 | 
 81 |     let calculate_column_width = |column_space: u16| {
 82 |         (column_space as f64 / total_column_spaces as f64) * (terminal_width as f64)
 83 |     };
 84 |     let empty_character = "\u{2800}";
 85 | 
 86 |     let mut row: String = String::new();
 87 |     for &max_column_space in &max_column_spaces {
 88 |         row.push_str(&format!(
 89 |             "| {} ",
 90 |             empty_character.repeat(calculate_column_width(max_column_space) as usize)
 91 |         ));
 92 |     }
 93 |     row.push_str("|\n");
 94 | 
 95 |     row
 96 | }
 97 | 
 98 | /// Prints all current connections in a pretty Markdown table.
 99 | ///
100 | /// # Arguments
101 | /// * `all_connections`: A list containing all current connections as a `Connection` struct.
102 | ///
103 | /// # Returns
104 | /// None
105 | pub fn print_connections_table(all_connections: &[Connection], use_compact_mode: bool) {
106 |     let skin: MadSkin = create_table_style(use_compact_mode);
107 |     let (terminal_width, _) = terminal_size();
108 | 
109 |     // Add table headers
110 |     static CENTER_MARKDOWN_ROW: &str = "| :-: | :-: | :-: | :-: | :-: | :-: | :-: |\n";
111 |     let mut markdown = CENTER_MARKDOWN_ROW.to_string();
112 |     markdown.push_str("| **#** | **proto** | **local port** | **remote address** | **remote port** | **pid** *program* | **state** |\n");
113 |     markdown.push_str(CENTER_MARKDOWN_ROW);
114 | 
115 |     for (idx, connection) in all_connections.iter().enumerate() {
116 |         let formatted_remote_address: String =
117 |             format_known_address(&connection.remote_address, &connection.address_type);
118 | 
119 |         markdown.push_str(&format!(
120 |             "| *{}* | {} | {} | {} | {} | {} *{}* | {} |\n",
121 |             idx + 1,
122 |             connection.proto,
123 |             connection.local_port,
124 |             &formatted_remote_address,
125 |             connection.remote_port,
126 |             connection.pid,
127 |             connection.program,
128 |             connection.state
129 |         ));
130 | 
131 |         if !use_compact_mode && idx < all_connections.len() - 1 {
132 |             markdown.push_str(CENTER_MARKDOWN_ROW);
133 |         }
134 |     }
135 | 
136 |     if !use_compact_mode {
137 |         // Create an empty row that forces the table to fit the terminal with respect to how much space ...
138 |         // ... each column should receive based on the max length of each column (in the array below)
139 |         let max_column_spaces: [u16; 7] = [5, 8, 8, 28, 7, 24, 13];
140 |         let terminal_filling_row: String = fill_terminal_width(terminal_width, max_column_spaces);
141 |         markdown.push_str(&terminal_filling_row);
142 |     }
143 | 
144 |     markdown.push_str(CENTER_MARKDOWN_ROW);
145 | 
146 |     soutln!("{}", skin.term_text(&markdown));
147 |     utils::pretty_print_info(&format!("{} Connections", all_connections.len()))
148 | }
149 | 
150 | /// Prints all current connections in a json format.
151 | ///
152 | /// # Arguments
153 | /// * `all_connections`: A list containing all current connections as a `Connection` struct.
154 | ///
155 | /// # Returns
156 | /// None
157 | pub fn get_connections_json(all_connections: &Vec<Connection>) -> String {
158 |     serde_json::to_string_pretty(all_connections).unwrap()
159 | }
160 | 
161 | /// Prints all current connections in a custom format.
162 | ///
163 | /// # Arguments
164 | /// * `all_connections`: A list containing all current connections as a `Connection` struct.
165 | /// * `template_string`: A string template format for an output
166 | ///
167 | /// # Returns
168 | /// None
169 | pub fn get_connections_formatted(
170 |     all_connections: &Vec<Connection>,
171 |     template_string: &String,
172 | ) -> String {
173 |     let mut registry = Handlebars::new();
174 |     registry.set_strict_mode(true);
175 | 
176 |     if let Err(err) = registry.register_template_string("connection_template", template_string) {
177 |         let (line_no, column_no) = err.pos().unwrap_or((1, 1));
178 | 
179 |         pretty_print_syntax_error(
180 |             "Invalid template syntax.",
181 |             template_string,
182 |             line_no,
183 |             column_no,
184 |         );
185 |         std::process::exit(2);
186 |     }
187 | 
188 |     let mut rendered_lines = Vec::new();
189 | 
190 |     for connection in all_connections {
191 |         let json_value = serde_json::to_value(connection).unwrap();
192 |         let rendered_line = registry.render("connection_template", &json_value);
193 | 
194 |         if let Err(err) = rendered_line {
195 |             let (line_no, column_no) = (err.line_no.unwrap_or(1), err.column_no.unwrap_or(1));
196 | 
197 |             match err.reason() {
198 |                 RenderErrorReason::MissingVariable(Some(var_name)) => {
199 |                     pretty_print_syntax_error(
200 |                         &format!("Invalid template variable '{var_name}'."),
201 |                         template_string,
202 |                         line_no,
203 |                         column_no,
204 |                     );
205 |                 }
206 |                 _ => {
207 |                     pretty_print_syntax_error(
208 |                         &format!("Template error - {}", err.reason()),
209 |                         template_string,
210 |                         line_no,
211 |                         column_no,
212 |                     );
213 |                 }
214 |             }
215 |             std::process::exit(2);
216 |         }
217 | 
218 |         rendered_lines.push(rendered_line.unwrap());
219 |     }
220 | 
221 |     rendered_lines.join("\n")
222 | }
223 | 
224 | #[cfg(test)]
225 | mod tests {
226 |     use std::net::{Ipv4Addr, Ipv6Addr};
227 | 
228 |     use super::*;
229 | 
230 |     #[test]
231 |     fn test_format_known_address_localhost() {
232 |         let addr = "127.0.0.1".to_string();
233 |         let result = format_known_address(&addr, &AddressType::Localhost);
234 |         assert_eq!(result, "*127.0.0.1 localhost*");
235 |     }
236 | 
237 |     #[test]
238 |     fn test_format_known_address_unspecified() {
239 |         let addr = "0.0.0.0".to_string();
240 |         let result = format_known_address(&addr, &AddressType::Unspecified);
241 |         assert_eq!(result, "*0.0.0.0*");
242 |     }
243 | 
244 |     #[test]
245 |     fn test_format_known_address_extern() {
246 |         let addr = "123.123.123".to_string();
247 |         let result = format_known_address(&addr, &AddressType::Extern);
248 |         assert_eq!(result, "123.123.123");
249 |     }
250 | 
251 |     #[test]
252 |     fn test_fill_terminal_width() {
253 |         let row = fill_terminal_width(80, [5, 8, 8, 28, 7, 24, 13]);
254 |         let columns = row.matches('|').count();
255 |         assert_eq!(columns, 8); // 7 columns + final pipe
256 |     }
257 | 
258 |     #[test]
259 |     fn test_table_style_alignment() {
260 |         let compact_skin = create_table_style(true);
261 |         assert_eq!(compact_skin.table.align, Alignment::Left);
262 | 
263 |         let non_compact_skin = create_table_style(false);
264 |         assert_eq!(non_compact_skin.table.align, Alignment::Center);
265 |     }
266 | 
267 |     #[test]
268 |     fn test_get_connections_formatted() {
269 |         let connections = vec![
270 |             Connection {
271 |                 proto: "tcp".to_string(),
272 |                 local_port: "44796".to_string(),
273 |                 remote_address: "192.168.1.0".to_string(),
274 |                 remote_port: "443".to_string(),
275 |                 program: "firefox".to_string(),
276 |                 pid: "200".to_string(),
277 |                 state: "established".to_string(),
278 |                 address_type: AddressType::Localhost,
279 |                 ipvx_raw: Ipv4Addr::new(192, 168, 1, 0).into(),
280 |             },
281 |             Connection {
282 |                 proto: "tcp".to_string(),
283 |                 local_port: "33263".to_string(),
284 |                 remote_address: "[::ffff:65.9.95.5]".to_string(),
285 |                 remote_port: "443".to_string(),
286 |                 program: "-".to_string(),
287 |                 pid: "-".to_string(),
288 |                 state: "timewait".to_string(),
289 |                 address_type: AddressType::Extern,
290 |                 ipvx_raw: Ipv6Addr::new(0, 0, 0, 0xffff, 65, 9, 95, 5).into(),
291 |             },
292 |         ];
293 | 
294 |         let template_and_expected_result = [
295 |             ("PID: {{pid}}, Protocol: {{proto}}, Remote Address: {{remote_address}}".to_string(),
296 |              "PID: 200, Protocol: tcp, Remote Address: 192.168.1.0\nPID: -, Protocol: tcp, Remote Address: [::ffff:65.9.95.5]".to_string()),
297 |             ("Protocol: {{proto}}, Local Port: {{local_port}}, Remote Address: {{remote_address}}, Remote Port: {{remote_port}}, Program: {{program}}, PID: {{pid}}, State: {{state}}, Address Type: {{address_type}}".to_string(),
298 |              "Protocol: tcp, Local Port: 44796, Remote Address: 192.168.1.0, Remote Port: 443, Program: firefox, PID: 200, State: established, Address Type: Localhost\nProtocol: tcp, Local Port: 33263, Remote Address: [::ffff:65.9.95.5], Remote Port: 443, Program: -, PID: -, State: timewait, Address Type: Extern".to_string()),
299 |         ];
300 | 
301 |         for (template, expected_result) in &template_and_expected_result {
302 |             let result = get_connections_formatted(&connections, template);
303 | 
304 |             assert_eq!(result.as_str(), expected_result.as_str());
305 |         }
306 |     }
307 | }
308 | 


--------------------------------------------------------------------------------
/src/utils.rs:
--------------------------------------------------------------------------------
 1 | use crate::soutln;
 2 | 
 3 | /// Wraps the input text in ANSI escape codes to print it in red.
 4 | fn red_text(text: &str) -> String {
 5 |     format!("\x1b[1;31m{text}\x1b[0m")
 6 | }
 7 | 
 8 | /// Wraps the input text in ANSI escape codes to print it in cyan.
 9 | fn cyan_text(text: &str) -> String {
10 |     format!("\x1B[36m{text}\x1B[0m")
11 | }
12 | 
13 | /// Wraps the input text in ANSI escape codes to print it in bold.
14 | fn bold_text(text: &str) -> String {
15 |     format!("\x1B[1m{text}\x1B[0m")
16 | }
17 | 
18 | /// Prints out formatted text starting with a cyan "Info:" prefix.
19 | ///
20 | /// # Arguments
21 | /// * `text`: The text to print to the console.
22 | ///
23 | /// # Returns
24 | /// None
25 | pub fn pretty_print_info(text: &str) {
26 |     soutln!(
27 |         "{}",
28 |         bold_text(&format!("{} {}", cyan_text("Info:"), bold_text(text)))
29 |     );
30 | }
31 | 
32 | /// Prints out formatted text starting with a red "Error:" prefix.
33 | ///
34 | /// # Arguments
35 | /// * `text`: The text to print to the console.
36 | ///
37 | /// # Returns
38 | /// None
39 | pub fn pretty_print_error(text: &str) {
40 |     soutln!(
41 |         "{}",
42 |         bold_text(&format!("{} {}", red_text("Error:"), bold_text(text)))
43 |     );
44 | }
45 | 
46 | /// Prints a syntax error message with an error preamble, the error line, and
47 | /// a caret pointing to the error column.
48 | ///
49 | /// # Arguments
50 | /// * `preamble`: The error message or description to print.
51 | /// * `text`: The full source text containing the error.
52 | /// * `line`: The line number (starting at 1) where the error occurred.
53 | /// * `column`: The column number (starting at 1) where the error occurred.
54 | ///
55 | /// # Returns
56 | /// None
57 | ///
58 | /// # Example
59 | /// ```
60 | /// let code = "let x = 5;\nlet y = 1o;";
61 | /// pretty_print_syntax_error("Unexpected token", code, 2, 9);
62 | /// ```
63 | pub fn pretty_print_syntax_error(preamble: &str, text: &str, line: usize, column: usize) {
64 |     let erronous_line: &str = text.lines().nth(line - 1).unwrap_or(text);
65 |     let line_pointer = "└─>";
66 | 
67 |     pretty_print_error(preamble);
68 |     soutln!("  {}", red_text("│"));
69 |     soutln!("  {}  {}", red_text(line_pointer), erronous_line);
70 |     soutln!(
71 |         "  {}  {}",
72 |         " ".repeat(line_pointer.chars().count() + column - 1),
73 |         red_text("^")
74 |     );
75 | }
76 | 


--------------------------------------------------------------------------------