├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── other.md └── workflows │ ├── create-release.yml │ ├── publish-binaries.yml │ └── rust-ci.yml ├── .gitignore ├── .rustfmt.toml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── screenshot.png └── src ├── app.rs ├── args.rs ├── draw.rs ├── main.rs ├── update.rs └── widgets ├── cpu_and_mem.rs ├── help_menu.rs ├── misc_info.rs ├── mod.rs └── proc.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | insert_final_newline = true 7 | indent_style = tab 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.{js,jsx,ts,tsx,html,php,vim,toml,yml,json}] 12 | indent_size = 2 13 | 14 | [*.yml] 15 | indent_style = space 16 | 17 | [Makefile,*.go] 18 | indent_style = tab 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Template to report bugs. 4 | --- 5 | 6 | 11 | 12 | Required information: 13 | 14 | - neotop version (`neotop -V`): 15 | - The output of `uname -a`: 16 | 17 | Include any of the following information if relevant: 18 | 19 | - Terminal emulator (e.g. iTerm or gnome terminal): 20 | - tmux version (`tmux -V`): 21 | - Any relevenat hardware info: 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: No template. 4 | --- 5 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Create Release 13 | uses: actions/create-release@v1.0.0 14 | env: 15 | # https://github.community/t5/GitHub-Actions/Github-Action-trigger-on-release-not-working-if-releases-was/td-p/34559 16 | GITHUB_TOKEN: ${{ secrets.PAT }} 17 | with: 18 | tag_name: ${{ github.ref }} 19 | release_name: ${{ github.ref }} 20 | draft: false 21 | prerelease: false 22 | -------------------------------------------------------------------------------- /.github/workflows/publish-binaries.yml: -------------------------------------------------------------------------------- 1 | name: Publish Binaries 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | publish: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: 15 | - ubuntu-latest 16 | - macOS-latest 17 | target: 18 | - x86_64-unknown-linux-gnu 19 | - x86_64-apple-darwin 20 | exclude: 21 | - os: macOS-latest 22 | target: x86_64-unknown-linux-gnu 23 | - os: ubuntu-latest 24 | target: x86_64-apple-darwin 25 | steps: 26 | - name: Checkout sources 27 | uses: actions/checkout@v1 28 | 29 | - name: Install stable toolchain 30 | uses: actions-rs/toolchain@v1 31 | with: 32 | toolchain: stable 33 | target: ${{ matrix.target }} 34 | override: true 35 | 36 | - name: Build release binary 37 | uses: actions-rs/cargo@v1 38 | with: 39 | use-cross: true 40 | command: build 41 | args: --release --target=${{ matrix.target }} 42 | 43 | - name: Package release 44 | run: tar -czf neotop-${{ github.event.release.tag_name }}-${{ matrix.target }}.tar.gz -C ./target/${{ matrix.target }}/release/ neotop 45 | 46 | - name: Upload release asset 47 | uses: actions/upload-release-asset@v1.0.1 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | with: 51 | upload_url: ${{ github.event.release.upload_url }} 52 | asset_path: neotop-${{ github.event.release.tag_name }}-${{ matrix.target }}.tar.gz 53 | asset_name: neotop-${{ github.event.release.tag_name }}-${{ matrix.target }}.tar.gz 54 | asset_content_type: application/zip 55 | -------------------------------------------------------------------------------- /.github/workflows/rust-ci.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md 2 | 3 | name: Rust CI 4 | 5 | on: [push, pull_request] 6 | 7 | jobs: 8 | check: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: 13 | - ubuntu-latest 14 | - macOS-latest 15 | steps: 16 | - name: Checkout sources 17 | uses: actions/checkout@v1 18 | 19 | - name: Install stable toolchain 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | profile: minimal 23 | toolchain: stable 24 | override: true 25 | 26 | - name: Run cargo check 27 | uses: actions-rs/cargo@v1 28 | with: 29 | command: check 30 | 31 | test: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout sources 35 | uses: actions/checkout@v1 36 | 37 | - name: Install stable toolchain 38 | uses: actions-rs/toolchain@v1 39 | with: 40 | profile: minimal 41 | toolchain: stable 42 | override: true 43 | 44 | - name: Run cargo test 45 | uses: actions-rs/cargo@v1 46 | with: 47 | command: test 48 | 49 | format: 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout sources 53 | uses: actions/checkout@v1 54 | 55 | - name: Install stable toolchain 56 | uses: actions-rs/toolchain@v1 57 | with: 58 | profile: minimal 59 | toolchain: stable 60 | override: true 61 | components: rustfmt 62 | 63 | - name: Run cargo fmt 64 | uses: actions-rs/cargo@v1 65 | with: 66 | command: fmt 67 | args: --all -- --check 68 | 69 | lint: 70 | runs-on: ubuntu-latest 71 | steps: 72 | - name: Checkout sources 73 | uses: actions/checkout@v1 74 | 75 | - name: Install stable toolchain 76 | uses: actions-rs/toolchain@v1 77 | with: 78 | profile: minimal 79 | toolchain: stable 80 | override: true 81 | components: clippy 82 | 83 | - name: Run cargo clippy 84 | uses: actions-rs/cargo@v1 85 | with: 86 | command: clippy 87 | args: -- -D warnings 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | > **Types of changes**: 8 | > 9 | > - **Added**: for new features. 10 | > - **Changed**: for changes in existing functionality. 11 | > - **Deprecated**: for soon-to-be removed features. 12 | > - **Removed**: for now removed features. 13 | > - **Fixed**: for any bug fixes. 14 | > - **Security**: in case of vulnerabilities. 15 | 16 | ## [Unreleased] 17 | 18 | [Unreleased]: https://github.com/cjbassi/neotop/compare/TODO...HEAD 19 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "ansi_term" 5 | version = "0.11.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 8 | dependencies = [ 9 | "winapi", 10 | ] 11 | 12 | [[package]] 13 | name = "anyhow" 14 | version = "1.0.34" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7" 17 | 18 | [[package]] 19 | name = "arc-swap" 20 | version = "0.4.6" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "b585a98a234c46fc563103e9278c9391fde1f4e6850334da895d27edb9580f62" 23 | 24 | [[package]] 25 | name = "arrayref" 26 | version = "0.3.6" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" 29 | 30 | [[package]] 31 | name = "arrayvec" 32 | version = "0.5.1" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" 35 | 36 | [[package]] 37 | name = "atty" 38 | version = "0.2.14" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 41 | dependencies = [ 42 | "hermit-abi", 43 | "libc", 44 | "winapi", 45 | ] 46 | 47 | [[package]] 48 | name = "autocfg" 49 | version = "1.0.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 52 | 53 | [[package]] 54 | name = "backtrace" 55 | version = "0.3.46" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "b1e692897359247cc6bb902933361652380af0f1b7651ae5c5013407f30e109e" 58 | dependencies = [ 59 | "backtrace-sys", 60 | "cfg-if 0.1.10", 61 | "libc", 62 | "rustc-demangle", 63 | ] 64 | 65 | [[package]] 66 | name = "backtrace-sys" 67 | version = "0.1.37" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "18fbebbe1c9d1f383a9cc7e8ccdb471b91c8d024ee9c2ca5b5346121fe8b4399" 70 | dependencies = [ 71 | "cc", 72 | "libc", 73 | ] 74 | 75 | [[package]] 76 | name = "base64" 77 | version = "0.11.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" 80 | 81 | [[package]] 82 | name = "better-panic" 83 | version = "0.2.0" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "3d12a680cc74d8c4a44ee08be4a00dedf671b089c2440b2e3fdaa776cd468476" 86 | dependencies = [ 87 | "backtrace", 88 | "console", 89 | ] 90 | 91 | [[package]] 92 | name = "bitflags" 93 | version = "1.2.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 96 | 97 | [[package]] 98 | name = "blake2b_simd" 99 | version = "0.5.10" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" 102 | dependencies = [ 103 | "arrayref", 104 | "arrayvec", 105 | "constant_time_eq", 106 | ] 107 | 108 | [[package]] 109 | name = "cassowary" 110 | version = "0.3.0" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 113 | 114 | [[package]] 115 | name = "cc" 116 | version = "1.0.52" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "c3d87b23d6a92cd03af510a5ade527033f6aa6fa92161e2d5863a907d4c5e31d" 119 | 120 | [[package]] 121 | name = "cfg-if" 122 | version = "0.1.10" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 125 | 126 | [[package]] 127 | name = "cfg-if" 128 | version = "1.0.0" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 131 | 132 | [[package]] 133 | name = "chrono" 134 | version = "0.4.19" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" 137 | dependencies = [ 138 | "libc", 139 | "num-integer", 140 | "num-traits", 141 | "time", 142 | "winapi", 143 | ] 144 | 145 | [[package]] 146 | name = "clap" 147 | version = "2.33.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" 150 | dependencies = [ 151 | "ansi_term", 152 | "atty", 153 | "bitflags", 154 | "strsim", 155 | "textwrap", 156 | "unicode-width", 157 | "vec_map", 158 | ] 159 | 160 | [[package]] 161 | name = "clicolors-control" 162 | version = "1.0.1" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "90082ee5dcdd64dc4e9e0d37fbf3ee325419e39c0092191e0393df65518f741e" 165 | dependencies = [ 166 | "atty", 167 | "lazy_static", 168 | "libc", 169 | "winapi", 170 | ] 171 | 172 | [[package]] 173 | name = "cloudabi" 174 | version = "0.1.0" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "4344512281c643ae7638bbabc3af17a11307803ec8f0fcad9fae512a8bf36467" 177 | dependencies = [ 178 | "bitflags", 179 | ] 180 | 181 | [[package]] 182 | name = "console" 183 | version = "0.9.2" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "45e0f3986890b3acbc782009e2629dfe2baa430ac091519ce3be26164a2ae6c0" 186 | dependencies = [ 187 | "clicolors-control", 188 | "encode_unicode", 189 | "lazy_static", 190 | "libc", 191 | "regex", 192 | "termios", 193 | "winapi", 194 | ] 195 | 196 | [[package]] 197 | name = "const_fn" 198 | version = "0.4.3" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "c478836e029dcef17fb47c89023448c64f781a046e0300e257ad8225ae59afab" 201 | 202 | [[package]] 203 | name = "constant_time_eq" 204 | version = "0.1.5" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 207 | 208 | [[package]] 209 | name = "crossbeam-channel" 210 | version = "0.5.0" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" 213 | dependencies = [ 214 | "cfg-if 1.0.0", 215 | "crossbeam-utils 0.8.0", 216 | ] 217 | 218 | [[package]] 219 | name = "crossbeam-utils" 220 | version = "0.7.2" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" 223 | dependencies = [ 224 | "autocfg", 225 | "cfg-if 0.1.10", 226 | "lazy_static", 227 | ] 228 | 229 | [[package]] 230 | name = "crossbeam-utils" 231 | version = "0.8.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "ec91540d98355f690a86367e566ecad2e9e579f230230eb7c21398372be73ea5" 234 | dependencies = [ 235 | "autocfg", 236 | "cfg-if 1.0.0", 237 | "const_fn", 238 | "lazy_static", 239 | ] 240 | 241 | [[package]] 242 | name = "crossterm" 243 | version = "0.18.2" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "4e86d73f2a0b407b5768d10a8c720cf5d2df49a9efc10ca09176d201ead4b7fb" 246 | dependencies = [ 247 | "bitflags", 248 | "crossterm_winapi", 249 | "lazy_static", 250 | "libc", 251 | "mio", 252 | "parking_lot", 253 | "signal-hook", 254 | "winapi", 255 | ] 256 | 257 | [[package]] 258 | name = "crossterm_winapi" 259 | version = "0.6.2" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "c2265c3f8e080075d9b6417aa72293fc71662f34b4af2612d8d1b074d29510db" 262 | dependencies = [ 263 | "winapi", 264 | ] 265 | 266 | [[package]] 267 | name = "ctrlc" 268 | version = "3.1.7" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "b57a92e9749e10f25a171adcebfafe72991d45e7ec2dcb853e8f83d9dafaeb08" 271 | dependencies = [ 272 | "nix 0.18.0", 273 | "winapi", 274 | ] 275 | 276 | [[package]] 277 | name = "darwin-libproc" 278 | version = "0.1.2" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "9fb90051930c9a0f09e585762152048e23ac74d20c10590ef7cf01c0343c3046" 281 | dependencies = [ 282 | "darwin-libproc-sys", 283 | "libc", 284 | "memchr", 285 | ] 286 | 287 | [[package]] 288 | name = "darwin-libproc-sys" 289 | version = "0.1.2" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "57cebb5bde66eecdd30ddc4b9cd208238b15db4982ccc72db59d699ea10867c1" 292 | dependencies = [ 293 | "libc", 294 | ] 295 | 296 | [[package]] 297 | name = "derive_more" 298 | version = "0.99.5" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "e2323f3f47db9a0e77ce7a300605d8d2098597fc451ed1a97bb1f6411bb550a7" 301 | dependencies = [ 302 | "proc-macro2", 303 | "quote", 304 | "syn", 305 | ] 306 | 307 | [[package]] 308 | name = "dirs" 309 | version = "2.0.2" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" 312 | dependencies = [ 313 | "cfg-if 0.1.10", 314 | "dirs-sys", 315 | ] 316 | 317 | [[package]] 318 | name = "dirs-sys" 319 | version = "0.3.5" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" 322 | dependencies = [ 323 | "libc", 324 | "redox_users", 325 | "winapi", 326 | ] 327 | 328 | [[package]] 329 | name = "encode_unicode" 330 | version = "0.3.6" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" 333 | 334 | [[package]] 335 | name = "getrandom" 336 | version = "0.1.14" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" 339 | dependencies = [ 340 | "cfg-if 0.1.10", 341 | "libc", 342 | "wasi", 343 | ] 344 | 345 | [[package]] 346 | name = "glob" 347 | version = "0.3.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" 350 | 351 | [[package]] 352 | name = "heck" 353 | version = "0.3.1" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 356 | dependencies = [ 357 | "unicode-segmentation", 358 | ] 359 | 360 | [[package]] 361 | name = "hermit-abi" 362 | version = "0.1.12" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "61565ff7aaace3525556587bd2dc31d4a07071957be715e63ce7b1eccf51a8f4" 365 | dependencies = [ 366 | "libc", 367 | ] 368 | 369 | [[package]] 370 | name = "instant" 371 | version = "0.1.8" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "cb1fc4429a33e1f80d41dc9fea4d108a88bec1de8053878898ae448a0b52f613" 374 | dependencies = [ 375 | "cfg-if 1.0.0", 376 | ] 377 | 378 | [[package]] 379 | name = "lazy_static" 380 | version = "1.4.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 383 | 384 | [[package]] 385 | name = "libc" 386 | version = "0.2.80" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" 389 | 390 | [[package]] 391 | name = "lock_api" 392 | version = "0.4.1" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "28247cc5a5be2f05fbcd76dd0cf2c7d3b5400cb978a28042abcd4fa0b3f8261c" 395 | dependencies = [ 396 | "scopeguard", 397 | ] 398 | 399 | [[package]] 400 | name = "log" 401 | version = "0.4.8" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 404 | dependencies = [ 405 | "cfg-if 0.1.10", 406 | ] 407 | 408 | [[package]] 409 | name = "mach" 410 | version = "0.3.2" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" 413 | dependencies = [ 414 | "libc", 415 | ] 416 | 417 | [[package]] 418 | name = "memchr" 419 | version = "2.3.3" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 422 | 423 | [[package]] 424 | name = "mio" 425 | version = "0.7.6" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "f33bc887064ef1fd66020c9adfc45bb9f33d75a42096c81e7c56c65b75dd1a8b" 428 | dependencies = [ 429 | "libc", 430 | "log", 431 | "miow", 432 | "ntapi", 433 | "winapi", 434 | ] 435 | 436 | [[package]] 437 | name = "miow" 438 | version = "0.3.6" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" 441 | dependencies = [ 442 | "socket2", 443 | "winapi", 444 | ] 445 | 446 | [[package]] 447 | name = "neotop" 448 | version = "0.1.0" 449 | dependencies = [ 450 | "anyhow", 451 | "better-panic", 452 | "chrono", 453 | "crossbeam-channel", 454 | "crossterm", 455 | "ctrlc", 456 | "num-rational", 457 | "platform-dirs", 458 | "psutil", 459 | "size", 460 | "structopt", 461 | "tui", 462 | ] 463 | 464 | [[package]] 465 | name = "nix" 466 | version = "0.17.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" 469 | dependencies = [ 470 | "bitflags", 471 | "cc", 472 | "cfg-if 0.1.10", 473 | "libc", 474 | "void", 475 | ] 476 | 477 | [[package]] 478 | name = "nix" 479 | version = "0.18.0" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055" 482 | dependencies = [ 483 | "bitflags", 484 | "cc", 485 | "cfg-if 0.1.10", 486 | "libc", 487 | ] 488 | 489 | [[package]] 490 | name = "ntapi" 491 | version = "0.3.4" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "7a31937dea023539c72ddae0e3571deadc1414b300483fa7aaec176168cfa9d2" 494 | dependencies = [ 495 | "winapi", 496 | ] 497 | 498 | [[package]] 499 | name = "num-bigint" 500 | version = "0.3.1" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf" 503 | dependencies = [ 504 | "autocfg", 505 | "num-integer", 506 | "num-traits", 507 | ] 508 | 509 | [[package]] 510 | name = "num-integer" 511 | version = "0.1.42" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" 514 | dependencies = [ 515 | "autocfg", 516 | "num-traits", 517 | ] 518 | 519 | [[package]] 520 | name = "num-rational" 521 | version = "0.3.2" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" 524 | dependencies = [ 525 | "autocfg", 526 | "num-bigint", 527 | "num-integer", 528 | "num-traits", 529 | ] 530 | 531 | [[package]] 532 | name = "num-traits" 533 | version = "0.2.11" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" 536 | dependencies = [ 537 | "autocfg", 538 | ] 539 | 540 | [[package]] 541 | name = "num_cpus" 542 | version = "1.13.0" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" 545 | dependencies = [ 546 | "hermit-abi", 547 | "libc", 548 | ] 549 | 550 | [[package]] 551 | name = "once_cell" 552 | version = "1.5.2" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" 555 | 556 | [[package]] 557 | name = "parking_lot" 558 | version = "0.11.0" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "a4893845fa2ca272e647da5d0e46660a314ead9c2fdd9a883aabc32e481a8733" 561 | dependencies = [ 562 | "instant", 563 | "lock_api", 564 | "parking_lot_core", 565 | ] 566 | 567 | [[package]] 568 | name = "parking_lot_core" 569 | version = "0.8.0" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "c361aa727dd08437f2f1447be8b59a33b0edd15e0fcee698f935613d9efbca9b" 572 | dependencies = [ 573 | "cfg-if 0.1.10", 574 | "cloudabi", 575 | "instant", 576 | "libc", 577 | "redox_syscall", 578 | "smallvec", 579 | "winapi", 580 | ] 581 | 582 | [[package]] 583 | name = "platform-dirs" 584 | version = "0.2.0" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "f1e6f10c0c97e3d27b298374c2c67a057216c98e0a86c44df6bcd1f02bacbe38" 587 | dependencies = [ 588 | "dirs", 589 | ] 590 | 591 | [[package]] 592 | name = "platforms" 593 | version = "0.2.1" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "feb3b2b1033b8a60b4da6ee470325f887758c95d5320f52f9ce0df055a55940e" 596 | 597 | [[package]] 598 | name = "proc-macro-error" 599 | version = "1.0.2" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" 602 | dependencies = [ 603 | "proc-macro-error-attr", 604 | "proc-macro2", 605 | "quote", 606 | "syn", 607 | "version_check", 608 | ] 609 | 610 | [[package]] 611 | name = "proc-macro-error-attr" 612 | version = "1.0.2" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" 615 | dependencies = [ 616 | "proc-macro2", 617 | "quote", 618 | "syn", 619 | "syn-mid", 620 | "version_check", 621 | ] 622 | 623 | [[package]] 624 | name = "proc-macro2" 625 | version = "1.0.24" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" 628 | dependencies = [ 629 | "unicode-xid", 630 | ] 631 | 632 | [[package]] 633 | name = "psutil" 634 | version = "3.2.0" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "7cdb732329774b8765346796abd1e896e9b3c86aae7f135bb1dda98c2c460f55" 637 | dependencies = [ 638 | "cfg-if 0.1.10", 639 | "darwin-libproc", 640 | "derive_more", 641 | "glob", 642 | "mach", 643 | "nix 0.17.0", 644 | "num_cpus", 645 | "once_cell", 646 | "platforms", 647 | "thiserror", 648 | "unescape", 649 | ] 650 | 651 | [[package]] 652 | name = "quote" 653 | version = "1.0.4" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "4c1f4b0efa5fc5e8ceb705136bfee52cfdb6a4e3509f770b478cd6ed434232a7" 656 | dependencies = [ 657 | "proc-macro2", 658 | ] 659 | 660 | [[package]] 661 | name = "redox_syscall" 662 | version = "0.1.56" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" 665 | 666 | [[package]] 667 | name = "redox_users" 668 | version = "0.3.4" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" 671 | dependencies = [ 672 | "getrandom", 673 | "redox_syscall", 674 | "rust-argon2", 675 | ] 676 | 677 | [[package]] 678 | name = "regex" 679 | version = "1.3.7" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" 682 | dependencies = [ 683 | "regex-syntax", 684 | ] 685 | 686 | [[package]] 687 | name = "regex-syntax" 688 | version = "0.6.17" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" 691 | 692 | [[package]] 693 | name = "rust-argon2" 694 | version = "0.7.0" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" 697 | dependencies = [ 698 | "base64", 699 | "blake2b_simd", 700 | "constant_time_eq", 701 | "crossbeam-utils 0.7.2", 702 | ] 703 | 704 | [[package]] 705 | name = "rustc-demangle" 706 | version = "0.1.16" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" 709 | 710 | [[package]] 711 | name = "scopeguard" 712 | version = "1.1.0" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 715 | 716 | [[package]] 717 | name = "signal-hook" 718 | version = "0.1.16" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "604508c1418b99dfe1925ca9224829bb2a8a9a04dda655cc01fcad46f4ab05ed" 721 | dependencies = [ 722 | "libc", 723 | "mio", 724 | "signal-hook-registry", 725 | ] 726 | 727 | [[package]] 728 | name = "signal-hook-registry" 729 | version = "1.2.0" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "94f478ede9f64724c5d173d7bb56099ec3e2d9fc2774aac65d34b8b890405f41" 732 | dependencies = [ 733 | "arc-swap", 734 | "libc", 735 | ] 736 | 737 | [[package]] 738 | name = "size" 739 | version = "0.1.2" 740 | source = "registry+https://github.com/rust-lang/crates.io-index" 741 | checksum = "3e5021178e8e70579d009fb545932e274ec2dde4c917791c6063d1002bee2a56" 742 | dependencies = [ 743 | "num-traits", 744 | ] 745 | 746 | [[package]] 747 | name = "smallvec" 748 | version = "1.4.0" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4" 751 | 752 | [[package]] 753 | name = "socket2" 754 | version = "0.3.16" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "7fd8b795c389288baa5f355489c65e71fd48a02104600d15c4cfbc561e9e429d" 757 | dependencies = [ 758 | "cfg-if 0.1.10", 759 | "libc", 760 | "redox_syscall", 761 | "winapi", 762 | ] 763 | 764 | [[package]] 765 | name = "strsim" 766 | version = "0.8.0" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 769 | 770 | [[package]] 771 | name = "structopt" 772 | version = "0.3.20" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "126d630294ec449fae0b16f964e35bf3c74f940da9dca17ee9b905f7b3112eb8" 775 | dependencies = [ 776 | "clap", 777 | "lazy_static", 778 | "structopt-derive", 779 | ] 780 | 781 | [[package]] 782 | name = "structopt-derive" 783 | version = "0.4.13" 784 | source = "registry+https://github.com/rust-lang/crates.io-index" 785 | checksum = "65e51c492f9e23a220534971ff5afc14037289de430e3c83f9daf6a1b6ae91e8" 786 | dependencies = [ 787 | "heck", 788 | "proc-macro-error", 789 | "proc-macro2", 790 | "quote", 791 | "syn", 792 | ] 793 | 794 | [[package]] 795 | name = "syn" 796 | version = "1.0.48" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "cc371affeffc477f42a221a1e4297aedcea33d47d19b61455588bd9d8f6b19ac" 799 | dependencies = [ 800 | "proc-macro2", 801 | "quote", 802 | "unicode-xid", 803 | ] 804 | 805 | [[package]] 806 | name = "syn-mid" 807 | version = "0.5.0" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" 810 | dependencies = [ 811 | "proc-macro2", 812 | "quote", 813 | "syn", 814 | ] 815 | 816 | [[package]] 817 | name = "termios" 818 | version = "0.3.2" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "6f0fcee7b24a25675de40d5bb4de6e41b0df07bc9856295e7e2b3a3600c400c2" 821 | dependencies = [ 822 | "libc", 823 | ] 824 | 825 | [[package]] 826 | name = "textwrap" 827 | version = "0.11.0" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 830 | dependencies = [ 831 | "unicode-width", 832 | ] 833 | 834 | [[package]] 835 | name = "thiserror" 836 | version = "1.0.22" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e" 839 | dependencies = [ 840 | "thiserror-impl", 841 | ] 842 | 843 | [[package]] 844 | name = "thiserror-impl" 845 | version = "1.0.22" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" 848 | dependencies = [ 849 | "proc-macro2", 850 | "quote", 851 | "syn", 852 | ] 853 | 854 | [[package]] 855 | name = "time" 856 | version = "0.1.43" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 859 | dependencies = [ 860 | "libc", 861 | "winapi", 862 | ] 863 | 864 | [[package]] 865 | name = "tui" 866 | version = "0.13.0" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "5d4e6c82bb967df89f20b875fa8835fab5d5622c6a5efa574a1f0b6d0aa6e8f6" 869 | dependencies = [ 870 | "bitflags", 871 | "cassowary", 872 | "crossterm", 873 | "unicode-segmentation", 874 | "unicode-width", 875 | ] 876 | 877 | [[package]] 878 | name = "unescape" 879 | version = "0.1.0" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" 882 | 883 | [[package]] 884 | name = "unicode-segmentation" 885 | version = "1.6.0" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 888 | 889 | [[package]] 890 | name = "unicode-width" 891 | version = "0.1.7" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" 894 | 895 | [[package]] 896 | name = "unicode-xid" 897 | version = "0.2.0" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 900 | 901 | [[package]] 902 | name = "vec_map" 903 | version = "0.8.2" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 906 | 907 | [[package]] 908 | name = "version_check" 909 | version = "0.9.1" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" 912 | 913 | [[package]] 914 | name = "void" 915 | version = "1.0.2" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 918 | 919 | [[package]] 920 | name = "wasi" 921 | version = "0.9.0+wasi-snapshot-preview1" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" 924 | 925 | [[package]] 926 | name = "winapi" 927 | version = "0.3.8" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 930 | dependencies = [ 931 | "winapi-i686-pc-windows-gnu", 932 | "winapi-x86_64-pc-windows-gnu", 933 | ] 934 | 935 | [[package]] 936 | name = "winapi-i686-pc-windows-gnu" 937 | version = "0.4.0" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 940 | 941 | [[package]] 942 | name = "winapi-x86_64-pc-windows-gnu" 943 | version = "0.4.0" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 946 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "neotop" 3 | version = "0.1.0" 4 | authors = ["Caleb Bassi "] 5 | license = "MIT" 6 | description = "A Rust port of htop with some additional improvements" 7 | edition = "2018" 8 | 9 | [dependencies] 10 | anyhow = "1.0.34" 11 | better-panic = "0.2.0" # TODO 12 | chrono = "0.4.19" # TODO 13 | crossbeam-channel = "0.5.0" 14 | crossterm = "0.18.2" 15 | ctrlc = { version = "3.1.7", features = ["termination"] } 16 | num-rational = "0.3.2" 17 | platform-dirs = "0.2.0" 18 | psutil = "3.1.0" 19 | size = "0.1.2" # TODO 20 | structopt = "0.3.20" 21 | tui = { version = "0.13.0", default-features = false, features = ["crossterm"] } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Caleb Bassi 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # neotop 2 | 3 | ![Minimum rustc version](https://img.shields.io/badge/rustc-1.39+-green.svg) 4 | [![crates.io](https://img.shields.io/crates/v/neotop.svg)](https://crates.io/crates/neotop) 5 | 6 | A Rust port of [htop] with some additional improvements including: 7 | 8 | - full vim keybindings 9 | - bottom keybind bar has been removed 10 | - fix a bug where the screen momentarily flashes blank on redraw 11 | - all configuration is now done with CLI flags instead of with in-app menus 12 | - all CLI flags can be persisted by specifying them in the config file 13 | - add a panel to the help menu explaining what each UI element is displaying 14 | 15 | Supported platforms: 16 | 17 | - Linux 18 | 19 | Windows support is not planned since it is not POSIX compliant and certain system information is not available. 20 | 21 |
22 | 23 |
24 | 25 | ## Installation 26 | 27 | ### Package managers 28 | 29 | [![Packaging status](https://repology.org/badge/vertical-allrepos/neotop.svg)](https://repology.org/project/neotop/versions) 30 | 31 | ### Prebuilt binaries 32 | 33 | Prebuilt binaries are provided in the [releases](https://github.com/cjbassi/neotop/releases) tab. 34 | 35 | ### From source 36 | 37 | ```bash 38 | cargo install neotop 39 | ``` 40 | 41 | ## Related projects 42 | 43 | - Rust 44 | - [bb](https://github.com/epilys/bb) 45 | - [bottom](https://github.com/ClementTsang/bottom) 46 | - [zenith](https://github.com/bvaisvil/zenith) 47 | - [bpytop](https://github.com/aristocratos/bpytop) 48 | - [glances](https://github.com/nicolargo/glances) 49 | - [gotop](https://github.com/xxxserxxx/gotop) 50 | - [gtop](https://github.com/aksakalli/gtop) 51 | - [htop] 52 | - [htop-vim](https://github.com/KoffeinFlummi/htop-vim) 53 | - [vtop](https://github.com/MrRio/vtop) 54 | 55 | [htop]: https://github.com/htop-dev/htop 56 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjbassi/neotop/b041c2ca4fb4a082447a34052df65fdc4532d37d/screenshot.png -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::widgets::*; 2 | 3 | pub struct App { 4 | pub help_menu: HelpMenu, 5 | pub widgets: Widgets, 6 | } 7 | 8 | pub struct Widgets { 9 | pub cpu_and_mem: CpuAndMemWidget, 10 | pub misc_info: MiscInfoWidget, 11 | pub proc: ProcWidget, 12 | } 13 | 14 | pub fn setup_app() -> App { 15 | let cpu_and_mem = CpuAndMemWidget::new(); 16 | let mem = MiscInfoWidget::new(); 17 | let proc = ProcWidget::new(); 18 | let help_menu = HelpMenu::new(); 19 | 20 | App { 21 | help_menu, 22 | widgets: Widgets { 23 | cpu_and_mem, 24 | misc_info: mem, 25 | proc, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/args.rs: -------------------------------------------------------------------------------- 1 | use num_rational::Ratio; 2 | use structopt::StructOpt; 3 | 4 | #[derive(StructOpt, Debug)] 5 | pub struct Args { 6 | /// Interval in seconds between updates as either a whole number or a ratio. 7 | #[structopt(short, long, default_value = "1")] 8 | pub interval: Ratio, 9 | 10 | /// Show only the given PIDs. 11 | #[structopt(short, long)] 12 | pub pids: Option>, 13 | 14 | /// Sort by a given column by default. 15 | #[structopt(short, long)] 16 | pub sort_key: Option, 17 | 18 | /// Show processes in tree mode by default. 19 | #[structopt(short, long)] 20 | pub tree: bool, 21 | 22 | /// Show only the processes of a given user. 23 | #[structopt(short, long)] 24 | pub user: Option, 25 | } 26 | -------------------------------------------------------------------------------- /src/draw.rs: -------------------------------------------------------------------------------- 1 | use tui::backend::Backend; 2 | use tui::layout::{Constraint, Direction, Layout, Rect}; 3 | use tui::{Frame, Terminal}; 4 | 5 | use crate::app::{App, Widgets}; 6 | 7 | pub fn draw_widgets(terminal: &mut Terminal, app: &mut App) { 8 | terminal 9 | .draw(|mut frame| { 10 | let vertical_chunks = Layout::default() 11 | .direction(Direction::Vertical) 12 | .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)].as_ref()) 13 | .split(frame.size()); 14 | draw_top_row(frame, widgets, vertical_chunks[0]); 15 | draw_bottom_row(frame, widgets, vertical_chunks[1]); 16 | }) 17 | .unwrap(); 18 | } 19 | 20 | pub fn draw_widgets(frame: &mut Frame, widgets: &mut Widgets, area: Rect) {} 21 | 22 | pub fn draw_top_row(frame: &mut Frame, widgets: &mut Widgets, area: Rect) { 23 | let horizontal_chunks = Layout::default() 24 | .direction(Direction::Horizontal) 25 | .constraints([Constraint::Percentage(100)].as_ref()) 26 | .split(area); 27 | frame.render_widget(&widgets.cpu_and_mem, horizontal_chunks[0]); 28 | } 29 | 30 | pub fn draw_bottom_row(frame: &mut Frame, widgets: &mut Widgets, area: Rect) { 31 | let horizontal_chunks = Layout::default() 32 | .direction(Direction::Horizontal) 33 | .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)].as_ref()) 34 | .split(area); 35 | if let Some(net) = widgets.net.as_ref() { 36 | frame.render_widget(net, horizontal_chunks[0]); 37 | } else { 38 | frame.render_widget(&widgets.misc_info, horizontal_chunks[0]); 39 | } 40 | frame.render_widget(&mut widgets.proc, horizontal_chunks[1]); 41 | } 42 | 43 | pub fn draw_help_menu(terminal: &mut Terminal, app: &mut App) { 44 | terminal 45 | .draw(|mut frame| { 46 | let rect = app.help_menu.get_rect(frame.size()); 47 | frame.render_widget(&app.help_menu, rect); 48 | }) 49 | .unwrap(); 50 | } 51 | 52 | // TODO: figure out how to draw the proc widget without clearing rest of the screen 53 | pub fn draw_proc(terminal: &mut Terminal, app: &mut App) { 54 | draw(terminal, app); 55 | } 56 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod args; 3 | mod draw; 4 | mod update; 5 | mod widgets; 6 | 7 | use std::io::{self, Write}; 8 | use std::panic; 9 | use std::thread; 10 | use std::time::Duration; 11 | 12 | use crossbeam_channel::{select, tick, unbounded, Receiver}; 13 | use crossterm::cursor; 14 | use crossterm::event::{Event, KeyCode, KeyModifiers}; 15 | use crossterm::execute; 16 | use crossterm::terminal; 17 | use num_rational::Ratio; 18 | use platform_dirs::AppDirs; 19 | use structopt::StructOpt; 20 | use tui::backend::CrosstermBackend; 21 | use tui::Terminal; 22 | 23 | use app::*; 24 | use args::*; 25 | use draw::*; 26 | use update::*; 27 | 28 | const PROGRAM_NAME: &str = env!("CARGO_PKG_NAME"); 29 | 30 | enum Columns { 31 | Pid, 32 | User, 33 | Pri, 34 | Ni, 35 | Virt, 36 | Res, 37 | Shr, 38 | S, 39 | Cpu, 40 | Mem, 41 | Time, 42 | Command, 43 | } 44 | 45 | struct RuntimeOptions { 46 | tree: bool, 47 | paused: bool, 48 | show_help: bool, 49 | interval: Ratio, 50 | sort_column: Columns, 51 | } 52 | 53 | fn setup_terminal() { 54 | let mut stdout = io::stdout(); 55 | 56 | execute!(stdout, terminal::EnterAlternateScreen).unwrap(); 57 | execute!(stdout, cursor::Hide).unwrap(); 58 | 59 | // Needed for when neotop is run in a TTY since TTYs don't actually have an alternate screen. 60 | // Must be executed after attempting to enter the alternate screen so that it only clears the 61 | // primary screen if we are running in a TTY. 62 | // If not running in a TTY, then we just end up clearing the alternate screen which should have 63 | // no effect. 64 | execute!(stdout, terminal::Clear(terminal::ClearType::All)).unwrap(); 65 | 66 | terminal::enable_raw_mode().unwrap(); 67 | } 68 | 69 | fn cleanup_terminal() { 70 | let mut stdout = io::stdout(); 71 | 72 | // Needed for when neotop is run in a TTY since TTYs don't actually have an alternate screen. 73 | // Must be executed before attempting to leave the alternate screen so that it only modifies the 74 | // primary screen if we are running in a TTY. 75 | // If not running in a TTY, then we just end up modifying the alternate screen which should have 76 | // no effect. 77 | execute!(stdout, cursor::MoveTo(0, 0)).unwrap(); 78 | execute!(stdout, terminal::Clear(terminal::ClearType::All)).unwrap(); 79 | 80 | execute!(stdout, terminal::LeaveAlternateScreen).unwrap(); 81 | execute!(stdout, cursor::Show).unwrap(); 82 | 83 | terminal::disable_raw_mode().unwrap(); 84 | } 85 | 86 | fn setup_ui_events() -> Receiver { 87 | let (sender, receiver) = unbounded(); 88 | thread::spawn(move || loop { 89 | sender.send(crossterm::event::read().unwrap()).unwrap(); 90 | }); 91 | 92 | receiver 93 | } 94 | 95 | fn setup_ctrl_c() -> Receiver<()> { 96 | let (sender, receiver) = unbounded(); 97 | ctrlc::set_handler(move || { 98 | sender.send(()).unwrap(); 99 | }) 100 | .unwrap(); 101 | 102 | receiver 103 | } 104 | 105 | // We need to catch panics since we need to close the UI and cleanup the terminal before logging any 106 | // error messages to the screen. 107 | fn setup_panic_hook() { 108 | panic::set_hook(Box::new(|panic_info| { 109 | cleanup_terminal(); 110 | better_panic::Settings::auto().create_panic_handler()(panic_info); 111 | })); 112 | } 113 | 114 | fn main() { 115 | better_panic::install(); 116 | 117 | let args = Args::from_args(); 118 | let draw_interval = args.interval; 119 | 120 | let app_dirs = AppDirs::new(Some(PROGRAM_NAME), true).unwrap(); 121 | 122 | let mut app = setup_app(); 123 | 124 | let backend = CrosstermBackend::new(io::stdout()); 125 | let mut terminal = Terminal::new(backend).unwrap(); 126 | 127 | setup_panic_hook(); 128 | setup_terminal(); 129 | 130 | let ticker = tick(Duration::from_secs_f64( 131 | *draw_interval.numer() as f64 / *draw_interval.denom() as f64, 132 | )); 133 | let ui_events_receiver = setup_ui_events(); 134 | let ctrl_c_events = setup_ctrl_c(); 135 | 136 | let mut show_help_menu = false; 137 | let mut paused = false; 138 | 139 | // Used to keep track of whether we need to redraw the process widget after it has been updated. 140 | let mut proc_modified: bool; 141 | 142 | update_widgets(&mut app.widgets); 143 | draw(&mut terminal, &mut app); 144 | 145 | loop { 146 | select! { 147 | recv(ctrl_c_events) -> _ => { 148 | break; 149 | } 150 | recv(ticker) -> _ => { 151 | if !paused { 152 | update_widgets(&mut app.widgets); 153 | if !show_help_menu { 154 | draw_widgets(&mut terminal, &mut app); 155 | } 156 | } 157 | } 158 | recv(ui_events_receiver) -> message => { 159 | proc_modified = false; 160 | 161 | match message.unwrap() { 162 | Event::Key(key_event) => { 163 | if key_event.modifiers.is_empty() { 164 | match key_event.code { 165 | KeyCode::Char('q') => { 166 | break 167 | }, 168 | KeyCode::Char('?') => { 169 | show_help_menu = !show_help_menu; 170 | if show_help_menu { 171 | draw_help_menu(&mut terminal, &mut app); 172 | } else { 173 | draw_widgets(&mut terminal, &mut app); 174 | } 175 | }, 176 | KeyCode::Char(' ') => { 177 | paused = !paused; 178 | }, 179 | KeyCode::Char('j') | KeyCode::Down => { 180 | app.widgets.proc.scroll_down(); 181 | proc_modified = true; 182 | }, 183 | KeyCode::Char('k') | KeyCode::Up => { 184 | app.widgets.proc.scroll_up(); 185 | proc_modified = true; 186 | }, 187 | KeyCode::Char('g') => { 188 | app.widgets.proc.scroll_top(); 189 | proc_modified = true; 190 | }, 191 | KeyCode::Home => { 192 | app.widgets.proc.scroll_top(); 193 | proc_modified = true; 194 | }, 195 | KeyCode::Char('G') | KeyCode::End => { 196 | app.widgets.proc.scroll_bottom(); 197 | proc_modified = true; 198 | }, 199 | KeyCode::Char('x') => { 200 | app.widgets.proc.kill_process(); 201 | }, 202 | KeyCode::Esc => { 203 | if show_help_menu { 204 | show_help_menu = false; 205 | draw(&mut terminal, &mut app); 206 | } 207 | } 208 | KeyCode::Tab => { 209 | app.widgets.proc.toggle_grouping(); 210 | proc_modified = true; 211 | }, 212 | KeyCode::Char('p') => { 213 | app.widgets.proc.sort_by_num(); 214 | proc_modified = true; 215 | }, 216 | KeyCode::Char('n') => { 217 | app.widgets.proc.sort_by_command(); 218 | proc_modified = true; 219 | }, 220 | KeyCode::Char('c') => { 221 | app.widgets.proc.sort_by_cpu(); 222 | proc_modified = true; 223 | }, 224 | KeyCode::Char('m') => { 225 | app.widgets.proc.sort_by_mem(); 226 | proc_modified = true; 227 | }, 228 | _ => {} 229 | } 230 | } else if key_event.modifiers == KeyModifiers::CONTROL { 231 | match key_event.code { 232 | KeyCode::Char('c') => { 233 | break 234 | }, 235 | KeyCode::Char('d') => { 236 | app.widgets.proc.scroll_half_page_down(); 237 | proc_modified = true; 238 | }, 239 | KeyCode::Char('u') => { 240 | app.widgets.proc.scroll_half_page_up(); 241 | proc_modified = true; 242 | }, 243 | KeyCode::Char('f') => { 244 | app.widgets.proc.scroll_full_page_down(); 245 | proc_modified = true; 246 | }, 247 | KeyCode::Char('b') => { 248 | app.widgets.proc.scroll_full_page_up(); 249 | proc_modified = true; 250 | }, 251 | _ => {} 252 | } 253 | } 254 | } 255 | Event::Resize(_width, _height) => { 256 | if show_help_menu { 257 | draw_help_menu(&mut terminal, &mut app); 258 | } else { 259 | draw_widgets(&mut terminal, &mut app); 260 | } 261 | } 262 | _ => {} 263 | } 264 | 265 | if !show_help_menu { 266 | if proc_modified { 267 | draw_proc(&mut terminal, &mut app); 268 | } 269 | } 270 | } 271 | } 272 | } 273 | 274 | cleanup_terminal(); 275 | } 276 | -------------------------------------------------------------------------------- /src/update.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::app::Widgets; 4 | 5 | pub trait UpdatableWidget { 6 | fn update(&mut self) -> Result<()>; 7 | } 8 | 9 | pub fn update_widgets(widgets: &mut Widgets) { 10 | widgets.cpu_and_mem.update(); 11 | widgets.misc_info.update(); 12 | widgets.proc.update(); 13 | } 14 | -------------------------------------------------------------------------------- /src/widgets/cpu_and_mem.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use psutil::{cpu, memory}; 3 | use tui::buffer::Buffer; 4 | use tui::layout::Rect; 5 | use tui::style::Style; 6 | use tui::widgets::Widget; 7 | 8 | use crate::update::UpdatableWidget; 9 | 10 | pub struct CpuAndMemWidget { 11 | cpu_count: usize, 12 | cpu_collector: cpu::CpuPercentCollector, 13 | cpu_percents: Vec, 14 | mem: memory::VirtualMemory, 15 | swp: memory::SwapMemory, 16 | } 17 | 18 | impl CpuAndMemWidget { 19 | pub fn new() -> Self { 20 | CpuAndMemWidget { 21 | cpu_count: cpu::cpu_count() as usize, 22 | cpu_collector: cpu::CpuPercentCollector::new().unwrap(), 23 | cpu_percents: Vec::new(), 24 | mem: memory::VirtualMemory::default(), 25 | swp: memory::SwapMemory::default(), 26 | } 27 | } 28 | } 29 | 30 | impl UpdatableWidget for CpuAndMemWidget { 31 | fn update(&mut self) -> Result<()> { 32 | self.cpu_percents = self.cpu_collector.cpu_percent_percpu()?; 33 | if self.cpu_percents.len() != self.cpu_count { 34 | // TODO 35 | } 36 | 37 | self.mem = memory::virtual_memory()?; 38 | self.swp = memory::swap_memory()?; 39 | 40 | Ok(()) 41 | } 42 | } 43 | 44 | // fn render_bar(label: &str, percent: f32, ) 45 | 46 | // We impl for a pointer to the struct because render consumes self. 47 | impl Widget for &CpuAndMemWidget { 48 | fn render(self, area: Rect, buf: &mut Buffer) { 49 | for (i, percent) in self.cpu_percents.iter().enumerate() { 50 | let y = area.y + 1 + i as u16; 51 | buf.set_string(area.x + 3, y, format!("{:3}[", i), Style::default()); 52 | for x in area.x..(f32::from(area.width - 10) * percent / 100.0) as u16 + area.x { 53 | buf.set_string(x, y, "|", Style::default()); 54 | } 55 | buf.set_string( 56 | area.x + area.width - 6, 57 | y, 58 | format!("{:3.1}%]", percent), 59 | Style::default(), 60 | ); 61 | } 62 | 63 | let y = area.y + 1 + self.cpu_count as u16; 64 | buf.set_string(area.x + 3, y, "Mem[", Style::default()); 65 | for x in area.x..(f32::from(area.width - 10) * self.mem.percent() / 100.0) as u16 + area.x { 66 | buf.set_string(x, y, "|", Style::default()); 67 | } 68 | buf.set_string( 69 | area.x + area.width - 6, 70 | y, 71 | format!("{:1.2}/{:1.2}]", self.mem.used(), self.mem.total()), 72 | Style::default(), 73 | ); 74 | 75 | let y = area.y + 1 + self.cpu_count as u16; 76 | buf.set_string(area.x + 3, y, "Swp[", Style::default()); 77 | for x in area.x..(f32::from(area.width - 10) * self.swp.percent() / 100.0) as u16 + area.x { 78 | buf.set_string(x, y, "|", Style::default()); 79 | } 80 | buf.set_string( 81 | area.x + area.width - 6, 82 | y, 83 | format!("{:1.2}/{:1.2}]", self.swp.used(), self.swp.total()), 84 | Style::default(), 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/widgets/help_menu.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | 3 | use tui::buffer::Buffer; 4 | use tui::layout::Rect; 5 | use tui::text::Text; 6 | use tui::widgets::{Paragraph, Widget}; 7 | 8 | const TEXT: &str = r"Quit: q or 9 | Pause: 10 | Process navigation: 11 | - k and : up 12 | - j and : down 13 | - : half page up 14 | - : half page down 15 | - : full page up 16 | - : full page down 17 | - g and : jump to top 18 | - G and : jump to bottom 19 | Process actions: 20 | - : toggle process grouping 21 | - x: kill selected process or process group 22 | Process sorting: 23 | - p: PID/Count 24 | - n: Command 25 | - c: CPU 26 | - m: Mem 27 | Process filtering: 28 | - /: start editing filter 29 | - (while editing): 30 | - : accept filter 31 | - and : clear filter"; 32 | 33 | const TEXT_WIDTH: u16 = 48; 34 | const TEXT_HEIGHT: u16 = 29; 35 | 36 | pub struct HelpMenu { 37 | text_vec: Vec>, 38 | } 39 | 40 | impl HelpMenu { 41 | pub fn new() -> Self { 42 | HelpMenu { 43 | text_vec: TEXT 44 | .lines() 45 | .map(|line| Text::raw(format!("{}\n", line))) 46 | .collect(), 47 | } 48 | } 49 | 50 | pub fn get_rect(&self, area: Rect) -> Rect { 51 | Rect { 52 | x: area.width.checked_sub(TEXT_WIDTH).unwrap_or_default() / 2, 53 | y: area.height.checked_sub(TEXT_HEIGHT).unwrap_or_default() / 2, 54 | width: cmp::min(TEXT_WIDTH, area.width), 55 | height: cmp::min(TEXT_HEIGHT, area.height), 56 | } 57 | } 58 | } 59 | 60 | // We impl for a pointer to the struct because render consumes self. 61 | impl Widget for &HelpMenu { 62 | fn render(self, area: Rect, buf: &mut Buffer) { 63 | Paragraph::new(self.text_vec.iter()).render(area, buf); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/widgets/misc_info.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::Result; 4 | use psutil::host; 5 | use tui::buffer::Buffer; 6 | use tui::layout::Rect; 7 | use tui::style::{Color, Modifier, Style}; 8 | use tui::text::{Span, Spans}; 9 | use tui::widgets::Widget; 10 | 11 | use crate::update::UpdatableWidget; 12 | 13 | #[derive(Default)] 14 | pub struct MiscInfoWidget { 15 | tasks: u64, 16 | running: u64, 17 | load_avg: host::LoadAvg, 18 | uptime: Duration, 19 | } 20 | 21 | impl MiscInfoWidget { 22 | pub fn new() -> Self { 23 | MiscInfoWidget::default() 24 | } 25 | } 26 | 27 | impl UpdatableWidget for MiscInfoWidget { 28 | fn update(&mut self) -> Result<()> { 29 | self.uptime = host::uptime()?; 30 | self.load_avg = host::loadavg()?; 31 | 32 | Ok(()) 33 | } 34 | } 35 | 36 | pub fn set_spans<'a>(buf: &mut Buffer, x: u16, y: u16, spans: &Spans<'a>) { 37 | let mut x = x; 38 | for span in &spans.0 { 39 | let content = span.content.as_ref(); 40 | buf.set_string(x, y, content, span.style); 41 | x += content.len() as u16; 42 | } 43 | } 44 | 45 | // We impl for a pointer to the struct because render consumes self. 46 | impl Widget for &MiscInfoWidget { 47 | fn render(self, area: Rect, buf: &mut Buffer) { 48 | set_spans( 49 | buf, 50 | area.x, 51 | area.y, 52 | Spans::from(vec![ 53 | Span::styled("Tasks: ", Style::default().fg(Color::Magenta)), 54 | Span::styled( 55 | format!("{}", self.tasks), 56 | Style::default() 57 | .fg(Color::Magenta) 58 | .add_modifier(Modifier::BOLD), 59 | ), 60 | Span::styled("; ", Style::default().fg(Color::Magenta)), 61 | Span::styled( 62 | format!("{}", self.running), 63 | Style::default() 64 | .fg(Color::Green) 65 | .add_modifier(Modifier::BOLD), 66 | ), 67 | Span::styled(" running", Style::default().fg(Color::Magenta)), 68 | ]), 69 | ); 70 | 71 | set_spans( 72 | buf, 73 | area.x, 74 | area.y + 1, 75 | Spans::from(vec![ 76 | Span::styled("Load average:", Style::default().fg(Color::Magenta)), 77 | Span::styled( 78 | format!(" {}", self.load_avg.one), 79 | Style::default().add_modifier(Modifier::BOLD), 80 | ), 81 | Span::styled( 82 | format!(" {}", self.load_avg.five), 83 | Style::default() 84 | .fg(Color::Magenta) 85 | .add_modifier(Modifier::BOLD), 86 | ), 87 | Span::styled( 88 | format!(" {}", self.load_avg.fifteen), 89 | Style::default().fg(Color::Magenta), 90 | ), 91 | ]), 92 | ); 93 | 94 | set_spans( 95 | buf, 96 | area.x, 97 | area.y + 2, 98 | &Spans::from(vec![ 99 | Span::styled("Uptime: ", Style::default().fg(Color::Magenta)), 100 | Span::styled( 101 | format!("{} days", self.load_avg.one), 102 | Style::default() 103 | .fg(Color::Magenta) 104 | .add_modifier(Modifier::BOLD), 105 | ), 106 | Span::styled(", ", Style::default().fg(Color::Magenta)), 107 | Span::styled( 108 | format!("{}", self.load_avg.five), 109 | Style::default() 110 | .fg(Color::Magenta) 111 | .add_modifier(Modifier::BOLD), 112 | ), 113 | ]), 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | mod cpu_and_mem; 2 | mod help_menu; 3 | mod misc_info; 4 | mod proc; 5 | 6 | pub use self::cpu_and_mem::CpuAndMemWidget; 7 | pub use self::help_menu::HelpMenu; 8 | pub use self::misc_info::MiscInfoWidget; 9 | pub use self::proc::ProcWidget; 10 | -------------------------------------------------------------------------------- /src/widgets/proc.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::ops::Not; 3 | use std::process::Command; 4 | 5 | use anyhow::Result; 6 | #[cfg(target_os = "linux")] 7 | use psutil::process::os::linux::Oneshot; 8 | use psutil::{cpu, memory, process}; 9 | use tui::buffer::Buffer; 10 | use tui::layout::{Constraint, Rect}; 11 | use tui::style::Modifier; 12 | use tui::widgets::{Row, Table, Widget}; 13 | 14 | use crate::update::UpdatableWidget; 15 | 16 | const UP_ARROW: &str = "▲"; 17 | const DOWN_ARROW: &str = "▼"; 18 | 19 | #[derive(PartialEq)] 20 | enum SortMethod { 21 | Cpu, 22 | Mem, 23 | Num, 24 | Command, 25 | } 26 | 27 | impl Default for SortMethod { 28 | fn default() -> Self { 29 | SortMethod::Cpu 30 | } 31 | } 32 | 33 | #[derive(PartialEq, Clone, Copy)] 34 | enum SortDirection { 35 | Up, 36 | Down, 37 | } 38 | 39 | impl Default for SortDirection { 40 | fn default() -> Self { 41 | SortDirection::Down 42 | } 43 | } 44 | 45 | impl Not for SortDirection { 46 | type Output = SortDirection; 47 | 48 | fn not(self) -> Self::Output { 49 | match self { 50 | SortDirection::Up => SortDirection::Down, 51 | SortDirection::Down => SortDirection::Up, 52 | } 53 | } 54 | } 55 | 56 | enum SelectedProc { 57 | Pid(u32), 58 | Name(String), 59 | } 60 | 61 | #[derive(Clone)] 62 | struct Proc { 63 | num: u32, 64 | name: String, 65 | commandline: String, 66 | cpu: f32, 67 | mem: f32, 68 | } 69 | 70 | pub struct ProcWidget { 71 | grouping: bool, 72 | selected_row: usize, 73 | selected_proc: Option, 74 | sort_method: SortMethod, 75 | sort_direction: SortDirection, 76 | view_offset: usize, 77 | scrolled: bool, 78 | view_height: usize, 79 | 80 | cpu_count: u64, 81 | 82 | procs: Vec, 83 | grouped_procs: HashMap, 84 | 85 | process_collector: process::ProcessCollector, 86 | } 87 | 88 | impl ProcWidget { 89 | pub fn new() -> Self { 90 | ProcWidget { 91 | grouping: true, 92 | selected_row: 0, 93 | selected_proc: None, 94 | sort_method: SortMethod::default(), 95 | sort_direction: SortDirection::default(), 96 | view_offset: 0, 97 | scrolled: false, 98 | view_height: 0, 99 | 100 | cpu_count: cpu::cpu_count(), 101 | 102 | procs: Vec::new(), 103 | grouped_procs: HashMap::new(), 104 | 105 | process_collector: process::ProcessCollector::new().unwrap(), 106 | } 107 | } 108 | 109 | fn scroll_count(&mut self, count: isize) { 110 | self.selected_row = isize::max(0, self.selected_row as isize + count) as usize; 111 | self.selected_proc = None; 112 | self.scrolled = true; 113 | } 114 | 115 | fn scroll_to(&mut self, count: usize) { 116 | self.selected_row = usize::min( 117 | count, 118 | if self.grouping { 119 | self.grouped_procs.len() 120 | } else { 121 | self.procs.len() 122 | } - 1, 123 | ); 124 | self.selected_proc = None; 125 | self.scrolled = true; 126 | } 127 | 128 | pub fn scroll_up(&mut self) { 129 | self.scroll_count(-1); 130 | } 131 | 132 | pub fn scroll_down(&mut self) { 133 | self.scroll_count(1); 134 | } 135 | 136 | pub fn scroll_top(&mut self) { 137 | self.scroll_to(0); 138 | } 139 | 140 | pub fn scroll_bottom(&mut self) { 141 | self.scroll_to(if self.grouping { 142 | self.grouped_procs.len() 143 | } else { 144 | self.procs.len() 145 | }); 146 | } 147 | 148 | pub fn scroll_half_page_down(&mut self) { 149 | self.scroll_count(self.view_height as isize / 2); 150 | } 151 | 152 | pub fn scroll_half_page_up(&mut self) { 153 | self.scroll_count(-(self.view_height as isize / 2)); 154 | } 155 | 156 | pub fn scroll_full_page_down(&mut self) { 157 | self.scroll_count(self.view_height as isize); 158 | } 159 | 160 | pub fn scroll_full_page_up(&mut self) { 161 | self.scroll_count(-(self.view_height as isize)); 162 | } 163 | 164 | pub fn toggle_grouping(&mut self) { 165 | self.grouping = !self.grouping; 166 | self.selected_proc = None; 167 | } 168 | 169 | pub fn kill_process(&self) { 170 | let (command, arg) = match self.selected_proc.as_ref().unwrap() { 171 | SelectedProc::Pid(pid) => ("kill", pid.to_string()), 172 | SelectedProc::Name(name) => ("pkill", name.clone()), 173 | }; 174 | Command::new(command).arg(arg).spawn().unwrap(); 175 | } 176 | 177 | fn sort(&mut self, sort_method: SortMethod) { 178 | if self.sort_method == sort_method { 179 | self.sort_direction = !self.sort_direction; 180 | } else { 181 | self.sort_method = sort_method; 182 | self.sort_direction = SortDirection::default(); 183 | } 184 | } 185 | 186 | pub fn sort_by_num(&mut self) { 187 | self.sort(SortMethod::Num); 188 | } 189 | 190 | pub fn sort_by_command(&mut self) { 191 | self.sort(SortMethod::Command); 192 | } 193 | 194 | pub fn sort_by_cpu(&mut self) { 195 | self.sort(SortMethod::Cpu); 196 | } 197 | 198 | pub fn sort_by_mem(&mut self) { 199 | self.sort(SortMethod::Mem); 200 | } 201 | } 202 | 203 | impl UpdatableWidget for ProcWidget { 204 | fn update(&mut self) -> Result<()> { 205 | self.process_collector.update().unwrap(); 206 | 207 | let cpu_count = self.cpu_count as f32; 208 | let virtual_memory = memory::virtual_memory().unwrap(); 209 | 210 | self.procs = self 211 | .process_collector 212 | .processes 213 | .values_mut() 214 | .map(|process| { 215 | let num = process.pid(); 216 | 217 | #[cfg(target_os = "linux")] 218 | let name = process.name_oneshot(); 219 | #[cfg(target_os = "macos")] 220 | let name = process.name()?; 221 | 222 | #[cfg(target_os = "linux")] 223 | let commandline = process.cmdline()?.unwrap_or_else(|| format!("[{}]", name)); 224 | #[cfg(target_os = "macos")] 225 | let commandline = String::default(); 226 | 227 | #[cfg(target_os = "linux")] 228 | let cpu = process.cpu_percent_oneshot() / cpu_count; 229 | #[cfg(target_os = "macos")] 230 | let cpu = process.cpu_percent()? / cpu_count; 231 | 232 | let mem = process.memory_percent_oneshot(&virtual_memory)?; 233 | 234 | Ok(Proc { 235 | num, 236 | name, 237 | commandline, 238 | cpu, 239 | mem, 240 | }) 241 | }) 242 | .filter_map(|process: process::ProcessResult| process.ok()) 243 | .collect(); 244 | 245 | self.grouped_procs.clear(); 246 | for proc in self.procs.iter() { 247 | self.grouped_procs 248 | .entry(proc.name.clone()) 249 | .and_modify(|e| { 250 | e.num += 1; 251 | e.cpu += proc.cpu; 252 | e.mem += proc.mem; 253 | }) 254 | .or_insert_with(|| Proc { 255 | num: 1, 256 | ..proc.clone() 257 | }); 258 | } 259 | 260 | Ok(()) 261 | } 262 | } 263 | 264 | // We impl for a pointer to the struct because render consumes self. 265 | impl Widget for &mut ProcWidget { 266 | fn render(self, area: Rect, buf: &mut Buffer) { 267 | if area.height < 3 { 268 | return; 269 | } 270 | 271 | let inner = Rect { 272 | x: area.x + 1, 273 | y: area.y + 1, 274 | width: area.width - 2, 275 | height: area.height - 2, 276 | }; 277 | 278 | self.view_height = inner.height as usize - 1; 279 | 280 | let mut procs = if self.grouping { 281 | self.grouped_procs.values().cloned().collect() 282 | } else { 283 | self.procs.clone() 284 | }; 285 | if self.sort_direction == SortDirection::Up { 286 | match self.sort_method { 287 | SortMethod::Cpu => procs.sort_by(|a, b| a.cpu.partial_cmp(&b.cpu).unwrap()), 288 | SortMethod::Mem => procs.sort_by(|a, b| a.mem.partial_cmp(&b.mem).unwrap()), 289 | SortMethod::Num => procs.sort_by(|a, b| a.num.cmp(&b.num)), 290 | SortMethod::Command => procs.sort_by(|a, b| a.commandline.cmp(&b.commandline)), 291 | } 292 | } else { 293 | match self.sort_method { 294 | SortMethod::Cpu => procs.sort_by(|b, a| a.cpu.partial_cmp(&b.cpu).unwrap()), 295 | SortMethod::Mem => procs.sort_by(|b, a| a.mem.partial_cmp(&b.mem).unwrap()), 296 | SortMethod::Num => procs.sort_by(|b, a| a.num.cmp(&b.num)), 297 | SortMethod::Command => procs.sort_by(|b, a| a.commandline.cmp(&b.commandline)), 298 | } 299 | } 300 | 301 | let mut header = [ 302 | if self.grouping { " Count" } else { " PID" }, 303 | "Command", 304 | "CPU%", 305 | "Mem%", 306 | ]; 307 | let header_index = match &self.sort_method { 308 | SortMethod::Cpu => 2, 309 | SortMethod::Mem => 3, 310 | SortMethod::Num => 0, 311 | SortMethod::Command => 1, 312 | }; 313 | let arrow = match &self.sort_direction { 314 | SortDirection::Up => UP_ARROW, 315 | SortDirection::Down => DOWN_ARROW, 316 | }; 317 | let updated_header = format!("{}{}", header[header_index], arrow); 318 | header[header_index] = &updated_header; 319 | 320 | self.selected_row = match &self.selected_proc { 321 | Some(selected_proc) => { 322 | match selected_proc { 323 | SelectedProc::Pid(pid) => procs.iter().position(|proc| proc.num == *pid), 324 | SelectedProc::Name(name) => procs.iter().position(|proc| proc.name == *name), 325 | } 326 | } 327 | .unwrap_or(self.selected_row), 328 | None => self.selected_row, 329 | }; 330 | self.scroll_to(self.selected_row); 331 | self.selected_proc = if self.grouping { 332 | Some(SelectedProc::Name( 333 | procs[self.selected_row].name.to_string(), 334 | )) 335 | } else { 336 | Some(SelectedProc::Pid(procs[self.selected_row].num)) 337 | }; 338 | 339 | if self.scrolled { 340 | self.scrolled = false; 341 | if self.selected_row > inner.height as usize + self.view_offset - 2 { 342 | self.view_offset = self.selected_row + 2 - inner.height as usize; 343 | } else if self.selected_row < self.view_offset { 344 | self.view_offset = self.selected_row; 345 | } 346 | } 347 | 348 | let procs_count = procs.len(); 349 | Table::new( 350 | header.iter(), 351 | procs.into_iter().skip(self.view_offset).map(|proc| { 352 | Row::StyledData( 353 | vec![ 354 | format!(" {}", proc.num), 355 | if self.grouping { 356 | proc.name 357 | } else { 358 | proc.commandline 359 | }, 360 | format!("{:>5.1}", proc.cpu), 361 | format!("{:>4.1}", proc.mem), 362 | ] 363 | .into_iter(), 364 | self.colorscheme.text, 365 | ) 366 | }), 367 | ) 368 | .header_style(self.colorscheme.text.modifier(Modifier::BOLD)) 369 | // TODO: this is only a temporary workaround until we fix the table column resizing 370 | // https://github.com/cjbassi/neotop/issues/23 371 | .widths(&[ 372 | // max PID can be 4194304 (7 digits) + 1 for padding 373 | Constraint::Length(8), 374 | // Constraint::Min(5), 375 | // width - (left + right border) - (column1 + column3 + column4 width) - (spaces 376 | // between colums) 377 | Constraint::Length(u16::max((area.width as i16 - 2 - 18 - 3) as u16, 5)), 378 | Constraint::Length(5), 379 | Constraint::Length(5), 380 | ]) 381 | .column_spacing(1) 382 | .header_gap(0) 383 | .render(area, buf); 384 | 385 | // Draw cursor. 386 | let cursor_y = inner.y + 1 + self.selected_row as u16 - self.view_offset as u16; 387 | if cursor_y < inner.bottom() { 388 | for i in inner.x..inner.right() { 389 | let cell = buf.get_mut(i, cursor_y); 390 | if cell.symbol != " " { 391 | cell.set_modifier(Modifier::REVERSED); 392 | cell.set_fg(self.colorscheme.proc_cursor); 393 | } else { 394 | cell.set_bg(self.colorscheme.proc_cursor); 395 | } 396 | } 397 | } 398 | } 399 | } 400 | --------------------------------------------------------------------------------