Images in plain text and the terminal; bounded build parallelism (from dev 16f82cc887af)

- @image under txt and ansi is a picture in characters, in five styles
  (Image_txt_style: halfblock the default, shade, ascii, quadrant,
  braille), sized to the terminal's width less a margin when ktext runs
  in one, every cell in its own colours under ansi; a figure's caption
  is centred under it and no txt caption is ruled off any more.
- The build compiles as many files at once as the machine has cores
  (JOBS; make -C com JOBS=1 for a small machine): the sub-makes' bare -j
  meant unlimited parallelism and exhausted 8 GB machines. The three
  install pages say "make -C com".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ackandClaude Fable 5.1 committed 2026-09-30 16:13:23 +02:00
1 parent d14da90bbf
commit ed7bedf5a7
13 files changed
+335 -41

No files matched your search

+37 -6
View File
@@ -75,20 +75,43 @@ _STYLE_MARKER_RGX = re.compile(
"|".join(re.escape(txt_style_marker(s, on))
for s in TXT_STYLES for on in (True, False)))
# A COLOUR marker carries its colour: __KTEXTFG<rrggbb>__ opens a foreground
# colour, __KTEXTFGOFF__ restores the default (SGR 39, never a blanket reset).
# Used by @image under txt/ansi (2026-09-27, Andy's terminal images): the
# character carries the luminance and the colour the hue, so stripping the
# escapes still leaves a monochrome picture -- the invariant kept.
def txt_color_marker(rgb=None, background=False):
"""A foreground colour marker, or with background=True a background one
(__KTEXTBG<rrggbb>__ / __KTEXTBGOFF__, SGR 48;2 and 49): the quadrant
and halfblock image styles set two colours per cell."""
ground = "BG" if background else "FG"
if rgb is None:
return f"__KTEXT{ground}OFF__"
r, g, b = rgb
return f"__KTEXT{ground}{int(r):02X}{int(g):02X}{int(b):02X}__"
_COLOR_MARKER_RGX = re.compile(r"__KTEXT(FG|BG)(?:([0-9A-F]{6})|OFF)__")
def txt_strip_styles(text, K):
return _STYLE_MARKER_RGX.sub("", text)
return _COLOR_MARKER_RGX.sub("", _STYLE_MARKER_RGX.sub("", text))
def ansi_resolve_styles(text, K):
for style, (on, off) in TXT_STYLES.items():
text = text.replace(txt_style_marker(style), on)
text = text.replace(txt_style_marker(style, False), off)
return text
def color(m):
ground, hex6 = m.group(1), m.group(2)
code = 38 if ground == "FG" else 48
if hex6 is None:
return f"\033[{code + 1}m" # 39 / 49: the default fore-/background
return f"\033[{code};2;{int(hex6[0:2], 16)};{int(hex6[2:4], 16)};{int(hex6[4:6], 16)}m"
return _COLOR_MARKER_RGX.sub(color, text)
def txt_visible_length(s):
"""How many columns s occupies once its markers resolve: a KTESC
marker is one glyph, an nbsp marker one space, a style marker nothing.
len() is the wrong measure for anything laid out (see txt_util)."""
s = _STYLE_MARKER_RGX.sub("", s).replace(txt_verbatim_marker(), "")
s = _COLOR_MARKER_RGX.sub("", _STYLE_MARKER_RGX.sub("", s)).replace(txt_verbatim_marker(), "")
return len(klammer_base.unescape_ktesc(s).replace(txt_nbsp_marker(), " "))
def txt_wrap(text, width, subsequent_indent=""):
@@ -257,6 +280,10 @@ def txt_insert_captions(text, K):
return widest
def replace(m):
indent_run, name, text = m.groups()
# The name may carry flags after "+": "center" sets the caption
# centred on its block (a figure's, as in html and tex).
name, _, flags = name.partition("+")
centered = "center" in flags.split("+")
indent = len(indent_run) // len(nbsp)
indented_width = max(1, width - indent)
# The block the caption belongs to: below a top caption, above a
@@ -278,12 +305,16 @@ def txt_insert_captions(text, K):
else:
caption = text
# Visible widths throughout: a caption may carry style markers.
# NO RULE under (or over) a caption: none of the targets draws one
# (Andy, 2026-09-28 -- the rule this pass drew since 2026-08 was a
# txt invention; a table's rules are its own :hline, a listing's its
# own frame). A centred caption is set on the block's width.
lines = [caption]
if txt_visible_length(caption) > indented_width:
lines = txt_wrap(caption, indented_width)
line_length = max(txt_visible_length(e) for e in lines)
line_length = max(line_length, block - indent)
lines.append("-" * line_length)
if centered:
measure = max(block - indent, max(txt_visible_length(e) for e in lines))
lines = [" " * max(0, (measure - txt_visible_length(e)) // 2) + e for e in lines]
lead = txt_verbatim_marker() + indent_run
# The first line's marker is already in the text, before the indent
# run this match begins with (add_caption); the others get theirs.
+7 -3
View File
@@ -58,7 +58,7 @@ def block_width(text_width, indent):
return max(1, int(text_width) - indent)
def add_caption(element, caption_label, number, caption_text,
side="top", indent=0):
side="top", indent=0, centered=False):
"""Attach a caption to an already-laid-out block.
`element` is the block's text, indented by `indent` columns of NBSP.
@@ -95,10 +95,14 @@ def add_caption(element, caption_label, number, caption_text,
# marker substitution, so a caption that arrived there as plain text
# got none of it: ":number false" silently lost its rule. The label
# is what says "number me", not the presence of the marker.
# `centered` rides in the marker's name as a "+center" flag, which the
# numbering pass reads off: a figure's caption is centred under it, as
# in html and tex; a table's or a listing's stays at the block's edge.
flag = "+center" if centered else ""
if number:
caption = kutil.caption_marker(caption_label, caption_text, None)
caption = kutil.caption_marker(caption_label + flag, caption_text, None)
elif caption_text:
caption = kutil.caption_marker("", caption_text, None)
caption = kutil.caption_marker(flag, caption_text, None)
else:
return element
# Marked laid-out, like the block: with no indent the caption begins