Merge pull request #28 from phranck/refactor/eliminate-environmentstorage-singleton

Refactor: Eliminate EnvironmentStorage singleton (last one)
This commit is contained in:
phranck
2026-01-30 23:28:06 +01:00
11 changed files with 44 additions and 35 deletions
+4 -4
View File
@@ -82,7 +82,7 @@ internal final class AppRunner<A: App> {
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<A: App> {
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<A: App> {
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<A: App> {
terminal.showCursor()
terminal.exitAlternateScreen()
AppState.active.clearObservers()
EnvironmentStorage.shared.reset()
EnvironmentStorage.active.reset()
focusManager.clear()
tuiContext.reset()
}
+1 -1
View File
@@ -93,7 +93,7 @@ internal struct RenderLoop<A: App> {
)
// Update global environment storage
EnvironmentStorage.shared.environment = environment
EnvironmentStorage.active.environment = environment
// Render main content (background fill happens in renderScene)
let scene = app.body
+1 -1
View File
@@ -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
}
}
)
+14 -5
View File
@@ -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<Value>: @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)
}
}
+2 -2
View File
@@ -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)
}
}
+3 -3
View File
@@ -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
}
}
)
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+7 -7
View File
@@ -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)
+4 -4
View File
@@ -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()
}
}
+4 -4
View File
@@ -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