From 6ed66026dd7308b0209b85e5b8c449818d99f60d Mon Sep 17 00:00:00 2001 From: phranck Date: Fri, 30 Jan 2026 23:11:47 +0100 Subject: [PATCH] Refactor: Eliminate EnvironmentStorage singleton - Replace EnvironmentStorage.shared with EnvironmentStorage.active (settable static property) - Make EnvironmentStorage.init() public instead of private - Update all 12 source references across Environment.swift, App.swift, RenderLoop.swift, Focus.swift, Theme.swift, Appearance.swift, ThemeManager.swift - Update all 17 test references across EnvironmentTests, FocusTests, StatusBarTests, ButtonTests This is the last singleton in the TUIkit framework. --- Sources/TUIkit/App/App.swift | 8 ++++---- Sources/TUIkit/App/RenderLoop.swift | 2 +- Sources/TUIkit/Core/Appearance.swift | 2 +- Sources/TUIkit/Core/Environment.swift | 19 ++++++++++++++----- Sources/TUIkit/Core/Focus.swift | 4 ++-- Sources/TUIkit/Core/Theme.swift | 6 +++--- Sources/TUIkit/Core/ThemeManager.swift | 4 ++-- Tests/TUIkitTests/ButtonTests.swift | 4 ++-- Tests/TUIkitTests/EnvironmentTests.swift | 14 +++++++------- Tests/TUIkitTests/FocusTests.swift | 8 ++++---- Tests/TUIkitTests/StatusBarTests.swift | 8 ++++---- 11 files changed, 44 insertions(+), 35 deletions(-) diff --git a/Sources/TUIkit/App/App.swift b/Sources/TUIkit/App/App.swift index e11dc52..5fac822 100644 --- a/Sources/TUIkit/App/App.swift +++ b/Sources/TUIkit/App/App.swift @@ -82,7 +82,7 @@ internal final class AppRunner { items: PaletteRegistry.all, applyToEnvironment: { item in if let palette = item as? any Palette { - EnvironmentStorage.shared.environment.palette = palette + EnvironmentStorage.active.environment.palette = palette } } ) @@ -90,7 +90,7 @@ internal final class AppRunner { items: AppearanceRegistry.all, applyToEnvironment: { item in if let appearance = item as? Appearance { - EnvironmentStorage.shared.environment.appearance = appearance + EnvironmentStorage.active.environment.appearance = appearance } } ) @@ -128,7 +128,7 @@ internal final class AppRunner { terminal.enableRawMode() // Set up environment with all managed subsystems - EnvironmentStorage.shared.environment = renderer.buildEnvironment() + EnvironmentStorage.active.environment = renderer.buildEnvironment() // Register for state changes AppState.active.observe { [signals] in @@ -169,7 +169,7 @@ internal final class AppRunner { terminal.showCursor() terminal.exitAlternateScreen() AppState.active.clearObservers() - EnvironmentStorage.shared.reset() + EnvironmentStorage.active.reset() focusManager.clear() tuiContext.reset() } diff --git a/Sources/TUIkit/App/RenderLoop.swift b/Sources/TUIkit/App/RenderLoop.swift index 198f042..eb20082 100644 --- a/Sources/TUIkit/App/RenderLoop.swift +++ b/Sources/TUIkit/App/RenderLoop.swift @@ -93,7 +93,7 @@ internal struct RenderLoop { ) // Update global environment storage - EnvironmentStorage.shared.environment = environment + EnvironmentStorage.active.environment = environment // Render main content (background fill happens in renderScene) let scene = app.body diff --git a/Sources/TUIkit/Core/Appearance.swift b/Sources/TUIkit/Core/Appearance.swift index dd3d2f5..8e9e1c8 100644 --- a/Sources/TUIkit/Core/Appearance.swift +++ b/Sources/TUIkit/Core/Appearance.swift @@ -221,7 +221,7 @@ private struct AppearanceManagerKey: EnvironmentKey { items: AppearanceRegistry.all, applyToEnvironment: { item in if let appearance = item as? Appearance { - EnvironmentStorage.shared.environment.appearance = appearance + EnvironmentStorage.active.environment.appearance = appearance } } ) diff --git a/Sources/TUIkit/Core/Environment.swift b/Sources/TUIkit/Core/Environment.swift index 08003ab..b6e0191 100644 --- a/Sources/TUIkit/Core/Environment.swift +++ b/Sources/TUIkit/Core/Environment.swift @@ -85,9 +85,17 @@ public struct EnvironmentValues: @unchecked Sendable { /// /// This allows views to access environment values without explicit passing. /// The environment is set by the rendering system before rendering each view. +/// +/// `AppRunner` creates and manages the active instance. Property wrappers +/// like ``Environment`` and view modifiers like ``EnvironmentModifier`` +/// access it through ``active``. public final class EnvironmentStorage: @unchecked Sendable { - /// The shared environment storage. - public static let shared = EnvironmentStorage() + /// The active environment storage for the current application. + /// + /// Set by `AppRunner` during initialization. The ``Environment`` + /// property wrapper, ``FocusState``, and ``EnvironmentModifier`` + /// all read and write through this property. + public nonisolated(unsafe) static var active = EnvironmentStorage() /// Lock protecting all mutable state. private let lock = NSLock() @@ -98,7 +106,8 @@ public final class EnvironmentStorage: @unchecked Sendable { /// Stack of environments for nested rendering. private var stack: [EnvironmentValues] = [] - private init() {} + /// Creates a new environment storage instance. + public init() {} /// The current environment values. public var environment: EnvironmentValues { @@ -189,7 +198,7 @@ public struct Environment: @unchecked Sendable { /// The current value from the environment. public var wrappedValue: Value { - EnvironmentStorage.shared.environment[keyPath: keyPath] + EnvironmentStorage.active.environment[keyPath: keyPath] } } @@ -225,7 +234,7 @@ extension EnvironmentModifier: Renderable { let modifiedContext = context.withEnvironment(modifiedEnvironment) // Render content with modified environment - return EnvironmentStorage.shared.withEnvironment(modifiedEnvironment) { + return EnvironmentStorage.active.withEnvironment(modifiedEnvironment) { TUIkit.renderToBuffer(content, context: modifiedContext) } } diff --git a/Sources/TUIkit/Core/Focus.swift b/Sources/TUIkit/Core/Focus.swift index aa5eb3f..4868ea7 100644 --- a/Sources/TUIkit/Core/Focus.swift +++ b/Sources/TUIkit/Core/Focus.swift @@ -287,13 +287,13 @@ public class FocusState { /// /// Reads from the current environment's focus manager. public var isFocused: Bool { - EnvironmentStorage.shared.environment.focusManager.isFocused(id: id) + EnvironmentStorage.active.environment.focusManager.isFocused(id: id) } /// Requests focus for this element. /// /// Uses the current environment's focus manager. public func requestFocus() { - EnvironmentStorage.shared.environment.focusManager.focus(id: id) + EnvironmentStorage.active.environment.focusManager.focus(id: id) } } diff --git a/Sources/TUIkit/Core/Theme.swift b/Sources/TUIkit/Core/Theme.swift index c1d8b41..17c3161 100644 --- a/Sources/TUIkit/Core/Theme.swift +++ b/Sources/TUIkit/Core/Theme.swift @@ -178,7 +178,7 @@ extension EnvironmentValues { extension Color { /// Access palette colors from the current environment. /// - /// These colors read from `EnvironmentStorage.shared` during rendering. + /// These colors read from `EnvironmentStorage.active` during rendering. /// /// # Example /// @@ -204,7 +204,7 @@ extension Color { public enum PaletteColors { /// Gets the current palette from environment storage. private static var current: any Palette { - EnvironmentStorage.shared.environment.palette + EnvironmentStorage.active.environment.palette } /// Primary background color. @@ -735,7 +735,7 @@ private struct PaletteManagerKey: EnvironmentKey { items: PaletteRegistry.all, applyToEnvironment: { item in if let palette = item as? any Palette { - EnvironmentStorage.shared.environment.palette = palette + EnvironmentStorage.active.environment.palette = palette } } ) diff --git a/Sources/TUIkit/Core/ThemeManager.swift b/Sources/TUIkit/Core/ThemeManager.swift index 0acae5a..c61ab50 100644 --- a/Sources/TUIkit/Core/ThemeManager.swift +++ b/Sources/TUIkit/Core/ThemeManager.swift @@ -61,7 +61,7 @@ public protocol Cyclable: Sendable { /// # Environment Integration /// /// On every change the manager writes the current item into -/// `EnvironmentStorage.shared` via the closure provided at init, +/// `EnvironmentStorage.active` via the closure provided at init, /// then triggers a re-render through `AppState.active.setNeedsRender()`. public final class ThemeManager: @unchecked Sendable { /// The current item index. @@ -78,7 +78,7 @@ public final class ThemeManager: @unchecked Sendable { /// - Parameters: /// - items: The items to cycle through. Must not be empty. /// - applyToEnvironment: A closure that writes the current item - /// into `EnvironmentStorage.shared.environment`. + /// into `EnvironmentStorage.active.environment`. public init(items: [any Cyclable], applyToEnvironment: @escaping @Sendable (any Cyclable) -> Void) { precondition(!items.isEmpty, "ThemeManager requires at least one item") self.items = items diff --git a/Tests/TUIkitTests/ButtonTests.swift b/Tests/TUIkitTests/ButtonTests.swift index 94bfccc..c97c4b4 100644 --- a/Tests/TUIkitTests/ButtonTests.swift +++ b/Tests/TUIkitTests/ButtonTests.swift @@ -16,7 +16,7 @@ private func createTestContext(width: Int = 80, height: Int = 24) -> RenderConte let focusManager = FocusManager() var environment = EnvironmentValues() environment.focusManager = focusManager - EnvironmentStorage.shared.environment = environment + EnvironmentStorage.active.environment = environment return RenderContext( availableWidth: width, @@ -27,7 +27,7 @@ private func createTestContext(width: Int = 80, height: Int = 24) -> RenderConte /// Cleans up the environment after a test. private func cleanupEnvironment() { - EnvironmentStorage.shared.reset() + EnvironmentStorage.active.reset() } // MARK: - Button Style Tests diff --git a/Tests/TUIkitTests/EnvironmentTests.swift b/Tests/TUIkitTests/EnvironmentTests.swift index b2e5132..86c3b6d 100644 --- a/Tests/TUIkitTests/EnvironmentTests.swift +++ b/Tests/TUIkitTests/EnvironmentTests.swift @@ -85,7 +85,7 @@ struct EnvironmentStorageTests { @Test("push and pop restore previous environment") func pushPop() { - let storage = EnvironmentStorage.shared + let storage = EnvironmentStorage.active storage.reset() let original = storage.environment @@ -100,7 +100,7 @@ struct EnvironmentStorageTests { @Test("Nested push/pop restores correctly") func nestedPushPop() { - let storage = EnvironmentStorage.shared + let storage = EnvironmentStorage.active storage.reset() var first = EnvironmentValues() @@ -120,7 +120,7 @@ struct EnvironmentStorageTests { @Test("Pop on empty stack is safe") func popEmptyStack() { - let storage = EnvironmentStorage.shared + let storage = EnvironmentStorage.active storage.reset() // Should not crash storage.pop() @@ -128,7 +128,7 @@ struct EnvironmentStorageTests { @Test("withEnvironment scopes environment correctly") func withEnvironment() { - let storage = EnvironmentStorage.shared + let storage = EnvironmentStorage.active storage.reset() var scoped = EnvironmentValues() @@ -143,7 +143,7 @@ struct EnvironmentStorageTests { @Test("reset clears all state") func reset() { - let storage = EnvironmentStorage.shared + let storage = EnvironmentStorage.active var modified = EnvironmentValues() modified[TestStringKey.self] = "dirty" storage.push(modified) @@ -160,7 +160,7 @@ struct EnvironmentPropertyWrapperTests { @Test("@Environment reads current value from shared storage") func readsFromStorage() { - let storage = EnvironmentStorage.shared + let storage = EnvironmentStorage.active storage.reset() var env = storage.environment @@ -176,7 +176,7 @@ struct EnvironmentPropertyWrapperTests { @Test("@Environment reflects changes after storage update") func reflectsChanges() { - let storage = EnvironmentStorage.shared + let storage = EnvironmentStorage.active storage.reset() let wrapper = Environment(\.testString) diff --git a/Tests/TUIkitTests/FocusTests.swift b/Tests/TUIkitTests/FocusTests.swift index 0c875c7..f981a2d 100644 --- a/Tests/TUIkitTests/FocusTests.swift +++ b/Tests/TUIkitTests/FocusTests.swift @@ -345,7 +345,7 @@ struct FocusStateTests { let manager = FocusManager() var environment = EnvironmentValues() environment.focusManager = manager - EnvironmentStorage.shared.environment = environment + EnvironmentStorage.active.environment = environment let state = FocusState(id: "state-test") let element = MockFocusable(id: "state-test") @@ -356,7 +356,7 @@ struct FocusStateTests { #expect(state.isFocused) // Cleanup - EnvironmentStorage.shared.reset() + EnvironmentStorage.active.reset() } @Test("FocusState requestFocus works") @@ -365,7 +365,7 @@ struct FocusStateTests { let manager = FocusManager() var environment = EnvironmentValues() environment.focusManager = manager - EnvironmentStorage.shared.environment = environment + EnvironmentStorage.active.environment = environment let element1 = MockFocusable(id: "req-1") let element2 = MockFocusable(id: "req-2") @@ -379,7 +379,7 @@ struct FocusStateTests { #expect(manager.isFocused(id: "req-2")) // Cleanup - EnvironmentStorage.shared.reset() + EnvironmentStorage.active.reset() } } diff --git a/Tests/TUIkitTests/StatusBarTests.swift b/Tests/TUIkitTests/StatusBarTests.swift index 1d85fbc..9d29841 100644 --- a/Tests/TUIkitTests/StatusBarTests.swift +++ b/Tests/TUIkitTests/StatusBarTests.swift @@ -923,7 +923,7 @@ struct StatusBarItemsModifierTests { environment: environment ) - EnvironmentStorage.shared.environment = environment + EnvironmentStorage.active.environment = environment _ = renderToBuffer(view, context: context) // Check that user items were set @@ -957,7 +957,7 @@ struct StatusBarItemsModifierTests { environment: environment ) - EnvironmentStorage.shared.environment = environment + EnvironmentStorage.active.environment = environment _ = renderToBuffer(view, context: context) // Context items should be active @@ -989,7 +989,7 @@ struct StatusBarItemsModifierTests { environment: environment ) - EnvironmentStorage.shared.environment = environment + EnvironmentStorage.active.environment = environment let buffer = renderToBuffer(view, context: context) // Content should be rendered @@ -1036,7 +1036,7 @@ struct StatusBarItemsModifierTests { environment: environment ) - EnvironmentStorage.shared.environment = environment + EnvironmentStorage.active.environment = environment _ = renderToBuffer(outerView, context: context) // Inner context should be on top