codecortex 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of codecortex might be problematic. Click here for more details.

codeintel/term.py ADDED
@@ -0,0 +1,162 @@
1
+ """Terminal output system — one small, dependency-free styling layer shared by every
2
+ human-facing command (doctor, status, query, setup) so they read as one tool.
3
+
4
+ Two independent axes, each auto-detected and overridable:
5
+ * color — raw ANSI SGR, ON only for a TTY, disabled by NO_COLOR / --no-color / TERM=dumb.
6
+ * glyphs — unicode by default, ASCII fallback when the stream can't encode them / --ascii.
7
+
8
+ Alignment note: status glyphs are drawn only from Unicode blocks that render as exactly one
9
+ column in monospace fonts. `⚠` (U+26A0, Misc Symbols) is deliberately AVOIDED — many coding
10
+ fonts draw it ~2 cells wide even though `unicodedata` calls it narrow, which is what made the
11
+ doctor table drift. `▲` (U+25B2, Geometric Shapes) is width-stable, so naive centering is correct.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import sys
17
+ from typing import Optional
18
+
19
+ _CODES = {"bold": "1", "dim": "2", "red": "31", "green": "32", "yellow": "33", "cyan": "36"}
20
+
21
+ # state -> (unicode glyph, ascii glyph, color role). Glyphs are single display-column safe.
22
+ GLYPHS = {
23
+ "ok": {"unicode": "✓", "ascii": "[ OK ]", "color": "green"}, # ✓
24
+ "fail": {"unicode": "✗", "ascii": "[FAIL]", "color": "red"}, # ✗
25
+ "warn": {"unicode": "▲", "ascii": "[WARN]", "color": "yellow"}, # ▲ (NOT ⚠)
26
+ "na": {"unicode": "n/a", "ascii": "[ N/A]", "color": "dim"},
27
+ }
28
+
29
+ # Blocks that render as exactly one terminal column in virtually every monospace font.
30
+ # EXCLUDES Miscellaneous Symbols (0x2600-0x26FF) — the width-ambiguous emoji-adjacent range.
31
+ _SAFE_GLYPH_BLOCKS = (
32
+ (0x0000, 0x007F), # Basic Latin
33
+ (0x2500, 0x257F), # Box Drawing ─ │ └
34
+ (0x25A0, 0x25FF), # Geometric Shapes ▲ ● ○
35
+ (0x2700, 0x27BF), # Dingbats ✓ ✗
36
+ )
37
+
38
+
39
+ def is_display_width_safe(ch: str) -> bool:
40
+ """True if every char in ``ch`` is from a block that draws as one monospace column.
41
+ Column-aligned glyphs (table/list cells) must satisfy this; prose banners need not."""
42
+ return all(any(lo <= ord(c) <= hi for lo, hi in _SAFE_GLYPH_BLOCKS) for c in ch)
43
+
44
+
45
+ class Console:
46
+ def __init__(
47
+ self,
48
+ *,
49
+ stream=None,
50
+ no_color: bool = False,
51
+ ascii_mode: Optional[bool] = None,
52
+ ) -> None:
53
+ self.stream = stream if stream is not None else sys.stdout
54
+ self.enabled = self._detect_color(no_color, self.stream)
55
+ self.ascii = self._detect_ascii(ascii_mode, self.stream)
56
+
57
+ @staticmethod
58
+ def _detect_color(no_color_flag: bool, stream) -> bool:
59
+ if no_color_flag or "NO_COLOR" in os.environ: # presence, not truthiness (no-color.org)
60
+ return False
61
+ if os.environ.get("TERM") == "dumb":
62
+ return False
63
+ if os.environ.get("FORCE_COLOR"):
64
+ return True
65
+ try:
66
+ return bool(stream.isatty())
67
+ except Exception:
68
+ return False
69
+
70
+ @staticmethod
71
+ def _detect_ascii(flag: Optional[bool], stream) -> bool:
72
+ if flag is not None:
73
+ return flag
74
+ enc = getattr(stream, "encoding", None) or ""
75
+ try:
76
+ "✓✗▲".encode(enc or "utf-8")
77
+ return False
78
+ except (LookupError, UnicodeEncodeError):
79
+ return True
80
+
81
+ def _wrap(self, code: str, s: str) -> str:
82
+ return f"\x1b[{code}m{s}\x1b[0m" if self.enabled else s
83
+
84
+ def bold(self, s: str) -> str: return self._wrap(_CODES["bold"], s)
85
+ def dim(self, s: str) -> str: return self._wrap(_CODES["dim"], s)
86
+ def red(self, s: str) -> str: return self._wrap(_CODES["red"], s)
87
+ def green(self, s: str) -> str: return self._wrap(_CODES["green"], s)
88
+ def yellow(self, s: str) -> str: return self._wrap(_CODES["yellow"], s)
89
+ def cyan(self, s: str) -> str: return self._wrap(_CODES["cyan"], s)
90
+
91
+ def glyph(self, state: str) -> str:
92
+ """A colored, width-safe status token for ``state`` in {ok, fail, warn, na}."""
93
+ g = GLYPHS.get(state, GLYPHS["na"])
94
+ text = g["ascii"] if self.ascii else g["unicode"]
95
+ color = g["color"]
96
+ return self.dim(text) if color == "dim" else getattr(self, color)(text)
97
+
98
+ def raw_glyph(self, state: str) -> str:
99
+ """The uncolored glyph text (for width math), still respecting ascii mode."""
100
+ g = GLYPHS.get(state, GLYPHS["na"])
101
+ return g["ascii"] if self.ascii else g["unicode"]
102
+
103
+ def status_cell(self, state: str, width: int) -> str:
104
+ """A glyph centered in ``width`` columns THEN colored — so the (zero-width on screen but
105
+ len()-counted) ANSI codes never corrupt the centering. This is the table-alignment fix."""
106
+ g = GLYPHS.get(state, GLYPHS["na"])
107
+ padded = self.raw_glyph(state).center(width)
108
+ color = g["color"]
109
+ return self.dim(padded) if color == "dim" else getattr(self, color)(padded)
110
+
111
+ def rule(self, n: int) -> str:
112
+ return self.dim(("-" if self.ascii else "─") * n)
113
+
114
+ def header(self, subcommand: str, context: str = "") -> str:
115
+ line = self.bold(f"codeintel {subcommand}")
116
+ if context:
117
+ line += f" {self.dim('—')} {self.dim(context)}"
118
+ return line
119
+
120
+
121
+ # Auto-detected singletons; the CLI calls configure() once after parsing --no-color/--ascii.
122
+ c = Console(stream=sys.stdout)
123
+ c_err = Console(stream=sys.stderr)
124
+
125
+
126
+ def configure(*, no_color: bool = False, ascii_mode: Optional[bool] = None) -> None:
127
+ """Re-detect both consoles honoring CLI flags (called once from __main__)."""
128
+ global c, c_err
129
+ c = Console(stream=sys.stdout, no_color=no_color, ascii_mode=ascii_mode)
130
+ c_err = Console(stream=sys.stderr, no_color=no_color, ascii_mode=ascii_mode)
131
+
132
+
133
+ class LiveStep:
134
+ """One status line for a slow op. On a TTY it redraws in place; on a pipe/CI/log it prints
135
+ exactly one clean line when done — never leaks carriage-return/cursor bytes to a non-TTY."""
136
+
137
+ def __init__(self, console: Console, label: str) -> None:
138
+ self.c = console
139
+ self.label = label
140
+ try:
141
+ self.live = console.enabled and console.stream.isatty()
142
+ except Exception:
143
+ self.live = False
144
+ if self.live:
145
+ try:
146
+ console.stream.write(f" {console.dim('…')} {label}\r")
147
+ console.stream.flush()
148
+ except Exception:
149
+ self.live = False
150
+
151
+ def done(self, state: str, detail: str = "") -> None:
152
+ text = f" {self.c.glyph(state)} {self.label}"
153
+ if detail:
154
+ text += f" {self.c.dim(detail)}"
155
+ try:
156
+ if self.live:
157
+ self.c.stream.write("\x1b[2K\r" + text + "\n") # erase line, redraw
158
+ else:
159
+ self.c.stream.write(text + "\n")
160
+ self.c.stream.flush()
161
+ except Exception:
162
+ pass