Raxol. UI. Rendering. PaintAuthority. FlatAuthority
(Raxol v2.6.1)
View Source
The degradation ladder's :flat tier: the append-only, zero-region,
zero-cursor-jump PaintAuthority implementation. Picked by
Raxol.UI.Rendering.PaintAuthority.ModeSelect.select/3 for TERM=dumb,
non-tty, CI-without-tty, and degenerate-geometry sessions — the
screen-reader answer, the CI/pipe answer, the block-hater answer.
The SGR/escape decision: zero escape bytes, enforced IN THIS MODULE
Unlike InlineAuthority (which never emits a full clear but DOES emit
DECSTBM/CUP/cursor-save bytes) this module never writes ANY byte in the
C0 control range (0x00-0x1F, which includes 0x1B/ESC) except
\t, \r, \n — not just cursor movement, but also SGR styling and
bare control bytes like BEL. This is a deliberate v1 simplification, not
an oversight, AND it is a property append_sealed/2 enforces itself by
scrubbing those bytes out of the iodata before it ever reaches the
device — it is not a contract callers have to uphold on their own.
Content flowing into this authority can originate from an LLM response,
a tool's stdout, or any other untrusted source; a caller forgetting to
sanitize it must not be able to smuggle a screen clear or a color
change through the flat tier.
- A conditionally-styled flat mode (SGR when the destination happens
to be a real tty, plain when it's a genuine pipe) would need its
own tty-detection seam threaded into the AUTHORITY itself, when
ModeSelectalready computed that exact fact once, at startup, to pick:flatin the first place. Re-deriving it a second time inside the authority (or plumbing the:tty?flag through as authority state) is more machinery than a v1 flat writer needs. - Zero escape bytes is also the easiest claim to make PROVABLY true:
the mechanical acceptance check ("flat output contains no
cursor-move/CUP/scroll sequences") reduces to "every scanned token
is
{:text, _}" with no exceptions to special-case for allowed SGR runs — and now that the scrub happens inside the module, that property holds regardless of what a caller passes in, not just for well-behaved callers. - Scrubbing the ESC lead byte and leaving the rest of a hostile
sequence's bytes untouched (e.g.
\e[31mbecomes the visible fragment[31m) is an intentional, HONEST failure mode: a reader sees garbled-looking text and knows something was stripped. The alternative — swallowing the whole sequence body, or worse, leaving it byte-for-byte intact — risks an invisible injection that changes what a downstream pipe or screen reader does without any visible trace. A visible fragment is strictly safer than a silent one. - This is additive, not a rewrite, to upgrade later: a future unit
that wants styled flat output for a genuine tty destination can
thread
ModeSelect's already-computed:tty?fact intonew/3as an opt-in flag and add SGR emission ONLY on that path, without touching the append-only/no-region/no-cursor contract this module ships today.
Line-terminator convention: plain \n
InlineAuthority.seal/2 requires \r\n-terminated content because a
real terminal in raw mode needs the explicit carriage return (output
translation is off). :flat's destination is a pipe, a log file, a
screen reader, or a CI capture buffer — none of which need or want the
extra \r. Callers building content for FlatAuthority should
terminate each line with plain \n. This module does not enforce or
rewrite terminators itself — \t, \r, and \n are all exempt from
the C0 scrub described above, so both conventions pass through
unchanged (exactly like PaintAuthority.IOAuthority's minimal-stub
convention for everything outside that scrub) — the \r\n vs. \n
choice lives entirely in what the CALLER passes in, so a caller that
already has \r\n-terminated content (e.g. replaying the same
fixture through both tiers for a parity check) gets it written through
unchanged rather than silently rewritten.
What every callback does
append_sealed/2— scrubs C0 control bytes (except\t/\r/\n) out ofiodata, then writes what remains. No CUP, ever.repaint_footer/2/keyframe_footer/2— NO-OP (return state unchanged, write nothing). Flat has no footer to repaint or keyframe; a caller that calls these on a flat authority gets silence, not stray bytes.with_cursor/3— runsfundirectly against the state. No save, no restore: there is no cursor position worth protecting when nothing ever moves it.resize/3— updates the trackedwidth/heightbut writes zero bytes. There is no region to re-pin.region_top/1— returnsheight: with no footer carved out, the entire screen is (conceptually) history/content.
Summary
Types
@type t() :: %Raxol.UI.Rendering.PaintAuthority.FlatAuthority{ device: IO.device(), height: pos_integer(), width: pos_integer() }
Functions
@spec new(IO.device(), pos_integer(), pos_integer()) :: t()
Builds a new flat authority state. No device writes happen at construction.
Sugar mirroring InlineAuthority.seal/2's call shape so a caller can
swap authorities without changing its call site: seal/2 is exactly
append_sealed/2 here (no cursor bracket to wrap it in).