Create logger.pretty and use PrintableString

This commit is contained in:
Oliver Atkinson 2020-03-05 21:45:40 +00:00
parent f0871503da
commit 489b5b66b1
29 changed files with 297 additions and 494 deletions

View File

@ -67,13 +67,13 @@ final class BundleCommand: Command {
}
let version = try String(contentsOf: versionFilePath.url)
logger.info("Bundling the version \(version) in the directory \(binFolderPath.pathString)".section())
logger.notice("Bundling the version \(version) in the directory \(binFolderPath.pathString)".section())
let versionPath = versionsController.path(version: version)
// Installing
if !FileHandler.shared.exists(versionPath) {
logger.info("Version \(version) not available locally. Installing...")
logger.notice("Version \(version) not available locally. Installing...")
try installer.install(version: version, force: false)
}
@ -83,6 +83,6 @@ final class BundleCommand: Command {
}
try FileHandler.shared.copy(from: versionPath, to: binFolderPath)
logger.info("tuist bundled successfully at \(binFolderPath.pathString)".success())
logger.notice("tuist bundled successfully at \(binFolderPath.pathString)".success())
}
}

View File

@ -63,9 +63,9 @@ class CommandRunner: CommandRunning {
switch resolvedVersion {
case let .bin(path):
logger.info("Using bundled version at path \(path.pathString)")
logger.notice("Using bundled version at path \(path.pathString)")
case let .versionFile(path, value):
logger.info("Using version \(value) defined at \(path.pathString)")
logger.notice("Using version \(value) defined at \(path.pathString)")
default:
break
}
@ -100,7 +100,7 @@ class CommandRunner: CommandRunning {
func runVersion(_ version: String) throws {
if !versionsController.versions().contains(where: { $0.description == version }) {
logger.info("Version \(version) not found locally. Installing...")
logger.notice("Version \(version) not found locally. Installing...")
try installer.install(version: version, force: false)
}

View File

@ -46,19 +46,19 @@ class LocalCommand: Command {
// MARK: - Fileprivate
private func printLocalVersions() throws {
logger.info("The following versions are available in the local environment:".section())
logger.notice("The following versions are available in the local environment:".section())
let versions = versionController.semverVersions()
let output = versions.sorted().reversed().map { "- \($0)" }.joined(separator: "\n")
logger.info("\(output)")
logger.notice("\(output)")
}
private func createVersionFile(version: String) throws {
let currentPath = FileHandler.shared.currentPath
logger.info("Generating \(Constants.versionFileName) file with version \(version)".section())
logger.notice("Generating \(Constants.versionFileName) file with version \(version)".section())
let tuistVersionPath = currentPath.appending(component: Constants.versionFileName)
try "\(version)".write(to: URL(fileURLWithPath: tuistVersionPath.pathString),
atomically: true,
encoding: .utf8)
logger.info("File generated at path \(tuistVersionPath.pathString)".success())
logger.notice("File generated at path \(tuistVersionPath.pathString)".success())
}
}

View File

@ -40,7 +40,7 @@ final class UninstallCommand: Command {
let versions = versionsController.versions().map { $0.description }
if versions.contains(version) {
try versionsController.uninstall(version: version)
logger.info("Version \(version) uninstalled".success())
logger.notice("Version \(version) uninstalled".success())
} else {
logger.warning("Version \(version) cannot be uninstalled because it's not installed")
}

View File

@ -51,7 +51,7 @@ final class UpdateCommand: Command {
/// - Throws: An error if the update process fails.
func run(with result: ArgumentParser.Result) throws {
let force = result.get(forceArgument) ?? false
logger.info("Checking for updates...".section())
logger.notice("Checking for updates...".section())
try updater.update(force: force)
}
}

View File

@ -18,6 +18,6 @@ class VersionCommand: NSObject, Command {
// MARK: - Command
func run(with _: ArgumentParser.Result) {
logger.info("\(Constants.version)")
logger.notice("\(Constants.version)")
}
}

View File

@ -78,7 +78,7 @@ final class Installer: Installing {
func install(version: String, temporaryDirectory: TemporaryDirectory, force: Bool = false) throws {
// We ignore the Swift version and install from the soruce code
if force {
logger.info("Forcing the installation of \(version) from the source code")
logger.notice("Forcing the installation of \(version) from the source code")
try installFromSource(version: version,
temporaryDirectory: temporaryDirectory)
return
@ -102,17 +102,17 @@ final class Installer: Installing {
try versionsController.install(version: version, installation: { installationDirectory in
// Download bundle
logger.info("Downloading version \(version)")
logger.notice("Downloading version \(version)")
let downloadPath = temporaryDirectory.path.appending(component: Constants.bundleName)
try System.shared.run("/usr/bin/curl", "-LSs", "--output", downloadPath.pathString, bundleURL.absoluteString)
// Unzip
logger.info("Installing...")
logger.notice("Installing...")
try System.shared.run("/usr/bin/unzip", "-q", downloadPath.pathString, "-d", installationDirectory.pathString)
try createTuistVersionFile(version: version, path: installationDirectory)
logger.info("Version \(version) installed")
logger.notice("Version \(version) installed")
})
}
@ -124,7 +124,7 @@ final class Installer: Installing {
let buildDirectory = temporaryDirectory.path.appending(RelativePath(".build/release/"))
// Cloning and building
logger.info("Pulling source code")
logger.notice("Pulling source code")
_ = try System.shared.observable(["/usr/bin/env", "git", "clone", Constants.gitRepositoryURL, temporaryDirectory.path.pathString])
.mapToString()
.printStandardError()
@ -144,7 +144,7 @@ final class Installer: Installing {
}
}
logger.info("Building using Swift (it might take a while)")
logger.notice("Building using Swift (it might take a while)")
let swiftPath = try System.shared
.observable(["/usr/bin/xcrun", "-f", "swift"])
.mapToString()
@ -185,7 +185,7 @@ final class Installer: Installing {
to: installationDirectory)
try createTuistVersionFile(version: version, path: installationDirectory)
logger.info("Version \(version) installed")
logger.notice("Version \(version) installed")
}
}

View File

@ -39,17 +39,17 @@ final class Updater: Updating {
}
if force {
logger.info("Forcing the update of version \(highestRemoteVersion)")
logger.notice("Forcing the update of version \(highestRemoteVersion)")
try installer.install(version: highestRemoteVersion.description, force: true)
} else if let highestLocalVersion = versionsController.semverVersions().sorted().last {
if highestRemoteVersion <= highestLocalVersion {
logger.info("There are no updates available")
logger.notice("There are no updates available")
} else {
logger.info("Installing new version available \(highestRemoteVersion)")
logger.notice("Installing new version available \(highestRemoteVersion)")
try installer.install(version: highestRemoteVersion.description, force: false)
}
} else {
logger.info("No local versions available. Installing the latest version \(highestRemoteVersion)")
logger.notice("No local versions available. Installing the latest version \(highestRemoteVersion)")
try installer.install(version: highestRemoteVersion.description, force: false)
}
}

View File

@ -77,7 +77,7 @@ final class ProjectGenerator: ProjectGenerating {
sourceRootPath: AbsolutePath? = nil,
xcodeprojPath: AbsolutePath? = nil) throws -> GeneratedProject {
logger.info("Generating project \(project.name)")
logger.notice("Generating project \(project.name)")
// Getting the path.
let sourceRootPath = sourceRootPath ?? project.path

View File

@ -88,7 +88,7 @@ final class WorkspaceGenerator: WorkspaceGenerating {
tuistConfig _: TuistConfig) throws -> AbsolutePath {
let workspaceName = "\(graph.name).xcworkspace"
logger.info("Generating workspace \(workspaceName)", metadata: .section)
logger.notice("Generating workspace \(workspaceName)", metadata: .section)
/// Projects

View File

@ -81,7 +81,7 @@ final class CocoaPodsInteractor: CocoaPodsInteracting {
// The installation of Pods might fail if the local repository that contains the specs
// is outdated.
logger.info("Installing CocoaPods dependencies defined in \(node.podfilePath)", metadata: .section)
logger.notice("Installing CocoaPods dependencies defined in \(node.podfilePath)", metadata: .section)
var mightNeedRepoUpdate: Bool = false
let outputClosure: ([UInt8]) -> Void = { bytes in

View File

@ -62,7 +62,7 @@ final class CacheController: CacheControlling {
try graphContentHasher.contentHashes(for: graph)
.filter { target, hash in
if let exists = try self.cache.exists(hash: hash).toBlocking().first(), exists {
logger.notice("The target \(target.name, .highlight) with hash \(hash, .highlight) is already in the cache. Skipping...")
logger.pretty("The target \(.bold(.raw(target.name))) with hash \(.bold(.raw(hash))) is already in the cache. Skipping...")
return false
}
return true

View File

@ -44,6 +44,6 @@ class DumpCommand: NSObject, Command {
}
let project = try manifestLoader.loadProject(at: path)
let json: JSON = try project.toJSON()
logger.info("\(json.toString(prettyPrint: true))")
logger.notice("\(json.toString(prettyPrint: true))")
}
}

View File

@ -52,10 +52,10 @@ class EditCommand: NSObject, Command {
try! FileHandler.shared.delete(EditCommand.temporaryDirectory.path)
exit(0)
}
logger.info("Opening Xcode to edit the project. Press \("CTRL + C", .keystroke) once you are done editing")
logger.pretty("Opening Xcode to edit the project. Press \(.keystroke("CTRL + C")) once you are done editing")
try opener.open(path: xcodeprojPath, wait: true)
} else {
logger.info("Xcode project generated at \(xcodeprojPath.pathString)", metadata: .success)
logger.notice("Xcode project generated at \(xcodeprojPath.pathString)", metadata: .success)
}
}

View File

@ -63,8 +63,8 @@ class GenerateCommand: NSObject, Command {
let time = String(format: "%.3f", timer.stop())
logger.info("Project generated.", metadata: .success)
logger.info("Total time taken: \(time)s")
logger.notice("Project generated.", metadata: .success)
logger.notice("Total time taken: \(time)s")
}
// MARK: - Fileprivate

View File

@ -45,11 +45,11 @@ class GraphCommand: NSObject, Command {
let path = FileHandler.shared.currentPath.appending(component: "graph.dot")
if FileHandler.shared.exists(path) {
logger.info("Deleting existing graph at \(path.pathString)")
logger.notice("Deleting existing graph at \(path.pathString)")
try FileHandler.shared.delete(path)
}
try FileHandler.shared.write(graph, path: path, atomically: true)
logger.info("Graph exported to \(path.pathString)", metadata: .success)
logger.notice("Graph exported to \(path.pathString)", metadata: .success)
}
}

View File

@ -96,7 +96,7 @@ class InitCommand: NSObject, Command {
try generateTuistConfig(path: path)
try generateGitIgnore(path: path)
logger.info("Project generated at path \(path.pathString).", metadata: .success)
logger.notice("Project generated at path \(path.pathString).", metadata: .success)
}

View File

@ -18,6 +18,6 @@ class VersionCommand: NSObject, Command {
// MARK: - Command
func run(with _: ArgumentParser.Result) {
logger.info("\(Constants.version)")
logger.notice("\(Constants.version)")
}
}

View File

@ -50,7 +50,7 @@ public class SetupLoader: SetupLoading {
.printAndThrowIfNeeded()
try setup.forEach { command in
if try !command.isMet(projectPath: path) {
logger.info("Configuring \(command.name, .command)", metadata: .subsection)
logger.notice("Configuring \(command.name)", metadata: .subsection)
try command.meet(projectPath: path)
}
}

View File

@ -45,7 +45,7 @@ class UpHomebrew: Up, GraphInitiatable {
/// - Throws: An error if any error is thrown while running it.
override func meet(projectPath _: AbsolutePath) throws {
if !toolInstalled("brew") {
logger.info("Installing Homebrew")
logger.notice("Installing Homebrew")
try System.shared.runAndPrint("/usr/bin/ruby",
"-e",
"\"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\"",
@ -54,7 +54,7 @@ class UpHomebrew: Up, GraphInitiatable {
}
let nonInstalledPackages = packages.filter { !toolInstalled($0) }
try nonInstalledPackages.forEach { package in
logger.info("Installing Homebrew package: \(package)")
logger.notice("Installing Homebrew package: \(package)")
try System.shared.runAndPrint("/usr/local/bin/brew", "install", package,
verbose: true,
environment: System.shared.env)

View File

@ -62,7 +62,7 @@ class UpHomebrewTap: Up, GraphInitiatable {
let taps = try self.taps()
let notConfigured = repositories.filter { !isTapConfigured($0, taps: taps) }
for repository in notConfigured {
logger.info("Adding repository tap: \(repository)")
logger.notice("Adding repository tap: \(repository)")
try System.shared.run(["brew", "tap", repository])
}
}

View File

@ -1,218 +0,0 @@
public enum ConsolePrettyToken: String {
case highlight
case command
case keystroke
var tokens: Set<ConsoleToken> {
switch self {
case .highlight:
return [ .bold ]
case .command:
return [ .white, .bold ]
case .keystroke:
return [ .cyan ]
}
}
}
extension Logger.Metadata {
public static let colored: String = "is"
public static let successKey: String = "success"
public static var success: Logger.Metadata {
return [ colored: .string(successKey) ]
}
public static let sectionKey: String = "section"
public static var section: Logger.Metadata {
return [ colored: .string(sectionKey) ]
}
public static let subsectionKey: String = "subsection"
public static var subsection: Logger.Metadata {
return [ colored: .string(subsectionKey) ]
}
}
extension String.StringInterpolation {
public mutating func appendInterpolation(_ value: String, _ first: ConsolePrettyToken, rest: ConsolePrettyToken...) {
appendInterpolation(value, [ first ] + rest)
}
public mutating func appendInterpolation(_ value: String, _ token: [ConsolePrettyToken]) {
let tokens = token.flatMap({ $0.tokens })
appendInterpolation(value, Set(tokens))
}
internal mutating func appendInterpolation(_ value: String, _ token: Set<ConsoleToken>) {
if Environment.shared.shouldOutputBeColoured {
appendLiteral(value.apply(token))
} else {
appendLiteral(value)
}
}
}
extension Set where Element == ConsoleToken {
func apply(to string: String) -> String {
reduce(string) { $1.apply($0) }
}
}
extension String {
func apply(_ token: Set<ConsoleToken>) -> String {
token.apply(to: self)
}
}
/// A token used in the console to colourize output. This OptionSet is used to
/// allow for consumers to specify one-or-many different tokens to apply to the
/// formatting of a string rendered inside the console. See ColourizeSwift for
/// implementation details on how this unfolds when it arrives in Terminal.app.
enum ConsoleToken: String {
case black
case blue
case cyan
case darkGray
case green
case lightBlue
case lightCyan
case lightGray
case lightGreen
case lightMagenta
case lightRed
case lightYellow
case magenta
case onBlack
case onBlue
case onCyan
case onDarkGray
case onGreen
case onLightBlue
case onLightCyan
case onLightGray
case onLightGreen
case onLightMagenta
case onLightRed
case onLightYellow
case onMagenta
case onRed
case onWhite
case onYellow
case red
case white
case yellow
case blink
case bold
case dim
case hidden
case italic
case reset
case reverse
case strikethrough
case underline
var apply: (String) -> String {
let ƒ: (String) -> () -> String
switch self {
case .black:
ƒ = String.black
case .blue:
ƒ = String.blue
case .cyan:
ƒ = String.cyan
case .darkGray:
ƒ = String.darkGray
case .green:
ƒ = String.green
case .lightBlue:
ƒ = String.lightBlue
case .lightCyan:
ƒ = String.lightCyan
case .lightGray:
ƒ = String.lightGray
case .lightGreen:
ƒ = String.lightGreen
case .lightMagenta:
ƒ = String.lightMagenta
case .lightRed:
ƒ = String.lightRed
case .lightYellow:
ƒ = String.lightYellow
case .magenta:
ƒ = String.magenta
case .onBlack:
ƒ = String.onBlack
case .onBlue:
ƒ = String.onBlue
case .onCyan:
ƒ = String.onCyan
case .onDarkGray:
ƒ = String.onDarkGray
case .onGreen:
ƒ = String.onGreen
case .onLightBlue:
ƒ = String.onLightBlue
case .onLightCyan:
ƒ = String.onLightCyan
case .onLightGray:
ƒ = String.onLightGray
case .onLightGreen:
ƒ = String.onLightGreen
case .onLightMagenta:
ƒ = String.onLightMagenta
case .onLightRed:
ƒ = String.onLightRed
case .onLightYellow:
ƒ = String.onLightYellow
case .onMagenta:
ƒ = String.onMagenta
case .onRed:
ƒ = String.onRed
case .onWhite:
ƒ = String.onWhite
case .onYellow:
ƒ = String.onYellow
case .red:
ƒ = String.red
case .white:
ƒ = String.white
case .yellow:
ƒ = String.yellow
case .bold:
ƒ = String.bold
case .dim:
ƒ = String.dim
case .italic:
ƒ = String.italic
case .underline:
ƒ = String.underline
case .blink:
ƒ = String.blink
case .reverse:
ƒ = String.reverse
case .hidden:
ƒ = String.hidden
case .strikethrough:
ƒ = String.strikethrough
case .reset:
ƒ = String.reset
}
return flip(ƒ)()
}
}

View File

@ -0,0 +1,25 @@
extension Logger.Metadata {
public static let tuist: String = "is"
public static let successKey: String = "success"
public static var success: Logger.Metadata {
return [ tuist: .string(successKey) ]
}
public static let sectionKey: String = "section"
public static var section: Logger.Metadata {
return [ tuist: .string(sectionKey) ]
}
public static let subsectionKey: String = "subsection"
public static var subsection: Logger.Metadata {
return [ tuist: .string(subsectionKey) ]
}
public static let prettyKey: String = "pretty"
public static var pretty: Logger.Metadata {
return [ tuist: .string(prettyKey) ]
}
}

View File

@ -6,7 +6,9 @@ let logger = Logger(label: "io.tuist.support")
public struct LoggingConfig {
public enum LoggerType {
case console, detailed, osLog
case console
case detailed
case osLog
}
public var loggerType: LoggerType
@ -15,6 +17,7 @@ public struct LoggingConfig {
}
extension LoggingConfig {
public static var `default`: LoggingConfig {
let environment = ProcessInfo.processInfo.environment
@ -31,8 +34,8 @@ extension LoggingConfig {
return .init(loggerType: .console, verbose: verbose)
}
}
}
public enum LogOutput {

View File

@ -1,22 +0,0 @@
extension Logger.Message {
func colorize(for logLevel: Logger.Level) -> Logger.Message {
Logger.Message(stringLiteral: token(for: logLevel)?.apply(to: description) ?? description)
}
func token(for logLevel: Logger.Level) -> Set<ConsoleToken>? {
switch logLevel {
case .critical:
return [ .red, .bold ]
case .error:
return [ .red ]
case .warning:
return [ .yellow ]
case .notice:
return [ .bold ]
case .debug, .trace, .info:
return .none
}
}
}

View File

@ -22,28 +22,36 @@ public struct StandardLogHandler: LogHandler {
metadata: Logger.Metadata?,
file: String, function: String, line: UInt
) {
let log: Logger.Message
if Environment.shared.shouldOutputBeColoured {
switch metadata?[Logger.Metadata.colored] {
case .string(Logger.Metadata.successKey)?:
log = Logger.Message(stringLiteral: message.description.apply([ .green, .bold ]))
case .string(Logger.Metadata.sectionKey)?:
log = Logger.Message(stringLiteral: message.description.apply([ .cyan, .bold ]))
case .string(Logger.Metadata.subsectionKey)?:
log = Logger.Message(stringLiteral: message.description.apply([ .cyan ]))
default:
log = message.colorize(for: level)
if metadata?.keys.contains(Logger.Metadata.prettyKey) == true {
return
}
let string: String
switch metadata?[Logger.Metadata.tuist] {
case Logger.Metadata.successKey?:
string = message.description.green().bold()
case Logger.Metadata.sectionKey?:
string = message.description.cyan().bold()
case Logger.Metadata.subsectionKey?:
string = message.description.cyan()
default:
switch level {
case .critical:
string = message.description.red().bold()
case .error:
string = message.description.red()
case .warning:
string = message.description.yellow()
case .notice, .info, .debug, .trace:
string = message.description
}
} else {
log = message
}
output(for: level).print(log.description)
output(for: level).print(string)
}
func output(for level: Logger.Level) -> FileHandle {
@ -69,3 +77,12 @@ extension FileHandle {
}
}
func ~= (lhs: String, rhs: Logger.MetadataValue) -> Bool {
switch rhs {
case let .string(s): return lhs == s
default: return false
}
}

View File

@ -164,22 +164,28 @@ public class FileHandler: FileHandling {
}
public func exists(_ path: AbsolutePath) -> Bool {
fileManager.fileExists(atPath: path.pathString)
let exists = fileManager.fileExists(atPath: path.pathString)
logger.debug("Checking if \(path) exists... \(exists)")
return exists
}
public func copy(from: AbsolutePath, to: AbsolutePath) throws {
logger.debug("Copying file from \(from) to \(to)")
try fileManager.copyItem(atPath: from.pathString, toPath: to.pathString)
}
public func move(from: AbsolutePath, to: AbsolutePath) throws {
logger.debug("Moving file from \(from) to \(to)")
try fileManager.moveItem(atPath: from.pathString, toPath: to.pathString)
}
public func readFile(_ at: AbsolutePath) throws -> Data {
try Data(contentsOf: at.url)
logger.debug("Reading contents of file at path \(at)")
return try Data(contentsOf: at.url)
}
public func readTextFile(_ at: AbsolutePath) throws -> String {
logger.debug("Reading contents of text file at path \(at)")
let data = try Data(contentsOf: at.url)
if let content = String(data: data, encoding: .utf8) {
return content
@ -189,6 +195,7 @@ public class FileHandler: FileHandling {
}
public func readPlistFile<T: Decodable>(_ at: AbsolutePath) throws -> T {
logger.debug("Reading contents of plist file at path \(at)")
guard let data = fileManager.contents(atPath: at.pathString) else {
throw FileHandlerError.fileNotFound(at)
}
@ -196,16 +203,19 @@ public class FileHandler: FileHandling {
}
public func linkFile(atPath: AbsolutePath, toPath: AbsolutePath) throws {
logger.debug("Creating a link from \(atPath) to \(toPath)")
try fileManager.linkItem(atPath: atPath.pathString, toPath: toPath.pathString)
}
public func write(_ content: String, path: AbsolutePath, atomically: Bool) throws {
logger.debug("Writing contents to file \(path) atomically \(atomically)")
do {
try content.write(to: path.url, atomically: atomically, encoding: .utf8)
} catch {}
}
public func locateDirectory(_ path: String, traversingFrom from: AbsolutePath) -> AbsolutePath? {
logger.debug("Traversing \(from) to locate \(path)")
let extendedPath = from.appending(RelativePath(path))
if exists(extendedPath) {
return extendedPath
@ -221,16 +231,19 @@ public class FileHandler: FileHandling {
}
public func createFolder(_ path: AbsolutePath) throws {
logger.debug("Creating folder at path \(path)")
try fileManager.createDirectory(at: path.url,
withIntermediateDirectories: true,
attributes: nil)
}
public func delete(_ path: AbsolutePath) throws {
logger.debug("Deleting item at path \(path)")
try fileManager.removeItem(atPath: path.pathString)
}
public func touch(_ path: AbsolutePath) throws {
logger.debug("Touching \(path)")
try fileManager.createDirectory(at: path.removingLastComponent().url,
withIntermediateDirectories: true,
attributes: nil)
@ -244,6 +257,9 @@ public class FileHandler: FileHandling {
}
public func locateDirectoryTraversingParents(from: AbsolutePath, path: String) -> AbsolutePath? {
logger.debug("Traversing \(from) to locate \(path)")
let tuistConfigPath = from.appending(component: path)
if FileHandler.shared.exists(tuistConfigPath) {

View File

@ -1,188 +1,158 @@
//import Basic
//import Foundation
//
//public struct PrintableString: Encodable, Decodable, Equatable {
// /// Contains a string that can be interpolated with options.
// let rawString: String
//}
//
//extension PrintableString: ExpressibleByStringLiteral {
// public init(stringLiteral: String) {
// rawString = stringLiteral
// }
//}
//
//extension PrintableString: CustomStringConvertible {
// public var description: String {
// rawString
// }
//}
//
//extension PrintableString: ExpressibleByStringInterpolation {
// public init(stringInterpolation: StringInterpolation) {
// rawString = stringInterpolation.string
// }
//
// public struct StringInterpolation: StringInterpolationProtocol {
// var string: String
//
// public init(literalCapacity _: Int, interpolationCount _: Int) {
// string = String()
// }
//
// public mutating func appendLiteral(_ literal: String) {
// string.append(literal)
// }
//
// public mutating func appendInterpolation(_ token: PrintableString.Token) {
// string.append(token.description)
// }
//
// public mutating func appendInterpolation(_ value: String) {
// string.append(value)
// }
//
// public mutating func appendInterpolation(_ value: CustomStringConvertible) {
// string.append(value.description)
// }
// }
//}
//
//extension PrintableString {
// public indirect enum Token: ExpressibleByStringLiteral {
// case raw(String)
// case command(Token)
// case keystroke(Token)
// case bold(Token)
// case error(Token)
// case success(Token)
// case warning(Token)
// case info(Token)
//
// public init(stringLiteral: String) {
// self = .raw(stringLiteral)
// }
//
// public var description: String {
// switch self {
// case let .raw(string):
// return string
// case let .command(token):
// return Environment.shared.shouldOutputBeColoured ? token.description.cyan() : token.description
// case let .keystroke(token):
// return Environment.shared.shouldOutputBeColoured ? token.description.cyan() : token.description
// case let .bold(token):
// return Environment.shared.shouldOutputBeColoured ? token.description.bold() : token.description
// case let .error(token):
// return Environment.shared.shouldOutputBeColoured ? token.description.red() : token.description
// case let .success(token):
// return Environment.shared.shouldOutputBeColoured ? token.description.green() : token.description
// case let .warning(token):
// return Environment.shared.shouldOutputBeColoured ? token.description.yellow() : token.description
// case let .info(token):
// return Environment.shared.shouldOutputBeColoured ? token.description.lightBlue() : token.description
// }
// }
// }
//}
//
//public protocol Printing: AnyObject {
// func print(_ text: PrintableString)
// func print(section: PrintableString)
// func print(subsection: PrintableString)
// func print(warning: PrintableString)
// func print(error: Error)
// func print(success: PrintableString)
// func print(errorMessage: PrintableString)
// func print(deprecation: PrintableString)
//}
//
//public class Printer: Printing {
// /// Shared instance
// public static var shared: Printing = Printer()
//
// // MARK: - Init
//
// init() {}
//
// // MARK: - Public
//
// public func print(_ text: PrintableString) {
// printStandardOutputLine(text.description)
// }
//
// public func print(error: Error) {
// if Environment.shared.shouldOutputBeColoured {
// printStandardErrorLine("Error: \(error.localizedDescription)".localizedDescription.red().bold())
// } else {
// printStandardErrorLine("Error: \(error.localizedDescription)")
// }
// }
//
// public func print(success: PrintableString) {
// if Environment.shared.shouldOutputBeColoured {
// printStandardOutputLine("Success: \(success)".green().bold())
// } else {
// printStandardOutputLine("Success: \(success)")
// }
// }
//
// /// Prints a deprecation message (yellow color)
// ///
// /// - Parameter deprecation: Deprecation message.
// public func print(deprecation: PrintableString) {
// if Environment.shared.shouldOutputBeColoured {
// printStandardOutputLine("Deprecated: \(deprecation)".yellow().bold())
// } else {
// printStandardOutputLine("Deprecated: \(deprecation)")
// }
// }
//
// public func print(warning: PrintableString) {
// if Environment.shared.shouldOutputBeColoured {
// printStandardOutputLine("Warning: \(warning)".yellow().bold())
// } else {
// printStandardOutputLine("Warning: \(warning)")
// }
// }
//
// public func print(errorMessage: PrintableString) {
// if Environment.shared.shouldOutputBeColoured {
// printStandardErrorLine("Error: \(errorMessage)".red().bold())
// } else {
// printStandardErrorLine("Error: \(errorMessage)")
// }
// }
//
// public func print(section: PrintableString) {
// if Environment.shared.shouldOutputBeColoured {
// printStandardOutputLine(section.description.cyan().bold())
// } else {
// printStandardOutputLine(section.description)
// }
// }
//
// public func print(subsection: PrintableString) {
// if Environment.shared.shouldOutputBeColoured {
// printStandardOutputLine(subsection.description.cyan())
// } else {
// printStandardOutputLine(subsection.description)
// }
// }
//
// // MARK: - Fileprivate
//
// fileprivate func printStandardOutputLine(_ string: String) {
// if let data = string.data(using: .utf8) {
// FileHandle.standardOutput.write(data)
// }
// FileHandle.standardOutput.write("\n".data(using: .utf8)!)
// }
//
// fileprivate func printStandardErrorLine(_ string: String) {
// if let data = string.data(using: .utf8) {
// FileHandle.standardError.write(data)
// }
// FileHandle.standardError.write("\n".data(using: .utf8)!)
// }
//}
import Foundation
public struct PrintableString: Encodable, Decodable, Equatable {
/// Contains a string that can be interpolated with options.
let rawString: String
let pretty: String
}
extension PrintableString: ExpressibleByStringLiteral {
public init(stringLiteral: String) {
rawString = stringLiteral
pretty = stringLiteral
}
}
extension PrintableString: CustomStringConvertible, CustomDebugStringConvertible {
public var description: String {
pretty
}
public var debugDescription: String {
rawString
}
}
extension PrintableString: ExpressibleByStringInterpolation {
public init(stringInterpolation: StringInterpolation) {
rawString = stringInterpolation.unformatted
pretty = stringInterpolation.string
}
public struct StringInterpolation: StringInterpolationProtocol {
var unformatted: String
var string: String
public init(literalCapacity _: Int, interpolationCount _: Int) {
string = ""
unformatted = ""
}
public mutating func appendLiteral(_ literal: String) {
string.append(literal)
unformatted.append(literal)
}
public mutating func appendInterpolation(_ token: PrintableString.Token) {
string.append(token.description)
unformatted.append(token.unformatted)
}
public mutating func appendInterpolation(_ value: String) {
string.append(value)
unformatted.append(value)
}
public mutating func appendInterpolation(_ value: CustomStringConvertible) {
string.append(value.description)
unformatted.append(value.description)
}
}
}
extension PrintableString {
public indirect enum Token: ExpressibleByStringLiteral {
case raw(String)
case command(Token)
case keystroke(Token)
case bold(Token)
case error(Token)
case success(Token)
case warning(Token)
case info(Token)
public init(stringLiteral: String) {
self = .raw(stringLiteral)
}
public var unformatted: String {
switch self {
case let .raw(string):
return string
case let .command(token):
return token.description
case let .keystroke(token):
return token.description
case let .bold(token):
return token.description
case let .error(token):
return token.description
case let .success(token):
return token.description
case let .warning(token):
return token.description
case let .info(token):
return token.description
}
}
public var description: String {
switch self {
case let .raw(string):
return string
case let .command(token):
return Environment.shared.shouldOutputBeColoured ? token.description.cyan() : token.description
case let .keystroke(token):
return Environment.shared.shouldOutputBeColoured ? token.description.cyan() : token.description
case let .bold(token):
return Environment.shared.shouldOutputBeColoured ? token.description.bold() : token.description
case let .error(token):
return Environment.shared.shouldOutputBeColoured ? token.description.red() : token.description
case let .success(token):
return Environment.shared.shouldOutputBeColoured ? token.description.green() : token.description
case let .warning(token):
return Environment.shared.shouldOutputBeColoured ? token.description.yellow() : token.description
case let .info(token):
return Environment.shared.shouldOutputBeColoured ? token.description.lightBlue() : token.description
}
}
}
}
extension Logger {
/// Log a message passing with the `Logger.Level.notice` log level.
///
/// `pretty` is always printed to the console, and is omitted to the logger as `notice`.
/// The standard swift-log API will recieve the
///
/// - parameters:
/// - message: The message to be logged. `message` can be used with any string interpolation literal.
/// - metadata: One-off metadata to attach to this log message
/// - file: The file this log message originates from (there's usually no need to pass it explicitly as it
/// defaults to `#file`).
/// - function: The function this log message originates from (there's usually no need to pass it explicitly as
/// it defaults to `#function`).
/// - line: The line this log message originates from (there's usually no need to pass it explicitly as it
/// defaults to `#line`).
public func pretty(
_ message: @autoclosure () -> PrintableString,
metadata: @autoclosure () -> Logger.Metadata? = nil,
file: String = #file, function: String = #function, line: UInt = #line
) {
let printableString = message()
self.log(
level: .notice, Logger.Message(stringLiteral: printableString.rawString),
metadata: metadata()?.merging(.pretty, uniquingKeysWith: { $1 }),
file: file,
function: function,
line: line
)
FileHandle.standardOutput.print(printableString.pretty)
}
}

View File

@ -37,12 +37,24 @@ The project is organized in several targets that are defined in the `Package.swi
All generic utilities live in the `TuistSupport` framework. This section documents some of the utilities and how to use them.
### Printer
### Logger
When printing output for the user, this is the utility that should be used over the global `print` method. The utility provides formatting that varies depending on the terminal the process is run from, and determines whether the standard output or error should be used based on the type of content being output.
The methods from the public interface take a `PrintableString` as an input. Printable strings support interpolating formatted strings. For instance, if we want to tell users they need to run a command, we can do the following:
If you want to print and do not want any special formatting you can use the normal `swift-log` API for printing. There is a variable in each tuist module called `logger` - any new modules should also adopt this style of logging to fit within the rest of the Tuist system.
```swift
logger.info("Please run the folling command to setup the environment: \(.command("tuist up"))")
logger.critical("") // Use `critical` for unrecoverable, system errors.
logger.error("") // Use `error` for user errors, particularly problems with their machine, manifest or configuration.
logger.warning("") // Use `warning` to highlight potential issues.
logger.notice("") // Use `notice` to log to console, this is the normal level of logging.
logger.info("") // Use `info` to provide small meta messages, this will be printed but won't be promenant.
logger.debug("") // Use `debug` to print in verbose mode.
logger.pretty("") // Use `pretty` to print a string with formatted interpolations, useful for highlighting certain elements in the string.
```
The `logger.pretty` takes a `PrintableString` as an input. Printable strings support interpolating formatted strings. For instance, if we want to tell users they need to run a command, we can do the following:
```swift
logger.pretty("Please run the folling command to setup the environment: \(.command("tuist up"))")
```