Skip to content

Add live terminal preview to settings#2049

Open
twoTimesAgnew wants to merge 16 commits into
stablyai:mainfrom
twoTimesAgnew:terminal-settings-preview
Open

Add live terminal preview to settings#2049
twoTimesAgnew wants to merge 16 commits into
stablyai:mainfrom
twoTimesAgnew:terminal-settings-preview

Conversation

@twoTimesAgnew
Copy link
Copy Markdown

@twoTimesAgnew twoTimesAgnew commented May 16, 2026

Summary

  • Add a live xterm-based preview to Settings → Terminal at xl widths (≥1280px)
  • Typography preview shows font / weight / line-height / ligature changes with a Moon/Sun toggle to flip themes without changing the active app theme
  • Hover-preview in the font picker — the preview updates as you mouse through dropdown options without committing the selection
  • Replace the static TerminalThemePreview in the Dark/Light theme sections with the new xterm preview, restoring divider color, divider thickness, and inactive pane opacity feedback in the same xterm + divider + stub-pane shape
  • Extract composeActiveTerminalTheme from applyTerminalAppearance so live panes and the preview share one theme-composition path (color overrides, background opacity, cursor opacity)

Screenshots

image

The Typography Preview lives in the right column of Settings → Terminal at ≥1280px window width and includes a Moon/Sun toggle in its card header.

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test — 10 new unit tests (snapshot for PREVIEW_BUFFER plus 7 cases for composeActiveTerminalTheme)
  • pnpm build
  • Added high-quality tests for the new pure helpers; the React component requires DOM/canvas which isn't practical to unit-test under jsdom — covered by manual verification across font, theme, divider, opacity, and search-filter changes.

AI Review Report

Reviewed for spec compliance, refactor safety, React effect cleanup, and cross-platform behavior. Notable items:

  • composeActiveTerminalTheme extraction is behavior-preserving — traced the theme / paneBackground flow through the refactored applyTerminalAppearance.
  • terminal.reset() instead of clear() for buffer rewrites — clear() keeps the cursor row, which caused a dangling-prompt duplication during font/theme swaps.
  • Effect dependencies: explicit per-property deps so font / theme / divider changes don't redraw unrelated parts. Suppressions are localized to one line each.
  • DOM renderer only (no WebGL) so multiple previews can mount without exhausting Chromium's WebGL context budget.

Security Audit

No new IPC, network, file, or shell surface introduced. Preview is read-only via disableStdin: true plus tabIndex={-1} / aria-hidden. The preview buffer is a fixed compile-time constant — no untrusted text reaches xterm. Theme composition reads existing user settings only.

Notes

  • Cross-platform safe — no metaKey or path-separator assumptions. The preview uses no PTY and no remote dependency, so it works the same in SSH-runtime sessions.
  • Below the xl breakpoint the preview hides automatically; the controls return to the original single-column layout.

Per code review: the prompt() comment leaked a layout detail (360px), and
def/render were collapsed into a single yellow span — losing the function-
name color distinction the preview is meant to demonstrate. Strip ANSI in
the marker test so future re-coloring doesn't break it.
…pacity

Per code review: lead the JSDoc with the why (preview reuse) instead of
narrating the merge steps, and add a test that pins zero-background-opacity
behavior so a future truthy-only guard refactor doesn't silently drop it.
Implements Tasks 3-7 of the terminal-settings-preview plan: a live
read-only xterm.js preview card (DOM renderer) that mutates font/cursor
options directly, repaints the ANSI buffer on theme changes, manages the
LigaturesAddon on toggle, and refits on ResizeObserver events.
Per code review: tabIndex/aria-hidden on the wrapper don't reach xterm's
internal textarea — disableStdin does. And bg-black on the wrapper showed
a stark black ring around light-mode previews; track the composed theme's
background instead so the padding band always matches the canvas.
Per final review: the original Ghostty rationale comments inside
applyTerminalAppearance were lost when the composition was extracted —
restore them on each branch of composeActiveTerminalTheme. And the preview
needs allowTransparency true when the user has set a sub-1 background
opacity, otherwise xterm renders the rgba background fully opaque.
Three issues from initial visual testing:

1. Buffer accumulated on theme/settings changes. terminal.clear() keeps
   the row the cursor sits on, and PREVIEW_BUFFER ends mid-line on the
   prompt — so a follow-up clear+write left the trailing prompt fragment
   and appended the new buffer beneath it. Switch to terminal.reset()
   which restores cursor home and wipes the buffer.

2. Long lines wrapped because FitAddon shrunk cols to fit the 360px
   container. Drop FitAddon (and the resize observer that supported it),
   pin cols=50 / rows=15 in the constructor, and let the container clip
   horizontal overflow at large fonts. Lines never break mid-content now.

3. Add hover-to-preview to the font picker. FontAutocomplete fires a new
   onPreviewFontFamily callback as the user moves through dropdown
   options; TerminalPane lifts a previewFontFamily state and hands it to
   both Typography and Cursor previews so users see each font without
   committing the selection.
…yle, share with theme sections

Three follow-ups from visual testing:

1. Trim preview content and pin cols=36 so PREVIEW_BUFFER fits in the
   ~312px-wide xterm canvas at the default 14px font. Larger fonts still
   extend past the right edge (clipped, not wrapped) — same behavior as
   before, just with the default size now on the right side of "fits".

2. Mirror cursorInactiveStyle to cursorStyle so the cursor shape always
   reflects the user's choice. xterm renders an outline cursor when
   unfocused by default, which masked shape changes in the preview because
   it's read-only and never gets focus. Blink still requires focus — same
   as a real terminal pane.

3. Replace TerminalThemePreview with TerminalSettingsPreview in the dark
   and light theme sections. Adds a themeOverride prop on the preview so
   each section can pin a specific theme regardless of the active app
   theme. Drops the static two-pane mock + palette grid in favor of the
   richer code/test/diff buffer the user prefers.
xterm only animates cursor blink when the terminal has focus, so the
Cursor section's blink toggle had no visible effect unless the user
clicked into the preview. Add an opt-in autoFocusOnCursorChange prop
that focuses the preview whenever cursorStyle/Blink/Opacity changes —
the user is interacting with cursor controls so a brief focus snap is
expected. Skip the first run so opening Settings doesn't steal initial
focus from elsewhere on the page. Only the Cursor preview opts in;
Typography and theme previews keep natural focus behavior.
Drop the first-render guard so the cursor preview blinks immediately on
mount, not just after a cursor setting change. Switch to focusing the
xterm helper textarea directly with preventScroll: true so the page
doesn't jump to the preview when Settings first opens — the preview can
be below the fold and a scroll-into-view would be jarring.
Use terminal.focus() (which already uses preventScroll: true internally)
and defer it via requestAnimationFrame. Focusing synchronously inside a
React effect can land before xterm finishes wiring the textarea focus
subscription, leaving _isFocused stale and the blink interval paused
even though the textarea has DOM focus. One frame is enough for xterm's
setup to complete.
…phy preview

Cursor preview was high-friction (xterm cursor blink only animates on
focus, and every workaround for that was either intrusive or fragile)
and low-value — the bar/block/underline toggle is visually obvious from
the controls themselves. Remove it. The Cursor section is back to a
single-column layout.

Add a Moon/Sun toggle to the Typography preview's card header so users
can flip the preview between dark and light themes without changing the
app theme. Toggle is hidden when themeOverride is set (Dark/Light theme
sections already pin a specific theme). Initial mode follows the active
app theme; flipping it doesn't change app settings.
The new xterm-based preview lost the visual feedback for terminal divider
color/thickness and inactive pane opacity that the old TerminalThemePreview
used to give. Bring back the same two-pane shape — real xterm on the left,
configurable-width divider in the middle, small color-only stub on the
right — so changing divider color, divider thickness, or inactive pane
opacity has immediate visible feedback in the Dark/Light theme previews
and the Typography preview.

Switch the preview prop from themeOverride: ITheme to modeOverride: 'dark'
| 'light' so the preview can derive both the theme and the matching
divider color from a single resolveEffectiveTerminalAppearance call —
keeps the dark/light variant rules in lockstep with live panes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant