diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index c85a1db8..f74a4d75 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -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):