diff --git a/Sources/CLTLogger.swift b/Sources/CLTLogger.swift index 177e9b7..e04bc4b 100644 --- a/Sources/CLTLogger.swift +++ b/Sources/CLTLogger.swift @@ -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 + " β†’ ", @@ -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)), diff --git a/Sources/Emoji.swift b/Sources/Emoji.swift index 8a42f15..0ff9521 100644 --- a/Sources/Emoji.swift +++ b/Sources/Emoji.swift @@ -15,27 +15,75 @@ 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 + 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. */ @@ -43,15 +91,24 @@ internal enum Emoji : String, CaseIterable { } 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 "" @@ -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: @@ -79,4 +138,7 @@ internal enum Emoji : String, CaseIterable { rawValue + padding(for: environment) } + /* See . */ + private static let negativeOneSpace: String = "\u{1B}[1D" + } diff --git a/Sources/EmojiSet.swift b/Sources/EmojiSet.swift index c56ec5c..b243f73 100644 --- a/Sources/EmojiSet.swift +++ b/Sources/EmojiSet.swift @@ -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) } } diff --git a/Sources/OutputEnvironment.swift b/Sources/OutputEnvironment.swift index e2291f3..f0562d2 100644 --- a/Sources/OutputEnvironment.swift +++ b/Sources/OutputEnvironment.swift @@ -2,7 +2,7 @@ import Foundation -internal enum OutputEnvironment : String { +internal enum OutputEnvironment : String, CaseIterable { case xcode = "XCODE" diff --git a/Tests/CLTLoggerTests/CLTLoggerTests.swift b/Tests/CLTLoggerTests/CLTLoggerTests.swift index 1e0991b..63144d9 100644 --- a/Tests/CLTLoggerTests/CLTLoggerTests.swift +++ b/Tests/CLTLoggerTests/CLTLoggerTests.swift @@ -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 . */ @@ -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) + } + } diff --git a/Tests/CLTLoggerTests/EmojiTests.swift b/Tests/CLTLoggerTests/EmojiTests.swift new file mode 100644 index 0000000..9324750 --- /dev/null +++ b/Tests/CLTLoggerTests/EmojiTests.swift @@ -0,0 +1,34 @@ +import Foundation +import XCTest + +import Logging + +@testable import CLTLogger + + + +final class EmojiTests : XCTestCase { + + func testNoAlternateEmojiSetHasNoAlternates() { + for env in OutputEnvironment.allCases { + for logLevel in Logger.Level.allCases { + let emoji = EmojiSet.noAlternates.emoji(for: logLevel, in: env) + let rendersAsText = emoji.rendersAsText(in: env) + XCTAssertFalse(rendersAsText) + if rendersAsText { + print("Found \(emoji.rawValue) which renders as text in \(env.rawValue).") + } + } + } + } + + func testEmojiAlignmentAndTextRenderingVisually() throws { + let envVars = ProcessInfo.processInfo.environment + let outputEnvironment: OutputEnvironment = .detect(from: .standardError, envVars) + for emoji in Emoji.allCases { + let lineStr = "\(emoji.rendersAsText(in: outputEnvironment) ? "πŸ”΄" : "🟒") - \(emoji.rawValue)\(emoji.padding(for: outputEnvironment)) |" + try FileHandle.standardError.write(contentsOf: Data((lineStr + "\n").utf8)) + } + } + +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index e367af8..f84f4f6 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -8,6 +8,11 @@ var tests: [XCTestCaseEntry] = [ ("testVisual1", CLTLoggerTests.testVisual1), ("testVisual2", CLTLoggerTests.testVisual2), ("testVisual3", CLTLoggerTests.testVisual3), + ("testBasicLogOutputWithAllEmojiSets", CLTLoggerTests.testBasicLogOutputWithAllEmojiSets), + ]), + testCase([ + ("testNoAlternateEmojiSetHasNoAlternates", EmojiTests.testNoAlternateEmojiSetHasNoAlternates), + ("testEmojiAlignmentAndTextRenderingVisually", EmojiTests.testEmojiAlignmentAndTextRenderingVisually), ]), testCase([ ("testSGRParseFail", SGRTests.testSGRParseFail),