Skip to content
11 changes: 6 additions & 5 deletions Sources/CLTLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@ public extension CLTLogger {
}()

static func defaultConstantsByLogLevelForEmoji(on fh: FileHandle) -> [Logger.Level: Constants] {
return defaultConstantsByLogLevelForEmoji(on: fh, forcedEmojiSet: nil)
}

/* The forced emoji set is for the tests. */
internal static func defaultConstantsByLogLevelForEmoji(on fh: FileHandle, forcedEmojiSet: EmojiSet? = nil) -> [Logger.Level: Constants] {
func addMeta(_ paddedEmoji: String) -> Constants {
return .init(
logPrefix: paddedEmoji + " → ",
Expand All @@ -284,11 +289,7 @@ public extension CLTLogger {
}
let envVars = ProcessInfo.processInfo.environment
let outputEnvironment: OutputEnvironment = .detect(from: fh, envVars)
let emojiSet = EmojiSet.default(for: outputEnvironment)
/* To see all the emojis with the padding. If padding is correct, everything should be aligned. */
//for emoji in Emoji.allCases {
// print("\(emoji.rawValue)\(emoji.padding(for: outputEnvironment)) |")
//}
let emojiSet = forcedEmojiSet ?? EmojiSet.default(for: outputEnvironment)
return [
.trace: addMeta(emojiSet.paddedEmoji(for: .trace, in: outputEnvironment)),
.debug: addMeta(emojiSet.paddedEmoji(for: .debug, in: outputEnvironment)),
Expand Down
116 changes: 89 additions & 27 deletions Sources/Emoji.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,43 +15,100 @@ internal enum Emoji : String, CaseIterable {
case redCross = "❌"
case policeLight = "🚨"
case worm = "🐛"
case orangeDiamond = "🔶"

case ambulance = "🚑"
case ladybug = "🐞"
case monocle = "🧐"
case greenCheck = "✅"
case fearFace = "😱"

case redHeart = "❤️"
case orangeHeart = "🧡"
case yellowHeart = "💛"
case greenHeart = "💚"
case blueHeart = "💙"
case purpleHeart = "💜"
case blackHeart = "🖤"
case greyHeart = "🩶"
case brownHeart = "🤎"
case whiteHeart = "🤍"
case pinkHeart = "🩷"
case lightBlueHeart = "🩵"

case ambulance = "🚑"
case ladybug = "🐞"
case monocle = "🧐"
case greenCheck = "✅"
case fearFace = "😱"

case greySmallSquare = "◽️"
case blackSmallSquare = "◾️"
case blueDiamond = "🔷"
case orangeDiamond = "🔶"

case deepRedHeart = "♥️"
case redHeart = "❤️"
case orangeHeart = "🧡"
case yellowHeart = "💛"
case greenHeart = "💚"
case blueHeart = "💙"
case purpleHeart = "💜"
case blackHeart = "🖤"
case greyHeart = "🩶"
case brownHeart = "🤎"
case whiteHeart = "🤍"
case pinkHeart = "🩷"
case lightBlueHeart = "🩵"

case wrongWayCircle = "⛔️"
case redCircle = "🔴"
case orangeCircle = "🟠"
case yellowCircle = "🟡"
case greenCircle = "🟢"
case blueCircle = "🔵"
case purpleCircle = "🟣"
case blackCircle = "⚫️"
case brownCircle = "🟤"
case whiteCircle = "⚪️"
case redStrokeCircle = "⭕️"
case selectedRadioCircle = "🔘" /* Ugly on Windows… */

case redSquare = "🟥"
case orangeSquare = "🟧"
case yellowSquare = "🟨"
case greenSquare = "🟩"
case blueSquare = "🟦"
case purpleSquare = "🟪"
case blackSquare = "⬛️"
case brownSquare = "🟫"
case whiteSquare = "⬜️"
case blackStrokeSquare = "🔲"
case whiteStrokeSquare = "🔳"

/* ⚠️ When this is modified, fallbacks in the EmojiSet enum should be verified. */
func rendersAsText(in environment: OutputEnvironment) -> Bool {
let textEmojis: Set<Emoji>
switch environment {
case .xcode, .macOSTerminal, .macOSiTerm2, .macOSUnknown, .unknown:
/* All emojis are correct on these environments (or we don’t know and assume they are). */
return false

case .windowsTerminal, .windowsConsole, .windowsUnknown:
textEmojis = [.doubleExclamationPoint, .greySmallSquare, .blackSmallSquare, .deepRedHeart, .redStrokeCircle, .blackSquare, .whiteSquare]

case .macOSVSCode: textEmojis = [.cog, .warning, .doubleExclamationPoint, .redHeart, .deepRedHeart, .greySmallSquare, .blackSmallSquare]
case .windowsVSCode: textEmojis = [.speaker, .doubleExclamationPoint, .deepRedHeart]
case .unknownVSCode: return rendersAsText(in: .macOSVSCode) || rendersAsText(in: .windowsVSCode)
}
return textEmojis.contains(self)
}

func padding(for environment: OutputEnvironment) -> String {
guard environment != .xcode else {
/* All emojis are correct on Xcode. */
return ""
}

switch self {
case .poo, .notebook, .eyebrow, .redCross, .policeLight, .worm, .orangeDiamond,
case .poo, .notebook, .eyebrow, .redCross, .policeLight, .worm,
.orangeHeart, .yellowHeart, .greenHeart, .blueHeart, .purpleHeart,
.blackHeart, .brownHeart, .whiteHeart:
return ""

case .ambulance, .ladybug, .monocle, .greenCheck, .fearFace:
case .redCircle, .orangeCircle, .yellowCircle, .greenCircle, .blueCircle,
.purpleCircle, .brownCircle, .selectedRadioCircle:
return ""

case .redSquare, .orangeSquare, .yellowSquare, .greenSquare, .blueSquare,
.purpleSquare, .brownSquare, .blackStrokeSquare, .whiteStrokeSquare:
return ""

case .ambulance, .ladybug, .monocle, .greenCheck, .fearFace,
.blueDiamond, .orangeDiamond:
return ""

case .cog, .warning, .doubleExclamationPoint, .redHeart:
case .cog, .warning, .doubleExclamationPoint, .redHeart, .deepRedHeart:
guard !environment.isVSCode, environment != .macOSTerminal
else {return " "}
return ""
Expand All @@ -61,11 +118,13 @@ internal enum Emoji : String, CaseIterable {
else {return " "}
return ""

case .exclamationPoint:
/* Note: For the Windows Terminal and Console, we’re a negative 1 space…
# We ignore this special case and return an empty string. */
case .exclamationPoint, .greySmallSquare, .blackSmallSquare, .wrongWayCircle, .blackCircle, .whiteCircle, .redStrokeCircle, .blackSquare, .whiteSquare:
/* Note: For the Windows Terminal and Console, we need a negative 1 space!
* The output uses more space than most of the other emojis.
* We could add one space to all other emojis but there is too much space if we do this,
* so instead we ask the console to go back one char when outputting these emojis. */
guard !environment.isWindowsShell
else {return ""/*negative one space*/}
else {return Self.negativeOneSpace}
return ""

case .greyHeart, .pinkHeart, .lightBlueHeart:
Expand All @@ -79,4 +138,7 @@ internal enum Emoji : String, CaseIterable {
rawValue + padding(for: environment)
}

/* See <https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#cursor-controls>. */
private static let negativeOneSpace: String = "\u{1B}[1D"

}
129 changes: 56 additions & 73 deletions Sources/EmojiSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,110 +4,93 @@ import Logging



internal enum EmojiSet : String {
internal enum EmojiSet : String, CaseIterable {

/**
The original set of emoji used in clt-logger.
These work well in Terminal and Xcode (and on macOS generally, though not in VSCode). */
case original = "ORIGINAL"
case originalForWindowsTerminal = "ORIGINAL+WINDOWS_TERMINAL"
case originalForVSCodeMacOS = "ORIGINAL+VSCODE_MACOS"
case originalForVSCodeWindows = "ORIGINAL+VSCODE_WINDOWS"
case original = "ORIGINAL"
case swiftyBeaver = "SWIFTY_BEAVER"
case cleanroomLogger = "CLEANROOM_LOGGER"
case vaibhavsingh97EmojiLogger = "VAIBHAVSINGH97_EMOJI_LOGGER"

case vaibhavsingh97EmojiLogger = "VAIBHAVSINGH97_EMOJI_LOGGER"
case vaibhavsingh97EmojiLoggerForVSCodeMacOS = "VAIBHAVSINGH97_EMOJI_LOGGER+VSCODE_MACOS"
/** The emoji set that works on all platforms (no need for a replacement emoji because the original renders as text). */
case noAlternates = "NO_ALTERNATES"

static func `default`(for environment: OutputEnvironment, _ envVars: [String: String] = ProcessInfo.processInfo.environment) -> EmojiSet {
if let envStr = envVars["CLTLOGGER_EMOJI_SET_NAME"], let ret = EmojiSet(rawValue: envStr) {
return ret
}
switch environment {
case .xcode, .macOSTerminal, .macOSiTerm2, .macOSUnknown:
return .original

case .macOSVSCode, .unknownVSCode, .unknown:
return .originalForVSCodeMacOS

case .windowsTerminal, .windowsConsole, .windowsUnknown:
return .originalForWindowsTerminal

case .windowsVSCode:
return .originalForVSCodeWindows
}
return .original
}

/* Exceptions:
* - ⚙️ on VSCode macOS renders as text
* - ⚠️ on VSCode macOS renders as text
* - ‼️ on VSCode macOS renders as text
* - ❤️ on VSCode macOS renders as text
* - 🗣 on VSCode Windows renders as text (I think)
* - ‼️ on VSCode Windows renders as text
* - ❗️ on Windows Terminal is larger than the rest (negative padding would be needed)
* - ‼️ on Windows Terminal renders as text */
func emoji(for logLevel: Logger.Level) -> Emoji {
let original: (Logger.Level) -> Emoji = {
switch $0 {
case .critical: return .doubleExclamationPoint
case .error: return .exclamationPoint
case .warning: return .warning
case .notice: return .speaker
case .info: return .notebook
case .debug: return .cog
case .trace: return .poo
}
}
let vaibhavsingh97: (Logger.Level) -> Emoji = {
switch $0 {
case .critical: return .ambulance
case .error: return .fearFace
case .warning: return .warning
case .notice: return .greenCheck /* Called success in upstream. */
case .info: return .monocle
case .debug: return .ladybug
case .trace: return .poo /* Does not exist in upstream. */
}
}

func emoji(for logLevel: Logger.Level, in environment: OutputEnvironment) -> Emoji {
let ret: Emoji
switch self {
case .original:
return original(logLevel)

case .originalForWindowsTerminal:
switch logLevel {
case .critical: return .policeLight
case .error: return .redCross
default: return original(logLevel)
case .critical: ret = .doubleExclamationPoint
case .error: ret = .exclamationPoint
case .warning: ret = .warning
case .notice: ret = .speaker
case .info: ret = .notebook
case .debug: ret = .cog
case .trace: ret = .poo
}

case .originalForVSCodeMacOS:
case .swiftyBeaver:
switch logLevel {
case .critical: return .policeLight
case .warning: return .orangeDiamond
case .debug: return .worm
default: return original(logLevel)
case .critical: ret = .redSquare
case .error: ret = .redSquare
case .warning: ret = .yellowSquare
case .notice: ret = .blueSquare /* Log level does not exist in upstream. */
case .info: ret = .blueSquare
case .debug: ret = .greenSquare
case .trace: ret = .whiteSquare
}

case .originalForVSCodeWindows:
case .cleanroomLogger:
switch logLevel {
case .critical: return .policeLight
case .notice: return .eyebrow
default: return original(logLevel)
case .critical: ret = .redCross /* Log level does not exist in upstream. */
case .error: ret = .redCross
case .warning: ret = .orangeDiamond
case .notice: ret = .blueDiamond /* Log level does not exist in upstream. */
case .info: ret = .blueDiamond
case .debug: ret = .blackSmallSquare
case .trace: ret = .greySmallSquare
}

case .vaibhavsingh97EmojiLogger:
return vaibhavsingh97(logLevel)
switch logLevel {
case .critical: ret = .ambulance
case .error: ret = .fearFace
case .warning: ret = .warning
case .notice: ret = .greenCheck /* Called success in upstream. */
case .info: ret = .monocle
case .debug: ret = .ladybug
case .trace: ret = .ladybug /* Log level does not exist in upstream. */
}

case .vaibhavsingh97EmojiLoggerForVSCodeMacOS:
case .noAlternates:
switch logLevel {
case .warning: return .orangeDiamond
default: return vaibhavsingh97(logLevel)
case .critical: return .redCross
case .error: return .redCircle
case .warning: return .orangeCircle
case .notice: return .yellowCircle
case .info: return .greenCircle
case .debug: return .purpleCircle
case .trace: return .whiteCircle
}
}
guard ret.rendersAsText(in: environment) else {
return ret
}
/* The no alternates emoji set will not check if its returned emojis render as text so there will never be an infinite loop here. */
return EmojiSet.noAlternates.emoji(for: logLevel, in: environment)
}

func paddedEmoji(for logLevel: Logger.Level, in environment: OutputEnvironment) -> String {
return emoji(for: logLevel).valueWithPadding(for: environment)
return emoji(for: logLevel, in: environment).valueWithPadding(for: environment)
}

}
2 changes: 1 addition & 1 deletion Sources/OutputEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation



internal enum OutputEnvironment : String {
internal enum OutputEnvironment : String, CaseIterable {

case xcode = "XCODE"

Expand Down
27 changes: 24 additions & 3 deletions Tests/CLTLoggerTests/CLTLoggerTests.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import Foundation
import XCTest

import Logging

@testable import CLTLogger
@testable import Logging



final class CLTLoggerTests : XCTestCase {

static let defaultBootstrapFactory: @Sendable (String) -> any LogHandler = { _ in CLTLogger(multilineMode: .allMultiline) }

override class func setUp() {
LoggingSystem.bootstrap{ _ in CLTLogger(multilineMode: .allMultiline) }
LoggingSystem.bootstrap(Self.defaultBootstrapFactory)
}

/* From <https://apple.github.io/swift-log/docs/current/Logging/Protocols/LogHandler.html#treat-log-level-amp-metadata-as-values>. */
Expand Down Expand Up @@ -90,4 +91,24 @@ final class CLTLoggerTests : XCTestCase {
logger.critical("YAM!\nhere is the second line\nand why not a third one", metadata: ["with": ["metadata", "again"], "because": "42"])
}

func testBasicLogOutputWithAllEmojiSets() throws {
XCTAssertTrue(true, "We only want to see how the log look, so please see the logs.")

for emojiSet in EmojiSet.allCases {
LoggingSystem.bootstrapInternal{ _ in CLTLogger(multilineMode: .allMultiline, constantsByLevel: CLTLogger.defaultConstantsByLogLevelForEmoji(on: .standardError, forcedEmojiSet: emojiSet)) }
try FileHandle.standardError.write(contentsOf: Data("\n***** \(emojiSet.rawValue) *****\n".utf8))
var logger = Logger(label: "my logger")
logger.logLevel = .trace
logger.critical("critical: Example of text at this level.")
logger.error( "error: Example of text at this level.")
logger.warning( "warning: Example of text at this level.")
logger.notice( "notice: Example of text at this level.")
logger.info( "info: Example of text at this level.")
logger.debug( "debug: Example of text at this level.")
logger.trace( "trace: Example of text at this level.")
}
/* Reset factory. */
LoggingSystem.bootstrapInternal(Self.defaultBootstrapFactory)
}

}
Loading