Skip to content

Commit 868965b

Browse files
committed
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 <[email protected]>
1 parent 312b521 commit 868965b

File tree

1 file changed

+116
-18
lines changed

1 file changed

+116
-18
lines changed

src/statd/python/cli_pretty/cli_pretty.py

Lines changed: 116 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,6 @@ class PadNtpSource:
189189
poll = 14
190190

191191

192-
class PadService:
193-
name = 16
194-
status = 8
195-
pid = 8
196-
description = 40
197-
198-
199192
class PadWifiScan:
200193
ssid = 40
201194
encryption = 30
@@ -217,6 +210,34 @@ class PadDiskUsage:
217210
avail = 12
218211
percent = 6
219212

213+
214+
def format_memory_bytes(bytes_val):
215+
"""Convert bytes to human-readable format"""
216+
if bytes_val == 0:
217+
return " "
218+
elif bytes_val < 1024:
219+
return f"{bytes_val}B"
220+
elif bytes_val < 1024 * 1024:
221+
return f"{bytes_val // 1024}K"
222+
elif bytes_val < 1024 * 1024 * 1024:
223+
return f"{bytes_val // (1024 * 1024):.1f}M"
224+
else:
225+
return f"{bytes_val // (1024 * 1024 * 1024):.1f}G"
226+
227+
228+
def format_uptime_seconds(seconds):
229+
"""Convert seconds to compact time format"""
230+
if seconds == 0:
231+
return " "
232+
elif seconds < 60:
233+
return f"{seconds}s"
234+
elif seconds < 3600:
235+
return f"{seconds // 60}m"
236+
elif seconds < 86400:
237+
return f"{seconds // 3600}h"
238+
else:
239+
return f"{seconds // 86400}d"
240+
220241
@classmethod
221242
def table_width(cls):
222243
"""Total width of disk usage table"""
@@ -259,6 +280,69 @@ def table_width(cls):
259280
return cls.zone_locked + cls.zone_name + cls.zone_type + cls.zone_data \
260281
+ cls.zone_services
261282

283+
class Column:
284+
"""Column definition for SimpleTable"""
285+
def __init__(self, name, length, align='left', formatter=None):
286+
self.name = name # Header text
287+
self.width = length # Max visible data length (excluding ANSI codes)
288+
self.align = align # 'left' or 'right' (defaults to 'left')
289+
self.formatter = formatter # Optional function to format values
290+
291+
class SimpleTable:
292+
"""Simple table formatter that handles ANSI colors correctly"""
293+
294+
def __init__(self, columns):
295+
self.columns = columns
296+
297+
@staticmethod
298+
def visible_width(text):
299+
"""Return visible character count, excluding ANSI escape sequences"""
300+
ansi_pattern = r'\x1b\[[0-9;]*m'
301+
clean_text = re.sub(ansi_pattern, '', str(text))
302+
return len(clean_text)
303+
304+
def _format_column(self, value, column):
305+
"""Format a single column value with proper alignment
306+
307+
The column length specifies max expected data length.
308+
Framework automatically adds 1 space for column separation.
309+
"""
310+
if column.formatter:
311+
value = column.formatter(value)
312+
313+
value_str = str(value)
314+
visible_len = self.visible_width(value_str)
315+
316+
if column.align == 'right':
317+
padding = column.width - visible_len
318+
return ' ' * max(0, padding) + value_str + ' '
319+
else: # left alignment (default)
320+
padding = column.width - visible_len
321+
return value_str + ' ' * max(0, padding) + ' '
322+
323+
def header(self, styled=True):
324+
"""Generate formatted header row"""
325+
header_parts = []
326+
for column in self.columns:
327+
if column.align == 'right':
328+
header_parts.append(f"{column.name:>{column.width}} ")
329+
else: # left alignment (default)
330+
header_parts.append(f"{column.name:{column.width}} ")
331+
332+
header_str = ''.join(header_parts)
333+
return Decore.invert(header_str) if styled else header_str
334+
335+
def row(self, *values):
336+
"""Generate formatted data row"""
337+
if len(values) != len(self.columns):
338+
raise ValueError(f"Expected {len(self.columns)} values, got {len(values)}")
339+
340+
row_parts = []
341+
for value, column in zip(values, self.columns):
342+
row_parts.append(self._format_column(value, column))
343+
344+
return ''.join(row_parts).rstrip()
345+
262346

263347
class Decore():
264348
@staticmethod
@@ -1661,17 +1745,27 @@ def show_services(json):
16611745
services_data = get_json_data({}, json, 'ietf-system:system-state', 'infix-system:services')
16621746
services = services_data.get("service", [])
16631747

1664-
hdr = (f"{'NAME':<{PadService.name}}"
1665-
f"{'STATUS':<{PadService.status}}"
1666-
f"{'PID':>{PadService.pid -1}}"
1667-
f" {'DESCRIPTION'}")
1668-
print(Decore.invert(hdr))
1748+
# This is the first usage of simple table. I assume this will be
1749+
# copied so I left a lot of comments. If you copy it feel free
1750+
# to be less verbose..
1751+
service_table = SimpleTable([
1752+
Column('NAME', 15, 'left'), # Max service name length
1753+
Column('STATUS', 10, 'left'), # Max status text length (e.g., "running")
1754+
Column('PID', 7, 'right'), # Max PID digits
1755+
Column('MEM', 6, 'right'), # Max memory string (e.g., "123.4M")
1756+
Column('UP', 4, 'right'), # Max uptime string (e.g., "3d")
1757+
Column('RST', 3, 'right'), # Max restart count digits
1758+
Column('DESCRIPTION', 30) # Last column needs no padding
1759+
])
1760+
1761+
print(service_table.header())
16691762

16701763
for svc in services:
16711764
name = svc.get('name', '')
16721765
status = svc.get('status', '')
16731766
pid = svc.get('pid', 0)
16741767
description = svc.get('description', '')
1768+
stats = svc.get('statistics', {})
16751769

16761770
if status in ('running', 'active', 'done'):
16771771
status_str = Decore.green(status)
@@ -1680,13 +1774,17 @@ def show_services(json):
16801774
else:
16811775
status_str = Decore.yellow(status)
16821776

1683-
pid_str = str(pid) if pid > 0 else '-'
1777+
pid_str = str(pid) if pid > 0 else ' '
16841778

1685-
row = f"{name:<{PadService.name}}"
1686-
row += f"{status_str:<{PadService.status + 9}}"
1687-
row += f"{pid_str:>{PadService.pid}}"
1688-
row += f" {description}"
1689-
print(row)
1779+
memory_bytes = int(stats.get('memory-usage', 0))
1780+
uptime_secs = int(stats.get('uptime', 0))
1781+
restart_count = stats.get('restart-count', 0)
1782+
1783+
memory_str = format_memory_bytes(memory_bytes)
1784+
uptime_str = format_uptime_seconds(uptime_secs)
1785+
1786+
print(service_table.row(name, status_str, pid_str, memory_str,
1787+
uptime_str, restart_count, description))
16901788

16911789

16921790
def show_hardware(json):

0 commit comments

Comments
 (0)