Skip to content

Commit b826bcb

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 f653cac commit b826bcb

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
@@ -190,13 +190,6 @@ class PadNtpSource:
190190
poll = 14
191191

192192

193-
class PadService:
194-
name = 16
195-
status = 8
196-
pid = 8
197-
description = 40
198-
199-
200193
class PadWifiScan:
201194
ssid = 40
202195
encryption = 30
@@ -218,6 +211,34 @@ class PadDiskUsage:
218211
avail = 12
219212
percent = 6
220213

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

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

264348
class Decore():
265349
@staticmethod
@@ -1693,17 +1777,27 @@ def show_services(json):
16931777
services_data = get_json_data({}, json, 'ietf-system:system-state', 'infix-system:services')
16941778
services = services_data.get("service", [])
16951779

1696-
hdr = (f"{'NAME':<{PadService.name}}"
1697-
f"{'STATUS':<{PadService.status}}"
1698-
f"{'PID':>{PadService.pid -1}}"
1699-
f" {'DESCRIPTION'}")
1700-
print(Decore.invert(hdr))
1780+
# This is the first usage of simple table. I assume this will be
1781+
# copied so I left a lot of comments. If you copy it feel free
1782+
# to be less verbose..
1783+
service_table = SimpleTable([
1784+
Column('NAME', 15, 'left'), # Max service name length
1785+
Column('STATUS', 10, 'left'), # Max status text length (e.g., "running")
1786+
Column('PID', 7, 'right'), # Max PID digits
1787+
Column('MEM', 6, 'right'), # Max memory string (e.g., "123.4M")
1788+
Column('UP', 4, 'right'), # Max uptime string (e.g., "3d")
1789+
Column('RST', 3, 'right'), # Max restart count digits
1790+
Column('DESCRIPTION', 30) # Last column needs no padding
1791+
])
1792+
1793+
print(service_table.header())
17011794

17021795
for svc in services:
17031796
name = svc.get('name', '')
17041797
status = svc.get('status', '')
17051798
pid = svc.get('pid', 0)
17061799
description = svc.get('description', '')
1800+
stats = svc.get('statistics', {})
17071801

17081802
if status in ('running', 'active', 'done'):
17091803
status_str = Decore.green(status)
@@ -1712,13 +1806,17 @@ def show_services(json):
17121806
else:
17131807
status_str = Decore.yellow(status)
17141808

1715-
pid_str = str(pid) if pid > 0 else '-'
1809+
pid_str = str(pid) if pid > 0 else ' '
17161810

1717-
row = f"{name:<{PadService.name}}"
1718-
row += f"{status_str:<{PadService.status + 9}}"
1719-
row += f"{pid_str:>{PadService.pid}}"
1720-
row += f" {description}"
1721-
print(row)
1811+
memory_bytes = int(stats.get('memory-usage', 0))
1812+
uptime_secs = int(stats.get('uptime', 0))
1813+
restart_count = stats.get('restart-count', 0)
1814+
1815+
memory_str = format_memory_bytes(memory_bytes)
1816+
uptime_str = format_uptime_seconds(uptime_secs)
1817+
1818+
print(service_table.row(name, status_str, pid_str, memory_str,
1819+
uptime_str, restart_count, description))
17221820

17231821

17241822
def show_hardware(json):

0 commit comments

Comments
 (0)