greatly simplify environment variables handling
This commit is contained in:
@@ -22,8 +22,7 @@ extension PHBar {
|
||||
|
||||
mutating func run() throws {
|
||||
let config = try PHConfig.load()
|
||||
let environment = PHEnvironment.process.merging(config.env)
|
||||
let theme = try PHTheme.load(config.theme, environment: environment)
|
||||
let theme = try PHTheme.load(config.theme)
|
||||
let layouts = try PHLayout.load(["default", "mini"])
|
||||
|
||||
// NOTE: Make sure NSApp.run() runs in the main thread
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
extension String {
|
||||
/// Expand `$VAR` and `${VAR}` references against `variables`.
|
||||
///
|
||||
/// Unresolved references are left intact rather than blanked, so a typo
|
||||
/// stays visible (e.g. a literal `"$TYPO"` in a color) instead of silently
|
||||
/// vanishing.
|
||||
func expanded(from environment: [String: String]) -> String {
|
||||
var result = self
|
||||
|
||||
// `${VAR}` first, so the `$VAR` pass never sees its inner name.
|
||||
result = result.replacing(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/) { match in
|
||||
environment[String(match.1)] ?? String(match.0)
|
||||
}
|
||||
// `$VAR`
|
||||
result = result.replacing(/\$([A-Za-z_][A-Za-z0-9_]*)/) { match in
|
||||
environment[String(match.1)] ?? String(match.0)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,10 @@ final class PHBarDelegate: NSObject, NSApplicationDelegate {
|
||||
private var screenObserver: NSObjectProtocol?
|
||||
|
||||
init(config: PHConfig, theme: PHTheme, layouts: [String : PHLayout], debug: Bool) {
|
||||
let environment = PHEnvironment.process.merging(config.env)
|
||||
self.factory = PHBarFactory(
|
||||
config: config,
|
||||
theme: theme,
|
||||
layouts: layouts,
|
||||
environment: environment,
|
||||
debug: debug
|
||||
)
|
||||
super.init()
|
||||
|
||||
@@ -7,7 +7,6 @@ struct PHBarFactory {
|
||||
let config: PHConfig
|
||||
let theme: PHTheme
|
||||
let layouts: [String: PHLayout]
|
||||
let environment: PHEnvironment
|
||||
let debug: Bool
|
||||
|
||||
/// Resolve layout for `screen` and build its controller.
|
||||
@@ -26,7 +25,6 @@ struct PHBarFactory {
|
||||
screen: screen,
|
||||
theme: theme,
|
||||
layout: layout,
|
||||
environment: environment,
|
||||
debug: debug
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,10 +25,6 @@ final class PHBlock: ObservableObject, Decodable, Identifiable {
|
||||
/// `nil` when omitted from config.
|
||||
let events: [PHEvent]?
|
||||
|
||||
/// Variables made visible to block scripts. Defaults to phbar's process
|
||||
/// environment; `BarController` overlays the `[env]` section on top.
|
||||
var environment: PHEnvironment = .process
|
||||
|
||||
/// Event source registry used to subscribe to system events. Defaults to the
|
||||
/// shared instance; inject a custom one for testing.
|
||||
var registry: PHEventRegistry = .shared
|
||||
|
||||
@@ -2,11 +2,10 @@ struct PHConfig: Decodable {
|
||||
let theme: String
|
||||
let window: String
|
||||
let layout: String
|
||||
let env: [String: String]?
|
||||
let monitors: [String: PHConfigMonitorOverride]?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case theme, window, layout, env
|
||||
case theme, window, layout
|
||||
case monitors = "monitor"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension PHConfig {
|
||||
/// Return a copy with each `[env]` value expanded against `environment`
|
||||
/// (defaulting to the process environment).
|
||||
///
|
||||
/// Unresolved references are left intact (see `PHEnvironment.expand`).
|
||||
func resolvingEnv(in environment: PHEnvironment = .process) -> PHConfig {
|
||||
guard let env, !env.isEmpty else { return self }
|
||||
return PHConfig(
|
||||
theme: theme,
|
||||
window: window,
|
||||
layout: layout,
|
||||
env: env.mapValues(environment.expand),
|
||||
monitors: monitors
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,20 @@ import Foundation
|
||||
import TOML
|
||||
|
||||
extension PHConfig {
|
||||
/// Load configuration from the resolved config directory (see `PHPaths`).
|
||||
/// Load the configuration from the resolved config directory (see `PHPaths`).
|
||||
/// If the file doesn't exist or fails to parse, the execution is interrupted.
|
||||
static func load() throws -> PHConfig {
|
||||
let url = PHPaths.configDirectory.appending(path: "config.toml")
|
||||
let url = PHPaths.configFile
|
||||
|
||||
if FileManager.default.fileExists(atPath: url.relativePath) {
|
||||
return try load(from: url)
|
||||
} else {
|
||||
let config = url.lastPathComponent
|
||||
throw PHBar.Error(
|
||||
"""
|
||||
Configuration file not found.
|
||||
Make sure to have a file named `config.toml` inside the
|
||||
directory: `\(url.deletingLastPathComponent().relativePath)`
|
||||
Make sure to have a file named `\(config)` inside the
|
||||
directory: `\(PHPaths.configDirectory.relativePath)`
|
||||
|
||||
Tip: If you've never used phbar before, run the `phbar install` command
|
||||
to automatically generate the required configuration files.
|
||||
@@ -23,31 +24,19 @@ extension PHConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode configuration from a file at URL.
|
||||
/// Load the configuration from the file at URL.
|
||||
/// If the file doesn't exist or fails to parse, the execution is interrupted.
|
||||
static private func load(from url: URL) throws -> PHConfig {
|
||||
guard let data = try? Data(contentsOf: url),
|
||||
let contents = String(data: data, encoding: .utf8), !contents.isEmpty
|
||||
else {
|
||||
throw PHBar.Error("Configuration file not readable or empty.")
|
||||
}
|
||||
|
||||
return try load(from: contents)
|
||||
}
|
||||
|
||||
/// Decode configuration from a TOML string.
|
||||
/// If the content fails to parse, the execution is interrupted.
|
||||
static private func load(from contents: String) throws -> PHConfig {
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
guard let contents = String(data: data, encoding: .utf8) else {
|
||||
throw PHBar.Error("content is not UTF8 encoded.")
|
||||
}
|
||||
let decoder = TOMLDecoder()
|
||||
let config = try decoder.decode(PHConfig.self, from: contents)
|
||||
// Expand `$VAR` references in `[env]` values against the process
|
||||
// environment (e.g. `PATH = "$PATH:/opt/homebrew/bin"`). Themes are
|
||||
// decoded against the merged result later, so their `$VAR` references
|
||||
// resolve the same set.
|
||||
return config.resolvingEnv()
|
||||
let configFile = contents.expanded(from: ProcessInfo.processInfo.environment)
|
||||
return try decoder.decode(PHConfig.self, from: configFile)
|
||||
} catch {
|
||||
throw PHBar.Error("Configuration file not valid", underlyingError: error)
|
||||
throw PHBar.Error("Configuration file not readable", underlyingError: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// A resolved set of environment variables with `$VAR` expansion support.
|
||||
///
|
||||
/// Two concerns meet here:
|
||||
///
|
||||
/// 1. **Scripts** — block commands run with these variables in their
|
||||
/// environment, built by layering the `[env]` section on top of phbar's own
|
||||
/// process environment (see `PHBlock.compute`).
|
||||
/// 2. **Config templating** — `@EnvExpanded` fields (e.g. theme colors, fonts)
|
||||
/// expand `$VAR` references against a `PHEnvironment` injected through
|
||||
/// `TOMLDecoder.userInfo` at decode time.
|
||||
///
|
||||
/// The environment is therefore resolved once (process → merged with `[env]`)
|
||||
/// and reused for both, so a variable defined in `[env]` is a single source of
|
||||
/// truth visible to scripts and themes alike.
|
||||
struct PHEnvironment: Sendable {
|
||||
let variables: [String: String]
|
||||
|
||||
/// phbar's own process environment.
|
||||
static let process = PHEnvironment(variables: ProcessInfo.processInfo.environment)
|
||||
|
||||
/// `CodingUserInfoKey` used to thread a `PHEnvironment` into `Decodable`
|
||||
/// types (and the `@EnvExpanded` wrapper) without global state.
|
||||
static let userInfoKey = CodingUserInfoKey(rawValue: "phbar.environment")!
|
||||
|
||||
/// Layer `overrides` (typically the `[env]` section) on top of this set.
|
||||
/// Existing values with the same name are replaced.
|
||||
func merging(_ overrides: [String: String]?) -> PHEnvironment {
|
||||
guard let overrides, !overrides.isEmpty else { return self }
|
||||
return PHEnvironment(variables: variables.merging(overrides) { _, override in override })
|
||||
}
|
||||
|
||||
/// Expand `$VAR` and `${VAR}` references against `variables`.
|
||||
///
|
||||
/// Unresolved references are left intact rather than blanked, so a typo
|
||||
/// stays visible (e.g. a literal `"$TYPO"` in a color) instead of silently
|
||||
/// vanishing.
|
||||
func expand(_ value: String) -> String {
|
||||
var result = value
|
||||
|
||||
// `${VAR}` first, so the `$VAR` pass never sees its inner name.
|
||||
result = result.replacing(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/) { match in
|
||||
variables[String(match.1)] ?? String(match.0)
|
||||
}
|
||||
// `$VAR`
|
||||
result = result.replacing(/\$([A-Za-z_][A-Za-z0-9_]*)/) { match in
|
||||
variables[String(match.1)] ?? String(match.0)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/// Opt-in property wrapper that expands `$VAR` / `${VAR}` references in a
|
||||
/// decoded string against the `PHEnvironment` carried in `decoder.userInfo`
|
||||
/// (falling back to the process environment when none is injected).
|
||||
///
|
||||
/// Apply only where config templating is wanted — never on `command`, whose
|
||||
/// `$`-references belong to the shell at run time, not to phbar at load time.
|
||||
@propertyWrapper
|
||||
struct EnvExpanded: Decodable, Equatable {
|
||||
private let value: String
|
||||
|
||||
var wrappedValue: String { value }
|
||||
|
||||
init(wrappedValue: String) {
|
||||
self.value = wrappedValue
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let raw = try decoder.singleValueContainer().decode(String.self)
|
||||
let environment = (decoder.userInfo[PHEnvironment.userInfoKey] as? PHEnvironment) ?? .process
|
||||
self.value = environment.expand(raw)
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,8 @@ extension PHLayout {
|
||||
throw PHBar.Error("content is not UTF8 encoded.")
|
||||
}
|
||||
let decoder = TOMLDecoder()
|
||||
return try decoder.decode(PHLayout.self, from: contents)
|
||||
let layoutFile = contents.expanded(from: ProcessInfo.processInfo.environment)
|
||||
return try decoder.decode(PHLayout.self, from: layoutFile)
|
||||
} catch {
|
||||
let layout = url.lastPathComponent
|
||||
throw PHBar.Error("Layout file `\(layout)` not readable", underlyingError: error)
|
||||
|
||||
@@ -16,9 +16,19 @@ enum PHPaths {
|
||||
/// The resolved configuration directory (computed once, cached).
|
||||
static let configDirectory: URL = resolveConfigDirectory()
|
||||
|
||||
/// The resolved config file (computed once, cached).
|
||||
static let configFile: URL = {
|
||||
configDirectory.appending(path: "config.toml")
|
||||
}()
|
||||
|
||||
/// The resolved themes directory (computed once, cached).
|
||||
static let themesDirectory: URL = {
|
||||
configDirectory.appending(path: "themes")
|
||||
}()
|
||||
|
||||
/// The resolved layouts directory (computed once, cached).
|
||||
static let layoutsDirectory: URL = {
|
||||
resolveConfigDirectory().appending(path: "layouts")
|
||||
configDirectory.appending(path: "layouts")
|
||||
}()
|
||||
|
||||
/// Ordered candidate directories, priority high → low.
|
||||
|
||||
@@ -2,22 +2,20 @@ import Foundation
|
||||
import TOML
|
||||
|
||||
extension PHTheme {
|
||||
/// Load theme from the resolved config directory (`PHPaths`/themes).
|
||||
/// Load a theme from the resolved config directory (see `PHPaths`).
|
||||
/// If the file doesn't exist or fails to parse, the execution is interrupted.
|
||||
///
|
||||
/// - Parameter environment: Used to expand `$VAR` references in `@EnvExpanded`
|
||||
/// fields (colors, fonts); defaults to the process environment.
|
||||
static func load(_ theme: String, environment: PHEnvironment = .process) throws -> PHTheme {
|
||||
let url = PHPaths.configDirectory.appending(path: "themes").appending(path: "\(theme).toml")
|
||||
static func load(_ theme: String) throws -> PHTheme {
|
||||
let url = PHPaths.themesDirectory.appending(path: "\(theme).toml")
|
||||
|
||||
if FileManager.default.fileExists(atPath: url.relativePath) {
|
||||
return try load(from: url, environment: environment)
|
||||
return try load(from: url)
|
||||
} else {
|
||||
let theme = url.lastPathComponent
|
||||
throw PHBar.Error(
|
||||
"""
|
||||
Theme file not found.
|
||||
Make sure to have a file named `\(theme).toml` inside the
|
||||
directory: `\(url.deletingLastPathComponent().relativePath)`
|
||||
Make sure to have a file named `\(theme)` inside the
|
||||
directory: `\(PHPaths.themesDirectory.relativePath)`
|
||||
|
||||
Tip: If you've never used phbar before, run the `phbar install` command
|
||||
to automatically generate the required configuration files.
|
||||
@@ -26,27 +24,20 @@ extension PHTheme {
|
||||
}
|
||||
}
|
||||
|
||||
/// Load theme from a file at URL.
|
||||
/// Load a theme from the file at URL.
|
||||
/// If the file doesn't exist or fails to parse, the execution is interrupted.
|
||||
static private func load(from url: URL, environment: PHEnvironment) throws -> PHTheme {
|
||||
guard let data = try? Data(contentsOf: url),
|
||||
let contents = String(data: data, encoding: .utf8), !contents.isEmpty
|
||||
else {
|
||||
throw PHBar.Error("Theme file not readable or empty.")
|
||||
}
|
||||
|
||||
return try load(from: contents, environment: environment)
|
||||
}
|
||||
|
||||
/// Load theme from a TOML string.
|
||||
/// If the content fails to parse, the execution is interrupted.
|
||||
static private func load(from contents: String, environment: PHEnvironment) throws -> PHTheme {
|
||||
static private func load(from url: URL) throws -> PHTheme {
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
guard let contents = String(data: data, encoding: .utf8) else {
|
||||
throw PHBar.Error("content is not UTF8 encoded.")
|
||||
}
|
||||
let decoder = TOMLDecoder()
|
||||
decoder.userInfo[PHEnvironment.userInfoKey] = environment
|
||||
return try decoder.decode(PHTheme.self, from: contents)
|
||||
let themeFile = contents.expanded(from: ProcessInfo.processInfo.environment)
|
||||
return try decoder.decode(PHTheme.self, from: themeFile)
|
||||
} catch {
|
||||
throw PHBar.Error("Theme file not valid", underlyingError: error)
|
||||
let theme = url.lastPathComponent
|
||||
throw PHBar.Error("Theme file `\(theme)` not readable", underlyingError: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import SwiftUI
|
||||
struct PHThemeStyleColor: Decodable, ShapeStyle {
|
||||
typealias Resolved = SwiftUI.Color
|
||||
|
||||
@EnvExpanded var color: String
|
||||
var color: String
|
||||
let alpha: Double?
|
||||
|
||||
var uiColor: SwiftUI.Color {
|
||||
|
||||
@@ -3,7 +3,7 @@ import SwiftUI
|
||||
|
||||
struct PHThemeText: Decodable {
|
||||
let name: String
|
||||
@EnvExpanded var fontFamily: String
|
||||
var fontFamily: String
|
||||
let size: Double
|
||||
let weight: PHThemeTextWeight
|
||||
let style: PHThemeTextStyle
|
||||
|
||||
@@ -16,7 +16,7 @@ final class BarController: ObservableObject {
|
||||
blocks.filter { $0.centered == true }
|
||||
}
|
||||
|
||||
init(config: PHConfig, screen: NSScreen, theme: PHTheme, layout: PHLayout, environment: PHEnvironment = .process, debug: Bool) {
|
||||
init(config: PHConfig, screen: NSScreen, theme: PHTheme, layout: PHLayout, debug: Bool) {
|
||||
self.config = config
|
||||
self.screen = screen
|
||||
self.theme = theme
|
||||
@@ -26,7 +26,6 @@ final class BarController: ObservableObject {
|
||||
for block in blocks {
|
||||
block.text = text(for: block)
|
||||
block.style = style(for: block)
|
||||
block.environment = environment
|
||||
block.debug = debug
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,16 +19,3 @@ layout = "default"
|
||||
# [monitor."DELL U2723QE"]
|
||||
# window = "clock"
|
||||
# layout = "external"
|
||||
|
||||
# Optional: environment variables passed to block scripts when they run.
|
||||
# - scripts always inherit phbar's own process environment (e.g. $PATH)
|
||||
# - entries listed here take precedence, overriding any inherited value
|
||||
# that shares the same name
|
||||
# - `$VAR` / `${VAR}` references in a value expand against the inherited
|
||||
# environment at load time, so you can extend variables (e.g.
|
||||
# `PATH = "$PATH:/opt/homebrew/bin"`) or pull in colors/fonts used by a
|
||||
# theme (e.g. `ACCENT = "#ff0000"`, then `color = "$ACCENT"` in the theme)
|
||||
#
|
||||
# [env]
|
||||
# PATH = "$PATH:/opt/homebrew/bin"
|
||||
# MY_CUSTOM_VAR = "hello world"
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
import TOML
|
||||
|
||||
@testable import phbar
|
||||
|
||||
// MARK: - expand
|
||||
|
||||
@Test func envExpandDollarBraceForm() {
|
||||
let env = PHEnvironment(variables: ["ACCENT": "#ff0000"])
|
||||
#expect(env.expand("${ACCENT}") == "#ff0000")
|
||||
}
|
||||
|
||||
@Test func envExpandDollarForm() {
|
||||
let env = PHEnvironment(variables: ["FOO": "bar"])
|
||||
#expect(env.expand("$FOO") == "bar")
|
||||
}
|
||||
|
||||
@Test func envExpandsEmbeddedReferences() {
|
||||
let env = PHEnvironment(variables: ["HOME": "/Users/tom", "BIN": "/opt/bin"])
|
||||
#expect(env.expand("$HOME/bin:${BIN}/extra") == "/Users/tom/bin:/opt/bin/extra")
|
||||
}
|
||||
|
||||
@Test func envExpandLeavesUnresolvedIntact() {
|
||||
// Unresolved references stay literal instead of being blanked, so a typo
|
||||
// remains visible.
|
||||
let env = PHEnvironment(variables: [:])
|
||||
#expect(env.expand("$MISSING and ${ALSO_MISSING}") == "$MISSING and ${ALSO_MISSING}")
|
||||
}
|
||||
|
||||
@Test func envExpandIgnoresNonIdentifierDollar() {
|
||||
let env = PHEnvironment(variables: ["FOO": "bar"])
|
||||
// A `$` not followed by an identifier name is left untouched.
|
||||
#expect(env.expand("cost: $5 and $FOO") == "cost: $5 and bar")
|
||||
}
|
||||
|
||||
// MARK: - merging
|
||||
|
||||
@Test func envMergingOverridesExistingKeys() {
|
||||
let env = PHEnvironment(variables: ["PATH": "/usr/bin", "HOME": "/u"])
|
||||
let merged = env.merging(["PATH": "/usr/bin:/extra"])
|
||||
#expect(merged.variables["PATH"] == "/usr/bin:/extra")
|
||||
#expect(merged.variables["HOME"] == "/u")
|
||||
}
|
||||
|
||||
@Test func envMergingNilReturnsSameVariables() {
|
||||
let env = PHEnvironment(variables: ["A": "1"])
|
||||
#expect(env.merging(nil).variables == ["A": "1"])
|
||||
}
|
||||
|
||||
// MARK: - [env] value expansion (config load)
|
||||
|
||||
private func makeConfig(env: [String: String]?) -> PHConfig {
|
||||
PHConfig(theme: "default", window: "default", blocks: nil, env: env, monitors: nil)
|
||||
}
|
||||
|
||||
@Test func resolvingEnvExpandsValuesAgainstGivenEnvironment() {
|
||||
// `PATH = "$PATH:/extra"` extends an inherited variable.
|
||||
let config = makeConfig(env: [
|
||||
"PATH": "$PATH:/opt/phbar/bin",
|
||||
"WORKDIR": "${TMPDIR}/phbar",
|
||||
"PLAIN": "literal",
|
||||
])
|
||||
let resolved = config.resolvingEnv(in: PHEnvironment(variables: [
|
||||
"PATH": "/usr/bin",
|
||||
"TMPDIR": "/var/tmp",
|
||||
]))
|
||||
|
||||
#expect(resolved.env?["PATH"] == "/usr/bin:/opt/phbar/bin")
|
||||
#expect(resolved.env?["WORKDIR"] == "/var/tmp/phbar")
|
||||
#expect(resolved.env?["PLAIN"] == "literal")
|
||||
}
|
||||
|
||||
@Test func resolvingEnvDefaultsToProcessEnvironment() {
|
||||
// `$TMPDIR` is set in phbar's launch environment, so this also exercises
|
||||
// real process-env expansion end to end.
|
||||
guard let tmpdir = ProcessInfo.processInfo.environment["TMPDIR"] else { return }
|
||||
let config = makeConfig(env: ["OUT": "$TMPDIR"])
|
||||
let resolved = config.resolvingEnv()
|
||||
#expect(resolved.env?["OUT"] == tmpdir)
|
||||
}
|
||||
|
||||
@Test func resolvingEnvLeavesConfigUntouchedWhenEnvAbsent() {
|
||||
let config = makeConfig(env: nil)
|
||||
#expect(config.resolvingEnv(in: .process).env == nil)
|
||||
}
|
||||
|
||||
// MARK: - @EnvExpanded wrapper
|
||||
|
||||
/// Stand-in Decodable that mirrors how a theme field opts into expansion.
|
||||
private struct Color: Decodable {
|
||||
@EnvExpanded var hex: String
|
||||
}
|
||||
|
||||
@Test func envExpandedResolvesAgainstInjectedEnvironment() throws {
|
||||
let decoder = TOMLDecoder()
|
||||
decoder.userInfo[PHEnvironment.userInfoKey] = PHEnvironment(variables: ["ACCENT": "#ff0000"])
|
||||
|
||||
let color = try decoder.decode(Color.self, from: #"hex = "$ACCENT""#)
|
||||
|
||||
#expect(color.hex == "#ff0000")
|
||||
}
|
||||
|
||||
@Test func envExpandedFallsBackToLaunchEnvironment() throws {
|
||||
// Without injected userInfo the wrapper falls back to phbar's launch
|
||||
// environment (`ProcessInfo`), which is a snapshot taken at process start.
|
||||
guard let path = ProcessInfo.processInfo.environment["PATH"] else { return }
|
||||
|
||||
let color = try TOMLDecoder().decode(Color.self, from: #"hex = "$PATH""#)
|
||||
|
||||
#expect(color.hex == path)
|
||||
}
|
||||
|
||||
@Test func envExpandedLeavesUnresolvedIntact() throws {
|
||||
let color = try TOMLDecoder().decode(Color.self, from: #"hex = "$NEVER_SET""#)
|
||||
|
||||
#expect(color.hex == "$NEVER_SET")
|
||||
}
|
||||
|
||||
// MARK: - Theme integration
|
||||
|
||||
@Test func themeStyleColorExpandsEnvReference() throws {
|
||||
let decoder = TOMLDecoder()
|
||||
decoder.userInfo[PHEnvironment.userInfoKey] = PHEnvironment(variables: ["ACCENT": "#ff0000"])
|
||||
|
||||
let style = try decoder.decode(PHThemeStyleColor.self, from: #"color = "$ACCENT""#)
|
||||
|
||||
#expect(style.color == "#ff0000")
|
||||
}
|
||||
|
||||
@Test func themeTextFontExpandsEnvReference() throws {
|
||||
let decoder = TOMLDecoder()
|
||||
decoder.userInfo[PHEnvironment.userInfoKey] = PHEnvironment(variables: ["FONT": "Comic Code"])
|
||||
|
||||
let text = try decoder.decode(
|
||||
PHThemeText.self,
|
||||
from: """
|
||||
name = "default"
|
||||
font = "$FONT"
|
||||
size = 14
|
||||
weight = "regular"
|
||||
style = "normal"
|
||||
"""
|
||||
)
|
||||
|
||||
#expect(text.fontFamily == "Comic Code")
|
||||
}
|
||||
Reference in New Issue
Block a user