"""Classic-CLI rendering helpers: reasoning-tag stripping, ANSI/skin colours, light-mode detection, markdown/final-content rendering, output-history replay, ``_cprint`` and the panel box/wrap helpers. Split out of ``cli.py``; ``cli`` re-exports every public name and moved bodies late-bind cli-level names through ``from cli import ...`` at call time so facade monkeypatch seams hold. """ from __future__ import annotations import functools import os import re import shutil import sys import textwrap import time from contextlib import contextmanager, suppress from hermes_cli.banner import format_banner_version_label from rich.console import Console from rich.text import Text as _RichText from typing import Any def _cli(): """Late import of the ``cli`` facade: mutable CLI module state (and its test seams) lives there.""" import cli return cli _REASONING_TAGS = ("REASONING_SCRATCHPAD", "think", "thinking", "reasoning", "thought") _TOOL_CALL_TAGS = ("tool_call", "tool_calls", "tool_result", "function_call", "function_calls") def _strip_reasoning_tags(text: str) -> str: """Strip reasoning blocks (closed, unterminated, orphan-close) and leaked tool-call XML from display text. Keep in sync with ``agent.agent_runtime_helpers.strip_think_blocks`` and the stream consumer's think-tag sets. Also strips tool-call XML blocks some open models leak into visible content (````, ````, Gemma-style ``…``). Ported from openclaw/openclaw#67318. """ from cli import _REASONING_TAGS, _TOOL_CALL_TAGS cleaned = text for tag in _REASONING_TAGS: cleaned = re.sub(rf"<{tag}>.*?\s*", "", cleaned, flags=re.DOTALL | re.IGNORECASE) cleaned = re.sub(rf"<{tag}>.*$", "", cleaned, flags=re.DOTALL | re.IGNORECASE) cleaned = re.sub(rf"\s*", "", cleaned, flags=re.IGNORECASE) for tc_tag in _TOOL_CALL_TAGS: cleaned = re.sub( rf"<(?:[\w.-]+:)?{tc_tag}\b[^>]*>.*?\s*", "", cleaned, flags=re.DOTALL | re.IGNORECASE, ) # — boundary + attribute gated to avoid prose false positives. cleaned = re.sub( r'(?:(?<=^)|(?<=[\n\r.!?:]))[ \t]*]*\bname\s*=[^>]*>(?:(?:(?!).)*)\s*', '', cleaned, flags=re.DOTALL | re.IGNORECASE, ) cleaned = re.sub( r'\s*', '', cleaned, flags=re.IGNORECASE, ) # Unterminated opener / stray / markup = stream cut # mid tool-call serialization (#101899); strip to end of text. cleaned = re.sub( r'(?:^|\n)[ \t]*<(?:[\w.-]+:)?(?:tool_call|tool_calls|tool_result|function_call|function_calls)\b[^>]*>.*$' r'|(?:^|\n)[^\n<]* str: if content is None: return "" if isinstance(content, str): return content if isinstance(content, list): parts = [str(part.get("text", "")) for part in content if isinstance(part, dict) and part.get("type") == "text"] return "\n".join(p for p in parts if p) return str(content) def _assistant_copy_text(content: Any) -> str: from cli import _assistant_content_as_text, _strip_reasoning_tags return _strip_reasoning_tags(_assistant_content_as_text(content)) _ACCENT_ANSI_DEFAULT = "\033[1;38;2;255;215;0m" # #FFD700 bold fallback _BOLD = "\033[1m" _RST = "\033[0m" _STREAM_PAD = "" # no indent: leading whitespace pollutes copy/paste _STREAM_PARTIAL_PREVIEW_LEN = 60 # tail of an unfinished line mirrored into the spinner def _hex_to_ansi(hex_color: str, *, bold: bool = False) -> str: """Convert '#RRGGBB' to a true-color ANSI escape, remapping dark-tuned colors in light mode.""" from cli import _ACCENT_ANSI_DEFAULT, _maybe_remap_for_light_mode hex_color = _maybe_remap_for_light_mode(hex_color) try: r, g, b = (int(hex_color[i:i + 2], 16) for i in (1, 3, 5)) return f"\033[{'1;' if bold else ''}38;2;{r};{g};{b}m" except (ValueError, IndexError): return _ACCENT_ANSI_DEFAULT if bold else "\033[38;2;184;134;11m" _TRUE_RE = re.compile(r"^(1|true|on|yes|y)$") _FALSE_RE = re.compile(r"^(0|false|off|no|n)$") _LIGHT_DEFAULT_TERM_PROGRAMS = frozenset() # Apple_Terminal isn't reliable; require explicit config def _luminance_from_hex(hex_str: str) -> float | None: """Rec.709 luma in [0, 1] for '#RGB'/'#RRGGBB', or None when malformed.""" s = (hex_str or "").strip().lstrip("#") if len(s) == 3: s = "".join(c * 2 for c in s) if len(s) != 6 or not all(c in "0123456789abcdefABCDEF" for c in s): return None try: r, g, b = int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16) except ValueError: return None return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255.0 _DA1_REPLY_RE = re.compile(rb"\x1b\[\?[0-9;]*c") def _query_osc11_background() -> str | None: """Terminal background via OSC 11 as "#RRGGBB", or None. Fenced with a DA1 sentinel (``ESC[c``): terminals answer in order and virtually all answer DA1, so its reply proves our OSC 11 was processed — otherwise a late reply leaks into prompt_toolkit's stdin as typed text. Skipped over SSH (round-trip too slow; a late BEL reads as Ctrl+G). A 50 ms drain after TCSAFLUSH catches stragglers. After the main read + TCSAFLUSH, a short drain window (50 ms) catches late-arriving bytes that slipped past the flush — a race observed on VPS and container terminals under load (#40250). """ from cli import _DA1_REPLY_RE if not sys.stdin.isatty() or not sys.stdout.isatty(): return None if any(os.environ.get(v) for v in ("SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY")): return None try: import select import termios import tty fd = sys.stdin.fileno() old = termios.tcgetattr(fd) except Exception: return None try: try: tty.setcbreak(fd) except Exception: return None try: # One write so the OSC 11 query and DA1 fence cannot reorder. sys.stdout.write("\x1b]11;?\x1b\\\x1b[c") sys.stdout.flush() except Exception: return None # Read until the DA1 fence closes; the 1s deadline only covers terminals ignoring DA1. deadline = time.monotonic() + 1.0 buf = b"" while time.monotonic() < deadline: r, _, _ = select.select([fd], [], [], deadline - time.monotonic()) if not r: continue try: chunk = os.read(fd, 64) except OSError: break if not chunk: break buf += chunk if _DA1_REPLY_RE.search(buf): break # Reply: \x1b]11;rgb:RRRR/GGGG/BBBB\x1b\\ — components are 1-4 hex digits. m = re.search(rb"rgb:([0-9a-fA-F]+)/([0-9a-fA-F]+)/([0-9a-fA-F]+)", buf) if not m: return None def norm(h: bytes) -> int: v = int(h, 16) bits = len(h) * 4 return (v * 255) // ((1 << bits) - 1) if bits else 0 r, g, b = norm(m.group(1)), norm(m.group(2)), norm(m.group(3)) return f"#{r:02X}{g:02X}{b:02X}" finally: # TCSAFLUSH discards unread input, scrubbing a partial reply before prompt_toolkit reads it. with suppress(Exception): termios.tcsetattr(fd, termios.TCSAFLUSH, old) try: drain_deadline = time.monotonic() + 0.05 while time.monotonic() < drain_deadline: r, _, _ = select.select([fd], [], [], drain_deadline - time.monotonic()) if not r or not os.read(fd, 64): break except Exception: pass def _heal_cooked_mode_drift(fd: int) -> bool: """Re-apply raw mode on *fd* when termios drifted back to cooked (POSIX only). A lost ``run_in_terminal`` cooked_mode() restore makes the kernel line-buffer every keystroke and the CLI looks dead. Mirrors prompt_toolkit's raw_mode flag surgery in place. Returns True when healed; False when already raw or not inspectable. """ try: import termios attrs = termios.tcgetattr(fd) except Exception: return False lflag = attrs[3] if not (lflag & (termios.ICANON | termios.ECHO)): return False # still raw — nothing to do attrs[3] = lflag & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG) attrs[0] = attrs[0] & ~(termios.IXON | termios.IXOFF | termios.ICRNL | termios.INLCR | termios.IGNCR) attrs[6][termios.VMIN] = 1 try: termios.tcsetattr(fd, termios.TCSANOW, attrs) except Exception: return False return True def _detect_light_mode_uncached() -> bool: """The detection ladder documented above; may raise (caller maps errors to dark).""" from cli import _FALSE_RE, _LIGHT_DEFAULT_TERM_PROGRAMS, _TRUE_RE, _luminance_from_hex, _query_osc11_background for var in ("HERMES_LIGHT", "HERMES_TUI_LIGHT"): v = (os.environ.get(var) or "").strip().lower() if _TRUE_RE.match(v): return True if _FALSE_RE.match(v): return False theme = (os.environ.get("HERMES_TUI_THEME") or "").strip().lower() if theme == "light": return True if theme == "dark": return False bg_lum = _luminance_from_hex(os.environ.get("HERMES_TUI_BACKGROUND") or "") if bg_lum is not None: return bg_lum >= 0.5 last = (os.environ.get("COLORFGBG") or "").strip().split(";")[-1] if last.isdigit() and 0 <= int(last) < 16: return int(last) in {7, 15} bg_color = _query_osc11_background() if bg_color: lum = _luminance_from_hex(bg_color) if lum is not None: return lum >= 0.5 return (os.environ.get("TERM_PROGRAM") or "").strip() in _LIGHT_DEFAULT_TERM_PROGRAMS # Light-mode equivalents of skin colors unreadable on cream backgrounds. Only colors used # as STANDALONE foregrounds: ones paired with a dark bg (status bar text on #1a1a2e) would # become invisible the other direction, hence #C0C0C0/#888888/#555555/#8B8682 are skipped. _LIGHT_MODE_REMAP: dict[str, str] = { "#FFF8DC": "#1A1A1A", "#FFD700": "#9A6B00", "#FFBF00": "#8A5A00", "#B8860B": "#5C4500", "#DAA520": "#6B4F00", "#F1E6CF": "#1A1A1A", "#c9d1d9": "#24292F", "#EAF7FF": "#0F1B26", "#F5F5F5": "#1A1A1A", "#FFF0D4": "#1A1A1A", "#CD7F32": "#8A4F1A", "#FFEFB5": "#3A2A00", } _LIGHT_MODE_REMAP_UPPER = {k.upper(): v for k, v in _LIGHT_MODE_REMAP.items()} def _maybe_remap_for_light_mode(hex_color: str) -> str: """In light mode, remap a dark-tuned color to its higher-contrast equivalent.""" from cli import _LIGHT_MODE_REMAP_UPPER, _detect_light_mode if not _detect_light_mode(): return hex_color if not hex_color or not hex_color.startswith("#"): return hex_color return _LIGHT_MODE_REMAP_UPPER.get(hex_color.upper(), hex_color) def _install_skin_light_mode_hook() -> None: """Wrap SkinConfig.get_color so EVERY skin color read goes through the light-mode remap. Idempotent.""" from cli import _maybe_remap_for_light_mode try: from hermes_cli.skin_engine import SkinConfig # type: ignore[import] except Exception: return if getattr(SkinConfig, "_hermes_light_mode_hook_installed", False): return _orig_get_color = SkinConfig.get_color def _wrapped_get_color(self, key, fallback=""): value = _orig_get_color(self, key, fallback) try: return _maybe_remap_for_light_mode(value) except Exception: return value SkinConfig.get_color = _wrapped_get_color # type: ignore[method-assign] SkinConfig._hermes_light_mode_hook_installed = True # type: ignore[attr-defined] class _SkinAwareAnsi: """Lazy ANSI escape resolved from the skin on first use; ``.reset()`` after a ``/skin`` switch.""" def __init__(self, skin_key: str, fallback_hex: str = "#FFD700", *, bold: bool = False): self._skin_key = skin_key self._fallback_hex = fallback_hex self._bold = bold self._cached: str | None = None def __str__(self) -> str: from cli import _hex_to_ansi if self._cached is None: try: from hermes_cli.skin_engine import get_active_skin self._cached = _hex_to_ansi( get_active_skin().get_color(self._skin_key, self._fallback_hex), bold=self._bold, ) except Exception: self._cached = _hex_to_ansi(self._fallback_hex, bold=self._bold) return self._cached def __add__(self, other: str) -> str: return str(self) + other def __radd__(self, other: str) -> str: return other + str(self) def reset(self) -> None: """Clear cache so the next access re-reads the skin.""" self._cached = None _ACCENT = _SkinAwareAnsi("response_border", "#FFD700", bold=True) # dim+italic attributes (not a hex) so dim text inherits the terminal foreground in both modes. _DIM = "\x1b[2;3m" def _tty_wrap(s: str, sgr: str) -> str: """Wrap *s* in an SGR attribute when stdout is a real TTY; plain text otherwise.""" try: return f"{sgr}{s}\x1b[0m" if sys.stdout.isatty() else str(s) except Exception: return str(s) _b = functools.partial(_tty_wrap, sgr="\x1b[1m") # bold when stdout is a real TTY _d = functools.partial(_tty_wrap, sgr="\x1b[2;3m") # dim-italic when stdout is a real TTY def _accent_hex() -> str: """Return the active skin accent color for legacy CLI output lines.""" try: from hermes_cli.skin_engine import get_active_skin return get_active_skin().get_color("ui_accent", "#FFBF00") except Exception: return "#FFBF00" def _rich_text_from_ansi(text: str) -> _RichText: """Rich Text from ANSI output; literal ``[brackets]`` are not treated as markup.""" return _RichText.from_ansi(text or "") def _strip_markdown_syntax(text: str) -> str: """Best-effort markdown marker removal for plain-text display.""" from cli import _rich_text_from_ansi plain = _rich_text_from_ansi(text or "").plain # HR markers: "-"/"_" runs of 3+, but "*" only when exactly 3 (cron schedules "* * * * *"). plain = re.sub(r"^\s{0,3}(?:[-_]\s*){3,}$", "", plain, flags=re.MULTILINE) plain = re.sub(r"^\s{0,3}(?:\*\s*){3}\s*$", "", plain, flags=re.MULTILINE) plain = re.sub(r"^\s{0,3}#{1,6}\s+", "", plain, flags=re.MULTILINE) # Blockquotes, lists, and checkboxes are preserved because they carry structure. plain = re.sub(r"(```+|~~~+)", "", plain) plain = re.sub(r"`([^`]*)`", r"\1", plain) plain = re.sub(r"!\[([^\]]*)\]\([^\)]*\)", r"\1", plain) plain = re.sub(r"\[([^\]]+)\]\([^\)]*\)", r"\1", plain) plain = re.sub(r"\*\*\*([^*]+)\*\*\*", r"\1", plain) plain = re.sub(r"(? str: r"""Double the ``\`` before hidden dirs in Windows paths: CommonMark reads ``\.`` as an escaped dot.""" from cli import _WINDOWS_PATH_WITH_DOT_SEGMENT_RE if "\\." not in text: return text def _protect(match: re.Match[str]) -> str: return re.sub(r"(? int: try: return shutil.get_terminal_size((80, 24)).columns except Exception: return 80 def _terminal_width_for_streaming() -> int: """Display cells inside the streamed response box (small margin for resize races).""" from cli import _STREAM_PAD, _terminal_columns return max(20, _terminal_columns() - len(_STREAM_PAD) - 2) def _render_final_assistant_content(text: str, mode: str = "render"): """Render final assistant content as markdown, stripped text, or raw text.""" from cli import _preserve_windows_dot_segments_for_markdown, _rich_text_from_ansi, _strip_markdown_syntax, _terminal_columns, realign_markdown_tables from rich.markdown import Markdown # 1 border cell each side + margin so resize races don't push a borderline table into soft-wrap. panel_width = max(20, _terminal_columns() - 4) normalized_mode = str(mode or "render").strip().lower() if normalized_mode == "strip": # Strip first (inline markdown changes cell width), then re-align padding. return _RichText(realign_markdown_tables(_strip_markdown_syntax(text), panel_width)) if normalized_mode == "raw": return _rich_text_from_ansi(text or "") # Normalising under-padded tables up front gives narrow-panel fallbacks consistent input. plain = _rich_text_from_ansi(text or "").plain plain = _preserve_windows_dot_segments_for_markdown(plain) plain = realign_markdown_tables(plain, panel_width) return Markdown(plain) def _post_stream_transform_output(response: str, result: dict | None) -> str: """Text still to display after a streamed response transform: the suffix, or the whole response when replaced.""" if not result or not result.get("response_transformed"): return "" original = result.get("pre_transform_response") or "" if original and response.startswith(original): return response[len(original):] return f"\n[Response transformed after streaming]\n{response}" def _coerce_output_history_limit(value) -> int: try: return max(10, int(value)) except (TypeError, ValueError): return 200 def _clear_output_history() -> None: _cli()._OUTPUT_HISTORY.clear() def _output_history_recording() -> bool: return _cli()._OUTPUT_HISTORY_ENABLED and not _cli()._OUTPUT_HISTORY_REPLAYING and not _cli()._OUTPUT_HISTORY_SUPPRESSED def _record_output_history_entry(entry) -> None: from cli import _output_history_recording if _output_history_recording(): _cli()._OUTPUT_HISTORY.append(entry) def _record_output_history(text: str) -> None: from cli import _output_history_recording if _output_history_recording(): _cli()._OUTPUT_HISTORY.extend(str(text).replace("\r", "").rstrip("\n").splitlines()) def _pt_print_ansi(text: str) -> None: """``_pt_print(ANSI(text))``, falling back to ``print`` when stdout is not a real console.""" from cli import _PT_ANSI, _pt_print try: _pt_print(_PT_ANSI(text)) except Exception: # NoConsoleScreenBufferError (Windows) / OSError when stdout is e.g. a worker log file. with suppress(Exception): print(text) def _cprint(text: str): """Print ANSI text through prompt_toolkit's renderer (patch_stdout swallows raw ANSI). From a background thread while an Application runs, a direct print races the input redraw and gets buried, so those go through ``run_in_terminal`` via ``call_soon_threadsafe``. """ from cli import _PT_ANSI, _pt_print, _pt_print_ansi, _record_output_history _record_output_history(text) try: from prompt_toolkit.application import get_app_or_none, run_in_terminal except Exception: _pt_print(_PT_ANSI(text)) return try: app = get_app_or_none() except Exception: app = None if app is None or not getattr(app, "_is_running", False): _pt_print_ansi(text) return import asyncio as _asyncio try: loop = app.loop # type: ignore[attr-defined] except Exception: loop = None try: # get_running_loop(): get_event_loop() warns from threads with no current loop. # Use get_running_loop() instead of get_event_loop() to avoid the DeprecationWarning / # RuntimeWarning emitted by Python 3.10+ when get_event_loop() is called from a thread that has no # current event loop set (e.g. the process_loop background thread). Fixes #19285. current_loop = _asyncio.get_running_loop() except Exception: current_loop = None if loop is None or (current_loop is loop and loop.is_running()): _pt_print(_PT_ANSI(text)) return def _schedule(): # run_in_terminal() returns an awaitable (pt >= 3.0) that must be scheduled or the # output is dropped, or None (mocks / older pt) when it already ran synchronously. # Never fall back to a bare print on error: the sync path already printed. with suppress(Exception): import inspect as _inspect coro = run_in_terminal(lambda: _pt_print(_PT_ANSI(text))) if coro is not None and (_inspect.isawaitable(coro) or _inspect.iscoroutine(coro)): _asyncio.ensure_future(coro) try: loop.call_soon_threadsafe(_schedule) except Exception: _pt_print_ansi(text) def _prepend_note_to_message(message, note: str): """Prepend a one-shot note to a user message (str, or content-part list when an image is attached). For lists the note is folded into the first text part or inserted as a leading one. Unknown shapes are returned unchanged. """ note = str(note or "").strip() if not note: return message if isinstance(message, str): return f"{note}\n\n{message}" if message else note if isinstance(message, list): parts = list(message) for i, part in enumerate(parts): if isinstance(part, dict) and part.get("type") == "text": text = part.get("text", "") parts[i] = {**part, "text": f"{note}\n\n{text}" if text else note} return parts return [{"type": "text", "text": note}, *parts] return message def _pt_app_is_running() -> bool: """Whether a prompt_toolkit Application currently owns the live terminal.""" try: from prompt_toolkit.application import get_app_or_none app = get_app_or_none() except Exception: return False return app is not None and bool(getattr(app, "_is_running", False)) def _cli_visible_print(text: str = "") -> None: """``print`` unless a prompt_toolkit Application owns the terminal (patch_stdout swallows bare prints).""" from cli import _cprint, _pt_app_is_running if _pt_app_is_running(): _cprint(text) else: print(text) class ChatConsole: """Rich Console drop-in routing rendered ANSI through ``_cprint`` so colors survive patch_stdout.""" def __init__(self): from io import StringIO self._buffer = StringIO() self._inner = Console(file=self._buffer, force_terminal=True, color_system="truecolor", highlight=False) def print(self, *args, **kwargs): from cli import _OSC_ESCAPE_RE, _cprint self._buffer.seek(0) self._buffer.truncate() self._inner.width = shutil.get_terminal_size((80, 24)).columns self._inner.print(*args, **kwargs) for line in _OSC_ESCAPE_RE.sub("", self._buffer.getvalue()).rstrip("\n").split("\n"): _cprint(line) @contextmanager def status(self, *_args, **_kwargs): """No-op ``console.status`` so slash helpers don't duplicate ``_busy_command()``'s indicator.""" yield self def _build_compact_banner() -> str: """Build a compact banner that fits the current terminal width.""" try: from hermes_cli.skin_engine import get_active_skin _skin = get_active_skin() except Exception: _skin = None def _color(key, default): return _skin.get_color(key, default) if _skin else default border_color = _color("banner_border", "#FFD700") title_color = _color("banner_title", "#FFBF00") dim_color = _color("banner_dim", "#B8860B") if (getattr(_skin, "name", "default") if _skin else "default") == "default": tiny_line = "☤ NOUS HERMES" else: tiny_line = _skin.get_branding("agent_name", "Hermes Agent") if _skin else "Hermes Agent" line1 = f"{tiny_line} - AI Agent Framework" if os.environ.get("HERMES_FAST_STARTUP_BANNER") == "1": from hermes_cli import __release_date__ as _release_date from hermes_cli import __version__ as _version version_line = f"Hermes Agent v{_version} ({_release_date})" else: version_line = format_banner_version_label() w = min(shutil.get_terminal_size().columns - 2, 88) if w < 30: return f"\n[{title_color}]{tiny_line}[/] [dim {dim_color}]- Nous Research[/]\n" inner = w - 2 # inside the box border bar = "═" * w content_width = inner - 2 line1 = line1[:content_width].ljust(content_width) line2 = version_line[:content_width].ljust(content_width) return ( f"\n[bold {border_color}]╔{bar}╗[/]\n" f"[bold {border_color}]║[/] [{title_color}]{line1}[/] [bold {border_color}]║[/]\n" f"[bold {border_color}]║[/] [dim {dim_color}]{line2}[/] [bold {border_color}]║[/]\n" f"[bold {border_color}]╚{bar}╝[/]\n" ) def _panel_box_width(title: str, content_lines: list[str], min_width: int = 46, max_width: int = 76) -> int: """Stable TUI panel width wide enough for the title and content (incl. borders).""" term_cols = shutil.get_terminal_size((100, 20)).columns longest = max([len(title)] + [len(line) for line in content_lines] + [min_width - 4]) inner = min(max(longest + 4, min_width - 2), max_width - 2, max(24, term_cols - 6)) return inner + 2 # leading/trailing space inside the borders def _wrap_panel_text(text: str, width: int, subsequent_indent: str = "", *, keep_ws: bool = False) -> list[str]: """Wrap panel text; ``keep_ws`` preserves whitespace (command/detail previews).""" kw = dict(replace_whitespace=False, drop_whitespace=False) if keep_ws else dict(break_long_words=False, break_on_hyphens=False) wrapped = textwrap.wrap(text, width=max(8, width), subsequent_indent=subsequent_indent, **kw) return wrapped or [""] _wrap_panel_text_keep_ws = functools.partial(_wrap_panel_text, keep_ws=True) def _append_panel_line(lines, border_style: str, content_style: str, text: str, box_width: int) -> None: lines.extend(((border_style, "│ "), (content_style, text.ljust(max(0, box_width - 2))), (border_style, " │\n"))) def _append_blank_panel_line(lines, border_style: str, box_width: int) -> None: lines.append((border_style, "│" + (" " * box_width) + "│\n"))