welcome

package
v0.0.24 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package welcome renders the first-run splash for the mecatui zero-state: a faithful mascot (half-block on any truecolor terminal, a zero-dependency kitty Unicode-placeholder high-res path on capable terminals), a gradient "mecatl" wordmark, and an info block. It imports ONLY the theme package, the charm libraries, and the standard library — never client or ui — so it slots beneath the ui package without violating the inward-only convention (ui imports it).

The splash is the un-framed BODY; the caller (ui.help.go) wraps it in the shared centerCard treatment. It shows on the empty zero-state and vanishes the instant the first conversation block is appended (the existing isEmpty guard in ui.View).

Index

Constants

View Source
const MascotImageID = 0x6D63 // 'm','c' — arbitrary but stable

MascotImageID is the fixed Kitty graphics image ID the splash transmits the mascot under and references from the placeholder grid. A constant (not a per-run counter) is fine: the splash transmits at most one image, and a stable id lets a re-transmit on a size-tier change replace the same slot rather than leak placements.

Variables

This section is empty.

Functions

func DecodeMascot

func DecodeMascot() (image.Image, error)

DecodeMascot decodes the embedded mascot PNG exactly once and caches the result. The zero-state re-renders on every frame, so decoding the 1254×1254 image per frame would be wasteful; sync.Once makes it a one-time cost. Both the decoded image and any decode error are memoised, so a corrupt embed fails the same way every call (and lets callers fall back cleanly).

func DeleteMascot

func DeleteMascot() string

DeleteMascot builds the escape that deletes the mascot image (a=d, by id) so the terminal frees the held image once the splash leaves. The ui reducer fires it on the empty→non-empty (splash→first-block) transition, via tea.Raw like the transmit. The image would stop PAINTING regardless once the placeholder cells leave the content (Unicode placeholders only paint where their cells are), so this is about freeing the terminal-side resource and keeping the transmit/delete lifecycle symmetric, not about hiding a lingering image.

func HalfBlockMascot

func HalfBlockMascot(cols int, bg rgb, margin int) string

HalfBlockMascot returns the memoised half-block render of the mascot at the given column count over background bg, indented by margin spaces. cols → a cols×cols sampling → cols/2 cell-rows (square). It decodes the mascot lazily (DecodeMascot is itself cached) and returns "" if decoding fails, so the caller degrades to a mascot-less splash rather than panicking. The result is cached by (cols, bg) so an idle zero-state re-render is a map lookup, not a downscale.

Note the cache key omits margin: margin is applied AFTER the cached body via a cheap per-line re-indent, so different margins share one downscale.

func KittyCapable

func KittyCapable() bool

KittyCapable is the exported entry point for the ui caller: it reports whether the terminal is likely Kitty-graphics-capable (see kittyCapable). Conservative and env-based; a miss falls back to the always-correct half-block path.

func ObsidianBG

func ObsidianBG() color.Color

ObsidianBG is the exported mascot keyed-background colour, for any caller that needs to match the card surround.

func PlaceholderGrid

func PlaceholderGrid(cols, rows, margin int) string

PlaceholderGrid builds the IN-CONTENT placeholder cell grid that paints the already-transmitted virtual image. It is rows×cols cells of kitty.Placeholder (U+10EEEE, a width-1 rune — verified), each cell carrying:

  • the image ID in its FOREGROUND colour (the Kitty Unicode-placeholder spec encodes the 24-bit image id as an RGB foreground: r=id>>16, g=id>>8, b=id),
  • a ROW diacritic and a COLUMN diacritic (kitty.Diacritic(row)/(col)) so the terminal knows which image cell each placeholder maps to.

margin spaces indent each row to match the half-block path's placement, so the splash layout is identical whichever mascot path is active (same cols×rows footprint). The grid is what welcome.Splash emits INSTEAD of the half-block mascot when kitty is active.

func Splash

func Splash(th theme.Theme, in Info, width, height int) string

Splash assembles the UN-framed welcome body and GUARANTEES it fits the height budget: mascot (top, sized by Tier and dropped entirely when it can't fit) · gradient wordmark · title · hint · then the OPTIONAL info sections (cwd / model·provider / version / tagline / affordances / memory) included greedily by keep-priority only while they still fit `height - cardChrome`. The caller frames it (centerCard, which adds cardChrome rows). The literal "Welcome to mecatui" always appears (a test greps it).

width/height are the conversation region dims. On a tiny region (width < minSplashWidth or height < minSplashHeight) it degrades to a minimal hint (title + prompt hint + affordances) that never panics. Whenever height > 0 the returned body satisfies lipgloss.Height(body) <= height - cardChrome, so it never clips.

func Tier

func Tier(width, height int) (cols, rows int)

Tier is the SINGLE mascot-footprint authority, used by Splash (to render the mascot) AND by the ui's maybeKittyTransmit (to bake the kitty virtual placement) — they MUST agree on (cols, rows) or the transmit and the placeholder grid would disagree. It is a pure function of (width, height): it picks the LARGEST mascot (60 → 48 → 36) whose width fits (cols + the left margin) AND whose cell-rows (cols/2) plus essentialReserve fit the height. It returns (0, 0) when even the smallest mascot can't fit — the caller then renders mascot-less (wordmark + head + whatever optional content fits). rows is always cols/2 (the square relation: cols×cols px sampling → cols/2 half-block cell-rows).

func TransmitMascot

func TransmitMascot(cols, rows int) string

TransmitMascot builds the OUT-OF-BAND Kitty escape that transmits the mascot image data to the terminal AND creates a virtual placement (a=T + U=1) under MascotImageID at cols×rows cells. The action MUST be transmit-and-put (a=T): placement keys (U=1, c=, r=) are only honoured by a put-style action — under a bare transmit (a=t) they are inert, the terminal stores the image with no placement, and the placeholder grid paints nothing (issue #44). A VIRTUAL placement never paints at the cursor, so a=T here still produces no visible output and no cursor movement. It is a control sequence: it MUST be written via tea.Raw (NOT placed in View content, where the ultraviolet renderer would parse it into cells and desync the cursor), so interleaving it with frames is harmless.

The mascot is first DOWNSCALED to the cols×rows cell footprint (see downscaleMascot) before being re-encoded to PNG. This matters on Ghostty: the full 1254×1254 PNG is ~1 MB across ~265 base64 chunks, and the exact a=T/U=1/U+10EEEE virtual-placement pattern is known-buggy on Ghostty 1.3.1 stable (ghostty-org/ghostty#13056) where a large transmit can render at a fraction of its intended width. A small, target-resolution PNG (~tens of KB, a handful of chunks) sidesteps the worst of that — the terminal's own cell-size scaling is never invoked because the image is already at the cell footprint's pixel dimensions. The data is chunked at kitty.MaxChunkSize. Returns "" if the image can't be decoded (caller then keeps the half-block path).

func Wordmark

func Wordmark(th theme.Theme, fullColor bool) string

Wordmark lays the "mecatl" block glyphs side-by-side into three rows and colours them. When fullColor is true AND both gradient stops resolve, it applies a per-grapheme-cluster jade→gold gradient computed with lipgloss.Blend1D over the rendered column count — so the colour sweeps smoothly left to right and the step count exactly matches the rendered width (an off-by-one would leave a visible seam). Otherwise it falls back to rendering EVERY glyph in the single accent colour — never unstyled, never the raw blend. The result is three newline-joined rows, no trailing newline.

Types

type Info

type Info struct {
	Cwd      string // workspace path (already display-trimmed by the caller if needed)
	Model    string // active model id ("" → omit the line)
	Provider string // provider name ("" → just the model)
	Version  string // mecatui build version ("" → omit)
	Tagline  string // one-line tagline under the identity ("" → omit)
	Submit   string // live submit chord used by the prompt hint ("" → "enter")

	// Affordances are the pre-rendered (themed) affordance rows the caller builds
	// from its caps-tailored zeroStateRows(), so the welcome card's chord list stays
	// byte-equivalent to the legacy card. Each entry is one full line.
	Affordances []string

	// MemoryNote is the pre-rendered "memory is on" line (already themed), or "" when
	// memory is off — the single caps-conditional content the card carries.
	MemoryNote string

	// GatewayNote is the pre-rendered "gateway detected" line (already themed), or
	// "" when no intent-driven provider is available-but-not-default. It surfaces a
	// no-API-key alternative at the moment a new operator is most attentive.
	GatewayNote string

	// FullColor is true on a truecolor terminal: the wordmark then gets the
	// jade→gold gradient; otherwise it collapses to the single accent colour.
	FullColor bool

	// Kitty is true when the terminal supports the Kitty graphics protocol and the
	// caller has transmitted the mascot image: the splash then emits the
	// placeholder-cell grid (high-res image) instead of the half-block mascot, at
	// the SAME cols×rows footprint.
	Kitty bool
}

Info is the presentation-ready context the splash renders, assembled by the caller (ui.help.go) from the model + relayed capabilities. The welcome package stays decoupled from ui/client: it receives plain strings + bools, not a Model.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL