diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 287ed323..ccfc43ac 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,6 +1,6 @@ # Copilot Instructions for TUIkit -TUIkit is a SwiftUI-like framework for building Terminal User Interfaces in pure Swift — no ncurses or C dependencies. +TUIkit is a SwiftUI-like framework for building Terminal User Interfaces in pure Swift: no ncurses or C dependencies. ## Build, Test & Lint @@ -30,8 +30,8 @@ swift-format format -i -r Sources Tests TUIkit uses two rendering paths: -1. **Composite views** — Implement `body` to compose other views. The renderer recurses into `body`. -2. **Primitive views** — Conform to `Renderable` protocol and produce a `FrameBuffer` directly. Set `body: Never` (with `fatalError()`). +1. **Composite views**: Implement `body` to compose other views. The renderer recurses into `body`. +2. **Primitive views**: Conform to `Renderable` protocol and produce a `FrameBuffer` directly. Set `body: Never` (with `fatalError()`). The `renderToBuffer(_:context:)` function checks `Renderable` first, then falls back to `body`. @@ -41,10 +41,10 @@ The `renderToBuffer(_:context:)` function checks `Renderable` first, then falls ### Key Components -- **`FrameBuffer`** — 2D grid of styled cells representing terminal output -- **`RenderContext`** — Carries layout constraints, environment values, and `TUIContext` -- **`TUIContext`** — Central DI container for lifecycle, key events, preferences, state storage -- **`ViewIdentity`** — Structural identity path for `@State` persistence across renders +- **`FrameBuffer`**: 2D grid of styled cells representing terminal output +- **`RenderContext`**: Carries layout constraints, environment values, and `TUIContext` +- **`TUIContext`**: Central DI container for lifecycle, key events, preferences, state storage +- **`ViewIdentity`**: Structural identity path for `@State` persistence across renders ### Directory Structure @@ -79,9 +79,9 @@ Public APIs **must** match SwiftUI signatures exactly unless terminal constraint ### Architecture Rules -- **No singletons** — All state flows through the Environment system +- **No singletons**: All state flows through the Environment system - **Consolidate existing functions** before adding new ones -- **Never merge PRs autonomously** — Stop after creating, let the user merge +- **Never merge PRs autonomously**: Stop after creating, let the user merge ### Testing diff --git a/README.md b/README.md index c1bfbf67..4594d9ac 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ > [!TIP] > **☕ Support TUIkit Development** > -> If you enjoy TUIkit and find it useful, consider supporting its development! Your donations help cover ongoing costs like hosting, tooling, and the countless cups of coffee that fuel late-night coding sessions. Every contribution — big or small — is greatly appreciated and keeps this project alive. Thank you! 💙 +> If you enjoy TUIkit and find it useful, consider supporting its development! Your donations help cover ongoing costs like hosting, tooling, and the countless cups of coffee that fuel late-night coding sessions. Every contribution: big or small: is greatly appreciated and keeps this project alive. Thank you! 💙 > > [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue?logo=paypal&logoColor=white)](https://paypal.me/LAYEREDwork) > [![Support on Ko-fi](https://img.shields.io/badge/Support-Ko--fi-FF5E5B?logo=ko-fi&logoColor=white)](https://ko-fi.com/layeredwork) @@ -19,7 +19,7 @@ > [!IMPORTANT] > **This project is currently a WORK IN PROGRESS! I strongly advise against using it in a production environment because APIs are subject to change at any time.** -A SwiftUI-like framework for building Terminal User Interfaces in Swift — no ncurses, no C dependencies, just pure Swift. +A SwiftUI-like framework for building Terminal User Interfaces in Swift: no ncurses, no C dependencies, just pure Swift. ## What is this? @@ -63,34 +63,34 @@ struct ContentView: View { ### Core -- **`View` protocol** — the core building block, mirroring SwiftUI's `View` -- **`@ViewBuilder`** — result builder for declarative view composition -- **`@State`** — reactive state management with automatic re-rendering -- **`@Environment`** — dependency injection for theme, focus manager, status bar -- **`App` protocol** — app lifecycle with signal handling and run loop +- **`View` protocol**: the core building block, mirroring SwiftUI's `View` +- **`@ViewBuilder`**: result builder for declarative view composition +- **`@State`**: reactive state management with automatic re-rendering +- **`@Environment`**: dependency injection for theme, focus manager, status bar +- **`App` protocol**: app lifecycle with signal handling and run loop ### Views & Components -- **Primitive views** — `Text`, `EmptyView`, `Spacer`, `Divider` -- **Layout containers** — `VStack`, `HStack`, `ZStack` with alignment and spacing -- **Interactive** — `Button` with focus states, `Menu` with keyboard navigation -- **Containers** — `Alert`, `Dialog`, `Panel`, `Box`, `Card` -- **`StatusBar`** — context-sensitive keyboard shortcuts -- **`ForEach`** — iterate over collections, ranges, or `Identifiable` data +- **Primitive views**: `Text`, `EmptyView`, `Spacer`, `Divider` +- **Layout containers**: `VStack`, `HStack`, `ZStack` with alignment and spacing +- **Interactive**: `Button` with focus states, `Menu` with keyboard navigation +- **Containers**: `Alert`, `Dialog`, `Panel`, `Box`, `Card` +- **`StatusBar`**: context-sensitive keyboard shortcuts +- **`ForEach`**: iterate over collections, ranges, or `Identifiable` data ### Styling -- **Text styling** — bold, italic, underline, strikethrough, dim, blink, inverted -- **Full color support** — ANSI colors, 256-color palette, 24-bit RGB, hex values, HSL -- **Theming** — 6 predefined palettes (Green, Amber, Red, Violet, Blue, White) -- **Border styles** — rounded, line, double, thick, ASCII, and more +- **Text styling**: bold, italic, underline, strikethrough, dim, blink, inverted +- **Full color support**: ANSI colors, 256-color palette, 24-bit RGB, hex values, HSL +- **Theming**: 6 predefined palettes (Green, Amber, Red, Violet, Blue, White) +- **Border styles**: rounded, line, double, thick, ASCII, and more ### Advanced -- **Lifecycle modifiers** — `.onAppear()`, `.onDisappear()`, `.task()` -- **Storage** — `@AppStorage`, `@SceneStorage` with JSON backend -- **Preferences** — bottom-up data flow with `PreferenceKey` -- **Focus system** — Tab/Shift+Tab navigation between interactive elements +- **Lifecycle modifiers**: `.onAppear()`, `.onDisappear()`, `.task()` +- **Storage**: `@AppStorage`, `@SceneStorage` with JSON backend +- **Preferences**: bottom-up data flow with `PreferenceKey` +- **Focus system**: Tab/Shift+Tab navigation between interactive elements ## Run the Example App @@ -136,19 +136,19 @@ struct MyApp: App { ``` Available palettes (all via `SystemPalette`): -- `.green` — Classic P1 phosphor CRT (default) -- `.amber` — P3 phosphor monochrome -- `.red` — IBM 3279 plasma -- `.violet` — Retro sci-fi terminal -- `.blue` — VFD/LCD displays -- `.white` — DEC VT100/VT220 (P4 phosphor) +- `.green`: Classic P1 phosphor CRT (default) +- `.amber`: P3 phosphor monochrome +- `.red`: IBM 3279 plasma +- `.violet`: Retro sci-fi terminal +- `.blue`: VFD/LCD displays +- `.white`: DEC VT100/VT220 (P4 phosphor) ## Architecture -- **No singletons for state** — All state flows through the Environment system -- **Pure ANSI rendering** — No ncurses or other C dependencies -- **Linux compatible** — Works on macOS and Linux (XDG paths supported) -- **Value types** — Views are structs, just like SwiftUI +- **No singletons for state**: All state flows through the Environment system +- **Pure ANSI rendering**: No ncurses or other C dependencies +- **Linux compatible**: Works on macOS and Linux (XDG paths supported) +- **Value types**: Views are structs, just like SwiftUI ## Project Structure @@ -173,7 +173,7 @@ Tests/ ## Developer Notes -- Tests use Swift Testing (`@Test`, `#expect`) — run with `swift test` +- Tests use Swift Testing (`@Test`, `#expect`): run with `swift test` - All 591 tests run in parallel - The `Terminal` class handles raw mode and cursor control via POSIX `termios` diff --git a/Sources/TUIkit/TUIkit.docc/Articles/AppLifecycle.md b/Sources/TUIkit/TUIkit.docc/Articles/AppLifecycle.md index da842bc3..5e72916f 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/AppLifecycle.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/AppLifecycle.md @@ -56,10 +56,10 @@ Before the main loop starts, `run()` prepares the terminal: In raw mode, the terminal delivers every keystroke immediately without waiting for Enter. TUIkit configures: -- **No echo** — typed characters are not displayed -- **No canonical mode** — input is byte-by-byte, not line-by-line -- **No signal processing** — Ctrl+C is handled by TUIkit, not the OS -- **100ms read timeout** — non-blocking input polling +- **No echo**: typed characters are not displayed +- **No canonical mode**: input is byte-by-byte, not line-by-line +- **No signal processing**: Ctrl+C is handled by TUIkit, not the OS +- **100ms read timeout**: non-blocking input polling The original terminal settings are saved and restored during cleanup. @@ -73,9 +73,9 @@ The main loop is synchronous and runs until shutdown: Three things cause a new frame to be rendered: -- **SIGWINCH** — the terminal was resized -- **``AppState``** — a `@State` property was mutated -- **`SignalManager`** — `requestRerender()` was called (used by the state observer) +- **SIGWINCH**: the terminal was resized +- **``AppState``**: a `@State` property was mutated +- **`SignalManager`**: `requestRerender()` was called (used by the state observer) All triggers set boolean flags that the main loop checks. The actual rendering always happens on the main thread. @@ -88,7 +88,7 @@ All triggers set boolean flags that the main loop checks. The actual rendering a | `SIGINT` | Ctrl+C | Sets a shutdown flag → main loop exits | | `SIGWINCH` | Terminal resize | Sets a re-render flag → next iteration re-renders | -Signal handlers only set `nonisolated(unsafe)` boolean flags — no allocations, no locks. The main loop reads these flags each iteration and acts accordingly. +Signal handlers only set `nonisolated(unsafe)` boolean flags: no allocations, no locks. The main loop reads these flags each iteration and acts accordingly. ## Key Event Dispatch @@ -100,7 +100,7 @@ When the terminal delivers a key event, the `InputHandler` dispatches it through ### Layer 2: View-Registered Handlers -The `KeyEventDispatcher` iterates handlers registered via `onKeyPress()` modifiers — in reverse order (newest first). If a handler returns `true`, dispatch stops. +The `KeyEventDispatcher` iterates handlers registered via `onKeyPress()` modifiers: in reverse order (newest first). If a handler returns `true`, dispatch stops. ### Layer 3: Default Bindings @@ -137,7 +137,7 @@ Steps 8–11 are the output optimization layer: line-level diffing reduces write ## Cleanup -When the main loop exits — via Ctrl+C, the quit key, or programmatic shutdown — `cleanup()` restores the terminal: +When the main loop exits: via Ctrl+C, the quit key, or programmatic shutdown: `cleanup()` restores the terminal: | Step | What | Why | |------|------|-----| diff --git a/Sources/TUIkit/TUIkit.docc/Articles/AppearanceAndColors.md b/Sources/TUIkit/TUIkit.docc/Articles/AppearanceAndColors.md index 202aa9b4..9a487213 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/AppearanceAndColors.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/AppearanceAndColors.md @@ -6,8 +6,8 @@ Control border styles, visual appearances, and the color system. TUIkit separates visual styling into two systems: -- **Appearance** — Controls border characters and container styling (rounded, doubleLine, heavy, etc.) -- **Colors** — A palette-aware color system with semantic tokens that resolve at render time +- **Appearance**: Controls border characters and container styling (rounded, doubleLine, heavy, etc.) +- **Colors**: A palette-aware color system with semantic tokens that resolve at render time Both systems integrate with the theming pipeline described in . @@ -101,7 +101,7 @@ let darker = color.darker(by: 0.3) // 30% darker ### In View Bodies (no RenderContext) -Use `Color.palette.*` — these return semantic tokens: +Use `Color.palette.*`: these return semantic tokens: ```swift Text("Hello") @@ -126,7 +126,7 @@ Available semantic tokens include: ### In renderToBuffer (with RenderContext) -Use `context.environment.palette.*` directly — these return concrete colors: +Use `context.environment.palette.*` directly: these return concrete colors: ```swift func renderToBuffer(context: RenderContext) -> FrameBuffer { diff --git a/Sources/TUIkit/TUIkit.docc/Articles/Architecture.md b/Sources/TUIkit/TUIkit.docc/Articles/Architecture.md index 5329704d..376f9e0d 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/Architecture.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/Architecture.md @@ -28,7 +28,7 @@ Built-in views include ``Text``, ``Button``, ``Menu``, ``Alert``, ``Dialog``, `` ### 3. Modifier Layer -View modifiers implement the ``ViewModifier`` protocol and operate at the ``FrameBuffer`` level. They transform rendered output — adding padding, borders, frames, backgrounds, or overlays. +View modifiers implement the ``ViewModifier`` protocol and operate at the ``FrameBuffer`` level. They transform rendered output: adding padding, borders, frames, backgrounds, or overlays. ```swift Text("Hello") @@ -39,19 +39,19 @@ Text("Hello") ### 4. State & Environment Layer -- **``State``** — Mutable per-view state that triggers re-renders -- **``Binding``** — Two-way connection to a value owned elsewhere -- **``EnvironmentValues``** — Values propagated down the view tree -- **``AppStorage``** — Persistent key-value storage via `UserDefaults` +- **``State``**: Mutable per-view state that triggers re-renders +- **``Binding``**: Two-way connection to a value owned elsewhere +- **``EnvironmentValues``**: Values propagated down the view tree +- **``AppStorage``**: Persistent key-value storage via `UserDefaults` ### 5. Rendering Layer The rendering pipeline converts the view tree into terminal output: -1. **View tree traversal** — Each view produces a ``FrameBuffer`` -2. **Modifier application** — Modifiers transform buffers -3. **ANSI rendering** — The `ANSIRenderer` converts colors and styles to escape codes -4. **Terminal output** — The ``FrameBuffer`` lines are written to the terminal +1. **View tree traversal**: Each view produces a ``FrameBuffer`` +2. **Modifier application**: Modifiers transform buffers +3. **ANSI rendering**: The `ANSIRenderer` converts colors and styles to escape codes +4. **Terminal output**: The ``FrameBuffer`` lines are written to the terminal ## Event Loop diff --git a/Sources/TUIkit/TUIkit.docc/Articles/CustomViews.md b/Sources/TUIkit/TUIkit.docc/Articles/CustomViews.md index b02050ad..ff3e30af 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/CustomViews.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/CustomViews.md @@ -38,7 +38,7 @@ struct StatusHeader: View { } ``` -The `body` property is annotated with `@ViewBuilder` at the protocol level, so you can use all result builder features — conditionals, optionals, loops: +The `body` property is annotated with `@ViewBuilder` at the protocol level, so you can use all result builder features: conditionals, optionals, loops: ```swift struct UserCard: View { @@ -160,7 +160,7 @@ Text("Important!").highlighted(.yellow) ### When to Use ViewModifier -Use ``ViewModifier`` when your transformation is a pure buffer-to-buffer operation — adding visual effects, changing backgrounds, or adjusting layout after rendering. The ``RenderContext`` gives you access to: +Use ``ViewModifier`` when your transformation is a pure buffer-to-buffer operation: adding visual effects, changing backgrounds, or adjusting layout after rendering. The ``RenderContext`` gives you access to: | Property | Description | |----------|-------------| diff --git a/Sources/TUIkit/TUIkit.docc/Articles/FocusSystem.md b/Sources/TUIkit/TUIkit.docc/Articles/FocusSystem.md index 7254438c..64a0f922 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/FocusSystem.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/FocusSystem.md @@ -6,13 +6,13 @@ Navigate between interactive elements using the keyboard. TUIkit provides a focus system that lets users move between interactive views (buttons, menus, text fields) using Tab, Shift+Tab, or arrow keys. The system consists of three parts: -- **``FocusManager``** — Tracks which element is focused, handles navigation -- **``Focusable``** — Protocol that views adopt to receive focus -- **``FocusState``** — Lightweight state object that views use to query and request focus +- **``FocusManager``**: Tracks which element is focused, handles navigation +- **``Focusable``**: Protocol that views adopt to receive focus +- **``FocusState``**: Lightweight state object that views use to query and request focus ## How Focus Works -Every frame, the ``FocusManager`` is cleared and interactive views re-register themselves during rendering. This means focus registrations are always in sync with the current view tree — removed views are automatically unregistered. +Every frame, the ``FocusManager`` is cleared and interactive views re-register themselves during rendering. This means focus registrations are always in sync with the current view tree: removed views are automatically unregistered. The focus order follows the rendering order: the first focusable view rendered is first in the Tab cycle. @@ -30,11 +30,11 @@ protocol Focusable: AnyObject { } ``` -- **`focusID`** — Unique identifier for this focusable element -- **`canBeFocused`** — Whether focus can move to this element (default: `true`) -- **`onFocusReceived()`** — Called when this element gains focus (default: no-op) -- **`onFocusLost()`** — Called when this element loses focus (default: no-op) -- **`handleKeyEvent(_:)`** — Handle a key event while focused; return `true` if consumed +- **`focusID`**: Unique identifier for this focusable element +- **`canBeFocused`**: Whether focus can move to this element (default: `true`) +- **`onFocusReceived()`**: Called when this element gains focus (default: no-op) +- **`onFocusLost()`**: Called when this element loses focus (default: no-op) +- **`handleKeyEvent(_:)`**: Handle a key event while focused; return `true` if consumed ## Using FocusState @@ -52,7 +52,7 @@ if focusState.isFocused { focusState.requestFocus() ``` -Built-in views like ``Button`` and ``Menu`` create their own `FocusState` internally — you only need it when building custom focusable views. +Built-in views like ``Button`` and ``Menu`` create their own `FocusState` internally: you only need it when building custom focusable views. ## Navigation Keys @@ -67,7 +67,7 @@ The ``FocusManager`` responds to these keys during dispatch: ## Focus Indicator -The currently focused element is rendered with **bold** text styling. There is no arrow or marker — bold is the sole visual indicator. +The currently focused element is rendered with **bold** text styling. There is no arrow or marker: bold is the sole visual indicator. ## Focus in the Event Loop diff --git a/Sources/TUIkit/TUIkit.docc/Articles/KeyboardShortcuts.md b/Sources/TUIkit/TUIkit.docc/Articles/KeyboardShortcuts.md index 4363c1b6..7cf94522 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/KeyboardShortcuts.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/KeyboardShortcuts.md @@ -1,10 +1,10 @@ # Keyboard Shortcuts -How keyboard input flows through TUIkit — from raw terminal bytes to your view handlers. +How keyboard input flows through TUIkit: from raw terminal bytes to your view handlers. ## Overview -TUIkit uses a layered event dispatch system. When a key is pressed, it passes through up to three layers. The first layer that consumes the event wins — remaining layers are skipped. +TUIkit uses a layered event dispatch system. When a key is pressed, it passes through up to three layers. The first layer that consumes the event wins: remaining layers are skipped. ``` Terminal raw bytes @@ -28,7 +28,7 @@ Terminal raw bytes └─────────────────────────────┘ ``` -Additionally, `Ctrl+C` (SIGINT) is handled at the OS signal level **before** any of these layers — it always terminates the application. +Additionally, `Ctrl+C` (SIGINT) is handled at the OS signal level **before** any of these layers: it always terminates the application. ## Available Keys @@ -89,9 +89,9 @@ public struct KeyEvent { The terminal encodes modifiers differently from GUI frameworks: -- **Ctrl+letter** — Detected from ASCII control codes (0x01–0x1A). For example, `Ctrl+C` produces byte `0x03`. -- **Alt+key** — Detected from ESC prefix sequences (`ESC` followed by the key byte). -- **Shift** — Only auto-detected for uppercase letters. The terminal does not send distinct shift codes for most keys. +- **Ctrl+letter**: Detected from ASCII control codes (0x01–0x1A). For example, `Ctrl+C` produces byte `0x03`. +- **Alt+key**: Detected from ESC prefix sequences (`ESC` followed by the key byte). +- **Shift**: Only auto-detected for uppercase letters. The terminal does not send distinct shift codes for most keys. ## Registering Key Handlers @@ -104,9 +104,9 @@ Text("Press any key") .onKeyPress { event in if event.key == .enter { doSomething() - return true // consumed — stops propagation + return true // consumed: stops propagation } - return false // not consumed — passes to next handler + return false // not consumed: passes to next handler } ``` @@ -134,7 +134,7 @@ Text("Press Enter to continue") ### Handler Priority -Handlers are dispatched in **reverse registration order** — the deepest view in the tree (most recently registered) gets the event first. This means inner views can intercept events before outer views see them. +Handlers are dispatched in **reverse registration order**: the deepest view in the tree (most recently registered) gets the event first. This means inner views can intercept events before outer views see them. ```swift VStack { @@ -164,7 +164,7 @@ The ``FocusManager`` handles two navigation keys: When an element is focused, all other key events are delegated to it first via `handleKeyEvent(_:)`. Only if the focused element doesn't consume the event does it propagate further. -Arrow keys are **not** handled by `FocusManager` itself — individual views (like ``Menu`` and ``Button``) handle arrows in their own key event handlers. +Arrow keys are **not** handled by `FocusManager` itself: individual views (like ``Menu`` and ``Button``) handle arrows in their own key event handlers. For more details, see . @@ -187,7 +187,7 @@ The ``QuitBehavior`` enum controls when `q` is allowed to quit: | `.always` | `q` quits from any screen (default) | | `.rootOnly` | `q` only quits when no status bar context is pushed | -`.rootOnly` is useful for modal dialogs — push a status bar context for the dialog, and `q` will be blocked until the user dismisses it: +`.rootOnly` is useful for modal dialogs: push a status bar context for the dialog, and `q` will be blocked until the user dismisses it: ```swift Dialog(title: "Confirm") { @@ -285,7 +285,7 @@ StatusBarItem(shortcut: Shortcut.arrowsUpDown, label: "nav") ## Status Bar Context Stack -The status bar supports a context stack for temporary shortcut overrides — useful for modals and nested navigation: +The status bar supports a context stack for temporary shortcut overrides: useful for modals and nested navigation: ```swift // Set global items diff --git a/Sources/TUIkit/TUIkit.docc/Articles/PaletteReference.md b/Sources/TUIkit/TUIkit.docc/Articles/PaletteReference.md index ed4047b7..957ec181 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/PaletteReference.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/PaletteReference.md @@ -4,7 +4,7 @@ A visual reference for all built-in color palettes with their exact color values ## Overview -TUIkit ships with **6 palettes** — all generated from hand-tuned HSL parameters via ``SystemPalette``. Each palette defines semantic color tokens that the framework resolves at render time. +TUIkit ships with **6 palettes**: all generated from hand-tuned HSL parameters via ``SystemPalette``. Each palette defines semantic color tokens that the framework resolves at render time. Users access palette colors via `Color.palette.*`: @@ -24,7 +24,7 @@ environment.paletteManager.setCurrent(SystemPalette(.amber)) TUIkit uses a single palette protocol: -- **``Palette``** — 13 essential color tokens (8 required, 5 with defaults) +- **``Palette``**: 13 essential color tokens (8 required, 5 with defaults) ``` Palette (13 properties) @@ -46,7 +46,7 @@ All 6 built-in palettes are instances of ``SystemPalette``, which conforms to `` | **Semantic** | `success`, `warning`, `error`, `info` | Status indicators | | **UI Elements** | `border` | Borders | -Only 8 tokens are required — the remaining have sensible defaults. See for details on creating custom palettes. +Only 8 tokens are required: the remaining have sensible defaults. See for details on creating custom palettes. ## Green (Default) @@ -165,10 +165,10 @@ An algorithmically generated palette based on HSL color theory with a base hue o The violet preset takes a base hue (270°) and derives all color tokens using HSL relationships: -- **Background** — Base hue at very low lightness (3%) with reduced saturation -- **Foregrounds** — Base hue at medium-high lightness (40–70%) -- **Accent** — Base hue at high lightness (78%) with high saturation -- **Semantic colors** — Derived from color theory offsets: +- **Background**: Base hue at very low lightness (3%) with reduced saturation +- **Foregrounds**: Base hue at medium-high lightness (40–70%) +- **Accent**: Base hue at high lightness (78%) with high saturation +- **Semantic colors**: Derived from color theory offsets: - `success` = base + 120° (triadic) - `warning` = base + 60° (analogous warm) - `error` = base + 180° (complementary) @@ -232,12 +232,12 @@ When pressing `t` to cycle themes, palettes rotate in this order: When you write `.foregroundColor(.palette.accent)`, TUIkit resolves the actual color at render time: -1. **Declaration** — `Color.palette.accent` creates a `Color` with a semantic token (`.accent`) -2. **Render pass** — The current palette is read from `context.environment.palette` -3. **Resolution** — The semantic token maps to the palette's `accent` property -4. **ANSI output** — The resolved RGB color is converted to terminal escape codes +1. **Declaration**: `Color.palette.accent` creates a `Color` with a semantic token (`.accent`) +2. **Render pass**: The current palette is read from `context.environment.palette` +3. **Resolution**: The semantic token maps to the palette's `accent` property +4. **ANSI output**: The resolved RGB color is converted to terminal escape codes -This means the same view code produces different colors depending on the active palette — no code changes needed when switching themes. +This means the same view code produces different colors depending on the active palette: no code changes needed when switching themes. ## Topics diff --git a/Sources/TUIkit/TUIkit.docc/Articles/Preferences.md b/Sources/TUIkit/TUIkit.docc/Articles/Preferences.md index 28bfb41e..d9beeed0 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/Preferences.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/Preferences.md @@ -4,13 +4,13 @@ Pass data from child views up to their ancestors. ## Overview -TUIkit's preference system enables **bottom-up data flow** — the reverse of environment values. While the environment passes data down from parent to child, preferences let child views publish values that ancestors can observe and react to. +TUIkit's preference system enables **bottom-up data flow**: the reverse of environment values. While the environment passes data down from parent to child, preferences let child views publish values that ancestors can observe and react to. The system mirrors SwiftUI's preference API and consists of three parts: -- **``PreferenceKey``** — Protocol that defines a named value type with a default and a reduce strategy -- **``PreferenceValues``** — Type-safe storage that holds all preference values for a scope -- **`PreferenceStorage`** — Stack-based collector that manages preference contexts per render pass +- **``PreferenceKey``**: Protocol that defines a named value type with a default and a reduce strategy +- **``PreferenceValues``**: Type-safe storage that holds all preference values for a scope +- **`PreferenceStorage`**: Stack-based collector that manages preference contexts per render pass A built-in example is ``NavigationTitleKey``: a child view sets `.navigationTitle("Settings")`, and the enclosing navigation container reads that preference to render the title bar. @@ -28,8 +28,8 @@ struct CounterKey: PreferenceKey { } ``` -- **`defaultValue`** — Returned when no child has set this preference -- **`reduce(value:nextValue:)`** — Combines values when multiple children set the same key +- **`defaultValue`**: Returned when no child has set this preference +- **`reduce(value:nextValue:)`**: Combines values when multiple children set the same key The default `reduce` implementation simply takes the last value. Override it when you need additive behavior (summing counts), array collection, or other merge strategies. @@ -90,11 +90,11 @@ struct SettingsPage: View { Preferences are collected fresh every frame. Here's the lifecycle within a single render pass: -1. **Reset** — `RenderLoop.render()` calls `preferences.beginRenderPass()`, clearing all callbacks and resetting the stack to a single empty context -2. **Set** — As the view tree renders top-down, `PreferenceModifier` views call `setValue(_:forKey:)`, which reduces values into the current stack frame -3. **Scope** — `OnPreferenceChangeModifier` pushes a new context before rendering its subtree, isolating child preferences -4. **Collect** — After the subtree finishes, the modifier pops the context (merging into the parent) and fires the callback with the scoped result -5. **Discard** — At the start of the next frame, everything resets — stale preferences never persist +1. **Reset**: `RenderLoop.render()` calls `preferences.beginRenderPass()`, clearing all callbacks and resetting the stack to a single empty context +2. **Set**: As the view tree renders top-down, `PreferenceModifier` views call `setValue(_:forKey:)`, which reduces values into the current stack frame +3. **Scope**: `OnPreferenceChangeModifier` pushes a new context before rendering its subtree, isolating child preferences +4. **Collect**: After the subtree finishes, the modifier pops the context (merging into the parent) and fires the callback with the scoped result +5. **Discard**: At the start of the next frame, everything resets: stale preferences never persist This per-frame reset ensures preferences always reflect the current view tree. Removed views stop contributing automatically. @@ -104,7 +104,7 @@ The `reduce` function determines how multiple children's values combine. Common | Strategy | Implementation | Use Case | |----------|---------------|----------| -| Last value wins | `value = nextValue()` (default) | Titles, labels — deepest child wins | +| Last value wins | `value = nextValue()` (default) | Titles, labels: deepest child wins | | Additive | `value += nextValue()` | Counting items, summing heights | | Array collection | `value.append(contentsOf: nextValue())` | Gathering menu items, breadcrumbs | diff --git a/Sources/TUIkit/TUIkit.docc/Articles/RenderCycle.md b/Sources/TUIkit/TUIkit.docc/Articles/RenderCycle.md index 189684de..fe446409 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/RenderCycle.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/RenderCycle.md @@ -1,10 +1,10 @@ # Render Cycle -Understand how TUIkit turns your view tree into terminal output — one frame at a time. +Understand how TUIkit turns your view tree into terminal output: one frame at a time. ## Overview -Every frame in TUIkit follows the same synchronous pipeline: **clear per-frame state → build environment → render the view tree → diff against previous frame → flush to terminal → track lifecycle**. The view tree is fully re-evaluated each frame, but only **changed terminal lines** are written — and all writes are collected in a frame buffer and flushed as a **single `write()` syscall**. +Every frame in TUIkit follows the same synchronous pipeline: **clear per-frame state → build environment → render the view tree → diff against previous frame → flush to terminal → track lifecycle**. The view tree is fully re-evaluated each frame, but only **changed terminal lines** are written: and all writes are collected in a frame buffer and flushed as a **single `write()` syscall**. ## What Triggers a Frame @@ -16,7 +16,7 @@ Three things cause `RenderLoop` to produce a new frame: | State mutation | `@State` property change | ``AppState`` notifies its observer via ``RenderNotifier``, which sets the rerender flag | | Programmatic | `appState.setNeedsRender()` | Same observer path as above (services receive `AppState` via constructor injection) | -All triggers converge on boolean flags that the main loop checks each iteration. The actual rendering always happens on the main thread — signal handlers never render directly. +All triggers converge on boolean flags that the main loop checks each iteration. The actual rendering always happens on the main thread: signal handlers never render directly. ## The Render Pipeline @@ -28,9 +28,9 @@ Each call to `RenderLoop.render()` executes these steps in order: Three subsystems are reset at the start of every frame: -- **`KeyEventDispatcher`** — All key handlers are removed. Views re-register them during rendering via `onKeyPress()` modifiers. -- **`PreferenceStorage`** — All preference callbacks are cleared and the stack is reset to a single empty `PreferenceValues`. -- **``FocusManager``** — All focus registrations are cleared. Focusable views re-register during rendering. +- **`KeyEventDispatcher`**: All key handlers are removed. Views re-register them during rendering via `onKeyPress()` modifiers. +- **`PreferenceStorage`**: All preference callbacks are cleared and the stack is reset to a single empty `PreferenceValues`. +- **``FocusManager``**: All focus registrations are cleared. Focusable views re-register during rendering. This ensures that views which disappeared between frames don't leave stale handlers or registrations behind. @@ -61,21 +61,21 @@ A ``RenderContext`` bundles everything a view needs to render: | Property | What | |----------|------| -| `availableWidth` | Terminal width (mutable — containers reduce this for children) | +| `availableWidth` | Terminal width (mutable: containers reduce this for children) | | `availableHeight` | Terminal height minus status bar (mutable) | | `environment` | The ``EnvironmentValues`` from step 3 | | `tuiContext` | The `TUIContext` (lifecycle, key dispatch, preferences, state storage) | | `identity` | The current view's structural identity (``ViewIdentity``) | -`RenderContext` is a pure data container — it does not hold a reference to `Terminal`. All terminal I/O happens after the view tree has been rendered into a ``FrameBuffer``. +`RenderContext` is a pure data container: it does not hold a reference to `Terminal`. All terminal I/O happens after the view tree has been rendered into a ``FrameBuffer``. -The context is passed down the view tree. Each view can create a modified copy for its children — for example, a border reduces `availableWidth` by 2 before rendering its content. Container views extend the `identity` path for each child. +The context is passed down the view tree. Each view can create a modified copy for its children: for example, a border reduces `availableWidth` by 2 before rendering its content. Container views extend the `identity` path for each child. ### Step 5: Evaluate Scene `app.body` is evaluated fresh each frame, producing a ``WindowGroup`` that wraps the root view. The `WindowGroup` implements `SceneRenderable` and bridges from the scene layer to the view layer. -> Note: Views are fully reconstructed on every frame. `@State` values survive because `State.init` self-hydrates from `StateStorage` — looking up the persistent value by the view's structural identity. +> Note: Views are fully reconstructed on every frame. `@State` values survive because `State.init` self-hydrates from `StateStorage`: looking up the persistent value by the view's structural identity. ### Step 6: Render View Tree @@ -112,8 +112,8 @@ The status bar renders in a separate pass (see below) but writes into the **same The `LifecycleManager` compares the current frame's tokens with the previous frame's: -- **Disappeared views** — tokens present last frame but absent now. Their `onDisappear` callbacks fire, and their tokens are removed from the appeared set (allowing future `onAppear` if they return). -- **Visible views** — the current token set becomes the baseline for the next frame. +- **Disappeared views**: tokens present last frame but absent now. Their `onDisappear` callbacks fire, and their tokens are removed from the appeared set (allowing future `onAppear` if they return). +- **Visible views**: the current token set becomes the baseline for the next frame. The `StateStorage` performs garbage collection: any state whose view identity was not marked active during this render pass is removed. This prevents memory leaks from views that have been permanently removed. @@ -125,7 +125,7 @@ The status bar renders in a separate pass but within the same buffered frame: 1. A ``StatusBar`` view is created with resolved palette colors 2. A dedicated ``RenderContext`` is created with `availableHeight` set to the status bar's height -3. `renderToBuffer()` runs on the status bar view — same dispatch as the main content +3. `renderToBuffer()` runs on the status bar view: same dispatch as the main content 4. `FrameDiffWriter.writeStatusBarDiff()` diffs the status bar independently from the main content 5. Changed lines are written into the same frame buffer as the content @@ -140,19 +140,19 @@ TUIkit has two ways for a view to produce output: Views that conform to `Renderable` implement `renderToBuffer(context:)` and produce a ``FrameBuffer`` directly. Their `body` property is **never called**. This path is used by: -- **Leaf views** — ``Text``, ``Spacer``, `Divider`, ``EmptyView`` -- **Layout containers** — `VStack`, `HStack`, `ZStack` -- **Interactive views** — ``Button``, ``ButtonRow``, ``Menu`` -- **Container views** — ``Panel``, ``Card``, ``Alert``, ``Dialog`` -- **Modifier wrappers** — `ModifiedView`, `BorderedView`, `DimmedModifier`, `OverlayModifier`, `EnvironmentModifier`, ``EquatableView``, and all lifecycle modifiers +- **Leaf views**: ``Text``, ``Spacer``, `Divider`, ``EmptyView`` +- **Layout containers**: `VStack`, `HStack`, `ZStack` +- **Interactive views**: ``Button``, ``ButtonRow``, ``Menu`` +- **Container views**: ``Panel``, ``Card``, ``Alert``, ``Dialog`` +- **Modifier wrappers**: `ModifiedView`, `BorderedView`, `DimmedModifier`, `OverlayModifier`, `EnvironmentModifier`, ``EquatableView``, and all lifecycle modifiers ### Path 2: Composition (body) Views that are **not** `Renderable` declare their content through `body`. The rendering system recursively renders the body until it hits a `Renderable` leaf. This path is used by: -- **Composite views** — ``Box`` returns `content.border(...)`, which wraps in a `BorderedView` (which is `Renderable`) -- **User-defined views** — Your custom views compose other views in `body` +- **Composite views**: ``Box`` returns `content.border(...)`, which wraps in a `BorderedView` (which is `Renderable`) +- **User-defined views**: Your custom views compose other views in `body` ### The Dispatch Function @@ -165,7 +165,7 @@ func renderToBuffer(_ view: V, context: RenderContext) -> FrameBuffer { return renderable.renderToBuffer(context: context) } - // Priority 2: Composite — set up hydration context and recurse into body. + // Priority 2: Composite: set up hydration context and recurse into body. // @State.init self-hydrates from StateStorage during body evaluation. if V.Body.self != Never.self { let childContext = context.withChildIdentity(type: V.Body.self) @@ -175,14 +175,14 @@ func renderToBuffer(_ view: V, context: RenderContext) -> FrameBuffer { return renderToBuffer(body, context: childContext) } - // Priority 3: No rendering path — empty buffer + // Priority 3: No rendering path: empty buffer return FrameBuffer() } ``` @Image(source: "render-cycle-dispatch.png", alt: "Decision tree showing the dual rendering dispatch: renderToBuffer checks Renderable conformance first, then body recursion, then returns an empty buffer as fallback.") -> Important: If a view conforms to `Renderable`, its `body` is never evaluated. This is intentional — `Renderable` views produce output directly and don't need compositional decomposition. +> Important: If a view conforms to `Renderable`, its `body` is never evaluated. This is intentional: `Renderable` views produce output directly and don't need compositional decomposition. ## FrameBuffer @@ -192,9 +192,9 @@ func renderToBuffer(_ view: V, context: RenderContext) -> FrameBuffer { Views create buffers in their `renderToBuffer(context:)`: -- ``Text`` — single line with ANSI style codes -- ``Spacer`` — empty lines -- ``EmptyView`` — empty buffer (no lines) +- ``Text``: single line with ANSI style codes +- ``Spacer``: empty lines +- ``EmptyView``: empty buffer (no lines) ### Combination @@ -235,17 +235,17 @@ The `EnvironmentModifier` (created by `.environment(_:_:)`) works by: 2. Creating a new `RenderContext` with that environment via `context.withEnvironment()` 3. Rendering its content with the new context -There is no global environment — everything flows through the context parameter. +There is no global environment: everything flows through the context parameter. ## Preference Collection -Preferences flow **bottom-up** — the reverse of environment values. Child views set values that parent views observe. +Preferences flow **bottom-up**: the reverse of environment values. Child views set values that parent views observe. `PreferenceStorage` uses a stack-based collection mechanism: -1. `OnPreferenceChangeModifier` calls `push()` — creates a new collection scope +1. `OnPreferenceChangeModifier` calls `push()`: creates a new collection scope 2. Its child tree renders, and `PreferenceModifier` calls `setValue()` on the current scope -3. `OnPreferenceChangeModifier` calls `pop()` — merges collected values into the parent scope and fires the callback +3. `OnPreferenceChangeModifier` calls `pop()`: merges collected values into the parent scope and fires the callback The `reduce(value:nextValue:)` function on ``PreferenceKey`` controls how multiple values from different children are combined. The default behavior: last value wins. @@ -263,21 +263,21 @@ public protocol ViewModifier { } ``` -`ModifiedView` wraps a view and a modifier — it renders the content first, then calls `modify(buffer:context:)`. Examples: +`ModifiedView` wraps a view and a modifier: it renders the content first, then calls `modify(buffer:context:)`. Examples: -- **`PaddingModifier`** — Adds empty lines (top/bottom) and spaces (leading/trailing) around the buffer -- **`BackgroundModifier`** — Wraps each line with background ANSI codes, padded to full width +- **`PaddingModifier`**: Adds empty lines (top/bottom) and spaces (leading/trailing) around the buffer +- **`BackgroundModifier`**: Wraps each line with background ANSI codes, padded to full width ### View-Level Modifiers (Renderable) More complex modifiers are full `View + Renderable` implementations that control when and how their content renders: -- **`BorderedView`** — Reduces `availableWidth` by 2, renders content, adds border characters via `BorderRenderer` -- **`FlexibleFrameView`** — Modifies `availableWidth`/`availableHeight` before rendering, applies min/max constraints and alignment after -- **`OverlayModifier`** — Renders base and overlay separately, composites via `FrameBuffer.composited(with:at:)` -- **`DimmedModifier`** — Renders content, then applies ANSI dim code to every line -- **`EnvironmentModifier`** — Creates modified context, renders content with it -- **``EquatableView``** — Checks ``RenderCache`` before rendering; returns cached buffer on hit, renders and stores on miss (see ) +- **`BorderedView`**: Reduces `availableWidth` by 2, renders content, adds border characters via `BorderRenderer` +- **`FlexibleFrameView`**: Modifies `availableWidth`/`availableHeight` before rendering, applies min/max constraints and alignment after +- **`OverlayModifier`**: Renders base and overlay separately, composites via `FrameBuffer.composited(with:at:)` +- **`DimmedModifier`**: Renders content, then applies ANSI dim code to every line +- **`EnvironmentModifier`**: Creates modified context, renders content with it +- **``EquatableView``**: Checks ``RenderCache`` before rendering; returns cached buffer on hit, renders and stores on miss (see ) ## Lifecycle Tracking @@ -291,7 +291,7 @@ The `OnAppearModifier` calls `lifecycle.recordAppear(token, action)` during rend - If the token has **never appeared before**: it's added to `appearedTokens` and the action fires - If it **has** appeared before: the action does **not** fire (prevents repeated triggers) -> Note: `onAppear` fires **synchronously** during the render traversal — not after the frame completes. This is because TUIkit uses single-pass rendering with no layout phase. +> Note: `onAppear` fires **synchronously** during the render traversal: not after the frame completes. This is because TUIkit uses single-pass rendering with no layout phase. ### onDisappear @@ -328,7 +328,7 @@ All terminal writes during a frame are collected in an internal `[UInt8]` buffer ### What Is NOT Diffed -The view tree is re-evaluated each frame — there is no virtual DOM. However, views wrapped in ``EquatableView`` (via `.equatable()`) can skip subtree rendering when their properties are unchanged. See below. +The view tree is re-evaluated each frame: there is no virtual DOM. However, views wrapped in ``EquatableView`` (via `.equatable()`) can skip subtree rendering when their properties are unchanged. See below. The alternate screen buffer (entered during setup) ensures that the user's previous terminal content is preserved and restored on exit. @@ -343,11 +343,11 @@ When a view is wrapped in `.equatable()`, the rendering system: 1. Looks up the cached ``FrameBuffer`` for this view's ``ViewIdentity`` 2. Compares the **current view value** with the cached snapshot via `Equatable.==` 3. Checks that the available **width and height** haven't changed -4. On **cache hit**: returns the cached buffer — the entire subtree is skipped +4. On **cache hit**: returns the cached buffer: the entire subtree is skipped 5. On **cache miss**: renders normally and stores the result ```swift -// A static info box — title and subtitle are the only inputs. +// A static info box: title and subtitle are the only inputs. struct FeatureBox: View, Equatable { let title: String let subtitle: String @@ -362,7 +362,7 @@ struct FeatureBox: View, Equatable { } } -// In a parent view — cached between frames when title/subtitle are unchanged: +// In a parent view: cached between frames when title/subtitle are unchanged: FeatureBox("Pure Swift", "No ncurses").equatable() ``` @@ -375,7 +375,7 @@ The ``RenderCache`` is **fully cleared** in two situations: | Any `@State` change | `StateBox.value.didSet` calls `renderCache.clearAll()` | | Environment change | `RenderLoop` compares an `EnvironmentSnapshot` (palette ID + appearance ID) each frame and clears on mismatch | -Between these events — for example during Spinner animation frames at 25 FPS — the cache is fully active. Static subtrees are rendered once and reused for every subsequent frame. +Between these events: for example during Spinner animation frames at 25 FPS: the cache is fully active. Static subtrees are rendered once and reused for every subsequent frame. ### When to Use `.equatable()` @@ -387,7 +387,7 @@ Between these events — for example during Spinner animation frames at 25 FPS | Bad candidates | Why | |---------------|-----| -| Views that read `@State` directly | State lives in a reference-type box — the view struct compares as equal even when state changed | +| Views that read `@State` directly | State lives in a reference-type box: the view struct compares as equal even when state changed | | Views that change every frame | Cache overhead with no benefit | | Tiny views (single `Text`) | Rendering cost is already minimal | @@ -413,7 +413,7 @@ Set `TUIKIT_DEBUG_RENDER=1` to enable per-frame cache statistics on stderr: [RenderCache] STORE Root/MainMenuPage/FeatureBox [RenderCache] HIT Root/MainMenuPage/FeatureBox [RenderCache] MISS (no entry) Root/SpinnersPage/Spinner -[RenderCache] FRAME — hits: 3, misses: 2, stores: 2, clears: 0, entries: 3, hit rate: 60% +[RenderCache] FRAME: hits: 3, misses: 2, stores: 2, clears: 0, entries: 3, hit rate: 60% ``` Redirect with `2>render.log` to capture without interfering with the TUI. diff --git a/Sources/TUIkit/TUIkit.docc/Articles/StateManagement.md b/Sources/TUIkit/TUIkit.docc/Articles/StateManagement.md index 178f1691..a454eb31 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/StateManagement.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/StateManagement.md @@ -100,12 +100,12 @@ struct SettingsView: View { ## How State Survives Re-Rendering TUIkit re-evaluates the entire view tree on every frame. When `body` is called, views are -reconstructed from scratch. Despite this, `@State` values persist — they are never reset +reconstructed from scratch. Despite this, `@State` values persist: they are never reset to their initial value. ### Structural Identity -Each view in the tree has a **structural identity** — a path like `"ContentView/VStack.0/Menu"`. +Each view in the tree has a **structural identity**: a path like `"ContentView/VStack.0/Menu"`. This path is built automatically during rendering based on: - The view's type name - Its position among siblings (child index) @@ -126,7 +126,7 @@ When a ``State`` value changes: 1. The ``StateBox`` triggers ``RenderNotifier/current`` → ``AppState/setNeedsRender()`` 2. The observer registered by `AppRunner` requests a re-render -3. The main loop re-evaluates `app.body` fresh — reconstructing all views +3. The main loop re-evaluates `app.body` fresh: reconstructing all views 4. Each `@State.init` self-hydrates from `StateStorage`, recovering persisted values 5. The new ``FrameBuffer`` output is written to the terminal @@ -136,4 +136,4 @@ Views that disappear from the tree (e.g., a conditional branch switches) have th automatically cleaned up at the end of each render pass. `ConditionalView` also immediately invalidates the inactive branch's state to prevent stale values. -This is simple and predictable — the view tree is fully re-evaluated each frame (no virtual DOM), with persistent state. Terminal output is then diffed at the line level — only changed lines are written. See for details on the output optimization pipeline. +This is simple and predictable: the view tree is fully re-evaluated each frame (no virtual DOM), with persistent state. Terminal output is then diffed at the line level: only changed lines are written. See for details on the output optimization pipeline. diff --git a/Sources/TUIkit/TUIkit.docc/Articles/StatusBarGuide.md b/Sources/TUIkit/TUIkit.docc/Articles/StatusBarGuide.md index dc488f6b..f392c530 100644 --- a/Sources/TUIkit/TUIkit.docc/Articles/StatusBarGuide.md +++ b/Sources/TUIkit/TUIkit.docc/Articles/StatusBarGuide.md @@ -6,15 +6,15 @@ Configure the shortcut bar at the bottom of the terminal. The status bar is a persistent row at the bottom of the terminal that shows keyboard shortcuts and contextual information. It is always visible and updates every frame. -TUIkit provides two status bar styles — ``StatusBarStyle/compact`` (single-line, shortcuts only) and ``StatusBarStyle/bordered`` (bordered with title support). +TUIkit provides two status bar styles: ``StatusBarStyle/compact`` (single-line, shortcuts only) and ``StatusBarStyle/bordered`` (bordered with title support). ## Architecture The status bar system has three parts: -- **``StatusBarState``** — Manages the item stack, style, and event handling -- **``StatusBarItem``** — A single shortcut entry (key + label + action) -- **``StatusBar``** — The view that renders items into a ``FrameBuffer`` +- **``StatusBarState``**: Manages the item stack, style, and event handling +- **``StatusBarItem``**: A single shortcut entry (key + label + action) +- **``StatusBar``**: The view that renders items into a ``FrameBuffer`` ## Defining Status Bar Items @@ -84,11 +84,11 @@ These appear on the right side of the status bar. You can configure quit behavio Two styles are available: -- **``StatusBarStyle/compact``** — Items rendered as `key Label` pairs in a single line, no border -- **``StatusBarStyle/bordered``** — Items inside a bordered container +- **``StatusBarStyle/compact``**: Items rendered as `key Label` pairs in a single line, no border +- **``StatusBarStyle/bordered``**: Items inside a bordered container Set the style during app configuration or at runtime via the status bar state. ## Event Dispatch Priority -Status bar items are dispatched in **Layer 1** of the key event pipeline — they take priority over view-registered handlers and default bindings. See for the full dispatch order. +Status bar items are dispatched in **Layer 1** of the key event pipeline: they take priority over view-registered handlers and default bindings. See for the full dispatch order. diff --git a/Sources/TUIkit/TUIkit.docc/TUIkit.md b/Sources/TUIkit/TUIkit.docc/TUIkit.md index 1d74f709..c391049e 100644 --- a/Sources/TUIkit/TUIkit.docc/TUIkit.md +++ b/Sources/TUIkit/TUIkit.docc/TUIkit.md @@ -10,7 +10,7 @@ A declarative, SwiftUI-like framework for building Terminal User Interfaces in S ## Overview -TUIkit lets you build terminal applications using a familiar, declarative syntax inspired by SwiftUI. No ncurses, no C dependencies — pure Swift. +TUIkit lets you build terminal applications using a familiar, declarative syntax inspired by SwiftUI. No ncurses, no C dependencies: pure Swift. ```swift @main @@ -32,13 +32,13 @@ struct MyApp: App { ### Key Features -- **Declarative syntax** — Build UIs with `VStack`, `HStack`, `Text`, `Button`, and more -- **SwiftUI-like API** — `@State`, `@ViewBuilder`, environment values, modifiers -- **Theming system** — 5 built-in phosphor themes with full RGB color support -- **Focus management** — Keyboard-driven navigation between interactive elements -- **Status bar** — Configurable shortcut bar with context stack -- **No dependencies** — Pure Swift, no ncurses or other C libraries -- **Cross-platform** — macOS and Linux +- **Declarative syntax**: Build UIs with `VStack`, `HStack`, `Text`, `Button`, and more +- **SwiftUI-like API**: `@State`, `@ViewBuilder`, environment values, modifiers +- **Theming system**: 5 built-in phosphor themes with full RGB color support +- **Focus management**: Keyboard-driven navigation between interactive elements +- **Status bar**: Configurable shortcut bar with context stack +- **No dependencies**: Pure Swift, no ncurses or other C libraries +- **Cross-platform**: macOS and Linux ## Topics diff --git a/docs/app/components/ActivityHeatmap.tsx b/docs/app/components/ActivityHeatmap.tsx index a14f64ca..ca144c52 100644 --- a/docs/app/components/ActivityHeatmap.tsx +++ b/docs/app/components/ActivityHeatmap.tsx @@ -167,8 +167,8 @@ export default function ActivityHeatmap({ weeks, loading = false }: ActivityHeat if (loading) { return (
-

- +

+ Commit Activity

@@ -199,8 +199,8 @@ export default function ActivityHeatmap({ weeks, loading = false }: ActivityHeat return (
-

- +

+ Commit Activity

@@ -209,7 +209,7 @@ export default function ActivityHeatmap({ weeks, loading = false }: ActivityHeat {/* Scrollable wrapper for mobile */}
- {/* Month labels — absolutely positioned above the cell grid */} + {/* Month labels: absolutely positioned above the cell grid */}
{monthLabels.map(({ label, offset }) => ( - {/* Cell grid — column-flow: 7 rows, columns auto-created per week */} + {/* Cell grid: column-flow: 7 rows, columns auto-created per week */}
({ }, []); const handleMouseLeave = useCallback((event?: React.MouseEvent) => { - // If leaving to a child (popover), do nothing here — the popover handlers manage hide. + // If leaving to a child (popover), do nothing here: the popover handlers manage hide. if (event) { const related = event.relatedTarget as Node | null; const popoverRoot = wrapperRef.current?.querySelector('.hover-popover-root') as Node | null; diff --git a/docs/app/components/CloudBackground.tsx b/docs/app/components/CloudBackground.tsx index 44e52ee4..3bf91e67 100644 --- a/docs/app/components/CloudBackground.tsx +++ b/docs/app/components/CloudBackground.tsx @@ -25,7 +25,7 @@ function cloudGradient( export default function CloudBackground() { return (