File size: 7,311 Bytes
d5dc912
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# SPDX-License-Identifier: MIT
"""Output formatters for the Byte Agent CLI.

Provides human-readable (tables, key-value, colored) and machine-readable (JSON)
output modes. All output goes through these formatters so the --json flag works
consistently across every command.
"""

from __future__ import annotations

import json
import sys
from datetime import UTC, datetime, timezone
from typing import Any, Final, TextIO

# ---------------------------------------------------------------------------
# ANSI Color Constants
# ---------------------------------------------------------------------------

_SUPPORTS_COLOR: Final[bool] = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()

_RESET: Final[str] = "\033[0m" if _SUPPORTS_COLOR else ""
_BOLD: Final[str] = "\033[1m" if _SUPPORTS_COLOR else ""
_DIM: Final[str] = "\033[2m" if _SUPPORTS_COLOR else ""
_GREEN: Final[str] = "\033[32m" if _SUPPORTS_COLOR else ""
_RED: Final[str] = "\033[31m" if _SUPPORTS_COLOR else ""
_YELLOW: Final[str] = "\033[33m" if _SUPPORTS_COLOR else ""
_CYAN: Final[str] = "\033[36m" if _SUPPORTS_COLOR else ""
_MAGENTA: Final[str] = "\033[35m" if _SUPPORTS_COLOR else ""


# ---------------------------------------------------------------------------
# Primitive Formatters
# ---------------------------------------------------------------------------


def print_success(message: str, *, file: TextIO = sys.stdout) -> None:
    """Print a success message in green."""
    file.write(f"{_GREEN}βœ“{_RESET} {message}\n")
    file.flush()


def print_error(message: str, *, file: TextIO = sys.stderr) -> None:
    """Print an error message in red to stderr."""
    file.write(f"{_RED}βœ—{_RESET} {message}\n")
    file.flush()


def print_warning(message: str, *, file: TextIO = sys.stderr) -> None:
    """Print a warning message in yellow."""
    file.write(f"{_YELLOW}⚠{_RESET} {message}\n")
    file.flush()


def print_info(message: str, *, file: TextIO = sys.stdout) -> None:
    """Print an informational message in cyan."""
    file.write(f"{_CYAN}β„Ή{_RESET} {message}\n")
    file.flush()


def print_header(title: str, *, file: TextIO = sys.stdout) -> None:
    """Print a bold section header."""
    file.write(f"\n{_BOLD}{title}{_RESET}\n")
    file.write(f"{_DIM}{'─' * min(len(title) + 4, 60)}{_RESET}\n")
    file.flush()


# ---------------------------------------------------------------------------
# JSON Output
# ---------------------------------------------------------------------------


def print_json(data: Any, *, file: TextIO = sys.stdout) -> None:
    """Pretty-print data as indented JSON."""
    output = json.dumps(data, indent=2, ensure_ascii=False, default=str)
    file.write(output)
    file.write("\n")
    file.flush()


# ---------------------------------------------------------------------------
# Table Output
# ---------------------------------------------------------------------------


def print_table(
    headers: list[str],
    rows: list[list[str]],
    *,
    file: TextIO = sys.stdout,
    max_col_width: int = 50,
) -> None:
    """Print an ASCII table with auto-sized columns.

    Columns are sized to fit the widest value (capped at max_col_width).
    """
    if not headers:
        return

    # Compute column widths
    col_count = len(headers)
    widths = [len(h) for h in headers]
    for row in rows:
        for i, cell in enumerate(row[:col_count]):
            widths[i] = max(widths[i], len(str(cell)))
    widths = [min(w, max_col_width) for w in widths]

    def _truncate(text: str, width: int) -> str:
        text = str(text)
        if len(text) <= width:
            return text.ljust(width)
        return text[: width - 1] + "…"

    # Header row
    header_line = " β”‚ ".join(_truncate(h, widths[i]) for i, h in enumerate(headers))
    separator = "─┼─".join("─" * w for w in widths)
    file.write(f" {_BOLD}{header_line}{_RESET}\n")
    file.write(f" {_DIM}{separator}{_RESET}\n")

    # Data rows
    if not rows:
        file.write(f" {_DIM}(empty){_RESET}\n")
    else:
        for row in rows:
            padded = [
                _truncate(row[i] if i < len(row) else "", widths[i]) for i in range(col_count)
            ]
            file.write(f" {' β”‚ '.join(padded)}\n")

    file.flush()


# ---------------------------------------------------------------------------
# Key-Value Output
# ---------------------------------------------------------------------------


def print_kv(
    data: dict[str, Any],
    *,
    file: TextIO = sys.stdout,
    label_width: int = 0,
) -> None:
    """Print key-value pairs aligned vertically.

    If label_width is 0, it auto-sizes to the longest key.
    """
    if not data:
        return

    width = label_width or max(len(str(k)) for k in data)
    for key, value in data.items():
        label = f"{_CYAN}{str(key).ljust(width)}{_RESET}"
        formatted_value = _format_value(value)
        file.write(f"  {label}  {formatted_value}\n")
    file.flush()


def _format_value(value: Any) -> str:
    """Format a single value for display with semantic coloring."""
    if value is None:
        return f"{_DIM}β€”{_RESET}"
    if isinstance(value, bool):
        return f"{_GREEN}yes{_RESET}" if value else f"{_RED}no{_RESET}"
    if isinstance(value, list | tuple):
        if not value:
            return f"{_DIM}(none){_RESET}"
        return ", ".join(str(v) for v in value)
    if isinstance(value, dict):
        if not value:
            return f"{_DIM}{{}}{_RESET}"
        return json.dumps(value, ensure_ascii=False, default=str)
    return str(value)


# ---------------------------------------------------------------------------
# Timestamp Formatter
# ---------------------------------------------------------------------------


def format_timestamp(iso_or_float: str | float | None) -> str:
    """Convert an ISO timestamp or Unix float into a human-readable string.

    Returns "β€”" for None/empty values.
    """
    if iso_or_float is None:
        return "β€”"

    if isinstance(iso_or_float, int | float):
        if iso_or_float <= 0:
            return "β€”"
        try:
            dt = datetime.fromtimestamp(iso_or_float, tz=UTC)
            return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
        except (OSError, ValueError, OverflowError):
            return str(iso_or_float)

    text = str(iso_or_float).strip()
    if not text:
        return "β€”"

    # Try to parse ISO format
    for fmt in ("%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S+00:00", "%Y-%m-%dT%H:%M:%S"):
        try:
            dt = datetime.strptime(text, fmt).replace(tzinfo=UTC)
            return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
        except ValueError:
            continue

    return text


# ---------------------------------------------------------------------------
# Dispatcher Helper
# ---------------------------------------------------------------------------


def output(
    data: Any,
    *,
    json_mode: bool = False,
    human_fn: Any = None,
) -> None:
    """Universal output dispatcher.

    In JSON mode: prints raw JSON.
    In human mode: calls the provided human_fn callback, or falls back to print_json.
    """
    if json_mode:
        print_json(data)
    elif callable(human_fn):
        human_fn(data)
    else:
        print_json(data)