Documentation
¶
Overview ¶
Package bubbleteandmd renders streaming LLM Markdown in a terminal, for bubbletea programs. Finished blocks are rendered once and cached; only the open tail is rendered again as tokens arrive, and that tail is healed (an open fence, `**`, a half-typed link) so raw syntax does not show mid-stream. It reads llx-markdown's directive syntax (`> [!STEPS]`, `> [!TABS]`, ```tree fences …). See README.md.
Index ¶
- Constants
- Variables
- func DisableLexTimeout()
- func Sanitize(src string) string
- type Action
- type Cache
- type DetailsState
- type DirectiveRenderer
- type Doc
- func (d *Doc) Blur()
- func (d *Doc) Do(a Action) bool
- func (d *Doc) Final(src string) (string, error)
- func (d *Doc) Focus() bool
- func (d *Doc) FocusLast() bool
- func (d *Doc) Focused() (Stop, bool)
- func (d *Doc) Frame(src string) (string, error)
- func (d *Doc) HandleKey(msg tea.KeyPressMsg, km KeyMap) bool
- func (d *Doc) Height() int
- func (d *Doc) Lines() int
- func (d *Doc) Offset() int
- func (d *Doc) Reset()
- func (d *Doc) SetHeight(h int)
- func (d *Doc) State() *State
- func (d *Doc) Stops() []Stop
- func (d *Doc) View() string
- type GlamourOptions
- type KeyMap
- type Mark
- type MarkKind
- type Renderer
- type State
- type StateRenderer
- type Stop
- type StopKind
- type Stream
- type TabsState
- type TreeState
Constants ¶
const ( MarkHeader = directive.MarkHeader MarkPanel = directive.MarkPanel MarkRow = directive.MarkRow )
Variables ¶
var Actions = []Action{ActionNext, ActionPrev, ActionToggle, ActionLeft, ActionRight, ActionLeave, ActionScrollUp, ActionScrollDown, ActionPageUp, ActionPageDown, ActionTop, ActionBottom}
Actions is every Action, in the order a key is looked up and a help list shows them.
var ErrTrimmed = errors.New("bubbleteandmd: Renderer output ends a paragraph without a newline; pass a raw *glamour.TermRenderer")
ErrTrimmed is returned by Frame and Final when the Renderer ends a paragraph without its newline (a wrapper that trims glamour's output): the blocks cannot be joined as the whole document renders.
Functions ¶
func DisableLexTimeout ¶ added in v0.4.1
func DisableLexTimeout()
DisableLexTimeout stops regexp2's match timeouts in the whole process, so chroma never draws valid code with an Error token because of a timeout.
Why: chroma (v2.27.0) gives every lexer rule a 250 ms regexp2 MatchTimeout on a wall clock and reads a timeout as no match, so the next character gets the Error style (a red background in most styles). A goroutine that is off the CPU for 250 ms in the middle of a match (a loaded machine, many goroutines) times out on code that is valid.
Trade-off: the setting is process-wide (regexp2 has no per-call setting). It removes chroma's guard, and the guard of every other regexp2 user in the process, against a runaway (catastrophic backtracking) match: such a match then runs until it ends.
Opt-in: the library never calls it. Call it once at program start, before any rendering or other regexp2 use (it writes a regexp2 setting without a lock).
func Sanitize ¶ added in v0.2.0
Sanitize removes from src what a terminal reads as a command, not text, so model output cannot write the clipboard (OSC 52), set the title, move the cursor, clear the screen or overwrite a line. glamour v2.0.1 passes these through unchanged, in prose, tables and code.
Removed:
- C0 controls (U+0000–U+001F) except tab and newline; "\r\n" becomes "\n" and any other "\r" goes too. ESC goes, so the text of an escape sequence after it stays as plain characters.
- DEL (U+007F) and C1 controls (U+0080–U+009F), among them CSI U+009B.
- bytes that are not UTF-8 (a raw 0x9B byte is one).
- an incomplete UTF-8 sequence at the end of src: it can complete in the next chunk, so it is held back, not shown as an error.
- Unicode bidi embeddings, overrides and isolates (U+202A–U+202E, U+2066–U+2069): they reorder the text shown, so a line can read differently from its bytes (spoofing).
Everything else stays, ZWJ and emoji included.
Sanitize is prefix-stable: for any cut k, Sanitize(s[:k]) is a prefix of Sanitize(s), so a stream that grows keeps its rendered blocks. It is idempotent. Stream.Frame and Stream.Final call it; a caller that renders model text some other way must call it first.
Types ¶
type Action ¶ added in v0.4.0
type Action string
Action is a named thing a key does to a Doc.
const ( // ActionNext and ActionPrev move the focus to the next or previous // stop. Past the last (first) stop the Doc blurs and the action is not // consumed, so the host moves its own focus on. ActionNext Action = "focus.next" ActionPrev Action = "focus.prev" // ActionToggle folds or opens a details form or the tree folder at the // cursor, or switches tabs between stacked and one-tab view. ActionToggle Action = "toggle" // ActionLeft and ActionRight: tabs, the previous or next panel (from // stacked, the first press switches to one-tab view at the selected // panel); tree, the cursor to the previous or next drawn folder. Both // wrap. ActionLeft Action = "move.left" ActionRight Action = "move.right" // ActionLeave blurs the Doc and is not consumed: the host still does // the key's job (esc). ActionLeave Action = "focus.leave" // The scroll actions move the view of a Doc with a height by a line, a // page (the height), or to the top or bottom. They act with or without // focus, and are consumed at an edge too (nothing moves). At height 0 // they are not consumed: the host scrolls. ActionScrollUp Action = "scroll.up" ActionScrollDown Action = "scroll.down" ActionPageUp Action = "page.up" ActionPageDown Action = "page.down" ActionTop Action = "scroll.top" ActionBottom Action = "scroll.bottom" )
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache holds rendered stable blocks, keyed by (Stream key, block source). One Cache can serve many Streams; it is safe for concurrent use.
type DetailsState ¶ added in v0.4.0
type DetailsState = directive.DetailsState
TabsState, DetailsState and TreeState are the state of one form.
type DirectiveRenderer ¶ added in v0.2.0
type DirectiveRenderer struct {
// contains filtered or unexported fields
}
DirectiveRenderer renders Markdown as glamour does, and draws llx-markdown's directive forms (callouts, steps, tabs, stats, pros and cons, details, sources, quotes with a source, file trees, titled code fences) and `$$` formulas. It satisfies Renderer, so NewStream can stream it. A document with no form and no `$$` formula renders exactly as glamour renders it.
A DirectiveRenderer is used by one goroutine at a time.
func NewRenderer ¶ added in v0.2.0
func NewRenderer(style ansi.StyleConfig, width int, opts ...glamour.TermRendererOption) (*DirectiveRenderer, error)
NewRenderer returns a renderer for style at word wrap width. opts go to glamour after the style, the word wrap and the chroma formatter; they must not change the style or the word wrap.
Code blocks use chroma formatter "bubbleteandmd-terminal256" (same bytes as "terminal256", style tables cached; chromafmt.go). The first NewRenderer in a process registers it in chroma's formatter registry, which has no lock: make it before other goroutines render with chroma.
func (*DirectiveRenderer) GlamourOptions ¶ added in v0.3.0
func (r *DirectiveRenderer) GlamourOptions() (ansi.Options, bool)
GlamourOptions returns the options of the glamour renderer that draws plain Markdown at the top level: the style, the word wrap and the chroma formatter. ok is false when NewRenderer was given glamour options: they cannot be read back. Stream uses it to render a growing table from cached rows (GlamourOptions).
func (*DirectiveRenderer) Render ¶ added in v0.2.0
func (r *DirectiveRenderer) Render(md string) (string, error)
Render renders md. An md that starts with join.Sentinel (as Stream sends every block after the first) renders its first block in not-first place.
func (*DirectiveRenderer) RenderState ¶ added in v0.4.0
RenderState renders md as Render does, with each form drawn in its state in st (nil: as streamed), and returns the marks of the interactive parts, lines counted in out (StateRenderer). offset is the document offset of md's text after the sentinel: form keys are document keys (form offsets in the source as written, before `$$` formulas are rewritten). md should be sanitized already, else offsets move.
func (*DirectiveRenderer) WideBlocks ¶ added in v0.4.0
func (r *DirectiveRenderer) WideBlocks(pane int)
WideBlocks turns wide blocks on: top-level code blocks and tables wider than the word wrap width render at their own width and are cut at pane. A pane below the word wrap width is read as that width; 0 turns wide blocks off (the default). Call it before the first Render. The Stream key must name pane too.
Not in wide blocks: code blocks and tables inside lists, quotes and form bodies; titled code fences and file trees (forms: cut at the width, as before); a table with a row the width model does not read (an email address, an emoji shortcode) or with glamour options given to NewRenderer.
type Doc ¶ added in v0.4.0
type Doc struct {
// contains filtered or unexported fields
}
Doc is one reply with interactive forms: a Stream that draws in a State the Doc holds, the stops (forms that take keys) of its last output, and the actions that change the State. It is not a tea.Model: Do and HandleKey report whether the key was consumed, and the host handles a key that was not (Carl 2026-11-11).
The layout stays as streamed until an action changes it: Frame and Final draw the zero State, and the end of the stream changes no form. One exception: a table tail that Frame held as a placeholder (fewer than 8 body rows, tablehold.go) is drawn by Final. Focus alone changes no line count or width (a same-width cue only). After an action that returns true, render again (Frame or Final with the same source) to show it; Stops and Focused are those of the last render. A scroll action needs no render: View shows it.
SetHeight > 0 makes the Doc a viewport that owns the scroll (viewport.go).
A Doc is not safe for concurrent use.
func NewDoc ¶ added in v0.4.0
NewDoc returns a Doc that renders through NewStream(r, key, c). r should be a StateRenderer (*DirectiveRenderer); with any other Renderer the Doc has no stops.
func (*Doc) Blur ¶ added in v0.4.0
func (d *Doc) Blur()
Blur takes the focus away; Focus returns to the same stop.
func (*Doc) Do ¶ added in v0.4.0
Do applies an action to the focused stop and reports whether it was consumed. With no focus nothing is consumed (the host calls Focus to give the Doc the keys). An action that leaves the Doc (ActionNext past the last stop, ActionPrev before the first, ActionLeave) blurs it and returns false, so the host still acts on the key. A scroll action is consumed when the Doc has a height (viewport.go), with or without focus.
func (*Doc) Final ¶ added in v0.4.0
Final is Stream.Final in the Doc's State; it updates the stops and returns View.
func (*Doc) Focus ¶ added in v0.4.0
Focus focuses the stop focused before the last Blur when it is still a stop, else the first. False when there is no stop.
func (*Doc) FocusLast ¶ added in v0.4.0
FocusLast focuses the last stop (the host's focus arrives from below). False when there is no stop.
func (*Doc) Frame ¶ added in v0.4.0
Frame is Stream.Frame in the Doc's State; it updates the stops and returns View (with a height, only the view).
func (*Doc) HandleKey ¶ added in v0.4.0
func (d *Doc) HandleKey(msg tea.KeyPressMsg, km KeyMap) bool
HandleKey does the action msg is bound to in km and reports whether the key was consumed (Do). False: the host handles the key.
func (*Doc) Lines ¶ added in v0.4.0
Lines is the number of lines in the last output (all of it, not the view).
func (*Doc) Offset ¶ added in v0.4.0
Offset is the first output line the view shows (0 when the height is 0). Stop lines are counted in the whole output: a stop shows when its lines are in [Offset, Offset+Height).
func (*Doc) Reset ¶ added in v0.4.0
func (d *Doc) Reset()
Reset clears all state: the layout as streamed, nothing focused. The height and the view's offset stay.
func (*Doc) SetHeight ¶ added in v0.4.0
SetHeight sets the number of lines the Doc shows (0 or less: all of them). A view at the bottom stays at the bottom. The view opens on the first output it shows: at the top when the output is taller than h, else at the bottom (it then follows a stream as it grows). That output is the current one when SetHeight comes after a render, else the next render's, so the order of SetHeight and Final does not matter. Width is the host's: for a new width make a new Renderer and Doc and copy the State (Doc.State).
func (*Doc) State ¶ added in v0.4.0
State returns the Doc's State. The host may read it (State.Focused in notty, where focus has no colour); a change to it shows on the next render. On a width change the host makes a new Renderer and Doc and copies the State: `*nd.State() = *old.State()`. Form keys are byte offsets in the source, the same at every width, so the forms keep their folds, tabs and focus. The copy shares the State's maps; use old no more.
type GlamourOptions ¶ added in v0.3.0
GlamourOptions is an optional interface of a Renderer: the glamour options it renders a plain Markdown table with. ok is false when they are not known (for example, glamour options were given that cannot be read back). When ok, Stream renders a growing table in the tail from cached rows (internal/tablestream), with the same bytes as r.Render; without it, the tail renders whole on every call. *DirectiveRenderer implements it.
type KeyMap ¶ added in v0.4.0
KeyMap binds each Action to keys, named as tea.KeyPressMsg.String names them ("tab", "shift+tab", "enter", "space", "left"). An action with no keys is off. The scroll keys act only in a Doc with a height; at height 0 HandleKey returns false for them and the host scrolls.
func DefaultKeyMap ¶ added in v0.4.0
func DefaultKeyMap() KeyMap
DefaultKeyMap: tab and shift+tab move between stops, enter and space toggle, ←/→ change the tab or the tree folder, esc leaves; with a height, ↑/↓ scroll a line, pgup/pgdown a page, home/end to the top/bottom.
type Mark ¶ added in v0.4.0
Mark is where an interactive part of a form is drawn: the form's Key, what part (MarkKind), its first line and its height in lines.
type Renderer ¶
Renderer renders one whole Markdown document. *glamour.TermRenderer satisfies it. A Renderer is used by one goroutine at a time.
Pass a raw *glamour.TermRenderer (or a DirectiveRenderer), not a wrapper that changes its output: Stream renders each block alone and joins the pieces by the document prefix, suffix and line ends glamour writes. A wrapper that trims the newlines makes Frame and Final return ErrTrimmed. Do pre-processing on src before Frame; do post-processing on the output of Frame and Final.
type State ¶ added in v0.4.0
State is the per-form state of one document (folded details, selected tab, closed tree folders, the focused form), keyed by form key: the byte offset of a top-level form in the document, "<parent key>/<slot>@<offset>" for a form in a body. The zero State and a nil *State draw the layout as streamed (Carl 2026-11-11). Only the host changes it.
type StateRenderer ¶ added in v0.4.0
type StateRenderer interface {
Renderer
RenderState(md string, offset int, st *State) (string, []Mark, error)
}
StateRenderer is an optional interface of a Renderer: it draws each form in its State and tells where the interactive parts are. RenderState(md, offset, nil) returns what Render(md) returns. offset is the document offset of md's text (after join's sentinel, when md has it), so form keys are document keys; mark lines are counted in the output. *DirectiveRenderer implements it.
type Stop ¶ added in v0.4.0
Stop is one form that takes keys, as drawn in the last output: Key is its State key; Line and Height are the lines of its marks (details: the title lines; tabs: the header and the drawn panels; tree: the drawn rows), counted in that output.
type Stream ¶
type Stream struct {
// contains filtered or unexported fields
}
Stream renders one document that grows at its end. Blocks before the last two are rendered once and cached; the block before the last is rendered when its source changes (the splitter can still reopen it); the last block (the tail) is rendered on every call. Both first remove terminal controls (Sanitize). Frame heals the tail; Final does not. The result equals r.Render of the whole sanitized (and healed, for Frame) text, with one exception (Carl B1): while a code fence of more than codestream.ChunkLines lines grows in the tail, Frame renders it in chunks, and SGR bytes (colours at a chunk seam) can differ; lines and their text do not. Final is exact. A growing table tail renders from cached rows, exact, when the Renderer tells its options (GlamourOptions).
With a StateRenderer, every block renders in the State given to SetState (Final equals RenderState of the whole sanitized text), and Marks returns where the interactive parts of the last output are.
A Stream is not safe for concurrent use.
func NewStream ¶
NewStream returns a Stream that renders with r and caches stable blocks in c under key. key must name everything that changes r's output (dun: width and style). c may be shared by Streams; nil means no cache.
func (*Stream) Frame ¶
Frame renders src while it is still streaming: the tail is healed (heal.Close), so half-written syntax does not show raw.
func (*Stream) Marks ¶ added in v0.4.0
Marks returns the marks of the last Frame or Final: where each interactive part is drawn, lines counted in that output. nil when the Renderer is not a StateRenderer, or the last call failed.
func (*Stream) SetState ¶ added in v0.4.0
SetState sets the State the next Frame and Final draw forms in (nil: as streamed). It is read on every call, so a change to *st shows on the next call; blocks whose part of the State did not change still come from the cache. It has an effect only when the Renderer is a StateRenderer.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
blocks
Package blocks cuts a Markdown document into top-level blocks that can be rendered, and cached, one by one.
|
Package blocks cuts a Markdown document into top-level blocks that can be rendered, and cached, one by one. |
|
codestream
Package codestream renders one growing fenced code block in chunks, so a frame does not re-render (and re-lex) the whole block.
|
Package codestream renders one growing fenced code block in chunks, so a frame does not re-render (and re-lex) the whole block. |
|
corpus
Package corpus holds the streaming test corpora, the seeded reply generator and the chunking helpers ported from llx-markdown, for invariant tests.
|
Package corpus holds the streaming test corpora, the seeded reply generator and the chunking helpers ported from llx-markdown, for invariant tests. |
|
directive
Package directive reads llx-markdown's directive forms out of one top-level block: callouts, steps, tabs, stats, pros and cons, folded sections, sources, quotes with a source, file trees and titled code fences.
|
Package directive reads llx-markdown's directive forms out of one top-level block: callouts, steps, tabs, stats, pros and cons, folded sections, sources, quotes with a source, file trees and titled code fences. |
|
heal
Package heal stabilizes the tail of a markdown text that is still streaming.
|
Package heal stabilizes the tail of a markdown text that is still streaming. |
|
join
Package join puts separately rendered glamour blocks together so that the result equals glamour's render of the whole document.
|
Package join puts separately rendered glamour blocks together so that the result equals glamour's render of the whole document. |
|
lextimeout
Package lextimeout stops regexp2's match timeouts in the process.
|
Package lextimeout stops regexp2's match timeouts in the process. |
|
lru
Package lru is a small bounded least-recently-used cache.
|
Package lru is a small bounded least-recently-used cache. |
|
tablestream
Package tablestream renders a growing GFM table block in less time than a whole render, with the same bytes.
|
Package tablestream renders a growing GFM table block in less time than a whole render, with the same bytes. |
|
Package prompt holds the instructions that teach a model to write the directive forms the renderer draws.
|
Package prompt holds the instructions that teach a model to write the directive forms the renderer draws. |