Edit on GitHub

sysmon_pytk.cli_sysmon

Command line system monitor.

Usage

usage: cli_sysmon [-h] [-v] [-r TIME] [-l {en,es,de,nb_NO}] [-d | -t | -b | -x]

System monitor: display CPU usage/temperature, memory usage, disk usage

Options

General Option Details
-h, -?, --help show this help message and exit
-v, --version show program's version number and exit
-r TIME, --refresh TIME time between screen refreshes (in seconds, default=1.0)
-l {en,es,de,nb_NO},
--language {en,es,de,nb_NO}
the language to use for display

Display Details

Display Option (choose one) Details
-d, --disk show disk details (default)
-t, --temperature show temperature details
-b, --both show both disk and temperature details
-x, --no-details show no details, only the header

Notes

By default, this program will use the same language as that selected for the GUI application. To override it, use the -l option. To quit, press Ctrl-C.

  1#!/usr/bin/env python3
  2
  3# SPDX-FileCopyrightText: © 2024 Stacey Adams <stacey.belle.rose@gmail.com>
  4# SPDX-License-Identifier: MIT
  5
  6"""
  7Command line system monitor.
  8
  9.. include:: ../docs/CLI_USAGE.md
 10"""
 11
 12import gettext
 13import sys
 14import time
 15from socket import gethostname
 16from typing import NoReturn
 17
 18import psutil
 19from blessings import Terminal
 20
 21from . import _common, about
 22from .app_locale import LANGUAGES, get_translator
 23
 24_ = get_translator()
 25
 26# set up argparse translation
 27gettext.gettext = get_translator(domain="argparse")
 28
 29# Don't move this to the top of the import list! gettext must be imported first
 30# and gettext.gettext must be reassigned for argparse translations to work.
 31
 32import argparse  # noqa: E402 # pylint: disable=C0411,C0413
 33
 34REFRESH_SLEEP = 1.0
 35TOTAL_BLOCKS = 50
 36
 37term = Terminal()
 38
 39# ruff: noqa: T201
 40
 41
 42def _get_usage_color(percent: float) -> str:
 43    if percent > _common.DISK_ALERT_LEVEL:
 44        return term.bright_red
 45    if percent > _common.DISK_WARN_LEVEL:
 46        return term.bright_yellow
 47    return term.bright_green
 48
 49
 50def _cpu_usage() -> str:
 51    percent = psutil.cpu_percent(interval=None)
 52    label = _("CPU Usage") + ": "
 53    details = f"{percent}%"
 54    hilite = _get_usage_color(percent)
 55    return label + hilite + details + term.normal + " "*5
 56
 57
 58def _cpu_temp() -> str:
 59    temperature = _common.cpu_temp()
 60    label = _("Temperature") + ": "
 61    details = f"{temperature}°C"
 62    hilite = _get_usage_color(temperature)
 63    return label + hilite + details + term.normal + " "*5
 64
 65
 66def _mem_usage() -> str:
 67    meminfo = psutil.virtual_memory()
 68    used = _common.bytes2human(meminfo.total - meminfo.available)
 69    total = _common.bytes2human(meminfo.total)
 70    label = _("RAM Usage") + ": "
 71    details = f"{used}/{total} {round(meminfo.percent)}%"
 72    hilite = _get_usage_color(meminfo.percent)
 73    return label + hilite + details + term.normal + " "*5
 74
 75
 76def _swap_usage() -> str:
 77    meminfo = psutil.swap_memory()
 78    used = _common.bytes2human(meminfo.used)
 79    total = _common.bytes2human(meminfo.total)
 80    label = _("Swap Memory") + ": "
 81    details = f"{used}/{total} {round(meminfo.percent)}%"
 82    hilite = _get_usage_color(meminfo.percent)
 83    return label + hilite + details + term.normal + " "*5
 84
 85
 86def _disk_usage(mountpoint: str) -> str:
 87    usage = psutil.disk_usage(mountpoint)
 88    used = _common.bytes2human(usage.used)
 89    total = _common.bytes2human(usage.total)
 90    details = f"{used}/{total} {round(usage.percent)}%"
 91    hilite = _get_usage_color(usage.percent)
 92    return hilite + details + term.normal + " "*5
 93
 94
 95def _disk_usage_rjust(mountpoint: str, width: int) -> str:
 96    usage = psutil.disk_usage(mountpoint)
 97    used = _common.bytes2human(usage.used)
 98    total = _common.bytes2human(usage.total)
 99    details = f"{used}/{total} {round(usage.percent)}%"
100    hilite = _get_usage_color(usage.percent)
101    return hilite + details.rjust(width) + term.normal
102
103
104def _disk_usage_bar(mountpoint: str) -> str:
105    usage = psutil.disk_usage(mountpoint)
106    used_blocks = int(usage.percent * TOTAL_BLOCKS / 100)
107    empty_blocks = TOTAL_BLOCKS - used_blocks
108    details = "█" * used_blocks + "━" * empty_blocks
109    hilite = _get_usage_color(usage.percent)
110    return hilite + details + term.normal + " "*5
111
112
113def _disk_details(*, starting_line: int) -> None:
114    print(
115        term.move(starting_line, 1) + term.black_on_bright_white
116        + _("Disk Usage").upper() + term.normal
117    )
118    for idx, part in enumerate(psutil.disk_partitions()):
119        line = 2*idx + starting_line + 1
120        if line + 1 > term.height:
121            break
122        mountpoint = part.mountpoint
123        print(term.move(line, 0) + " "*(term.width-1))
124        print(term.move(line, 1) + mountpoint + term.el)
125        print(term.move(line + 1, 1) + _disk_usage_bar(mountpoint))
126        print(term.move(line + 1, TOTAL_BLOCKS + 3) + _disk_usage(mountpoint) + term.el)
127    print(term.clear_eos)
128
129
130def _temp_details(*, starting_line: int) -> None:
131    print(
132        term.move(starting_line, 1) + term.black_on_bright_white
133        + _("Temperature Sensors").upper() + term.normal
134    )
135    temps = psutil.sensors_temperatures()
136    line = starting_line + 1
137    for name, entries in temps.items():
138        if line + len(entries) + 1 > term.height:
139            break
140        print(term.move(line, 0) + " " + term.magenta + name + term.normal + term.el)
141        line += 1
142        for entry in entries:
143            tag = (entry.label or name).ljust(20) + " "
144            display = _("{current}°C (high = {high}°C, critical = {critical}°C)").format(
145                current=entry.current, high=entry.high, critical=entry.critical
146            )
147            print(term.move(line, 0) + " "*10 + tag + display + term.el)
148            line += 1
149    print(term.clear_eos)
150
151
152def _blank_below_line(line: int, start_col: int, width: int) -> None:
153    """
154    Blank a rectangular section of the screen with spaces.
155    """
156    for row in range(line, term.height-1):
157        print(term.move(row, start_col) + " "*width)
158
159
160def _disk_details_half(*, starting_line: int) -> None:
161    allowed_width = term.width//2 - 1
162    print(
163        term.move(starting_line, 1) + term.black_on_bright_white
164        + _("Disk Usage").upper() + term.normal
165    )
166    partitions = psutil.disk_partitions()
167    for idx, part in enumerate(partitions):
168        line = 2*idx + starting_line + 1
169        if line + 1 > term.height:
170            break
171        mountpoint = part.mountpoint
172        print(term.move(line, 1) + mountpoint.ljust(allowed_width))
173        print(term.move(line + 1, 1) + _disk_usage_rjust(mountpoint, allowed_width))
174    _blank_below_line(2*len(partitions) + starting_line + 1, 1, allowed_width)
175
176
177def _temp_details_half(*, starting_line: int) -> None:
178    start_col = term.width//2 + 1
179    print(
180        term.move(starting_line, start_col) + term.black_on_bright_white
181        + _("Temperature Sensors").upper() + term.normal
182    )
183    temps = psutil.sensors_temperatures()
184    line = starting_line + 1
185    for name, entries in temps.items():
186        if line + len(entries) + 1 > term.height:
187            break
188        print(term.move(line, start_col) + term.magenta + name + term.normal + term.el)
189        line += 1
190        for entry in entries:
191            label_width = term.width//2 - 7
192            print(
193                term.move(line, start_col) + " "*5
194                + (entry.label or name).ljust(label_width)[:label_width]
195            )
196            print(term.move(line, term.width-11) + f"{entry.current}°C".rjust(10))
197            line += 1
198    _blank_below_line(line, start_col, term.width-start_col)
199
200
201def _monitor_system(args: argparse.Namespace) -> NoReturn:
202    """
203    Monitor the system usage.
204    """
205    app_title = _("System Monitor").upper()
206    while True:
207        print(term.move(0, 1) + term.black_on_bright_white + app_title + term.normal)
208        print(term.move(1, 1) + _("Hostname: {}").format(gethostname()) + term.el)
209        print(term.move(1, 31) + _("IP Address: {}").format(_common.net_addr()) + term.el)
210        print(term.move(2, 1) + _("Processes: {}").format(len(psutil.pids())) + term.el)
211        print(term.move(2, 31) + _("Uptime: {}").format(_common.system_uptime()) + term.el)
212        print(term.move(4, 1) + _cpu_usage())
213        print(term.move(5, 1) + _cpu_temp())
214        print(term.move(4, 31) + _mem_usage())
215        print(term.move(5, 31) + _swap_usage())
216        start = 7
217        if args.details == "d":
218            _disk_details(starting_line=start)
219        elif args.details == "t":
220            _temp_details(starting_line=start)
221        elif args.details == "b":
222            _disk_details_half(starting_line=start)
223            _temp_details_half(starting_line=start)
224        msg = _("<Ctrl-C> to quit")
225        print(
226            term.move(term.height-1, term.width-len(msg)-1)
227            + term.cyan + msg + term.normal + term.move(0, 0)
228        )
229        time.sleep(args.refresh)
230
231
232def _get_parser() -> argparse.Namespace:
233    app_desc = _(
234        "System monitor: display CPU usage/temperature, memory usage, disk usage"
235    )
236    epilog = _(
237        "By default, this program will use the same language as that selected "
238        "for the GUI application. To override it, use the '-l' option. To "
239        "quit, press <Ctrl-C>."
240    )
241    parser = argparse.ArgumentParser(
242        description=app_desc, epilog=epilog, add_help=False
243    )
244    options = parser.add_argument_group(_("Options"))
245    options.add_argument(
246        "-h", "-?", "--help", action="help",
247        help=_("show this help message and exit")
248    )
249    options.add_argument(
250        "-v", "--version", action="version",
251        version=f"{about.__app_name__} {about.__version__}",
252        help=_("show program's version number and exit")
253    )
254    options.add_argument(
255        "-r", "--refresh", type=float, default=REFRESH_SLEEP, metavar="TIME",
256        help=_("time between screen refreshes (in seconds, default=%(default)s)")
257    )
258    options.add_argument(
259        "-l", "--language", choices=LANGUAGES.values(),
260        help=_("the language to use for display")
261    )
262    detail_options = parser.add_argument_group(_("Display Details"))
263    details = detail_options.add_mutually_exclusive_group()
264    details.add_argument(
265        "-d", "--disk", action="store_const", dest="details", const="d",
266        help=_("show disk details (default)")
267    )
268    details.add_argument(
269        "-t", "--temperature", action="store_const", dest="details", const="t",
270        help=_("show temperature details")
271    )
272    details.add_argument(
273        "-b", "--both", action="store_const", dest="details", const="b",
274        help=_("show both disk and temperature details")
275    )
276    details.add_argument(
277        "-x", "--no-details", action="store_const", dest="details", const="",
278        help=_("show no details, only the header")
279    )
280    parser.set_defaults(details="d")
281    return parser.parse_args()
282
283
284def monitor() -> NoReturn:
285    """
286    Entry point for CLI monitor.
287    """
288    args = _get_parser()
289    global _  # noqa: PLW0603 # pylint: disable=global-statement
290    _ = get_translator(forced_lang=args.language)
291    print(term.enter_fullscreen() + term.clear + term.civis)
292    try:
293        _monitor_system(args)
294    except KeyboardInterrupt:
295        print(term.cnorm + term.clear + term.exit_fullscreen())
296        sys.exit(0)
297
298
299if __name__ == "__main__":
300    monitor()
def monitor() -> NoReturn:
285def monitor() -> NoReturn:
286    """
287    Entry point for CLI monitor.
288    """
289    args = _get_parser()
290    global _  # noqa: PLW0603 # pylint: disable=global-statement
291    _ = get_translator(forced_lang=args.language)
292    print(term.enter_fullscreen() + term.clear + term.civis)
293    try:
294        _monitor_system(args)
295    except KeyboardInterrupt:
296        print(term.cnorm + term.clear + term.exit_fullscreen())
297        sys.exit(0)

Entry point for CLI monitor.