Skip to content

Commit 4a9bd7b

Browse files
committed
Make the SGR RawRepresentable again, including in ancient OSes
1 parent dadcb17 commit 4a9bd7b

File tree

3 files changed

+166
-36
lines changed

3 files changed

+166
-36
lines changed

Sources/SGR.swift

Lines changed: 29 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import Foundation
1515
- [The 8-bits colors table (direct image link)](<https://i.stack.imgur.com/KTSQa.png>);
1616
- [List of Terminals supporting True Colors](<https://gist.github.com/XVilka/8346728>);
1717
- [The ODA Specs](<https://en.wikipedia.org/wiki/Open_Document_Architecture#External_links>) aka. CCITT T.411-T.424 (equivalent to ISO 8613, but freely downloadable). */
18-
public struct SGR : Hashable, CustomStringConvertible, CLTLogger_Sendable {
18+
public struct SGR : RawRepresentable, Hashable, CustomStringConvertible, CLTLogger_Sendable {
1919

20-
public enum Modifier : Hashable, CustomStringConvertible, CLTLogger_Sendable {
20+
public enum Modifier : RawRepresentable, Hashable, CustomStringConvertible, CLTLogger_Sendable {
2121

2222
/** Reset/Normal -- All attributes off. */
2323
case reset
@@ -441,8 +441,7 @@ public struct SGR : Hashable, CustomStringConvertible, CLTLogger_Sendable {
441441
}
442442
}
443443

444-
@available(macOS 10.15, iOS 13.0, *)
445-
init?(rawValue: String) {
444+
public init?(rawValue: String) {
446445
let s = Scanner(forParsing: rawValue)
447446

448447
self.init(scanner: s)
@@ -452,23 +451,22 @@ public struct SGR : Hashable, CustomStringConvertible, CLTLogger_Sendable {
452451
}
453452
}
454453

455-
/* Note: This init probably has terrible perfs, which is why it is internal (only used in the tests). */
456-
@available(macOS 10.15, iOS 13.0, *)
454+
/* Note: This init probably has terrible perfs. */
457455
init?(scanner: Scanner) {
458456
struct DummyError : Error {}
459-
let originalScannerIndex = scanner.currentIndex
457+
let originalScanLocation = scanner.compatibleScanLocation
460458
do {
461459
enum ColorDestination : String {
462460
case fg = "38"
463461
case bg = "48"
464462
case ul = "58"
465463
}
466-
let token = scanner.scanUpToCharacters(from: CharacterSet(charactersIn: String(Self.separatorChar) + String(SGR.sgrEndChar))) ?? ""
464+
let token = scanner.xl_scanUpToCharacters(from: CharacterSet(charactersIn: String(Self.separatorChar) + String(SGR.sgrEndChar))) ?? ""
467465
if token.contains(":") {
468466
/* Let’s process the special ODA cases. */
469467
let subScanner = Scanner(forParsing: token)
470-
let subToken = subScanner.scanUpToString(":") ?? ""
471-
_ = subScanner.scanString(":")! /* !: The string contains a colon, so the scan cannot fail. */
468+
let subToken = subScanner.xl_scanUpToString(":") ?? ""
469+
_ = subScanner.xl_scanString(":")! /* !: The string contains a colon, so the scan cannot fail. */
472470

473471
let isFgColor: Bool
474472
switch subToken {
@@ -479,7 +477,7 @@ public struct SGR : Hashable, CustomStringConvertible, CLTLogger_Sendable {
479477
* Note however, it might be possible to get the 58 case too (underline color), though because it is not part of the ODA it shouldn’t be valid with this notation. */
480478
throw DummyError()
481479
}
482-
let colorFormat = subScanner.scanUpToString(":") ?? ""
480+
let colorFormat = subScanner.xl_scanUpToString(":") ?? ""
483481
switch colorFormat {
484482
case "0", "":
485483
guard isFgColor, subScanner.isAtEnd else {
@@ -501,10 +499,10 @@ public struct SGR : Hashable, CustomStringConvertible, CLTLogger_Sendable {
501499
func scanParam() throws -> Int? {
502500
if subScanner.isAtEnd {return nil}
503501
else {
504-
guard subScanner.scanString(":") != nil else {
502+
guard subScanner.xl_scanString(":") != nil else {
505503
throw DummyError()
506504
}
507-
guard let str = subScanner.scanUpToString(":") else {
505+
guard let str = subScanner.xl_scanUpToString(":") else {
508506
return nil
509507
}
510508
/* We assume “+1” is a valid value. */
@@ -581,9 +579,9 @@ public struct SGR : Hashable, CustomStringConvertible, CLTLogger_Sendable {
581579
}
582580
if let colorDestination = ColorDestination(rawValue: token) {
583581
guard
584-
scanner.scanCharacter() == Self.separatorChar,
585-
let colorType = scanner.scanCharacter(),
586-
scanner.scanCharacter() == Self.separatorChar
582+
scanner.xl_scanCharacter() == Self.separatorChar,
583+
let colorType = scanner.xl_scanCharacter(),
584+
scanner.xl_scanCharacter() == Self.separatorChar
587585
else {
588586
throw DummyError()
589587
}
@@ -602,19 +600,19 @@ public struct SGR : Hashable, CustomStringConvertible, CLTLogger_Sendable {
602600
/* Wikipedia says empty values are treated as 0.
603601
* But Terminal for instance does not seem to know that.
604602
* We don’t care, we do like Wikipedia says. */
605-
let r = try uint8(scanner.scanUpToString(String(Self.separatorChar)) ?? "0")
606-
guard scanner.scanCharacter() == Self.separatorChar else {throw DummyError()}
607-
let g = try uint8(scanner.scanUpToString(String(Self.separatorChar)) ?? "0")
608-
guard scanner.scanCharacter() == Self.separatorChar else {throw DummyError()}
609-
let b = try uint8(scanner.scanUpToString(String(Self.separatorChar)) ?? "0")
603+
let r = try uint8(scanner.xl_scanUpToString(String(Self.separatorChar)) ?? "0")
604+
guard scanner.xl_scanCharacter() == Self.separatorChar else {throw DummyError()}
605+
let g = try uint8(scanner.xl_scanUpToString(String(Self.separatorChar)) ?? "0")
606+
guard scanner.xl_scanCharacter() == Self.separatorChar else {throw DummyError()}
607+
let b = try uint8(scanner.xl_scanUpToString(String(Self.separatorChar)) ?? "0")
610608
switch colorDestination {
611609
case .fg: self = .fgColorToRGB(red: r, green: g, blue: b); return
612610
case .bg: self = .bgColorToRGB(red: r, green: g, blue: b); return
613611
case .ul: self = .underlineColorToRGB(red: r, green: g, blue: b); return
614612
}
615613

616614
case "5":
617-
let v = try uint8(scanner.scanUpToString(String(Self.separatorChar)) ?? "0")
615+
let v = try uint8(scanner.xl_scanUpToString(String(Self.separatorChar)) ?? "0")
618616
switch colorDestination {
619617
case .fg: self = .fgColorTo256PaletteValue(v); return
620618
case .bg: self = .bgColorTo256PaletteValue(v); return
@@ -709,7 +707,7 @@ public struct SGR : Hashable, CustomStringConvertible, CLTLogger_Sendable {
709707
default: throw DummyError()
710708
}
711709
} catch {
712-
scanner.currentIndex = originalScannerIndex
710+
scanner.compatibleScanLocation = originalScanLocation
713711
return nil
714712
}
715713
}
@@ -745,8 +743,7 @@ public struct SGR : Hashable, CustomStringConvertible, CLTLogger_Sendable {
745743
self.modifiers = modifiers
746744
}
747745

748-
@available(macOS 10.15, iOS 13.0, *)
749-
init?(rawValue: String) {
746+
public init?(rawValue: String) {
750747
let s = Scanner(forParsing: rawValue)
751748

752749
self.init(scanner: s)
@@ -757,21 +754,20 @@ public struct SGR : Hashable, CustomStringConvertible, CLTLogger_Sendable {
757754
}
758755

759756
/* For symetry w/ SGR.Modifier init, but not really needed, at least for now. */
760-
@available(macOS 10.15, iOS 13.0, *)
761757
init?(scanner: Scanner) {
762758
struct DummyError : Error {}
763-
let originalScannerIndex = scanner.currentIndex
759+
let originalScanLocation = scanner.compatibleScanLocation
764760
do {
765761
guard
766-
scanner.scanCharacter() == Self.escapeChar,
767-
scanner.scanCharacter() == Self.csiChar
762+
scanner.xl_scanCharacter() == Self.escapeChar,
763+
scanner.xl_scanCharacter() == Self.csiChar
768764
else {
769765
/* Not a CSI. */
770766
throw DummyError()
771767
}
772768

773-
let csiContent = scanner.scanUpToCharacters(from: Self.possibleFinalByte) ?? ""
774-
guard scanner.scanCharacter() == Self.sgrEndChar else {
769+
let csiContent = scanner.xl_scanUpToCharacters(from: Self.possibleFinalByte) ?? ""
770+
guard scanner.xl_scanCharacter() == Self.sgrEndChar else {
775771
/* Not an SGR. */
776772
throw DummyError()
777773
}
@@ -785,15 +781,15 @@ public struct SGR : Hashable, CustomStringConvertible, CLTLogger_Sendable {
785781
/* A modifier has been parsed.
786782
* Either scan location is now at a semicolon or at the end.
787783
* If on semicolon we must consume it. */
788-
let c = contentScanner.scanCharacter()
784+
let c = contentScanner.xl_scanCharacter()
789785
assert(c == Modifier.separatorChar)
790786
}
791787
guard contentScanner.isAtEnd else {
792788
/* Not all modifiers parsed in the content. */
793789
throw DummyError()
794790
}
795791
} catch {
796-
scanner.currentIndex = originalScannerIndex
792+
scanner.compatibleScanLocation = originalScanLocation
797793
return nil
798794
}
799795
}

Sources/Scanner+OldOS.swift

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
Copyright 2019 happn
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License. */
15+
16+
import Foundation
17+
18+
19+
20+
/* This is from XibLoc. */
21+
extension Scanner {
22+
23+
enum CompatibleScanLocation {
24+
case int(Int)
25+
case index(String.Index)
26+
/** Crashes if the enum contains an index. */
27+
var intValue: Int {
28+
guard case let .int(i) = self else {
29+
fatalError("Asked for the int value but I have an index.")
30+
}
31+
return i
32+
}
33+
var indexValue: String.Index {
34+
guard case let .index(i) = self else {
35+
fatalError("Asked for the index value but I have an int.")
36+
}
37+
return i
38+
}
39+
}
40+
41+
var compatibleScanLocation: CompatibleScanLocation {
42+
get {
43+
#if canImport(Darwin)
44+
if #available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) {
45+
return .index(currentIndex)
46+
} else {
47+
return .int(scanLocation)
48+
}
49+
#else
50+
return .index(currentIndex)
51+
#endif
52+
}
53+
set {
54+
#if canImport(Darwin)
55+
if #available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) {
56+
currentIndex = newValue.indexValue
57+
} else {
58+
scanLocation = newValue.intValue
59+
}
60+
#else
61+
currentIndex = newValue.indexValue
62+
#endif
63+
}
64+
}
65+
66+
func xl_scanString(_ string: String) -> String? {
67+
#if canImport(Darwin)
68+
if #available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) {
69+
return scanString(string)
70+
} else {
71+
var result: NSString?
72+
guard scanString(string, into: &result) else {return nil}
73+
return result! as String
74+
}
75+
#else
76+
return scanString(string)
77+
#endif
78+
}
79+
80+
func xl_scanCharacter() -> Character? {
81+
#if canImport(Darwin)
82+
if #available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) {
83+
return scanCharacter()
84+
} else {
85+
let character = string[string.index(string.startIndex, offsetBy: scanLocation)]
86+
var result: NSString?
87+
guard scanString(String(character), into: &result) else {return nil}
88+
return Character(result! as String)
89+
}
90+
#else
91+
return scanCharacter()
92+
#endif
93+
}
94+
95+
func xl_scanUpToString(_ string: String) -> String? {
96+
#if canImport(Darwin)
97+
if #available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) {
98+
return scanUpToString(string)
99+
} else {
100+
var result: NSString?
101+
guard scanUpTo(string, into: &result) else {return nil}
102+
return result! as String
103+
}
104+
#else
105+
return scanUpToString(string)
106+
#endif
107+
}
108+
109+
func xl_scanUpToCharacters(from characterSet: CharacterSet) -> String? {
110+
#if canImport(Darwin)
111+
if #available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) {
112+
return scanUpToCharacters(from: characterSet)
113+
} else {
114+
var result: NSString?
115+
guard scanUpToCharacters(from: characterSet, into: &result) else {return nil}
116+
return result! as String
117+
}
118+
#else
119+
return scanUpToCharacters(from: characterSet)
120+
#endif
121+
}
122+
123+
func xl_scanCharacters(from set: CharacterSet) -> String? {
124+
#if canImport(Darwin)
125+
if #available(macOS 10.15, tvOS 13.0, iOS 13.0, watchOS 6.0, *) {
126+
return scanCharacters(from: set)
127+
} else {
128+
var result: NSString?
129+
guard scanCharacters(from: set, into: &result) else {return nil}
130+
return result! as String
131+
}
132+
#else
133+
return scanCharacters(from: set)
134+
#endif
135+
}
136+
137+
}

Tests/CLTLoggerTests/SGRTests.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,11 @@ import XCTest
88
/* TODO: A lot more tests! */
99
final class SGRTests : XCTestCase {
1010

11-
@available(macOS 10.15, iOS 13.0, *)
1211
func testSGRParseFail() {
1312
XCTAssertNil(SGR(rawValue: "\(escape)[38;2;255;255m"))
1413
XCTAssertNil(SGR(rawValue: "\(escape)[38;2;255;255;+1m"))
1514
}
1615

17-
@available(macOS 10.15, iOS 13.0, *)
1816
func testSGRParse() {
1917
XCTAssertEqual(SGR(rawValue: "\(escape)[m"), SGR(.reset))
2018
XCTAssertEqual(SGR(rawValue: "\(escape)[0m"), SGR(.reset))
@@ -27,7 +25,6 @@ final class SGRTests : XCTestCase {
2725
XCTAssertEqual(SGR(rawValue: "\(escape)[38;2;255;255;m"), SGR(.fgColorToRGB(red: 0xFF, green: 0xFF, blue: 0x00)))
2826
}
2927

30-
@available(macOS 10.15, iOS 13.0, *)
3128
func testMultipleSGRParse() {
3229
XCTAssertEqual(SGR(rawValue: "\(escape)[38;5;7;m"), SGR(.fgColorTo256PaletteValue(7), .reset))
3330
XCTAssertEqual(SGR(rawValue: "\(escape)[38;2;255;255;0;m"), SGR(.fgColorToRGB(red: 0xFF, green: 0xFF, blue: 0x00), .reset))

0 commit comments

Comments
 (0)