Commit Graph
178 Commits
Author SHA1 Message Date
phranck 941d8b704d Refactor: Checkbox uses U+25FC (black small square)
- Changed indicator from  to ◼ (U+25FC)
- Smaller, more compact checkbox appearance
- Updated example page state summary
- All 571 tests passing
2026-02-06 17:45:45 +01:00
phranck 6d1a4e876e Refactor: Checkbox uses U+25FE (black medium square) instead of U+25A3
- Changed indicator from ▣ to  (U+25FE)
- Better visual weight and consistency
- Updated example page state summary
- All 571 tests passing
2026-02-06 17:44:19 +01:00
phranck 0713f3ead7 Refactor: Checkbox uses U+25A3 (square with horizontal fill) instead of dot
- Changed indicator from ● to ▣ (U+25A3)
- More visually distinctive for checkboxes
- Updated example page state summary
- All 571 tests passing
2026-02-06 17:43:21 +01:00
phranck 9a0634463b Refactor: Only brackets pulse, indicator content stays static
- Separate rendering for brackets ([, ]) and indicator content (●○, ●, space)
- Brackets pulse on focus (35% → 100% accent)
- Indicator content: accent when focused, foregroundSecondary when unfocused
- Matches exact Button bracket pulse behavior pattern
- All 571 tests passing
2026-02-06 17:42:20 +01:00
phranck 989861738c Refactor: Checkbox style uses dot (●) instead of x
- Checkbox toggle now shows [●] when on, [ ] when off
- More consistent with dot-based toggle style [●○]
- Updated example page to show new checkbox indicator
- All 571 tests passing
2026-02-06 17:41:17 +01:00
phranck 2a2e57a240 Refactor: Toggle brackets pulse on focus, remove dot prefix
- Toggle brackets [●○], [x] now pulse like Button brackets
- Subtle pulse: 35% → 100% accent interpolation on focus
- No more focus dot prefix (•) — brackets are the focus indicator
- Bold styling on focused toggle brackets
- Matches Button focus behavior pattern (PR #81)
- All 571 tests passing, 0 lint violations
2026-02-06 17:40:34 +01:00
phranck a0af77af15 Feat: Toggle component with toggle and checkbox styles
- Toggle struct with string initializer for SwiftUI API parity
- ToggleStyle enum: .toggle (slider ●○) and .checkbox ([x])
- ToggleHandler for Space/Enter keyboard events
- Focus indicator with pulsing accent dot (inherited from Button pattern)
- Disabled state with tertiary color
- .disabled() modifier for control
- Comprehensive tests (17 tests, 571 total passing)
- TogglePage example with both styles, disabled states, and live state demo
- Added to main menu with shortcut key 7
2026-02-06 17:34:00 +01:00
phranck 6a055ca126 Feat: Pulsing focus indicator for buttons and improved styling
- Button focus: brackets pulse in accent color when focused
- Plain buttons: use pulsing dot indicator (no brackets)
- Button styles: default is now dimmer, primary is accent+bold
- Disabled buttons: darker appearance (50% tertiary)
- Shift+Tab: parse CSI Z sequence for backward focus navigation
- PulseTimer: reset to bright on focus change
- Faster pulse: 100ms steps (was 150ms)
- Shared helper: BorderRenderer.focusIndicatorPrefix() for future use
2026-02-06 15:47:45 +01:00
phranck d3f8ff60f3 Chore: Change license to MIT
- Add LICENSE file (MIT)
- Update 141 Swift file headers: CC BY-NC-SA 4.0 → License: MIT
2026-02-06 00:21:51 +01:00
phranck 64940714a3 Refactor: Change darker/lighter to relative percentage scaling
Previously darker(by: 0.5) subtracted 50 absolute lightness points.
Now it scales relative to current position: 0.5 means 'move halfway
toward the extreme'. More intuitive for humans — 20% darker always
means 20% of the remaining range toward black.
2026-02-05 23:18:45 +01:00
phranck 45d08c447d Feat: Add ProgressView with 5 bar styles and SwiftUI-matching API
Determinate progress bar with block, blockFine, shade, bar, and dot
styles. 4 initializers matching SwiftUI signatures (value/total, label,
currentValueLabel, string title). Style selection via .progressBarStyle()
modifier. Colors: foregroundSecondary (filled), foregroundTertiary (empty),
accent (dot head). 26 tests in 3 suites.
2026-02-05 23:18:38 +01:00
phranck 7f3d6f67ba Refactor: eliminate BorderedView, consolidate into ContainerView
Remove BorderedView entirely — .border() now creates a ContainerView
without title or footer. This ensures consistent 1-char inner padding
across all bordered containers (Box, Card, Panel, Alert, Dialog).

- Delete BorderModifier.swift (BorderedView)
- Update .border() to create ContainerView with padding
- Add empty-content guard to ContainerView
- Fix StatusBar bordered padding (1-char inset)
- Update Box docs to reflect new padding behavior
- Update Renderable.swift docs
- Migrate BorderedView tests to .border() API (527 tests passing)
2026-02-05 22:23:23 +01:00
phranck 0bde1d9f4d Refactor: remove block/flat appearances, ascii border style, and update documentation
Remove the block and flat appearance systems entirely, keeping only the
four standard border-based appearances (line, rounded, doubleLine, heavy).
Remove BorderStyle.ascii preset. Clean up all stale references in doc
comments, DocC articles, and README.

- Remove Appearance.flat/.block and BlockPalette protocol
- Remove surface color tokens (surfaceBackground, surfaceHeaderBackground, elevatedBackground)
- Remove BorderStyle.block, .ascii, and related statics
- Remove flat/block rendering paths from all views
- Simplify BorderRenderer, BorderModifier, ContainerView, Menu, Button, StatusBar, AppHeader
- Fix BorderStyle doc examples (add missing right padding)
- Update DocC: PaletteReference, ThemingGuide, AppearanceAndColors, RenderCycle, TUIkit.md
- Update README palette references to SystemPalette
- Delete FlatThemePage from example app
- Remove related tests (526 tests / 84 suites passing)
2026-02-05 22:08:53 +01:00
phranck f4fd383a76 Fix: pad notification content lines to full Box width 2026-02-05 20:32:13 +01:00
phranck 88d3165c4b Feat: add fire-and-forget notification system with NotificationService
Centralized NotificationService with .notificationHost() modifier for
rendering stacked, auto-dismissing notifications in the top-right corner.
Notifications use a single style with theme border colors — severity
differentiation belongs to Alerts, not notifications.

- NotificationService with static accessor and environment key
- NotificationHostModifier with fade-in/out animation and vertical stacking
- NotificationTiming for opacity interpolation and word-wrap
- Box(lines:) convenience init with BufferView for pre-styled content
- LifecycleManager.resetAppearance(token:) for re-triggering animations
- 17 tests covering service, timing, word-wrap, and rendering
2026-02-05 18:05:29 +01:00
phranck fef1233fd4 Feat: add conditional Equatable conformance to TupleView via parameter packs 2026-02-05 16:15:12 +01:00
phranck 71f6e12a42 Docs: add Subtree Memoization section to RenderCycle.md
Document the EquatableView/RenderCache system in the DocC article:
- How .equatable() works (5-step cache lookup)
- Cache invalidation triggers (State change, environment change)
- When to use / not use .equatable() (decision guide with tables)
- Which types support Equatable conformance
- Debug logging via TUIKIT_DEBUG_RENDER=1
- Code example using FeatureBox from the example app

Also updates existing sections:
- Correct 'no subtree memoization' claim in 'What Is NOT Diffed'
- Add EquatableView to Renderable lists (Direct Rendering, View-Level Modifiers)
2026-02-05 14:49:58 +01:00
phranck 20547de056 Feat: apply .equatable() to static example app subtrees
Add Equatable conformance and .equatable() wrapper to extracted views:
- FeatureBox (MainMenuPage): title/subtitle comparison, 3 instances
- ContainerTypesRow (ContainersPage): property-free, always cache-hits
- SettingsAndAlignmentRow (ContainersPage): property-free, always cache-hits

These subtrees are purely palette-driven with no @State dependencies.
During Spinner/Pulse animation frames (25 FPS), the render cache skips
their entire subtree rendering — only the animated views re-render.
2026-02-05 14:47:31 +01:00
phranck 095c67e021 Refactor: decompose ContainersPage and MainMenuPage into smaller view structs
Extract static, state-free subtrees into standalone View structs to
prepare for .equatable() memoization in Phase 5:

- FeatureBox: extracted from MainMenuPage's private featureBox() method
- ContainerTypesRow: Card/Box/Panel examples from ContainersPage
- SettingsAndAlignmentRow: settings panel + alignment demos

ButtonsPage left as-is — nearly all sections depend on @State clickCount
through Button actions, making decomposition ineffective for memoization.
2026-02-05 14:46:12 +01:00
phranck 08daee3258 Feat: render cache debug tooling with per-frame stats and review fixes
Add RenderCache.Stats for cache performance tracking:
- Hit/miss/store/clear counters with hit rate calculation
- Per-frame delta via Stats.delta(since:) for meaningful frame-level logging
- TUIKIT_DEBUG_RENDER=1 env var enables stderr logging (zero cost when off)
- logDebug uses @autoclosure to avoid string allocation in production

Review fixes applied:
- F1: logFrameStats() now logs per-frame delta, not cumulative totals
- F2: clearAll() no longer reads entries.count unconditionally
- F3: Remove unnecessary Sendable from Stats (main-thread only type)
- F5: reset() now also clears stats and frame snapshot
- F7: Move EnvironmentSnapshot to RenderLoop.swift (private, where it belongs)
- F8: Build snapshot from EnvironmentValues, not from ThemeManager directly
2026-02-05 14:40:58 +01:00
phranck 773bb15f2b Feat: render performance phase 2 — cache invalidation fix + Equatable conformances
Phase 1: Add EnvironmentSnapshot comparison in RenderLoop to automatically
clear render cache on theme/appearance changes. Eliminates latent bug where
EquatableView-cached subtrees could serve stale content after palette changes.

Phase 2: Add Equatable conformances to enable subtree memoization:
- Types: TextStyle, Alignment, ContainerConfig, ContainerStyle
- Leaf: Text
- Containers: VStack, HStack, ZStack, Box, Panel, Card, Dialog,
  ContainerView, BorderedView (conditional where Content: Equatable)
- Modifiers: FlexibleFrameView, OverlayModifier, DimmedModifier
  (conditional where Content/Base/Overlay: Equatable)

Also fixes two pre-existing SwiftLint violations:
- State.swift: vertical whitespace before closing brace
- ContainerView.swift: line length (extract local variable)
2026-02-05 14:21:12 +01:00
phranck a705414c82 Feat: Add subtree memoization via EquatableView (render pipeline phase 5)
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.
2026-02-03 23:08:17 +01:00
phranck cb00b29d56 Refactor: Nest PalettePreset as SystemPalette.Preset 2026-02-03 22:31:33 +01:00
phranck ea44f1fd13 Refactor: Consolidate 6 palette structs into PalettePreset enum + SystemPalette 2026-02-03 22:27:59 +01:00
phranck df9cb27c88 Refactor: Apply extension separation rule to all 35 Swift source files — move ~135 functions from type bodies into access-level-prefixed extensions (public/internal/private) 2026-02-03 22:05:46 +01:00
phranck df4ee3253b Feat: AppHeader — framework-managed header bar rendered outside the view tree
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.
2026-02-03 21:21:41 +01:00
phranck f77cd8bdec Chore: Unified file headers across all Swift files
Update IDETemplateMacros.plist and replace headers in 136 Swift files with
new format: 🖥️ TUIKit — Terminal UI Kit for Swift. Remove sdsd.swift template draft.
2026-02-03 20:48:29 +01:00
phranck 55fbfb0f4f Refactor: Dimmed overlay background with palette colors, ANSI-aware compositing, centered overlays
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.
2026-02-03 20:48:14 +01:00
phranck 7be9c05343 Refactor: Single-line bracket buttons, palette-based styles, brighter elevated background 2026-02-03 18:27:04 +01:00
phranck db03e4e41c Refactor: HSL-based color system for all palettes — lighter/darker preserve hue, unified baseHue architecture 2026-02-03 18:11:39 +01:00
phranck 93958e0308 Feat: Modal/Alert as focus sections — replace focusManager.clear() with section activation
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.
2026-02-03 16:59:57 +01:00
phranck b078661e7e Feat: StatusBar cascading — .statusBarItems(.merge/.replace) composition API
New declarative StatusBar item composition system:
- StatusBarItemComposition enum (.merge/.replace)
- .statusBarItems(.replace) { } for cascade barriers (modals)
- .statusBarItems { } defaults to .merge (combines with parent items)
- StatusBarState resolves items per active focus section:
  .replace = only section items, .merge = section + global (section wins)
- Section items rebuilt each frame via clearSectionItems()/registerSectionItems()
- Legacy context: push/pop API preserved for backward compatibility
2026-02-03 16:58:33 +01:00
phranck 818fc5d198 Feat: Breathing focus indicator — pulsing ● in active section border
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
2026-02-03 16:52:50 +01:00
phranck b7f51e1b85 Fix: Preserve focus state across render passes
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).
2026-02-03 16:47:20 +01:00
phranck 8ebda6371e Feat: Section-aware navigation — Up/Down cycles within active section
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).
2026-02-03 16:43:34 +01:00
phranck 2a3c96e782 Feat: Add FocusSection registration — section-aware FocusManager and .focusSection() modifier
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.
2026-02-03 16:41:37 +01:00
phranck 186e444fcc Fix: ESC closes modal via StatusBar context switching
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.
2026-02-03 12:35:07 +01:00
phranck ca9a0cf49c Fix: Modal focus isolation, ESC dismiss, footer layout, palette colors
- 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
2026-02-03 12:28:39 +01:00
phranck 7e69b9e924 Fix: Right-align dismiss buttons in all overlay demo variants
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.
2026-02-03 02:22:46 +01:00
phranck 44895b59cc Feat: Redesign OverlaysPage with interactive demo menu
Replace the static single-alert demo with an interactive page featuring:
- Menu with 8 overlay variants (5 Alert presets, 2 Dialog styles, 1 custom modal)
- Description panel showing details and API usage for the selected variant
- Enter key triggers the selected overlay with dimmed background
- Dismiss button closes any overlay

Variants demonstrated:
1. Alert (Standard) — default theme colors
2. Alert (Warning) — yellow preset
3. Alert (Error) — red preset
4. Alert (Info) — cyan preset
5. Alert (Success) — green preset
6. Dialog — content-only, no footer separator
7. Dialog with Footer — footer section with separator line
8. Modal (Custom) — arbitrary view content with .modal(isPresented:)
2026-02-03 02:19:12 +01:00
phranck c1d83ef915 Merge pull request #66 from phranck/chore/dead-code-cleanup
Chore: Remove 19 dead production symbols and 12 dead tests
2026-02-03 02:06:59 +01:00
phranck 82c8eaf4b2 Fix: Enforce absolute SwiftUI API parity for .alert() modifier
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.
2026-02-03 02:03:39 +01:00
phranck d25705f219 Fix: Remove redundant content rendering when isPresented is false
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.
2026-02-03 01:58:07 +01:00
phranck f9488e839c Chore: Remove 19 dead production symbols and 12 dead tests
Removed dead code that is no longer used after render pipeline optimization:

Terminal (7 dead methods/properties):
- width, height (replaced by getSize())
- writeLine(), clear(), fillBackground()
- readChar(), readLine()

ANSIRenderer (12 dead symbols):
- cursorUp(), cursorDown(), cursorForward(), cursorBack()
- saveCursor, restoreCursor
- clearScreen, clearToEnd, clearToBeginning
- clearLine, clearLineToEnd, clearLineToBeginning

Tests (12 dead tests + 1 empty suite):
- Removed 12 tests for deleted ANSIRenderer cursor/screen methods
- Removed entire ANSIRendererScreenTests suite (now empty)

Consistency fixes (3 files):
- RenderLoop.swift: buffer.lines.count → buffer.height
- FrameModifier.swift: buffer.lines.count → buffer.height
- FrameDiffWriter.swift: buffer.lines.count → buffer.height

Result: 443 tests in 78 suites (down from 455 tests in 80 suites)
2026-02-03 01:40:35 +01:00
phranck 45503c0d80 Feat: Add SwiftUI-style presentation API with .alert() and .modal(isPresented:)
- 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.
2026-02-03 01:34:30 +01:00
phranck 3f73327ca2 Refactor: Self-review fixes — early return, test correctness, doc completeness
- 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
2026-02-03 01:11:09 +01:00
phranck 71ec79d34a Refactor: Render Pipeline Phase 4 — remove terminal from RenderContext, update 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.
2026-02-02 22:30:20 +01:00
phranck 176cfc0db3 Refactor: Cache FrameBuffer.width, eliminate redundant regex and ioctl calls
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.
2026-02-02 22:10:37 +01:00
phranck a26582bfd8 Refactor: Frame-buffered output — collect all writes per frame, flush in one syscall
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).
2026-02-02 21:52:32 +01:00
phranck 5a2c8109c9 Refactor: Self-review fixes — deduplicate diff logic, fix redundant buildEnvironment call 2026-02-02 21:38:18 +01:00