Skip to content

Commit db9fef2

Browse files
johnbuteJohn ButeMaxDesiatov
authored
Add color-diagnostics/no-color-diagnostics CLI flags (#8365)
Added the no-color-diagnostics flag to allow users to toggle on/off color diagnostics Continuation of #8316 Motivation: Not everyone has access to tty, or like colors in their terminal, or are color blind. This feature allows them to control if they want to see colors or not. Modifications: Added the color-diagnostics/no-color-diagnostics flag Added test cases to test for colors edited snippets to include this feature too Result: Developers can now control their color diagnostics when building, running, testing, installing sdks, or browsing code snippets within the swift package learn feature. --------- Co-authored-by: John Bute <[email protected]> Co-authored-by: Max Desiatov <[email protected]>
1 parent 204052c commit db9fef2

16 files changed

+295
-112
lines changed

Sources/Basics/Observability.swift

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Foundation
1616
import struct TSCBasic.Diagnostic
1717
import protocol TSCBasic.DiagnosticData
1818
import protocol TSCBasic.DiagnosticLocation
19+
import class TSCBasic.TerminalController
1920
import class TSCBasic.UnknownLocation
2021
import protocol TSCUtility.DiagnosticDataConvertible
2122
import enum TSCUtility.Diagnostics
@@ -161,7 +162,7 @@ public protocol DiagnosticsHandler: Sendable {
161162
func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic)
162163
}
163164

164-
// helper protocol to share default behavior
165+
/// Helper protocol to share default behavior.
165166
public protocol DiagnosticsEmitterProtocol {
166167
func emit(_ diagnostic: Diagnostic)
167168
}
@@ -393,7 +394,7 @@ public struct Diagnostic: Sendable, CustomStringConvertible {
393394
case info
394395
case debug
395396

396-
internal var naturalIntegralValue: Int {
397+
var naturalIntegralValue: Int {
397398
switch self {
398399
case .debug:
399400
return 0
@@ -409,6 +410,39 @@ public struct Diagnostic: Sendable, CustomStringConvertible {
409410
public static func < (lhs: Self, rhs: Self) -> Bool {
410411
lhs.naturalIntegralValue < rhs.naturalIntegralValue
411412
}
413+
414+
/// A string that represents the log label associated with the severity level.
415+
/// This property provides a descriptive prefix for log messages, indicating the type of message based on its
416+
/// severity.
417+
public var logLabel: String {
418+
switch self {
419+
case .debug:
420+
return "debug: "
421+
case .info:
422+
return "info: "
423+
case .warning:
424+
return "warning: "
425+
case .error:
426+
return "error: "
427+
}
428+
}
429+
430+
public var color: TerminalController.Color {
431+
switch self {
432+
case .debug:
433+
return .white
434+
case .info:
435+
return .white
436+
case .error:
437+
return .red
438+
case .warning:
439+
return .yellow
440+
}
441+
}
442+
443+
public var isBold: Bool {
444+
return true
445+
}
412446
}
413447
}
414448

@@ -417,7 +451,8 @@ public struct Diagnostic: Sendable, CustomStringConvertible {
417451
/// Provides type-safe access to the ObservabilityMetadata's values.
418452
/// This API should ONLY be used inside of accessor implementations.
419453
///
420-
/// End users should use "accessors" the key's author MUST define rather than using this subscript, following this pattern:
454+
/// End users should use "accessors" the key's author MUST define rather than using this subscript, following this
455+
/// pattern:
421456
///
422457
/// extension ObservabilityMetadata {
423458
/// var testID: String? {
@@ -538,7 +573,8 @@ public struct ObservabilityMetadata: Sendable, CustomDebugStringConvertible {
538573
}
539574
}
540575

541-
/// A type-erased `ObservabilityMetadataKey` used when iterating through the `ObservabilityMetadata` using its `forEach` method.
576+
/// A type-erased `ObservabilityMetadataKey` used when iterating through the `ObservabilityMetadata` using its
577+
/// `forEach` method.
542578
public struct AnyKey: Sendable {
543579
/// The key's type represented erased to an `Any.Type`.
544580
public let keyType: Any.Type

Sources/Basics/ProgressAnimation/PercentProgressAnimation.swift

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,17 @@ extension ProgressAnimation {
1919
public static func percent(
2020
stream: WritableByteStream,
2121
verbose: Bool,
22-
header: String
22+
header: String,
23+
isColorized: Bool
2324
) -> any ProgressAnimationProtocol {
2425
Self.dynamic(
2526
stream: stream,
2627
verbose: verbose,
27-
ttyTerminalAnimationFactory: { RedrawingPercentProgressAnimation(terminal: $0, header: header) },
28+
ttyTerminalAnimationFactory: { RedrawingPercentProgressAnimation(
29+
terminal: $0,
30+
header: header,
31+
isColorized: isColorized
32+
) },
2833
dumbTerminalAnimationFactory: { SingleLinePercentProgressAnimation(stream: stream, header: header) },
2934
defaultAnimationFactory: { MultiLinePercentProgressAnimation(stream: stream, header: header) }
3035
)
@@ -35,11 +40,13 @@ extension ProgressAnimation {
3540
final class RedrawingPercentProgressAnimation: ProgressAnimationProtocol {
3641
private let terminal: TerminalController
3742
private let header: String
43+
private let isColorized: Bool
3844
private var hasDisplayedHeader = false
3945

40-
init(terminal: TerminalController, header: String) {
46+
init(terminal: TerminalController, header: String, isColorized: Bool) {
4147
self.terminal = terminal
4248
self.header = header
49+
self.isColorized = isColorized
4350
}
4451

4552
/// Creates repeating string for count times.
@@ -48,14 +55,23 @@ final class RedrawingPercentProgressAnimation: ProgressAnimationProtocol {
4855
return String(repeating: string, count: max(count, 0))
4956
}
5057

58+
func colorizeText(color: TerminalController.Color = .noColor) -> TerminalController.Color {
59+
if self.isColorized {
60+
return color
61+
}
62+
return .noColor
63+
}
64+
5165
func update(step: Int, total: Int, text: String) {
5266
assert(step <= total)
67+
let isBold = self.isColorized
5368

5469
let width = terminal.width
70+
5571
if !hasDisplayedHeader {
5672
let spaceCount = width / 2 - header.utf8.count / 2
5773
terminal.write(repeating(string: " ", count: spaceCount))
58-
terminal.write(header, inColor: .cyan, bold: true)
74+
terminal.write(header, inColor: colorizeText(color: .cyan), bold: isBold)
5975
terminal.endLine()
6076
hasDisplayedHeader = true
6177
} else {
@@ -65,14 +81,18 @@ final class RedrawingPercentProgressAnimation: ProgressAnimationProtocol {
6581
terminal.clearLine()
6682
let percentage = step * 100 / total
6783
let paddedPercentage = percentage < 10 ? " \(percentage)" : "\(percentage)"
68-
let prefix = "\(paddedPercentage)% " + terminal.wrap("[", inColor: .green, bold: true)
84+
let prefix = "\(paddedPercentage)% " + terminal
85+
.wrap("[", inColor: colorizeText(color: .green), bold: isBold)
6986
terminal.write(prefix)
7087

7188
let barWidth = width - prefix.utf8.count
7289
let n = Int(Double(barWidth) * Double(percentage) / 100.0)
7390

74-
terminal.write(repeating(string: "=", count: n) + repeating(string: "-", count: barWidth - n), inColor: .green)
75-
terminal.write("]", inColor: .green, bold: true)
91+
terminal.write(
92+
repeating(string: "=", count: n) + repeating(string: "-", count: barWidth - n),
93+
inColor: colorizeText(color: .green)
94+
)
95+
terminal.write("]", inColor: colorizeText(color: .green), bold: isBold)
7696
terminal.endLine()
7797

7898
terminal.clearLine()
@@ -131,9 +151,7 @@ final class MultiLinePercentProgressAnimation: ProgressAnimationProtocol {
131151
lastDisplayedText = text
132152
}
133153

134-
func complete(success: Bool) {
135-
}
154+
func complete(success: Bool) {}
136155

137-
func clear() {
138-
}
156+
func clear() {}
139157
}

Sources/Build/BuildOperation.swift

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,20 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import _Concurrency
1314
@_spi(SwiftPMInternal)
1415
import Basics
15-
import _Concurrency
16+
import Foundation
1617
import LLBuildManifest
1718
import PackageGraph
1819
import PackageLoading
1920
import PackageModel
2021
import SPMBuildCore
2122
import SPMLLBuild
22-
import Foundation
2323

24+
import class Basics.AsyncProcess
2425
import class TSCBasic.DiagnosticsEngine
2526
import protocol TSCBasic.OutputByteStream
26-
import class Basics.AsyncProcess
2727
import struct TSCBasic.RegEx
2828

2929
import enum TSCUtility.Diagnostics
@@ -88,7 +88,7 @@ package struct LLBuildSystemConfiguration {
8888
}
8989
}
9090

91-
func buildEnvironment(for destination: BuildParameters.Destination) -> BuildEnvironment {
91+
func buildEnvironment(for destination: BuildParameters.Destination) -> BuildEnvironment {
9292
switch destination {
9393
case .host: self.toolsBuildParameters.buildEnvironment
9494
case .target: self.destinationBuildParameters.buildEnvironment
@@ -246,11 +246,13 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
246246
) {
247247
/// Checks if stdout stream is tty.
248248
var productsBuildParameters = productsBuildParameters
249-
productsBuildParameters.outputParameters.isColorized = outputStream.isTTY
250-
249+
if productsBuildParameters.outputParameters.isColorized {
250+
productsBuildParameters.outputParameters.isColorized = outputStream.isTTY
251+
}
251252
var toolsBuildParameters = toolsBuildParameters
252-
toolsBuildParameters.outputParameters.isColorized = outputStream.isTTY
253-
253+
if toolsBuildParameters.outputParameters.isColorized {
254+
toolsBuildParameters.outputParameters.isColorized = outputStream.isTTY
255+
}
254256
self.config = LLBuildSystemConfiguration(
255257
toolsBuildParameters: toolsBuildParameters,
256258
destinationBuildParameters: productsBuildParameters,
@@ -362,9 +364,9 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
362364
fileSystem: localFileSystem,
363365
executor: executor)
364366
guard !consumeDiagnostics.hasErrors else {
365-
// If we could not init the driver with this command, something went wrong,
366-
// proceed without checking this target.
367-
continue
367+
// If we could not init the driver with this command, something went wrong,
368+
// proceed without checking this target.
369+
continue
368370
}
369371
let imports = try driver.performImportPrescan().imports
370372
let nonDependencyTargetsSet =
@@ -521,9 +523,11 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
521523
self.preparationStepName = preparationStepName
522524
self.progressTracker = progressTracker
523525
}
526+
524527
func willCompilePlugin(commandLine: [String], environment: [String: String]) {
525528
self.progressTracker?.preparationStepStarted(preparationStepName)
526529
}
530+
527531
func didCompilePlugin(result: PluginCompilationResult) {
528532
self.progressTracker?.preparationStepHadOutput(
529533
preparationStepName,
@@ -539,6 +543,7 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
539543
}
540544
self.progressTracker?.preparationStepFinished(preparationStepName, result: (result.succeeded ? .succeeded : .failed))
541545
}
546+
542547
func skippedCompilingPlugin(cachedResult: PluginCompilationResult) {
543548
// Historically we have emitted log info about cached plugins that are used. We should reconsider whether this is the right thing to do.
544549
self.progressTracker?.preparationStepStarted(preparationStepName)
@@ -682,7 +687,6 @@ public final class BuildOperation: PackageStructureDelegate, SPMBuildCore.BuildS
682687
fileSystem: self.fileSystem,
683688
observabilityScope: self.observabilityScope
684689
)
685-
686690
}
687691

688692
/// Create the build plan and return the build description.

Sources/Commands/Snippets/CardStack.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,17 @@ struct CardStack {
6060
}
6161

6262
func askForLineInput(prompt: String?) -> String? {
63+
let isColorized: Bool = swiftCommandState.options.logging.colorDiagnostics
64+
6365
if let prompt {
64-
print(brightBlack { prompt }.terminalString())
66+
isColorized ?
67+
print(brightBlack { prompt }.terminalString()) :
68+
print(plain { prompt }.terminalString())
6569
}
66-
terminal.write(">>> ", inColor: .green, bold: true)
70+
isColorized ?
71+
terminal.write(">>> ", inColor: .green, bold: true)
72+
: terminal.write(">>> ", inColor: .noColor, bold: false)
73+
6774
return readLine(strippingNewline: true)
6875
}
6976

@@ -87,10 +94,12 @@ struct CardStack {
8794

8895
askForLine: while let line = askForLineInput(prompt: top.inputPrompt) {
8996
inputFinished = false
90-
let trimmedLine = String(line.drop { $0.isWhitespace }
91-
.reversed()
92-
.drop { $0.isWhitespace }
93-
.reversed())
97+
let trimmedLine = String(
98+
line.drop { $0.isWhitespace }
99+
.reversed()
100+
.drop { $0.isWhitespace }
101+
.reversed()
102+
)
94103
let response = await top.acceptLineInput(trimmedLine)
95104
switch response {
96105
case .none:

Sources/Commands/Snippets/Cards/SnippetCard.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import Basics
1414
import CoreCommands
1515
import PackageModel
1616

17-
import enum TSCBasic.ProcessEnv
1817
import func TSCBasic.exec
18+
import enum TSCBasic.ProcessEnv
1919

2020
/// A card displaying a ``Snippet`` at the terminal.
2121
struct SnippetCard: Card {
@@ -29,6 +29,7 @@ struct SnippetCard: Card {
2929
}
3030
}
3131
}
32+
3233
/// The snippet to display in the terminal.
3334
var snippet: Snippet
3435

@@ -39,20 +40,35 @@ struct SnippetCard: Card {
3940
var swiftCommandState: SwiftCommandState
4041

4142
func render() -> String {
42-
var rendered = colorized {
43+
let isColorized: Bool = swiftCommandState.options.logging.colorDiagnostics
44+
var rendered = isColorized ? colorized {
4345
brightYellow {
4446
"# "
4547
snippet.name
4648
}
4749
"\n\n"
4850
}.terminalString()
51+
:
52+
plain {
53+
plain {
54+
"# "
55+
snippet.name
56+
}
57+
"\n\n"
58+
}.terminalString()
4959

5060
if !snippet.explanation.isEmpty {
51-
rendered += brightBlack {
61+
rendered += isColorized ? brightBlack {
62+
snippet.explanation
63+
.split(separator: "\n", omittingEmptySubsequences: false)
64+
.map { "// " + $0 }
65+
.joined(separator: "\n")
66+
}.terminalString()
67+
: plain {
5268
snippet.explanation
53-
.split(separator: "\n", omittingEmptySubsequences: false)
54-
.map { "// " + $0 }
55-
.joined(separator: "\n")
69+
.split(separator: "\n", omittingEmptySubsequences: false)
70+
.map { "// " + $0 }
71+
.joined(separator: "\n")
5672
}.terminalString()
5773

5874
rendered += "\n\n"

0 commit comments

Comments
 (0)