mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-23 09:23:02 +02:00
cli: print service statistics using new table framework
Replace manual f-string formatting with SimpleTable/Column classes. This new "framework" handles ANSI colors and padding. Removing the hassle of manually calculating padding. You simply specify the header with max number of chars the data can be and if you want left/right padding and the "framework" calculates the padding for you. We use this new "framework" to pretty print the newly added services statistics. Signed-off-by: Richard Alpe <richard@bit42.se>
This commit is contained in:
@@ -190,13 +190,6 @@ class PadNtpSource:
|
||||
poll = 14
|
||||
|
||||
|
||||
class PadService:
|
||||
name = 16
|
||||
status = 8
|
||||
pid = 8
|
||||
description = 40
|
||||
|
||||
|
||||
class PadWifiScan:
|
||||
ssid = 40
|
||||
encryption = 30
|
||||
@@ -218,6 +211,34 @@ class PadDiskUsage:
|
||||
avail = 12
|
||||
percent = 6
|
||||
|
||||
|
||||
def format_memory_bytes(bytes_val):
|
||||
"""Convert bytes to human-readable format"""
|
||||
if bytes_val == 0:
|
||||
return " "
|
||||
elif bytes_val < 1024:
|
||||
return f"{bytes_val}B"
|
||||
elif bytes_val < 1024 * 1024:
|
||||
return f"{bytes_val // 1024}K"
|
||||
elif bytes_val < 1024 * 1024 * 1024:
|
||||
return f"{bytes_val // (1024 * 1024):.1f}M"
|
||||
else:
|
||||
return f"{bytes_val // (1024 * 1024 * 1024):.1f}G"
|
||||
|
||||
|
||||
def format_uptime_seconds(seconds):
|
||||
"""Convert seconds to compact time format"""
|
||||
if seconds == 0:
|
||||
return " "
|
||||
elif seconds < 60:
|
||||
return f"{seconds}s"
|
||||
elif seconds < 3600:
|
||||
return f"{seconds // 60}m"
|
||||
elif seconds < 86400:
|
||||
return f"{seconds // 3600}h"
|
||||
else:
|
||||
return f"{seconds // 86400}d"
|
||||
|
||||
@classmethod
|
||||
def table_width(cls):
|
||||
"""Total width of disk usage table"""
|
||||
@@ -260,6 +281,69 @@ class PadFirewall:
|
||||
return cls.zone_locked + cls.zone_name + cls.zone_type + cls.zone_data \
|
||||
+ cls.zone_services
|
||||
|
||||
class Column:
|
||||
"""Column definition for SimpleTable"""
|
||||
def __init__(self, name, length, align='left', formatter=None):
|
||||
self.name = name # Header text
|
||||
self.width = length # Max visible data length (excluding ANSI codes)
|
||||
self.align = align # 'left' or 'right' (defaults to 'left')
|
||||
self.formatter = formatter # Optional function to format values
|
||||
|
||||
class SimpleTable:
|
||||
"""Simple table formatter that handles ANSI colors correctly"""
|
||||
|
||||
def __init__(self, columns):
|
||||
self.columns = columns
|
||||
|
||||
@staticmethod
|
||||
def visible_width(text):
|
||||
"""Return visible character count, excluding ANSI escape sequences"""
|
||||
ansi_pattern = r'\x1b\[[0-9;]*m'
|
||||
clean_text = re.sub(ansi_pattern, '', str(text))
|
||||
return len(clean_text)
|
||||
|
||||
def _format_column(self, value, column):
|
||||
"""Format a single column value with proper alignment
|
||||
|
||||
The column length specifies max expected data length.
|
||||
Framework automatically adds 1 space for column separation.
|
||||
"""
|
||||
if column.formatter:
|
||||
value = column.formatter(value)
|
||||
|
||||
value_str = str(value)
|
||||
visible_len = self.visible_width(value_str)
|
||||
|
||||
if column.align == 'right':
|
||||
padding = column.width - visible_len
|
||||
return ' ' * max(0, padding) + value_str + ' '
|
||||
else: # left alignment (default)
|
||||
padding = column.width - visible_len
|
||||
return value_str + ' ' * max(0, padding) + ' '
|
||||
|
||||
def header(self, styled=True):
|
||||
"""Generate formatted header row"""
|
||||
header_parts = []
|
||||
for column in self.columns:
|
||||
if column.align == 'right':
|
||||
header_parts.append(f"{column.name:>{column.width}} ")
|
||||
else: # left alignment (default)
|
||||
header_parts.append(f"{column.name:{column.width}} ")
|
||||
|
||||
header_str = ''.join(header_parts)
|
||||
return Decore.invert(header_str) if styled else header_str
|
||||
|
||||
def row(self, *values):
|
||||
"""Generate formatted data row"""
|
||||
if len(values) != len(self.columns):
|
||||
raise ValueError(f"Expected {len(self.columns)} values, got {len(values)}")
|
||||
|
||||
row_parts = []
|
||||
for value, column in zip(values, self.columns):
|
||||
row_parts.append(self._format_column(value, column))
|
||||
|
||||
return ''.join(row_parts).rstrip()
|
||||
|
||||
|
||||
class Decore():
|
||||
@staticmethod
|
||||
@@ -1693,17 +1777,27 @@ def show_services(json):
|
||||
services_data = get_json_data({}, json, 'ietf-system:system-state', 'infix-system:services')
|
||||
services = services_data.get("service", [])
|
||||
|
||||
hdr = (f"{'NAME':<{PadService.name}}"
|
||||
f"{'STATUS':<{PadService.status}}"
|
||||
f"{'PID':>{PadService.pid -1}}"
|
||||
f" {'DESCRIPTION'}")
|
||||
print(Decore.invert(hdr))
|
||||
# This is the first usage of simple table. I assume this will be
|
||||
# copied so I left a lot of comments. If you copy it feel free
|
||||
# to be less verbose..
|
||||
service_table = SimpleTable([
|
||||
Column('NAME', 15, 'left'), # Max service name length
|
||||
Column('STATUS', 10, 'left'), # Max status text length (e.g., "running")
|
||||
Column('PID', 7, 'right'), # Max PID digits
|
||||
Column('MEM', 6, 'right'), # Max memory string (e.g., "123.4M")
|
||||
Column('UP', 4, 'right'), # Max uptime string (e.g., "3d")
|
||||
Column('RST', 3, 'right'), # Max restart count digits
|
||||
Column('DESCRIPTION', 30) # Last column needs no padding
|
||||
])
|
||||
|
||||
print(service_table.header())
|
||||
|
||||
for svc in services:
|
||||
name = svc.get('name', '')
|
||||
status = svc.get('status', '')
|
||||
pid = svc.get('pid', 0)
|
||||
description = svc.get('description', '')
|
||||
stats = svc.get('statistics', {})
|
||||
|
||||
if status in ('running', 'active', 'done'):
|
||||
status_str = Decore.green(status)
|
||||
@@ -1712,13 +1806,17 @@ def show_services(json):
|
||||
else:
|
||||
status_str = Decore.yellow(status)
|
||||
|
||||
pid_str = str(pid) if pid > 0 else '-'
|
||||
pid_str = str(pid) if pid > 0 else ' '
|
||||
|
||||
row = f"{name:<{PadService.name}}"
|
||||
row += f"{status_str:<{PadService.status + 9}}"
|
||||
row += f"{pid_str:>{PadService.pid}}"
|
||||
row += f" {description}"
|
||||
print(row)
|
||||
memory_bytes = int(stats.get('memory-usage', 0))
|
||||
uptime_secs = int(stats.get('uptime', 0))
|
||||
restart_count = stats.get('restart-count', 0)
|
||||
|
||||
memory_str = format_memory_bytes(memory_bytes)
|
||||
uptime_str = format_uptime_seconds(uptime_secs)
|
||||
|
||||
print(service_table.row(name, status_str, pid_str, memory_str,
|
||||
uptime_str, restart_count, description))
|
||||
|
||||
|
||||
def show_hardware(json):
|
||||
|
||||
Reference in New Issue
Block a user