Files
Python/topless.py
T

234 lines
7.7 KiB
Python
Raw Normal View History

2025-07-31 14:04:19 -07:00
#!/usr/bin/env python3
2025-08-04 09:28:24 -07:00
# Description: Display top system processes with less mess and more color.
# Usage: python3 topless.py
2025-07-31 14:04:19 -07:00
# Author: Justin Oros
# Source: https://github.com/JustinOros
import os
import time
import psutil
import sys
import select
import argparse
2025-07-31 14:08:27 -07:00
import tty
import termios
2025-08-04 10:36:01 -07:00
from shutil import get_terminal_size
2025-07-31 14:04:19 -07:00
2025-08-04 11:06:48 -07:00
CONFIG_PATH = os.path.expanduser('~/.topless')
DEFAULT_CONFIG = {
'text_color': 'black',
2025-08-04 12:18:03 -07:00
'bar_color': 'gray',
2025-08-04 11:06:48 -07:00
'low_value': 0,
2025-08-04 12:18:03 -07:00
'low_color': 'gray',
2025-08-04 11:06:48 -07:00
'medium_value': 25,
'medium_color': 'yellow',
'medium_high_value': 50,
'medium_high_color': 'orange',
'high_value': 75,
'high_color': 'red',
}
ANSI_COLORS = {
'black': "\033[30m",
'red': "\033[91m",
'green': "\033[92m",
'yellow': "\033[93m",
'blue': "\033[94m",
'magenta': "\033[95m",
'cyan': "\033[96m",
'white': "\033[97m",
'gray': "\033[90m",
'orange': "\033[38;5;214m",
}
ANSI_BG_COLORS = {
'black': "\033[40m",
'red': "\033[41m",
'green': "\033[42m",
'yellow': "\033[43m",
'blue': "\033[44m",
'magenta': "\033[45m",
'cyan': "\033[46m",
'white': "\033[47m",
'gray': "\033[100m",
'orange': "\033[48;5;214m",
}
def load_or_create_config():
if not os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, 'w') as f:
for k, v in DEFAULT_CONFIG.items():
f.write(f"{k}={v}\n")
config = DEFAULT_CONFIG.copy()
try:
with open(CONFIG_PATH, 'r') as f:
for line in f:
line = line.strip()
if not line or '=' not in line or line.startswith('#'):
continue
key, val = line.split('=', 1)
key = key.strip()
val = val.strip()
if key in config:
if key.endswith('_value'):
try:
config[key] = float(val)
except ValueError:
pass
else:
config[key] = val.lower()
except Exception:
pass
return config
2025-07-31 14:08:27 -07:00
def clear_screen():
2025-08-04 10:36:01 -07:00
print("\033[H\033[J", end='')
2025-07-31 14:08:27 -07:00
2025-08-04 10:36:01 -07:00
def get_top_processes(limit=10, sort_key='cpu', reverse=True):
2025-07-31 14:04:19 -07:00
processes = []
2025-08-04 10:36:01 -07:00
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'username']):
2025-07-31 14:04:19 -07:00
try:
2025-08-04 10:36:01 -07:00
pinfo = proc.info
processes.append({
'cpu': pinfo['cpu_percent'] or 0.0,
'mem': pinfo['memory_percent'] or 0.0,
'name': pinfo['name'][:30] if pinfo['name'] else '',
'user': pinfo['username'][:14] if pinfo['username'] else ''
})
2025-07-31 14:04:19 -07:00
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue
2025-08-04 10:36:01 -07:00
return sorted(processes, key=lambda x: x[sort_key], reverse=reverse)[:limit]
2025-08-04 11:06:48 -07:00
def colorize(cpu, mem, name, user, config, use_color=True, line_number=None, width=0):
if not use_color:
2025-08-04 10:36:01 -07:00
return f"{str(line_number).rjust(width)} {cpu:5.1f} {mem:5.1f} {name:35} {user:15}" if line_number else f"{cpu:5.1f} {mem:5.1f} {name:35} {user:15}"
2025-08-04 11:06:48 -07:00
if cpu >= config['high_value']:
color = ANSI_COLORS.get(config['high_color'], "\033[91m")
elif cpu >= config['medium_high_value']:
color = ANSI_COLORS.get(config['medium_high_color'], "\033[38;5;214m")
elif cpu >= config['medium_value']:
color = ANSI_COLORS.get(config['medium_color'], "\033[93m")
elif cpu >= config['low_value']:
2025-08-04 11:59:49 -07:00
color = ANSI_COLORS.get(config['low_color'], "\033[90m")
2025-07-31 14:27:49 -07:00
else:
2025-08-04 11:06:48 -07:00
color = ANSI_COLORS.get(config['text_color'], "\033[30m")
2025-08-04 10:36:01 -07:00
RESET = "\033[0m"
line = f"{cpu:5.1f} {mem:5.1f} {name:35} {user:15}"
if line_number is not None:
return f"{color}{str(line_number).rjust(width)} {line}{RESET}"
return f"{color}{line}{RESET}"
2025-07-31 14:04:19 -07:00
2025-08-04 11:06:48 -07:00
def print_processes(limit, sort_key='cpu', reverse=True, show_quit_hint=True, use_color=True, show_line_numbers=False, config=None):
if config is None:
config = DEFAULT_CONFIG
2025-07-31 14:08:27 -07:00
clear_screen()
top_processes = get_top_processes(limit, sort_key=sort_key, reverse=reverse)
2025-07-31 16:20:51 -07:00
2025-08-04 11:06:48 -07:00
BG_GRAY = ANSI_BG_COLORS.get(config['bar_color'], "\033[100m") if use_color else ""
BLACK_TEXT = ANSI_COLORS.get(config['text_color'], "\033[30m") if use_color else ""
ARROW_UP = "↑"
ARROW_DOWN = "↓"
arrow = ARROW_DOWN if reverse else ARROW_UP
def col_label(col_key, label, width):
2025-08-04 11:59:49 -07:00
return f"{arrow if sort_key == col_key else ' '} {label}".ljust(width)
2025-08-04 11:06:48 -07:00
cpu_col = col_label('cpu', "CPU", 6)
mem_col = col_label('mem', "MEM", 6)
2025-08-04 12:07:33 -07:00
name_col = col_label('name', "PROC", 35)
user_col = col_label('user', "USER", 15)
2025-07-31 16:25:11 -07:00
line_prefix = "Ln " if show_line_numbers else ""
header = f"{line_prefix}{cpu_col} {mem_col} {name_col} {user_col}"
2025-08-04 09:28:24 -07:00
2025-08-04 11:59:49 -07:00
term_width = get_terminal_size().columns
block_width = len(header)
padding = max((term_width - block_width) // 2, 0)
pad = ' ' * padding
print(f"{pad}{BG_GRAY}{BLACK_TEXT}{header}\033[0m")
2025-07-31 16:16:03 -07:00
2025-07-31 16:20:51 -07:00
width = len(str(limit))
2025-08-04 12:18:03 -07:00
line_number_fmt = f"{{:0{width}d}}"
for i, proc in enumerate(top_processes, start=1):
cpu = proc['cpu']
mem = proc['mem']
name = proc['name']
user = proc['user']
if show_line_numbers:
2025-08-04 12:18:03 -07:00
formatted_line_number = line_number_fmt.format(i)
line = colorize(cpu, mem, name, user, config, use_color, line_number=formatted_line_number, width=width)
else:
2025-08-04 11:06:48 -07:00
line = colorize(cpu, mem, name, user, config, use_color)
2025-08-04 11:59:49 -07:00
print(f"{pad}{line}")
2025-07-31 17:08:29 -07:00
2025-08-04 12:30:26 -07:00
bottom_bar = "PRESS [C]PU [M]EM [P]ROC [U]SER TO SORT OR [Q]UIT."
2025-08-04 11:59:49 -07:00
footer = bottom_bar if show_quit_hint else ' '
2025-08-04 12:30:26 -07:00
centered_footer = footer.center(block_width)
print(f"{pad}{BG_GRAY}{BLACK_TEXT}{centered_footer}\033[0m")
2025-07-31 14:08:27 -07:00
2025-08-04 10:36:01 -07:00
def timed_input(timeout=0.1):
2025-07-31 14:08:27 -07:00
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setcbreak(fd)
2025-08-04 10:36:01 -07:00
rlist, _, _ = select.select([fd], [], [], timeout)
if rlist:
2025-07-31 14:08:27 -07:00
return sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return None
2025-07-31 14:04:19 -07:00
def main():
2025-08-04 10:36:01 -07:00
parser = argparse.ArgumentParser()
parser.add_argument('-n', type=int, help="Number of processes to display")
parser.add_argument('--no-color', action='store_true', help="Disable color output")
2025-08-04 12:30:26 -07:00
parser.add_argument('-l', '--lines', action='store_true', help="Show line numbers")
2025-07-31 14:04:19 -07:00
args = parser.parse_args()
2025-08-04 11:06:48 -07:00
config = load_or_create_config()
2025-08-04 10:36:01 -07:00
use_color = not args.no_color
show_line_numbers = args.lines
term_height = get_terminal_size().lines
limit = args.n if args.n else max(5, term_height - 10)
sort_key = 'cpu'
reverse = True
refresh_count = 0
start_time = time.time()
2025-08-04 10:36:01 -07:00
while True:
elapsed = time.time() - start_time
show_quit = True
2025-08-04 11:06:48 -07:00
print_processes(limit, sort_key, reverse, show_quit, use_color, show_line_numbers, config=config)
2025-08-04 10:36:01 -07:00
key = timed_input(1)
refresh_count += 1
if key:
key = key.lower()
if key == 'q':
break
elif key == 'c':
reverse = not reverse if sort_key == 'cpu' else True
sort_key = 'cpu'
elif key == 'm':
reverse = not reverse if sort_key == 'mem' else True
sort_key = 'mem'
elif key == 'p':
reverse = not reverse if sort_key == 'name' else True
sort_key = 'name'
elif key == 'u':
reverse = not reverse if sort_key == 'user' else True
sort_key = 'user'
elif key == 'r':
reverse = not reverse
2025-07-31 14:04:19 -07:00
if __name__ == "__main__":
main()