diff --git a/.github/workflows/docc.yml b/.github/workflows/docc.yml new file mode 100644 index 0000000..32385f6 --- /dev/null +++ b/.github/workflows/docc.yml @@ -0,0 +1,47 @@ +name: Build and Deploy DocC Documentation + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build-and-deploy: + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Swift + uses: swift-actions/setup-swift@v1 + with: + swift-version: '6.0' + + - name: Build documentation + run: | + xcrun docc build \ + --product TUIKit \ + --output-path ./docs + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: './docs' + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + if: github.event_name == 'push' && github.ref == 'refs/heads/main' diff --git a/Sources/TUIKit/Views/Box.swift b/Sources/TUIKit/Views/Box.swift index 678e664..d165fa0 100644 --- a/Sources/TUIKit/Views/Box.swift +++ b/Sources/TUIKit/Views/Box.swift @@ -7,23 +7,77 @@ /// A simple bordered container view. /// -/// `Box` wraps content in a border without additional styling. -/// Use `Card` if you need padding and background as well. +/// `Box` wraps content in a border without additional styling, padding, or background. +/// It's the most minimal container - just a border around content. /// -/// # Example +/// # Choosing the Right Container +/// +/// - **Box**: Minimal - just a border, no padding or background +/// - **Card**: Padded and filled - includes padding and subtle background +/// - **Panel**: Titled box - includes optional title in the border +/// - **ContainerView**: Full-featured - header, body, footer sections +/// +/// # Appearance Integration +/// +/// `Box` respects the current ``Appearance`` style. By default, it uses the +/// theme's border color and the current appearance (rounded, doubleLine, etc.): /// /// ```swift /// Box { -/// Text("Boxed content") +/// Text("Uses current appearance") /// } +/// .environment(\.appearance, .block) // Now renders with block characters +/// ``` /// -/// Box(.doubleLine, color: .yellow) { -/// VStack { -/// Text("Line 1") -/// Text("Line 2") +/// You can override both style and color: +/// +/// ```swift +/// Box(.heavy, color: .theme.accent) { +/// Text("Heavy bold border in accent color") +/// } +/// ``` +/// +/// # Example - Basic Usage +/// +/// ```swift +/// Box { +/// Text("Simple bordered content") +/// } +/// ``` +/// +/// # Example - Custom Styling +/// +/// ```swift +/// VStack { +/// Box(.doubleLine, color: .brightCyan) { +/// Text("Double-line border") +/// Text("In cyan") +/// } +/// +/// Box(.line, color: .yellow) { +/// Text("Thin ASCII border") /// } /// } /// ``` +/// +/// # Example - With Multiple Children +/// +/// ```swift +/// Box { +/// VStack(spacing: 1) { +/// Text("Item 1").bold() +/// Text("Item 2") +/// Text("Item 3") +/// } +/// } +/// ``` +/// +/// # Size Behavior +/// +/// The `Box` size is determined by its content: +/// - If content has a fixed size, `Box` will be that size plus border +/// - If content is flexible, `Box` expands to fill available space +/// - Content inside `Box` respects its layout constraints public struct Box: View { /// The content of the box. public let content: Content diff --git a/TUIKit.docc/Articles/Appearance.md b/TUIKit.docc/Articles/Appearance.md new file mode 100644 index 0000000..5c1c52a --- /dev/null +++ b/TUIKit.docc/Articles/Appearance.md @@ -0,0 +1,256 @@ +# Appearance System + +Learn about the 5 structural appearance styles for rendering borders and containers. + +## Overview + +TUIKit provides 5 appearance styles that control how borders and containers are rendered: + +- **Line**: Simple ASCII lines +- **Rounded**: Rounded corners (default) +- **DoubleLine**: Double-line borders +- **Heavy**: Heavy/bold borders +- **Block**: Half-block Unicode characters for solid appearance + +## Appearance Styles + +### Line Appearance + +Simple single-line ASCII borders: + +```swift +VStack { + Text("Content") +} +.border(.line) +``` + +Characters used: +- Horizontal: `-` +- Vertical: `|` +- Corners: `+` + +### Rounded Appearance + +Rounded corners (default appearance): + +```swift +VStack { + Text("Content") +} +.border(.rounded) +``` + +Characters used: +- Top-left: `╭` +- Top-right: `╮` +- Bottom-left: `╰` +- Bottom-right: `╯` +- Horizontal: `─` +- Vertical: `│` + +### DoubleLine Appearance + +Double-line borders for emphasis: + +```swift +VStack { + Text("Content") +} +.border(.doubleLine) +``` + +Characters used: +- Top-left: `╔` +- Top-right: `╗` +- Bottom-left: `╚` +- Bottom-right: `╝` +- Horizontal: `═` +- Vertical: `║` + +### Heavy Appearance + +Bold/heavy borders: + +```swift +VStack { + Text("Content") +} +.border(.heavy) +``` + +Characters used: +- Top-left: `┏` +- Top-right: `┓` +- Bottom-left: `┗` +- Bottom-right: `┛` +- Horizontal: `━` +- Vertical: `┃` + +### Block Appearance + +Modern half-block Unicode characters for a solid, integrated look: + +```swift +VStack { + Text("Content") +} +.border(.block) +``` + +Special rendering: +- **Top border**: `▄` (lower half-block) with foreground = container background +- **Side borders**: `█` (full block) with foreground = container background +- **Bottom border**: `▀` (upper half-block) with foreground = container background +- **Content**: Filled with container background color +- Uses full-block characters to create solid visual edges + +The block appearance creates a seamless, filled container that appears integrated with the terminal background. + +## Setting Global Appearance + +Use the appearance environment to set the default for all components: + +```swift +@main +struct MyApp: App { + var body: some Scene { + WindowGroup { + ContentView() + .environment(\.appearance, .block) + } + } +} +``` + +## Cycling Appearances + +The example app supports appearance cycling with the `a` key: + +```swift +@main +struct ExampleApp: App { + @State private var currentAppearance: Appearance = .rounded + + var body: some Scene { + WindowGroup { + ContentView() + .environment(\.appearance, currentAppearance) + .statusBarItems { + StatusBarItem( + label: "Appearance", + shortcut: Shortcut.letter("a"), + action: { + currentAppearance = currentAppearance.next() + } + ) + } + } + } +} +``` + +## Using Appearance with Containers + +All container views respect the appearance setting: + +```swift +// Using Panel with different appearances +VStack { + Panel(title: "Settings") { + Text("Configure options here") + } + .border(.rounded) + + Panel(title: "Status") { + Text("System information") + } + .border(.block) + + Card { + Text("Card content") + } + .border(.heavy) +} +``` + +## BorderStyle + +Use the ``BorderStyle`` type to customize individual borders: + +```swift +extension View { + func border( + _ style: BorderStyle = .default, + width: Int = 1, + color: Color = .theme.border, + appearance: Appearance = .rounded + ) -> some View { + // Returns bordered view + } +} + +// Custom border +Text("Custom border") + .border(.custom, width: 2, color: .theme.accent) +``` + +## Appearance in Components + +Different components use appearance differently: + +### Text with Border + +```swift +Text("Bordered text") + .border(.block) +``` + +### Card with Appearance + +```swift +Card { + Text("Content") +} +.border(.rounded) +``` + +### Panel with Title + +```swift +Panel(title: "Title") { + Text("Content") +} +.border(.block) +``` + +### Alerts and Dialogs + +Alerts automatically use the current appearance: + +```swift +Alert( + title: "Confirm", + message: "Are you sure?", + borderColor: .theme.border, + titleColor: .theme.accent +) +``` + +## Best Practices + +1. **Consistency**: Choose one appearance and stick with it +2. **Theme integration**: Use theme colors for borders +3. **Readability**: Ensure sufficient contrast for borders +4. **Performance**: Complex borders render efficiently +5. **Testing**: Test appearance on various terminal sizes + +## Related Topics + +- ``Appearance`` +- ``BorderStyle`` +- ``View/border(_:style:)-4xzvw`` +- +- ``Panel`` +- ``Card`` +- ``ContainerView`` diff --git a/TUIKit.docc/Articles/Architecture.md b/TUIKit.docc/Articles/Architecture.md new file mode 100644 index 0000000..779f06a --- /dev/null +++ b/TUIKit.docc/Articles/Architecture.md @@ -0,0 +1,258 @@ +# Architecture Overview + +Understand the architecture and design patterns that power TUIKit. + +## Core Design Principles + +TUIKit follows these key principles: + +1. **Declarative UI**: Describe what you want, not how to build it +2. **Composability**: Build complex UIs from simple, reusable components +3. **Value Semantics**: Views are lightweight value types, not stateful objects +4. **Data Flow**: Clear, unidirectional data flow through Environment and State +5. **Pure Swift**: No external dependencies or C bindings + +## The View System + +### View Protocol + +The `View` protocol is the foundation of TUIKit: + +```swift +public protocol View { + associatedtype Body: View + @ViewBuilder var body: Body { get } +} +``` + +Views are **not** UI elements—they're descriptions of UI that get rendered when needed. This enables: +- Lightweight composition +- Efficient updates +- Declarative syntax + +### Primitive vs Composite Views + +**Primitive Views** directly render content: +- Conform to `Renderable` +- Have `body: Never` +- Examples: `Text`, `Button`, `Menu` + +**Composite Views** combine other views: +- Define a `body` property +- Renderer walks the tree recursively +- Examples: `VStack`, `HStack`, `Card` + +### Rendering Pipeline + +1. **View Tree**: User creates view hierarchy +2. **Traversal**: Renderer walks tree depth-first +3. **Rendering**: Primitive views produce `FrameBuffer` character data +4. **Compositing**: Overlays blend on top with character-level precision +5. **Output**: Terminal renders the final buffer + +## State Management Architecture + +### Local State (@State) + +Stored in the view itself: +```swift +@State var count: Int = 0 +``` + +When `@State` changes, the view re-renders automatically. + +### Environment (Top-Down) + +Parent views pass data to children: + +```swift +@Environment(\.theme) var theme + +// Theme flows from App → all children +``` + +### Preferences (Bottom-Up) + +Children can propagate values to parents (rarely used in TUIKit). + +### Storage (@AppStorage, @SceneStorage) + +Persistent storage via: +- **@AppStorage**: UserDefaults (macOS) or JSON file (Linux) +- **@SceneStorage**: Per-scene state restoration + +## Data Flow Pattern + +``` +┌─────────────────────────────────────┐ +│ App or Scene │ +│ @AppStorage, @State, Environment │ +└────────┬────────────────────────────┘ + │ passes theme, data + ▼ +┌─────────────────────────────────────┐ +│ Composite Views (VStack, etc) │ +│ pass Environment to children │ +└────────┬────────────────────────────┘ + │ may use @State locally + ▼ +┌─────────────────────────────────────┐ +│ Primitive Views (Text, Button) │ +│ render to FrameBuffer │ +└─────────────────────────────────────┘ +``` + +## Theme and Appearance System + +### Theme Protocol + +Defines semantic colors: + +```swift +public protocol Theme { + var background: Color { get } + var accent: Color { get } + // ... other colors +} +``` + +### Appearance Protocol + +Defines structural styles: + +```swift +public enum Appearance { + case line, rounded, doubleLine, heavy, block +} +``` + +Both are passed through Environment, enabling: +- Global theme switching +- Dynamic appearance changes +- Consistent styling across app + +## Focus Management System + +``FocusManager`` tracks which element has keyboard focus: + +1. **Focus ID**: Each interactive element has a unique ID +2. **Navigation**: Tab/Shift+Tab moves focus +3. **Rendering**: Focused element shows visual indicator +4. **Events**: Keyboard events route to focused element + +```swift +@Environment(\.focusManager) var focusManager + +// Navigation automatically updates focusManager +// onKeyPress receives keyboard events +``` + +## Modifier System + +Modifiers wrap views to add behavior: + +```swift +extension View { + func padding(_ amount: Int) -> some View { + ModifiedView(content: self, modifier: PaddingModifier(amount)) + } +} +``` + +Modifiers compose: +```swift +Text("Hello") + .bold() // Text → BoldView + .padding(1) // BoldView → PaddedView + .border(.rounded) // PaddedView → BorderedView +``` + +## Component Library + +### Container Views +- `VStack`, `HStack`, `ZStack`: Layout primitives +- `Card`, `Box`, `Panel`: Styled containers +- `ContainerView`: Header/body/footer structure + +### Interactive Views +- `Button`: Clickable button with focus +- `Menu`: Selection menu with keyboard +- `Alert`, `Dialog`: Modal overlays + +### Structural Views +- `ForEach`: Render collections +- `Text`: Display text +- `Spacer`, `Divider`: Layout helpers + +### Status Bar +- `StatusBar`: Application status bar +- `StatusBarItem`: Individual item +- `Shortcut`: Keyboard shortcut display + +## Rendering Engine + +### FrameBuffer + +Character-level buffer for compositing: +- 2D array of characters and attributes +- Supports color, styling (bold, italic, etc.) +- Compositing for overlays + +### ANSI Renderer + +Converts buffer to ANSI escape codes: +- Color codes (foreground/background) +- Styling (bold, italic, underline) +- Terminal state management + +### Terminal Abstraction + +Platform-specific terminal handling: +- macOS: termios +- Linux: termios via libc +- Raw mode, alternate screen, signal handling + +## Performance Considerations + +1. **Lazy Rendering**: Only visible areas render +2. **Diff Optimization**: Only changed areas redraw +3. **Buffer Reuse**: FrameBuffer recycled across frames +4. **View Value Semantics**: Lightweight copying and discarding + +## Error Handling + +TUIKit uses Swift's error handling: +- Fatal errors for programmer mistakes +- Non-fatal errors for recovery scenarios +- Storage backend errors are silenced (fallback to defaults) + +## Platform Support + +### macOS +- Native UserDefaults for @AppStorage +- Full terminal support +- Modern Swift runtime + +### Linux +- JSON file storage for @AppStorage +- glibc-based terminal support +- XDG paths for configuration + +## Future Extensibility + +The architecture supports: +- Custom storage backends +- Custom themes +- Custom appearances +- Custom view components +- Animation system (planned) + +## Related Topics + +- ``View`` +- ``Theme`` +- ``Appearance`` +- ``FocusManager`` +- ``FrameBuffer`` +- +- diff --git a/TUIKit.docc/Articles/Focus.md b/TUIKit.docc/Articles/Focus.md new file mode 100644 index 0000000..d0631e4 --- /dev/null +++ b/TUIKit.docc/Articles/Focus.md @@ -0,0 +1,252 @@ +# Focus Management + +Understand how keyboard navigation and focus works in TUIKit. + +## Overview + +TUIKit provides a sophisticated focus management system that enables Tab/Shift+Tab navigation, keyboard shortcuts, and custom focus logic. + +## The Focus Manager + +``FocusManager`` manages which component currently has keyboard focus: + +```swift +@Environment(\.focusManager) var focusManager + +Button("Click me") { + // Handle click +} +.focused(focusID: "myButton") +``` + +## Tab Navigation + +By default, all focusable elements can be navigated with Tab and Shift+Tab: + +- **Tab**: Move focus to the next element +- **Shift+Tab**: Move focus to the previous element +- **Enter/Space**: Activate focused button + +```swift +VStack(spacing: 1) { + Text("Navigation") + + Button("First") { } + Button("Second") { } + Button("Third") { } +} +``` + +Users can Tab between the buttons in order. + +## Focus Indicators + +Focusable elements show a focus indicator when active: + +```swift +Button("Focusable") { + print("Activated") +} +``` + +The button displays: +- A border around the element +- Visual highlight (usually arrow prefix: `▸`) +- Theme accent color + +## Programmatic Focus + +Set focus programmatically: + +```swift +struct Settings: View { + @State private var focusedField: String? + + var body: some View { + VStack(spacing: 1) { + Button("First") { focusedField = "first" } + .focused(focusID: "first", focused: focusedField == "first") + + Button("Second") { focusedField = "second" } + .focused(focusID: "second", focused: focusedField == "second") + + Button("Reset") { focusedField = nil } + } + } +} +``` + +## Keyboard Events + +Handle keyboard events with `onKeyPress`: + +```swift +struct App: View { + var body: some View { + VStack { + ContentView() + .onKeyPress { event in + if event.key == .character("h") && event.modifiers.contains(.ctrl) { + print("Help requested") + return true + } + return false + } + } + } +} +``` + +### Key Event Structure + +``KeyEvent`` contains: + +- **key**: The key pressed (character, arrow, enter, etc.) +- **modifiers**: Ctrl, Shift, Alt flags +- **raw**: Raw terminal escape sequence + +### Special Keys + +Handle special keys like arrows and function keys: + +```swift +.onKeyPress { event in + switch event.key { + case .up: + print("Arrow up") + case .down: + print("Arrow down") + case .left: + print("Arrow left") + case .right: + print("Arrow right") + case .enter: + print("Enter pressed") + case .escape: + print("Escape pressed") + case .tab: + print("Tab pressed") + case .character(let char): + print("Character: \(char)") + default: + break + } + return true +} +``` + +## Focusable Protocol + +Components conform to ``Focusable`` to support focus: + +```swift +public protocol Focusable { + var focusID: String? { get } + var isFocused: Bool { get } +} +``` + +Button, Menu, and other interactive components automatically implement this. + +## Custom Focus Logic + +Create custom focus behavior: + +```swift +struct CustomMenu: View { + @State private var selectedIndex: Int = 0 + let items: [String] + + var body: some View { + VStack(spacing: 0) { + ForEach(Array(items.enumerated()), id: \.offset) { index, item in + Text(item) + .padding(1) + .background(selectedIndex == index ? .theme.accent : .clear) + } + } + .onKeyPress { event in + switch event.key { + case .up: + selectedIndex = max(0, selectedIndex - 1) + return true + case .down: + selectedIndex = min(items.count - 1, selectedIndex + 1) + return true + default: + return false + } + } + } +} +``` + +## Focus in Modals + +Focus works correctly with modals and overlays: + +```swift +@State var showDialog: Bool = false + +var body: some View { + VStack { + if showDialog { + Alert( + title: "Confirm", + message: "Continue?" + ) { + VStack(spacing: 1) { + Button("Yes") { showDialog = false } + Button("No") { showDialog = false } + } + } + .modal() + } + + Button("Show Dialog") { showDialog = true } + } +} +``` + +When a modal appears, focus automatically moves to the first focusable element in the modal. + +## Best Practices + +1. **Tab order**: Arrange elements logically for Tab navigation +2. **Feedback**: Always provide visual feedback for focused elements +3. **Shortcuts**: Implement keyboard shortcuts for common actions +4. **Testing**: Test focus behavior with keyboard navigation +5. **Accessibility**: Consider users with motor impairments who rely on keyboard + +## Status Bar Integration + +Status bar items show keyboard shortcuts: + +```swift +.statusBarItems { + StatusBarItem( + label: "Help", + shortcut: Shortcut.letter("?"), + action: { + print("Show help") + } + ) + + StatusBarItem( + label: "Quit", + shortcut: Shortcut.letter("q"), + action: { + print("Exit app") + } + ) +} +``` + +## Related Topics + +- ``FocusManager`` +- ``KeyEvent`` +- ``Focusable`` +- ``View/onKeyPress(_:)`` +- ``StatusBar`` +- ``StatusBarItem`` diff --git a/TUIKit.docc/Articles/GettingStarted.md b/TUIKit.docc/Articles/GettingStarted.md new file mode 100644 index 0000000..3a35f39 --- /dev/null +++ b/TUIKit.docc/Articles/GettingStarted.md @@ -0,0 +1,180 @@ +# Getting Started with TUIKit + +Create your first terminal user interface in minutes. + +## Installation + +### Using Swift Package Manager + +Add TUIKit to your `Package.swift`: + +```swift +.package(url: "https://github.com/anthropics/SwiftTUI.git", from: "0.1.0") +``` + +Or via Xcode: File > Add Packages > Enter repository URL + +## Creating Your First App + +### 1. Define Your App + +Every TUIKit app needs an `@main` entry point that conforms to the `App` protocol: + +```swift +import TUIKit + +@main +struct HelloApp: App { + var body: some Scene { + WindowGroup { + Text("Hello, TUIKit!") + } + } +} +``` + +### 2. Add Some Layout + +Use `VStack` and `HStack` to organize your content: + +```swift +@main +struct LayoutApp: App { + var body: some Scene { + WindowGroup { + VStack(spacing: 1) { + Text("Welcome") + .bold() + + Text("Build terminal UIs in Swift") + + Spacer() + + Text("Press 'q' to quit") + .foregroundColor(.theme.foregroundSecondary) + } + .padding() + } + } +} +``` + +### 3. Make It Interactive + +Add buttons and state to create interactive interfaces: + +```swift +import TUIKit + +@main +struct InteractiveApp: App { + @State var count: Int = 0 + + var body: some Scene { + WindowGroup { + VStack(spacing: 1) { + Text("Counter: \(count)") + .bold() + + HStack(spacing: 2) { + Button("Increment") { count += 1 } + Button("Decrement") { count = max(0, count - 1) } + } + + Spacer() + } + .padding() + } + } +} +``` + +## Running Your App + +### From Command Line + +```bash +swift run +``` + +### With a Custom Executable Name + +Add to `Package.swift`: + +```swift +.executableTarget( + name: "MyApp", + dependencies: [.product(name: "TUIKit", package: "SwiftTUI")] +) +``` + +Then run: + +```bash +swift run MyApp +``` + +## Understanding the Basics + +### Views + +Everything in TUIKit is a `View`. Views are lightweight, composable units that render content to the terminal. + +Common views: +- **`Text`**: Display text with optional styling +- **`Button`**: Interactive button with action handler +- **`VStack`**: Arrange views vertically +- **`HStack`**: Arrange views horizontally +- **`Spacer`**: Fill available space + +### State Management + +Use `@State` to make your views interactive: + +```swift +@State var isVisible: Bool = true + +VStack { + if isVisible { + Text("Visible") + } + Button("Toggle") { isVisible.toggle() } +} +``` + +### Modifiers + +Customize views with modifiers: + +```swift +Text("Hello") + .bold() + .foregroundColor(.theme.accent) + .padding(1) + .border(.rounded) +``` + +## Next Steps + +- Explore the to understand component organization +- Learn for complex state scenarios +- Discover to customize colors and appearance +- Check out for all available styling options +- Try tutorial for step-by-step guidance + +## Key Bindings + +By default, TUIKit apps support: +- **`q`**: Quit the application +- **`t`**: Cycle through themes +- **`a`**: Cycle through appearance styles +- **`?`**: Show help + +See ``KeyEvent`` for custom keyboard handling. + +## Terminal Requirements + +- Minimum 80x24 character terminal +- ANSI color support (256 colors recommended) +- UTF-8 encoding +- Supports: macOS Terminal, iTerm2, Kitty, and Linux terminals (gnome-terminal, konsole, etc.) diff --git a/TUIKit.docc/Articles/Modifiers.md b/TUIKit.docc/Articles/Modifiers.md new file mode 100644 index 0000000..0288254 --- /dev/null +++ b/TUIKit.docc/Articles/Modifiers.md @@ -0,0 +1,340 @@ +# View Modifiers + +Learn how to use modifiers to customize the appearance and behavior of views. + +## Overview + +Modifiers are methods that return a modified copy of a view. Chain them together to build complex layouts and styling: + +```swift +Text("Hello") + .bold() + .foregroundColor(.theme.accent) + .padding(1) + .border(.rounded) +``` + +## Layout Modifiers + +### padding(_:) + +Add padding around content: + +```swift +Text("Content") + .padding() // Default 1 unit + .padding(2) // 2 units all sides + .padding(.top, 1) // 1 unit top + .padding(.horizontal, 2) // 2 units left/right +``` + +### frame(width:height:alignment:) + +Set fixed size: + +```swift +Text("Fixed") + .frame(width: 20, height: 5) + +Button("Button") + .frame(width: 15) +``` + +### frame(minWidth:maxWidth:minHeight:maxHeight:) + +Set flexible size constraints: + +```swift +Text("Flexible") + .frame(minWidth: 10, maxWidth: 50) + .frame(minHeight: 3, maxHeight: 10) + +Text("Fill available space") + .frame(maxWidth: .infinity, maxHeight: .infinity) +``` + +## Color Modifiers + +### foregroundColor(_:) + +Set text color: + +```swift +Text("Colored") + .foregroundColor(.theme.accent) + +Text("Green") + .foregroundColor(.ansi(.brightGreen)) + +Text("Custom") + .foregroundColor(.hex("FF5500")) +``` + +### background(_:) + +Add background color: + +```swift +Text("Background") + .background(.theme.accent) + .foregroundColor(.theme.background) +``` + +## Text Styling + +### Bold, Italic, Underline + +```swift +Text("Bold").bold() +Text("Italic").italic() +Text("Underline").underlined() +Text("Strikethrough").strikethrough() +Text("Dim").dimmed() +Text("Blinking").blinking() +Text("Inverted").inverted() +``` + +Combine multiple modifiers: + +```swift +Text("Complex") + .bold() + .italic() + .foregroundColor(.theme.accent) +``` + +## Border and Structure + +### border(_:) + +Add borders with different styles: + +```swift +Text("Bordered") + .border(.rounded) + +Text("Heavy border") + .border(.heavy, width: 2) + +VStack { + Text("Content") +} +.border(.block, color: .theme.accent) +``` + +## Overlay and Compositing + +### overlay(_:) + +Layer content on top: + +```swift +Text("Background") + .overlay { + Text("Foreground") + } +``` + +### dimmed() + +Reduce visual emphasis: + +```swift +VStack { + Text("Normal") + Text("Dimmed") + .dimmed() +} +``` + +### modal() + +Combine dimmed + centered overlay: + +```swift +showAlert { + Alert(title: "Alert", message: "Message") + .modal() +} +``` + +## Event Modifiers + +### onKeyPress(_:) + +Handle keyboard input: + +```swift +VStack { + ContentView() +} +.onKeyPress { event in + if event.key == .character("q") { + // Quit + return true + } + return false +} +``` + +### onAppear(_:) + +Run code when view appears: + +```swift +Text("Content") + .onAppear { + print("View appeared") + } +``` + +### onDisappear(_:) + +Run code when view disappears: + +```swift +Text("Content") + .onDisappear { + print("View disappearing") + } +``` + +### task(_:) + +Run async code: + +```swift +VStack { + ContentView() +} +.task { + // Fetch data + let data = try await fetchData() +} +``` + +## Data Flow Modifiers + +### environment(_:_:) + +Pass environment values to children: + +```swift +VStack { + ContentView() +} +.environment(\.theme, customTheme) +``` + +### statusBarItems(_:) + +Define status bar items: + +```swift +VStack { + ContentView() +} +.statusBarItems { + StatusBarItem( + label: "Help", + shortcut: Shortcut.letter("?"), + action: { print("Help") } + ) +} +``` + +## Combining Modifiers + +Modifiers are applied in order—the order matters: + +```swift +// This looks different... +Text("Order matters") + .foregroundColor(.theme.accent) + .padding(1) + .border(.rounded) + +// ...than this +Text("Order matters") + .border(.rounded) + .padding(1) + .foregroundColor(.theme.accent) +``` + +### Creating Custom Modifiers + +Create reusable modifiers: + +```swift +struct PrimaryButtonStyle: ViewModifier { + func body(content: Content) -> some View { + content + .bold() + .foregroundColor(.theme.background) + .background(.theme.accent) + .padding(1) + .border(.rounded) + } +} + +extension View { + func primaryButton() -> some View { + modifier(PrimaryButtonStyle()) + } +} + +// Usage +Button("Click") { } + .primaryButton() +``` + +## Common Patterns + +### Centered Text + +```swift +Text("Centered") + .frame(maxWidth: .infinity, alignment: .center) +``` + +### Full-Size Container + +```swift +VStack { + ContentView() +} +.frame(maxWidth: .infinity, maxHeight: .infinity) +``` + +### Bordered Section + +```swift +VStack(spacing: 1) { + Text("Title").bold() + ContentView() +} +.padding(1) +.border(.rounded) +``` + +### Button Row + +```swift +HStack(spacing: 2) { + Button("OK") { } + Button("Cancel") { } +} +.frame(maxWidth: .infinity, alignment: .trailing) +``` + +## Related Topics + +- ``View`` +- ``ViewModifier`` +- ``View/padding(_:)-19gu9`` +- ``View/frame(width:height:alignment:)`` +- ``View/border(_:style:)-4xzvw`` +- ``View/background(_:)`` +- ``View/foregroundColor(_:)`` +- ``View/overlay(alignment:content:)`` +- ``View/onKeyPress(_:)`` diff --git a/TUIKit.docc/Articles/StateManagement.md b/TUIKit.docc/Articles/StateManagement.md new file mode 100644 index 0000000..8966b17 --- /dev/null +++ b/TUIKit.docc/Articles/StateManagement.md @@ -0,0 +1,259 @@ +# State Management + +Learn how to manage application state and data flow in TUIKit. + +## Overview + +TUIKit provides several mechanisms for managing state: + +- **`@State`**: Local view state +- **`@Environment`**: Top-down data flow +- **`Binding`**: Two-way data binding +- **`@AppStorage`**: Persistent application settings +- **`@SceneStorage`**: Scene-specific state restoration + +## @State Property Wrapper + +`@State` stores local state within a view and automatically triggers re-renders when the value changes: + +```swift +struct Counter: View { + @State var count: Int = 0 + + var body: some View { + VStack(spacing: 1) { + Text("Count: \(count)") + .bold() + + HStack(spacing: 2) { + Button("Decrement") { count -= 1 } + Button("Increment") { count += 1 } + } + } + } +} +``` + +### Rules for @State + +- Declare as `private` to prevent external modification +- Initialize with a default value +- Use only in views, not in view models +- State is destroyed when view is removed from hierarchy + +```swift +struct InputForm: View { + @State private var name: String = "" + @State private var age: Int = 0 + @State private var agreed: Bool = false + + var body: some View { + VStack(spacing: 1) { + // View content + } + } +} +``` + +## Binding + +A `Binding` creates a two-way connection between a view and a state variable: + +```swift +struct Parent: View { + @State var isExpanded: Bool = false + + var body: some View { + VStack { + Child(isExpanded: $isExpanded) + } + } +} + +struct Child: View { + @Binding var isExpanded: Bool + + var body: some View { + Button(isExpanded ? "Collapse" : "Expand") { + isExpanded.toggle() + } + } +} +``` + +Use `$` to create a binding to a state property. + +### Creating Custom Bindings + +Create computed bindings for complex logic: + +```swift +struct FilteredList: View { + @State private var showDetails: Bool = false + + var detailsBinding: Binding { + Binding( + get: { showDetails }, + set: { newValue in + if newValue { + print("Details opened") + } + showDetails = newValue + } + ) + } + + var body: some View { + Button("Toggle") { + detailsBinding.wrappedValue.toggle() + } + } +} +``` + +## @Environment and Environment Keys + +Environment provides top-down data flow to all child views: + +```swift +struct App: View { + var body: some View { + VStack { + ContentView() + .environment(\.theme, customTheme) + } + } +} + +struct ContentView: View { + @Environment(\.theme) var theme + + var body: some View { + Text("Using current theme") + .foregroundColor(theme.foreground) + } +} +``` + +### Custom Environment Keys + +Define custom environment values: + +```swift +struct UserPreferencesKey: EnvironmentKey { + static let defaultValue = UserPreferences() +} + +extension EnvironmentValues { + var userPreferences: UserPreferences { + get { self[UserPreferencesKey.self] } + set { self[UserPreferencesKey.self] = newValue } + } +} + +// Usage +struct App: View { + var body: some View { + VStack { + ContentView() + .environment(\.userPreferences, UserPreferences(language: "de")) + } + } +} + +struct ContentView: View { + @Environment(\.userPreferences) var prefs + + var body: some View { + Text("Language: \(prefs.language)") + } +} +``` + +## @AppStorage + +`@AppStorage` persists values to storage (UserDefaults on macOS, JSON file on Linux): + +```swift +struct App: View { + @AppStorage("theme") var selectedTheme: String = "green" + @AppStorage("fontSize") var fontSize: Int = 12 + + var body: some View { + VStack { + Text("Current theme: \(selectedTheme)") + // Settings UI + } + } +} +``` + +Changes to `@AppStorage` variables are automatically saved and restored across app launches. + +## @SceneStorage + +`@SceneStorage` preserves state for the current scene: + +```swift +struct MyScene: Scene { + @SceneStorage("selectedTab") var selectedTab: String = "home" + @SceneStorage("scrollPosition") var scrollPosition: Int = 0 + + var body: some Scene { + WindowGroup { + VStack { + if selectedTab == "home" { + HomeView() + } + } + } + } +} +``` + +## Data Flow Pattern + +### Recommended Pattern + +1. **Local state**: Use `@State` for temporary UI state +2. **Shared state**: Use `@Environment` to pass to child views +3. **Persistent state**: Use `@AppStorage` for settings +4. **Child updates**: Use `@Binding` to let children modify parent state + +```swift +@main +struct MyApp: App { + // Persistent settings + @AppStorage("theme") var theme: String = "green" + + var body: some Scene { + WindowGroup { + ContentView() + .environment(\.theme, themeForString(theme)) + } + } +} + +struct ContentView: View { + @Environment(\.theme) var theme + @State private var showMenu: Bool = false + + var body: some View { + VStack { + if showMenu { + MenuView(showing: $showMenu) + } + } + } +} +``` + +## Related Topics + +- ``State`` +- ``Binding`` +- ``@Environment`` +- ``@AppStorage`` +- ``@SceneStorage`` +- ``EnvironmentKey`` +- ``EnvironmentValues`` diff --git a/TUIKit.docc/Articles/Theming.md b/TUIKit.docc/Articles/Theming.md new file mode 100644 index 0000000..8f4dc0e --- /dev/null +++ b/TUIKit.docc/Articles/Theming.md @@ -0,0 +1,261 @@ +# Theming System + +Customize the appearance of your application with TUIKit's flexible theming system. + +## Built-in Themes + +TUIKit includes 4 beautiful phosphor-style themes: + +### Green Theme + +Classic green phosphor aesthetic for a retro terminal feel: + +```swift +.environment(\.themeManager.currentTheme, .green) +``` + +- **Accent**: Bright green +- **Background**: Deep dark green +- **Foreground**: Light green text + +### Amber Theme + +Warm amber/orange phosphor theme: + +```swift +.environment(\.themeManager.currentTheme, .amber) +``` + +- **Accent**: Bright amber +- **Background**: Dark brown-orange +- **Foreground**: Light amber text + +### White Theme + +Cool white/blue-tinted theme for modern appearance: + +```swift +.environment(\.themeManager.currentTheme, .white) +``` + +- **Accent**: Bright white +- **Background**: Deep blue-black +- **Foreground**: Light white text + +### Red Theme + +Bold red phosphor theme: + +```swift +.environment(\.themeManager.currentTheme, .red) +``` + +- **Accent**: Bright red +- **Background**: Dark red-black +- **Foreground**: Light red text + +## Using Themes + +### Default Theme + +By default, the Green theme is active: + +```swift +@main +struct MyApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} +``` + +### Switching Themes + +Use the theme manager to switch themes at runtime: + +```swift +struct App: View { + @Environment(\.themeManager) var themeManager + + var body: some View { + VStack { + Button("Switch to Amber") { + themeManager.currentTheme = .amber + } + Button("Switch to White") { + themeManager.currentTheme = .white + } + } + } +} +``` + +### Accessing Theme Colors + +Use `Color.theme` shorthand to access current theme colors: + +```swift +VStack { + Text("This uses the theme accent color") + .foregroundColor(.theme.accent) + + Text("This uses the theme foreground") + .foregroundColor(.theme.foreground) + + Text("This uses secondary text color") + .foregroundColor(.theme.foregroundSecondary) +} +``` + +## Theme Protocol + +Themes conform to the ``Theme`` protocol: + +```swift +public protocol Theme { + var background: Color { get } + var containerBackground: Color { get } + var containerHeaderBackground: Color { get } + var buttonBackground: Color { get } + var statusBarBackground: Color { get } + var foreground: Color { get } + var foregroundSecondary: Color { get } + var foregroundTertiary: Color { get } + var accent: Color { get } + var border: Color { get } + var statusBarForeground: Color { get } +} +``` + +## Creating Custom Themes + +Implement the `Theme` protocol to create custom themes: + +```swift +struct CompanyTheme: Theme { + let background = Color.hex("0a0a0a") + let containerBackground = Color.hex("1a1a2e") + let containerHeaderBackground = Color.hex("16213e") + let buttonBackground = Color.hex("0f3460") + let statusBarBackground = Color.hex("0f3460") + let foreground = Color.hex("eaeaea") + let foregroundSecondary = Color.hex("999999") + let foregroundTertiary = Color.hex("666666") + let accent = Color.hex("00d4ff") + let border = Color.hex("00d4ff") + let statusBarForeground = Color.hex("666666") +} + +// Use it +@main +struct MyApp: App { + var body: some Scene { + WindowGroup { + ContentView() + .environment(\.themeManager.currentTheme, CompanyTheme()) + } + } +} +``` + +## Color System + +TUIKit supports multiple color spaces: + +### ANSI Colors + +Use standard terminal colors: + +```swift +Color.ansi(.brightGreen) +Color.ansi(.red) +``` + +### Hex Colors + +Specify colors as hex strings or integers: + +```swift +Color.hex("FF5500") +Color.hex("#FF5500") +Color.hex(0xFF5500) +``` + +### RGB Colors + +Define colors by RGB values: + +```swift +Color.rgb(red: 255, green: 85, blue: 0) +Color.rgb(red: 1.0, green: 0.33, blue: 0.0) +``` + +### 256-Color Palette + +Use 256-color terminal palette (0-255): + +```swift +Color.palette256(196) +``` + +### HSL Colors + +Create colors using HSL (Hue, Saturation, Lightness): + +```swift +Color.hsl(hue: 25, saturation: 100, lightness: 50) +``` + +## Color Modifiers + +Adjust colors programmatically: + +```swift +let baseColor = Color.hex("#FF5500") + +let lighter = baseColor.lighter(by: 0.2) +let darker = baseColor.darker(by: 0.3) +let transparent = baseColor.opacity(0.5) +``` + +## Theme Cycling + +The example app supports theme cycling with the `t` key: + +```swift +@main +struct ExampleApp: App { + var body: some Scene { + WindowGroup { + ContentView() + .statusBarItems { + StatusBarItem( + label: "Theme", + shortcut: Shortcut.letter("t"), + action: { + // Cycle themes + } + ) + } + } + } +} +``` + +## Best Practices + +1. **Use semantic colors**: Reference `Color.theme.accent` instead of hardcoding colors +2. **Test readability**: Ensure sufficient contrast in custom themes +3. **Support all modes**: Create both dark and light themed colors if needed +4. **Store preferences**: Use `@AppStorage` to remember user theme choice +5. **Provide visual feedback**: Show current theme in settings + +## Related Topics + +- ``Color`` +- ``Theme`` +- ``ThemeManager`` +- +- diff --git a/TUIKit.docc/Articles/ViewHierarchy.md b/TUIKit.docc/Articles/ViewHierarchy.md new file mode 100644 index 0000000..c248b0a --- /dev/null +++ b/TUIKit.docc/Articles/ViewHierarchy.md @@ -0,0 +1,239 @@ +# Understanding the View Hierarchy + +Learn how TUIKit's declarative view model works and how to compose views effectively. + +## The View Protocol + +Everything in TUIKit conforms to the `View` protocol. A view is a lightweight value type that describes a piece of UI: + +```swift +public protocol View { + associatedtype Body: View + @ViewBuilder var body: Body { get } +} +``` + +Views are **not** the rendered output—they're descriptions of UI that get rendered when needed. + +### Primitive Views + +Some views have `body: Never` and directly conform to `Renderable`. These render their own content: + +- **`Text`**: Display text with styling +- **`Button`**: Interactive clickable element +- **`Menu`**: Selection menu with keyboard navigation +- **`Spacer`**: Occupy available space +- **`Divider`**: Horizontal or vertical separator +- **`EmptyView`**: Invisible placeholder view + +### Composite Views + +Most views define a `body` that combines other views: + +```swift +struct MyCard: View { + var body: some View { + VStack(spacing: 1) { + Text("Title").bold() + Text("Content") + } + .border(.rounded) + } +} +``` + +Composite views don't render directly—the renderer walks the view tree and renders primitives. + +## Container Views + +### VStack (Vertical Stack) + +Stack views vertically with optional spacing: + +```swift +VStack(spacing: 1) { + Text("First") + Text("Second") + Text("Third") +} +``` + +### HStack (Horizontal Stack) + +Stack views horizontally: + +```swift +HStack(spacing: 2) { + Button("OK") { } + Button("Cancel") { } +} +``` + +### ZStack (Depth Stack) + +Layer views on top of each other: + +```swift +ZStack { + Text("Background") + Text("Foreground") +} +``` + +### ForEach + +Render a collection of items: + +```swift +ForEach(items) { item in + Text(item.name) +} +``` + +## Control Flow + +### Conditionals + +Use standard Swift `if` statements: + +```swift +if isLoading { + Text("Loading...") +} else { + Text("Loaded!") +} +``` + +### Optional Values + +Handle optional views: + +```swift +if let name = userName { + Text("Hello, \(name)") +} else { + Text("Not logged in") +} +``` + +## The ViewBuilder Result Builder + +`@ViewBuilder` is a result builder that enables the declarative syntax: + +```swift +@ViewBuilder +var content: some View { + if condition { + Text("A") + } else { + Text("B") + } + Text("C") +} +``` + +It supports: +- Up to 10 children without nesting +- `if/else if/else` conditionals +- `if let` optional unwrapping +- Arrays and `ForEach` +- Nested result builders + +## Type Erasure with AnyView + +When you need to return different view types, use `AnyView`: + +```swift +func makeView(condition: Bool) -> AnyView { + if condition { + return AnyView(Text("A")) + } else { + return AnyView(VStack { Text("B") }) + } +} +``` + +> Note: Use `AnyView` sparingly—prefer `@ViewBuilder` when possible. + +## Custom Views + +Create your own reusable components: + +```swift +struct CustomButton: View { + let label: String + let action: () -> Void + + var body: some View { + Button(label, action: action) + .padding(1) + .border(.rounded) + } +} + +// Usage +CustomButton(label: "Click me", action: { + print("Clicked") +}) +``` + +## View Composition Pattern + +Build complex UIs by composing smaller views: + +```swift +struct ContentView: View { + var body: some View { + VStack(spacing: 1) { + HeaderView() + BodyView() + FooterView() + } + } +} + +struct HeaderView: View { + var body: some View { + Text("Header").bold() + } +} + +struct BodyView: View { + var body: some View { + Text("Content").padding(1) + } +} + +struct FooterView: View { + var body: some View { + Text("Footer") + } +} +``` + +## View Lifetime + +Views are value types—they're created, rendered, and discarded. They don't have persistent storage. Use `@State` for state that persists across renders: + +```swift +struct Counter: View { + @State var count: Int = 0 + + var body: some View { + VStack { + Text("Count: \(count)") + Button("Increment") { count += 1 } + } + } +} +``` + +## Related Topics + +- ``View`` +- ``@ViewBuilder`` +- ``VStack`` +- ``HStack`` +- ``ZStack`` +- +- diff --git a/TUIKit.docc/Info.plist b/TUIKit.docc/Info.plist new file mode 100644 index 0000000..a7ba34d --- /dev/null +++ b/TUIKit.docc/Info.plist @@ -0,0 +1,18 @@ + + + + + CFBundleName + TUIKit + CFBundleIdentifier + com.anthropic.tuikit.documentation + CFBundleVersion + 0.1.0 + CFBundlePackageType + dext + NSHumanReadableCopyright + Copyright © 2026 Anthropic. All rights reserved. + NSExtensionPointIdentifier + com.apple.documentation.extension + + diff --git a/TUIKit.docc/TUIKit.md b/TUIKit.docc/TUIKit.md new file mode 100644 index 0000000..5d1f359 --- /dev/null +++ b/TUIKit.docc/TUIKit.md @@ -0,0 +1,124 @@ +# ``TUIKit`` + +Build beautiful, interactive terminal user interfaces in Swift. + +## Overview + +TUIKit is a modern Swift framework for creating sophisticated terminal user interfaces (TUIs) on macOS and Linux. It provides a declarative, SwiftUI-like API with support for themes, styling, focus management, and interactive components. + +### Key Features + +- **Declarative UI**: Build interfaces using Swift's result builders and view composition +- **5 Appearance Styles**: line, rounded, doubleLine, heavy, and block rendering modes +- **4 Phosphor Themes**: Green, Amber, White, and Red with customizable colors +- **Rich Components**: Text, Button, Menu, Alert, Dialog, Card, Panel, and more +- **Focus Management**: Tab/Shift+Tab navigation with keyboard shortcuts +- **State Management**: `@State`, `@Environment`, `@AppStorage`, `@SceneStorage` property wrappers +- **No Dependencies**: Pure Swift implementation for macOS 10.15+ and Linux + +## Getting Started + +Create your first TUIKit app in minutes: + +```swift +import TUIKit + +@main +struct MyApp: App { + var body: some Scene { + WindowGroup { + VStack(spacing: 1) { + Text("Welcome to TUIKit!") + .bold() + .foregroundColor(.theme.accent) + + Spacer() + + Button("Press me") { + print("Button pressed!") + } + + Spacer() + } + .padding() + } + } +} +``` + +Run with: `swift run` + +## Topics + +### Essentials + +- ``View`` +- ``App`` +- ``Scene`` +- ``@main`` + +### Building Views + +- +- +- ``VStack`` +- ``HStack`` +- ``ZStack`` +- ``ForEach`` + +### Interactive Components + +- ``Button`` +- ``Menu`` +- ``Alert`` +- ``Dialog`` +- ``Text`` + +### Styling & Appearance + +- +- +- ``Color`` +- ``Theme`` +- ``Appearance`` + +### Layout & Modifiers + +- +- ``View/padding(_:)-19gu9`` +- ``View/frame(width:height:alignment:)`` +- ``View/border(_:style:)-4xzvw`` + +### State Management + +- +- ``State`` +- ``Binding`` +- ``@Environment`` +- ``@AppStorage`` + +### Advanced Topics + +- +- +- +- ``FocusManager`` +- ``KeyEvent`` + +### Examples + +- +- +- + +## Resources + +- [GitHub Repository](https://github.com/anthropics/SwiftTUI) +- [Example Application](https://github.com/anthropics/SwiftTUI/tree/main/Sources/TUIKitExample) +- [Issue Tracker](https://github.com/anthropics/SwiftTUI/issues) + +## Minimum Requirements + +- Swift 6.0 or later +- macOS 10.15+ or Linux (glibc) +- Terminal with ANSI color support (256 colors or better) diff --git a/TUIKit.docc/Tutorials/BuildInteractiveMenu.md b/TUIKit.docc/Tutorials/BuildInteractiveMenu.md new file mode 100644 index 0000000..9ffe1a1 --- /dev/null +++ b/TUIKit.docc/Tutorials/BuildInteractiveMenu.md @@ -0,0 +1,375 @@ +# Building an Interactive Menu + +Create a navigation menu with keyboard shortcuts and status bar hints. + +@Intro(title: "Build an Interactive Menu App") { + Learn how to create a functional menu system with keyboard navigation, + selection handling, and status bar integration. + + You'll build a simple settings menu that demonstrates focus management, + keyboard events, and status bar items. +} + +## Overview + +In this tutorial, you'll create a menu-driven application featuring: + +- `Menu` component for selection +- Keyboard shortcut handling +- Status bar with hints +- Page navigation patterns + +@Section(title: "Create the Menu Component") { + @ContentAndMedia { + Start with a basic menu that displays options and tracks selection. + } + + @Steps { + @Step { + Create your app with a `Menu`: + + ```swift + import TUIKit + + @main + struct MenuApp: App { + @State private var selectedOption: String = "home" + + var body: some Scene { + WindowGroup { + VStack(spacing: 2) { + Text("Main Menu") + .bold() + .foregroundColor(.theme.accent) + + Spacer() + + Menu( + items: [ + MenuItem("📄 View Profile", id: "profile"), + MenuItem("⚙️ Settings", id: "settings"), + MenuItem("💾 Save Data", id: "save"), + MenuItem("❌ Exit", id: "exit") + ], + selection: $selectedOption + ) + + Spacer() + + Text("Selected: \(selectedOption)") + .foregroundColor(.theme.foregroundSecondary) + } + .padding() + } + } + } + ``` + + - `Menu` creates an interactive selectable list + - `$selectedOption` creates a two-way binding + - Use Tab/Arrow keys to navigate + - Press Enter to confirm selection + } + + @Step { + Run the app: + + ```bash + swift run + ``` + + Test navigating with Tab, arrow keys, and Enter. + The "Selected" text updates as you navigate. + } + } +} + +@Section(title: "Add Page Navigation") { + @ContentAndMedia { + Create different pages that show based on menu selection. + } + + @Steps { + @Step { + Add conditional pages: + + ```swift + @main + struct MenuApp: App { + @State private var selectedOption: String = "home" + + var body: some Scene { + WindowGroup { + if selectedOption == "exit" { + VStack { + Text("Exiting...") + } + } else { + VStack(spacing: 2) { + Text("Main Menu") + .bold() + .foregroundColor(.theme.accent) + + Spacer() + + Menu( + items: [ + MenuItem("📄 View Profile", id: "profile"), + MenuItem("⚙️ Settings", id: "settings"), + MenuItem("💾 Save Data", id: "save"), + MenuItem("❌ Exit", id: "exit") + ], + selection: $selectedOption + ) + + Spacer() + + currentPageContent + } + .padding() + } + } + } + + @ViewBuilder + var currentPageContent: some View { + switch selectedOption { + case "profile": + VStack(spacing: 1) { + Text("📄 Profile Information").bold() + Text("Name: John Doe") + Text("Role: Developer") + } + case "settings": + VStack(spacing: 1) { + Text("⚙️ Application Settings").bold() + Text("Theme: Green") + Text("Language: English") + } + case "save": + Text("✓ Data saved successfully!") + .foregroundColor(.theme.success) + default: + Text("Select an option above") + } + } + } + ``` + + The `currentPageContent` computed property shows different content + based on the selected menu item. + } + + @Step { + Run and test navigation: + + ```bash + swift run + ``` + + Navigate to each menu item and see the page content change. + } + } +} + +@Section(title: "Add Status Bar Hints") { + @ContentAndMedia { + Add a status bar at the bottom with helpful keyboard hints. + } + + @Steps { + @Step { + Add status bar items: + + ```swift + @main + struct MenuApp: App { + @State private var selectedOption: String = "home" + + var body: some Scene { + WindowGroup { + if selectedOption == "exit" { + VStack { + Text("Exiting...") + } + } else { + VStack(spacing: 2) { + Text("Main Menu") + .bold() + .foregroundColor(.theme.accent) + + Spacer() + + Menu( + items: [ + MenuItem("📄 View Profile", id: "profile"), + MenuItem("⚙️ Settings", id: "settings"), + MenuItem("💾 Save Data", id: "save"), + MenuItem("❌ Exit", id: "exit") + ], + selection: $selectedOption + ) + + Spacer() + + currentPageContent + } + .padding() + } + .statusBarItems { + StatusBarItem( + label: "Select", + shortcut: Shortcut.enter, + action: { } + ) + + StatusBarItem( + label: "Theme", + shortcut: Shortcut.letter("t"), + action: { } + ) + + StatusBarItem( + label: "Help", + shortcut: Shortcut.letter("?"), + action: { } + ) + + StatusBarItem( + label: "Quit", + shortcut: Shortcut.letter("q"), + action: { } + ) + } + } + } + + @ViewBuilder + var currentPageContent: some View { + switch selectedOption { + case "profile": + VStack(spacing: 1) { + Text("📄 Profile Information").bold() + Text("Name: John Doe") + Text("Role: Developer") + } + case "settings": + VStack(spacing: 1) { + Text("⚙️ Application Settings").bold() + Text("Theme: Green") + Text("Language: English") + } + case "save": + Text("✓ Data saved successfully!") + .foregroundColor(.theme.success) + default: + Text("Select an option above") + } + } + } + ``` + + `statusBarItems` adds a bottom status bar with keyboard shortcuts. + } + + @Step { + Run the app: + + ```bash + swift run + ``` + + The status bar appears at the bottom showing available shortcuts. + } + } +} + +@Section(title: "Add Keyboard Shortcuts") { + @ContentAndMedia { + Handle custom keyboard shortcuts to navigate directly to menu items. + } + + @Steps { + @Step { + Add keyboard event handling: + + ```swift + .onKeyPress { event in + switch event.key { + case .character("1"): + selectedOption = "profile" + return true + case .character("2"): + selectedOption = "settings" + return true + case .character("3"): + selectedOption = "save" + return true + case .character("e"): + selectedOption = "exit" + return true + default: + return false + } + } + ``` + + Add this modifier to your main `VStack`. Now users can: + - Press `1` to go to Profile + - Press `2` to go to Settings + - Press `3` to Save + - Press `e` to Exit + } + + @Step { + Update the status bar to show these shortcuts: + + ```swift + .statusBarItems { + StatusBarItem( + label: "Profile", + shortcut: Shortcut.digit("1"), + action: { } + ) + + StatusBarItem( + label: "Settings", + shortcut: Shortcut.digit("2"), + action: { } + ) + + StatusBarItem( + label: "Save", + shortcut: Shortcut.digit("3"), + action: { } + ) + + StatusBarItem( + label: "Exit", + shortcut: Shortcut.letter("e"), + action: { } + ) + } + ``` + } + + @Step { + Test your shortcuts: + + ```bash + swift run + ``` + + Press number keys to jump directly to menu items. + } + } +} + +## Next Steps + +You've created an interactive menu-driven application! + +- Learn about for advanced focus management +- Explore for more complex state patterns +- Try building a with theme switching +- Check out for custom styling diff --git a/TUIKit.docc/Tutorials/BuildThemableUI.md b/TUIKit.docc/Tutorials/BuildThemableUI.md new file mode 100644 index 0000000..32b8079 --- /dev/null +++ b/TUIKit.docc/Tutorials/BuildThemableUI.md @@ -0,0 +1,421 @@ +# Building a Themable UI + +Create an application with dynamic theme switching and persistence. + +@Intro(title: "Build a Themable App") { + Learn how to integrate TUIKit's theming system into your application, + allowing users to switch between themes and persist their preference. + + You'll work with the theme environment, theme manager, and storage to + create a professional themable application. +} + +## Overview + +In this tutorial, you'll create an app that features: + +- Theme environment integration +- Dynamic theme switching +- Persisted theme preference +- Visual theme selection UI + +@Section(title: "Set Up Theme Storage") { + @ContentAndMedia { + Use `@AppStorage` to remember the user's theme choice across sessions. + } + + @Steps { + @Step { + Create an app that stores the selected theme: + + ```swift + import TUIKit + + @main + struct ThemableApp: App { + @AppStorage("selectedTheme") var selectedThemeName: String = "green" + + var body: some Scene { + WindowGroup { + ContentView() + .environment(\.theme, themeForName(selectedThemeName)) + } + } + + func themeForName(_ name: String) -> Theme { + switch name { + case "amber": + return AmberPhosphorTheme() + case "white": + return WhitePhosphorTheme() + case "red": + return RedPhosphorTheme() + default: + return GreenPhosphorTheme() + } + } + } + + struct ContentView: View { + var body: some View { + VStack(spacing: 2) { + Text("Themable Application") + .bold() + .foregroundColor(.theme.accent) + + Spacer() + + Text("Select a theme to customize the appearance") + .foregroundColor(.theme.foregroundSecondary) + + Spacer() + } + .padding() + } + } + ``` + + `@AppStorage("selectedTheme")` automatically persists the theme choice. + When the app restarts, it loads the saved theme. + } + + @Step { + Run your app: + + ```bash + swift run + ``` + + The app launches with the default Green theme. + } + } +} + +@Section(title: "Create Theme Selection UI") { + @ContentAndMedia { + Display the 4 available themes and let users select one. + } + + @Steps { + @Step { + Add a theme menu: + + ```swift + @main + struct ThemableApp: App { + @AppStorage("selectedTheme") var selectedThemeName: String = "green" + + var body: some Scene { + WindowGroup { + ContentView(selectedThemeName: $selectedThemeName) + .environment(\.theme, themeForName(selectedThemeName)) + } + } + + func themeForName(_ name: String) -> Theme { + // ... same as before + } + } + + struct ContentView: View { + @Binding var selectedThemeName: String + + let themes = [ + ("green", "🟢 Green (Default)"), + ("amber", "🟡 Amber"), + ("white", "⚪ White"), + ("red", "🔴 Red") + ] + + var body: some View { + VStack(spacing: 2) { + Text("Themable Application") + .bold() + .foregroundColor(.theme.accent) + + Spacer() + + Text("Select a theme:") + .bold() + + Menu( + items: themes.map { id, label in + MenuItem(label, id: id) + }, + selection: $selectedThemeName + ) + + Spacer() + + Text("Current: \(themeName(selectedThemeName))") + .foregroundColor(.theme.foregroundSecondary) + + Spacer() + } + .padding() + } + + func themeName(_ id: String) -> String { + themes.first(where: { $0.0 == id })?.1 ?? id + } + } + ``` + + Pass `$selectedThemeName` as a binding to allow the menu to change it. + The environment automatically updates because it depends on this value. + } + + @Step { + Test theme switching: + + ```bash + swift run + ``` + + Use Tab/Arrow keys to select different themes. + Notice the colors change immediately! + } + } +} + +@Section(title: "Display Theme Colors") { + @ContentAndMedia { + Show a visual preview of the current theme's colors. + } + + @Steps { + @Step { + Add a color preview: + + ```swift + struct ContentView: View { + @Binding var selectedThemeName: String + @Environment(\.theme) var theme + + let themes = [ + ("green", "🟢 Green (Default)"), + ("amber", "🟡 Amber"), + ("white", "⚪ White"), + ("red", "🔴 Red") + ] + + var body: some View { + VStack(spacing: 2) { + Text("Themable Application") + .bold() + .foregroundColor(.theme.accent) + + Spacer() + + Text("Select a theme:").bold() + + Menu( + items: themes.map { id, label in + MenuItem(label, id: id) + }, + selection: $selectedThemeName + ) + + Spacer() + + // Color preview box + VStack(spacing: 1) { + Text("Theme Colors") + .bold() + + HStack(spacing: 1) { + Text(" ").background(theme.foreground) + Text(" ").background(theme.accent) + Text(" ").background(theme.border) + Text(" ").background(theme.warning) + } + + Text("Foreground | Accent | Border | Warning") + .foregroundColor(.theme.foregroundSecondary) + } + .padding(1) + .border(.rounded) + + Spacer() + } + .padding() + } + + func themeName(_ id: String) -> String { + themes.first(where: { $0.0 == id })?.1 ?? id + } + } + ``` + + `@Environment(\.theme)` gives access to the current theme. + Use `theme.foreground`, `theme.accent`, etc. to access colors. + } + + @Step { + Run the app: + + ```bash + swift run + ``` + + Switch themes and see the color preview update. + } + } +} + +@Section(title: "Add Theme Shortcuts and Status Bar") { + @ContentAndMedia { + Let users cycle themes quickly with keyboard shortcuts. + } + + @Steps { + @Step { + Add quick theme cycling: + + ```swift + struct ContentView: View { + @Binding var selectedThemeName: String + @Environment(\.theme) var theme + + let themes = [ + ("green", "🟢 Green (Default)"), + ("amber", "🟡 Amber"), + ("white", "⚪ White"), + ("red", "🔴 Red") + ] + + let themeIds = ["green", "amber", "white", "red"] + + var body: some View { + VStack(spacing: 2) { + // ... menu UI as before ... + + Spacer() + } + .padding() + .onKeyPress { event in + if event.key == .character("t") { + // Cycle to next theme + if let currentIndex = themeIds.firstIndex(of: selectedThemeName) { + let nextIndex = (currentIndex + 1) % themeIds.count + selectedThemeName = themeIds[nextIndex] + } + return true + } + return false + } + .statusBarItems { + StatusBarItem( + label: "Cycle Theme", + shortcut: Shortcut.letter("t"), + action: { } + ) + + StatusBarItem( + label: "Help", + shortcut: Shortcut.letter("?"), + action: { } + ) + + StatusBarItem( + label: "Quit", + shortcut: Shortcut.letter("q"), + action: { } + ) + } + } + + func themeName(_ id: String) -> String { + themes.first(where: { $0.0 == id })?.1 ?? id + } + } + ``` + + Now users can press `t` to quickly cycle through themes. + } + + @Step { + Test theme cycling: + + ```bash + swift run + ``` + + Press `t` repeatedly to cycle through all 4 themes. + The selection updates and the preview colors change. + } + } +} + +@Section(title: "Enhance with Appearance Switching") { + @ContentAndMedia { + Add appearance style switching alongside theme switching. + } + + @Steps { + @Step { + Add appearance cycling: + + ```swift + @main + struct ThemableApp: App { + @AppStorage("selectedTheme") var selectedThemeName: String = "green" + @AppStorage("selectedAppearance") var selectedAppearance: String = "rounded" + + var body: some Scene { + WindowGroup { + ContentView( + selectedThemeName: $selectedThemeName, + selectedAppearance: $selectedAppearance + ) + .environment(\.theme, themeForName(selectedThemeName)) + .environment(\.appearance, appearanceForName(selectedAppearance)) + } + } + + func themeForName(_ name: String) -> Theme { + // ... existing code ... + } + + func appearanceForName(_ name: String) -> Appearance { + switch name { + case "line": + return .line + case "doubled": + return .doubleLine + case "heavy": + return .heavy + case "block": + return .block + default: + return .rounded + } + } + } + ``` + + Now users can customize both theme AND appearance style! + } + + @Step { + The changes are automatically persisted with `@AppStorage`. + Run the app: + + ```bash + swift run + ``` + + Select a theme and appearance, exit with `q`, and run again. + Your selection is remembered! + } + } +} + +## Next Steps + +Congratulations! You've built a fully themed, customizable application. + +- Learn more about for creating custom themes +- Explore for structural styles +- See for advanced state patterns +- Check the overview for deeper understanding diff --git a/TUIKit.docc/Tutorials/BuildYourFirstApp.md b/TUIKit.docc/Tutorials/BuildYourFirstApp.md new file mode 100644 index 0000000..915cc09 --- /dev/null +++ b/TUIKit.docc/Tutorials/BuildYourFirstApp.md @@ -0,0 +1,270 @@ +# Building Your First App + +Create a simple counter application to learn TUIKit basics. + +@Intro(title: "Create Your First TUIKit App") { + Learn the fundamentals of building a terminal user interface with TUIKit + by creating a simple counter application. + + You'll learn how to set up an `@main` app, use `@State` for local state, + add interactive buttons, and use basic layout with `VStack` and `HStack`. +} + +## Overview + +In this tutorial, you'll build a working counter app with increment/decrement buttons. +This will teach you: + +- Creating an app with the `@main` attribute +- Using `VStack` and `HStack` for layout +- Handling button taps with `@State` +- Running and testing your app + +@Section(title: "Create the App Entry Point") { + @ContentAndMedia { + Every TUIKit app needs an entry point decorated with `@main`. + This tells Swift to use your app as the entry point for the program. + } + + @Steps { + @Step { + Create a new file called `main.swift` in your project: + + ```swift + import TUIKit + + @main + struct CounterApp: App { + var body: some Scene { + WindowGroup { + Text("Hello, TUIKit!") + } + } + } + ``` + + The `@main` attribute marks this as the app entry point. + `WindowGroup` represents the main window of your terminal app. + } + + @Step { + Run your app with: + + ```bash + swift run + ``` + + You should see "Hello, TUIKit!" displayed in the terminal. + } + } +} + +@Section(title: "Add Layout and Text") { + @ContentAndMedia { + Now let's organize content with `VStack` (vertical layout). + We'll add a title and multiple text lines. + } + + @Steps { + @Step { + Modify the `body` to use `VStack`: + + ```swift + @main + struct CounterApp: App { + var body: some Scene { + WindowGroup { + VStack(spacing: 1) { + Text("Counter App") + .bold() + + Text("Simple counter to learn TUIKit") + .foregroundColor(.theme.foregroundSecondary) + + Spacer() + + Text("Current count: 0") + + Spacer() + } + .padding() + } + } + } + ``` + + - `VStack(spacing: 1)` arranges content vertically with 1-unit spacing + - `.bold()` makes the title bold + - `.foregroundColor()` colors text + - `Spacer()` creates flexible vertical space + - `.padding()` adds space around all edges + } + + @Step { + Run the app again: + + ```bash + swift run + ``` + + Now you should see a nicely formatted display with title, + description, and spacing. + } + } +} + +@Section(title: "Add Interactive State") { + @ContentAndMedia { + Use `@State` to track the counter value and re-render when it changes. + This makes your app interactive! + } + + @Steps { + @Step { + Add a `@State` property for the counter: + + ```swift + @main + struct CounterApp: App { + @State private var count: Int = 0 + + var body: some Scene { + WindowGroup { + VStack(spacing: 1) { + Text("Counter App") + .bold() + + Text("Simple counter to learn TUIKit") + .foregroundColor(.theme.foregroundSecondary) + + Spacer() + + Text("Current count: \(count)") + + Spacer() + } + .padding() + } + } + } + ``` + + The `@State` property wrapper stores local state. When `count` changes, + the view automatically re-renders. + } + } +} + +@Section(title: "Add Buttons") { + @ContentAndMedia { + Now add buttons to increment and decrement the counter. + Use `HStack` to arrange them horizontally. + } + + @Steps { + @Step { + Add buttons using `HStack`: + + ```swift + @main + struct CounterApp: App { + @State private var count: Int = 0 + + var body: some Scene { + WindowGroup { + VStack(spacing: 1) { + Text("Counter App") + .bold() + + Text("Simple counter to learn TUIKit") + .foregroundColor(.theme.foregroundSecondary) + + Spacer() + + Text("Current count: \(count)") + + HStack(spacing: 2) { + Button("Decrement") { count -= 1 } + Button("Increment") { count += 1 } + } + + Spacer() + + Text("Press 'q' to quit") + .foregroundColor(.theme.foregroundTertiary) + } + .padding() + } + } + } + ``` + + - `HStack(spacing: 2)` arranges buttons horizontally + - `Button(label) { action }` creates a clickable button + - The closure after the label runs when the button is pressed + } + + @Step { + Run your app: + + ```bash + swift run + ``` + + Now use Tab to navigate to the buttons and press Enter to increment/decrement. + The counter value updates in real-time! + } + } +} + +@Section(title: "Test and Iterate") { + @ContentAndMedia { + Test your counter app and try making improvements. + } + + @Steps { + @Step { + Test the following interactions: + + - Press `Tab` to move focus to the next button + - Press `Shift+Tab` to move focus backward + - Press `Enter` when a button is focused to activate it + - Watch the counter update + - Press `q` to quit the app + - Press `t` to cycle through themes + - Press `a` to cycle through appearance styles + } + + @Step { + Try these enhancements: + + Add a reset button: + + ```swift + HStack(spacing: 2) { + Button("Decrement") { count = max(0, count - 1) } + Button("Reset") { count = 0 } + Button("Increment") { count += 1 } + } + ``` + + Or add a border around the counter: + + ```swift + Text("Current count: \(count)") + .bold() + .padding(1) + .border(.rounded) + ``` + } + } +} + +## Next Steps + +Congratulations! You've built your first TUIKit app. + +- Learn more about for complex state scenarios +- Explore to customize colors +- Try building an interactive +- Check out for more styling options