├── src ├── __init__.py ├── cpu.py ├── mem.py ├── disk.py └── battery.py ├── tests ├── __init__.py ├── test_disk.py └── test_battery.py ├── .gitignore ├── requirements.txt ├── pytest.ini ├── img ├── demo.png ├── cpu_mem_disk.png ├── cpu_mem_disk_f.png └── cpu_mem_t_disk_t.png ├── requirements-dev.txt ├── pyproject.toml ├── tmux_cpu_mem_monitor.tmux ├── .github └── workflows │ └── ci.yml ├── README.md └── LICENSE /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | psutil==6.0.0 2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = src 3 | -------------------------------------------------------------------------------- /img/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hendrikmi/tmux-cpu-mem-monitor/HEAD/img/demo.png -------------------------------------------------------------------------------- /img/cpu_mem_disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hendrikmi/tmux-cpu-mem-monitor/HEAD/img/cpu_mem_disk.png -------------------------------------------------------------------------------- /img/cpu_mem_disk_f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hendrikmi/tmux-cpu-mem-monitor/HEAD/img/cpu_mem_disk_f.png -------------------------------------------------------------------------------- /img/cpu_mem_t_disk_t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hendrikmi/tmux-cpu-mem-monitor/HEAD/img/cpu_mem_t_disk_t.png -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | iniconfig==2.0.0 3 | packaging==24.1 4 | pluggy==1.5.0 5 | pytest==8.3.2 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pytest.ini_options] 2 | pythonpath = ["src"] 3 | 4 | [tool.ruff] 5 | output-format = "github" 6 | 7 | [tool.ruff.lint] 8 | select = ["I"] 9 | -------------------------------------------------------------------------------- /src/cpu.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | import psutil 4 | 5 | 6 | def get_cpu_usage(interval: int, percpu: bool) -> str: 7 | """Display CPU usage as a percentage""" 8 | cpu_usage = psutil.cpu_percent(interval=interval, percpu=percpu) 9 | 10 | if percpu: 11 | percpu_str = ", ".join(map(str, cpu_usage)) 12 | return percpu_str 13 | return f"{cpu_usage}%" 14 | 15 | 16 | def main(args): 17 | cpu_usage = get_cpu_usage(args.interval, args.percpu) 18 | print(cpu_usage) 19 | 20 | 21 | if __name__ == "__main__": 22 | parser = argparse.ArgumentParser() 23 | parser.add_argument( 24 | "-i", "--interval", type=int, default=1, help="interval in seconds" 25 | ) 26 | parser.add_argument( 27 | "--percpu", action="store_true", default=False, help="display per cpu usage" 28 | ) 29 | args = parser.parse_args() 30 | main(args) 31 | -------------------------------------------------------------------------------- /src/mem.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | import psutil 4 | 5 | 6 | def get_mem_usage_percent(): 7 | """Display memory usage as a percentage""" 8 | mem = psutil.virtual_memory() 9 | mem_usage = mem.percent 10 | return f"{mem_usage}%" 11 | 12 | 13 | def get_mem_usage_total(): 14 | """Display memory usage as used/total in GB""" 15 | mem = psutil.virtual_memory() 16 | available_gb = mem.available / (1024**3) # Convert to GB 17 | total_gb = mem.total / (1024**3) 18 | used_gb = total_gb - available_gb 19 | return f"{used_gb:.2f}GB/{total_gb:.2f}GB" 20 | 21 | 22 | def main(args): 23 | if args.total: 24 | mem_usage = get_mem_usage_total() 25 | else: 26 | mem_usage = get_mem_usage_percent() 27 | print(mem_usage) 28 | 29 | 30 | if __name__ == "__main__": 31 | parser = argparse.ArgumentParser() 32 | parser.add_argument( 33 | "-t", 34 | "--total", 35 | action="store_true", 36 | default=False, 37 | help="display memory usage as used/total in GB", 38 | ) 39 | args = parser.parse_args() 40 | main(args) 41 | -------------------------------------------------------------------------------- /tests/test_disk.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from src import disk 4 | 5 | 6 | def test_disk_percentage_format(): 7 | """Assert that the disk percentage is properly formatted 8 | Example: 5.7% 9 | """ 10 | percent = disk.get_disk_usage_percent() 11 | 12 | # Check that the output is formatted as a decimal number followed by a percent sign 13 | assert re.match(r"\d+(\.\d+)?%", percent) is not None 14 | 15 | 16 | def test_disk_free_format(): 17 | """Assert that the disk free space is properly formatted 18 | Example: 55.7G 19 | """ 20 | free = disk.get_disk_usage_free() 21 | 22 | # Check that the output is formatted as a decimal number followed by a unit 23 | assert re.match(r"\d+(\.\d+)?[A-Z]", free) is not None 24 | 25 | 26 | def test_disk_total_format(): 27 | """Assert that the total disk is properly formatted 28 | Example: 55.7G/1006.9G 29 | """ 30 | total = disk.get_disk_usage_total() 31 | 32 | # Check that the output is used/total where used and total are formatted as decimal numbers followed by a unit 33 | assert re.match(r"^\d+(\.\d+)?[A-Z]/\d+(\.\d+)?[A-Z]$", total) is not None 34 | -------------------------------------------------------------------------------- /tests/test_battery.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | 5 | from src.battery import get_battery_compact, get_battery_long 6 | 7 | 8 | @patch("src.battery._get_charging_status", return_value=True) 9 | def test_charging(mock_charging_status): 10 | """Test the battery charging status""" 11 | result = get_battery_long() 12 | assert result == "Charging" 13 | 14 | 15 | @patch("src.battery._get_charging_status", return_value=False) 16 | @patch("psutil.sensors_battery") 17 | @pytest.mark.parametrize( 18 | "percentage,status", 19 | [ 20 | (100, "Fully charged"), 21 | (95, "Almost full"), 22 | (74, "More than 3/4 full"), 23 | (50, "More than half full"), 24 | (26, "Less than half full"), 25 | (6, "Battery is running low"), 26 | (2, "Battery is almost empty"), 27 | (1, "I'm dying over here"), 28 | (0, "Out of battery"), 29 | ], 30 | ) 31 | def test_battery_long(mock_battery, mock_charging_status, percentage, status): 32 | """Test battery with a range of percentages""" 33 | mock_battery.return_value.percent = percentage 34 | result = get_battery_long() 35 | assert result == status 36 | 37 | 38 | @patch("src.battery._get_charging_status", return_value=False) 39 | @patch("psutil.sensors_battery") 40 | def test_battery_compact(mock_battery, mock_charging_status): 41 | """Test battery compact mode with a range of percentages""" 42 | print("Battery percentage:", end=" ") 43 | for i in range(0, 101, 10): 44 | mock_battery.return_value.percent = i 45 | character = get_battery_compact() 46 | print(i, character, end=" ") 47 | 48 | result = ord(character) 49 | assert 0x00002581 <= result <= 0x00002588 50 | -------------------------------------------------------------------------------- /src/disk.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | 5 | import psutil 6 | from psutil._common import bytes2human 7 | 8 | 9 | def _get_default_path(): 10 | """Get the default path for the current platform""" 11 | if sys.platform == "win32": 12 | return "C:" 13 | elif sys.platform == "darwin": 14 | return "/System/Volumes/Data" 15 | elif ( 16 | # IF kind of linux AND WSL is installed 17 | sys.platform.startswith("linux") and os.path.exists("/usr/lib/wsl") 18 | ): 19 | return "/usr/lib/wsl/drivers" 20 | else: 21 | return "/" 22 | 23 | 24 | def get_disk_usage_percent(path=None): 25 | """Display disk usage as a percentage""" 26 | if path is None: 27 | path = _get_default_path() 28 | 29 | disk = psutil.disk_usage(path) 30 | disk_usage = disk.percent 31 | return f"{disk_usage}%" 32 | 33 | 34 | def get_disk_usage_free(path=None): 35 | """Display free disk in GB""" 36 | if path is None: 37 | path = _get_default_path() 38 | 39 | disk = psutil.disk_usage(path) 40 | return f"{bytes2human(disk.free)}" 41 | 42 | 43 | def get_disk_usage_total(path=None): 44 | """Display disk usage as used/total in GB""" 45 | if path is None: 46 | path = _get_default_path() 47 | 48 | disk = psutil.disk_usage(path) 49 | return f"{bytes2human(disk.used)}/{bytes2human(disk.total)}" 50 | 51 | 52 | def main(args): 53 | if args.total: 54 | disk_usage = get_disk_usage_total(args.path) 55 | elif args.free: 56 | disk_usage = get_disk_usage_free(args.path) 57 | else: 58 | disk_usage = get_disk_usage_percent(args.path) 59 | 60 | print(disk_usage) 61 | 62 | 63 | if __name__ == "__main__": 64 | parser = argparse.ArgumentParser() 65 | group = parser.add_mutually_exclusive_group() 66 | group.add_argument( 67 | "-t", 68 | "--total", 69 | action="store_true", 70 | default=False, 71 | help="display disk usage as used/total in GB", 72 | ) 73 | group.add_argument( 74 | "-f", 75 | "--free", 76 | action="store_true", 77 | default=False, 78 | help="display free disk in GB", 79 | ) 80 | parser.add_argument( 81 | "-p", "--path", type=str, default=None, help="path to check disk usage" 82 | ) 83 | args = parser.parse_args() 84 | main(args) 85 | -------------------------------------------------------------------------------- /tmux_cpu_mem_monitor.tmux: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 4 | 5 | # Checks if Python3 is installed 6 | check_python_installation() { 7 | if ! command -v python3 >/dev/null; then 8 | tmux display-message "Python3 is required but not installed. Please install Python3." 9 | exit 1 # Exit the script if Python3 is not installed 10 | fi 11 | } 12 | 13 | # Sets up the virtual environment and install dependencies 14 | setup_virtual_env() { 15 | if [ ! -d "$CURRENT_DIR/venv" ]; then 16 | tmux display-message "tmux-cpu-memory: Setting up virtual environment..." 17 | if python3 -m venv "$CURRENT_DIR/venv"; then 18 | if "$CURRENT_DIR/venv/bin/pip" install -r "$CURRENT_DIR/requirements.txt"; then 19 | tmux display-message "tmux-cpu-memory plugin installed successfully." 20 | else 21 | tmux display-message "tmux-cpu-memory: Failed to install dependencies." 22 | exit 1 # Exit if pip fails to install dependencies 23 | fi 24 | else 25 | tmux display-message "tmux-cpu-memory: Failed to create virtual environment." 26 | exit 1 # Exit if virtual environment creation fails 27 | fi 28 | else 29 | tmux display-message "tmux-cpu-memory: Virtual environment already exists." 30 | fi 31 | } 32 | 33 | # Updates tmux option with the cpu or mem script command 34 | update_placeholder() { 35 | local placeholder="$1" 36 | local option="$2" 37 | local script="$3" 38 | local option_value="$(tmux show-option -gqv "$option")" 39 | 40 | # Extract everything between #{placeholder and } from the option value 41 | local flags=$(echo "$option_value" | awk -F "#{$placeholder" '{print $2}' | sed 's/\}.*//') 42 | 43 | # Construct the command to execute the Python script with the appropriate flags 44 | local command="#($CURRENT_DIR/venv/bin/python $CURRENT_DIR/src/$script$flags)" 45 | 46 | # Replace the #{placeholder ...} placeholder with the actual command in the option value 47 | local new_option_value="${option_value//\#\{$placeholder$flags\}/$command}" 48 | 49 | # Update the tmux option with the new value 50 | tmux set-option -g "$option" "$new_option_value" 51 | } 52 | 53 | main() { 54 | check_python_installation 55 | setup_virtual_env 56 | update_placeholder "cpu" "status-right" "cpu.py" 57 | update_placeholder "mem" "status-right" "mem.py" 58 | update_placeholder "disk" "status-right" "disk.py" 59 | update_placeholder "battery" "status-right" "battery.py" 60 | update_placeholder "cpu" "status-left" "cpu.py" 61 | update_placeholder "mem" "status-left" "mem.py" 62 | update_placeholder "disk" "status-left" "disk.py" 63 | update_placeholder "battery" "status-left" "battery.py" 64 | } 65 | main 66 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'Run test suite' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | types: 11 | - opened 12 | - synchronize 13 | - reopened 14 | - ready_for_review 15 | 16 | jobs: 17 | test: 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | python-version: ['3.10', '3.11', '3.12'] 22 | os: [ubuntu-latest, windows-latest, macos-latest] 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | 32 | - name: Install dependencies 33 | if: runner.os != 'Windows' 34 | run: | 35 | python -m venv .venv 36 | source .venv/bin/activate 37 | python -m ensurepip --upgrade 38 | pip install -r requirements-dev.txt 39 | 40 | - name: Install dependencies (Windows) 41 | if: runner.os == 'Windows' 42 | run: | 43 | python -m venv .venv 44 | .\.venv\Scripts\Activate.ps1 45 | python -m ensurepip --upgrade 46 | pip install -r requirements-dev.txt 47 | 48 | - name: Run tests 49 | if: runner.os != 'Windows' 50 | run: | 51 | source .venv/bin/activate 52 | pytest -v 53 | 54 | - name: Run tests (Windows) 55 | if: runner.os == 'Windows' 56 | run: | 57 | .\.venv\Scripts\Activate.ps1 58 | pytest -v 59 | 60 | format: 61 | runs-on: ubuntu-latest 62 | permissions: 63 | contents: write 64 | steps: 65 | - uses: actions/checkout@v4 66 | 67 | - name: Install Python 68 | uses: actions/setup-python@v5 69 | with: 70 | python-version: "3.11" 71 | 72 | - name: Install dependencies 73 | run: | 74 | python -m venv .venv 75 | source .venv/bin/activate 76 | python -m ensurepip --upgrade 77 | pip install ruff 78 | 79 | - name: Run Ruff 80 | id: ruff 81 | run: | 82 | source .venv/bin/activate 83 | # It would be nice to push the changes back to the PR branch, but that's difficult to do with forks 84 | # Run lint check 85 | ruff check --exit-non-zero-on-fix 86 | # Run format check, non-zero exit code if there are any changes 87 | ruff format --check 88 | 89 | coverage: 90 | runs-on: ubuntu-latest 91 | 92 | steps: 93 | - uses: actions/checkout@v4 94 | 95 | - name: Python Vulture Action 96 | run: | 97 | python -m venv .venv 98 | source .venv/bin/activate 99 | python -m ensurepip --upgrade 100 | pip install vulture 101 | vulture src 102 | 103 | -------------------------------------------------------------------------------- /src/battery.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from datetime import timedelta 3 | 4 | import psutil 5 | 6 | 7 | def _get_charging_status(): 8 | """Get the battery charging status""" 9 | battery = psutil.sensors_battery() 10 | return battery.power_plugged 11 | 12 | 13 | def get_battery_percent(): 14 | """Display battery percentage""" 15 | if _get_charging_status(): 16 | return "Charging" 17 | 18 | battery = round(psutil.sensors_battery().percent) 19 | return f"{battery}%" 20 | 21 | 22 | def get_battery_time(): 23 | """Display battery time remaining in hours and minutes""" 24 | if _get_charging_status(): 25 | return "Charging" 26 | 27 | time_left = timedelta(seconds=psutil.sensors_battery().secsleft) 28 | print(time_left) 29 | return str(time_left).split(".")[0] 30 | 31 | 32 | def get_battery_long(): 33 | """Display the remaining battery amount in a human-readable format. 34 | 35 | Examples: 36 | - Charging 37 | - Out of battery 38 | - Battery is almost empty 39 | - Battery is running low 40 | - More than half full 41 | ... 42 | """ 43 | if _get_charging_status(): 44 | return "Charging" 45 | 46 | battery_status = { 47 | (100, 100): "Fully charged", 48 | (95, 99): "Almost full", 49 | (74, 94): "More than 3/4 full", 50 | (50, 74): "More than half full", 51 | (26, 49): "Less than half full", 52 | (6, 25): "Battery is running low", 53 | (2, 5): "Battery is almost empty", 54 | (1, 1): "I'm dying over here", 55 | (0, 0): "Out of battery", 56 | } 57 | 58 | for (low, high), status in battery_status.items(): 59 | if low <= psutil.sensors_battery().percent <= high: 60 | return status 61 | 62 | 63 | def _remap_range(value, low, high, remap_low, remap_high): 64 | """Remap the battery percentage into a whole number from 0 up to 7""" 65 | return remap_low + (value - low) * (remap_high - remap_low) // (high - low) 66 | 67 | 68 | def get_battery_compact(): 69 | """Display battery percentage in a compact format""" 70 | if _get_charging_status(): 71 | return chr(0x00002593) 72 | 73 | battery = _remap_range(psutil.sensors_battery().percent, 0, 100, 0, 7) 74 | 75 | # Unicode characters for the battery indicator 76 | # 0x00002581-0x00002588 77 | battery_indicator = chr(0x00002581 + int(battery)) 78 | 79 | return f"{battery_indicator}" 80 | 81 | 82 | def main(args): 83 | if args.percent: 84 | battery = get_battery_percent() 85 | elif args.time: 86 | battery = get_battery_time() 87 | elif args.long: 88 | battery = get_battery_long() 89 | elif args.fun: 90 | battery = get_battery_long(mode="humor") 91 | elif args.compact: 92 | battery = get_battery_compact() 93 | else: 94 | battery = get_battery_percent() 95 | 96 | print(battery) 97 | 98 | 99 | if __name__ == "__main__": 100 | parser = argparse.ArgumentParser() 101 | type_option = parser.add_mutually_exclusive_group() 102 | type_option.add_argument( 103 | "-p", 104 | "--percent", 105 | action="store_true", 106 | default=False, 107 | help="display remaining battery percentage", 108 | ) 109 | type_option.add_argument( 110 | "-t", 111 | "--time", 112 | action="store_true", 113 | default=False, 114 | help="display remaining battery time", 115 | ) 116 | 117 | group = parser.add_mutually_exclusive_group() 118 | group.add_argument( 119 | "-l", 120 | "--long", 121 | action="store_true", 122 | default=False, 123 | help="display remaining battery as a sentence", 124 | ) 125 | group.add_argument( 126 | "-f", 127 | "--fun", 128 | action="store_true", 129 | default=False, 130 | help="display remaining battery with humor", 131 | ) 132 | 133 | parser.add_argument( 134 | "-c", 135 | "--compact", 136 | action="store_true", 137 | default=False, 138 | help="display remaining battery as an icon", 139 | ) 140 | args = parser.parse_args() 141 | main(args) 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tmux CPU & Memory Monitor 2 | 3 | A simple yet flexible tool designed to display CPU and memory usage in the Tmux status bar. 4 | 5 | 6 | 7 | ## Installation 8 | 9 | 1. Install it with the [Tmux Plugin Manager (TPM)](https://github.com/tmux-plugins/tpm) by including the following line in your `.tmux.conf` file. 10 | 11 | ```bash 12 | set -g @plugin 'hendrikmi/tmux-cpu-mem-monitor' 13 | ``` 14 | 15 | 1. Then trigger the installation with `Prefix + I`. 16 | 17 | > [!NOTE] 18 | > Some people have encountered issues with their python environment during installation. Check out [issue #11](https://github.com/hendrikmi/tmux-cpu-mem-monitor/issues/11) for some debugging tips. 19 | 20 | ## Basic Usage 21 | 22 | Once installed, the plugin exposes the placeholders `#{cpu}` and `#{mem}`, which can be used in `status-right` and `status-left`. By default, these placeholders display the current CPU and memory usage as a raw percentage. 23 | 24 | You can customize the display by passing additional options. For example, `#{mem --total}` will display memory usage as used/total in GB. 25 | 26 | ## Options 27 | 28 | ### `#{cpu}` Placeholder 29 | 30 | - `-i , --interval ` (default `1`): 31 | - `0`: Compares system CPU times elapsed since last call (non-blocking). 32 | - `>0`: Compares system CPU times (seconds) elapsed before and after the interval (blocking). 33 | - `--precpu`: Shows the utilization as a percentage for each CPU. 34 | 35 | For more details, see the documentation of the underlying [psutil library](https://psutil.readthedocs.io/en/latest/#psutil.cpu_percent). 36 | 37 | ### `#{mem}` Placeholder 38 | 39 | - `-t, --total`: Display memory usage as used/total in GB instead of a percentage. 40 | 41 | ### `#{disk}` Placeholder 42 | - `-p , --path `: Specify the path to monitor. Defaults: `C:` for Windows, `/System/Volumes/Data` for Mac, and `/` for Linux. 43 | - `-t, --total`: Display disk usage as used/total in GB instead of a percentage. 44 | - `-f, --free`: Display free disk space in GB. 45 | 46 | ### `#{battery}` Placeholder 47 | - `-t, --time`: Display the remaining battery life time. 48 | - `-p, --percentage`: Display the remaining battery percentage. 49 | - `-l, --long`: Display the remaining battery as a sentence. 50 | - `-c, --compact`: Display the remaining battery using an icon. 51 | - `-f, --fun`: Display the remaining battery in a fun way. 52 | 53 | ## Examples 54 | 55 | ```bash 56 | set -g status-right "#{cpu} | #{mem} | #{disk}" 57 | ``` 58 | 59 | 60 | 61 | ```bash 62 | set -g status-right " CPU: #{cpu} |  MEM: #{mem -t} | 󱛟 DISK: #{disk -t}" 63 | ``` 64 | 65 | 66 | 67 | ```bash 68 | set -g status-right " CPU: #{cpu -i 3} |  MEM: #{mem} | 󱛟 DISK: #{disk -f}" 69 | ``` 70 | 71 | 72 | 73 | ## Why Another Plugin? 74 | 75 | This plugin was created as a personal project to learn more about Tmux plugins and Python scripting. While exploring existing plugins that display CPU and memory usage, I noticed a few limitations that sparked my interest in building something exactly how I wanted it: 76 | 77 | 1. **Predefined Styling:** Many of the plugins I found came with predefined styling that didn't quite match what I was looking for. 78 | 79 | 2. **Missing Metrics:** Either CPU or memory metrics were missing. 80 | 81 | 3. **Lack of Configurability:** I found that other plugins often didn't offer the level of configurability I wanted. For example, I wanted to add icons to the display. 82 | 83 | Overall, I prefer a minimalist approach, where I can simply use placeholders like `#{cpu}` and `#{mem}` with full flexibility to choose how they're presented. 84 | 85 | ## Why Python? 86 | 87 | I chose to write this plugin in Python instead of Shell as part of my learning journey, and because of several practical reasons: 88 | 89 | - **Powerful Libraries:** Python’s [psutil](https://psutil.readthedocs.io/en/latest/#) library provides a wide range of system and process utilities that are easy to use. For example, displaying CPU usage per core (`--precpu`) is much simpler with `psutil` compared to implementing it in a shell script. 90 | 91 | - **Ease of Adaptation:** Working with Python makes it easier for me to adapt and add functionalities. 92 | 93 | - **Simplified Argument Parsing:** Python's built-in `argparse` module makes it straightforward to handle command-line arguments, allowing me to easily add and manage options like `--interval` and `--total`. Additional features of `psutil` can be easily adapted. 94 | 95 | - **Cross-Platform Compatibility:** Python with `psutil`, offers a consistent way to gather system metrics across different operating systems, which avoids dealing with the quirks of different shell environments. 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------