# Image Parachute

> For agents: start with the [agent guide](https://pi-tui.ratstack.sh/llms.txt). Every page here has a Markdown twin, and every pattern has a JSON record.

Presentation / Tool and message output · `image-parachute` · [HTML](https://pi-tui.ratstack.sh/patterns/image-parachute/) · [JSON](https://pi-tui.ratstack.sh/patterns/image-parachute.json) · [all patterns](https://pi-tui.ratstack.sh/patterns.md)

Also known as Inline image fallback.

## Intent

Render terminal images where supported and text placeholders otherwise.

## Motivation

Nico's inline-image component encounters terminals and modes with different graphics support.

## Applicability

- Use this when custom output or staged previews contain images.

## Structure

```text
image + capabilities + mode
  supported -> image
  otherwise -> placeholder
```

## Participants

- [`Image`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/tui/README.md#image): Renders an inline image or unsupported-terminal placeholder.
- [`detectCapabilities`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/tui/README.md#image): Reports detected terminal graphics capabilities.
- `Renderer mode`: Restricts image presentation where protocol repainting is unsupported.

Pi component APIs: [`Image`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/tui/README.md#image), [`detectCapabilities`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/tui/README.md#image), `setCapabilities`, `TuiAltScreen`

## Consequences

- Supported terminals show images while other paths remain readable as text.
- Fallback paths cannot provide the same visual image detail.

## Implementation

- Capabilities depend on terminal protocol and renderer mode.
- Fullscreen iTerm2 uses placeholders rather than repainting inline image placements.
- Do not infer unchanged Ghostty heuristics from declarations alone.

## States

Each state was rendered at 40, 60, 80 and 120 columns in Pi's dark and light themes. Each frame links one WebP. An animated frame links its first image, then the animation.

### Kitty PNG

`supported`: Show a generated synthetic thumbnail under supported Kitty capabilities.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ pass

Frames:

- 40 columns: ![Image Parachute, Kitty PNG, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/supported-40-dark.32014cbe6f5e.webp) ![Image Parachute, Kitty PNG, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/supported-40-light.c241a9a2c1f8.webp)
- 60 columns: ![Image Parachute, Kitty PNG, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/supported-60-dark.3d1f69521699.webp) ![Image Parachute, Kitty PNG, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/supported-60-light.52ab315e6b80.webp)
- 80 columns: ![Image Parachute, Kitty PNG, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/supported-80-dark.9a344bf059f2.webp) ![Image Parachute, Kitty PNG, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/supported-80-light.b79535a365df.webp)
- 120 columns: ![Image Parachute, Kitty PNG, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/supported-120-dark.0a042b82782e.webp) ![Image Parachute, Kitty PNG, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/supported-120-light.65294bb37e2a.webp)

### iTerm2 PNG

`iterm`: Show the same thumbnail through the inline iTerm2 image protocol.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ pass

Frames:

- 40 columns: ![Image Parachute, iTerm2 PNG, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-40-dark.8c384674eea9.webp) ![Image Parachute, iTerm2 PNG, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-40-light.5c7e4d381459.webp)
- 60 columns: ![Image Parachute, iTerm2 PNG, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-60-dark.bc5a53667aa1.webp) ![Image Parachute, iTerm2 PNG, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-60-light.2234b24fd519.webp)
- 80 columns: ![Image Parachute, iTerm2 PNG, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-80-dark.248424235682.webp) ![Image Parachute, iTerm2 PNG, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-80-light.2177a0451d88.webp)
- 120 columns: ![Image Parachute, iTerm2 PNG, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-120-dark.e944e6416f05.webp) ![Image Parachute, iTerm2 PNG, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-120-light.98aeeaaa6ef4.webp)

### Text fallback

`unsupported`: Show a meaningful placeholder when image support is absent.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ pass

Frames:

- 40 columns: ![Image Parachute, Text fallback, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/unsupported-40-dark.6d17bad0156b.webp) ![Image Parachute, Text fallback, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/unsupported-40-light.e7da1fe303a4.webp)
- 60 columns: ![Image Parachute, Text fallback, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/unsupported-60-dark.b708ee45ce40.webp) ![Image Parachute, Text fallback, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/unsupported-60-light.9aa8fc440573.webp)
- 80 columns: ![Image Parachute, Text fallback, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/unsupported-80-dark.4b73d2a5b9e2.webp) ![Image Parachute, Text fallback, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/unsupported-80-light.1e63124bbabe.webp)
- 120 columns: ![Image Parachute, Text fallback, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/unsupported-120-dark.921830fd4f5d.webp) ![Image Parachute, Text fallback, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/unsupported-120-light.42214972a426.webp)

### Fullscreen iTerm2

`iterm-fullscreen`: In fullscreen, Pi switches iTerm2 images to the text fallback and restores them when fullscreen ends.

Checks at every width and theme:

- width: ✓ pass
- style-leak: ✓ pass
- hard-coded-colour: ✓ pass
- height: ✓ pass

Frames:

- 40 columns: ![Image Parachute, Fullscreen iTerm2, 40 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-fullscreen-40-dark.078300825ee0.webp) ![Image Parachute, Fullscreen iTerm2, 40 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-fullscreen-40-light.e4300c8bf73a.webp)
- 60 columns: ![Image Parachute, Fullscreen iTerm2, 60 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-fullscreen-60-dark.ab207580ae90.webp) ![Image Parachute, Fullscreen iTerm2, 60 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-fullscreen-60-light.ae1c4b538252.webp)
- 80 columns: ![Image Parachute, Fullscreen iTerm2, 80 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-fullscreen-80-dark.8a4c7ce92d0e.webp) ![Image Parachute, Fullscreen iTerm2, 80 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-fullscreen-80-light.bb201d735e37.webp)
- 120 columns: ![Image Parachute, Fullscreen iTerm2, 120 columns, dark theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-fullscreen-120-dark.bf5f69f6b0ba.webp) ![Image Parachute, Fullscreen iTerm2, 120 columns, light theme](https://pi-tui.ratstack.sh/frames/image-parachute/iterm-fullscreen-120-light.a6b122b083df.webp)

## Sample code

`stories/patterns/presentation/image-parachute.ts`, the story the frames above were rendered from.

```ts
// Image Parachute: Pi chooses either readable fallback or a real PNG placement.
import { Image, Text, getCapabilities, detectCapabilities, setCapabilities } from "@earendil-works/pi-tui";
import { syntheticPreview } from "../../fixtures/synthetic-images.ts";
import type { PatternStory } from "../../../src/pattern.ts";

export const story: PatternStory = {
  id: "image-parachute", title: "Image Parachute", kind: "component",
  apis: ["Image", "detectCapabilities", "setCapabilities", "TuiAltScreen"], rows: 10,
  states: [
    { id: "unsupported", label: "Text fallback" },
    { id: "supported", label: "Kitty PNG", steps: [{ type: "action", name: "supported" }] },
    { id: "iterm", label: "iTerm2 PNG", steps: [{ type: "action", name: "iterm" }] },
    { id: "iterm-fullscreen", label: "iTerm2 fullscreen fallback", capture: "fullscreen", steps: [{ type: "action", name: "iterm" }] },
  ],
  setup({ theme, tui, action }) {
    const previous = getCapabilities(), detected = detectCapabilities(() => false);
    let protocol: "kitty" | "iterm2" | null = null;
    const image = new Image(syntheticPreview("tasks"), "image/png", {
      fallbackColor: text => theme.fg("muted", text),
    }, { filename: "sample.png", maxWidthCells: 30, maxHeightCells: 4 });
    function use(next: typeof protocol) {
      protocol = next;
      setCapabilities({ ...detected, trueColor: true, hyperlinks: true, images: next });
      image.invalidate(); tui.requestRender();
    }
    use(null); action("supported", () => use("kitty")); action("iterm", () => use("iterm2"));
    return {
      invalidate() { image.invalidate(); },
      render(width) {
        return [
          ...new Text(theme.fg("accent", "Staged attachment · sample.png"), 0, 0).render(width),
          "", ...image.render(width), "",
          ...new Text(theme.fg("dim", getCapabilities().images === null
            ? protocol === "iterm2" ? "Fullscreen · iTerm2 text fallback" : "No graphics support · text retained"
            : "PNG preview · " + protocol), 0, 0).render(width),
        ];
      },
      dispose() { setCapabilities(previous); },
    };
  },
};
```

## Known uses: seen in Nico's repos

- [**earendil-works/pi**](https://github.com/earendil-works/pi): Render inline terminal images
  - [packages/tui/src/components/image.ts:1-78](https://github.com/earendil-works/pi/blob/9e9d5c94ed4aad92876a15fab64e78573133695a/packages/tui/src/components/image.ts#L1-L78) @9e9d5c94
  - [packages/tui/src/terminal-image.ts:1-90](https://github.com/earendil-works/pi/blob/9e9d5c94ed4aad92876a15fab64e78573133695a/packages/tui/src/terminal-image.ts#L1-L90) @9e9d5c94
- [**earendil-works/pi**](https://github.com/earendil-works/pi): Detect Ghostty through tmux for inline images
  - [packages/tui/src/terminal-image.ts:36-57](https://github.com/earendil-works/pi/blob/e904b11e7b4fb97105712a951ffb72b2572df69f/packages/tui/src/terminal-image.ts#L36-L57) @e904b11e
- [**dot314**](https://github.com/nicobailon/dot314): Select and preview multiple screenshots in a custom picker
  - [extensions/screenshots-picker/index.ts:800-830](https://github.com/nicobailon/dot314/blob/17cce138f3f687211d0485d204408a9650953c61/extensions/screenshots-picker/index.ts#L800-L830) @17cce138

## For agents: choose and check

Choose Image Parachute when your job matches its intent and applicability above. Its neighbours in Tool and message output are listed below. Read the one whose intent fits your job more closely before you commit.

- [Detail Fold](https://pi-tui.ratstack.sh/patterns/detail-fold.md): Show compact progress and summaries with bounded expanded tool detail.
- [Call Capsule](https://pi-tui.ratstack.sh/patterns/call-capsule.md): Retain a display shell through call and result renders of one tool execution.
- [Hinge Diff](https://pi-tui.ratstack.sh/patterns/hinge-diff.md): Choose compact, unified or split diff presentation from available width.
- [Word Spotlight](https://pi-tui.ratstack.sh/patterns/word-spotlight.md): Emphasize changed words inside parsed patch lines.
- [Error Digest](https://pi-tui.ratstack.sh/patterns/error-digest.md): Project structured errors into compact and expanded diagnostic output.
- [Renderer Chain](https://pi-tui.ratstack.sh/patterns/renderer-chain.md): Add tool rendering while preserving an existing renderer or fallback.
- [Message Fold](https://pi-tui.ratstack.sh/patterns/message-fold.md): Render custom message content as a preview with expanded detail.

Check your version:

- Render your version at 40, 60, 80 and 120 columns in the dark and light themes.
- Check width: no rendered line is wider than the terminal.
- Check style-leak: no line ends with colour, bold or a link still switched on.
- Check hard-coded-colour: every colour on screen comes from the active theme.
- Check height: the output fits in the rows the terminal has.
- Read the Pi 1.0.3 docs for [`Image`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/tui/README.md#image), [`detectCapabilities`](https://github.com/earendil-works/pi/blob/v1.0.3/packages/tui/README.md#image), `setCapabilities`, `TuiAltScreen` before you use them.
- Every reference frame gets the verdict its state expects.

Next actions: `related({ id: "image-parachute" })` lists what to read next, and `states({ id: "image-parachute", width: 40 })` returns every frame and verdict at one width. The [build-pi-tui-component skill](https://pi-tui.ratstack.sh/skills/build-pi-tui-component.md) walks through the whole loop.

## Related

- [Preview Basket](https://pi-tui.ratstack.sh/patterns/preview-basket.md): uses image-backed staged inspection

## Linked from

- [Preview Basket](https://pi-tui.ratstack.sh/patterns/preview-basket.md): handles preview capability limits
