diff --git a/src/fabric_cli/utils/fab_ui.py b/src/fabric_cli/utils/fab_ui.py
index 708f7507..8f321eb6 100644
--- a/src/fabric_cli/utils/fab_ui.py
+++ b/src/fabric_cli/utils/fab_ui.py
@@ -3,6 +3,7 @@
import builtins
import html
+import shutil
import sys
import unicodedata
from argparse import Namespace
@@ -135,7 +136,9 @@ def print_output_format(
_print_output_format_result_text(output)
case _:
raise FabricCLIError(
- ErrorMessages.Common.output_format_not_supported(str(format_type)),
+ ErrorMessages.Common.output_format_not_supported(
+ str(format_type)
+ ),
fab_constant.ERROR_NOT_SUPPORTED,
)
@@ -196,7 +199,9 @@ def print_output_error(
return
case _:
raise FabricCLIError(
- ErrorMessages.Common.output_format_not_supported(str(format_type)),
+ ErrorMessages.Common.output_format_not_supported(
+ str(format_type)
+ ),
fab_constant.ERROR_NOT_SUPPORTED,
)
@@ -276,7 +281,13 @@ def print_entries_unix_style(
if header:
widths = [
- max(len(field), max(get_visual_length(entry, field) for entry in _entries))
+ max(
+ len(field),
+ max(
+ get_visual_length(entry, field)
+ for entry in _entries
+ ),
+ )
for field in fields
]
@@ -288,13 +299,35 @@ def print_entries_unix_style(
# Add extra space for better alignment
# Adjust this value for more space if needed
widths = [w + 2 for w in widths]
+
+ # Cap column widths so the total table width fits within the terminal width.
+ # Total visible chars = sum(widths) + (len(widths) - 1) separator spaces.
+ terminal_width = shutil.get_terminal_size((80, 24)).columns
+ total_width = sum(widths) + len(widths) - 1
+ if total_width > terminal_width and widths:
+ min_col_width = 8 # minimum width to fit wrapped text comfortably
+ available = terminal_width - (len(widths) - 1)
+ if available > 0:
+ scale = available / sum(widths)
+ new_widths = [max(min_col_width, int(w * scale)) for w in widths]
+ # Fine-tune: trim the largest column until we fit
+ while sum(new_widths) + len(new_widths) - 1 > terminal_width:
+ max_idx = max(range(len(new_widths)),
+ key=lambda k: new_widths[k])
+ if new_widths[max_idx] <= min_col_width:
+ break
+ new_widths[max_idx] -= 1
+ widths = new_widths
+
if header:
- print_grey(_format_unix_style_field(fields, widths), to_stderr=False)
+ for line in _format_unix_style_field(fields, widths):
+ print_grey(line, to_stderr=False)
# Print a separator line, offset of 1 for each field
print_grey("-" * (sum(widths) + len(widths)), to_stderr=False)
for entry in _entries:
- print_grey(_format_unix_style_entry(entry, fields, widths), to_stderr=False)
+ for line in _format_unix_style_entry(entry, fields, widths):
+ print_grey(line, to_stderr=False)
# Others
@@ -356,7 +389,11 @@ def _print_output_format_result_text(output: FabricCLIOutput) -> None:
):
data_keys = output.result.get_data_keys() if output_result.data else []
if len(data_keys) > 0:
- print_entries_unix_style(output_result.data, data_keys, header=(len(data_keys) > 1 or show_headers))
+ print_entries_unix_style(
+ output_result.data, data_keys, header=(
+ len(data_keys) > 1 or show_headers
+ )
+ )
else:
_print_raw_data(output_result.data)
elif output.show_key_value_list:
@@ -368,23 +405,23 @@ def _print_output_format_result_text(output: FabricCLIOutput) -> None:
print_grey("------------------------------")
_print_raw_data(output_result.hidden_data)
-
if output_result.message:
print_done(f"{output_result.message}\n")
+
def _print_raw_data(data: list[Any], to_stderr: bool = False) -> None:
"""
Print raw data without headers/formatting using appropriate display strategy.
-
+
This function intelligently chooses the output format based on data structure:
- Complex dictionaries (multiple keys or list values) → JSON formatting
- Simple dictionaries (single key-value pairs) → Extract and display values only
- Other data types → Direct string conversion
-
+
Args:
data: List of data items to print
to_stderr: Whether to output to stderr (True) or stdout (False)
-
+
Returns:
None
"""
@@ -402,7 +439,7 @@ def _print_raw_data(data: list[Any], to_stderr: bool = False) -> None:
def _print_dict(data: list[Any], to_stderr: bool) -> None:
"""
Format and print data as pretty-printed JSON.
-
+
Args:
data: Data to format as JSON
to_stderr: Output stream selection
@@ -440,7 +477,9 @@ def _print_error_format_json(output: str) -> None:
def _print_error_format_text(message: str, command: Optional[str] = None) -> None:
command_text = f"{command}: " if command else ""
- _safe_print_formatted_text(f"x {command_text}{message}", message)
+ _safe_print_formatted_text(
+ f"x {command_text}{message}", message
+ )
def _print_fallback(text: str, e: Exception, to_stderr: bool = False) -> None:
@@ -452,32 +491,65 @@ def _print_fallback(text: str, e: Exception, to_stderr: bool = False) -> None:
raise
-def _format_unix_style_field(fields: list[str], widths: list[int]) -> str:
- formatted = ""
- # Dynamically format based on the fields provided
- for i, field in enumerate(fields):
- # Adjust spacing for better alignment
- formatted += f"{field:<{widths[i]}} "
-
- return formatted.strip()
+def _wrap_text(text: str, width: int) -> list[str]:
+ """Wrap text to fit within width visual characters, returning a list of lines."""
+ if width <= 0:
+ return [text]
+ lines: list[str] = []
+ current_line = ""
+ current_width = 0
+ for char in text:
+ char_width = 2 if unicodedata.east_asian_width(char) in [
+ "F", "W"] else 1
+ if current_width + char_width > width:
+ lines.append(current_line)
+ current_line = char
+ current_width = char_width
+ else:
+ current_line += char
+ current_width += char_width
+ if current_line or not lines:
+ lines.append(current_line)
+ return lines
+
+
+def _format_unix_style_field(fields: list[str], widths: list[int]) -> list[str]:
+ # Wrap each header field to fit within its column width
+ cells = [_wrap_text(field, widths[i]) for i, field in enumerate(fields)]
+ num_rows = max(len(cell) for cell in cells) if cells else 1
+ rows = []
+ for row_idx in range(num_rows):
+ formatted = ""
+ for i, cell in enumerate(cells):
+ value = cell[row_idx] if row_idx < len(cell) else ""
+ formatted += f"{value:<{widths[i]}} "
+ rows.append(formatted.rstrip())
+ return rows
def _format_unix_style_entry(
entry: dict[str, str], fields: list[str], widths: list[int]
-) -> str:
- formatted = ""
- # Dynamically format based on the fields provided
- for i, field in enumerate(fields):
- value = str(entry.get(field, ""))
- # Adjust spacing for better alignment
- length = len(value)
- visual_length = _get_visual_length(value)
- if visual_length > length:
- formatted += f"{value:<{widths[i] - (visual_length - length) + 2 }} "
- else:
- formatted += f"{value:<{widths[i]}} "
-
- return formatted.strip()
+) -> list[str]:
+ # Wrap each cell value to fit within its column width
+ cells = [
+ _wrap_text(str(entry.get(field, "")), widths[i])
+ for i, field in enumerate(fields)
+ ]
+ num_rows = max(len(cell) for cell in cells) if cells else 1
+ rows = []
+ for row_idx in range(num_rows):
+ formatted = ""
+ for i, cell in enumerate(cells):
+ value = cell[row_idx] if row_idx < len(cell) else ""
+ visual_length = _get_visual_length(value)
+ length = len(value)
+ # Wide characters require adjusted padding
+ if visual_length > length:
+ formatted += f"{value:<{widths[i] - (visual_length - length)}} "
+ else:
+ formatted += f"{value:<{widths[i]}} "
+ rows.append(formatted.rstrip())
+ return rows
def _get_visual_length(string: str) -> int:
@@ -496,10 +568,10 @@ def _get_visual_length(string: str) -> int:
def _print_entries_key_value_list_style(entries: Any) -> None:
"""Print entries in a key-value list format with formatted keys.
-
+
Args:
entries: Dictionary or list of dictionaries to print
-
+
Example output:
Logged In: true
Account: johndoe@example.com
@@ -526,28 +598,31 @@ def _print_entries_key_value_list_style(entries: Any) -> None:
def _format_key_to_convert_to_title_case(key: str) -> str:
"""Convert a snake_case key to a Title Case name.
-
+
Args:
key: The key to format in snake_case format (e.g. 'user_id', 'account_name')
-
+
Returns:
str: Formatted to title case name (e.g. 'User ID', 'Account Name')
-
+
Raises:
ValueError: If the key is not in the expected underscore-separated format
"""
# Allow letters, numbers, and underscores only
if not key.replace('_', '').replace(' ', '').isalnum():
- raise ValueError(f"Invalid key format: '{key}'. Only underscore-separated words are allowed.")
-
+ raise ValueError(
+ f"Invalid key format: '{key}'. Only underscore-separated words are allowed.")
+
# Check for invalid patterns (camelCase, spaces mixed with underscores, etc.)
if ' ' in key and '_' in key:
- raise ValueError(f"Invalid key format: '{key}'. Only underscore-separated words are allowed.")
-
+ raise ValueError(
+ f"Invalid key format: '{key}'. Only underscore-separated words are allowed.")
+
# Check for camelCase pattern (uppercase letters not at the start)
if any(char.isupper() for char in key[1:]) and '_' not in key:
- raise ValueError(f"Invalid key format: '{key}'. Only underscore-separated words are allowed.")
-
+ raise ValueError(
+ f"Invalid key format: '{key}'. Only underscore-separated words are allowed.")
+
pretty = key.replace('_', ' ').title().strip()
return _check_special_cases(pretty)
@@ -564,4 +639,4 @@ def _check_special_cases(pretty: str) -> str:
for case_key, case_value in special_cases.items():
pretty = pretty.replace(case_key.title(), case_value)
- return pretty
\ No newline at end of file
+ return pretty