# psmon [![CI](https://github.com/dumidusw/psmon/actions/workflows/ci.yml/badge.svg)](https://github.com/dumidusw/psmon/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ``` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ PID PPID USER %CPU %MEM TIME COMMAND ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2201 2176 dumidu 51.6 1.5 48:07 opera 8110 8046 dumidu 47.5 5.0 09:20 chrome 8073 8043 dumidu 23.5 1.2 09:20 chrome ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Sorted by: %cpu | Showing: 3 | Excluded: (ps|ps) Press 'q' to quit, 'c'=CPU, 'm'=Memory, 't'=Time ``` A fast, colorful, interactive process monitor built on top of `ps`. No ncurses dependency, no compiled binary: just zsh and standard Linux utilities. > **Linux only.** psmon relies on GNU `ps` flags and reads `/proc` directly, so it does not work on macOS or BSD. See [Requirements](#requirements). ## Why psmon? `top` and `htop` are great, but sometimes you just want: - A quick colorized snapshot of the heaviest processes, straight in your normal terminal scrollback - A simple live-refreshing view you can switch sort order on with a keypress - A one-shot way to trace a process's ancestry, or peek at its open file descriptors `psmon` combines `ps`, `awk`, and other standard Linux utilities into a single small zsh utility. ## Features ![psmon](assets/psmon.png) - 🎨 Colorized output: CPU-heavy processes highlighted in yellow/red - 📊 Sort by CPU, memory, elapsed time, PID, or user - 🔴 Live watch mode with in-place key-driven sort switching (`c`/`m`/`t`/`q`) - 🔍 Process ancestry / tree view: trace a PID up to `init` and list its children (The summary shows the full, unredacted command line, so be mindful when sharing output) - 📂 File descriptor inspector (via `lsof`) with type-coded coloring and a summary - 💀 Interactive kill mode: list, pick a PID, confirm, done (SIGTERM or SIGKILL) - 📦 Sandbox-Aware Kill: traces Flatpak/Bubblewrap containerized apps to their root and terminates the connected process tree. - 🌐 PWA & Webapp Tagging: automatically extracts URL and Class arguments to help you distinguish between isolated browser instances. - 🚫 Exclude processes by pattern (regex-friendly, e.g. `-e 'opera|msedge'`) - ✅ No dependencies beyond zsh, `ps`, `awk`, and (optionally) `lsof` ## Requirements - Linux. psmon depends on GNU-specific ps flags (--sort, --ppid, -o field= for header suppression) that don't exist on BSD/macOS ps, and psmon blame reads /proc/loadavg, /proc/meminfo, /proc/net/dev, and /proc//io directly along with GNU nproc, none of which exist on macOS. There's no macOS/BSD support and none planned. - `zsh` - `ps` (GNU procps, standard on Linux) - `lsof` (optional, only needed for `-d`/file descriptor mode) ## Installation ### Quick install (recommended) ```bash curl -fsSL https://raw.githubusercontent.com/dumidusw/psmon/main/install.sh | zsh ``` This installs psmon to `~/.local/share/psmon`, symlinks the executable into `~/.local/bin/psmon`, and installs zsh tab-completion. ### Manual install ```bash git clone https://github.com/dumidusw/psmon.git ~/.local/share/psmon ln -s ~/.local/share/psmon/psmon ~/.local/bin/psmon chmod +x ~/.local/bin/psmon ``` Make sure `~/.local/bin` is on your `$PATH`: ```bash export PATH="$HOME/.local/bin:$PATH" ``` ### As a zsh plugin If you use a plugin manager, psmon ships an optional psmon.plugin.zsh wrapper. (Linux only, see Requirements; installing via a plugin manager on macOS will not work.) **zinit** ```zsh zinit light dumidusw/psmon ``` **oh-my-zsh** ```bash git clone https://github.com/dumidusw/psmon.git $ZSH_CUSTOM/plugins/psmon ``` then add `psmon` to the `plugins=(...)` list in `~/.zshrc`. ### Uninstall ```bash ~/.local/share/psmon/install.sh --uninstall ``` ## Usage ``` psmon [OPTIONS] psmon blame [-l N] ``` | Flag | Description | |--------------|----------------------------------------------------------| | `-c` | Sort by CPU (default) | | `-m` | Sort by memory | | `-t` | Sort by elapsed time | | `-p` | Sort by PID | | `-u` | Sort by user | | `-l N` | Show N lines (default: 16, or 5 for `blame`) | | `-e PATTERN` | Exclude processes matching PATTERN (regex, `\|`-joinable) | | `-w` | Watch mode, live updates | | `-i N` | Update interval in seconds with `-w` (default: 2) | | `-f PID` | Show ancestry/tree view for a PID | | `-d PID` | Show open file descriptors for a PID (needs `lsof`) | | `-k` | Interactive kill mode, prompts before sending SIGTERM | | `-K` | Interactive kill mode, prompts before sending SIGKILL | | `-h` | Show help | > **Sort direction:** `-c`, `-m`, and `-t` sort descending (heaviest / most recent first). `-p` and `-u` sort ascending (numeric / alphabetical order). > **Note on `-e`:** the pattern matches against the *entire* process line: PID, PPID, user, and command, not just the command name. So `-e dumidu` excludes every process owned by that user, not only processes literally named `dumidu`. | Subcommand | Description | |---------------|----------------------------------------------------------| | `blame` | Quick "why is my machine slow" diagnostic summary | ### Examples ```bash psmon # Top 16 processes by CPU psmon -m # Top 16 processes by memory psmon -l 10 # Top 10 processes by CPU psmon -e snap # Exclude snap processes psmon -e 'opera|msedge' # Exclude multiple patterns psmon -w # Live view, refreshes every 2s psmon -w -i 5 # Live view, refreshes every 5s psmon -m -e opera # Top memory users, excluding opera psmon -f 2182 # Process tree / ancestry for PID 2182 psmon -f 1 # Full ancestry from init psmon -d 2182 # File descriptors for PID 2182 psmon -d 2182 -l 20 # First 20 file descriptors psmon -k # Interactive kill mode (SIGTERM) psmon -K -m # Interactive kill mode (SIGKILL), sorted by memory psmon blame # Quick "why is my machine slow" summary psmon blame -l 8 # Same, but 8 rows per section ``` ### `psmon blame`: quick diagnostic summary ![psmon blame](assets/psmon-blame.png) ``` psmon blame ``` For when something feels slow and you just want the likely culprit, fast. Shows: - **Load average** (1/5/15 min) with a plain-language read against your core count - **Swap usage**, with a warning if it's heavily in use (a common sign of memory pressure) - **Memory usage** (based on `MemAvailable`, not raw free RAM, so it reflects real memory pressure rather than reclaimable disk cache), with a warning as it climbs - **Disk usage** across real filesystems, flagging any that are nearly full - **Network throughput**: aggregate download/upload rate, sampled live across all non-loopback interfaces - **Top CPU** processes - **Top Memory** processes - **Top I/O** processes: cumulative disk bytes read+written since each process started (from `/proc//io`) I/O stats are best-effort: Linux only exposes `/proc//io` for your own processes unless you're root, so on a shared or multi-user system you may see fewer I/O rows than CPU/memory rows. Everything else works without root. ### Interactive kill mode ![psmon interactive kill mode](assets/demo.gif) Example: resolving a name to multiple PWA instances, then a full sandbox tree. ![psmon PWA and sandbox kill example](assets/kill-pwa-sandbox.png) ```bash psmon -k ``` Shows a compact PID %CPU %MEM COMMAND list. Enter either: - **a PID**: psmon resolves it to the main process of that app by walking up the parent chain. - **a process name** (e.g. brave): psmon finds every matching process and groups them by their application instance. **Advanced sandbox & webapp handling:** Modern Linux apps are complicated. Chromium-based browsers spawn dozens of renderers and crashpad handlers, and Flatpak wraps everything in isolated bwrap containers. psmon handles this automatically: - **Container Grouping**: If a process is running inside a Flatpak or Bubblewrap sandbox, psmon traces the tree all the way up to the structural container root. - **Container Tree Termination**: Confirming a kill sends the signal to the entire container tree, so hidden zygotes, renderers, and background workers are terminated along with the process you targeted. - **PWA Disambiguation**: If you have multiple independent webapps running (e.g., a YouTube PWA and a standard browsing session), psmon parses the command arguments and tags them in the UI (e.g., [App: https://youtube.com]) so you know exactly which instance you are targeting. You will see the resolved target and the total number of connected processes, followed by a single confirmation: ``` Send SIGTERM to 18 process(es)? [y/N] ``` ``` psmon -K ``` Same flow, but sends `SIGKILL`, useful when something is ignoring `SIGTERM`. > **`-k` vs `-K`:** Try `-k` (`SIGTERM`) first; it asks the process to shut down, giving it a chance to save state and exit cleanly. Some apps, especially Electron/Node-based ones, don't act on `SIGTERM` right away (or at all) if they're mid-cleanup or waiting on something else, in which case the process will still show up when you check again. `-K` (`SIGKILL`) is the fallback for that: it's handled by the kernel directly and can't be caught, delayed, or ignored, so it always terminates immediately. That also means it skips whatever graceful shutdown/state-save the app would normally do, so treat it as a last resort rather than a default. **Safety notes**: - Always confirms before sending anything; nothing happens from typing a PID/name alone. - Refuses to signal PID 1 (`init`) or psmon's own process/shell, and bails out entirely if the resolved tree contains any protected PID. - After signaling, re-checks each PID (polling for up to ~2s) and reports how many actually terminated (correctly treating zombie processes as terminated, since a zombie is already dead and just awaiting reap by its parent). **Known quirk:** For heavy multi-process apps (browsers especially), a full graceful shutdown can occasionally take a bit longer than the ~2s poll window + list refresh. If the very next screen inside kill mode still shows one of the processes you just killed, that's usually just teardown finishing a moment late; it's already gone by the time you check again with a fresh `psmon` invocation a few seconds later. This is cosmetic and doesn't affect whether the kill actually worked. ### Interactive keys (watch mode) | Key | Action | |-----|------------------| | `q` | Quit | | `c` | Switch to CPU sort | | `m` | Switch to memory sort | | `t` | Switch to time sort | ## Project structure ``` psmon/ ├── psmon # entrypoint: arg parsing + dispatch ├── lib/ │ ├── display.zsh # colors, help text, process list rendering │ ├── watch.zsh # live watch-mode loop │ ├── descriptors.zsh # lsof-based file descriptor inspector │ ├── tree.zsh # process ancestry / tree view │ ├── kill.zsh # interactive kill mode │ └── blame.zsh # quick diagnostic summary (`psmon blame`) ├── completions/ │ └── _psmon # zsh tab-completion ├── psmon.plugin.zsh # optional plugin-manager wrapper └── install.sh # installer / uninstaller ``` The entrypoint resolves its own real path (following symlinks), so it works whether you run it directly, symlink it into `~/.local/bin`, or load it via a plugin manager, as long as `lib/` sits alongside the real `psmon` file. ## Contributing Issues and PRs welcome. A few ideas if you're looking for something to add: - Additional sort fields (e.g. `-o` for open files count) - A `--json` output mode for scripting - Per-user filtering (`-U username`) ## License MIT. See [LICENSE](LICENSE).