Equatable views can opt into render caching with .equatable(). On cache hit,
the entire subtree is skipped and the cached FrameBuffer returned directly.
Cache is cleared on every @State change; GC removes stale entries per frame.
AppHeader is rendered at the top of the terminal by RenderLoop, similar to
StatusBar at the bottom. Views declare header content via .appHeader { }
ViewBuilder modifier. Supports standard (thin divider) and block (half-block
with appHeaderBackground) appearance. Hidden when no content is set.
Diff cache invalidates on header height changes to prevent ghosting.
Update IDETemplateMacros.plist and replace headers in 136 Swift files with
new format: 🖥️ TUIKit — Terminal UI Kit for Swift. Remove sdsd.swift template draft.
DimmedModifier now strips all ANSI codes and ornament characters, re-renders
with uniform palette.foregroundTertiary on palette.overlayBackground.
FrameBuffer.insertOverlay preserves base ANSI styling via ansiAwarePrefix/Suffix
and leadingANSISequences restoration. Overlays center relative to terminal size
with -2 vertical offset. Alert presets no longer color borders — only titles.
ModalPresentationModifier and AlertPresentationModifier now register
dedicated focus sections (__modal__/__alert__) and activate them instead
of clearing the entire FocusManager. Modal buttons register in the modal
section. When the modal closes, the section is not re-registered and
endRenderPass() falls back to the previous active section automatically.
New components:
- PulseTimer: GCD-based timer driving a sine-wave phase (0→1→0, 3s cycle)
- Color.lerp(): RGB linear interpolation between two colors
- BorderRenderer: standardTopBorder accepts focusIndicatorColor for ● rendering
Integration:
- FocusSectionModifier computes interpolated accent color when section is active
- RenderContext carries pulsePhase (from PulseTimer) and focusIndicatorColor
- BorderedView and ContainerView consume the indicator color on first border,
then nil it out so nested borders don't duplicate the indicator
- AppRunner owns PulseTimer, passes phase to RenderLoop each frame
RenderLoop called focusManager.clear() every frame, which reset the
active section and focused element. Replaced with beginRenderPass()
(clears sections/focusables for re-registration) and endRenderPass()
(validates preserved activeSectionID/focusedID against re-registered
sections, falls back to first available if removed from tree).
FocusManager.dispatchKeyEvent now intercepts Up/Down arrows to navigate
between focusable elements within the active section. Tab/Shift+Tab
continues to cycle between sections. Views needing custom Up/Down
handling (e.g. Menu) register via KeyEventDispatcher (Layer 2), which
takes priority over the FocusManager (Layer 3).
FocusManager reorganized from flat focusable list to section-based architecture.
Each section groups its own focusable elements. Tab cycles sections when multiple
exist, falls back to within-section cycling for single section (backward compat).
New types: FocusSection, FocusSectionModifier
New API: .focusSection("id"), register(_:inSection:), activateNextSection()
RenderContext carries activeFocusSectionID for child registration routing.
OverlaysPage now manages its own StatusBar items based on modal state:
- Modal open: ESC → close modal, Enter → dismiss
- Modal closed: ESC → back to menu, arrows → nav, Enter → show
Removes redundant ESC handlers from Modal/AlertPresentationModifier.
The correct approach is context-aware StatusBar items, not multiple
competing ESC handlers.
- Wire FocusManager.dispatchKeyEvent() into InputHandler as Layer 3
(Tab/Shift+Tab navigation, Enter/Space button activation)
- Modal/Alert presenters isolate base content from focus/key systems
using RenderContext.isolatedForBackground() so only modal buttons
receive focus and key events
- ESC automatically dismisses any modal or alert (framework-level)
- Fix ContainerView footer layout: Spacer() now fills correctly by
constraining footer context to actual inner width
- Remove body background color from standard style containers
(only block style uses distinct section backgrounds)
- Replace all hardcoded ANSI colors with palette semantic colors
(.palette.warning/error/info/success) in Alert presets and example app
Changed dismissButton to use HStack { Spacer(); Button } pattern for
consistent right-alignment across all alerts, dialogs, and modals.
Also simplified Alert preset usage — replaced explicit Alert<Button>.warning()
with standard Alert() initializer to avoid generic type inference issues.
Changed .alert() signature to match SwiftUI exactly:
- message parameter is now @ViewBuilder () -> Message (was String)
- Parameter order matches SwiftUI: title, isPresented, actions, message
- Message content is rendered to string internally for Alert view
Updated:
- AlertPresentationModifier generic signature (added Message type parameter)
- View+Presentation.swift API signatures
- OverlaysPage demo to use ViewBuilder message
- All AlertPresentationModifierTests to use ViewBuilder message
Added permanent rule to .claude/CLAUDE.md:
- ABSOLUTE SwiftUI API Parity is non-negotiable
- Must research exact SwiftUI signatures before implementing
- Only deviate when terminal constraints require it
This ensures TUIKit provides a familiar API for SwiftUI developers.
Both AlertPresentationModifier and ModalPresentationModifier were rendering
the base content twice when isPresented == true:
1. Once at the start (unused)
2. Again via DimmedModifier
Changed to only render content once per code path:
- If not presented: render content directly
- If presented: render content via DimmedModifier only
This eliminates unnecessary render work and improves performance.
- Add AlertPresentationModifier with Binding<Bool> support
- Add ModalPresentationModifier for custom modal content
- Add View+Presentation.swift with .alert() and .modal(isPresented:) extensions
- Update OverlaysPage to demonstrate new declarative presentation API
- Add comprehensive tests for both presentation modifiers (13 tests)
- Follows SwiftUI API parity rule: same naming, parameter order, and behavior
The new API eliminates manual if/else branching for modals:
Before:
if showModal {
content.dimmed().overlay { AlertView() }
} else {
content
}
After:
content.alert("Title", isPresented: $showModal) {
Button("OK") { showModal = false }
}
This mirrors SwiftUI's .alert(isPresented:) and .sheet(isPresented:) patterns.
- FrameBuffer.appendVertically: Early return for empty other buffer,
use repeatElement instead of Array(repeating:) for spacing
- TerminalOutputBufferTests: Fix multipleFrames test to use sequential
frames on the same Terminal instance (was testing separate instances)
- ChildInfoProvider.childInfos: Add missing parameter/return docs
Architecture cleanup:
- RenderContext no longer holds a Terminal reference (was never read
after construction). availableWidth/availableHeight are now required.
- ViewRenderer queries terminal size directly, documented as
convenience-only (not part of main pipeline).
- ChildInfo/ChildInfoProvider extracted from ViewRenderer.swift into
own file.
Documentation updates across all 4 phases:
- RenderCycle.md: Rewrote for 12-step pipeline with diffing, buffering,
and width caching. Replaced 'Why No Double Buffer' with 'Output
Optimization' section.
- AppLifecycle.md: Updated pipeline from 8 to 12 steps.
- StateManagement.md: Clarified 'no diffing' applies to view tree only,
not terminal output.
455 tests in 79 suites passing.
Three caching optimizations for the render pipeline:
1. FrameBuffer.width is now a stored property, recomputed only when
lines mutates (didSet). Eliminates ~125 redundant ANSI-stripping
regex runs per frame from repeated .width accesses.
2. strippedLength counts visible characters by subtracting ANSI match
lengths instead of allocating an intermediate stripped string.
3. RenderLoop.render() calls terminal.getSize() once per frame instead
of terminal.width + terminal.height (2 ioctl syscalls → 1).
All 455 tests passing.
Terminal.beginFrame()/endFrame() collects all write() calls in a [UInt8]
buffer (16 KB pre-allocated) and flushes them as a single POSIX write()
syscall. RenderLoop wraps content diff + status bar diff in one frame.
Reduces per-frame syscalls from ~40+ to exactly 1. Combined with Phase 1
line-level diffing, only changed lines are buffered and flushed.
6 new tests with pipe()-based stdout capturing (455 tests in 79 suites).
Introduce FrameDiffWriter that stores the previous frame's output and
compares line-by-line on each render. Only lines that actually differ
are written to the terminal, reducing I/O by ~94% for mostly-static UI.
- Extract output line building from WindowGroup.renderScene into
FrameDiffWriter.buildOutputLines() pure function
- WindowGroup.renderScene now returns FrameBuffer instead of writing
directly to terminal
- RenderLoop (now final class) owns FrameDiffWriter for state tracking
- Content and status bar are diffed independently
- SIGWINCH invalidates diff cache for full repaint on resize
- SignalManager gains separate consumeResizeFlag() for resize detection
- 13 new tests for diff computation and output line building
Replace mixed block characters (▇/■) with a single ● for all track
positions. Visual distinction comes purely from opacity fade. Extends
trail to 6 steps for a smoother motion effect.
Radically simplify the Spinner public API from 6 parameters to 3.
Remove SpinnerSpeed, BouncingTrackWidth, and BouncingTrailLength enums.
Hardcode calibrated intervals (dots 110ms, line 140ms, bouncing 100ms),
fixed track width (9), and trail opacities with edge overshoot for
smooth fade-in/fade-out at track edges.
Auto-animating loading indicator with three visual styles:
- Dots (braille rotation), Line (ASCII rotation), Bouncing (Larson scanner
with fade trail). Time-based frame calculation ensures each spinner runs
at its own speed independent of render triggers.
Configurable: SpinnerSpeed (.slow/.regular/.fast), BouncingTrackWidth
(.minimum/.default/.maximum/.fixed(Int)), BouncingTrailLength
(.short/.regular/.long), custom color.
Run loop upgraded from 100ms to 40ms polling (~25 FPS) to support
smooth animations.
Implement SwiftUI-style structural identity so @State values survive
full view-tree reconstruction on every render pass. State is keyed by
ViewIdentity (path-based) and property index in a central StateStorage.
- Add ViewIdentity (path-based structural identity for views)
- Add StateStorage with GC and branch invalidation
- Refactor @State to self-hydrate from StateStorage during init
- Set root hydration context in RenderLoop before app.body evaluation
- Extend RenderContext with identity propagation helpers
- Add ConditionalView branch invalidation on switch
- Remove scene cache from RenderLoop (app.body evaluated fresh per frame)
- Enhance Example App pages with @State (ButtonsPage, OverlaysPage, ContainersPage)
- Update DocC articles (StateManagement, RenderCycle)
- Add 12 tests for state storage identity
- Reduce Palette protocol from 18 to 13 essential properties
- Add BlockPalette protocol with surfaceBackground, surfaceHeaderBackground, elevatedBackground
- BlockPalette defaults compute via lighter(by:) from background
- Add Color.rgbComponents for universal ANSI/256/RGB conversion
- Make lighter(by:), darker(by:), opacity() work on all color types
- Migrate views to use BlockPalette accessors
- Remove NCursesPalette (to be reworked later)
- Update all 6 palette structs to BlockPalette conformance
- Update DocC articles, topic index, and palette-preview.html
- All 492 tests in 83 suites passing
Remove borderFocused, separator, selection, selectionBackground, disabled,
statusBarForeground, statusBarHighlight from Palette protocol and all
consumers. Views now use palette essentials directly (e.g. palette.accent
instead of palette.borderFocused, palette.foregroundTertiary instead of
palette.disabled). Update tests and DocC articles accordingly.
Remove explicit borderFocused, selection, selectionBackground,
statusBarForeground, statusBarHighlight from all palette implementations.
These remain as default-derived properties on the protocol extension.
Add foregroundPlaceholder as 4th foreground level for input field
placeholder text, dimmer than foregroundTertiary.
Fix SwiftLint force_try violations in luminance tests.
- Add BluePalette (VFD-style, #00AAFF base) and VioletPalette (HSL-generated from hue 270°)
- Remove GeneratedPalette entirely (replaced by dedicated VioletPalette)
- Reorder palette cycling: Green → Amber → Red → Violet → Blue → White → NCurses
- Sync all landing page CSS theme colors with exact Swift palette values
- Add blue theme to landing page with matching VFD colors
- Fix theme switcher FOUC: useEffect syncs React state after hydrate
- Update default theme to Green (was Amber)
- Update landing page logo to latest DocC version