summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTommaso Negri <me@tommasongr.com>2026-10-14 22:21:05 +0200
committerTommaso Negri <me@tommasongr.com>2026-10-14 22:21:05 +0200
commit4942df3a59c0dd7c95b82727f8b03ddf80657973 (patch)
tree0d1395de2bce8257e251ed78ab921ec3da7762f8
parentec076ee6e315e6059545d0364a329cdb4b80b306 (diff)
downloadbar-4942df3a59c0dd7c95b82727f8b03ddf80657973.tar.gz
bar-4942df3a59c0dd7c95b82727f8b03ddf80657973.zip
improve hex color conversion
-rw-r--r--Sources/bar/Models/Color+Hex.swift14
-rw-r--r--Sources/bar/bar.docc/customisation.md3
-rw-r--r--Tests/barTests/ColorHexTests.swift39
-rw-r--r--tasks/20261014-192228/TASK.md19
4 files changed, 68 insertions, 7 deletions
diff --git a/Sources/bar/Models/Color+Hex.swift b/Sources/bar/Models/Color+Hex.swift
index 4f82e78..9d8fc36 100644
--- a/Sources/bar/Models/Color+Hex.swift
+++ b/Sources/bar/Models/Color+Hex.swift
@@ -7,10 +7,8 @@ extension SwiftUI.Color {
/// `#RRGGBB`, and `#RRGGBBAA`. Single-digit components are expanded by
/// repeating the digit. When present, the trailing digits encode alpha
/// (opacity); otherwise opacity defaults to `1`. Surrounding whitespace is
- /// trimmed. Unsupported lengths resolve to opaque black. At supported
- /// lengths, Scanner may parse only a leading hexadecimal portion; its
- /// success result is not checked. Invalid characters therefore do not
- /// reliably produce opaque black (alpha formats can be transparent).
+ /// trimmed. Unsupported lengths or any non-hexadecimal characters resolve
+ /// to opaque black.
///
/// - Parameter code: A hex color code.
static func hex(_ code: String) -> SwiftUI.Color {
@@ -19,8 +17,12 @@ extension SwiftUI.Color {
sanitized.removeFirst()
}
- var rgba: UInt64 = 0
- Scanner(string: sanitized).scanHexInt64(&rgba)
+ // Restrict input to ASCII hex digits; integer conversion also accepts signs.
+ guard sanitized.utf8.allSatisfy({
+ (48...57).contains($0) || (65...70).contains($0) || (97...102).contains($0)
+ }), let rgba = UInt64(sanitized, radix: 16) else {
+ return Color(.sRGB, red: 0, green: 0, blue: 0, opacity: 1)
+ }
let red: Double
let green: Double
diff --git a/Sources/bar/bar.docc/customisation.md b/Sources/bar/bar.docc/customisation.md
index f5d5195..cdf48d9 100644
--- a/Sources/bar/bar.docc/customisation.md
+++ b/Sources/bar/bar.docc/customisation.md
@@ -36,6 +36,9 @@ static let foreground: Color = .hex("#CDD6F4")
```
Supported formats (the leading `#` is optional): `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`.
+Surrounding whitespace is ignored, and letters can be uppercase or lowercase.
+Short components repeat their digit (`#3aF` becomes `#33AAFF`); trailing alpha sets opacity, which otherwise defaults to 1.
+Unsupported lengths or invalid characters produce opaque black.
## Spacing
diff --git a/Tests/barTests/ColorHexTests.swift b/Tests/barTests/ColorHexTests.swift
new file mode 100644
index 0000000..14f60c0
--- /dev/null
+++ b/Tests/barTests/ColorHexTests.swift
@@ -0,0 +1,39 @@
+import SwiftUI
+import Testing
+@testable import bar
+
+private func expectColor(_ color: Color, _ red: Double, _ green: Double, _ blue: Double, _ alpha: Double = 1) {
+ let actual = color.resolve(in: EnvironmentValues())
+ let expected = Color(.sRGB, red: red, green: green, blue: blue, opacity: alpha)
+ .resolve(in: EnvironmentValues())
+ #expect(abs(actual.red - expected.red) < 0.00001)
+ #expect(abs(actual.green - expected.green) < 0.00001)
+ #expect(abs(actual.blue - expected.blue) < 0.00001)
+ #expect(abs(actual.opacity - expected.opacity) < 0.00001)
+}
+
+@Test func validHexColors() {
+ expectColor(.hex("#3aF"), 0x33 / 255.0, 0xAA / 255.0, 1)
+ expectColor(.hex("3Af8"), 0x33 / 255.0, 0xAA / 255.0, 1, 0x88 / 255.0)
+ expectColor(.hex("33aaff"), 0x33 / 255.0, 0xAA / 255.0, 1)
+ expectColor(.hex(" \n#33AAff80\t"), 0x33 / 255.0, 0xAA / 255.0, 1, 0x80 / 255.0)
+ expectColor(.hex("#0000"), 0, 0, 0, 0)
+ expectColor(.hex("#ffffff00"), 1, 1, 1, 0)
+ expectColor(.hex("#FFFFFFFF"), 1, 1, 1)
+}
+
+@Test(arguments: [
+ "", " \n\t", "#", "12", "12345", "1234567", "123456789",
+ "g12", "1g2", "12g", "g123", "1g23", "123g",
+ "g12345", "12g345", "12345g", "g1234567", "123g4567", "1234567g",
+ "+123", "-123", "0xFF", "# 123", "12 3", "##123", "123", "abc💙",
+]) func malformedHexIsOpaqueBlack(_ input: String) {
+ expectColor(.hex(input), 0, 0, 0)
+}
+
+@Test @MainActor func configuredColorsKeepTheirAppearance() {
+ expectColor(Config.foreground, 0xAE / 255.0, 0xD3 / 255.0, 0xF3 / 255.0)
+ expectColor(Config.background, 0x01 / 255.0, 0x04 / 255.0, 0x08 / 255.0, 0.825)
+ expectColor(Config.highlights[0], 0x0F / 255.0, 0x30 / 255.0, 0x4A / 255.0)
+ expectColor(Config.highlights[1], 0x1F / 255.0, 0x68 / 255.0, 0x9D / 255.0)
+}
diff --git a/tasks/20261014-192228/TASK.md b/tasks/20261014-192228/TASK.md
index 453663a..9fd771c 100644
--- a/tasks/20261014-192228/TASK.md
+++ b/tasks/20261014-192228/TASK.md
@@ -1,6 +1,6 @@
# Validate complete hex color input
-- STATUS: OPEN
+- STATUS: CLOSED
- PRIORITY: 160
- TAGS: review,bug,core
@@ -13,3 +13,20 @@ Approach: Validate every character and require complete successful conversion be
Completion checks: Valid short and long RGB/RGBA resolve correctly, including lowercase and alpha. Invalid characters at the start/middle/end, unsupported lengths, empty input, and partial scans all resolve to opaque black. Keep parsing independent of any new configuration framework and verify existing Config colors retain their appearance.
Design constraint: Follow README.md: keep source-based configuration, no dependencies or plugin framework, and use the smallest readable change that makes the existing behavior reliable. This is a suggestion for individual review, not a deadline. Findings were recorded on 2026-10-14; re-check the current code before implementing.
+
+## Completion
+
+Updated the retained helper in Sources/bar/Models/Color+Hex.swift (HEX.swift was
+removed by task 20261014-192305). Validate all characters as ASCII hexadecimal
+digits and require complete UInt64(radix: 16) conversion before extracting
+components. Unsupported lengths and malformed input consistently return opaque
+sRGB black. Optional #, surrounding whitespace, letter case, repeated short
+digits, trailing alpha, and Config appearance are preserved. Updated the helper
+comment and customization documentation to state the fallback.
+
+Added ColorHexTests covering all four valid formats, mixed case, whitespace,
+transparent and opaque alpha, 27 malformed inputs including invalid characters
+at start/middle/end and partial scans, and the configured colors' resolved RGBA.
+Validation: swift test passed all seven tests (including 27 malformed-input
+cases); the clean DocC workflow built the app and documentation without warnings;
+git diff --check passed.