AQIT 0.1.0
Loading...
Searching...
No Matches
ascii_layout.py
Go to the documentation of this file.
1# Copyright (c) 2025-present Aquin Labs Private Limited. All Rights Reserved.
2"""Shared ASCII terminal layout primitives for aquin CLI surfaces."""
3
4from __future__ import annotations
5
6import shutil
7import textwrap
8
9# Hard-coded slant figlet — no runtime dep on pyfiglet.
10BANNER = r"""
11 ___ ____ __ _______ __
12 / | / __ \/ / / / _/ | / /
13 / /| |/ / / / / / // // |/ /
14 / ___ / /_/ / /_/ // // /| /
15/_/ |_\___\_\____/___/_/ |_/
16""".strip("\n")
17
18
19def width() -> int:
20 try:
21 cols = shutil.get_terminal_size().columns
22 except OSError:
23 cols = 80
24 return max(64, min(cols, 96))
25
26
27def ascii_safe(text: str) -> str:
28 return (
29 text.replace("\u2014", "-")
30 .replace("\u2500", "-")
31 .replace("\u00b7", "|")
32 .replace("\u2019", "'")
33 .replace("\u2026", "...")
34 )
35
36
37def rule(char: str = "=", w: int | None = None) -> str:
38 return char * (w or width())
39
40
41def center(text: str, w: int | None = None) -> str:
42 return text.center(w or width())
43
44
45def center_block(lines: list[str], w: int | None = None) -> list[str]:
46 """Pad lines to equal width, then center the block as one unit."""
47 w = w or width()
48 if not lines:
49 return []
50 block_w = max(len(line) for line in lines)
51 padded = [line.ljust(block_w) for line in lines]
52 left = max(0, (w - block_w) // 2)
53 return [(" " * left) + line for line in padded]
54
55
56def box_top(w: int | None = None) -> str:
57 w = w or width()
58 return "+" + "-" * (w - 2) + "+"
59
61def box_bottom(w: int | None = None) -> str:
62 return box_top(w)
63
64
65def box_line(text: str, w: int | None = None) -> str:
66 w = w or width()
67 inner = w - 4
68 text = ascii_safe(text)
69 if len(text) > inner:
70 text = text[: inner - 3] + "..."
71 return f"| {text.ljust(inner)} |"
72
73
74def box_lines(text: str, w: int | None = None) -> list[str]:
75 w = w or width()
76 inner = w - 4
77 text = ascii_safe(text)
78 stripped = text.strip()
79
80 if not stripped:
81 return [box_line("", w)]
82
83 if len(text) <= inner:
84 return [box_line(text, w)]
85
86 if all(c in "-=_." for c in stripped):
87 return [box_line("-" * inner, w)]
88
89 wrapped = textwrap.wrap(
90 text,
91 width=inner,
92 break_long_words=False,
93 break_on_hyphens=False,
94 )
95 return [box_line(line, w) for line in wrapped]
96
97
98def kv_line(label: str, value: str, *, label_width: int = 10, w: int | None = None) -> str:
99 """Boxed key/value row: ' label value'."""
100 w = w or width()
101 key = label[:label_width].ljust(label_width)
102 return box_line(f" {key} {ascii_safe(value)}", w)
103
104
105def print_logo() -> None:
106 """Bare Aquin figlet — used for `aquin` with no args."""
107 print(BANNER)
108 print()
110
111def print_banner(*, subtitle: str, w: int | None = None) -> None:
112 w = w or width()
113 print()
114 print(rule("=", w))
115 for line in center_block(BANNER.splitlines(), w):
116 print(line.rstrip())
117 print(center(subtitle, w))
118 print(rule("=", w))
119 print()
120
121
122def print_section(title: str, body_lines: list[str], *, w: int | None = None) -> None:
123 """Print a titled ASCII box. body_lines are already boxed or plain content rows."""
124 w = w or width()
125 print(box_top(w))
126 print(box_line(title.upper(), w))
127 print(box_line("", w))
128 for line in body_lines:
129 if line.startswith("| ") and line.endswith(" |"):
130 print(line)
131 else:
132 for boxed in box_lines(line, w):
133 print(boxed)
134 print(box_bottom(w))
135 print()
str box_bottom(int|None w=None)
str box_line(str text, int|None w=None)
None print_banner(*, str subtitle, int|None w=None)
str box_top(int|None w=None)
str ascii_safe(str text)
str rule(str char="=", int|None w=None)
None print_section(str title, list[str] body_lines, *, int|None w=None)
str center(str text, int|None w=None)
list[str] center_block(list[str] lines, int|None w=None)
list[str] box_lines(str text, int|None w=None)
str kv_line(str label, str value, *, int label_width=10, int|None w=None)