Skip to content

Powerline prompts render as smudged/banded segments: font-rasterized separators + unclamped fallback glyphs #184

Description

@cunninghamcard-bit

Symptom

Embedding ghostty-web (0.4.0) with a starship Powerline prompt (segment backgrounds, fading-arrow separators U+E0B0, rounded caps U+E0B4/E0B6, Nerd icons), the prompt renders as soft "watercolor" blobs with ragged edges and horizontal banding inside segments — while plain ASCII output stays crisp. Reproduced on a 2x display at 13px with JetBrains Mono + Symbols Nerd Font Mono.

Root causes (two, compounding)

1. Powerline separators are font-rasterized.
renderCellText draws U+E0B0-U+E0B7 via fillText like any glyph. Two physical problems:

  • the diagonal edges keep anti-aliasing fringe on both sides, which visually smears against adjacent solid segment backgrounds;
  • the glyph's ink height is smaller than the cell height (font ascent/descent box ≠ cell box), so the neighboring background bleeds through above/below every separator — with stacked "fading arrow" themes this shows as banding stripes.

This is why WezTerm and xterm.js (customGlyphs) both special-case these codepoints and draw them as exact vector geometry covering the cell box, bypassing the font entirely.

2. Oversized fallback glyphs are not clamped to their cells.
Cell metrics come from the primary font's M, but glyphs served by fallback fonts carry their own advance — e.g. Symbols Nerd Font Mono icons advance 1em (13px = 1.67 cells at a 7.8px cell). fillText never scales, so the ink bleeds into neighbor cells. Since rows repaint by background fill (no clearRect), the cursor-row repaint shaves that overhanging ink asymmetrically every blink — accumulating the ragged artifacts.

renderCellText already computes the cell budget (cellWidth = metrics.width * cell.width) but doesn't pass it to fillText, whose 4th argument (maxWidth) does exactly this clamping.

Fix

PR incoming that (a) draws U+E0B0-U+E0B7 as vector paths (solid triangles / angle outlines / half-circles) using the cell's fillStyle, and (b) passes cellWidth as fillText's maxWidth for everything else. Verified pixel-level against WezTerm rendering the same prompt.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions