ansi_model.dhover×117 error×9all
// Ghostty-free presentation types shared by the markdown-preview model and
// the ANSI decoder. Split out of `gui_ansi.d` so the preview model
// (`gui_preview.d`) compiles into the raylib-/ghostty-free `no-gui` build:
// only `decodeAnsi` (the off-screen libghostty-vt bridge) still lives in
// `gui_ansi.d` and is pulled in solely by the GUI build.
module 
(package) sparkles
sparkles
.
(package) sparkles.doc_view
doc_view
.
(module) sparkles.doc_view.ansi_model
ansi_model
;
import sparkles.base.term_color : RgbColor;
unable to read module `term_color` Expected 'sparkles/base/term_color.d' or 'sparkles/base/term_color/package.d' in one of the following import paths:
unable to read module `term_color` Expected 'sparkles/base/term_color.d' or 'sparkles/base/term_color/package.d' in one of the following import paths:
unable to read module `term_color` Expected 'sparkles/base/term_color.d' or 'sparkles/base/term_color/package.d' in one of the following import paths:
import sparkles.syntax : AnsiOptions, ColorDepth;
unable to read module `syntax` Expected 'sparkles/syntax.d' or 'sparkles/syntax/package.d' in one of the following import paths:
unable to read module `syntax` Expected 'sparkles/syntax.d' or 'sparkles/syntax/package.d' in one of the following import paths:
unable to read module `syntax` Expected 'sparkles/syntax.d' or 'sparkles/syntax/package.d' in one of the following import paths:
import sparkles.ui.widget : TextSpan;
unable to read module `widget` Expected 'sparkles/ui/widget.d' or 'sparkles/ui/widget/package.d' in one of the following import paths:
unable to read module `widget` Expected 'sparkles/ui/widget.d' or 'sparkles/ui/widget/package.d' in one of the following import paths:
unable to read module `widget` Expected 'sparkles/ui/widget.d' or 'sparkles/ui/widget/package.d' in one of the following import paths:
/// How the theme background is applied in terminal rendering (`--background`, /// hue spec `BGM`) — shared by the whole-file ANSI emit, the markdown-preview /// painter and the TUI. (Moved here from the retired `previewer.d`.) enum
(enum) sparkles.doc_view.ansi_model.BackgroundMode

How the theme background is applied in terminal rendering (--background, hue spec BGM) — shared by the whole-file ANSI emit, the markdown-preview painter and the TUI. (Moved here from the retired previewer.d.)

BackgroundMode
{ /// `no-background`: foreground only; the terminal's own background shows /// through (`BGM1`).
(enum value) sparkles.doc_view.ansi_model.BackgroundMode.noBackground = 0

no-background: foreground only; the terminal's own background shows through (BGM1).

noBackground
,
/// `spans`: emit a background only where a theme rule sets one, stopping at /// each line's last glyph (`BGM2`).
(enum value) sparkles.doc_view.ansi_model.BackgroundMode.spans = 1

spans: emit a background only where a theme rule sets one, stopping at each line's last glyph (BGM2).

spans
,
/// `full`: fill every line edge-to-edge with the theme's page background /// (`BGM3`, the default), matching a back-color-erase look.
(enum value) sparkles.doc_view.ansi_model.BackgroundMode.full = 2

full: fill every line edge-to-edge with the theme's page background (BGM3, the default), matching a back-color-erase look.

full
,
} /// The `AnsiOptions` background flags for whole-file rendering under `mode`. AnsiOptions
sparkles.doc_view.ansi_model.backgroundOptions

The AnsiOptions background flags for whole-file rendering under mode.

backgroundOptions
(
(enum) sparkles.doc_view.ansi_model.BackgroundMode

How the theme background is applied in terminal rendering (--background, hue spec BGM) — shared by the whole-file ANSI emit, the markdown-preview painter and the TUI. (Moved here from the retired previewer.d.)

BackgroundMode
mode, ColorDepth depth, bool italics)
@safe pure nothrow @nogc { return AnsiOptions( depth: depth, italics: italics, emitBackground: mode != BackgroundMode.
BackgroundMode.noBackground
noBackground
,
fillLine: mode == BackgroundMode.
BackgroundMode.full
full
,
); } /// Neutral text-attribute bits shared by the ANSI decoder (`gui_ansi`) and /// the views that consume decoded spans (`gui.d`/`document.d` map them onto /// the toolkit's `TextStyle`). enum
(enum) sparkles.doc_view.ansi_model.Attr

Neutral text-attribute bits shared by the ANSI decoder (gui_ansi) and the views that consume decoded spans (gui.d/document.d map them onto the toolkit's TextStyle).

Attr
: ubyte
{
(enum value) sparkles.doc_view.ansi_model.Attr.bold = cast(ubyte)1u
bold
= 1 << 0,
(enum value) sparkles.doc_view.ansi_model.Attr.italic = cast(ubyte)2u
italic
= 1 << 1,
(enum value) sparkles.doc_view.ansi_model.Attr.underline = cast(ubyte)4u
underline
= 1 << 2,
(enum value) sparkles.doc_view.ansi_model.Attr.strikethrough = cast(ubyte)8u
strikethrough
= 1 << 3,
} /// A maximal run of same-styled cells on one line. `fgDefault`/`bgDefault` mark /// a cell that used the terminal *default* color (SGR 39/49 or never set): the /// layout substitutes the theme's page fg/bg so ` ```ansi ` blocks stay /// theme-consistent. `text` is owned (GC) UTF-8. struct
(struct) sparkles.doc_view.ansi_model.AnsiSpan

A maximal run of same-styled cells on one line. fgDefault/bgDefault mark a cell that used the terminal default color (SGR 39/49 or never set): the layout substitutes the theme's page fg/bg so ``ansi blocks stay theme-consistent. text` is owned (GC) UTF-8.

AnsiSpan
{
(alias) object.string = string
string
(field) string sparkles.doc_view.ansi_model.AnsiSpan.text
text
;
RgbColor
(field) _error_ sparkles.doc_view.ansi_model.AnsiSpan.fg
fg
;
RgbColor
(field) _error_ sparkles.doc_view.ansi_model.AnsiSpan.bg
bg
;
bool
(field) bool sparkles.doc_view.ansi_model.AnsiSpan.fgDefault
fgDefault
;
bool
(field) bool sparkles.doc_view.ansi_model.AnsiSpan.bgDefault
bgDefault
;
ubyte
(field) ubyte sparkles.doc_view.ansi_model.AnsiSpan.attrs
attrs
;
} /// One decoded line: its styled spans, left to right. struct
(struct) sparkles.doc_view.ansi_model.AnsiLine

One decoded line: its styled spans, left to right.

AnsiLine
{
(struct) sparkles.doc_view.ansi_model.AnsiSpan

A maximal run of same-styled cells on one line. fgDefault/bgDefault mark a cell that used the terminal default color (SGR 39/49 or never set): the layout substitutes the theme's page fg/bg so ``ansi blocks stay theme-consistent. text` is owned (GC) UTF-8.

AnsiSpan
[]
(field) sparkles.doc_view.ansi_model.AnsiSpan[] sparkles.doc_view.ansi_model.AnsiLine.spans
spans
;
} /++ Gives the spans of a decoded ` ```ansi ` fence their place in the source (`SEL6`), so selecting inside the fence is as precise as in a terminal. Each line of `lines` is the decoding of the same line of `body_`. A decoded span's text is the source text between escapes, so the spans are matched against the source in order, skipping every escape sequence. A span that an escape interrupts (the decoder merges runs whose style the escape did not change) is split there. Every resulting piece is a contiguous slice of the source, which is what a pointer's column → byte mapping needs: `srcStart + byte index`. Offsets are relative to `body_`, as a fence renderer's are. Where the decoded text stops matching the source (a cursor movement, a tab the terminal expanded), the rest of that line keeps no identity rather than a wrong one. +/ TextSpan[][]
sparkles.doc_view.ansi_model.anchorAnsiLines

Gives the spans of a decoded ``ansi fence their place in the source (SEL6`), so selecting inside the fence is as precise as in a terminal.

Each line of lines is the decoding of the same line of body_. A decoded span's text is the source text between escapes, so the spans are matched against the source in order, skipping every escape sequence. A span that an escape interrupts (the decoder merges runs whose style the escape did not change) is split there. Every resulting piece is a contiguous slice of the source, which is what a pointer's column → byte mapping needs: srcStart + byte index.

Offsets are relative to body_, as a fence renderer's are. Where the decoded text stops matching the source (a cursor movement, a tab the terminal expanded), the rest of that line keeps no identity rather than a wrong one.

anchorAnsiLines
(TextSpan[][] lines, scope const(char)[]
(parameter) const(char)[] body_
body_
)
@safe pure { TextSpan[][]
_error_ anchored
anchored
;
anchored.reserve(lines.length);
(unresolved type) size_t
size_t
_error_ lineStart
lineStart
= 0;
foreach (
(parameter) line
line
; lines)
{
(unresolved type) size_t
size_t
_error_ lineEnd
lineEnd
= lineStart;
while (lineEnd < body_.length && body_[lineEnd] != '\n') ++lineEnd; TextSpan[]
_error_ out_
out_
;
(unresolved type) size_t
size_t
_error_ at
at
= lineStart;
bool
_error_ lost
lost
= lineStart > body_.length;
foreach (
(parameter) sp
sp
; line)
{
(unresolved type) size_t
size_t
_error_ i
i
= 0;
while (i < sp.
sp.text
text
.length)
{ if (!lost) at = skipEscapes(body_, at, lineEnd);
(unresolved type) size_t
size_t
_error_ j
j
= i,
_error_ k
k
= at;
if (!lost) while (j < sp.
sp.text
text
.length && k < lineEnd
&& body_[k] != '\x1b' && body_[k] == sp.
sp.text
text
[j])
{ ++j; ++k; } TextSpan
_error_ piece
piece
= sp;
if (j == i) // no longer the source: no identity from here on { lost = true; piece.
piece.text
text
= sp.
sp.text
text
[i .. $];
piece.srcStart = piece.srcEnd = size_t.
size_t.max
max
;
out_ ~= piece; break; } piece.
piece.text
text
= sp.
sp.text
text
[i .. j];
piece.srcStart = at; piece.srcEnd = k; out_ ~= piece; i = j; at = k; } } anchored ~= out_; lineStart = lineEnd + 1; } return anchored; } /// The first byte at or after `at` that is not inside an escape sequence: /// CSI (`ESC [ … final`), OSC (`ESC ] … BEL` or `ESC \`), or a two-byte /// `ESC x`. private
(alias) object.size_t = ulong
size_t
ulong sparkles.doc_view.ansi_model.skipEscapes(scope const(char)[] s, ulong at, ulong end) pure nothrow @nogc @safe

The first byte at or after at that is not inside an escape sequence: CSI (ESC [ … final), OSC (ESC ] … BEL or ESC \), or a two-byte ESC x.

skipEscapes
(scope const(char)[]
(parameter) const(char)[] s
s
,
(alias) object.size_t = ulong
size_t
(parameter) ulong at
at
,
(alias) object.size_t = ulong
size_t
(parameter) ulong end
end
)
@safe pure nothrow @nogc { while (
(parameter) ulong at
at
<
(parameter) ulong end
end
&&
(parameter) const(char)[] s
s
[
(parameter) ulong at
at
] == '\x1b')
{ if (
(parameter) ulong at
at
+ 1 >=
(parameter) ulong end
end
)
return
(parameter) ulong end
end
;
const
(local variable) const(char) kind
kind
=
(parameter) const(char)[] s
s
[
(parameter) ulong at
at
+ 1];
(parameter) ulong at
at
+= 2;
if (
(local variable) const(char) kind
kind
== '[')
{ while (
(parameter) ulong at
at
<
(parameter) ulong end
end
&& !(
(parameter) const(char)[] s
s
[
(parameter) ulong at
at
] >= 0x40 &&
(parameter) const(char)[] s
s
[
(parameter) ulong at
at
] <= 0x7E))
++
(parameter) ulong at
at
;
if (
(parameter) ulong at
at
<
(parameter) ulong end
end
)
++
(parameter) ulong at
at
; // the final byte
} else if (
(local variable) const(char) kind
kind
== ']')
{ while (
(parameter) ulong at
at
<
(parameter) ulong end
end
&&
(parameter) const(char)[] s
s
[
(parameter) ulong at
at
] != '\x07'
&& !(
(parameter) const(char)[] s
s
[
(parameter) ulong at
at
] == '\x1b' &&
(parameter) ulong at
at
+ 1 <
(parameter) ulong end
end
&&
(parameter) const(char)[] s
s
[
(parameter) ulong at
at
+ 1] == '\\'))
++
(parameter) ulong at
at
;
if (
(parameter) ulong at
at
<
(parameter) ulong end
end
)
(parameter) ulong at
at
+=
(parameter) const(char)[] s
s
[
(parameter) ulong at
at
] == '\x07' ? 1 : 2;
} } return
(parameter) ulong at
at
;
} @("ansi_model.anchorAnsiLines.eachPieceIsTheSourceItCameFrom") @safe pure unittest { // 0 1 2 // 0123456789012345678901234 const
(local variable) const(string) body_
body_
= "\x1b[31mred\x1b[0m plain\nnext\n";
auto
(local variable) _error_ decoded
decoded
= [
[TextSpan("red"), TextSpan(" plain")], [TextSpan("next")], ]; const
(local variable) const(_error_) a
a
= anchorAnsiLines(decoded, body_);
assert(a.length == 2); assert(a[0][0].
(field) _error_ (__error)[0].text
text
== "red" && a[0][0].srcStart == 5 && a[0][0].srcEnd == 8);
assert(
(local variable) const(string) body_
body_
[a[0][1].srcStart .. a[0][1].srcEnd] == " plain");
assert(
(local variable) const(string) body_
body_
[a[1][0].srcStart .. a[1][0].srcEnd] == "next");
} @("ansi_model.anchorAnsiLines.aSpanAnEscapeInterruptsIsSplitThere") @safe pure unittest { // A reset that changes nothing visible: the decoder reports one run "ab", // but its bytes are not contiguous in the source. const
(local variable) const(string) body_
body_
= "a\x1b[0mb";
const
(local variable) const(_error_) a
a
= anchorAnsiLines([[TextSpan("ab")]], body_);
assert(a[0].length == 2, "split at the escape"); assert(a[0][0].
(field) _error_ (__error)[0].text
text
== "a" && a[0][0].srcStart == 0 && a[0][0].srcEnd == 1);
assert(a[0][1].
(field) _error_ (__error)[1].text
text
== "b" && a[0][1].srcStart == 5 && a[0][1].srcEnd == 6);
// An OSC hyperlink around text is skipped the same way. const
(local variable) const(string) link
link
= "\x1b]8;;https://x\x07go\x1b]8;;\x1b\\";
const
(local variable) const(_error_) b
b
= anchorAnsiLines([[TextSpan("go")]], link);
assert(
(local variable) const(string) link
link
[b[0][0].srcStart .. b[0][0].srcEnd] == "go");
} @("ansi_model.anchorAnsiLines.textThatIsNotTheSourceKeepsNoIdentity") @safe pure unittest { // A terminal-expanded tab: the decoded spaces are not in the source. const
(local variable) const(_error_) a
a
= anchorAnsiLines([[TextSpan("x"), TextSpan(" y")]], "x\ty");
assert(a[0][0].srcStart == 0 && a[0][0].srcEnd == 1, "the match holds"); assert(a[0][1].srcStart ==
(ulong) ulong
size_t
.max, "and stops where the text diverges");
assert(a[0][1].
(field) _error_ (__error)[1].text
text
== " y", "the text itself is kept");
}