abstractcode 0.1.0__py3-none-any.whl → 0.3.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.
- abstractcode/__init__.py +6 -37
- abstractcode/cli.py +401 -0
- abstractcode/flow_cli.py +1413 -0
- abstractcode/fullscreen_ui.py +1453 -0
- abstractcode/input_handler.py +81 -0
- abstractcode/py.typed +1 -0
- abstractcode/react_shell.py +6440 -0
- abstractcode/recall.py +384 -0
- abstractcode/remember.py +184 -0
- abstractcode/terminal_markdown.py +168 -0
- abstractcode/workflow_agent.py +894 -0
- abstractcode-0.3.0.dist-info/METADATA +270 -0
- abstractcode-0.3.0.dist-info/RECORD +17 -0
- abstractcode-0.1.0.dist-info/METADATA +0 -114
- abstractcode-0.1.0.dist-info/RECORD +0 -7
- {abstractcode-0.1.0.dist-info → abstractcode-0.3.0.dist-info}/WHEEL +0 -0
- {abstractcode-0.1.0.dist-info → abstractcode-0.3.0.dist-info}/entry_points.txt +0 -0
- {abstractcode-0.1.0.dist-info → abstractcode-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {abstractcode-0.1.0.dist-info → abstractcode-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Minimal, terminal-friendly Markdown renderer.
|
|
2
|
+
|
|
3
|
+
Goal: improve readability in the TUI without attempting full CommonMark compliance.
|
|
4
|
+
We deliberately keep this conservative:
|
|
5
|
+
- Only style headings, code fences, and a few inline constructs.
|
|
6
|
+
- Never mutate the underlying content used for copy-to-clipboard.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class AnsiPalette:
|
|
18
|
+
reset: str = "\033[0m"
|
|
19
|
+
dim: str = "\033[2m"
|
|
20
|
+
bold: str = "\033[1m"
|
|
21
|
+
cyan: str = "\033[36m"
|
|
22
|
+
green: str = "\033[32m"
|
|
23
|
+
blue: str = "\033[38;5;39m"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TerminalMarkdownRenderer:
|
|
27
|
+
"""Render a subset of Markdown to ANSI-styled plain text."""
|
|
28
|
+
|
|
29
|
+
_re_heading = re.compile(r"^(?P<hashes>#{1,6})\s+(?P<title>.+?)\s*$")
|
|
30
|
+
_re_hr = re.compile(r"^\s*(-{3,}|_{3,}|\*{3,})\s*$")
|
|
31
|
+
_re_bold = re.compile(r"\*\*(?P<txt>[^*]+)\*\*")
|
|
32
|
+
_re_inline_code = re.compile(r"`(?P<code>[^`]+)`")
|
|
33
|
+
|
|
34
|
+
def __init__(self, *, color: bool = True, palette: AnsiPalette | None = None) -> None:
|
|
35
|
+
self._color = bool(color)
|
|
36
|
+
self._p = palette or AnsiPalette()
|
|
37
|
+
|
|
38
|
+
def _style(self, text: str, *codes: str) -> str:
|
|
39
|
+
if not self._color or not codes:
|
|
40
|
+
return text
|
|
41
|
+
return "".join(codes) + text + self._p.reset
|
|
42
|
+
|
|
43
|
+
def _style_inline(self, line: str) -> str:
|
|
44
|
+
# Bold
|
|
45
|
+
def _bold(m: re.Match) -> str:
|
|
46
|
+
return self._style(m.group("txt"), self._p.bold)
|
|
47
|
+
|
|
48
|
+
# Inline code
|
|
49
|
+
def _code(m: re.Match) -> str:
|
|
50
|
+
return self._style(m.group("code"), self._p.blue)
|
|
51
|
+
|
|
52
|
+
out = self._re_bold.sub(_bold, line)
|
|
53
|
+
out = self._re_inline_code.sub(_code, out)
|
|
54
|
+
return out
|
|
55
|
+
|
|
56
|
+
def _unescape_newlines_if_needed(self, s: str) -> str:
|
|
57
|
+
"""Convert literal "\\n" / "\\r" / "\\r\\n" sequences into real newlines.
|
|
58
|
+
|
|
59
|
+
Some upstream layers accidentally pass serialized strings (repr/json) where newlines are
|
|
60
|
+
encoded as the two characters backslash+n. We only unescape when the input has *no* real
|
|
61
|
+
newlines to avoid corrupting valid code like `print("a\\nb")`.
|
|
62
|
+
"""
|
|
63
|
+
if "\n" in s or "\r" in s:
|
|
64
|
+
return s
|
|
65
|
+
if "\\n" not in s and "\\r" not in s:
|
|
66
|
+
return s
|
|
67
|
+
|
|
68
|
+
out: List[str] = []
|
|
69
|
+
i = 0
|
|
70
|
+
n = len(s)
|
|
71
|
+
while i < n:
|
|
72
|
+
ch = s[i]
|
|
73
|
+
if ch != "\\":
|
|
74
|
+
out.append(ch)
|
|
75
|
+
i += 1
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
# Count consecutive backslashes.
|
|
79
|
+
j = i
|
|
80
|
+
while j < n and s[j] == "\\":
|
|
81
|
+
j += 1
|
|
82
|
+
run_len = j - i
|
|
83
|
+
|
|
84
|
+
if j >= n:
|
|
85
|
+
out.append("\\" * run_len)
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
nxt = s[j]
|
|
89
|
+
|
|
90
|
+
# Only treat "\n"/"\r" as escapes when the escape backslash is not itself escaped.
|
|
91
|
+
if nxt in ("n", "r") and (run_len % 2 == 1):
|
|
92
|
+
# Preserve all but the escape backslash.
|
|
93
|
+
if run_len > 1:
|
|
94
|
+
out.append("\\" * (run_len - 1))
|
|
95
|
+
out.append("\n")
|
|
96
|
+
i = j + 1
|
|
97
|
+
|
|
98
|
+
# Collapse \r\n into a single newline (Windows-style payloads).
|
|
99
|
+
if nxt == "r" and i < n and s[i] == "\\":
|
|
100
|
+
k = i
|
|
101
|
+
while k < n and s[k] == "\\":
|
|
102
|
+
k += 1
|
|
103
|
+
run2_len = k - i
|
|
104
|
+
if k < n and s[k] == "n" and (run2_len % 2 == 1):
|
|
105
|
+
if run2_len > 1:
|
|
106
|
+
out.append("\\" * (run2_len - 1))
|
|
107
|
+
i = k + 1
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Not an escape we handle; emit literally.
|
|
111
|
+
out.append("\\" * run_len)
|
|
112
|
+
out.append(nxt)
|
|
113
|
+
i = j + 1
|
|
114
|
+
|
|
115
|
+
return "".join(out)
|
|
116
|
+
|
|
117
|
+
def render(self, text: str) -> str:
|
|
118
|
+
s = "" if text is None else str(text)
|
|
119
|
+
s = self._unescape_newlines_if_needed(s)
|
|
120
|
+
lines = s.splitlines()
|
|
121
|
+
out: List[str] = []
|
|
122
|
+
|
|
123
|
+
in_code = False
|
|
124
|
+
fence_lang = ""
|
|
125
|
+
|
|
126
|
+
for raw in lines:
|
|
127
|
+
line = raw.rstrip("\n")
|
|
128
|
+
|
|
129
|
+
# Code fences
|
|
130
|
+
if line.strip().startswith("```"):
|
|
131
|
+
if not in_code:
|
|
132
|
+
in_code = True
|
|
133
|
+
fence_lang = line.strip()[3:].strip()
|
|
134
|
+
label = f"code" + (f" ({fence_lang})" if fence_lang else "")
|
|
135
|
+
out.append(self._style(f"┌─ {label}", self._p.dim))
|
|
136
|
+
else:
|
|
137
|
+
in_code = False
|
|
138
|
+
out.append(self._style("└─", self._p.dim))
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
if in_code:
|
|
142
|
+
# Keep code unmodified; add a subtle gutter.
|
|
143
|
+
out.append(self._style("│ ", self._p.dim) + line)
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
# Horizontal rules
|
|
147
|
+
if self._re_hr.match(line):
|
|
148
|
+
out.append(self._style("─" * 60, self._p.dim))
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
# Headings
|
|
152
|
+
m = self._re_heading.match(line)
|
|
153
|
+
if m:
|
|
154
|
+
hashes = m.group("hashes")
|
|
155
|
+
title = m.group("title").strip()
|
|
156
|
+
level = len(hashes)
|
|
157
|
+
if level <= 2:
|
|
158
|
+
out.append(self._style(title, self._p.bold, self._p.cyan))
|
|
159
|
+
else:
|
|
160
|
+
out.append(self._style(title, self._p.bold))
|
|
161
|
+
continue
|
|
162
|
+
|
|
163
|
+
out.append(self._style_inline(line))
|
|
164
|
+
|
|
165
|
+
return "\n".join(out)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|