diff --git a/Sources/Backend/FileManagement/FileVerificationError.swift b/Sources/Backend/FileManagement/FileVerificationError.swift index efe8b5e..127bd6f 100644 --- a/Sources/Backend/FileManagement/FileVerificationError.swift +++ b/Sources/Backend/FileManagement/FileVerificationError.swift @@ -11,32 +11,32 @@ enum FileVerificationError: FormattedError { case isNotDirectory(String) case invalidPathExtension(String, FileType?) - var errorMessage: FormattedText { + var errorMessage: String { switch self { case .alreadyExists(let path): - return "'\(path, color: .yellow)' already exists" + return "'\(path.formatted(color: .yellow))' already exists" case .doesNotExist(let path): - return "No such file or directory '\(path, color: .yellow)'" + return "No such file or directory '\(path.formatted(color: .yellow))'" case .isDirectory(let path): - return "'\(path, color: .yellow)' is a directory" + return "'\(path.formatted(color: .yellow))' is a directory" case .isNotDirectory(let path): - return "'\(path, color: .yellow)' is not a directory" + return "'\(path.formatted(color: .yellow))' is not a directory" case .invalidPathExtension(let pathExtension, let outputType): - let start: FormattedText = "Invalid path extension '\(pathExtension, color: .yellow, style: .bold)'" + let start = "Invalid path extension '\(pathExtension.formatted(color: .yellow, style: .bold))'" guard let outputType else { return start } if let type = outputType.preferredFilenameExtension { - return start + " for expected output type '\(type, color: .cyan, style: .bold)'" + return start + " for expected output type '\(type.formatted(color: .cyan, style: .bold))'" } return start + " for unknown output type" } } - var fix: FormattedText? { + var fix: String? { if case .invalidPathExtension(_, let outputType) = self { if let type = outputType?.preferredFilenameExtension { - return "Use path extension '\(type, color: .green, style: .bold)'" + return "Use path extension '\(type.formatted(color: .green, style: .bold))'" } } return nil diff --git a/Sources/Backend/ImageProcessing/Iconset.swift b/Sources/Backend/ImageProcessing/Iconset.swift index 3a1cb0d..a8f00c9 100644 --- a/Sources/Backend/ImageProcessing/Iconset.swift +++ b/Sources/Backend/ImageProcessing/Iconset.swift @@ -76,8 +76,8 @@ extension Iconset { enum ValidationError: String, FormattedError { case invalidDimensions = "Image width and height must be equal." - var errorMessage: FormattedText { - "\("Invalid icon", color: .red) — \(rawValue, style: .bold)" + var errorMessage: String { + "Invalid icon".formatted(color: .red) + " - " + rawValue.formatted(style: .bold) } } diff --git a/Sources/Backend/ImageProcessing/ImageProcessingError.swift b/Sources/Backend/ImageProcessing/ImageProcessingError.swift index 0ac7c4a..282367a 100644 --- a/Sources/Backend/ImageProcessing/ImageProcessingError.swift +++ b/Sources/Backend/ImageProcessing/ImageProcessingError.swift @@ -14,7 +14,7 @@ enum ImageProcessingError: String, FormattedError { case pdfDocumentError = "Error with PDF document." case svgCreationError = "Error creating image data from SVG." - var errorMessage: FormattedText { - "\("Could not process image", color: .red) — \(rawValue, style: .bold)" + var errorMessage: String { + "Could not process image".formatted(color: .red) + " - " + rawValue.formatted(style: .bold) } } diff --git a/Sources/Backend/Runners/MainRunner.swift b/Sources/Backend/Runners/MainRunner.swift index 5e9118b..6ad20b5 100644 --- a/Sources/Backend/Runners/MainRunner.swift +++ b/Sources/Backend/Runners/MainRunner.swift @@ -55,15 +55,9 @@ public struct MainRunner: Runner { func exit(with error: Error) -> Never { let box = FormattedErrorBox(error: error) - let errorText = FormattedText("error:", color: .red, style: .bold) - .appending(" ") - .appending(box.errorMessage) - print(errorText) + print("error:".formatted(color: .red, style: .bold) + " " + box.errorMessage) if let fix = box.fix { - let fixText = FormattedText("fix:", color: .green, style: .bold) - .appending(" ") - .appending(fix) - print(fixText) + print("fix:".formatted(color: .green, style: .bold) + " " + fix) } Darwin.exit(EXIT_FAILURE) } diff --git a/Sources/Backend/Utilities/Errors.swift b/Sources/Backend/Utilities/Errors.swift index 521deba..7742ff6 100644 --- a/Sources/Backend/Utilities/Errors.swift +++ b/Sources/Backend/Utilities/Errors.swift @@ -11,21 +11,15 @@ import Foundation /// to a command line interface. public protocol FormattedError: Error, TextOutputStreamable { /// The formatted error message to display. - /// - /// If one of either standard output or standard error does not point to - /// a terminal, the message is displayed without formatting. - var errorMessage: FormattedText { get } + var errorMessage: String { get } /// An optional formatted message to display that describes how the user /// can remedy this error. - /// - /// If one of either standard output or standard error does not point to - /// a terminal, the message is displayed without formatting. - var fix: FormattedText? { get } + var fix: String? { get } } extension FormattedError { - public var fix: FormattedText? { nil } + public var fix: String? { nil } } extension FormattedError { @@ -40,14 +34,14 @@ extension FormattedError { struct FormattedErrorBox: FormattedError { let error: any Error - var errorMessage: FormattedText { + var errorMessage: String { if let error = error as? FormattedError { return error.errorMessage } - return FormattedText(contentsOf: error.localizedDescription) + return error.localizedDescription } - var fix: FormattedText? { + var fix: String? { if let error = error as? FormattedError { return error.fix } @@ -66,12 +60,12 @@ struct ContextualDataError: FormattedError { /// A string describing the context in which this error occurred. let context: String - var errorMessage: FormattedText { - var message = FormattedText(context + ":", color: .yellow, style: .bold) + var errorMessage: String { + var message = "\(context):".formatted(color: .yellow, style: .bold) + " " if let string = String(data: data, encoding: .utf8) { - message.append(" \("An error occurred with the following data:", color: .red) \(string)") + message += "An error occurred with the following data:".formatted(color: .red) + " " + string } else { - message.append(" An unknown error occurred") + message += "An unknown error occurred" } return message } diff --git a/Sources/Backend/Utilities/Extensions.swift b/Sources/Backend/Utilities/Extensions.swift index c785888..b4fa307 100644 --- a/Sources/Backend/Utilities/Extensions.swift +++ b/Sources/Backend/Utilities/Extensions.swift @@ -6,39 +6,51 @@ // MARK: - CustomStringConvertible extension CustomStringConvertible { - /// Returns a formatted text instance that formats a textual representation of - /// this value using the given color and style. + /// Returns a textual representation of this value, formatted using + /// the given color and style. /// /// - Parameters: - /// - color: The color to use in the resulting formatted text instance. - /// - style: The style to use in the resulting formatted text instance. + /// - color: The color to use in the resulting formatted text. + /// - style: The style to use in the resulting formatted text. /// - /// - Returns: A formatted text instance that formats a textual representation - /// of this value using the given color and style. - func formatted(color: TextOutputColor, style: TextOutputStyle) -> FormattedText { - FormattedText(self, color: color, style: style) + /// - Returns: A textual representation of this value, formatted using + /// the given color and style. + public func formatted(color: TextOutputColor, style: TextOutputStyle) -> String { + if OutputHandle.standardOutput.isTerminal && OutputHandle.standardError.isTerminal { + return style.onCode + color.onCode + String(describing: self) + color.offCode + style.offCode + } else { + return String(describing: self) + } } - /// Returns a formatted text instance that formats a textual representation of - /// this value using the given color. + /// Returns a textual representation of this value, formatted using + /// the given color. /// - /// - Parameter color: The color to use in the resulting formatted text instance. + /// - Parameter color: The color to use in the resulting formatted text. /// - /// - Returns: A formatted text instance that formats a textual representation - /// of this value using the given color. - func formatted(color: TextOutputColor) -> FormattedText { - FormattedText(self, color: color) + /// - Returns: A textual representation of this value, formatted using + /// the given color. + public func formatted(color: TextOutputColor) -> String { + if OutputHandle.standardOutput.isTerminal && OutputHandle.standardError.isTerminal { + return color.onCode + String(describing: self) + color.offCode + } else { + return String(describing: self) + } } - /// Returns a formatted text instance that formats a textual representation of - /// this value using the given style. + /// Returns a textual representation of this value, formatted using + /// the given style. /// - /// - Parameter style: The style to use in the resulting formatted text instance. + /// - Parameter style: The style to use in the resulting formatted text. /// - /// - Returns: A formatted text instance that formats a textual representation - /// of this value using the given style. - func formatted(style: TextOutputStyle) -> FormattedText { - FormattedText(self, style: style) + /// - Returns: A textual representation of this value, formatted using + /// the given style. + public func formatted(style: TextOutputStyle) -> String { + if OutputHandle.standardOutput.isTerminal && OutputHandle.standardError.isTerminal { + return style.onCode + String(describing: self) + style.offCode + } else { + return String(describing: self) + } } } diff --git a/Sources/Backend/Utilities/Formatting.swift b/Sources/Backend/Utilities/Formatting.swift index 33402de..421fcf9 100644 --- a/Sources/Backend/Utilities/Formatting.swift +++ b/Sources/Backend/Utilities/Formatting.swift @@ -3,37 +3,17 @@ // createicns // -private func shouldFormat(formattingHint: FormattedText.FormattingHint) -> Bool { - switch formattingHint { - case .formatted: - return true - case .unformatted: - return false - case .inferFromStandardOutput: - return OutputHandle.standardOutput.isTerminal - case .inferFromStandardError: - return OutputHandle.standardError.isTerminal - case .inferFromStandardOutputAndStandardError: - return OutputHandle.standardOutput.isTerminal && OutputHandle.standardError.isTerminal - } -} - // MARK: - TextOutputColor /// Colors to use to format text when displayed in a terminal. -public enum TextOutputColor { - /// Formats the text in red. +public enum TextOutputColor: Hashable { case red - /// Formats the text in green. case green - /// Formats the text in yellow. case yellow - /// Formats the text in cyan. case cyan - /// Formats the text in the default color. case `default` - fileprivate var onCode: String { + var onCode: String { switch self { case .red: return "\u{001B}[31m" @@ -48,7 +28,7 @@ public enum TextOutputColor { } } - fileprivate var offCode: String { + var offCode: String { switch self { case .red, .green, .yellow, .cyan: return "\u{001B}[0m" @@ -61,13 +41,11 @@ public enum TextOutputColor { // MARK: - TextOutputStyle /// Styles to use to format text when displayed in a terminal. -public enum TextOutputStyle { - /// Formats the text in bold. +public enum TextOutputStyle: Hashable { case bold - /// Formats the text in the default style. case `default` - fileprivate var onCode: String { + var onCode: String { switch self { case .bold: return "\u{001B}[1m" @@ -76,7 +54,7 @@ public enum TextOutputStyle { } } - fileprivate var offCode: String { + var offCode: String { switch self { case .bold: return "\u{001B}[22m" @@ -85,414 +63,3 @@ public enum TextOutputStyle { } } } - -// MARK: TextOutputStyle: Equatable -extension TextOutputStyle: Equatable { } - -// MARK: TextOutputStyle: Hashable -extension TextOutputStyle: Hashable { } - -// MARK: - FormattedTextComponent - -/// A component in a formatted text instance. -/// -/// A formatted text component is capable of producing both a formatted and -/// unformatted string representation of itself. When an instance of ``FormattedText`` -/// needs to display its components, it uses the value of a provided formatting -/// hint to decide which representation to use. -public enum FormattedTextComponent: Hashable { - /// A component that contains unformatted text. - case unformatted(String) - - /// A component that contains text that is formatted with a specified color. - case color(String, TextOutputColor) - - /// A component that contains text that is formatted with a specified style. - case style(String, TextOutputStyle) - - /// A component that contains text that is formatted with a specified format - /// and style. - case colorAndStyle(String, TextOutputColor, TextOutputStyle) - - /// The formatted representation of this component. - public var formattedRepresentation: String { - switch self { - case .unformatted(let string): - return string - case .color(let string, let color): - return [ - color.onCode, - string, - color.offCode, - ].joined() - case .style(let string, let style): - return [ - style.onCode, - string, - style.offCode, - ].joined() - case .colorAndStyle(let string, let color, let style): - return [ - style.onCode, - color.onCode, - string, - color.offCode, - style.offCode, - ].joined() - } - } - - /// The unformatted representation of this component. - var unformattedRepresentation: String { - switch self { - case .unformatted(let string), - .color(let string, _), - .style(let string, _), - .colorAndStyle(let string, _, _): - return string - } - } -} - -// MARK: - FormattedText - -/// Text that is displayed in a formatted representation when printed to a terminal. -public struct FormattedText { - - // MARK: Types - - /// Constants that specify how to determine what representation of a formatted - /// text instance to display. - public enum FormattingHint { - /// Specifies that a formatted text instance should display itself with a - /// formatted representation. - case formatted - - /// Specifies that a formatted text instance should display itself with an - /// unformatted representation. - case unformatted - - /// Specifies that the formatted text instance should infer which - /// representation to display based on whether the standard output handle is - /// a terminal. - case inferFromStandardOutput - - /// Specifies that the formatted text instance should infer which - /// representation to display based on whether the standard error handle is - /// a terminal. - case inferFromStandardError - - /// Specifies that the formatted text instance should infer which representation - /// to display based on whether both the standard output and standard error - /// handles are terminals. - case inferFromStandardOutputAndStandardError - } - - /// The components that make up this formatted text instance. - private var components: ContiguousArray - - /// Creates a formatted text instance with the given components. - public init(components: S) where S.Element == FormattedTextComponent { - self.components = ContiguousArray(components) - } - - /// Creates a formatted text instance with the given component. - public init(component: FormattedTextComponent) { - self.init(components: CollectionOfOne(component)) - } - - /// Creates an empty formatted text instance. - public init() { - self.init(components: EmptyCollection()) - } - - /// Creates a formatted text instance that is equivalent to the given instance. - public init(_ formattedText: Self) { - self = formattedText - } - - /// Creates a formatted text instance with the contents of the given string. - public init(contentsOf string: S) { - self.init(component: .unformatted(String(string))) - } - - /// Creates a formatted text instance with the given value, color, and style. - public init( - _ value: Value, - color: TextOutputColor, - style: TextOutputStyle - ) { - self.init(component: .colorAndStyle(String(describing: value), color, style)) - } - - /// Creates a formatted text instance with the given value and color. - public init(_ value: Value, color: TextOutputColor) { - self.init(component: .color(String(describing: value), color)) - } - - /// Creates a formatted text instance with the given value and style. - public init(_ value: Value, style: TextOutputStyle) { - self.init(component: .style(String(describing: value), style)) - } - - /// Returns a string representation from the components in this instance. - /// - /// If a value is provided for the `formattingHint` parameter, it will be used to - /// determine whether the string will be returned in a formatted or unformatted - /// representation. If no value is provided, the representation will be inferred - /// based on whether the standard output and standard error handles are terminals. - /// - /// - Parameter formattingHint: A formatting hint to use to determine whether to - /// return the string in a formatted or unformatted representation. - /// - /// - Returns: A string representation of this instance's components. - public func string(formattingHint: FormattingHint? = nil) -> String { - if shouldFormat(formattingHint: formattingHint ?? .inferFromStandardOutputAndStandardError) { - return lazy.map { $0.formattedRepresentation }.joined() - } - return lazy.map { $0.unformattedRepresentation }.joined() - } - - /// Appends the given formatted text instance to this instance. - public mutating func append(_ formattedText: Self) { - components.append(contentsOf: formattedText.components) - } - - /// Appends the contents of the given string to this instance. - public mutating func append(contentsOf string: S) { - append(Self(contentsOf: string)) - } - - /// Appends a formatted text instance created using the given value, color, and style. - public mutating func append( - _ value: Value, - color: TextOutputColor, - style: TextOutputStyle - ) { - append(Self(value, color: color, style: style)) - } - - /// Appends a formatted text instance created using the given value and color. - public mutating func append(_ value: Value, color: TextOutputColor) { - append(Self(value, color: color)) - } - - /// Appends a formatted text instance created using the given value and style. - public mutating func append(_ value: Value, style: TextOutputStyle) { - append(Self(value, style: style)) - } - - /// Returns a new formatted text instance by appending the given formatted text - /// instance to this instance. - public func appending(_ formattedText: Self) -> Self { - var copy = self - copy.append(formattedText) - return copy - } - - /// Returns a new formatted text instance by appending the contents of the given - /// string to this instance. - public func appending(contentsOf string: S) -> Self { - var copy = self - copy.append(contentsOf: string) - return copy - } - - /// Returns a new formatted text instance by appending a formatted text instance - /// created using the given value, color, and style. - public func appending( - _ value: Value, - color: TextOutputColor, - style: TextOutputStyle - ) -> Self { - var copy = self - copy.append(value, color: color, style: style) - return copy - } - - /// Returns a new formatted text instance by appending a formatted text instance - /// created using the given value and color. - public func appending(_ value: Value, color: TextOutputColor) -> Self { - var copy = self - copy.append(value, color: color) - return copy - } - - /// Returns a new formatted text instance by appending a formatted text instance - /// created using the given value and style. - public func appending(_ value: Value, style: TextOutputStyle) -> Self { - var copy = self - copy.append(value, style: style) - return copy - } -} - -// MARK: FormattedText Operators -extension FormattedText { - public static func + (lhs: Self, rhs: Self) -> Self { - lhs.appending(rhs) - } - - public static func + (lhs: Self, rhs: S) -> Self { - lhs.appending(contentsOf: rhs) - } - - public static func + (lhs: S, rhs: Self) -> Self { - Self(contentsOf: lhs).appending(rhs) - } - - public static func += (lhs: inout Self, rhs: Self) { - lhs.append(rhs) - } - - public static func += (lhs: inout Self, rhs: S) { - lhs.append(contentsOf: rhs) - } -} - -// MARK: FormattedText: ExpressibleByStringLiteral -extension FormattedText: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - self.init(component: .unformatted(value)) - } -} - -// MARK: FormattedText: ExpressibleByStringInterpolation -extension FormattedText: ExpressibleByStringInterpolation { - public struct StringInterpolation: StringInterpolationProtocol { - private var components = ContiguousArray() - - /// Returns a formatted text instance from the components in this interpolation. - var formattedText: FormattedText { - FormattedText(components: components) - } - - public init(literalCapacity: Int, interpolationCount: Int) { } - - private mutating func appendComponents(_ components: S) where S.Element == FormattedTextComponent { - self.components.append(contentsOf: components) - } - - private mutating func appendComponent(_ component: FormattedTextComponent) { - appendComponents(CollectionOfOne(component)) - } - - public mutating func appendLiteral(_ literal: String) { - appendComponent(.unformatted(literal)) - } - - public mutating func appendInterpolation(_ formattedText: FormattedText) { - appendComponents(formattedText.components) - } - - public mutating func appendInterpolation( - _ value: Value, - color: TextOutputColor, - style: TextOutputStyle - ) { - appendComponent(.colorAndStyle(String(describing: value), color, style)) - } - - public mutating func appendInterpolation( - _ value: Value, - color: TextOutputColor - ) { - appendComponent(.color(String(describing: value), color)) - } - - public mutating func appendInterpolation( - _ value: Value, - style: TextOutputStyle - ) { - appendComponent(.style(String(describing: value), style)) - } - - public mutating func appendInterpolation(_ value: Value) { - appendComponent(.unformatted(String(describing: value))) - } - } - - public init(stringInterpolation interpolation: StringInterpolation) { - self = interpolation.formattedText - } -} - -// MARK: FormattedText: CustomStringConvertible -extension FormattedText: CustomStringConvertible { - public var description: String { - string(formattingHint: nil) - } -} - -// MARK: FormattedText: TextOutputStreamable -extension FormattedText: TextOutputStreamable { - public func write(to target: inout Target) { - target.write(string(formattingHint: nil)) - } -} - -// MARK: FormattedText: Sequence -extension FormattedText: Sequence { - public typealias Element = FormattedTextComponent - - public struct Iterator: IteratorProtocol { - private var base: IndexingIterator> - - fileprivate init(_ formattedText: FormattedText) { - self.base = formattedText.components.makeIterator() - } - - public mutating func next() -> Element? { - base.next() - } - } - - public func makeIterator() -> Iterator { - Iterator(self) - } -} - -// MARK: FormattedText: Collection -extension FormattedText: Collection { - public var startIndex: Int { - components.startIndex - } - - public var endIndex: Int { - components.endIndex - } - - public func index(after i: Int) -> Int { - components.index(after: i) - } -} - -// MARK: FormattedText: RangeReplaceableCollection -extension FormattedText: RangeReplaceableCollection { - public mutating func replaceSubrange( - _ subrange: Range, - with newElements: C - ) where C.Element == Element { - components.replaceSubrange(subrange, with: newElements) - } -} - -// MARK: FormattedText: MutableCollection -extension FormattedText: MutableCollection { - public subscript(position: Int) -> FormattedTextComponent { - get { components[position] } - set { components[position] = newValue } - } -} - -// MARK: FormattedText: Equatable -extension FormattedText: Equatable { } - -// MARK: FormattedText: Hashable -extension FormattedText: Hashable { } - -// MARK: FormattedText: BidirectionalCollection -extension FormattedText: BidirectionalCollection { } - -// MARK: FormattedText: RandomAccessCollection -extension FormattedText: RandomAccessCollection { } diff --git a/Sources/Frontend/Options.swift b/Sources/Frontend/Options.swift index 9ee703a..73af0d1 100644 --- a/Sources/Frontend/Options.swift +++ b/Sources/Frontend/Options.swift @@ -28,13 +28,13 @@ struct Options: ParsableArguments { if isIconset { type = .iconset // set the type to simulate the behavior of "--iconset" print( - FormattedText("warning:", color: .yellow, style: .bold) + "warning:".formatted(color: .yellow, style: .bold) .appending(" '") - .appending("-s", color: .yellow) + .appending("-s".formatted(color: .yellow)) .appending(", ") - .appending("--iconset", color: .yellow) + .appending("--iconset".formatted(color: .yellow)) .appending("' is deprecated: use '") - .appending("--type", color: .cyan) + .appending("--type".formatted(color: .cyan)) .appending("' instead.") ) }